Skip to content

feat(cli): add git warp doctor structural diagnostics#28

Merged
flyingrobots merged 14 commits intomainfrom
doctor
Feb 14, 2026
Merged

feat(cli): add git warp doctor structural diagnostics#28
flyingrobots merged 14 commits intomainfrom
doctor

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 14, 2026

Summary

  • Adds git warp doctor command with 7 diagnostic checks: repo-accessible, refs-consistent, coverage-complete, checkpoint-fresh, audit-consistent, clock-skew, hooks-installed
  • Read-only, O(writers) fast path — no materialization required
  • Budget enforcement with global 10s deadline; skipped checks appear as findings
  • Machine-readable --json output with versioned DoctorPayload (v1), policy echo, sorted findings, deterministic health derivation
  • --strict flag treats warnings as failures (exit 4 vs exit 3)
  • Human-readable output with colored status icons and priority action summary

Test plan

  • doctorSchema unit tests (3 tests in schemas.test.js)
  • Golden-JSON unit tests (4 tests in doctor.test.js) — healthy graph, warnings, strict mode, sort order
  • BATS E2E tests (5 scenarios in cli-doctor.bats) — healthy graph, broken ref, missing checkpoint, strict mode
  • Pre-push: lint + typecheck + ts-policy + 3564 unit tests all green
  • CI: BATS in Docker (node22)

Summary by CodeRabbit

  • New Features

    • Added a read-only "doctor" diagnostics command with multiple non-throwing health checks, --strict mode, JSON and human outputs, standardized finding codes, priority actions, and explicit exit codes; CLI help and renderer updated.
  • Documentation

    • Added changelog entry for 10.13.0, expanded README, introduced a WHITEPAPER, and updated the roadmap.
  • Tests

    • New unit and end-to-end tests plus test helpers covering doctor behavior and schema.
  • Chores

    • Version bumped to 10.13.0

CI Bot added 2 commits February 13, 2026 18:52
7 diagnostic checks (repo-accessible, refs-consistent, coverage-complete,
checkpoint-fresh, audit-consistent, clock-skew, hooks-installed) with
budget enforcement, sorted findings, deterministic health derivation,
and machine-readable JSON output.

Includes --strict flag (exit 4 on warnings), text presenter with colored
status icons, doctorSchema, golden-JSON unit tests, and BATS E2E tests.
- Place TODO(ts-cleanup) tags on the same line as wildcard casts
- Fix circular JsonValue typedef for TypeScript compatibility
- Add type annotations to doctor.test.js mock variables
@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new read-only CLI command doctor with a full diagnostic framework: orchestrator, seven non-throwing checks, centralized codes/types, presenter support (text/JSON), CLI wiring and schema, tests (unit + BATS), docs updates, hook template path tweak, and package bump to 10.13.0.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md, README.md, ROADMAP.md, WHITEPAPER.md
Changelog entry for v10.13.0 and extensive README rework; marked DOCTOR done in roadmap and added DOCTOR backlog items; new WHITEPAPER.md describing graph/CRDT model and architecture.
Doctor Command Core
bin/cli/commands/doctor/index.js, bin/cli/commands/doctor/checks.js, bin/cli/commands/doctor/codes.js, bin/cli/commands/doctor/types.js
New handleDoctor orchestrator, ordered non-throwing checks (ALL_CHECKS), centralized CODES, JSDoc types, DOCTOR_EXIT_CODES, payload assembly, deadline/budget and strict-mode/exit-code logic, and many new check implementations and helpers.
CLI Wiring & Schemas
bin/cli/commands/registry.js, bin/cli/infrastructure.js, bin/cli/schemas.js, bin/cli/types.js
Registered doctor command, added to KNOWN_COMMANDS and HELP_TEXT with --strict flag, exported doctorSchema, and added nodeExists to Persistence typedef.
Presentation Layer
bin/presenters/index.js, bin/presenters/text.js
Added renderDoctor renderer and wired it into TEXT_RENDERERS; human-friendly report formatting (icons, colors, priority actions).
Tests & Helpers
test/unit/cli/doctor.test.js, test/unit/cli/schemas.test.js, test/bats/cli-doctor.bats, test/bats/helpers/seed-doctor-graph.js
Added comprehensive unit tests (healthy/warn/fail/strict/sorting), schema tests for doctorSchema, BATS E2E tests and a seed helper to prepare demo graphs.
Shared / Hooks
bin/cli/shared.js
Changed HookInstaller templateDir resolution to src/hooks (template path change).
Packaging
package.json
Bumped package version 10.12.0 → 10.13.0.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Handler
    participant Ctx as DoctorContext
    participant Reg as ALL_CHECKS Registry
    participant Check as Individual Check
    participant Pers as Persistence/Services
    participant Presenter as Presenter

    CLI->>Ctx: build context (persistence, graph, writer heads, policy)
    CLI->>Reg: run ALL_CHECKS with deadline/budget
    loop per check until deadline
        Reg->>Check: execute check(ctx)
        Check->>Pers: query refs, coverage, checkpoints, audit, hooks, clock
        Pers-->>Check: return data / evidence
        Check-->>Reg: return DoctorFinding(s)
    end
    Reg-->>CLI: aggregated findings
    CLI->>CLI: assemble payload (health, summary, duration, exit code)
    CLI->>Presenter: render payload (json or text)
    Presenter-->>CLI: formatted output
    CLI-->>CLI: exit with computed code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through refs and checked each tree,
