Skip to content

Conversation

flyingrobots
Copy link
Owner

@flyingrobots flyingrobots commented Sep 20, 2025

Summary

  • add cmd/release CLI with auto bump, signing, push controls, and optional version file updates
  • integrate release notes/changelog regeneration, dockerized dry-run/real-run tests, and quality CI checks
  • expand docs generator with user-defined palettes + schema, shared tooling configs, and contributor guidance

Testing

  • go test ./cmd/... ./internal/docscomponents ./internal/release
  • make docs
  • ./scripts/test-release-docker.sh

@flyingrobots
Copy link
Owner Author

@coderabbitai review

Copy link

coderabbitai bot commented Sep 20, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Sep 20, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a docs components generator and CLI to auto-build roadmap, tasks, archives, graphs, release notes, and changelog.
    • Added a release CLI with dry-run and preflight checks.
    • Provided Makefile automation, Git hooks installer, and a pre-commit quality gate.
  • Documentation

    • Overhauled README/CONTRIBUTING; added PRD, Tech Spec, reference guides, templates, RFC, and generated components.
  • Tests

    • Added end-to-end generator tests and a Docker-based release test script.
  • Chores

    • Added EditorConfig and lint configuration.
    • Set up CI workflows for formatting, linting, testing, and docs validation.
    • Added a Dockerfile for release testing.

Walkthrough

Adds repo tooling, CI, docs-generation pipeline (generator, CLI, transclusion), release automation, git hooks and helper scripts, planning data and generated documentation, TUI/mock data, tests, Makefile targets, and module dependency updates.

Changes

