Skip to content

feat: manage a running offbook instance from any directory (D-032, R-044–R-047) - #19

Merged
nzneit merged 48 commits into
mainfrom
feat/instance-discovery
Aug 20, 2026
Merged

nzneit merged 48 commits into
mainfrom
feat/instance-discovery

Conversation

@nzneit

@nzneit nzneit commented Aug 19, 2026

Copy link
Copy Markdown
Owner

What

Manage a running offbook instance from any directory (D-032, R-044–R-047). Previously every management verb had to run from the directory where offbook up was started — an undocumented cwd premise that confused first-light and daily use.

  • Instance identity (R-044): a 128-bit launch token (constant across --watch respawns) + host recorded in the runfile and boot file; GET /v1/server serves the identity (plain 404 on pre-D-032 servers); probeServer classifies answerers as server / legacy / silent.
  • Discovery + resolution (R-045): a machine-local pointer registry under ${OFFBOOK_STATE_DIR:-${XDG_STATE_HOME:-~/.local/state}/offbook}/instances/, a cwd-first-then-registry resolver implementing a 10-row instance state table, guarded compare-and-act mutation at every deletion site, a deletion law (a live-pid record is only ever skipped, never reaped), and self-heal for dangling pointers.
  • Verb surface (R-046): all management verbs (down, status, logs, topics, specs update, doctor) resolve through one front door; up [dir] starts a project's instance without cd; refusals print paste-ready --run-dir selector tables with a 0/1/2 exit-code contract and typed --json envelopes; down gains identity-gated kill-safety (compare-and-signal, token-verified SIGKILL escalation, ESRCH-in-race = success).
  • Docs sweep (R-047): guides, README, adoption spec, and the onboarding skill drop the cwd premise; contracts.md §1a/§5 amended; D-032 recorded with the sweep's grep evidence and the mutation-campaign appendix.

Notable finds along the way

  • A real production bug exposed by a spec-mandated pin: the --watch respawn inherited a deleted launch cwd (posix_spawn resolves the caller's cwd), dying silently with no successor. Fixed with explicit spawn cwd at both spawn sites plus an event-gated handoff.
  • The final whole-branch review caught up's stale-runfile reclaim contradicting the deletion law the branch itself froze into contracts.md (it could delete a wedged or foreign-host instance's records); up now refuses instead, with the dead-pid clear behind the guarded compare-and-clear discipline.
  • Mutation hardening surfaced that probeServer had no test bounding a hang on a listener that accepts but never answers; the retry bound is now pinned.

Verification

  • Full gate set at HEAD, judged by exit code: check-docs 0, lint 0, typecheck 0, demo-app:build 0, full bun test 0 (674 pass / 0 fail).
  • Mutation campaign over the extended scope (D-032 obligation, discharged): engine 99.77%; the four new CLI modules after hardening — guard.ts 100%, runfile.ts 97.25%, resolve.ts 95.14%, registry.ts 93.67%; 68 of 88 undetected mutants killed by discriminating tests (each verified by hand-applying the mutant), the 21 measured equivalents recorded with arguments in the D-032 appendix.
  • State-table rows 1–10 each pinned by a named test (test/instance-discovery.test.ts row map).
  • Tests never touch the real ~/.local/state (suite-wide OFFBOOK_STATE_DIR pin in test/preload.ts; the registry throws without an injected state dir).

Process

22-task plan executed subagent-driven: fresh implementer per task, per-task spec+quality review, fix waves with re-reviews, a final whole-branch review (its three Important findings fixed and re-verified, including one exit-code adjudication: wrong-host refusals on up/demo --serve exit 2 like every verb), and a mutation-hardening wave. Final review verdict: ready to merge, no residual concerns.