Seven tiny probes humbly inspecting thee,
I counted clocks, hooks, coverage with delight,
Report in paw, in JSON or text—tidy and bright,
A cheerful thump: the repo sleeps well tonight.

🚥 Pre-merge checks | ✅ 4
✅ 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 clearly and specifically describes the main change: adding a new 'git warp doctor' CLI command for structural diagnostics.
Docstring Coverage ✅ Passed Docstring coverage is 90.32% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch doctor

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.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

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: 4

🤖 Fix all issues with AI agents
In `@bin/cli/commands/doctor/checks.js`:
- Around line 291-301: collectWriterDates currently calls
ctx.persistence.getNodeInfo(head.sha) without guarding head.sha, which will
throw if head.sha is null; update the function (collectWriterDates) to skip or
filter out ctx.writerHeads entries with falsy head.sha before calling
ctx.persistence.getNodeInfo (e.g., continue/omit entries where !head.sha) so you
only call getNodeInfo with a valid SHA and avoid the CHECK_INTERNAL_ERROR during
the clock-skew check.

In `@bin/cli/commands/doctor/index.js`:
- Around line 142-152: The loop invoking check.fn(ctx) lacks an error boundary
so a thrown exception aborts the doctor run; wrap the call to check.fn(ctx) in a
try/catch around the code that measures checkStart/checkDuration and processes
resultArray (the block using checkStart, checkDuration, normalizeResult,
findings, and checksRun), and on catch push a finding built using the existing
CHECK_INTERNAL_ERROR code from codes.js (include context like
check.name/identifier and the caught error message), increment checksRun, set
durationMs appropriately, and continue to the next check so one failing check
doesn't crash the whole command.

In `@CHANGELOG.md`:
- Line 21: The changelog entry for "BATS E2E tests" incorrectly enumerates 4
scenarios; update the parenthetical in the line that starts with "**BATS E2E
tests**" to list all five scenarios (healthy JSON, human output, broken ref,
missing checkpoint, strict mode) so the text matches the actual tests.

In `@test/unit/cli/doctor.test.js`:
- Around line 72-81: The JSDoc for CLI_OPTIONS incorrectly claims it's a
DoctorPayload; update the annotation for CLI_OPTIONS to reflect the actual
CLI/options type (or a generic type) instead of DoctorPayload—locate the correct
type name in the doctor command's types file (or remove the specific type and
use /** `@type` {*} */) and replace DoctorPayload in the JSDoc so the symbol
CLI_OPTIONS is typed appropriately and no longer misleading.
🧹 Nitpick comments (8)
test/bats/cli-doctor.bats (1)

43-60: Unnecessary cd round-trips when git -C is already used.