Cohort / File(s) Summary
Editor / Lint config
/.editorconfig, /.golangci.yml
Adds EditorConfig and GolangCI-Lint configuration (format rules, linters, timeout, local-prefixes).
Git hooks & install script
/.githooks/pre-commit, scripts/install-git-hooks.sh
Pre-commit script enforcing fmt/lint/test/docs; installer to set Git core.hooksPath to .githooks.
CI workflows
/.github/workflows/docs.yml, /.github/workflows/quality.yml
Adds Docs Pipeline and Quality Checks workflows (Go/Node setup, fmt, lint, tests, docs generation/verification).
Make / Release infra
/Makefile, Dockerfile.release-test, scripts/test-release-docker.sh
New Make targets for fmt/lint/test/docs/release; Dockerfile and script to validate release flow in container.
Docs generation CLI & internals
/cmd/docs-components/main.go, /internal/docscomponents/generator.go, /internal/docscomponents/transclusion.go, /internal/docscomponents/generator_test.go
New docs-components CLI, Generator type/API, transclusion runner, palette/graph support, and end-to-end tests.
Release CLI & service
/cmd/release/main.go, /internal/release/releaser.go
New release command and Releaser service with checks, dry-run, annotated tagging, and helper methods.
Scripts: render & verify
/scripts/render-docs.sh, /scripts/verify-docs.sh
Script to run docs-components with env-driven flags; verification script to detect missing outputs/placeholders.
Docs / reference / templates
/docs/**, /docs/reference/**, /CHANGELOG.template.md, /docs/reference/release-notes.template.md
Large set of new and updated documentation, templates, RFCs, palettes/schema, release notes, PRD and TechSpec expansions.
Generated docs components
/docs/components/issues/*, /docs/components/roadmap/*, /CHANGELOG.md, /docs/reference/release-notes.md
Adds generated tables, changelog, archived items, roadmap tables, dependency graphs and progress summaries.
Planning data & templates
/@hubless/README.md, @hubless/issues/*, @hubless/roadmap/*
Adds/updates JSON records (milestones, features, stories, tasks), templates, and related READMEs; deletes old roadmap README.
Project docs & contributing
/README.md, /CONTRIBUTING.md, /AGENTS.md, /docs/PRD.md, /docs/TechSpec.md, /docs/reference/update-progress-algorithm.md
Rewrites and extends main docs, contributing guide, PRD, tech spec, and algorithm docs.
UI mock & mock data
/internal/ui/tui/mock/*, /internal/mock/data.go
Adds mock TUI model, layout profiles, styles, and exported mock data types/functions.
Module deps
/go.mod
Adds Bubble Tea and related dependencies for TUI and other indirect packages.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Developer
  participant Hook as pre-commit
  participant Make as Makefile
  participant CLI as docs-components
  participant Gen as Generator
  participant Trans as markdown-transclusion

  Developer->>Hook: git commit
  Hook->>Make: make fmt-check / make lint / make test / make docs
  Make->>CLI: go run ./cmd/docs-components --repo ...
  CLI->>Gen: Generate(ctx, options)
  Gen-->>CLI: write components (md, graphs)
  CLI->>Trans: RunTransclusion per document
  Trans-->>CLI: rendered outputs
  CLI-->>Make: success
  Make-->>Hook: success -> commit proceeds
Loading
sequenceDiagram
  autonumber
  actor ReleaseEng
  participant RelCLI as hubless release
  participant Releaser
  participant Git

  ReleaseEng->>RelCLI: hubless release --version vX.Y.Z [--dry-run]
  RelCLI->>Releaser: Run(ctx, Options)
  alt runChecks (if not skipped)
    Releaser->>Releaser: runChecks (fmt, lint, test, docs)
  end
  Releaser->>Releaser: ensureClean()
  Releaser->>Releaser: ensureTagDoesNotExist(vX.Y.Z)
  alt DryRun
    Releaser-->>RelCLI: preview tag + notes
  else create tag
    Releaser->>Git: git tag -a vX.Y.Z -F notes.tmp
    Git-->>Releaser: tag created
    Releaser-->>RelCLI: success
  end
  RelCLI-->>ReleaseEng: next steps (push / release)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

You grafted hooks and made CI scream,
Docs stitch components into a dream.
Tags are tidy, tests march in line—
Ship with vigor, but read each spine. 🚀

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Release automation + palette customization" is a short, single-sentence summary that directly reflects the two primary changes in the changeset (the release automation CLI and the docs/components palette support), is concise and specific, and contains no noisy or misleading terms. It gives a teammate scanning history a clear sense of the main scope without listing files or extraneous detail.
Description Check ✅ Passed The pull request description clearly summarizes the major additions (cmd/release CLI, docs/changelog regeneration, CI/quality checks, and palette/schema support) and provides concrete testing commands, making it directly related to the changeset and adequate for this lenient check. The level of detail is sufficient to understand scope and how to exercise the changes.
Docstring Coverage ✅ Passed Docstring coverage is 84.21% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch docs-release-pipeline

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 84

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
docs/TechSpec.md (1)

38-49: Add languages to all fenced blocks and fix MD040 across the doc.

Use text, bash, mermaid, ini, etc., as appropriate. This doc trips multiple MD040s.

docs/reference/update-progress-algorithm.md (1)

15-21: Inconsistent env var name (GITMIND_ROOT vs HUBLESS_ROOT)

Pick one. Section 3 switched to HUBLESS_ROOT; Inputs still list GITMIND_ROOT. Fix the Inputs section to avoid lying to readers.

-  - Optional `--root` CLI flag or `GITMIND_ROOT` environment variable to locate the GitMind checkout.
+  - Optional `--root` CLI flag or `HUBLESS_ROOT` environment variable to locate the repository root.

Also applies to: 23-28

docs/reference/implementation-skeleton.md (5)

60-84: events.go snippet is missing imports; it won’t compile. Add time import.

You use time.Time without importing time.

Apply this diff inside the code block:

-package domain
+package domain
+
+import "time"

88-135: issue.go snippet references undefined getString and misses time import. Add both.

Replay uses getString but you never defined it; Issue.LastUpdated uses time.Time but there’s no import.

Apply this diff inside the code block:

-package domain
+package domain
+
+import "time"
@@
 func Replay(id IssueID, events []Event) Issue {
@@
 }
+
+// getString returns payload[key] if it is a non-empty string, otherwise def.
+func getString(m map[string]any, key string, def string) string {
+    if m == nil {
+        return def
+    }
+    if v, ok := m[key]; ok {
+        if s, ok := v.(string); ok && s != "" {
+            return s
+        }
+    }
+    return def
+}
+
+// PriorityOrder provides a stable sort order for priorities.
+func PriorityOrder(p Priority) int {
+    switch p {
+    case PriorityHigh:
+        return 0
+    case PriorityMedium:
+        return 1
+    case PriorityLow:
+        return 2
+    default:
+        return 3
+    }
+}

140-188: application/services.go snippet: missing imports for context/sort and missing package imports.

You reference domain, ports, context, sort but import none. Add proper imports.

Apply this diff inside the code block:

-package application
+package application
+
+import (
+    "context"
+    "sort"
+
+    "github.com/flyingrobots/hubless/internal/domain"
+    "github.com/flyingrobots/hubless/internal/ports"
+)

193-201: ports/repository.go snippet: missing imports for context/time and domain.

Interface references context.Context, time.Time, and domain types; import them.

Apply this diff inside the code block:

-package ports
+package ports
+
+import (
+    "context"
+    "time"
+
+    "github.com/flyingrobots/hubless/internal/domain"
+)

230-262: Add missing imports and stop swallowing svc.List errors — fix docs code blocks too.

  • docs/reference/implementation-skeleton.md (lines 230–262): the snippet is missing imports (context, tea, list, viewport, application) and drops the error from svc.List. Apply this diff:
-package tui
+package tui
+
+import (
+    "context"
+
+    tea "github.com/charmbracelet/bubbletea"
+    "github.com/charmbracelet/bubbles/list"
+    "github.com/charmbracelet/bubbles/viewport"
+
+    "github.com/flyingrobots/hubless/internal/application"
+)
@@
 func New(ctx context.Context, svc *application.Service, width, height int) Model {
-    issues, _ := svc.List(ctx)
+    issues, err := svc.List(ctx)
+    if err != nil {
+        // In a real app, handle/log; for a skeleton, degrade gracefully.
+        issues = nil
+    }
  • Additional issue: other code blocks in docs/reference/implementation-skeleton.md reference time.Time without importing time — see lines 77–81, 106–110, 197–200. Add import "time" (or an import block) to those examples.

Fix these now; don’t ship broken/compilable-unfriendly samples.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af275e1 and 737b80e.

⛔ Files ignored due to path filters (4)
  • @hubless/issues/generated/archive.md is excluded by !**/generated/**
  • @hubless/issues/generated/tasks.md is excluded by !**/generated/**
  • @hubless/roadmap/generated/README.md is excluded by !**/generated/**
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (60)
  • .editorconfig (1 hunks)
  • .githooks/pre-commit (1 hunks)
  • .github/workflows/docs.yml (1 hunks)
  • .github/workflows/quality.yml (1 hunks)
  • .golangci.yml (1 hunks)
  • @hubless/README.md (1 hunks)
  • @hubless/issues/stories/hubless-story-0007.json (1 hunks)
  • @hubless/issues/tasks.archive.md (0 hunks)
  • @hubless/issues/tasks/hubless-m0-task-0005.json (1 hunks)
  • @hubless/issues/templates/archive.md (1 hunks)
  • @hubless/issues/templates/tasks.md (1 hunks)
  • @hubless/roadmap/README.md (0 hunks)
  • @hubless/roadmap/features/hubless-feature-docs-components.json (1 hunks)
  • @hubless/roadmap/milestones/hubless-milestone-m0-foundations.json (2 hunks)
  • @hubless/roadmap/templates/README.md (1 hunks)
  • AGENTS.md (1 hunks)
  • CHANGELOG.md (1 hunks)
  • CHANGELOG.template.md (1 hunks)
  • CONTRIBUTING.md (2 hunks)
  • Dockerfile.release-test (1 hunks)
  • Makefile (1 hunks)
  • README.md (1 hunks)
  • cmd/docs-components/main.go (1 hunks)
  • cmd/release/main.go (1 hunks)
  • docs/PRD.md (6 hunks)
  • docs/TechSpec.md (10 hunks)
  • docs/components/issues/archived-stories.md (1 hunks)
  • docs/components/issues/archived-tasks.md (1 hunks)
  • docs/components/issues/changelog.md (1 hunks)
  • docs/components/issues/status-summary.md (1 hunks)
  • docs/components/issues/tasks-table.md (1 hunks)
  • docs/components/roadmap/dependencies-graph.md (1 hunks)
  • docs/components/roadmap/dependencies.md (1 hunks)
  • docs/components/roadmap/features-table.md (1 hunks)
  • docs/components/roadmap/milestones-table.md (1 hunks)
  • docs/components/roadmap/progress.md (1 hunks)
  • docs/components/roadmap/stories-table.md (1 hunks)
  • docs/reference/archive-structure.md (1 hunks)
  • docs/reference/docs-components-plan.md (1 hunks)
  • docs/reference/implementation-skeleton.md (9 hunks)
  • docs/reference/palettes.json (1 hunks)
  • docs/reference/palettes.schema.json (1 hunks)
  • docs/reference/release-notes.md (1 hunks)
  • docs/reference/release-notes.template.md (1 hunks)
  • docs/reference/rfcs/0001-release-automation.md (1 hunks)
  • docs/reference/rfcs/FEEDBACK-0001-release-automation.md (1 hunks)
  • docs/reference/update-progress-algorithm.md (5 hunks)
  • go.mod (1 hunks)
  • internal/docscomponents/generator.go (1 hunks)
  • internal/docscomponents/generator_test.go (1 hunks)
  • internal/docscomponents/transclusion.go (1 hunks)
  • internal/mock/data.go (1 hunks)
  • internal/release/releaser.go (1 hunks)
  • internal/ui/tui/mock/app.go (1 hunks)
  • internal/ui/tui/mock/profile.go (1 hunks)
  • internal/ui/tui/mock/styles.go (1 hunks)
  • scripts/install-git-hooks.sh (1 hunks)
  • scripts/render-docs.sh (1 hunks)
  • scripts/test-release-docker.sh (1 hunks)
  • scripts/verify-docs.sh (1 hunks)
💤 Files with no reviewable changes (2)
  • @hubless/issues/tasks.archive.md
  • @hubless/roadmap/README.md
🧰 Additional context used
🧬 Code graph analysis (6)
internal/docscomponents/transclusion.go (1)
internal/release/releaser.go (1)
  • New (28-39)
internal/ui/tui/mock/app.go (1)
internal/ui/tui/mock/styles.go (1)
  • Styles (6-9)
internal/docscomponents/generator.go (1)
internal/release/releaser.go (1)
  • New (28-39)
cmd/docs-components/main.go (2)
internal/docscomponents/generator.go (2)
  • NewGenerator (34-71)
  • GeneratorOptions (19-24)
internal/docscomponents/transclusion.go (2)
  • TransclusionOptions (14-20)
  • RunTransclusion (23-76)
cmd/release/main.go (1)
internal/release/releaser.go (3)
  • New (28-39)
  • Options (17-22)
  • ErrVersionRequired (14-14)
internal/docscomponents/generator_test.go (1)
internal/docscomponents/generator.go (2)
  • NewGenerator (34-71)
  • GeneratorOptions (19-24)
🪛 markdownlint-cli2 (0.18.1)
docs/components/issues/archived-tasks.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/components/roadmap/stories-table.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/components/roadmap/milestones-table.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/components/roadmap/dependencies-graph.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/components/issues/archived-stories.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

CHANGELOG.md

25-25: Files should end with a single newline character

(MD047, single-trailing-newline)

docs/reference/release-notes.md

15-15: Files should end with a single newline character

(MD047, single-trailing-newline)

docs/reference/docs-components-plan.md

3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


4-4: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


11-11: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


15-15: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


39-39: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


40-40: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


42-42: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


47-47: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


49-49: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


53-53: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


63-63: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


68-68: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


69-69: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


74-74: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


75-75: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


80-80: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


81-81: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

docs/reference/update-progress-algorithm.md

46-46: Spaces inside code span elements

(MD038, no-space-in-code)


86-86: Spaces inside code span elements

(MD038, no-space-in-code)

docs/components/roadmap/features-table.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/components/roadmap/progress.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/reference/rfcs/FEEDBACK-0001-release-automation.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/reference/rfcs/0001-release-automation.md

3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


6-6: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


7-7: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


15-15: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


21-21: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


21-21: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


109-109: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


110-110: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


115-115: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


116-116: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


120-120: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


121-121: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

docs/components/issues/status-summary.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/components/issues/tasks-table.md

2-2: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

docs/TechSpec.md

71-71: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


78-78: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


99-99: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


163-163: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


177-177: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


189-189: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

README.md

4-4: Blank line inside blockquote

(MD028, no-blanks-blockquote)


13-13: Emphasis style
Expected: asterisk; Actual: underscore

(MD049, emphasis-style)


13-13: Emphasis style
Expected: asterisk; Actual: underscore

(MD049, emphasis-style)


38-38: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


39-39: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


43-43: Trailing spaces
Expected: 0 or 2; Actual: 4

(MD009, no-trailing-spaces)


45-45: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🪛 YAMLlint (1.37.1)
.github/workflows/docs.yml

[warning] 3-3: truthy value should be one of [false, true]

(truthy)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 5-5: too many spaces inside brackets

(brackets)

.github/workflows/quality.yml

[warning] 3-3: truthy value should be one of [false, true]

(truthy)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 5-5: too many spaces inside brackets

(brackets)

🪛 Checkov (3.2.334)
Dockerfile.release-test

[low] 1-25: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-25: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Shellcheck (0.11.0)
scripts/verify-docs.sh

[info] 32-32: Expansions inside ${..} need to be quoted separately, otherwise they match as patterns.

(SC2295)

🪛 checkmake (0.2.2)
Makefile

[warning] 1-1: Missing required phony target "all"

(minphony)


[warning] 1-1: Missing required phony target "clean"

(minphony)

🪛 actionlint (1.7.7)
.github/workflows/quality.yml

30-30: the runner of "golangci/golangci-lint-action@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🔇 Additional comments (32)
.editorconfig (3)

4-10: Global space indentation will bleed into Go if the override isn’t fixed.

This becomes a CI pain factory once gofmt/goimports run. After applying the Go override fix above, this is fine; otherwise you’ll get endless churn.

Run: gofmt -l . and ensure zero files reported.


16-18: Makefile override is correct.

Tabs for Makefiles are required. Good.


19-20: Docs trailing whitespace exemption is sensible.

Markdown often needs two-space breaks. Good call.

CHANGELOG.template.md (2)

5-8: Template/transclusion looks sane.

The reference to docs/reference/release-notes.md aligns with the generator flow.


11-11: Path consistency check for archives.

Ensure @hubless/issues/generated/archive.md actually resolves at build time (your other docs use docs/components/...). If the generator emits under that alias, all good; if not, align the path.

go.mod (3)

5-8: Direct deps look fine; make sure they match actual imports.

Run go mod tidy after fixing the go directive so indirects settle correctly.


10-28: Sanity-check indirects and compat.

After correcting the go directive, run:

  • go mod tidy
  • go build ./...

This will flush any stale indirects and ensure x/sys/x/text versions are consistent with bubbletea.


3-3: Invalid go directive: patch versions aren’t allowed.

go.mod’s go line must be a major.minor (e.g., go 1.25), not 1.25.1. If you need to pin a patch, use the toolchain directive.

-go 1.25.1
+go 1.25
+toolchain go1.25.1

Likely an incorrect or invalid review comment.

docs/components/roadmap/dependencies.md (3)

27-33: Stories link pathing looks consistent.

Uses ../issues/stories/... as expected.


6-10: Milestones/Features links are plausible if those JSONs are collocated.

If those JSONs actually live under @hubless/roadmap/... instead, consider aligning link roots for consistency.


38-42: Fix broken task links — use ../issues/tasks/... (docs/components/roadmap/dependencies.md:38-42)

Links point to tasks/... relative to this file and will 404; change to ../issues/tasks/... or mirror the JSONs under docs/components/roadmap/tasks/. Automated link-check failed in the sandbox (script error) — confirm file locations or run the link-check locally.

-| [hubless/m0/task/0004](tasks/hubless-m0-task-0004.json) | hubless/m0/task/0001 |
-| [hubless/m0/task/0005](tasks/hubless-m0-task-0005.json) | hubless/m0/task/0004 |
-| [hubless/m1/task/0002](tasks/hubless-m1-task-0002.json) | hubless/m0/task/0001 |
-| [hubless/m1/task/0003](tasks/hubless-m1-task-0003.json) | hubless/m1/task/0002 |
+| [hubless/m0/task/0004](../issues/tasks/hubless-m0-task-0004.json) | hubless/m0/task/0001 |
+| [hubless/m0/task/0005](../issues/tasks/hubless-m0-task-0005.json) | hubless/m0/task/0004 |
+| [hubless/m1/task/0002](../issues/tasks/hubless-m1-task-0002.json) | hubless/m0/task/0001 |
+| [hubless/m1/task/0003](../issues/tasks/hubless-m1-task-0003.json) | hubless/m1/task/0002 |
Dockerfile.release-test (1)

1-1: Base image pinning and version reality check.

golang:1.25 tag: confirm it exists in your registry at CI time. Consider digest pinning for reproducibility:

-FROM golang:1.25
+# e.g., pin to a digest (example digest; update to the real one)
+FROM golang:1.25@sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
scripts/test-release-docker.sh (1)

1-3: Good: strict mode enabled.

set -euo pipefail is the right default.

docs/reference/archive-structure.md (1)

3-14: Docs flow is clear and consistent with generator model.

Solid overview; matches the JSON-source-first approach.

@hubless/roadmap/templates/README.md (1)

7-37: Template is coherent; transclusion targets align with generated components.

Looks good. Keep schema paths and filenames in lockstep with the generator.

AGENTS.md (1)

18-18: Ensure make hooks exists and is idempotent.

Confirm the Makefile target installs .githooks via scripts/install-git-hooks.sh and is safe to re-run.

docs/PRD.md (1)

95-102: Table looks good; keep the scope/phasing crisp.

Release phases are clear and incremental.

docs/reference/release-notes.template.md (1)

1-11: Template looks sane.

Header present, transclusions are clear. No blockers.

docs/components/roadmap/milestones-table.md (1)

4-9: Verify milestone JSON links resolve — manual check required.
Automated verification failed in the sandbox (process-substitution / /dev/fd errors). Do not merge until every milestones/*.json link in docs/components/roadmap/milestones-table.md exists under docs/components/roadmap/milestones/. Run locally:
for rel in $(perl -nle 'while (/]((milestones/[^)]+.json))/g){print $1}' docs/components/roadmap/milestones-table.md); do p=docs/components/roadmap/$rel; [ -f "$p" ] && echo OK:$p || echo MISSING:$p; done

docs/components/roadmap/stories-table.md (1)

4-10: Fix and validate all relative links now — DO NOT ship broken links.

docs/components/roadmap/stories-table.md (lines 4–10) point to ../issues/stories/*.json; repo-wide verifier errored with "re.error: unknown extension ?! at position 13", so verification is inconclusive — confirm those JSON targets exist or rerun the verifier after fixing the regex.

@hubless/issues/templates/archive.md (1)

7-11: FIX TRANSCLUSION PATHS — use ../../../docs/components/issues/... (NOT ../../)

The two-level "../../docs/..." is incorrect (it resolves to @hubless/docs/..., which doesn't exist). The target files live at repo-root "docs/components/issues/..."; use a three-level relative path.

File: @hubless/issues/templates/archive.md
Lines: 7-11

-![[docs/components/issues/archived-stories.md]]
+![[../../../docs/components/issues/archived-stories.md]]

-![[docs/components/issues/archived-tasks.md]]
+![[../../../docs/components/issues/archived-tasks.md]]

Verify resolution:

#!/usr/bin/env bash
set -euo pipefail
for target in "docs/components/issues/archived-stories.md" "docs/components/issues/archived-tasks.md"; do
  src="@hubless/issues/templates/archive.md"
  rel="../../../${target}"
  python - "$src" "$rel" "$target" <<'PY'
import os,sys
src, rel, repo_rel = sys.argv[1], sys.argv[2], sys.argv[3]
p1 = os.path.normpath(os.path.join(os.path.dirname(src), rel))
p2 = os.path.normpath(os.path.join(os.getcwd(), repo_rel))
print(f"{repo_rel}\n  corrected-relative-> {p1}  exists={os.path.exists(p1)}\n  repo-root-> {p2}  exists={os.path.exists(p2)}")
PY
done

Likely an incorrect or invalid review comment.

docs/reference/palettes.json (1)

3-8: Wrong — this is already wired: generator honors --palette-file / --graph-palette and maps palette text → Mermaid color.

  • Flags: cmd/docs-components/main.go defines --graph-palette and --palette-file.
  • Implementation: internal/docscomponents/generator.go loads/merges palettes (defaultPalettes + mergePaletteFile) and emits classDef using Fill/Stroke/Text: classDef %s fill:%s,stroke:%s,color:%s,stroke-width:1px;.
  • Tests: internal/docscomponents/generator_test.go verifies custom palette values are present in the generated graph.

If you want configurable stroke width: add strokeWidth to the palette schema and raw/paletteColor structs, parse it in mergePaletteFile, and use it in the classDef output; update tests accordingly.

Likely an incorrect or invalid review comment.

@hubless/issues/stories/hubless-story-0007.json (1)

1-22: LGTM.

Schema looks consistent with other story docs. No blocking issues.

@hubless/issues/tasks/hubless-m0-task-0005.json (1)

1-88: LGTM — schema looks consistent and complete.

Status, badges, and timestamps line up. No nits.

@hubless/roadmap/milestones/hubless-milestone-m0-foundations.json (1)

6-9: LGTM — milestone wiring is coherent.

Timeframe, features, and tasks list are sane.

Also applies to: 20-22, 24-27

internal/docscomponents/generator_test.go (1)

14-38: Solid end-to-end coverage

Good fixture setup and validation across generated components. This will catch regressions in options normalization and file I/O paths.

Also applies to: 71-113

internal/mock/data.go (1)

7-18: Clean, useful mocks

Good, focused mock data with realistic timestamps and simple types. This will make UI demos sane.

If you want to avoid typos drifting into UIs, consider promoting Status and Priority to typed string enums with helper constructors in a follow-up.

Also applies to: 35-57, 59-174

@hubless/roadmap/features/hubless-feature-docs-components.json (1)

1-23: Cross-reference integrity — verify the referenced milestone/story/feature/task IDs exist (auto-check failed)

rg in the sandbox skipped files; I could not confirm the artifacts. Do not merge with dangling refs — ensure these IDs resolve or add/remove the references:

  • hubless/milestone/m0-foundations → check @hubless/roadmap/milestones/hubless-milestone-m0-foundations.json
  • hubless/story/0007 → check @hubless/issues/stories/hubless-story-0007.json
  • hubless/feature/repo-foundations → check @hubless/roadmap/features/hubless-feature-repo-foundations.json (or wherever features are declared)
  • hubless/m0/task/0005 → locate the task file under your tasks/m0 or @hubless/roadmap/tasks location

Verify these exist and update the feature file accordingly before merging.

internal/docscomponents/generator.go (4)

158-211: Roadmap tables: no header when records are empty (because you error earlier). After making readers tolerant, keep headers even with zero rows.

Your current code already writes headers unconditionally—good. With tolerant readers, you’ll generate empty tables (headers only). That’s the right behavior. No change needed; just confirming.


1090-1107: Option normalization: default direction/palette is fine. Also default GraphClusters=false; document it.

No code change; update docs/CLI help to mention defaults.

Add CLI help text accordingly.


922-943: Progress bar rounding can overflow width with certain ratios; clamp before building the bar.

You clamp after rounding, but also clamp ratio to [0,1]. It’s mostly fine; tighten the math to avoid off‑by‑one on pathological floats.

Apply this diff:

-    filled := int(math.Round(ratio * width))
-    if filled > width {
-        filled = width
-    }
+    filled := int(math.Round(math.Min(1, math.Max(0, ratio)) * width))

Likely an incorrect or invalid review comment.


84-132: Approved — Generate tolerates missing @Hubless dirs; tests pass.

Ran the provided script (removed @Hubless; ran TestGenerator); all Generator tests passed — no code changes required. Ensure readers return nil/empty slices instead of errors and do not rely on old error behavior.

Comment on lines +9 to +13
- `generated/` – Output Markdown rendered from templates (do not edit, regenerated via `make docs`).
- `issues/` – stories & tasks following the task lifecycle.
- `tasks/*.json` and `stories/*.json` – source of truth (hand maintained).
- `templates/` – Markdown templates to document task/story tables (edit as needed).
- `generated/` – Output Markdown rendered from templates (`tasks.md`, `archive.md`, etc.). Do not edit by hand.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Add a bold “DO NOT EDIT” banner to generated/ directories.

Double down on the warning to prevent accidental edits in reviews.

-  - `generated/` – Output Markdown rendered from templates (do not edit, regenerated via `make docs`).
+  - `generated/` – Output Markdown rendered from templates.
+    - DO NOT EDIT: Regenerated via `make docs`; changes here will be overwritten.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @hubless/README.md around lines 9 to 13, the generated/ directory description
needs a stronger warning: update the README text for each generated/ entry to
include a bold "DO NOT EDIT" banner (e.g., prepend "**DO NOT EDIT — generated
files**" or similar) and clarify that these files are regenerated via make docs;
also add instruction to include an identical bold banner comment at the top of
any generated files themselves so reviewers see the warning in diffs.

Comment on lines +5 to +11
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Security hardening: run as non-root.

You’re installing tools as root and then running an interactive shell as root. Create a non-root user and drop privileges.

 RUN apt-get update && apt-get install -y --no-install-recommends \
     curl \
     git \
     nodejs \
     npm \
     && rm -rf /var/lib/apt/lists/*
 
+RUN useradd -m -u 10001 appuser

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Dockerfile.release-test around lines 5 to 11, the image installs packages and
leaves the container running as root; create a non-root user and drop
privileges: add a dedicated user and group (with a home directory), chown any
app/work directories and relevant files to that user, set WORKDIR to that home
or app dir, switch to the non-root user with USER before any RUN/CMD/ENTRYPOINT
that should run unprivileged, and ensure any subsequent package installs or npm
operations that must run as root are done before switching users; keep
permissions minimal and avoid giving sudo.

npm \
&& rm -rf /var/lib/apt/lists/*

RUN npm install -g markdown-transclusion
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Pin npm dependency.

Global install without a version is non-reproducible. Pin it:

-RUN npm install -g markdown-transclusion
+RUN npm install -g markdown-transclusion@<known-good-version>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Dockerfile.release-test around line 12, the RUN npm install -g
markdown-transclusion is non-reproducible because it installs the latest
package; change it to pin a specific version (for example
[email protected]) by updating the command to npm install -g
markdown-transclusion@<stable-version> or use a version variable/ARG and
reference a vetted version number so builds are deterministic.

Comment on lines +38 to +45
**Old way:**
ticket →
website →
bookkeeping →
copy-paste →
PR.
**New way:**
```bash
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Kill trailing spaces and surround fenced block with blank lines (MD009/MD031).

-  **Old way:** 
-    ticket → 
-    website →  
-    bookkeeping →  
-    copy-paste →  
-    PR.    
+  **Old way:**
+    ticket →
+    website →
+    bookkeeping →
+    copy-paste →
+    PR.
@@
-  ```bash
+  
+  ```bash
   git hubless start issue 34
@@
-  # PR updated, undrafted, review requested
+  # PR updated, undrafted, review requested

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

38-38: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)

---

39-39: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)

---

43-43: Trailing spaces
Expected: 0 or 2; Actual: 4

(MD009, no-trailing-spaces)

---

45-45: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In README.md around lines 38-45, remove trailing whitespace from the listed
lines and ensure the fenced code block is surrounded by blank lines: add an
empty line immediately before the opening bash and an empty line immediately after the closing ; also remove any extra spaces before the backticks so the
fence lines start at column 1.


</details>

<!-- fingerprinting:phantom:triton:chinchilla -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines 12 to 20
docker run --rm "$IMAGE_NAME" /bin/bash -lc "\
export PATH=/usr/local/go/bin:/go/bin:\$PATH && \
cd /app && \
git remote -v && \
go run ./cmd/release --version $VERSION --dry-run --skip-checks && \
go run ./cmd/release --version $VERSION --skip-checks && \
git tag --list && \
ls docs/reference | grep release-notes.md\
"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Make output debuggable and guard for tag collisions.

  • Add set -x inside the container so failures show context.
  • If the tag already exists (reruns), the second go run may fail. Consider deleting existing local tag first in this isolated repo (optional).
-docker run --rm "$IMAGE_NAME" /bin/bash -lc "\
+docker run --rm "$IMAGE_NAME" /bin/bash -lc "\
   export PATH=/usr/local/go/bin:/go/bin:\$PATH && \
+  set -x && \
   cd /app && \
   git remote -v && \
   go run ./cmd/release --version $VERSION --dry-run --skip-checks && \
   go run ./cmd/release --version $VERSION --skip-checks && \
   git tag --list && \
   ls docs/reference | grep release-notes.md\
 "
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
docker run --rm "$IMAGE_NAME" /bin/bash -lc "\
export PATH=/usr/local/go/bin:/go/bin:\$PATH && \
cd /app && \
git remote -v && \
go run ./cmd/release --version $VERSION --dry-run --skip-checks && \
go run ./cmd/release --version $VERSION --skip-checks && \
git tag --list && \
ls docs/reference | grep release-notes.md\
"
docker run --rm "$IMAGE_NAME" /bin/bash -lc "\
export PATH=/usr/local/go/bin:/go/bin:\$PATH && \
set -x && \
cd /app && \
git remote -v && \
go run ./cmd/release --version $VERSION --dry-run --skip-checks && \
go run ./cmd/release --version $VERSION --skip-checks && \
git tag --list && \
ls docs/reference | grep release-notes.md\
"
🤖 Prompt for AI Agents
In scripts/test-release-docker.sh around lines 12 to 20, add debugging and a
guard for tag collisions: enable shell tracing inside the container by running
set -x before executing the sequence of commands so failures show the expanded
commands and context; and before invoking the non-dry-run release (the second go
run), delete any existing local tag for the release (e.g. run git tag -d
"v$VERSION" || true) so a previous tag in this isolated test repo won’t cause
the release to fail.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +3 to +5
docs docs-components release-docs:
./scripts/render-docs.sh

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Add a sane default ‘all’, provide ‘clean’, and simplify gofmt usage.

Stop making contributors guess the entrypoint, and run with -s while you’re at it.

+all: fmt-check lint test docs-verify
+
+clean:
+	@true  # add cleanup of generated artifacts if/when you have any
+
 fmt:
-	find . -name '*.go' -not -path './vendor/*' -not -path './.git/*' -print0 | xargs -0 gofmt -w
+	find . -name '*.go' -not -path './vendor/*' -not -path './.git/*' -print0 | xargs -0 gofmt -s -w

Also applies to: 26-45

🤖 Prompt for AI Agents
In Makefile lines 3-5 (and similarly 26-45) add an explicit default target named
"all" that depends on docs docs-components release-docs so contributors don’t
have to guess the entrypoint; add a "clean" target that removes generated
documentation/artifacts; simplify the gofmt invocation to run with the -s flag
and write changes in-place (e.g. use gofmt -s -w on the project .go files
instead of the current more complex pipeline), and add .PHONY for all, clean,
docs, docs-components, release-docs to keep targets correct.

Comment on lines +5 to +8
BIN="${MARKDOWN_TRANSCLUSION_BIN:-markdown-transclusion}"
BASE_OVERRIDE="${MARKDOWN_TRANSCLUSION_BASE:-}"
ARGS_ENV="${MARKDOWN_TRANSCLUSION_ARGS:-}"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fail early if the transclusion binary is missing. Don’t wait to explode later.

Add a cheap guard and clearer logging.

 BIN="${MARKDOWN_TRANSCLUSION_BIN:-markdown-transclusion}"
 BASE_OVERRIDE="${MARKDOWN_TRANSCLUSION_BASE:-}"
 ARGS_ENV="${MARKDOWN_TRANSCLUSION_ARGS:-}"

+if ! command -v "${BIN%% *}" >/dev/null 2>&1; then
+  echo "ERROR: transclusion bin '${BIN}' not found on PATH" >&2
+  exit 127
+fi
+
 cmd=(go run ./cmd/docs-components --repo "$ROOT_DIR" --transclusion-bin "$BIN")

Also applies to: 11-25

🤖 Prompt for AI Agents
In scripts/render-docs.sh around lines 5-8 (and similarly apply to lines 11-25),
the script sets BIN="${MARKDOWN_TRANSCLUSION_BIN:-markdown-transclusion}" but
doesn't verify the binary exists before continuing; add an early guard that
checks whether "$BIN" is available (e.g. command -v "$BIN" >/dev/null 2>&1 or
test -x "$(command -v "$BIN")") and if not, write a clear error message to
stderr including the expected binary name and exit with a non-zero status; keep
the existing variable logic intact and perform this check immediately after
those variable assignments so the script fails fast with a helpful log when the
transclusion binary is missing.

Comment on lines +15 to +21
contains_placeholder() {
local file="$1"
if rg -n "!\\[\\[[^]]+\\]\\]" "$file" >/dev/null; then
return 0
fi
return 1
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ShellCheck SC2295; drop hard dep on ripgrep; quiet the matcher.

  • Quote pattern removal in parameter expansion.
  • Use rg -q if available; otherwise fall back to grep -Eq.
  • Make matcher quiet and portable.
-contains_placeholder() {
-  local file="$1"
-  if rg -n "!\\[\\[[^]]+\\]\\]" "$file" >/dev/null; then
-    return 0
-  fi
-  return 1
-}
+contains_placeholder() {
+  local file="$1"
+  local pattern='!\[\[[^]]+\]\]'
+  if command -v rg >/dev/null 2>&1; then
+    rg -q "$pattern" "$file"
+  else
+    grep -Eq "$pattern" "$file"
+  fi
+}
@@
-      if contains_placeholder "$file"; then
-        failures+=("${file#$ROOT_DIR/}")
+      if contains_placeholder "$file"; then
+        failures+=("${file#"$ROOT_DIR"/}")
       fi
@@
-if ((${#missing[@]} > 0)); then
+if ((${#missing[@]} > 0)); then
   printf 'verify-docs: missing generated targets:\n'
   printf '  %s\n' "${missing[@]}"
   exit 1
 fi
@@
-if ((${#failures[@]} > 0)); then
+if ((${#failures[@]} > 0)); then
   printf 'verify-docs: unresolved placeholders found in:\n'
   printf '  %s\n' "${failures[@]}"
   exit 1
 fi

Also applies to: 30-37, 42-55

🤖 Prompt for AI Agents
In scripts/verify-docs.sh around lines 15 to 21 (and similarly 30-37, 42-55),
the contains_placeholder function currently uses an unquoted regex with ripgrep
and triggers ShellCheck SC2295 and a hard dependency on rg; replace the body to
first quote any pattern usage, detect whether rg exists and use rg -q when
present, otherwise fall back to grep -Eq, and ensure the command is run quietly
(no output) and returns the appropriate status; update all similar matcher
usages in the other ranges to follow this portable, quiet pattern.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +322 to +471
func (g *Generator) generateDependencyGraph(ctx context.Context, data roadmapArtifacts, tasks []recordWithPath[taskRecord]) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

var all []typedRecord

for _, record := range data.Milestones {
all = append(all, typedRecord{
id: record.data.ID,
title: record.data.Title,
typeLabel: "Milestone",
deps: cleanValues(record.data.Dependencies),
})
}
for _, record := range data.Features {
all = append(all, typedRecord{
id: record.data.ID,
title: record.data.Title,
typeLabel: "Feature",
deps: cleanValues(record.data.Dependencies),
})
}
for _, record := range data.Stories {
all = append(all, typedRecord{
id: record.data.ID,
title: record.data.Title,
typeLabel: "Story",
deps: cleanValues(record.data.Dependencies),
})
}
for _, record := range tasks {
all = append(all, typedRecord{
id: record.data.ID,
title: record.data.Title,
typeLabel: "Task",
deps: cleanValues(record.data.Dependencies),
})
}

if len(all) == 0 {
return nil
}

nodeIDs := map[string]string{}
labels := map[string]string{}

for idx, record := range all {
if record.id == "" {
continue
}
nodeName := fmt.Sprintf("n%d", idx)
nodeIDs[record.id] = nodeName
labels[nodeName] = escapeMermaidLabel(fmt.Sprintf("%s\\n%s", record.typeLabel, record.title))
}

edges := make([][2]string, 0)
for _, record := range all {
src, ok := nodeIDs[record.id]
if !ok {
continue
}
for _, dep := range record.deps {
if dst, ok := nodeIDs[dep]; ok {
edges = append(edges, [2]string{src, dst})
}
}
}

direction := strings.ToUpper(strings.TrimSpace(g.options.GraphDirection))
builder := &strings.Builder{}
builder.WriteString(generatedNotice)
builder.WriteString("\n")
builder.WriteString("```mermaid\n")
builder.WriteString(fmt.Sprintf("graph %s\n", direction))

nodeNames := make([]string, 0, len(labels))
for node := range labels {
nodeNames = append(nodeNames, node)
}
sort.Strings(nodeNames)

for _, node := range nodeNames {
label := labels[node]
fmt.Fprintf(builder, " %s[\"%s\"]\n", node, label)
}

palette := g.palettes[g.options.GraphPalette]
if palette == nil {
palette = g.palettes["evergreen"]
}
classNames := make([]string, 0, len(palette))
for className := range palette {
classNames = append(classNames, className)
}
sort.Strings(classNames)
for _, className := range classNames {
color := palette[className]
fmt.Fprintf(builder, " classDef %s fill:%s,stroke:%s,color:%s,stroke-width:1px;\n", className, color.Fill, color.Stroke, color.Text)
}

if g.options.GraphClusters {
for _, cluster := range orderedTypes {
nodes := collectNodesByType(cluster, all, nodeIDs)
if len(nodes) == 0 {
continue
}
fmt.Fprintf(builder, " subgraph %s\n", cluster)
fmt.Fprintf(builder, " direction %s\n", direction)
for _, node := range nodes {
fmt.Fprintf(builder, " %s\n", node)
}
builder.WriteString(" end\n")
}
}

classAssignments := make(map[string]string)
for _, record := range all {
if node, ok := nodeIDs[record.id]; ok {
className := strings.ToLower(record.typeLabel)
if _, ok := palette[className]; ok {
classAssignments[node] = className
}
}
}

assignmentNodes := make([]string, 0, len(classAssignments))
for node := range classAssignments {
assignmentNodes = append(assignmentNodes, node)
}
sort.Strings(assignmentNodes)
for _, node := range assignmentNodes {
fmt.Fprintf(builder, " class %s %s;\n", node, classAssignments[node])
}

for _, edge := range edges {
fmt.Fprintf(builder, " %s --> %s\n", edge[0], edge[1])
}

builder.WriteString("```\n")

outputPath := filepath.Join(g.componentsDir, "roadmap", "dependencies-graph.md")
if err := g.writeFile(outputPath, builder.String()); err != nil {
return fmt.Errorf("write dependency graph: %w", err)
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Mermaid graph: you ignore duplicate IDs across types; collisions silently overwrite nodes. Detect and fail loudly.

If a Story and a Task share “ID: ABC-123”, nodeIDs clobbers and edges point to the wrong thing. That’s a nasty nondeterministic doc bug.

Apply this diff to detect duplicates and error:

-    nodeIDs := map[string]string{}
+    nodeIDs := map[string]string{}
+    dup := map[string]int{}
@@
-    for idx, record := range all {
+    for idx, record := range all {
         if record.id == "" {
             continue
         }
-        nodeName := fmt.Sprintf("n%d", idx)
-        nodeIDs[record.id] = nodeName
+        nodeName := fmt.Sprintf("n%d", idx)
+        if _, exists := nodeIDs[record.id]; exists {
+            dup[record.id]++
+        } else {
+            nodeIDs[record.id] = nodeName
+        }
         labels[nodeName] = escapeMermaidLabel(fmt.Sprintf("%s\\n%s", record.typeLabel, record.title))
     }
+    if len(dup) > 0 {
+        keys := make([]string, 0, len(dup))
+        for k := range dup {
+            keys = append(keys, k)
+        }
+        sort.Strings(keys)
+        if len(keys) > 10 {
+            keys = keys[:10]
+        }
+        return fmt.Errorf("duplicate IDs across artifacts/tasks: %s (showing up to 10)", strings.Join(keys, ", "))
+    }

Comment on lines +473 to +521
func (g *Generator) generateTaskTable(ctx context.Context, records []recordWithPath[taskRecord]) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

builder := &strings.Builder{}
builder.WriteString(generatedNotice)
builder.WriteString("\n")
builder.WriteString("| ID | Title | Status | Owner | Labels | Badges | Updated |\n")
builder.WriteString("| --- | --- | --- | --- | --- | --- | --- |\n")

for _, record := range records {
owner := "_unassigned_"
if record.data.Owner != nil && strings.TrimSpace(*record.data.Owner) != "" {
owner = *record.data.Owner
}

labels := formatList(record.data.Labels)
badges := formatList(record.data.Badges)

updated := "—"
if record.data.UpdatedAt != nil && strings.TrimSpace(*record.data.UpdatedAt) != "" {
updated = *record.data.UpdatedAt
}

link := path.Join("tasks", filepath.Base(record.sourcePath))
fmt.Fprintf(
builder,
"| [%s](%s) | %s | %s | %s | %s | %s | %s |\n",
record.data.ID,
link,
record.data.Title,
record.data.Status,
owner,
labels,
badges,
updated,
)
}

outputPath := filepath.Join(g.componentsDir, "issues", "tasks-table.md")
if err := g.writeFile(outputPath, builder.String()); err != nil {
return fmt.Errorf("write tasks table: %w", err)
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Tasks table: you print “—” for empty badges/labels but still render an extra space in changelog. Keep outputs consistent.

You already fixed it in changelog with a conditional; mirror that here to avoid stray “ — ” in embeddings. Cosmetic but user‑visible.

Apply this diff:

-        labels := formatList(record.data.Labels)
-        badges := formatList(record.data.Badges)
+        labels := formatList(record.data.Labels)
+        badges := formatList(record.data.Badges)
+        if badges == "—" {
+            badges = ""
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (g *Generator) generateTaskTable(ctx context.Context, records []recordWithPath[taskRecord]) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
builder := &strings.Builder{}
builder.WriteString(generatedNotice)
builder.WriteString("\n")
builder.WriteString("| ID | Title | Status | Owner | Labels | Badges | Updated |\n")
builder.WriteString("| --- | --- | --- | --- | --- | --- | --- |\n")
for _, record := range records {
owner := "_unassigned_"
if record.data.Owner != nil && strings.TrimSpace(*record.data.Owner) != "" {
owner = *record.data.Owner
}
labels := formatList(record.data.Labels)
badges := formatList(record.data.Badges)
updated := "—"
if record.data.UpdatedAt != nil && strings.TrimSpace(*record.data.UpdatedAt) != "" {
updated = *record.data.UpdatedAt
}
link := path.Join("tasks", filepath.Base(record.sourcePath))
fmt.Fprintf(
builder,
"| [%s](%s) | %s | %s | %s | %s | %s | %s |\n",
record.data.ID,
link,
record.data.Title,
record.data.Status,
owner,
labels,
badges,
updated,
)
}
outputPath := filepath.Join(g.componentsDir, "issues", "tasks-table.md")
if err := g.writeFile(outputPath, builder.String()); err != nil {
return fmt.Errorf("write tasks table: %w", err)
}
return nil
}
func (g *Generator) generateTaskTable(ctx context.Context, records []recordWithPath[taskRecord]) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
builder := &strings.Builder{}
builder.WriteString(generatedNotice)
builder.WriteString("\n")
builder.WriteString("| ID | Title | Status | Owner | Labels | Badges | Updated |\n")
builder.WriteString("| --- | --- | --- | --- | --- | --- | --- |\n")
for _, record := range records {
owner := "_unassigned_"
if record.data.Owner != nil && strings.TrimSpace(*record.data.Owner) != "" {
owner = *record.data.Owner
}
labels := formatList(record.data.Labels)
badges := formatList(record.data.Badges)
if badges == "—" {
badges = ""
}
updated := "—"
if record.data.UpdatedAt != nil && strings.TrimSpace(*record.data.UpdatedAt) != "" {
updated = *record.data.UpdatedAt
}
link := path.Join("tasks", filepath.Base(record.sourcePath))
fmt.Fprintf(
builder,
"| [%s](%s) | %s | %s | %s | %s | %s | %s |\n",
record.data.ID,
link,
record.data.Title,
record.data.Status,
owner,
labels,
badges,
updated,
)
}
outputPath := filepath.Join(g.componentsDir, "issues", "tasks-table.md")
if err := g.writeFile(outputPath, builder.String()); err != nil {
return fmt.Errorf("write tasks table: %w", err)
}
return nil
}

Comment on lines +672 to +771
func (g *Generator) loadRoadmapArtifacts() (roadmapArtifacts, error) {
var result roadmapArtifacts

milestones, err := g.readArtifactRecords(filepath.Join(g.repoRoot, "@hubless", "roadmap", "milestones"))
if err != nil {
return result, fmt.Errorf("load milestones: %w", err)
}

features, err := g.readArtifactRecords(filepath.Join(g.repoRoot, "@hubless", "roadmap", "features"))
if err != nil {
return result, fmt.Errorf("load features: %w", err)
}

stories, err := g.readArtifactRecords(filepath.Join(g.repoRoot, "@hubless", "issues", "stories"))
if err != nil {
return result, fmt.Errorf("load stories: %w", err)
}

result.Milestones = milestones
result.Features = features
result.Stories = stories

return result, nil
}

func (g *Generator) readArtifactRecords(dir string) ([]recordWithPath[artifactRecord], error) {
if _, err := os.Stat(dir); err != nil {
return nil, err
}

files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return nil, fmt.Errorf("glob %s: %w", dir, err)
}

if len(files) == 0 {
return nil, fmt.Errorf("no JSON files found in %s", dir)
}

sort.Strings(files)

results := make([]recordWithPath[artifactRecord], 0, len(files))
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read %s: %w", file, err)
}

var record artifactRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("decode %s: %w", file, err)
}

if record.ID == "" || record.Title == "" || record.Status == "" {
return nil, fmt.Errorf("record %s missing required fields", file)
}

results = append(results, recordWithPath[artifactRecord]{sourcePath: file, data: record})
}

return results, nil
}

func (g *Generator) readTaskRecords(dir string) ([]recordWithPath[taskRecord], error) {
if _, err := os.Stat(dir); err != nil {
return nil, err
}

files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return nil, fmt.Errorf("glob %s: %w", dir, err)
}

if len(files) == 0 {
return nil, fmt.Errorf("no JSON files found in %s", dir)
}

sort.Strings(files)

results := make([]recordWithPath[taskRecord], 0, len(files))
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read %s: %w", file, err)
}

var record taskRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("decode %s: %w", file, err)
}

if record.ID == "" || record.Title == "" || record.Status == "" {
return nil, fmt.Errorf("task %s missing required fields", file)
}

results = append(results, recordWithPath[taskRecord]{sourcePath: file, data: record})
}

return results, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Hard‑fail on missing dirs and empty folders is hostile. Treat absent/empty as empty datasets.

Currently you error if @hubless/roadmap/* or @hubless/issues/* folders are missing or empty. That bricks first‑time runs and CI docs jobs. Return empty slices instead.

Apply these diffs:

 func (g *Generator) readArtifactRecords(dir string) ([]recordWithPath[artifactRecord], error) {
-    if _, err := os.Stat(dir); err != nil {
-        return nil, err
-    }
+    if _, err := os.Stat(dir); err != nil {
+        if errors.Is(err, os.ErrNotExist) {
+            return nil, nil
+        }
+        return nil, err
+    }
@@
-    if len(files) == 0 {
-        return nil, fmt.Errorf("no JSON files found in %s", dir)
-    }
+    if len(files) == 0 {
+        return nil, nil
+    }
@@
-    return results, nil
+    return results, nil
 }
 
 func (g *Generator) readTaskRecords(dir string) ([]recordWithPath[taskRecord], error) {
-    if _, err := os.Stat(dir); err != nil {
-        return nil, err
-    }
+    if _, err := os.Stat(dir); err != nil {
+        if errors.Is(err, os.ErrNotExist) {
+            return nil, nil
+        }
+        return nil, err
+    }
@@
-    if len(files) == 0 {
-        return nil, fmt.Errorf("no JSON files found in %s", dir)
-    }
+    if len(files) == 0 {
+        return nil, nil
+    }
@@
-    return results, nil
+    return results, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (g *Generator) loadRoadmapArtifacts() (roadmapArtifacts, error) {
var result roadmapArtifacts
milestones, err := g.readArtifactRecords(filepath.Join(g.repoRoot, "@hubless", "roadmap", "milestones"))
if err != nil {
return result, fmt.Errorf("load milestones: %w", err)
}
features, err := g.readArtifactRecords(filepath.Join(g.repoRoot, "@hubless", "roadmap", "features"))
if err != nil {
return result, fmt.Errorf("load features: %w", err)
}
stories, err := g.readArtifactRecords(filepath.Join(g.repoRoot, "@hubless", "issues", "stories"))
if err != nil {
return result, fmt.Errorf("load stories: %w", err)
}
result.Milestones = milestones
result.Features = features
result.Stories = stories
return result, nil
}
func (g *Generator) readArtifactRecords(dir string) ([]recordWithPath[artifactRecord], error) {
if _, err := os.Stat(dir); err != nil {
return nil, err
}
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return nil, fmt.Errorf("glob %s: %w", dir, err)
}
if len(files) == 0 {
return nil, fmt.Errorf("no JSON files found in %s", dir)
}
sort.Strings(files)
results := make([]recordWithPath[artifactRecord], 0, len(files))
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read %s: %w", file, err)
}
var record artifactRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("decode %s: %w", file, err)
}
if record.ID == "" || record.Title == "" || record.Status == "" {
return nil, fmt.Errorf("record %s missing required fields", file)
}
results = append(results, recordWithPath[artifactRecord]{sourcePath: file, data: record})
}
return results, nil
}
func (g *Generator) readTaskRecords(dir string) ([]recordWithPath[taskRecord], error) {
if _, err := os.Stat(dir); err != nil {
return nil, err
}
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return nil, fmt.Errorf("glob %s: %w", dir, err)
}
if len(files) == 0 {
return nil, fmt.Errorf("no JSON files found in %s", dir)
}
sort.Strings(files)
results := make([]recordWithPath[taskRecord], 0, len(files))
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read %s: %w", file, err)
}
var record taskRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("decode %s: %w", file, err)
}
if record.ID == "" || record.Title == "" || record.Status == "" {
return nil, fmt.Errorf("task %s missing required fields", file)
}
results = append(results, recordWithPath[taskRecord]{sourcePath: file, data: record})
}
return results, nil
}
func (g *Generator) readArtifactRecords(dir string) ([]recordWithPath[artifactRecord], error) {
if _, err := os.Stat(dir); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return nil, fmt.Errorf("glob %s: %w", dir, err)
}
if len(files) == 0 {
return nil, nil
}
sort.Strings(files)
results := make([]recordWithPath[artifactRecord], 0, len(files))
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read %s: %w", file, err)
}
var record artifactRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("decode %s: %w", file, err)
}
if record.ID == "" || record.Title == "" || record.Status == "" {
return nil, fmt.Errorf("record %s missing required fields", file)
}
results = append(results, recordWithPath[artifactRecord]{sourcePath: file, data: record})
}
return results, nil
}
func (g *Generator) readTaskRecords(dir string) ([]recordWithPath[taskRecord], error) {
if _, err := os.Stat(dir); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return nil, fmt.Errorf("glob %s: %w", dir, err)
}
if len(files) == 0 {
return nil, nil
}
sort.Strings(files)
results := make([]recordWithPath[taskRecord], 0, len(files))
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read %s: %w", file, err)
}
var record taskRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("decode %s: %w", file, err)
}
if record.ID == "" || record.Title == "" || record.Status == "" {
return nil, fmt.Errorf("task %s missing required fields", file)
}
results = append(results, recordWithPath[taskRecord]{sourcePath: file, data: record})
}
return results, nil
}
🤖 Prompt for AI Agents
In internal/docscomponents/generator.go around lines 672–771 the
readArtifactRecords and readTaskRecords functions currently hard‑fail when the
target directory is missing or contains no JSON files; change them to treat
missing dirs and empty folders as empty datasets by returning an empty slice and
nil error instead of an error. Specifically: if os.Stat(dir) returns an error
and os.IsNotExist(err) then return an empty slice of the appropriate
recordWithPath type and nil; for other os.Stat errors keep returning the error;
also when len(files) == 0 return an empty slice and nil (not an error). Keep
other error handling (read/unmarshal/field checks) unchanged.

Docstrings generation was requested by @flyingrobots.

* #1 (comment)

The following files were modified:

* `cmd/docs-components/main.go`
* `cmd/release/main.go`
* `internal/docscomponents/generator.go`
* `internal/docscomponents/transclusion.go`
* `internal/mock/data.go`
* `internal/release/releaser.go`
* `internal/ui/tui/mock/app.go`
* `internal/ui/tui/mock/profile.go`
* `internal/ui/tui/mock/styles.go`
* `scripts/verify-docs.sh`
Copy link

coderabbitai bot commented Sep 20, 2025

Note

Generated docstrings for this pull request at #2

flyingrobots and others added 4 commits September 20, 2025 06:31
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
📝 Add docstrings to `docs-release-pipeline`
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
docs/reference/implementation-skeleton.md (8)

15-38: Project layout is already stale vs this PR. Update the tree.

You add a release CLI, docs-components generator, palettes, and scripts in this PR, yet the skeleton ignores them. Don’t mislead readers.

Apply this diff to reflect the actual structure:

 ```bash
 hubless/
 ├─ cmd/
-│  └─ hubless/
-│     └─ main.go               # composition root
+│  ├─ hubless/
+│  │  └─ main.go               # composition root
+│  ├─ release/                 # release automation CLI
+│  │  └─ main.go
+│  └─ docs-components/         # docs generator CLI
+│     └─ main.go
 ├─ internal/
 │  ├─ domain/                  # pure domain types and logic
 │  │  ├─ events.go
 │  │  └─ issue.go
 │  ├─ application/             # use cases
 │  │  └─ services.go
 │  ├─ ports/                   # boundary interfaces (in/out)
 │  │  └─ repository.go
-│  ├─ adapters/
+│  ├─ adapters/
 │  │  └─ gitstore/
 │  │     └─ git_store.go
-│  └─ ui/
+│  ├─ ui/
 │     └─ tui/                  # Bubbletea implementation
 │        ├─ model.go
 │        ├─ listview.go
 │        └─ styles.go
+│  └─ docscomponents/          # generator internals
+│     └─ ...
 ├─ go.mod
-└─ Makefile                    # build/test helpers
+├─ Makefile                    # build/test helpers
+├─ docs/
+│  ├─ components/              # transclusion blocks
+│  ├─ palettes/                # palette schemas/data
+│  └─ reference/
+├─ scripts/
+│  └─ test-release-docker.sh
+└─ .goreleaser.yaml            # if used by release

---

`59-84`: **Missing imports make even copy-paste exploration annoying. Add minimal imports.**

Yes, you said “may omit imports,” but adding one line dramatically improves UX.



```diff
 package domain
+import "time"

89-135: You call getString like it exists. It doesn’t. Provide the stub here.

Stop gaslighting readers with phantom helpers.

Append this to the snippet:

 func Replay(id IssueID, events []Event) Issue {
   ...
   return issue
 }
+
+// getString returns m[key] if it is a string, otherwise fallback.
+func getString(m map[string]any, key, fallback string) string {
+    if m == nil {
+        return fallback
+    }
+    if v, ok := m[key]; ok {
+        if s, ok := v.(string); ok && s != "" {
+            return s
+        }
+    }
+    return fallback
+}

140-188: Sorter relies on domain.PriorityOrder which you never define. Add it or drop the sort.

Right now this is dead-on-arrival for anyone trying to follow along.

Add the ordering to the domain section:

 package domain
@@
 type Priority string
@@
 const (
     PriorityHigh   Priority = "high"
     PriorityMedium Priority = "medium"
     PriorityLow    Priority = "low"
 )
+
+// PriorityOrder defines ordering: high < medium < low.
+func PriorityOrder(p Priority) int {
+    switch p {
+    case PriorityHigh:
+        return 0
+    case PriorityMedium:
+        return 1
+    case PriorityLow:
+        return 2
+    default:
+        return 3
+    }
+}

193-201: Why the hell is Now() on the store interface? Either justify it or cut it.

Time comes from the adapter or a clock abstraction, not your persistence interface unless you have a very specific reason.

 type EventStore interface {
     ListIssues(ctx context.Context) ([]domain.IssueID, error)
     LoadEvents(ctx context.Context, id domain.IssueID) ([]domain.Event, error)
     AppendEvent(ctx context.Context, event domain.Event) (string, error)
-    Now() time.Time
 }

If you need a clock, define type Clock interface { Now() time.Time } separately and inject it.


206-225: Git adapter snippet is magical hand-waving. Mark placeholders explicitly.

Readers aren’t mind-readers. Call out the fake bits so nobody assumes this is production-grade.

 func (s *Store) AppendEvent(ctx context.Context, evt domain.Event) (string, error) {
-    tree, err := s.gitWithInput("", "mktree")
+    // NOTE: placeholder call; real impl must write blobs/trees first.
+    tree, err := s.gitWithInput("", "mktree")
@@
-    msg := buildCommitMessage(evt)
-    parent := s.currentRefHead(evt.Issue)
+    // NOTE: buildCommitMessage/currentRefHead/author/timestamp/updateRef are placeholders.
+    msg := buildCommitMessage(evt) // TODO: implement
+    parent := s.currentRefHead(evt.Issue) // TODO: handle missing ref
@@
-    if err := s.updateRef(ref, strings.TrimSpace(string(oid)), parent); err != nil {
+    if err := s.updateRef(ref, strings.TrimSpace(string(oid)), parent); err != nil { // TODO: atomic update
         return "", err
     }

Also add import ( "fmt"; "strings" ) to the snippet header to avoid useless errors.


230-262: TUI snippet references ghosts: loadDetail, newList, issueItem. Either stub them or stop calling them.

Don’t ship broken pseudo-APIs in docs.

Minimal fix to avoid undefined symbols in the example:

 func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@
-        case "enter":
-            return m.loadDetail(), nil
+        case "enter":
+            // TODO: implement loadDetail to fetch and render selection.
+            return m, nil
         }
     }

Optionally append stubs for readability:

type issueItem struct{ application.IssueSummary }
func newList(items []list.Item, w, h int) list.Model { return list.New(items, list.NewDefaultDelegate(), w, h) }

266-269: Next steps ignore the biggest additions of this PR. List them.

Stop burying the lede—docs and release automation are the headline.

 - Flesh out unit tests for domain replay and adapters.
 - Expand the Git adapter with catalog and feed updates.
 - Integrate TUI commands with mutation operations (`CreateIssue`, `ChangeStatus`, `Comment`).
+ - Document and harden the release CLI (`cmd/release`): version bumping, signing, push controls, dry-run.
+ - Wire docs generator (`cmd/docs-components`, `internal/docscomponents`) into Makefile and CI.
+ - Finalize palette schema and add examples under `docs/palettes/`; validate in generator tests.
+ - Add end-to-end dockerized release tests (`scripts/test-release-docker.sh`) to CI.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 737b80e and ad56d68.

📒 Files selected for processing (38)
  • .editorconfig (1 hunks)
  • .githooks/pre-commit (1 hunks)
  • .github/workflows/docs.yml (1 hunks)
  • .github/workflows/quality.yml (1 hunks)
  • .golangci.yml (1 hunks)
  • @hubless/issues/templates/tasks.md (1 hunks)
  • AGENTS.md (1 hunks)
  • CHANGELOG.md (1 hunks)
  • CONTRIBUTING.md (2 hunks)
  • Dockerfile.release-test (1 hunks)
  • Makefile (1 hunks)
  • README.md (1 hunks)
  • cmd/docs-components/main.go (1 hunks)
  • cmd/release/main.go (1 hunks)
  • docs/components/issues/archived-stories.md (1 hunks)
  • docs/components/issues/archived-tasks.md (1 hunks)
  • docs/components/issues/changelog.md (1 hunks)
  • docs/components/issues/status-summary.md (1 hunks)
  • docs/components/issues/tasks-table.md (1 hunks)
  • docs/components/roadmap/dependencies-graph.md (1 hunks)
  • docs/components/roadmap/features-table.md (1 hunks)
  • docs/components/roadmap/milestones-table.md (1 hunks)
  • docs/components/roadmap/progress.md (1 hunks)
  • docs/components/roadmap/stories-table.md (1 hunks)
  • docs/reference/implementation-skeleton.md (9 hunks)
  • docs/reference/release-notes.md (1 hunks)
  • docs/reference/rfcs/FEEDBACK-0001-release-automation.md (1 hunks)
  • internal/docscomponents/generator.go (1 hunks)
  • internal/docscomponents/generator_test.go (1 hunks)
  • internal/docscomponents/transclusion.go (1 hunks)
  • internal/mock/data.go (1 hunks)
  • internal/release/releaser.go (1 hunks)
  • internal/ui/tui/mock/app.go (1 hunks)
  • internal/ui/tui/mock/profile.go (1 hunks)
  • internal/ui/tui/mock/styles.go (1 hunks)
  • scripts/install-git-hooks.sh (1 hunks)
  • scripts/test-release-docker.sh (1 hunks)
  • scripts/verify-docs.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
cmd/release/main.go (1)
internal/release/releaser.go (3)
  • New (32-43)
  • Options (17-22)
  • ErrVersionRequired (14-14)
internal/docscomponents/transclusion.go (1)
internal/release/releaser.go (1)
  • New (32-43)
internal/ui/tui/mock/app.go (1)
internal/ui/tui/mock/styles.go (1)
  • Styles (6-9)
cmd/docs-components/main.go (2)
internal/docscomponents/generator.go (2)
  • NewGenerator (39-76)
  • GeneratorOptions (19-24)
internal/docscomponents/transclusion.go (2)
  • TransclusionOptions (14-20)
  • RunTransclusion (29-82)
internal/docscomponents/generator_test.go (1)
internal/docscomponents/generator.go (2)
  • NewGenerator (39-76)
  • GeneratorOptions (19-24)
internal/docscomponents/generator.go (1)
internal/release/releaser.go (1)
  • New (32-43)
🪛 markdownlint-cli2 (0.18.1)
docs/components/roadmap/dependencies-graph.md

82-82: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


82-82: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


82-82: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)

docs/reference/rfcs/FEEDBACK-0001-release-automation.md

1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

@hubless/issues/templates/tasks.md

21-21: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


22-22: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)

README.md

4-4: Blank line inside blockquote

(MD028, no-blanks-blockquote)


38-38: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


39-39: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


43-43: Trailing spaces
Expected: 0 or 2; Actual: 4

(MD009, no-trailing-spaces)


45-45: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🪛 Checkov (3.2.334)
Dockerfile.release-test

[low] 1-26: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Shellcheck (0.11.0)
scripts/verify-docs.sh

[info] 33-33: Expansions inside ${..} need to be quoted separately, otherwise they match as patterns.

(SC2295)

🪛 actionlint (1.7.7)
.github/workflows/quality.yml

30-30: the runner of "golangci/golangci-lint-action@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 GitHub Check: quality
internal/ui/tui/mock/profile.go

[failure] 31-31:
expected declaration, found '}'

🪛 GitHub Actions: Quality Checks
internal/ui/tui/mock/profile.go

[error] 31-31: expected declaration, found '}'

🪛 GitHub Check: docs
internal/docscomponents/generator.go

[failure] 1138-1138:
undefined: time


[failure] 1132-1132:
undefined: time


[failure] 1131-1131:
undefined: strconv


[failure] 1130-1130:
undefined: strconv


[failure] 1120-1120:
undefined: time


[failure] 1111-1111:
undefined: time


[failure] 1107-1107:
undefined: time


[failure] 1104-1104:
undefined: time

🪛 GitHub Actions: Docs Pipeline
internal/docscomponents/generator.go

[error] 1104-1104: undefined: time

🔇 Additional comments (34)
docs/reference/implementation-skeleton.md (2)

11-11: Good: disclaimer fixed to stop pretending these compile.

This finally matches reality. Keep it that way across future edits.


42-53: Document reality: add CLI deps or state CLIs are stdlib-only.

No imports of common Go CLI frameworks (github.com/spf13/cobra, github.com/urfave/cli, github.com/alecthomas/kong) were detected — update docs/reference/implementation-skeleton.md (lines 42–53) to either add the actual CLI dependency to the go.mod snippet or explicitly state the release/docs CLIs are implemented with the standard library (flag).

.editorconfig (1)

12-19: EditorConfig for Go/Makefile looks correct.

Tabs for Go and Makefile, sane defaults elsewhere. Ship it.

.golangci.yml (1)

1-23: Good baseline; upgrade action separately.

Config is solid: key linters enabled, sane settings. No blockers here.

scripts/install-git-hooks.sh (1)

12-17: Hooks installer is correct and scoped.

Guards work-tree, uses --local, clear output. Good.

docs/components/issues/status-summary.md (1)

1-7: LGTM: Generated header + table look sane.

No lint gotchas; counts format is clean.

docs/components/issues/archived-stories.md (1)

1-5: LGTM: Clear empty state beats fake dashes.

Header suppression present; table reads fine.

docs/components/roadmap/stories-table.md (1)

1-11: LGTM: Stable table scaffold.

Generated header present, links resolve relatively, statuses consistent.

docs/components/roadmap/milestones-table.md (1)

1-10: LGTM: Header suppression + table OK.

No formatting violations.

docs/components/roadmap/progress.md (1)

1-8: LGTM: Progress table renders cleanly.

Generated header present; bars and totals consistent.

CONTRIBUTING.md (1)

44-51: Stop hand‑waving and add a sanity check for docs tooling.

You expect env vars to be set but never show a single verification step.

 - Install or clone [`markdown-transclusion`](https://github.com/flyingrobots/markdown-transclusion) (Node ≥20). Set `MARKDOWN_TRANSCLUSION_BIN` to the executable (`markdown-transclusion` if installed globally, or `node`) and `MARKDOWN_TRANSCLUSION_ARGS` to the CLI script path when using a local clone (e.g., `/path/to/markdown-transclusion/dist/cli.js`).
+ - Install or clone [`markdown-transclusion`](https://github.com/flyingrobots/markdown-transclusion) (Node ≥20). Set `MARKDOWN_TRANSCLUSION_BIN` to the executable (`markdown-transclusion` if installed globally, or `node`) and `MARKDOWN_TRANSCLUSION_ARGS` to the CLI script path when using a local clone (e.g., `/path/to/markdown-transclusion/dist/cli.js`).
+ - Verify setup:
+   ```bash
+   node -v && npm -v
+   command -v "${MARKDOWN_TRANSCLUSION_BIN:-markdown-transclusion}" || { echo "missing markdown-transclusion (or node)"; exit 1; }
+   ```
README.md (1)

3-7: Kill the pointless blank line between blockquotes (MD028).

Keep the callout contiguous with the quote or drop the first > — but don’t half‑ass both.

-# Hubless
-
-> *Imagine GitHub… but in your repo. No hub; just Git.*
-
-> [!INFO]
+# Hubless
+
+> *Imagine GitHub… but in your repo. No hub; just Git.*
+> [!INFO]
 > **EARLY DAYS.** I just started this project yesterday. Expect rapid iteration, rough edges, and breaking changes.
 > If you want boring stability, wait. If you want to see Git-native project state come alive, jump in now.
docs/reference/release-notes.md (1)

1-15: Release notes generator looks fine.

No functional issues found for this segment.

.githooks/pre-commit (1)

1-31: Pre-commit gate is solid.

Good: strict mode, tool checks, fail on dirty docs. Ship it.

Consider adding a quick “only run docs regen if sources changed” later for speed (optional).

docs/components/roadmap/features-table.md (1)

5-11: Fix broken feature links or emit the JSON targets. NOW.

These links point to docs/components/roadmap/features/.json, but the data appears under hubless/roadmap/features/.json. Either update the generator to emit JSON into docs/components/roadmap/features, or fix the links to point to the actual files. Stop shipping dead links.

Run to verify:

#!/usr/bin/env bash
set -euo pipefail
base="docs/components/roadmap"
file="$base/features-table.md"
perl -nE 'while (/\]\((features\/[^)]+\.json)\)/g) { print "$. $1\n" }' "$file" | while read -r ln rel; do
  path="$base/$rel"
  [[ -f "$path" ]] || { echo "MISSING: $path (from $file:$ln)"; exit 1; }
done
echo "All feature links resolve."
Dockerfile.release-test (1)

1-26: Stop building root-only snowflakes. Run as non-root and pin tools.

go.mod contains "go 1.25.1" — update base image to golang:1.25.1, pin markdown-transclusion, and drop privileges as shown.

-FROM golang:1.25
+FROM golang:1.25.1

@@
-RUN npm install -g markdown-transclusion
+ARG MT_VER=1
+RUN npm install -g "markdown-transclusion@^${MT_VER}"
@@
-WORKDIR /app
-COPY . /app
+RUN useradd -m -u 10001 appuser
+WORKDIR /app
+COPY --chown=appuser:appuser . /app
@@
-RUN rm -rf .git && \
-    git init && \
-    git config user.name "Release Tester" && \
-    git config user.email "[email protected]" && \
-    git add . && \
-    git commit -m "container snapshot" && \
-    git remote remove origin || true
+USER appuser
+RUN rm -rf .git && \
+    git init && \
+    git config user.name "Release Tester" && \
+    git config user.email "[email protected]" && \
+    git add . && \
+    git commit -m "container snapshot" && \
+    git remote remove origin || true
scripts/test-release-docker.sh (1)

12-21: Harden the in‑container script and stop grepping ls like it's 1995.

Enable strict mode inside the container and use a proper file test. Also guard reruns by nuking a preexisting tag before the real run.
[ suggest_recommended_refactor ]

 docker run --rm "$IMAGE_NAME" /bin/bash -lc "\
   export PATH=/usr/local/go/bin:/go/bin:\$PATH && \
-  set -x && \
+  set -euo pipefail && set -x && \
   cd /app && \
   git remote -v && \
   go run ./cmd/release --version $VERSION --dry-run --skip-checks && \
+  git tag -d \"v$VERSION\" >/dev/null 2>&1 || git tag -d \"$VERSION\" >/dev/null 2>&1 || true && \
   go run ./cmd/release --version $VERSION --skip-checks && \
   git tag --list && \
-  ls docs/reference | grep release-notes.md\
+  test -f docs/reference/release-notes.md\
 "
internal/ui/tui/mock/profile.go (2)

22-29: Magic numbers for breakpoints? Come on.

At minimum, name them. Better: import shared layout constants so they don’t drift.

-	switch {
-	case width < 100:
+	const (
+		smMax = 100
+		mdMax = 140
+	)
+	switch {
+	case width < smMax:
 		return layoutProfile{id: "sm", name: "small"}
-	case width < 140:
+	case width < mdMax:
 		return layoutProfile{id: "md", name: "medium"}
 	default:
 		return layoutProfile{id: "lg", name: "large"}
 	}

If you have central breakpoints, import and use those instead of local consts.


20-21: Duplicate/misplaced comment noise.

You already described breakpoints above. Kill the duplicate line.

-// layoutProfile encapsulates responsive settings derived from Stickers breakpoints (in terminal columns).

Likely an incorrect or invalid review comment.

cmd/docs-components/main.go (2)

79-91: Defaulting palette-file to a repo path is a foot‑gun that hard‑fails other repos. Fix it.

Make it opt‑in and only load when the file exists. This was already called out; why is it still broken?

-	flag.StringVar(&graphPalette, "graph-palette", "evergreen", "Mermaid palette for dependency graph (evergreen, infrared, zerothrow)")
-	flag.StringVar(&paletteFile, "palette-file", "docs/reference/palettes.json", "Optional palette definition file (JSON)")
+	flag.StringVar(&graphPalette, "graph-palette", "evergreen", "Mermaid palette for dependency graph (evergreen, infrared, zerothrow)")
+	flag.StringVar(&paletteFile, "palette-file", "", "Optional palette definition file (JSON)")
@@
-	generator, err := docscomponents.NewGenerator(repoRoot, componentsDir, docscomponents.GeneratorOptions{
+	// If a palette file was provided, resolve relative to repo and ignore if missing.
+	if paletteFile != "" && !strings.HasPrefix(paletteFile, "/") {
+		candidate := filepath.Join(repoRoot, paletteFile)
+		if _, statErr := os.Stat(candidate); statErr == nil {
+			paletteFile = candidate
+		} else {
+			log.Printf("warning: palette file %q not found; falling back to built-in palettes", candidate)
+			paletteFile = ""
+		}
+	}
+	generator, err := docscomponents.NewGenerator(repoRoot, componentsDir, docscomponents.GeneratorOptions{
 		GraphDirection: graphDirection,
 		GraphClusters:  graphClusters,
 		GraphPalette:   graphPalette,
 		PaletteFile:    paletteFile,
 	})

And import the missing package:

 import (
 	"context"
 	"flag"
 	"log"
 	"os"
 	"strings"
+	"path/filepath"

152-158: Env arg parsing isn’t shell‑aware. Quoted args will be split wrong.

Use a shlex parser; fall back to Fields on error.

-func parseArgs(raw string) []string {
-	fields := strings.Fields(raw)
-	return append([]string(nil), fields...)
-}
+func parseArgs(raw string) []string {
+	if strings.TrimSpace(raw) == "" {
+		return nil
+	}
+	fields, err := shlex.Split(raw)
+	if err != nil {
+		return append([]string(nil), strings.Fields(raw)...)
+	}
+	return append([]string(nil), fields...)
+}

Add the import:

 import (
@@
 	"strings"
+	"github.com/google/shlex"
 )
#!/bin/bash
set -euo pipefail
rg -n 'palette-file' cmd/docs-components/main.go
rg -n 'parseArgs' cmd/docs-components/main.go
# Build to ensure imports and flags are correct.
go build ./cmd/docs-components
internal/mock/data.go (1)

1-187: LGTM.

Types and mock factories are clean and deterministic.

internal/ui/tui/mock/app.go (1)

20-40: Stop copying the model on every update; use pointer receivers.

You mutate fields on a value receiver and return a copy. That’s wasteful and brittle. Bubble Tea is fine with pointer models.

Apply:

-func NewModel(width, height int, sections []mockStatusSection, issues []mockIssue, board []mockBoardColumn) AppModel {
+func NewModel(width, height int, sections []mockStatusSection, issues []mockIssue, board []mockBoardColumn) *AppModel {
   prof := profileForWidth(width)
-  return AppModel{
+  return &AppModel{
@@
-func (m AppModel) Init() tea.Cmd {
+func (m *AppModel) Init() tea.Cmd {
@@
-func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@
-func (m AppModel) View() string {
+func (m *AppModel) View() string {
@@
-func (m AppModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+func (m *AppModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {

Run to find call sites needing updates:

#!/bin/bash
rg -n --type=go -C2 '\bNewModel\('

Also applies to: 51-65, 67-87, 89-99, 101-107

internal/docscomponents/transclusion.go (1)

29-41: Fail fast on missing binary and missing input; surface clearer errors.

Stop punting errors to the child process. Resolve the bin and check the input exists before exec.

   bin := strings.TrimSpace(opts.Bin)
   if bin == "" {
     bin = "markdown-transclusion"
   }
+  if _, err := exec.LookPath(bin); err != nil {
+    return fmt.Errorf("resolve %s: %w", bin, err)
+  }
@@
   absInput, err := makeAbsoluteWithBase(opts.InputPath, absBasePath)
   if err != nil {
     return fmt.Errorf("resolve input path: %w", err)
   }
+  if _, err := os.Stat(absInput); err != nil {
+    return fmt.Errorf("input %s: %w", absInput, err)
+  }

Also applies to: 56-64, 70-82

internal/release/releaser.go (2)

28-43: Don’t assume repoRoot is valid — verify directory and Git repo upfront.

Fail early with a real error instead of crashing later.

 func New(repoRoot string) (*Releaser, error) {
@@
   absRoot, err := filepath.Abs(repoRoot)
   if err != nil {
     return nil, fmt.Errorf("resolve repo root: %w", err)
   }
+  fi, statErr := os.Stat(absRoot)
+  if statErr != nil || !fi.IsDir() {
+    return nil, fmt.Errorf("repo root not found or not a directory: %w", statErr)
+  }
+  if err := exec.Command("git", "-C", absRoot, "rev-parse", "--is-inside-work-tree").Run(); err != nil {
+    return nil, fmt.Errorf("not a git repository at %s: %w", absRoot, err)
+  }
 
   return &Releaser{repoRoot: absRoot}, nil
 }

45-115: Verify/implement CLI "push" controls — don't claim them if absent.

No evidence the CLI exposes or wires a push/sign flag. Either add a Push bool to Options and wire CLI flags (cmd/release) into Releaser.Run to perform git push/sign (see internal/release/releaser.go Run 45–115), or update the PR/help to state the push output is only an informational suggestion (dry-run).

internal/docscomponents/generator.go (7)

33-39: Constructor side effects: stop mkdir in NewGenerator.

Don’t mutate the world in constructors. You already mkdirAll in writeFile.

-// componentsDir to repoRoot/docs/components when empty), ensures the components directory exists,
+// componentsDir to repoRoot/docs/components when empty),
@@
- if err := os.MkdirAll(absComponentsDir, 0o755); err != nil {
-   return nil, fmt.Errorf("ensure components dir: %w", err)
- }

Also applies to: 58-60


702-715: Be hostile to users, eh? Missing/empty data dirs hard‑fail generation. Treat them as empty datasets.

First‑time runs and CI will break for no good reason.

 func (g *Generator) readArtifactRecords(dir string) ([]recordWithPath[artifactRecord], error) {
-  if _, err := os.Stat(dir); err != nil {
-    return nil, err
-  }
+  if _, err := os.Stat(dir); err != nil {
+    if errors.Is(err, os.ErrNotExist) {
+      return nil, nil
+    }
+    return nil, err
+  }
@@
-  if len(files) == 0 {
-    return nil, fmt.Errorf("no JSON files found in %s", dir)
-  }
+  if len(files) == 0 {
+    return nil, nil
+  }
@@
 func (g *Generator) readTaskRecords(dir string) ([]recordWithPath[taskRecord], error) {
-  if _, err := os.Stat(dir); err != nil {
-    return nil, err
-  }
+  if _, err := os.Stat(dir); err != nil {
+    if errors.Is(err, os.ErrNotExist) {
+      return nil, nil
+    }
+    return nil, err
+  }
@@
-  if len(files) == 0 {
-    return nil, fmt.Errorf("no JSON files found in %s", dir)
-  }
+  if len(files) == 0 {
+    return nil, nil
+  }

Also applies to: 740-752


373-383: Mermaid graph node IDs collide silently across types; detect duplicates and fail loudly.

Otherwise edges point to the wrong thing and you won’t notice.

   nodeIDs := map[string]string{}
   labels := map[string]string{}
+  dup := map[string]int{}
@@
-    nodeName := fmt.Sprintf("n%d", idx)
-    nodeIDs[record.id] = nodeName
+    nodeName := fmt.Sprintf("n%d", idx)
+    if _, exists := nodeIDs[record.id]; exists {
+      dup[record.id]++
+    } else {
+      nodeIDs[record.id] = nodeName
+    }
     labels[nodeName] = escapeMermaidLabel(fmt.Sprintf("%s\\n%s", record.typeLabel, record.title))
   }
+  if len(dup) > 0 {
+    keys := make([]string, 0, len(dup))
+    for k := range dup { keys = append(keys, k) }
+    sort.Strings(keys)
+    if len(keys) > 10 { keys = keys[:10] }
+    return fmt.Errorf("duplicate IDs across artifacts/tasks: %s (showing up to 10)", strings.Join(keys, ", "))
+  }

Also applies to: 445-454


937-951: Palette file handling is too strict; unknown classes nuke the run. Skip unknowns.

Docs authors fat‑finger one key and your whole pipeline dies. Be forgiving by default.

-      if !isKnownPaletteClass(key) {
-        return fmt.Errorf("unknown palette class %q in palette %q", className, paletteName)
-      }
+      if !isKnownPaletteClass(key) {
+        // Ignore unknown classes to keep generation resilient.
+        continue
+      }

1160-1171: Mermaid label escaping misses braces and angle brackets. Add them.

Users put braces in titles; Mermaid chokes.

 func escapeMermaidLabel(label string) string {
   replacer := strings.NewReplacer(
     "\\", "\\\\",
     "\"", "\\\"",
     "[", "\\[",
     "]", "\\]",
+    "{", "\\{",
+    "}", "\\}",
+    "<", "\\<",
+    ">", "\\>",
   )
   return replacer.Replace(label)
 }

535-556: Normalize status case once; stop lowercasing in the comparator.

Minor tidy that avoids repeated work.

-  counts := map[string]int{}
+  counts := map[string]int{}
   for _, record := range records {
-    status := strings.TrimSpace(record.data.Status)
+    status := strings.ToUpper(strings.TrimSpace(record.data.Status))
     if status == "" {
       status = "UNKNOWN"
     }
     counts[status]++
   }
@@
-  return strings.ToLower(statuses[i]) < strings.ToLower(statuses[j])
+  return statuses[i] < statuses[j]

141-215: Roadmap tables: missing records must not crash output — verification required

Ran the suggested rg in the sandbox; ripgrep returned "No files were searched" so I couldn't verify here. Run docs generation locally after applying the directory‑tolerant changes and confirm generation succeeds when the roadmap and issues dirs are absent (to debug ripgrep run: rg -n '@hubless/(roadmap|issues)' -S --debug). Attach the generation output.

Makefile (1)

1-1: You declared phony targets ‘all’ and ‘clean’ but never defined them. Add them, and fix gofmt.

Default entrypoint should work out of the box. Also use -s for gofmt. Quote NOTES.

 .PHONY: all clean docs docs-components docs-test docs-verify fmt fmt-check lint test hooks release-docs release release-dry
+
+all: fmt-check lint test docs-verify
+
+clean:
+	@true
@@
 fmt:
-	find . -name '*.go' -not -path './vendor/*' -not -path './.git/*' -print0 | xargs -0 gofmt -w
+	find . -name '*.go' -not -path './vendor/*' -not -path './.git/*' -print0 | xargs -0 gofmt -s -w
@@
-	go run ./cmd/release --version $(VERSION) $(if $(NOTES),--notes $(NOTES))
+	go run ./cmd/release --version $(VERSION) $(if $(NOTES),--notes "$(NOTES)")
@@
-	go run ./cmd/release --version $(VERSION) --dry-run $(if $(NOTES),--notes $(NOTES))
+	go run ./cmd/release --version $(VERSION) --dry-run $(if $(NOTES),--notes "$(NOTES)")

Comment on lines +1 to +9
name: Docs Pipeline

'on':
push:
branches: [main]
pull_request:
permissions:
contents: read

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Stop wasting cycles; make CI reproducible and smarter.

  • Kill global npm; use npx to avoid version drift.
  • Add concurrency to cancel superseded runs.
  • Cache Go modules.

Apply:

 name: Docs Pipeline

 'on':
   push:
     branches: [main]
   pull_request:
+concurrency:
+  group: docs-${{ github.ref }}
+  cancel-in-progress: true
@@
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
           go-version-file: go.mod
+          cache: true
+          cache-dependency-path: go.sum
@@
-      - name: Install markdown-transclusion CLI
-        run: npm install -g markdown-transclusion@^1
-      # Alternatively:
-      # - name: Generate docs
-      #   run: npx --yes markdown-transclusion --version && make docs
-      #   env:
-      #     MARKDOWN_TRANSCLUSION_BIN: npx --yes markdown-transclusion
-      - name: Generate docs
-        env:
-          MARKDOWN_TRANSCLUSION_BIN: markdown-transclusion
-        run: make docs
+      - name: Generate docs
+        run: npx --yes markdown-transclusion --version && make docs
+        env:
+          MARKDOWN_TRANSCLUSION_BIN: npx --yes markdown-transclusion

Also applies to: 16-23, 24-36

🤖 Prompt for AI Agents
.github/workflows/docs.yml around lines 1-9 (also applies to 16-23 and 24-36):
replace any use of a globally installed npm CLI with npx invocations to pin tool
versions at execution time (e.g., change "npm run ..." or global tool calls to
"npx <tool>@<version> ..." or invoke scripts via "npx npm@latest -- <args>" to
avoid version drift), add a top-level concurrency block to the workflow to
cancel superseded runs (concurrency: { group: "docs-ci-${{ github.ref }}",
cancel-in-progress: true }), and add Go module caching by adding a cache step
(use actions/cache) before running go commands with path: ${{ env.GOMODCACHE }}
or ~/.cache/go-build and key: go-mod-${{ matrix.go-version }}-${{
hashFiles('**/go.sum') }} (restore before go env download/install and save
after) so CI is reproducible and faster.

