Skip to content

feat(registry): live-reload + supervisor reconcile (PRD-005) — fix onboarding fleet-green deadlock - #20

Merged
thenotoriousllama merged 4 commits into
mainfrom
feat/prd-005-registry-live-reload
Jul 12, 2026
Merged

feat(registry): live-reload + supervisor reconcile (PRD-005) — fix onboarding fleet-green deadlock#20
thenotoriousllama merged 4 commits into
mainfrom
feat/prd-005-registry-live-reload

Conversation

@thenotoriousllama

@thenotoriousllama thenotoriousllama commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Doctor read its supervised-daemon registry only once, at boot. When onboarding installs doctor first (booting it on a hive-only registry), honeycomb and nectar register their entries after doctor has already booted, so doctor never supervises them — deadlocking hive's "Bringing the fleet up green" gate (which requires honeycomb, V1_REQUIRED_PEERS) until the machine reboots. Multiple users hit this on fresh irm | iex / curl | sh installs.

This PR makes doctor re-read the registry at runtime and reconcile its live supervisor set — without a reboot and without any cross-product coordination (product installers are unchanged). It is the durable fix for the onboarding deadlock and also fixes the residual case where a daemon registered after doctor boots stays unsupervised until reboot.

Implements PRD-005 (library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/).

What changed

  • src/registry-reload.ts (new) — a mtime-gated periodic re-resolve of both registry locations (fleet ~/.apiary/registry.json + legacy ~/.honeycomb/doctor.daemons.json). Unchanged registry ⇒ strict no-op (a single stat). A torn/malformed read preserves the live set (never falls back to the primary, never advances the observed mtimes), records needs-attention, and recovers when the file parses again.
  • src/registry-reconcile.ts (new) — diff the fresh entry list against the live set: add (build + arm + startup-grace), remove (supervisor.stop() only — no process kill, no shard delete; zero filesystem I/O by construction), rebuild a changed entry (state shard persists), no-op an identical one. honeycomb (primary) is never dropped on a transient omission and is re-pointed on rebuild so the process-global surfaces never dangle. The telemetry poll loop's entry set is kept in lockstep via the existing PollLoop.reload seam.
  • src/compose/index.tsbuilt[] → mutable Map<name, BuiltDaemon>, reassignable primary, an armDaemon seam, reload loop armed in start() and disarmed first in stop(); status-page / exposed getters read the live map.
  • src/config.tsDOCTOR_REGISTRY_RELOAD_INTERVAL_MS (default 2000 ms).

Zero new runtime dependencies (Node built-ins only); doctor's can't-crash invariant preserved (every new async loop/seam swallows all exceptions).

Verification

  • npm run ci (typecheck + vitest): 832 tests passing, 69 files, zero pre-existing tests broken. npm run build (tsc && esbuild) succeeds.
  • package.json dependencies unchanged: {}.

Wave plan (executed via /the-smoker)

Wave Bee Model Outcome
1 typescript-node-worker-bee opus Implemented 005a + 005b in full (one cohesive compose/index.ts change); 24 new tests. CI green.
2 security-worker-bee opus Clean — 0 Critical, 0 High. Added a regression test proving a poisoned registry (link-local healthUrl + traversal telemetryDbPath) is neutralized on the reload path.
3 quality-worker-bee opus PASS — all 28 ACs independently VERIFIED, no PARTIAL/FAIL.

Model routing per the model-comparison rubric: a subtle concurrency + can't-crash composition-root refactor and its security/quality close-out → opus (deep reasoning, autonomous multi-file, top code quality).

Close-out results

  • Security (qa/2026-07-12-security-audit.md): 0 Critical / 0 High. Reload resolves through the exact production coercion path boot uses — loopback-only healthUrl SSRF gate, assertWithinBase telemetryDbPath containment — so no bypass; reconcile performs no fs writes and no process signals. Only pre-existing Lows at boot parity (documented as follow-ups: pidPath containment, a daemons[] length cap, splicing settled supervisorRuns).
  • Quality (qa/2026-07-12-qa-report.md): all 28 ACs VERIFIED with meaningful tests; strict no-op on unchanged registry, malformed-reload preserves the live set, removal does zero fs I/O, primary invariant holds, telemetry coherence via PollLoop.reload. Two beyond-AC suggestions (S-1 the reload signature is spec-mandated by a-AC-3; S-2 an integration test for the armDaemon running-branch) — neither an AC gap.

Ordering was correct (security before quality).

AC Ledger — 28/28 VERIFIED

