Skip to content

fix: relative --dir, worktree re-registration, dashboard history, health badges, cleanup - #104

Merged
justinjdev merged 12 commits into
mainfrom
fix/cli-correctness-2
Sep 5, 2026
Merged

justinjdev merged 12 commits into
mainfrom
fix/cli-correctness-2

Conversation

@justinjdev

@justinjdev justinjdev commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

What

  • gitutil.MainRepoRoot now filepath.Abss its input before joining a relative --git-common-dir answer, so a relative --dir (., sub, ../x) resolves the same repo root as an absolute one — checkDir compares them correctly from both a main checkout and a linked worktree.
  • fellowship.upsertQuest handles a worktree already held by a different quest row (the unique index added in schema v2 would otherwise reject the insert): it clears the previous holder's worktree and reports the reassignment, rather than failing. Updated the now-stale FindQuest comment in state.go to match (comment-only change to that file).
  • The dashboard's handleGateApprove/handleGateReject now share state.Approve/state.Reject + gate.RecordApproval/RecordRejection with the CLI and group.BatchApprove, instead of a hand-rolled mutation that recorded events but skipped tome (history) rows.
  • health.Sweep now populates QuestHealth.Worktree by joining fellowship_quests; app.js matches a quest's health badge by name as a fallback to worktree, and its "Implement+" count derives the threshold from PHASES.indexOf("Implement") instead of a hardcoded index.
  • Dead code / duplication cleanup: group show --json gained a progress field wired to group.CalculateProgress (deleted the now-unused LoadAndMarshalProgress/ProgressSummary/FindGroupForQuest); db/migrate.go's listWorktreePaths was replaced with gitutil.ListWorktrees (same empty-list fallback); todo.Init no longer silently discards a --task it never persisted (dropped the flag — no schema change was in scope to actually store it); todo.List returns [] instead of nil; removed an unreachable status == "failed" branch in failures.go (that status is never set); gate.RecordApproval now records a real duration_s instead of a hardcoded 0; deleted the dashboard's unused worktreeDirs (staticcheck U1000).
  • Added a subprocess test (cmd/fellowship) that builds the real binary and runs hook gate-guard end to end for a missing store (exit 0) and a corrupt store (exit 2), asserting on cmd.ProcessState.ExitCode(). Gave events.DetectProblems an injectable Options{Now} (variadic, existing callers unaffected) and switched problems_test.go to it — this also fixed a real bug where the zombie "no activity for" message was computed against a zero time.Time because the caller's own Now was never resolved the way health.Sweep's internal copy is.
  • Added a README "Unreleased" bullet summarizing the above.

Two minimal, out-of-scope touches, called out per this batch's scope discipline: cmd/fellowship/main.go's --help text still documented the removed todo init --task flag, so I deleted that one doc line (plugin/skills/fellowship/SKILL.md documents --task too, but plugin/ is owned by another agent in this batch and is left alone — flagging it here for a follow-up); cmd/fellowship/events.go's single DetectProblems call site needed no change since the new option is variadic.

Left out: sharing one resolveDirQuest/resolveHoldQuest resolver would require editing cmd/fellowship/gate.go (owned by another agent in this batch) and misc.go beyond checkDir (also restricted to checkDir-only for this PR), so it's skipped rather than crossing that boundary. init on a completed quest is likewise left to another agent's PR.

Why

Six correctness/cleanup issues flagged in review of the recent state-machine and dashboard work (PRs #94-#103): a relative --dir silently resolving to the wrong repo root, a worktree-reassignment path that the new unique index would reject, a dashboard gate decision that recorded events but not history, health/dashboard data not carrying a quest's worktree, several small dead-code/duplication spots, and thin test coverage around the hook binary's store-open exit codes and the zombie-detection clock.

Verification

  • cd cli && gofmt -l . — clean
  • cd cli && go vet ./... — clean
  • cd cli && go test -race ./... — all packages pass
  • cd site && npm ci && npm run build — site builds (app.js lives in cli/internal/dashboard/static/, not site/, but this was requested as part of this PR's verification)
  • Smoke, against the built binary in a scratch repo:
    • fellowship todo list --dir . from a main checkout after state add-quest --dir . --worktree . (relative --dir resolves)
    • registering q1 then q1b on the same shared worktree — prints Note: worktree "…" was registered to quest "q1"; reassigning it to "q1b", and the worktree then resolves to q1b
    • a dashboard POST /api/gate/approve against a quest with a pending gate — history.LoadGates shows the approved row and history.LoadPhases shows a nonzero duration_s

🤖 Generated with Claude Code

https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Relative --dir paths now resolve correctly.
    • Worktree reassignment and health reporting now remain consistent across CLI and dashboard views.
    • Dashboard gate approvals and rejections now record matching history and accurate phase durations.
    • Dashboard progress counts adapt to configured phases.
    • Empty todo lists now return items: [] instead of null.
    • Group details now include progress information.
  • CLI Changes
    • Removed the unsupported --task option from todo init and its help text.

…dirs

gitutil.MainRepoRoot joined a relative `--git-common-dir` answer onto
its input `dir` without ever making `dir` absolute first, so calling
it with a relative --dir (".", "sub", "../x") produced a relative main
repo root. Every comparison of that value (checkDir, quest lookup,
worktree dedup) then depended on the caller happening to canonicalize
it. filepath.Abs the input before joining so MainRepoRoot always
returns an absolute root.

Extends TestCheckDir with relative-path cases from both a main
checkout and a linked worktree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
fellowship_quests.worktree carries a UNIQUE index (schema v2), but
upsertQuest's ON CONFLICT clause only deduped on name, so registering
a worktree already held by a different quest row hit a constraint
violation instead of being treated as re-registration. upsertQuest now
clears the worktree from whichever row previously held it and prints
a note naming the previous holder before completing the upsert;
registering the same quest name against the same worktree again is
still just an update in place.

Updates the now-stale FindQuest comment (state.go) to describe the
current one-row-per-worktree invariant instead of the old
multiple-claimants tiebreak.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
handleGateApprove and handleGateReject hand-rolled the gate/phase
mutation and only recorded events, skipping tome (history) entries —
diverging from `fellowship gate approve|reject` and
group.BatchApprove, which use state.Approve/state.Reject +
gate.RecordApproval/RecordRejection and get history for free. Both
handlers now go through the same functions, so a dashboard-driven gate
decision produces gate/phase history rows exactly as a CLI or group
approval does.

Adds httptest coverage asserting history.LoadGates shows the resulting
approved/rejected row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
quest_state carries no worktree column, so Sweep left QuestHealth.Worktree
empty for every quest — it now joins fellowship_quests by name to fill
it in, the same way the dashboard already looks up a quest's worktree.

app.js's getQuestHealth only matched health entries by worktree, which
silently dropped the badge whenever a quest had no registered worktree
yet; it now also matches by quest name. renderGroupHeader's "Implement+"
count compared PHASES.indexOf(phase) against a hardcoded 3 (Review only,
in the default four-phase order) instead of PHASES.indexOf("Implement"),
so it now derives the threshold from the actual phase list.

Adds TestSweepPopulatesWorktree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
…s/gate

- group.LoadDetail (`group show --json`) now wires CalculateProgress into a
  `progress` field, reusing fellowship.DiscoverQuests for quest status
  instead of a second query path. Deletes the now-unused
  LoadAndMarshalProgress, ProgressSummary, FindGroupForQuest, and the
  questEntryStatuses helper that only LoadAndMarshalProgress called.
- db/migrate.go's listWorktreePaths duplicated gitutil.ListWorktrees's git
  worktree parsing; discoverJSONFiles now calls gitutil.ListWorktrees
  directly and keeps the same empty-list-falls-back-to-mainRepo behavior.
  Rewrites the two migrate tests that relied on mocking git output to use a
  real git repo with a linked worktree instead, since the mockable
  execCommand var went away with the duplicate.
- todo.Init took a task description it silently discarded — neither errands
  nor errand_deps has a column for it and adding one is a schema change out
  of scope here, so `todo init` drops --task entirely (removed the flag,
  its usage line, its doc line in `fellowship` --help, and the always-empty
  QuestTodoList.Task field) rather than keep accepting and dropping it.
- todo.List returned nil for an empty quest; `todo show` marshals it
  straight to JSON, so it now returns [] instead of null.
- failures.go's inferTriggerFromDB checked status == "failed", a status
  this codebase never sets (fellowship_quests.status is constrained to
  active/completed/cancelled) — removed the unreachable branch and fixed
  history.QuestHistory's stale doc comment to match.
- gate.RecordApproval recorded duration_s as a hardcoded 0. It now measures
  the real time spent in the phase being left: from the previous phase's
  completed_at, or the quest's created_at for its first phase completion.
- Deleted dashboard's unused worktreeDirs (staticcheck U1000).

Left out: sharing one resolveDirQuest/resolveHoldQuest resolver would
require editing cmd/fellowship/gate.go (explicitly off-limits — owned by
another agent in this batch) and misc.go beyond checkDir (also restricted
to checkDir-only here), so it's skipped; noting it rather than crossing
that line. `init` on a completed quest is likewise left alone per the task.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
…tic clock for DetectProblems

Adds TestHookGateGuard_StoreOpenExitCodes (cmd/fellowship): builds the real
binary to a temp dir and runs `hook gate-guard` against it end to end for
the two extremes of storeOpenExit's posture — no store at all (exit 0,
and must not create one just by running) and a store that exists but
can't be opened (exit 2, fail closed) — asserting on
cmd.ProcessState.ExitCode() rather than parsing Run()'s error.

events.DetectProblems gains a variadic Options{Now} so callers (and
tests) can inject a fixed clock; existing call sites are unaffected. This
also fixes a real bug: the zombie message's activityAge lookup read the
Now field off the caller's own health.DefaultOptions() value, which is
never resolved from its zero value the way health.Sweep resolves its own
copy internally — so every zombie's "no activity for" duration was
computed against a zero time.Time. problems_test.go now passes a fixed
Options{Now: ...} instead of live time.Now() offsets, and
TestZombieDetection asserts the resulting message string exactly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
…fixes

Summarizes this branch's fixes for the README changelog: relative --dir
resolution, worktree re-registration, dashboard gate history sharing,
health worktree/name matching, group show --json progress, todo
init/show cleanup, and real gate duration_s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
@coderabbitai

coderabbitai Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: a05a1bd4-4f5a-4dcb-bc03-0eeadcda3523

📥 Commits

Reviewing files that changed from the base of the PR and between 18527c7 and 1b57375.

📒 Files selected for processing (5)
  • README.md
  • cli/internal/events/problems.go
  • cli/internal/fellowship/fellowship.go
  • cli/internal/group/group.go
  • cli/internal/group/group_test.go
📝 Walkthrough

Walkthrough

The changes correct relative repository path handling, worktree reassignment, dashboard gate transitions, gate history durations, health reporting, dashboard progress matching, group progress output, todo initialization, and empty todo serialization. Tests cover these behaviors.

Changes

CLI correctness updates

Layer / File(s) Summary
Repository paths and migration discovery
cli/internal/gitutil/gitutil.go, cli/cmd/fellowship/main_test.go, cli/internal/db/migrate.go, cli/internal/db/migrate_test.go, cli/cmd/fellowship/hook_subprocess_test.go
Relative paths resolve against absolute repository roots. Migration discovery uses shared worktree listing. Tests cover linked worktrees and hook exit codes.
Worktree ownership reassignment
cli/internal/fellowship/fellowship.go, cli/internal/fellowship/fellowship_test.go, cli/internal/state/state.go
Quest registration clears a previous worktree holder before reassignment. Same-name registration remains an in-place update.
Dashboard gate state and history
cli/internal/dashboard/server.go, cli/internal/dashboard/server_test.go, cli/internal/gate/record.go, cli/internal/gate/record_test.go
Dashboard gate endpoints use shared state transitions and record gate history. Approval duration derives from the previous phase completion or quest creation time.
Health, progress, and dashboard data
cli/internal/events/*, cli/internal/health/*, cli/internal/group/*, cli/internal/dashboard/static/app.js, cli/internal/history/history.go
Health reports include worktrees. Event detection accepts an injectable clock. Group details include progress. Dashboard counts use the configured phase order and match health by worktree or quest name.
Todo API and CLI cleanup
cli/cmd/fellowship/todo.go, cli/internal/todo/*, cli/internal/failures/failures.go, README.md
todo init no longer accepts --task. Empty todo lists serialize as []. Failure inference only handles the supported cancelled status. The changelog records the corrections.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 18527

Zombie health messages can show an inactivity-age bucket inconsistent with their classification near a threshold. Resolve the clock once before merge to keep health output coherent.

Sequence Diagram(s)

sequenceDiagram
  participant DashboardAPI
  participant State
  participant GateRecord
  participant History
  DashboardAPI->>State: Approve or reject gate
  State-->>DashboardAPI: Transition result
  DashboardAPI->>GateRecord: Record gate event and duration
  GateRecord->>History: Persist gate and phase history
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 71 functions across 24 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes several primary changes, including relative --dir handling, worktree re-registration, dashboard history, and health badges. It is concise and relevant.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 59.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 71 functions across 24 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-correctness-2

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 4f2907f1-ab51-4795-8c16-3c13163bfb54

📥 Commits

Reviewing files that changed from the base of the PR and between 5c7973b and 18527c7.

📒 Files selected for processing (26)
  • README.md
  • cli/cmd/fellowship/hook_subprocess_test.go
  • cli/cmd/fellowship/main.go
  • cli/cmd/fellowship/main_test.go
  • cli/cmd/fellowship/todo.go
  • cli/internal/dashboard/server.go
  • cli/internal/dashboard/server_test.go
  • cli/internal/dashboard/static/app.js
  • cli/internal/db/migrate.go
  • cli/internal/db/migrate_test.go
  • cli/internal/events/problems.go
  • cli/internal/events/problems_test.go
  • cli/internal/failures/failures.go
  • cli/internal/fellowship/fellowship.go
  • cli/internal/fellowship/fellowship_test.go
  • cli/internal/gate/record.go
  • cli/internal/gate/record_test.go
  • cli/internal/gitutil/gitutil.go
  • cli/internal/group/group.go
  • cli/internal/group/group_test.go
  • cli/internal/health/health.go
  • cli/internal/health/health_test.go
  • cli/internal/history/history.go
  • cli/internal/state/state.go
  • cli/internal/todo/todo.go
  • cli/internal/todo/todo_test.go
💤 Files with no reviewable changes (1)
  • cli/cmd/fellowship/main.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread cli/internal/events/problems.go Outdated
group.LoadDetail called fellowship.DiscoverQuests to build the
[]QuestStatus CalculateProgress needs, which runs state.Load and
todo.Progress for every quest across the whole fellowship rather than
just this group's members (and duplicates the state.Load the existing
loop already does per member). Restores a lean questEntryStatuses
helper (one cheap query) and builds QuestStatus from data already
gathered in the loop.

Two other findings from this pass were rejected as out of scope for
this branch rather than applied: a stale --task example in
plugin/skills/fellowship/SKILL.md (plugin/ is owned by another agent
in this batch) and a missing site changelog entry (this task's docs
scope is README Unreleased bullets only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
Round 1's questEntryStatuses-based LoadDetail dropped a case
fellowship.DiscoverQuests used to handle: a quest whose fellowship
entry is completed/cancelled but has no quest_state row (e.g. a
history-only or pre-phase-machinery entry) was silently excluded from
CalculateProgress instead of counting toward Progress.Completed the
way it always had.

LoadDetail's per-quest loop now falls back to a synthetic
terminal-phase QuestStatus for that case, same as DiscoverQuests did.
Adds TestLoadDetail_Progress_CompletedNoQuestState (fails without the
fix) and fixes a stale comment left over from round 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
Rounds 1-2 left the "completed/cancelled quest with no quest_state row"
synthetic-entry logic duplicated between fellowship.DiscoverQuests and
group.LoadDetail — the exact kind of drift that caused round 1 to drop
it in LoadDetail in the first place. Extracts it into a shared
fellowship.TerminalQuestStatus(name, worktree, entryStatus) helper and
switches both call sites to it. Pure de-duplication: behavior is
unchanged, verified by the existing DiscoverQuests and LoadDetail
progress tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193Ae6GSguC4hYBDA7NhJoN
@justinjdev
justinjdev merged commit 4967213 into main Sep 5, 2026
5 checks passed
@justinjdev
justinjdev deleted the fix/cli-correctness-2 branch September 5, 2026 22:28
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.

2 participants