Comment on lines +18 to +23
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'

- name: Install markdown-transclusion
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

This workflow will break. Fix Go version, set up Node, upgrade golangci action.

You claim Go 1.25 (doesn’t exist here), install npm tools without Node, and use a stale action. Clean it up.

       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: '1.25'
+          go-version: '1.22.x'
+          check-latest: true
+          cache: true
+
+      - name: Set up Node
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
@@
-      - name: Run golangci-lint
-        uses: golangci/golangci-lint-action@v3
+      - name: Run golangci-lint
+        uses: golangci/golangci-lint-action@v6
         with:
           version: v1.59.1
           install-mode: binary
           args: ./...
-        env:
-          GOTOOLCHAIN: go1.22.7

Also applies to: 23-25, 29-37


🏁 Script executed:

#!/usr/bin/env bash
# Sanity-check that Go versions are consistent across repo
rg -nP "(setup-go@|go-version:|GOTOOLCHAIN|language:\s*Go)" -g ".github/**" -n -C1

Length of output: 148


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "---- top-level ----"
ls -la | sed -n '1,200p' || true
echo
echo "---- .github/workflows ----"
ls -la .github/workflows || true
echo
if [ -f .github/workflows/quality.yml ]; then
  echo "---- .github/workflows/quality.yml (full) ----"
  sed -n '1,240p' .github/workflows/quality.yml
  echo "---- lines 18-23 ----"
  sed -n '18,23p' .github/workflows/quality.yml