Module (index): AC-1 add honeycomb within interval ✅ · AC-2 onboarding order converges, gate greens ✅ · AC-3 removal stops+drops, no process kill ✅ · AC-4 unchanged = strict no-op ✅ · AC-5 malformed preserved + needs-attention, resumes ✅ · AC-6 primary stays bound ✅ · AC-7 telemetry coherence ✅ · AC-8 failures swallowed, never crashes ✅ · AC-9 Node built-ins only ✅

005a — reload trigger: a-AC-1 stat both locations ✅ · a-AC-2 unchanged no-op ✅ · a-AC-3 changed → resolve + onEntries ✅ · a-AC-4 malformed: no reconcile, mtimes not advanced, logged once ✅ · a-AC-5 recovers on next healthy read ✅ · a-AC-6 never falls back to primary ✅ · a-AC-7 interval knob honored ✅ · a-AC-8 stop() idempotent, injected clock ✅ · a-AC-9 exceptions swallowed ✅

005b — reconcile: b-AC-1 add build+arm+grace ✅ · b-AC-2 added in status + telemetry ✅ · b-AC-3 onboarding convergence ✅ · b-AC-4 remove stop+drop ✅ · b-AC-5 no process kill, shards kept ✅ · b-AC-6 primary rebuild re-points surfaces ✅ · b-AC-7 transient omission keeps primary ✅ · b-AC-8 identical untouched ✅ · b-AC-9 changed field rebuilds one only ✅ · b-AC-10 per-supervisor fault isolated ✅

Full ledger: library/ledger/EXECUTION_LEDGER-prd-005.md.

Notes

  • Scoped to the doctor submodule; the superproject submodule-pointer bump follows on merge.
  • No installer, install-order, or hive health-gate change — this makes the ordering irrelevant to the bug rather than editing it (superseding the fragile "reorder the queue" band-aid).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added runtime registry live-reload for supervised daemons (no restart required).
    • Automatically adds, updates, or removes supervisors to match the latest registry.
    • Introduced a configurable reload interval (default: 2 seconds).
    • Status/telemetry now stay in sync with the live supervised set.
  • Bug Fixes
    • Resolved onboarding hangs for daemons registered after startup.
    • Improved handling of malformed/unavailable registry files with safe recovery.
  • Quality
    • Added end-to-end and unit coverage for reload, reconciliation, recovery, and security validation.

Doctor read its supervised-daemon registry only once, at boot. When onboarding
installs doctor first (booting it on a hive-only registry), honeycomb and nectar
register AFTER doctor booted and were never supervised — deadlocking hive's
"Bringing the fleet up green" gate (which requires honeycomb) until a reboot.

Doctor now re-reads the registry at runtime and reconciles its live supervisor
set without a reboot and without any cross-product coordination:

- src/registry-reload.ts: a mtime-gated periodic re-resolve of BOTH registry
  locations (fleet + legacy). Unchanged => strict no-op (single stat). A torn/
  malformed read preserves the live set (never falls back to the primary, never
  advances mtimes) and records needs-attention; recovers when the file parses.
- src/registry-reconcile.ts: diff the fresh entry list against the live set —
  add (build+arm+grace), remove (stop only; no process kill, no shard delete —
  zero fs I/O by construction), rebuild a changed entry, no-op an identical one.
  honeycomb (primary) is never dropped on a transient omission and is re-pointed
  on rebuild so the process-global surfaces never dangle. Telemetry poll loop
  entry set kept in lockstep via the existing PollLoop.reload seam.
- src/compose/index.ts: built[] -> mutable Map, reassignable primary, armDaemon
  seam, reload loop armed in start()/disarmed first in stop().
- src/config.ts: DOCTOR_REGISTRY_RELOAD_INTERVAL_MS (default 2000ms).

Zero new runtime dependencies (Node built-ins only); can't-crash preserved.
All 28 acceptance criteria (module + 005a + 005b) implemented, tested, and
independently verified; security audit clean (0 Critical/High). CI: 832 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The daemon registry is re-read periodically using mtime gating. Valid changes reconcile live supervisors, telemetry entries, and status rows while preserving the primary supervisor. Configuration, tests, QA records, security audit results, and the package version were updated.

Changes

Registry live-reload and reconciliation

Layer / File(s) Summary
Contracts, configuration, and delivery records
CHANGELOG.md, package.json, src/config.ts, library/ledger/*, library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/*
PRD-005 behavior, acceptance criteria, release metadata, QA records, security results, and the configurable 2000 ms reload interval are documented.
Mtime-gated registry reload loop
src/registry-reload.ts, tests/registry-reload.test.ts
Both registry locations are monitored; changed entries are resolved, malformed reads preserve the live set, failures are deduplicated, recovery is reported, and loop lifecycle is controllable.
Supervisor reconciliation and Doctor lifecycle wiring
src/registry-reconcile.ts, src/compose/index.ts
Supervisors are added, removed, rebuilt, or left unchanged by entry differences; primary handling, telemetry synchronization, status rendering, and start/stop lifecycle wiring are updated.
Reconciliation and integration tests
tests/registry-reconcile.test.ts, tests/compose/registry-reload-reconcile.test.ts
Tests cover onboarding, removals, updates, primary preservation, no-churn behavior, malformed recovery, telemetry/status convergence, security coercion, and isolated failures.

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

Sequence Diagram(s)

sequenceDiagram
  participant RegistryReloadLoop
  participant RegistryResolver
  participant SupervisorReconciler
  participant DoctorState
  participant TelemetryPollLoop
  RegistryReloadLoop->>RegistryResolver: resolve changed registry
  RegistryResolver-->>RegistryReloadLoop: DaemonEntry list
  RegistryReloadLoop->>SupervisorReconciler: reconcile entries
  SupervisorReconciler->>DoctorState: update supervisors and primary
  SupervisorReconciler->>TelemetryPollLoop: update polled entries
Loading

Possibly related issues

Possibly related PRs

  • legioncodeinc/doctor#6 — Related telemetry polling and SSE behavior uses the registry-derived daemon set updated by this change.
  • legioncodeinc/doctor#11 — Provides the registry-resolution and malformed-registry diagnostic contract reused by live reload.

Poem

A rabbit watched the registry glow,
New daemons hopped in rows.
Old ones left without a fright,
The primary stayed tucked in tight.
Telemetry hummed, loops ran true—
“A timely carrot!” cried the crew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.84% which is insufficient. The required threshold is 80.00%. 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 clearly and concisely summarizes the main change: registry live-reload and supervisor reconciliation to fix the onboarding deadlock.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prd-005-registry-live-reload

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

@thenotoriousllama

Copy link
Copy Markdown
Contributor Author

Approved Release

@thenotoriousllama

Copy link
Copy Markdown
Contributor Author

✅ Approved — applying the minor bump. The release-gate check will go green; it publishes on merge.

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-qa-report.md`:
- Line 129: Update the fenced code block in the QA report to include the text
language identifier, using the requested ```text opening fence while preserving
the block contents and closing fence.
- Line 25: Update the correctness statement describing removal behavior to
replace the “zero fs I/O” claim with the precise guarantee that removal performs
no process kill or shard deletion. Apply the same wording change to the
additional occurrence, while preserving the existing telemetry and SQLite-handle
reconciliation behavior.

In
`@library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-security-audit.md`:
- Line 94: Add the text language identifier to the fenced code block in the
security audit document, changing the opening fence to use ```text while
preserving the block’s contents and closing fence.
- Line 72: Correct the remediation summary in the security audit to classify
Low-3 as PR-introduced unless prior behavior is demonstrated. Update the
disposition to explicitly document how Low-3 is handled, and remove the blanket
statement that all findings are pre-existing and deferred; preserve the separate
scope and disposition for Findings 1–2.
- Around line 108-110: Remove the stale “Ordering note” section claiming that no
QA report exists or that quality-worker-bee has not run. Keep the security audit
content intact and ensure the document no longer contradicts the completed QA
report or validation ordering.

In `@src/registry-reconcile.ts`:
- Around line 187-211: The reconcile update path must build and prepare the
replacement before stopping the existing supervisor. In the rebuild logic, call
buildDaemon and armStartupGrace first; only after that succeeds stop
existing.supervisor, arm the replacement, update current, and call setPrimary
for the primary daemon. Preserve the existing warning behavior while ensuring a
failed build leaves the current active daemon and primary reference unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17efc5ca-8a69-4dd1-9940-1a3722803790

📥 Commits

Reviewing files that changed from the base of the PR and between 9f4badc and a9bd415.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • CHANGELOG.md
  • library/ledger/EXECUTION_LEDGER-prd-005.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/prd-005-registry-live-reload-and-supervisor-reconcile-index.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/prd-005a-registry-live-reload-and-supervisor-reconcile-reload-trigger.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/prd-005b-registry-live-reload-and-supervisor-reconcile-supervisor-reconcile.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/.gitkeep
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-qa-report.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-security-audit.md
  • package.json
  • src/compose/index.ts
  • src/config.ts
  • src/registry-reconcile.ts
  • src/registry-reload.ts
  • tests/compose/registry-reload-reconcile.test.ts
  • tests/registry-reconcile.test.ts
  • tests/registry-reload.test.ts

Comment thread src/registry-reconcile.ts
Code:
- registry-reconcile.ts UPDATE path builds the replacement BEFORE stopping the
  old supervisor, so a throwing buildDaemon leaves the current (still-running)
  supervisor and the primary reference intact instead of a stopped-but-referenced
  daemon (strengthens b-AC-10). +2 regression tests.
- compose armDaemon: run-promises now self-splice from supervisorRuns on settle
  (.finally), bounding the join list under sustained add/remove reload churn
  (security audit Low-3 — PR-introduced, now fixed).

Docs (QA + security audit):
- narrowed the "zero fs I/O" claim to the precise "no process kill or shard
  deletion" guarantee (reconcile still triggers a telemetry handle close).
- added `text` language identifiers to fenced blocks (markdownlint MD040).
- reclassified security Low-3 as PR-introduced-and-remediated.
- corrected the stale "quality-worker-bee has not run" ordering note.

npm run ci green (834 tests, typecheck clean); dependencies still {}.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thenotoriousllama

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — all six comments addressed in 5dc5f4b:

Code

  • registry-reconcile.ts UPDATE path (build before stop): the replacement is now built first; the old supervisor is stopped and swapped only after the new one is ready. A throwing buildDaemon now leaves the current (still-running) supervisor and the primary reference intact — no dead-but-referenced daemon. Added 2 regression tests (non-primary + primary update-build-throw).
  • armDaemon settled-promise growth (security Low-3): run-promises now self-splice from supervisorRuns on settle (.finally), bounding the join list under sustained add/remove churn. Reclassified in the audit as PR-introduced and remediated (not pre-existing).

Docs

  • Narrowed the "zero fs I/O" claim to the precise "no process kill or shard deletion" (reconcile still triggers a telemetry SQLite-handle close via PollLoop.reload).
  • Added text language identifiers to the two flagged fenced blocks (MD040).
  • Removed the stale "quality-worker-bee has not run" ordering note — the completed QA report (qa/2026-07-12-qa-report.md) is now referenced.

npm run ci green (834 tests, typecheck clean); dependencies still {}.

@thenotoriousllama
thenotoriousllama merged commit 5489851 into main Jul 12, 2026
7 checks passed
@thenotoriousllama
thenotoriousllama deleted the feat/prd-005-registry-live-reload branch July 12, 2026 16:48

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

Caution

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

⚠️ Outside diff range comments (1)
src/compose/index.ts (1)

745-763: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Self-splice boot-seeded supervisor runs too src/compose/index.ts:1028-1050start() seeds supervisorRuns with bare supervisor.start() promises, so a daemon that boots and is later removed/rebuilt via reload leaves its original settled promise in the array until shutdown. Share the same track-and-splice helper here; the reload test doesn’t cover this boot-seeded removal path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compose/index.ts` around lines 745 - 763, The boot path in start() still
adds bare supervisor.start() promises to supervisorRuns, leaving settled
references after reload removal. Reuse the same tracking and cleanup helper used
by armDaemon so boot-seeded runs are caught, tracked, and removed from
supervisorRuns in finally, while preserving shutdown snapshot behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/compose/index.ts`:
- Around line 745-763: The boot path in start() still adds bare
supervisor.start() promises to supervisorRuns, leaving settled references after
reload removal. Reuse the same tracking and cleanup helper used by armDaemon so
boot-seeded runs are caught, tracked, and removed from supervisorRuns in
finally, while preserving shutdown snapshot behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5183451d-8bc8-47e8-b7e1-b9c3bc58892c

📥 Commits

Reviewing files that changed from the base of the PR and between a9bd415 and 5dc5f4b.

📒 Files selected for processing (6)
  • library/ledger/EXECUTION_LEDGER-prd-005.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-qa-report.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-security-audit.md
  • src/compose/index.ts
  • src/registry-reconcile.ts
  • tests/registry-reconcile.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • library/ledger/EXECUTION_LEDGER-prd-005.md
  • library/requirements/in-work/prd-005-registry-live-reload-and-supervisor-reconcile/qa/2026-07-12-qa-report.md
  • src/registry-reconcile.ts

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

@thenotoriousllama Thanks for the detailed rundown — this all looks solid:

  • Build-before-stop in registry-reconcile.ts: swapping only after the new supervisor is successfully built is the right invariant here; a throwing buildDaemon no longer leaves a dangling supervisor or corrupts the primary reference. Glad the regression tests cover both the non-primary and primary throw paths.
  • Self-splicing run-promises in armDaemon: bounding supervisorRuns via .finally() closes off the unbounded-growth concern under churn — good catch reclassifying it as PR-introduced/remediated rather than pre-existing.
  • Docs: the narrowed "no process kill or shard deletion" claim is more accurate given the telemetry SQLite-handle close, and the MD040 fences / stale ordering note cleanup look good too.

834/834 green with a clean typecheck and empty dependencies is a strong signal. Nice work closing this out.

(ᵔᴥᵔ)

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants