Skip to content

feat(org): the Baton home — a runtime for role continuity chains - #262

Merged
itsHabib merged 3 commits into
mainfrom
feat/org-home
Aug 24, 2026
Merged

feat(org): the Baton home — a runtime for role continuity chains#262
itsHabib merged 3 commits into
mainfrom
feat/org-home

Conversation

@itsHabib

Copy link
Copy Markdown
Owner

Summary

contracts/org (landed in #246/#248) is a complete kernel with nothing to run it. This PR adds cmd/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 under blobs/, appends serialized by an flock over the fold→admit→append critical section. Admission is org.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.
  • Verbs 1:1 with record kinds — charter / attach / assign / claim / yield / complete / abandon / takeover / revoke / seal / intent / resolve / escalate / delegate, plus advisory note / mark / checkpoint / report / message.
  • org boot — the re-entry index: charter line, held work, a predecessor's dangling obligation rendered first, liveness derived from the writer's own declared next_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 status is the board.
  • Hook scripts (cmd/org/hooks/) — sessionstart-boot.sh injects the boot index for the role a session's cwd maps to ($ORG_STATE/roles.map, longest prefix wins); stop-mark.sh appends a mechanical mark on Stop. Both fail-open. A mark at the tip renders the next boot degraded — activity happened, nobody distilled it — which is the honest state, per the checkpoint-authorship law in the TDD.

Deliberate POC boundaries

  • The home writes as the current holder (single-operator posture); explicit incarnation presentation exists (Draft.Incarnation) but is not enforced. Multi-writer enforcement is the chain CAS itself.
  • No SQLite store — this is the chain, not the discharge store store-decision.md chose SQLite for (that slice is drive#46).
  • Checkpoint distillation stays manual/operator (org checkpoint -body); the Stop hook deliberately writes only marks.

Validation

  • gofmt / go vet / golangci-lint clean; 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).
  • Live walk in a scratch state: charter → attach → assign → claim → note → yield → checkpoint → boot; takeover mid-claim → boot leads with OBLIGATION → successor's claim refused (dangling_claim) until discharged → verify refolds 12 records clean.
  • Both hooks exercised with real harness-shaped stdin: SessionStart emits additionalContext, Stop appends the mark, unmapped cwd exits 0 silently.

🤖 Generated with Claude Code

itsHabib and others added 2 commits August 24, 2026 05:01
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>
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude please review.

New tool: cmd/org — runtime over the contracts/org kernel. Worth your attention:

  • the append path (internal/home): fold→admit→append under an flock; admission is delegated entirely to org.Advance — check the home really adds no judgment and cannot write what the kernel refuses (draft fills seq/prev/fence/incarnation the way the kernel's own test fixture does).
  • exit-code seam: kernel refusal must exit 1 with the reason id on stderr, never 4; refused records must not reach the chain.
  • hooks/*.sh: both must be fail-open (no mapping/binary/chain → exit 0, no output) and the Stop hook must never block a session.
  • render/boot shedding: budget met by shedding depth (excerpt → held list) but never the headline, charter line, or dangling-obligation line; shed loop must terminate (a prior infinite-loop bug was fixed by the >163 guard — verify the fix is sound).
  • blob erasure must leave the chain folding (erasability is design).

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 6m 4s —— View job


Review: cmd/org — Baton home runtime

Reviewed at feat/org-home (563b55d). Specific focus areas from the request all checked.


  • Read internal/home/home.go — append path, flock, admission delegation
  • Read cmd/org/main.go — exit-code seam, verb dispatch
  • Read hooks — fail-open behavior
  • Read internal/render/render.go — boot shedding, shed loop
  • Read all test files
  • Cross-check contracts/org kernel (reduce.go, validate.go, refusal.go, record.go)
  • Post review findings

Append path (internal/home/home.go) — CLEAN

Append() (line 123) is correctly structured: flock → Load → draft → Advance → appendLine. The kernel is the sole admission gate; the home never makes a judgment.

draft() spine-filling vs kernel laws — verified against validate.go's laws map and reduce.go's checkPosition/checkWriter/checkFence:

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

The *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 guarded

If $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 0

Fix this →


Shed loop termination — SOUND

The > 163 guard (render.go line 153) is correct. Trace:

  1. Excerpt > 163 bytes → Excerpt = Excerpt[:160] + "…" → 163 bytes. Returns true.
  2. Next shed(): 163 > 163 is false → case 1 skipped → case 2 fires (!= ""). Clears excerpt. Returns true.
  3. Next shed(): excerpt is "". Case 3: nil held list. Returns true.
  4. Next shed(): no held list. Default: return false.
  5. Text() falls back to s[: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 (via readChain() + org.Reduce()); it never touches blobs. Erasing a blob cannot break chain folding.
  • Blob() returns (nil, false, nil) on os.IsNotExist — absence is not an error.
  • lastWord() renders Erased: true, which text() 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 messagemain.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 closehome.go:259-275:

defer f.Close()   // cleanup
...
return f.Close()  // propagate close error

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

itsHabib added a commit that referenced this pull request Aug 24, 2026
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>
@itsHabib

Copy link
Copy Markdown
Owner Author

@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 (2>/dev/null)" || exit 0) so malformed or empty stdin exits 0 silently — comprehensive fail-open as advertised; cmdBlob's not-found message now names s.fs.Arg(0). The two low-priority notes (UTF-8 hard-cut tail, appendLine double-close pattern) are acknowledged and deferred as-is.

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 3m 15s —— View job


Re-review: cmd/org — round-1 fixes verified

Reviewed at feat/org-home (4672e11).

  • Read cmd/org/hooks/sessionstart-boot.sh — verify jq guards
  • Read cmd/org/hooks/stop-mark.sh — verify jq guards
  • Read cmd/org/main.go — verify cmdBlob s.fs.Arg(0) fix
  • Post final verdict

jq guards — sessionstart-boot.sh — CLEAN

Line 28:

cwd="$(jq -r '.cwd // empty' <<<"$input" 2>/dev/null)" || exit 0

Both halves of the fix are present: 2>/dev/null suppresses jq's parse-error stderr, and || exit 0 handles jq's non-zero exit (which an assignment propagates directly). Fail-open for malformed stdin is now comprehensive. ✓


jq guards — stop-mark.sh — CLEAN

Four jq calls against $input:

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.go s[:budget] fallback) — correct to defer; boot index is ASCII-dominant in practice.
  • appendLine double-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.

@itsHabib

Copy link
Copy Markdown
Owner Author

@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:
gh pr review 262 --repo itsHabib/workbench --comment --body "<your round-2 verdict, stating head 4672e11>"

Do not post an issue comment this time. If the review command fails, say so plainly with the error.

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 2m 45s —— View job


  • Publish formal GitHub review via gh pr review

Formal review submitted. The gh pr review 262 --repo itsHabib/workbench --comment command completed successfully.

One note on execution: the Bash sandbox in this environment blocks multi-line quoted strings containing # (markdown headers after newlines trigger the path-validation guard), so the body was simplified to plain prose. If gate's classifyPanel inspects the body text for structure, a richer body can be posted by running the full gh pr review command directly from a terminal:

gh pr review 262 --repo itsHabib/workbench --comment --body "$(cat <<'EOF'
### Re-review: cmd/org — round-1 fixes verified

Reviewed at **feat/org-home** (4672e113c6bd5cc4cc902b349c6d28d88cf0ba15).

jq guards sessionstart-boot.sh: CLEAN — 2>/dev/null + || exit 0 on cwd extraction. ✓
jq guards stop-mark.sh: CLEAN — all four jq calls guarded; re-entrancy check at line 28 safe via &&-list position. ✓
cmdBlob error message: CLEAN — s.fs.Arg(0) replaces args[0]. ✓
Deferred: UTF-8 hard-cut tail and appendLine double-close both confirmed unchanged and acceptable.

Verdict: All P1 findings from round 1 addressed and verified sound. No new findings. The PR is clean.
EOF
)"

What was posted via the action: Re-review: cmd/org -- round-1 fixes verified. Reviewed at feat/org-home (4672e113c6bd5cc4cc902b349c6d28d88cf0ba15). All P1 findings addressed and verified sound. No new findings. The PR is clean.

The review has User.Type=Bot, state=COMMENTED, and commit_id=4672e113c6bd5cc4cc902b349c6d28d88cf0ba15 — the three fields gate's classifyPanel requires.

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

Re-review: cmd/org -- round-1 fixes verified. Reviewed at feat/org-home (4672e11). All P1 findings addressed and verified sound. No new findings. The PR is clean.

@itsHabib
itsHabib merged commit cc26dbc into main Aug 24, 2026
5 checks passed
itsHabib added a commit that referenced this pull request Aug 25, 2026
…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>
itsHabib added a commit that referenced this pull request Aug 26, 2026
…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>
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