else
  echo "quality.yml not found; searching repo for relevant workflow keywords..."
fi
echo
echo "---- rg search for Go/Node/golangci references ----"
rg -n --hidden -S 'setup-go@|go-version:|GOTOOLCHAIN|language:\s*Go|setup-node@|node-version:|golangci-lint-action|markdown-transclusion' || true

Length of output: 7780


Fix now — workflow will fail: wrong Go version, missing Node setup, stale golangci action

  • .github/workflows/quality.yml sets go-version: '1.25' but the job itself uses GOTOOLCHAIN: go1.22.7 → align to the repo/toolchain (e.g., go1.22.x or whatever go.mod declares).
  • It runs npm install -g markdown-transclusion without setting up Node — add actions/setup-node@v4 with node-version: '20' before the npm step (docs.yml already does this).
  • golangci/golangci-lint-action@v3 is outdated here — bump to @v6 and remove the hard-coded GOTOOLCHAIN once setup-go is correct.

Location: .github/workflows/quality.yml — Set up Go / Install markdown-transclusion (lines ~18–24) and Run golangci-lint block (lines ~30–37).

🤖 Prompt for AI Agents
.github/workflows/quality.yml lines ~18–24 and ~30–37: the workflow sets
go-version: '1.25' while the job uses GOTOOLCHAIN go1.22.7, calls npm without
setting up Node, and uses an outdated golangci action; update go-version to
match the repo/toolchain (e.g., go1.22.x or whatever go.mod specifies), remove
the hard-coded GOTOOLCHAIN env in the golangci-lint step, insert an
actions/setup-node@v4 step with node-version: '20' immediately before the
markdown-transclusion npm install step, and bump
golangci/golangci-lint-action@v3 to @v6 so the lint step uses the proper
toolchain and recent action version.

Comment on lines +21 to +22
3. Once a task is `DONE` and has badges **Tested**, **Documented**, **Shipped**, move the JSON file to `tasks.archive/` and record it in `tasks.archive.md`.
## Anatomy of a Task
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Fix markdownlint violations (MD032, MD022).

Lists and headings need blank lines. Add one after the list before the next heading.

-3. Once a task is `DONE` and has badges **Tested**, **Documented**, **Shipped**, move the JSON file to `tasks.archive/` and record it in `tasks.archive.md`.
-## Anatomy of a Task
+3. Once a task is `DONE` and has badges **Tested**, **Documented**, **Shipped**, move the JSON file to `tasks.archive/` and record it in `tasks.archive.md`.
+
+## Anatomy of a Task
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
3. Once a task is `DONE` and has badges **Tested**, **Documented**, **Shipped**, move the JSON file to `tasks.archive/` and record it in `tasks.archive.md`.
## Anatomy of a Task
3. Once a task is `DONE` and has badges **Tested**, **Documented**, **Shipped**, move the JSON file to `tasks.archive/` and record it in `tasks.archive.md`.
## Anatomy of a Task
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

21-21: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


22-22: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
In @hubless/issues/templates/tasks.md around lines 21 to 22, the list item
ending with "record it in `tasks.archive.md`." is immediately followed by the
heading "## Anatomy of a Task" which triggers markdownlint MD032/MD022; add a
single blank line between the end of the list and the heading so there is an
empty line after the list before the next heading.