Lines 45-47 do cd "${PROJECT_ROOT}", then git -C "${TEST_REPO}" update-ref ..., then cd "${TEST_REPO}". Since git -C targets TEST_REPO directly, both cd commands are unnecessary. Same pattern appears in Test 4 (lines 64-66) and Test 5 (lines 82-84).

That said, this follows the same pattern used in other BATS tests in this repo, so this may be intentional for consistency.

♻️ Simplified version (applicable to all three tests)
-  cd "${PROJECT_ROOT}" || return 1
   git -C "${TEST_REPO}" update-ref refs/warp/demo/writers/ghost deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
-  cd "${TEST_REPO}" || return 1
bin/cli/commands/doctor/index.js (1)

73-94: Three linear scans over findings where one suffices.

Lines 74-76 iterate findings three times to count ok/warn/fail. With a single pass this is trivial and more efficient if the findings array ever grows.

♻️ Single-pass counting
-  const ok = findings.filter((f) => f.status === 'ok').length;
-  const warn = findings.filter((f) => f.status === 'warn').length;
-  const fail = findings.filter((f) => f.status === 'fail').length;
+  let ok = 0, warn = 0, fail = 0;
+  for (const f of findings) {
+    if (f.status === 'ok') ok++;
+    else if (f.status === 'warn') warn++;
+    else if (f.status === 'fail') fail++;
+  }
test/unit/cli/doctor.test.js (2)

197-210: Sort-order test silently passes when preconditions aren't met.

The if (lastFailIdx >= 0 && firstOkIdx >= 0) guard on line 207 means the test body is skipped entirely if the mock setup doesn't actually produce both fail and ok findings. A test that conditionally asserts nothing is fragile and can mask regressions.

Consider asserting the preconditions unconditionally so the test fails loudly if the setup is wrong.

Proposed fix
     const statuses = result.payload.findings.map((/** `@type` {*} */ f) => f.status);
-    // fail should come before warn, which comes before ok
-    const firstOkIdx = statuses.indexOf('ok');
-    const lastFailIdx = statuses.lastIndexOf('fail');
-    if (lastFailIdx >= 0 && firstOkIdx >= 0) {
-      expect(lastFailIdx).toBeLessThan(firstOkIdx);
-    }
+    // Precondition: the mock must produce both fail and ok findings
+    expect(statuses).toContain('fail');
+    expect(statuses).toContain('ok');
+
+    const firstOkIdx = statuses.indexOf('ok');
+    const lastFailIdx = statuses.lastIndexOf('fail');
+    expect(lastFailIdx).toBeLessThan(firstOkIdx);

89-107: Dynamic import inside beforeEach is redundant after the first run.

ESM modules are cached after the first await import(...). Subsequent calls in later beforeEach invocations return the same cached module, so handleDoctor is always the same reference. The dynamic import is only needed once. This isn't a bug (it works correctly), but it's misleading — it reads as though the module is re-evaluated per test to "pick up mocks," which isn't what happens. The comment on line 104 reinforces that misconception.

If the intent is just to load the module after vi.mock hoisting takes effect, a single top-level await import(...) (like the one on line 28 for shared.js) would be clearer.

bin/cli/commands/doctor/checks.js (4)

43-62: checkRepoAccessible instantiates HealthCheckService and ClockAdapter directly, bypassing any injected/mocked versions.

The test file mocks both HealthCheckService and ClockAdapter at the module level, so this works for testing. However, this check creates its own HealthCheckService using ctx.persistence rather than receiving it via the context — unlike the other checks that only use ctx members. If HealthCheckService or ClockAdapter is ever replaced or configured differently at the CLI layer, this check won't pick up those changes.

This is acceptable for now given the mocking strategy, but worth noting for future maintainability.


191-213: Checkpoint with unparseable date is reported as ok.