nzneit added 30 commits August 18, 2026 21:02
nzneit added 15 commits August 19, 2026 12:48
- adoption.md $10 staleness: merge the doubled 'reads ... boot record'
  clauses into one (resolve, then read services.yaml via the RESOLVED
  run dir's boot record).
- adoption.md $10 + SKILL.md port-conflict recipe: restore the trailing
  '; check the others separately if they persist' clause to the quoted
  unproven-owner hint, matching doctor.ts/index.ts byte-for-byte.
…y what it checked

Two D-032 defects found by the final whole-branch review.

launchDetached reclaimed on `existing.live === false`, which is also true
for an alive pid with a silent control port (a wedged instance, or one
inside its 30s boot window) and never looked at `host`; the clear was
unguarded and its note ("pid N is gone") was false whenever the pid was
alive. So the natural retry `offbook up` deleted a wedged instance's
runfile and pointer -- breaking M12's promise that `offbook down` still
stops it -- and on a shared network home an `up` from the wrong machine
deleted a foreign live instance's records. Now: row 10 refuses with M10,
row 3 refuses with the new M23, and only row 5 (provably dead, local)
reclaims, through guarded site #2. `up`'s refusals stay exit 1, matching
its already-running convention (an operational error, not a selector
refusal).

`topics --json --run-dir <dir>` with nothing live printed M11: "no runfile
in .offbook, and nothing else is running on this machine". Under an
explicit --run-dir the resolver deliberately never scans the registry, so
both clauses were unchecked claims -- and this is the exact command the
onboarding skill's step 6 mandates as the first-light check. cmdTopics now
mirrors targetFor's explicit-path carve-out (extracted as runDirRefusal),
naming the directory actually checked. The human demo fallback (M0 gate
ii) is untouched.
…dentity claims

contracts.md §5 carried two clauses that disagreed: P7's "a stale runfile
(pid dead, or the control port silent) is auto-reclaimed" against D-032's
deletion law ("a live-pid record is only ever skipped"). Contracts is
canonical, so P7 is amended to the law it contradicted, and the
supersession is recorded as a one-sentence appendix on D-032 (the existing
grep-evidence fence is untouched).

D-032's release note and daily-loop.md both claimed `--run-dir` consumers
see behavior byte-identical to pre-D-032. True of live-instance output;
not true of refusals, which now carry the --json envelope (and, after this
round, the run-dir-qualified wording inside it). Both now claim pinned
targeting semantics and point scripted consumers at the exit code and
error.code instead of the wording. R-043's body loses the stale
"run-dir-qualified message" phrasing for the same reason.
The 2026-08-19 full campaign left 88 undetected mutants across resolve.ts,
registry.ts and runfile.ts. 67 are killed here with behavioral tests; 21 are
argued (and measured) equivalent — see .superpowers/sdd/mutation-kill-report.md
for the per-mutant disposition.

New behavior pinned: row 10 inertness on both the cwd and the registry path
(a foreign-host runfile is never reclaimed and sets foreignSeen), the boot
file's naming of skipped instances and its fallbacks, token-over-runDir
proof, --run-dir preferring the dir's own runfile, a silent cwd instance not
hiding the registry's live one, row 9 refusing to heal from a mismatched
runDir or a foreign host and re-recording the canonical pointer when it does
heal, the tiebreak running on the served projectDir with stage 2 strict, the
demo note naming exactly the passed-over demo, attributeCtrlPort's negatives,
registry shape/name/dedupe rules, and runfile's shape check, clear
idempotence and probe classifications.
…ation

The focused verification run (95.44% overall) left 22 survivors. 21 are the
accepted equivalents argued in the hardening round; the 22nd, runfile.ts:138
(probeServer's AbortSignal.timeout), was never triaged — it was killed in the
first campaign and only surfaced here. It is a real gap, not an equivalent:
without the signal the identity probe never gives up on a listener that
accepts and never answers, so every verb that resolves an instance hangs with
it. Killed with a bound-not-hung test against a raw accept-only listener.

Records: D-032's obligation (1) is marked discharged in place (grep-evidence
fence untouched) and a mutation appendix carries both score sets and all 21
accepted equivalents with their arguments; AGENTS.md's status sentence
replaces the obligation clause with the discharged outcome.
…view ruling)

M10 is pinned exit-2 in the catalog and D-032 enumerates wrong-host under
the refusal contract; shipping the same catalog id with two exit codes
per verb reintroduces the drift class the catalog exists to prevent.
launchDetached returns a wrong-host sentinel both call sites map to
exit 2; the demo --serve flow is now asserted, not just intended.
M23 (already-running family) stays operational at exit 1.
@github-actions

github-actions Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

mutation gate: pass (score 100.00, break 100)

Mutated 4 file(s): src/cli/guard.ts, src/cli/registry.ts, src/cli/resolve.ts, src/cli/runfile.ts
Mutants: 450 killed, 1 timeout, 0 survived, 0 no-coverage; 32 ignored, 0 errored, 0 pending.

nzneit added 3 commits August 20, 2026 02:23
…independent

The gate's second CI run flipped the registry dedupe MethodExpression
mutant from killed to survived: its killing test discriminated only
when readdir presented the non-canonical twin first (tmpfs does, ext4
did not). scanPointers now sorts its listing; the dedupe is pinned in
BOTH scan orders via name-constructed twins plus a return-order pin,
each verified by hand-applying its mutant. The gate's port-collision
nondeterminism (EADDRINUSE crash-kills at concurrency 4, sandbox-orphan
poisoning) is recorded as open intake with the STRYKER_MUTATOR_WORKER
namespacing seam as the recommended fix.
@nzneit
nzneit merged commit 6cc6557 into main Aug 20, 2026
2 checks passed
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