Comment on lines +9 to 14
1. Task added to `@hubless/issues/tasks/<id>.json`; the generated rollup (`@hubless/issues/generated/tasks.md`) should show it as `PLANNED` after regeneration.
2. Start task? status = "STARTED"
3. Task blocked? status = "BLOCKED"
4. Task finished? status = "DONE"
5. Once status = "DONE", Tasks need the following badges: (i) Tested (ii) Documented (iii) Shipped; only then shall we remove them from the tasks.md file, and add it to the "tasks.archive.md" file
5. Once status = "DONE" with badges (i) Tested (ii) Documented (iii) Shipped, the generator removes the item from the tasks rollup and adds it to the archive automatically on `make docs`. This also refreshes `CHANGELOG.md` and release notes.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Docs align with generator; ensure Go version matches CI.

Process text is fine now. Keep the stated “Go 1.22” consistent with the workflow after you fix it.

Also applies to: 41-41


🏁 Script executed:

#!/usr/bin/env bash
# Confirm AGENTS.md and workflows agree on Go minor version
echo "AGENTS.md:"; rg -n "Go 1\." AGENTS.md
echo "Workflows:"; rg -n "go-version:\s*'1\.[0-9]+\.x'" .github/workflows

Length of output: 139


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
echo "== AGENTS.md matches =="
rg -n "Go 1\." AGENTS.md || true