When ageHours is null (i.e., the date couldn't be parsed), buildCheckpointFinding returns an ok status with message "Checkpoint exists (age unknown)". An unparseable date could indicate data corruption or an unexpected format. Returning ok here could mask a real issue — a warn with a code like CHECKPOINT_OK (or a dedicated code) might be more appropriate for "age unknown."

This is a design-level observation; up to you whether to address it now.


348-356: checkHooksInstalled declared as sync but the JSDoc and ALL_CHECKS type expect Promise.

The function manually wraps return values in Promise.resolve() to match the async contract. This works but is inconsistent with the other six checks, which are all async. Marking it async would remove the need for manual wrapping and align the style.

Proposed fix
-export function checkHooksInstalled(ctx) {
+export async function checkHooksInstalled(ctx) {
   try {
     const installer = createHookInstaller();
     const s = installer.getHookStatus(ctx.repoPath);
-    return Promise.resolve(buildHookFinding(s));
+    return buildHookFinding(s);
   } catch (/** `@type` {*} */ err) { // TODO(ts-cleanup): narrow error type
-    return Promise.resolve(internalError('hooks-installed', err));
+    return internalError('hooks-installed', err);
   }
 }

240-254: auditIds.includes(id) is an O(n·m) lookup — fine for small sets but Set would be more idiomatic.

writerIds is already a Set, but the comparison target auditIds is an array, making the .filter(id => !auditIds.includes(id)) check linear per writer. For consistency and clarity, converting auditIds to a Set would match the existing pattern.

Proposed fix
   const writerIds = new Set(ctx.writerHeads.map((h) => h.writerId));
-  const auditIds = auditRefs.map((r) => r.slice(auditPrefix.length)).filter((id) => id.length > 0);
-  const missing = [...writerIds].filter((id) => !auditIds.includes(id));
+  const auditIdSet = new Set(auditRefs.map((r) => r.slice(auditPrefix.length)).filter((id) => id.length > 0));
+  const missing = [...writerIds].filter((id) => !auditIdSet.has(id));

CI Bot added 2 commits February 13, 2026 19:34
- Single-pass counting in assemblePayload instead of 3 filter scans
- Error boundary in runChecks so one failing check doesn't crash doctor
- Guard falsy head.sha in collectWriterDates before calling getNodeInfo
- Use Set for auditId lookups instead of Array.includes
- Remove unnecessary cd round-trips in BATS tests (git -C suffices)
- Assert preconditions unconditionally in sort-order test
- Fix CLI_OPTIONS JSDoc type (CliOptions, not DoctorPayload)
- Clarify ESM cache comment in beforeEach
- Fix CHANGELOG BATS scenario enumeration (list all 5)
@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

- Add seed-doctor-graph.js that materializes (creates checkpoint +
  coverage refs) so the "healthy graph" tests pass
- Install hooks in healthy-graph tests so hooks-installed check passes
- Write dangling ref directly to .git/refs/ instead of git update-ref
  (which rejects nonexistent objects in some git versions)
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: 1

🤖 Fix all issues with AI agents
In `@bin/cli/commands/doctor/checks.js`:
- Around line 119-125: In checkCoverageComplete, avoid calling
ctx.persistence.isAncestor with a falsy head.sha; add a null/falsy guard inside
the loop over ctx.writerHeads (e.g., if (!head.sha) continue) before invoking
ctx.persistence.isAncestor(head.sha, coverageSha) so heads without a sha are
skipped (consistent with checkRefsConsistent and collectWriterDates) and will
not cause a CHECK_INTERNAL_ERROR.
🧹 Nitpick comments (2)
bin/cli/commands/doctor/checks.js (1)

351-359: checkHooksInstalled is not async but declared to return Promise<DoctorFinding>.

Every other check function is async, naturally returning a Promise. This one manually wraps results in Promise.resolve. Making it async would be more consistent and slightly simpler.

Suggested refactor
-export function checkHooksInstalled(ctx) {
+export async function checkHooksInstalled(ctx) {
   try {
     const installer = createHookInstaller();
     const s = installer.getHookStatus(ctx.repoPath);
-    return Promise.resolve(buildHookFinding(s));
+    return buildHookFinding(s);
   } catch (/** `@type` {*} */ err) { // TODO(ts-cleanup): narrow error type
-    return Promise.resolve(internalError('hooks-installed', err));
+    return internalError('hooks-installed', err);
   }
 }
test/unit/cli/doctor.test.js (1)

197-211: Sort-order test: nodeExists(false) affects multiple checks.

Setting nodeExists to always return false will cause failures in both checkRefsConsistent and probeAuditRefs (audit dangling detection). This is fine for testing sort order since the test only asserts that fail findings precede ok findings, but be aware that if the sort logic or finding shapes change, this broad mock could make the test fragile.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

- Add _run_json helper that captures stdout only (BATS 1.8+ merges
  stderr into $output, corrupting JSON when git emits diagnostics)
- Remove install-hooks from healthy-graph tests (template path doesn't
  resolve inside Docker container)
- Relax healthy-graph assertion: check fail==0 in JSON instead of
  exit code 0 (hooks-installed warning is expected without template)
- Human output test checks content only, not exit code
@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

createHookInstaller() resolved templateDir to <root>/hooks/ which does
not exist. The actual template lives at src/hooks/post-merge.sh.
Fix the path to resolve to <root>/src/hooks/.

Restore BATS tests to properly install hooks and assert health == ok.
Keep _run_json helper for stderr isolation in JSON-parsing tests.
@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

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: 1

🤖 Fix all issues with AI agents
In `@test/bats/helpers/seed-doctor-graph.js`:
- Around line 1-5: The file-level doc comment in seed-doctor-graph.js
incorrectly states "and installs hooks" even though the script does not install
hooks; update the header comment (the top block comment in seed-doctor-graph.js)
to remove or reword the "installs hooks" clause so it accurately reflects
behavior (e.g., "Creates patches, materializes (checkpoint + coverage)." or note
that hooks are installed by the BATS test) so the comment matches the actual
implementation.

CI Bot added 2 commits February 13, 2026 22:10
- checkCoverageComplete: null-sha writer heads reported as missing
  (not silently skipped) with regression test
- checkClockSkew: regression test for null-sha guard in collectWriterDates
- checkHooksInstalled: made async (await Promise.resolve pattern)
- Sort-order test: exercises all three status tiers with targeted mocks;
  asserts full three-key comparator (status > impact > id)
- seed-doctor-graph.js: removed stale "installs hooks" doc comment
- CHANGELOG: added Fixed section for v10.13.0
Add Record<string,number> annotations for STATUS_ORDER/IMPACT_ORDER
maps and cast findings to any for indexing. Cast checkCoverageComplete
return to any for evidence.missingWriters access.
@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

@flyingrobots: I'll review the changes in this PR now! 🥕

✅ 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.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

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: 1

🤖 Fix all issues with AI agents
In `@bin/cli/commands/doctor/checks.js`:
- Around line 67-101: The OK message in checkRefsConsistent wrongly uses
allRefs.length even when some writer heads have null sha; update the logic to
count only refs with a non-null sha (e.g., compute checkedCount by filtering
allRefs for entries with sha or build allRefsFiltered =
ctx.writerHeads.map(...).filter(r => r.sha)) and use that checkedCount in the
success message and any related text so the message accurately reflects how many
refs were actually verified; leave the dangling-ref detection loop and findings
unchanged.
🧹 Nitpick comments (3)
bin/cli/commands/doctor/checks.js (2)

355-363: Unnecessary await Promise.resolve(…) wrapping.

buildHookFinding and internalError are synchronous. In an async function, simply returning a value already wraps it in a resolved promise. The await Promise.resolve(…) pattern adds noise without benefit.

Proposed simplification
 export async function checkHooksInstalled(ctx) {
   try {
     const installer = createHookInstaller();
     const s = installer.getHookStatus(ctx.repoPath);
-    return await Promise.resolve(buildHookFinding(s));
+    return buildHookFinding(s);
   } catch (/** `@type` {*} */ err) { // TODO(ts-cleanup): narrow error type
-    return await Promise.resolve(internalError('hooks-installed', err));
+    return internalError('hooks-installed', err);
   }
 }

43-62: ClockAdapter.global() and new HealthCheckService(…) instantiated on every invocation.

This is fine for a diagnostic command that runs once, but worth noting: checkRepoAccessible creates a fresh HealthCheckService and clock adapter each call. If the check registry ever supports re-runs or batching, consider injecting these via ctx.

test/unit/cli/doctor.test.js (1)

110-156: Healthy-graph test doesn't verify CLOCK_SYNCED or AUDIT_OK codes.

Lines 150-155 assert the presence of REPO_OK, REFS_OK, COVERAGE_OK, CHECKPOINT_OK, and HOOKS_OK but omit CLOCK_SYNCED and AUDIT_OK. Since the mock has only one writer, checkClockSkew returns CLOCK_SYNCED (skipped path), and checkAuditConsistent returns AUDIT_OK — both would pass if asserted. Consider adding them for completeness.

     expect(codes).toContain(CODES.CHECKPOINT_OK);
     expect(codes).toContain(CODES.HOOKS_OK);
+    expect(codes).toContain(CODES.CLOCK_SYNCED);
+    expect(codes).toContain(CODES.AUDIT_OK);

CI Bot added 2 commits February 14, 2026 00:15
checkRefsConsistent now filters writer heads with null sha into a
separate `checkable` array so the OK message accurately reports how
many refs were actually verified. Adds regression test confirming the
count excludes null-sha entries.
@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

@flyingrobots: I'll review the changes now! 🥕

✅ 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.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

- Remove redundant `await Promise.resolve()` wrapping in checkHooksInstalled
- Add CLOCK_SYNCED and AUDIT_OK assertions to healthy-graph test
- Update CHANGELOG to reflect simplified async pattern
@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

@flyingrobots: I'll review the changes now! 🥕

✅ 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.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

Root causes:
- collectWriterHeads crashed when readRef encountered a ref pointing
  to a missing object (git show-ref exits 128, not 1, for dangling refs)
- seed-doctor-graph.js called materialize() but not createCheckpoint()
  or syncCoverage(), so doctor always found missing checkpoint/coverage
- _run_json BATS helper had status capture bug: `|| true` made $?
  always 0, masking real exit codes

Fixes:
- collectWriterHeads now catches readRef failures and includes the
  writer head with sha=null for downstream checks to report
- checkRefsConsistent reports null-sha heads as REFS_DANGLING_OBJECT
- seed-doctor-graph.js calls createCheckpoint() + syncCoverage()
- _run_json properly captures exit code via local variable
@flyingrobots
Copy link
Member Author

@coderabbitai review please 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

@flyingrobots: I'll review the changes now! 🥕

✅ 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.

@github-actions
Copy link

Release Preflight

  • package version: 10.13.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.13.0, release workflow will publish.

@flyingrobots flyingrobots merged commit 685f1bc into main Feb 14, 2026
7 checks passed
@flyingrobots flyingrobots deleted the doctor branch February 14, 2026 09:05
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: 1

🤖 Fix all issues with AI agents
In `@bin/cli/commands/doctor/index.js`:
- Around line 151-174: The duration timer checkStart is declared inside the try
so the catch path reports 0; move hoisting of the start time by declaring and
assigning checkStart = Date.now() before the try that calls check.fn(ctx), then
inside both the try (after await) and the catch compute checkDuration =
Date.now() - checkStart (keeping the existing checkDuration variable) so that
findings pushed in both the success path (via normalizeResult loop) and the
failure push include the actual elapsed time; ensure checksRun++ behavior is
preserved.
🧹 Nitpick comments (2)
bin/cli/commands/doctor/checks.js (2)

204-211: Consider warn instead of ok when checkpoint age is unknown.

When ageHours is null (date missing or unparseable), the finding is reported as ok with "age unknown." Since the freshness policy can't actually be verified, warn with CHECKPOINT_STALE (or a dedicated code) would be more defensive — a checkpoint with an unparseable date is unusual and worth surfacing.


239-253: Silent skip when audit ref resolves to null sha.

If readRef returns null for a listed audit ref (Line 241–242), the ref is silently skipped with no finding. Unlike writer heads in checkRefsConsistent (which report REFS_DANGLING_OBJECT for null shas), an unresolvable audit ref produces no diagnostic output. Consider emitting an AUDIT_DANGLING finding here for consistency.

Proposed fix
     const sha = await ctx.persistence.readRef(ref);
     if (!sha) {
-      continue;
+      findings.push({
+        id: 'audit-consistent', status: 'warn', code: CODES.AUDIT_DANGLING,
+        impact: 'data_integrity',
+        message: `Audit ref ${ref} points to a missing or unreadable object`,
+        evidence: { ref },
+      });
+      continue;
     }

Comment on lines +151 to +174
let checkDuration;
try {
const checkStart = Date.now();
const result = await check.fn(ctx);
checkDuration = Date.now() - checkStart;
checksRun++;

const resultArray = normalizeResult(result);
for (const f of resultArray) {
f.durationMs = checkDuration;
findings.push(f);
}
} catch (/** @type {*} TODO(ts-cleanup): narrow error type */ err) {
checkDuration = checkDuration ?? 0;
checksRun++;
findings.push({
id: check.id,
status: 'fail',
code: CODES.CHECK_INTERNAL_ERROR,
impact: 'data_integrity',
message: `Internal error in ${check.id}: ${err?.message || String(err)}`,
durationMs: checkDuration,
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

checkStart is block-scoped to try, so errored checks always report durationMs: 0.

const checkStart at Line 153 is inside the try block and inaccessible in catch. When check.fn(ctx) throws, checkDuration falls through to ?? 0 at Line 164 instead of reflecting actual elapsed time.

Proposed fix — hoist checkStart above try
     let checkDuration;
+    const checkStart = Date.now();
     try {
-      const checkStart = Date.now();
       const result = await check.fn(ctx);
       checkDuration = Date.now() - checkStart;
       checksRun++;
@@ ..
     } catch (/** `@type` {*} TODO(ts-cleanup): narrow error type */ err) {
-      checkDuration = checkDuration ?? 0;
+      checkDuration = Date.now() - checkStart;
       checksRun++;
📝 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
let checkDuration;
try {
const checkStart = Date.now();
const result = await check.fn(ctx);
checkDuration = Date.now() - checkStart;
checksRun++;
const resultArray = normalizeResult(result);
for (const f of resultArray) {
f.durationMs = checkDuration;
findings.push(f);
}
} catch (/** @type {*} TODO(ts-cleanup): narrow error type */ err) {
checkDuration = checkDuration ?? 0;
checksRun++;
findings.push({
id: check.id,
status: 'fail',
code: CODES.CHECK_INTERNAL_ERROR,
impact: 'data_integrity',
message: `Internal error in ${check.id}: ${err?.message || String(err)}`,
durationMs: checkDuration,
});
}
let checkDuration;
const checkStart = Date.now();
try {
const result = await check.fn(ctx);
checkDuration = Date.now() - checkStart;
checksRun++;
const resultArray = normalizeResult(result);
for (const f of resultArray) {
f.durationMs = checkDuration;
findings.push(f);
}
} catch (/** `@type` {*} TODO(ts-cleanup): narrow error type */ err) {
checkDuration = Date.now() - checkStart;
checksRun++;
findings.push({
id: check.id,
status: 'fail',
code: CODES.CHECK_INTERNAL_ERROR,
impact: 'data_integrity',
message: `Internal error in ${check.id}: ${err?.message || String(err)}`,
durationMs: checkDuration,
});
}
🤖 Prompt for AI Agents
In `@bin/cli/commands/doctor/index.js` around lines 151 - 174, The duration timer
checkStart is declared inside the try so the catch path reports 0; move hoisting
of the start time by declaring and assigning checkStart = Date.now() before the
try that calls check.fn(ctx), then inside both the try (after await) and the
catch compute checkDuration = Date.now() - checkStart (keeping the existing
checkDuration variable) so that findings pushed in both the success path (via
normalizeResult loop) and the failure push include the actual elapsed time;
ensure checksRun++ behavior is preserved.

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