feat(org): the Baton home — a runtime for role continuity chains - #262
Conversation
contracts/org has the kernel: the record spine, the closed kind set, the fold, and every admission law, proven over 86 reachable states. Nothing could run it. This adds cmd/org, the home: chains as JSONL on disk, bodies as content-addressed erasable blobs, appends serialized by an flock over the fold->admit->append critical section, with admission delegated entirely to org.Advance — the home adds no judgment of its own, and a record the kernel refuses exits 1 carrying the kernel's reason id. Verbs map one-to-one onto record kinds. The read side is the point: `org boot` renders a role's re-entry index — charter, held work, a dangling obligation first, liveness derived from the writer's own declared next_due, and the last incarnation's final word — byte-capped for injection, shedding depth but never the headline or an obligation. `org status` is the board across every chain. Two fail-open hook scripts close the loop for a Claude Code session: SessionStart injects the boot index for the role its cwd maps to, and Stop appends a mechanical mark — never a checkpoint, because a record demanded of the working agent is a verb wearing a costume. A mark at the tip renders the next boot degraded, which is the honest state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@claude please review. New tool: cmd/org — runtime over the contracts/org kernel. Worth your attention:
|
|
Claude finished @itsHabib's task in 6m 4s —— View job Review:
|
| field | draft fills | kernel expects |
|---|---|---|
Seq |
state.Seq + 1 |
state.Seq + 1 (checkPosition) |
Prev |
state.Tip |
state.Tip (checkPosition) |
Fence |
state.Fence, advanced to r.Seq for takeover/revoke |
monotone (checkFence) |
Incarnation |
state.Holder for non-minting kinds, empty for minting |
forbidden for charter/attach/takeover, required for everything else |
mints() (line 202) exactly mirrors the kernel's incarnation: forbidden law in validate.go for charter, attach, and takeover — the three kinds whose own digest becomes the identity. Revoke (incarnation: required) is correctly excluded from mints() and receives Incarnation = state.Holder. ✓
Refused record never reaches disk: org.Advance() (line 144) returns an error → function returns before appendLine() at line 146. TestRefusalCarriesKernelReason pins this invariant.
Concurrent serialization: the flock at lock() (line 280) uses syscall.LOCK_EX on a separate lock file (not the chain), so readers never contend with the fsync. TestConcurrentAppendsSerialize (8 writers) validates contiguous sequences.
Exit-code seam (cmd/org/main.go) — CORRECT
if org.RefusalReason(err) != "" {
fmt.Fprintf(stderr, "%v\n", err)
return codeRefused // 1
}
fmt.Fprintf(stderr, "org: %v\n", err)
return codeError // 4The *org.Refusal returned by org.Advance() percolates through appendAndReport() → verb handler → run() without wrapping (it uses errors.As internally via RefusalReason). Refusal exits 1 with the reason id leading the stderr line (e.g. org: work_not_held: …). Non-refusal errors exit 4. TestVerbLoopExitCodes pins both paths. ✓
Hooks — MOSTLY CORRECT, one fail-open gap
Documented fail-open cases — all correct:
| condition | guard |
|---|---|
| missing org binary | command -v "$ORG_BIN" || exit 0 |
| missing jq | command -v jq || exit 0 |
| missing roles.map | [ -f "$MAP" ] || exit 0 |
| no matching prefix | [ -n "$role" ] || exit 0 |
org boot fails |
boot="$(...)" || exit 0 |
| mark fails | … || exit 0 at end of pipeline |
Stop re-entrancy — the stop_hook_active guard at line 28 of stop-mark.sh is correct: if Stop fires again from the mark causing the agent to continue, the hook exits 0 silently.
Stop non-blocking — the mark's flock could block if another writer holds it. The timeout: 10 in the install config is the bound; within 10 s the hook is killed and the session proceeds. Acceptable.
Gap: set -euo pipefail + unguarded jq calls — both hooks do:
set -euo pipefail
...
input="$(cat)"
cwd="$(jq -r '.cwd // empty' <<<"$input")" # ← not guardedIf $input is empty or not valid JSON, jq exits 3. With set -e, the script exits non-zero — violating fail-open for malformed stdin. Claude Code's harness always sends valid JSON in practice, so this is unlikely to fire. But the intent (evidenced by \|\| exit 0 everywhere else) was comprehensive fail-open.
Fix (both hooks, same pattern):
cwd="$(jq -r '.cwd // empty' <<<"$input" 2>/dev/null)" || exit 0Shed loop termination — SOUND
The > 163 guard (render.go line 153) is correct. Trace:
- Excerpt > 163 bytes →
Excerpt = Excerpt[:160] + "…"→ 163 bytes. Returns true. - Next
shed():163 > 163is false → case 1 skipped → case 2 fires (!= ""). Clears excerpt. Returns true. - Next
shed(): excerpt is "". Case 3: nil held list. Returns true. - Next
shed(): no held list. Default: return false. Text()falls back tos[:budget]hard cut.
The prior infinite-loop bug would have been > 160: truncating to 163, then 163 > 160 = true, re-truncating to the same 163 bytes forever. The > 163 fix terminates the truncation case after one application. ✓
What shed never drops: headline (# baton boot — …), charter line, dangling-obligation line — these are always emitted by text() and never touched by shed(). Confirmed by TestBootLeadsWithObligation with Text(220). ✓
One edge note: the hard-cut fallback s[:budget] slices bytes, not runes. A budget landing mid-UTF-8 sequence produces invalid UTF-8 at the tail. Not a correctness problem for the boot injection use case, but worth noting for future i18n robustness.
Blob erasure — CORRECT
The design is sound:
Load()reads only the JSONL chain (viareadChain()+org.Reduce()); it never touches blobs. Erasing a blob cannot break chain folding.Blob()returns(nil, false, nil)onos.IsNotExist— absence is not an error.lastWord()rendersErased: true, whichtext()emits as(body erased)rather than silently omitting the last-word line.
TestBlobRoundTripAndErasure proves the full loop: write blob → read back → erase file → Load() still succeeds → Blob() returns found=false. ✓
Minor issues
cmdBlob wrong variable in error message — main.go:481:
return fmt.Errorf("blob %s is erased or unknown", args[0]) // ← should be s.fs.Arg(0)args[0] is the first element of the raw args slice passed to the handler, which could be a flag (e.g., -state) if the user passes flags before the positional digest. s.fs.Arg(0) is the correct accessor for the first non-flag argument after s.fs.Parse. The check above uses s.fs.Arg(0) correctly; only the error message is wrong. Fix this →
appendLine double close — home.go:259-275:
defer f.Close() // cleanup
...
return f.Close() // propagate close errorStandard Go pattern for propagating close errors while still having a cleanup defer. On the success path, Close() is called twice; the deferred call's error is silently discarded. Functionally safe on Linux (a second close of an already-closed fd returns EBADF, discarded by defer). Idiomatic Go would use defer for cleanup only and return the close error inline with a named return variable, but this is low priority.
Verdict
The two invariants stated in the PR description hold: the home adds no judgment (kernel is the sole gate, verified by tracing the code and the test), and a refused record cannot reach the chain. The exit-code seam is correct. The shed loop terminates soundly. Blob erasure is design-conformant.
The jq fail-open gap in the hooks is the one finding I'd address before ship — the hooks advertise comprehensive fail-open and nearly deliver it; two || exit 0 additions close the gap. The cmdBlob args[0] is a minor error-message bug. Everything else is sound.
The brief's §6 note said dossier task_list omits notes and no CLI verb reads one. Re-verified against the installed binary and source: refuted — task_list returns structured notes, so the write tier can read conclusions back. The correction names the two arguments that leaned on the false premise (hooks #43's substrate-coupling rationale, store-decision finding 2) rather than silently rewriting them. where-this-stands gains a build update: the re-entry slice is no longer a claim — cmd/org and cmd/org-mcp shipped CI-green in PRs #262/#263, with the deliberate deviations recorded where they were made. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every jq call parsing the hook event is now guarded, so an empty or non-JSON stdin exits 0 silently instead of tripping set -e — the hooks advertise comprehensive fail-open and now deliver it. Also the cmdBlob not-found message names the digest argument rather than whatever happened to be first in the raw args slice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@claude re-review on the current head. Round-1 fixes for your findings: every jq call parsing the hook event in both hook scripts is now guarded ( |
|
Claude finished @itsHabib's task in 3m 15s —— View job Re-review:
|
| line | form | guard |
|---|---|---|
| 28 | re-entrancy: [ "$(jq … 2>/dev/null)" = "true" ] && exit 0 |
&& list, non-final position |
| 29 | cwd="$(jq … 2>/dev/null)" || exit 0 |
explicit || exit 0 |
| 30 | session="$(jq … 2>/dev/null)" || exit 0 |
explicit || exit 0 |
| 31 | transcript="$(jq … 2>/dev/null)" || exit 0 |
explicit || exit 0 |
Line 28 warrants a brief note on bash semantics: if jq fails inside $(...) within [ … ], bash's set -e does not trigger because [ … ] is in a non-final position of the && list — per the bash manual's explicit exception for "commands executed in an && or || list except the command following the final && or ||." If jq fails, [ "" = "true" ] evaluates false, exit 0 is not called, and the script falls through to line 29 where || exit 0 fires. The net result is still exit 0 silently. The re-entrancy guard is safe and no additional || exit 0 is needed there. ✓
cmdBlob error message — CLEAN
main.go:483:
return fmt.Errorf("blob %s is erased or unknown", s.fs.Arg(0))args[0] is gone; s.fs.Arg(0) is the correct post-parse positional. ✓
Deferred items
Both acknowledged and confirmed unchanged:
- UTF-8 hard-cut tail (
render.gos[:budget]fallback) — correct to defer; boot index is ASCII-dominant in practice. appendLinedouble-close (home.go) — standard Go pattern; functionally safe on Linux.
Verdict
All P1 findings from round 1 are addressed and verified sound. No new findings. The PR is clean.
|
@claude publish your existing review as a FORMAL GitHub review, pinned to head 4672e11. You already reviewed this PR twice (round 1 found the hook jq fail-open gap and the cmdBlob message; round 2 verified both fixed, verdict clean). Both times the output landed as an issue comment, which gate cannot count: classifyPanel requires a formal review with User.Type=Bot, a COMMENTED/APPROVED/CHANGES_REQUESTED state, and commit_id == the judged head. The PR is parked on missing=[claude] purely because of the delivery mechanism. Run exactly: Do not post an issue comment this time. If the review command fails, say so plainly with the error. |
|
Claude finished @itsHabib's task in 2m 45s —— View job
Formal review submitted. The One note on execution: the Bash sandbox in this environment blocks multi-line quoted strings containing What was posted via the action: The review has |
…collide cmd/org and cmd/org-mcp are on main (#262, #263). The re-entry surface the reconciliation brief called the closest-to-buildable gap is built: charter/attach/assign/claim/yield/checkpoint/boot plus sessionstart-boot and stop-mark hook scripts. It is not wired — settings.json still has only PreToolUse and PostToolUse. That makes the brief's §5 stale and surfaces something sharper. There are now TWO Stop hooks, neither wired, built days apart by different sessions with no reference to each other: cmd/org/hooks/stop-mark.sh appends a mechanical mark to the role's chain, and hooks PR #42's stop-discharge.sh appends what a session did to the dossier tasks it touched. Both refuse to ask the model, and their headers reach that conclusion independently in different words — which is the strongest signal in this corpus and also a duplication that will double-write if both are installed. The handoff leads with that as the deliverable rather than the vocabulary question. It also carries the two corrections this session earned: the notes error (a skip_serializing_if field sampled at .[0] reads exactly like a field that does not exist) and the mktemp BSD/GNU divergence that passed every local run and failed every CI one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cisions to fold Independent reconciliation of the whole ownership-continuity lineage at 2026-08-26 heads: what #245 designed, what #246/#248/#262/#263 shipped through the governed path, what #265/#266 measure, what hooks#42/#43 and drive#47 add, what cc-skills#29 proves in fixture, and what remains hypothesis. Every major claim classified on the honest rung ladder; duplications and missing joins named; D1-D10 written to be accepted, amended, or struck in place. Indexed from vision.md's document table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
contracts/org(landed in #246/#248) is a complete kernel with nothing to run it. This PR addscmd/org— the Baton home: the runtime that keeps role continuity chains on disk, lets sessions act as roles, and closes the boot/discharge loop the org TDD (#245) calls the re-entry axis.What this adds
internal/home— mechanism only: one JSONL chain per role under$ORG_STATE, content-addressed erasable bodies underblobs/, appends serialized by an flock over the fold→admit→append critical section. Admission isorg.Advance; the home adds no judgment, and a kernel refusal exits 1 with the kernel's reason id (dangling_claim,work_not_held, …) — the exit-code seam other tools can compose on.org boot— the re-entry index: charter line, held work, a predecessor's dangling obligation rendered first, liveness derived from the writer's own declarednext_due, and the last incarnation's final word. Byte-capped (default 2048) by shedding depth (excerpt, held list) but never the headline, charter, or an obligation.org statusis the board.cmd/org/hooks/) —sessionstart-boot.shinjects the boot index for the role a session's cwd maps to ($ORG_STATE/roles.map, longest prefix wins);stop-mark.shappends a mechanicalmarkon Stop. Both fail-open. A mark at the tip renders the next bootdegraded— activity happened, nobody distilled it — which is the honest state, per the checkpoint-authorship law in the TDD.Deliberate POC boundaries
Draft.Incarnation) but is not enforced. Multi-writer enforcement is the chain CAS itself.store-decision.mdchose SQLite for (that slice is drive#46).org checkpoint -body); the Stop hook deliberately writes only marks.Validation
gofmt/go vet/golangci-lintclean;go test ./cmd/org/...green (home lifecycle fold-back, kernel-reason pass-through, 8-writer concurrent append serialization, blob erasure leaving the chain folding, fence advance on takeover, boot shedding/obligation-first/erased-body rendering, CLI exit-code seam).dangling_claim) until discharged → verify refolds 12 records clean.additionalContext, Stop appends the mark, unmapped cwd exits 0 silently.🤖 Generated with Claude Code