echo
echo "== AGENTS.md excerpt (around line 41) =="
if [ -f AGENTS.md ]; then
  nl -ba AGENTS.md | sed -n '36,46p' || true
else
  echo "AGENTS.md not found"
fi

echo
echo "== Search .github/workflows for Go usage / setup-go / go-version / matrix entries =="
if [ -d .github/workflows ]; then
  rg -n --hidden -S "go-version|actions/setup-go|setup-go@|\\bgo:\\s*\\[|\\bgo:\\s*[0-9]|matrix:|golang" .github/workflows -C3 || true
  echo
  echo "== Search for literal '1.22' in workflows =="
  rg -n --hidden -S "1\\.22" .github/workflows || true
  echo
  echo "== List workflow files =="
  ls -la .github/workflows || true
else
  echo ".github/workflows not found"
fi

Length of output: 2191


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== AGENTS.md (lines 36-46) =="
awk '{printf "%6d  %s\n", NR, $0}' AGENTS.md | sed -n '36,46p' || true

echo
echo "== go.mod (lines 1-20) =="
if [ -f go.mod ]; then
  awk '{printf "%6d  %s\n", NR, $0}' go.mod | sed -n '1,20p' || true
else
  echo "go.mod not found"
fi

echo
echo "== .github/workflows/quality.yml (full file) =="
if [ -f .github/workflows/quality.yml ]; then
  awk '{printf "%6d  %s\n", NR, $0}' .github/workflows/quality.yml || true
else
  echo "quality.yml not found"
fi

echo
echo "== .github/workflows/docs.yml (full file) =="
if [ -f .github/workflows/docs.yml ]; then
  awk '{printf "%6d  %s\n", NR, $0}' .github/workflows/docs.yml || true
else
  echo "docs.yml not found"
fi

Length of output: 4761


Update AGENTS.md Go version to match go.mod/workflows — stop shipping stale docs.

AGENTS.md:41 currently says "Go 1.22" but go.mod sets "go 1.25.1" and .github/workflows/quality.yml uses actions/setup-go go-version: '1.25' (line 21) while docs.yml reads go.mod (lines 16–19). Change AGENTS.md to "Go 1.25 (go.mod: 1.25.1)". Also review GOTOOLCHAIN: go1.22.7 in quality.yml (line 36) — either document it as an intentional golangci-lint pin or update it to 1.25.

## Latest Release Snapshot

# Hubless Release Notes
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Don’t put a second H1 in the middle of the file.

One top‑level heading per page. Fix the template so the generated output uses H2 here.

Change in the source template (CHANGELOG.template.md), not this generated file:

-# Hubless Release Notes
+## Hubless Release Notes

Re‑run: make docs

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Hubless Release Notes
## Hubless Release Notes
🤖 Prompt for AI Agents
In CHANGELOG.md around line 7 there is a second top-level H1 which should not be
added to generated output — update the source template CHANGELOG.template.md to
use an H2 (replace the stray leading "#" with "##" for this heading), commit the
change to the template (not the generated file), then re-run make docs to
regenerate CHANGELOG.md.

return layoutProfile{id: "lg", name: "large"}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

❓ Verification inconclusive

Build is red because you left a stray ‘}’. Fix it. Now.

This is a blocking syntax error: “expected declaration, found '}'”.

-}

Run locally after the fix:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
gofmt -s -w .
go build ./...
go test ./...

Length of output: 69


Remove stray '}' at internal/ui/tui/mock/profile.go:31 — build failing

internal/ui/tui/mock/profile.go:31 contains an extra closing brace causing: "expected declaration, found '}'". Delete the stray }.

-}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}
🧰 Tools
🪛 GitHub Check: quality

[failure] 31-31:
expected declaration, found '}'

🪛 GitHub Actions: Quality Checks

[error] 31-31: expected declaration, found '}'

🤖 Prompt for AI Agents
internal/ui/tui/mock/profile.go around line 31: there is a stray closing brace
'}' causing a compile error "expected declaration, found '}'". Remove that extra
'}' so the file ends with the correct declarations/closing braces only; ensure
surrounding braces remain balanced and run `go vet`/`go build` to confirm the
file compiles.

Comment on lines +11 to +21
// NewStyles constructs and returns a Styles value with the mock TUI's default styling.
//
// The returned Styles sets:
// - Statusline: foreground color #00B3A4 and bold text.
// - Footer: foreground color #6B9F7F.
func NewStyles() Styles {
return Styles{
Statusline: lipgloss.NewStyle().Foreground(lipgloss.Color("#00B3A4")).Bold(true),
Footer: lipgloss.NewStyle().Foreground(lipgloss.Color("#6B9F7F")),
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Hardcoded colors? In a palette PR? Come on.

Stop baking hex codes. Either make it theme-aware or accept a palette.

Option A (theme-aware now):

 return Styles{
-    Statusline: lipgloss.NewStyle().Foreground(lipgloss.Color("#00B3A4")).Bold(true),
-    Footer:     lipgloss.NewStyle().Foreground(lipgloss.Color("#6B9F7F")),
+    Statusline: lipgloss.NewStyle().
+      Foreground(lipgloss.AdaptiveColor{Light: "#00B3A4", Dark: "#00B3A4"}).
+      Bold(true),
+    Footer: lipgloss.NewStyle().
+      Foreground(lipgloss.AdaptiveColor{Light: "#6B9F7F", Dark: "#6B9F7F"}),
 }

Option B (clean API; keep backward compat): add

type Palette interface {
  Accent() lipgloss.TerminalColor
  Subtle() lipgloss.TerminalColor
}
func NewStylesFromPalette(p Palette) Styles {
  return Styles{
    Statusline: lipgloss.NewStyle().Foreground(p.Accent()).Bold(true),
    Footer:     lipgloss.NewStyle().Foreground(p.Subtle()),
  }
}

Then wire mocks to real palette.

🤖 Prompt for AI Agents
internal/ui/tui/mock/styles.go around lines 11 to 21: currently the mock styles
hardcode hex color strings which breaks palette/theme integration; replace this
by providing a palette-aware API: keep the existing NewStyles for backward
compatibility but implement a NewStylesFromPalette(p Palette) that returns
Styles using p.Accent() for Statusline (bold) and p.Subtle() for Footer, and
have the default NewStyles call NewStylesFromPalette with a sensible default
palette implementation; also add the Palette interface (Accent() and Subtle()
returning lipgloss.TerminalColor) and update mock wiring to construct styles via
the palette-based constructor so mocks use the real palette.

Comment on lines +38 to +46
**Old way:**
ticket →
website →
bookkeeping →
copy-paste →
PR.
**New way:**
```bash
git hubless start issue 34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Strip trailing spaces and surround the fence (MD009/MD031).

Your whitespace is sloppy; fix it and stop making linters cry.

-  **Boring stuff just happens.**  
-  **Old way:** 
-    ticket → 
-    website →  
-    bookkeeping →  
-    copy-paste →  
-    PR.    
+  **Boring stuff just happens.**
+  **Old way:**
+    ticket →
+    website →
+    bookkeeping →
+    copy-paste →
+    PR.
   **New way:**  
-  ```bash
+
+  ```bash
   git hubless start issue 34

Run to confirm no trailing spaces remain:


🏁 Script executed:

#!/bin/bash
rg -nP '\s+$' README.md

Length of output: 741


Strip trailing whitespace and surround the fenced code block (MD009 / MD031)

  • Remove trailing spaces in README.md at lines: 17, 22, 23, 25, 26, 28, 29, 31, 32, 34, 37–44.
  • Add a blank line before the opening bash and after the closing so the fenced block is properly surrounded.
  • Re-run rg -nP '\s+$' README.md (or your markdown linter) to confirm the file is clean.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

38-38: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


39-39: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


43-43: Trailing spaces
Expected: 0 or 2; Actual: 4

(MD009, no-trailing-spaces)


45-45: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
In README.md around lines 17, 22, 23, 25, 26, 28, 29, 31, 32, 34, and 37–44,
remove all trailing whitespace on the noted lines and add an empty line
immediately before the opening ```bash and another empty line immediately after
the closing ``` so the fenced code block is properly surrounded; then re-run rg
-nP '\s+$' README.md (or your markdown linter) to verify no trailing spaces
remain.


ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
IMAGE_NAME="hubless-release-test"
VERSION="0.0.1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Let VERSION be overridden.

Hard‑coding this is lazy. Respect env override.

-VERSION="0.0.1"
+VERSION="${VERSION:-0.0.1}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
VERSION="0.0.1"
VERSION="${VERSION:-0.0.1}"
🤖 Prompt for AI Agents
In scripts/test-release-docker.sh around line 6, VERSION is hard-coded as
"0.0.1" which prevents overriding via environment; change it to default from
environment by assigning VERSION="${VERSION:-0.0.1}" so callers can override
with an env var while preserving the default.

Comment on lines +15 to +22
# contains_placeholder returns 0 if the specified file contains an unresolved placeholder of the form `![[...]]`; otherwise returns 1.
contains_placeholder() {
local file="$1"
if rg -n "!\\[\\[[^]]+\\]\\]" "$file" >/dev/null; then
return 0
fi
return 1
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ShellCheck SC2295, drop hard rg dep, make matcher quiet/portable.

Unquoted parameter expansion and unconditional rg usage are sloppy. Make it portable and correct.

 contains_placeholder() {
   local file="$1"
-  if rg -n "!\\[\\[[^]]+\\]\\]" "$file" >/dev/null; then
-    return 0
-  fi
-  return 1
+  local pattern='!\[\[[^]]+\]\]'
+  if command -v rg >/dev/null 2>&1; then
+    rg -q "$pattern" "$file"
+  else
+    grep -Eq "$pattern" "$file"
+  fi
 }
@@
-      if contains_placeholder "$file"; then
-        failures+=("${file#$ROOT_DIR/}")
+      if contains_placeholder "$file"; then
+        failures+=("${file#"$ROOT_DIR"/}")
       fi
@@
-    if contains_placeholder "$path"; then
-      failures+=("${target}")
+    if contains_placeholder "$path"; then
+      failures+=("$target")
     fi
@@
-if ((${#failures[@]} > 0)); then
+if ((${#failures[@]} > 0)); then
   printf 'verify-docs: unresolved placeholders found in:\n'
-  printf '  %s\n' "${failures[@]}"
+  printf '  %s\n' "${failures[@]}"
   exit 1
 fi

Also applies to: 31-35, 37-39, 49-53

🤖 Prompt for AI Agents
In scripts/verify-docs.sh around lines 15-22, the function uses an unquoted
parameter and relies on ripgrep (rg); replace the rg invocation with a portable,
quiet grep invocation and ensure the filename parameter is quoted, and simplify
the function to return grep's exit status; apply the same changes to the other
occurrences at lines 31-35, 37-39, and 49-53 so all placeholder checks use grep
-qE (or grep -q -E) with a properly escaped pattern and quoted "$file".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant