Skip to content

feat(org): adopt — put work already in flight onto a lane's plate - #277

Open
itsHabib wants to merge 2 commits into
mainfrom
feat/org-adopt
Open

feat(org): adopt — put work already in flight onto a lane's plate#277
itsHabib wants to merge 2 commits into
mainfrom
feat/org-adopt

Conversation

@itsHabib

Copy link
Copy Markdown
Owner

Summary

A session that never attached still produced a branch, a PR and a head SHA. All of that is observable from outside the session — and none of it could become ownership, because registering work needed the session's cooperation, which is exactly what is missing when a session never attached.

Measured in one live tenant on 2026-08-31: 31 open operator-authored PRs and 66 session directories against three chartered lanes, all unlinked, while org sweep reported 100% distilled session ends and liveness ok. The metric was not wrong — its denominator is the sessions that attached, which are the ones that were always going to be fine.

org adopt writes four records on an unheld lane: attach → note → assign → release. No claim.

Why a verb and not a documented recipe

assign takes no -body and attach rejects one, so an adopter's identity has nowhere to live except a record of its own. In a three-command recipe that record is the optional step — which means it is the step missing from the tick where it mattered. An unattributed assign is indistinguishable from the lane's own coverage sweep, and that indistinguishability is the entire cost of adopting at all. So -by is required, and the note is not skippable.

Two properties that come from the kernel, not from this verb

Both are what make adoption safe to put in a watcher's hands:

  • It cannot displace. attach on a held lane is refused already_held, whose message already says "displacing a holder is a takeover". Granting adopt authority therefore cannot leak into takeover authority — the kernel draws that line for free, and checkTakeover still gates the other side on Terms.Supervisors.
  • It never claims. Adoption writes Held, never Active. A lane holds many items and acts on exactly one (claim_active), so putting work on a plate and starting it are different acts. That is what makes held 31 · active 0 a truthful state rather than a broken one.

Refusals

case result
lane is held already_held, exit 1, naming the live holder
another lane in the tenant holds the work work_already_held, exit 1 — refuses rather than manufacturing the assign_conflict sweep reports. Which of two lanes should own something is a routing question no mechanical verb gets to answer.
a peer chain is unreadable refuses — absence of a readable answer is not evidence of absence
this lane already holds it no-op, says so, writes nothing (idempotent by state, like transfer)
out of charter scope warns and proceeds, matching transfer

Both refusals name frozen reasons, so the documented exit-code seam holds: 1 for the substrate declining, not 4 for the command failing.

It manufactures no authority

attach carries no authorization check, so any process that can read the state directory could already write these four appends by hand — verified. The composite adds only that the trace is not optional and the pin is not hand-assembled. Who may invoke it is a charter question for the invoking role; for /task-supervisor specifically it is an open operator decision, argued both ways in cc-skills docs/supervisor-as-manager.md §4.

Validation

go test ./cmd/org/... ./contracts/org/... -count=1
ok  cmd/org · cmd/org/internal/{home,render,survey} · contracts/org

gofmt -l clean, go vet clean. Four new tests; the one worth reading is the handoff assertion — adopted work is picked up with org begin -work <uri> and no pin, because the pin is already on the chain. Adoption is only worth anything if a successor can resume without knowing an adoption happened.

End-to-end against a throwaway state root, three adoptions onto a released lane:

attach seq 2 … (phase held) note seq 3 … assign seq 4 … release seq 5 … (phase chartered)
TENANT  ROLE               PHASE      ACTIVE  HELD  OPEN  LIVENESS
work    steward:sidebar    chartered  -          3     0  ok

Not included

Not exposed on org-mcp, matching begin/done/intake/transfer — that surface lags the CLI deliberately.

A session that never attached still produced a branch, a PR and a head SHA.
Every one of those is observable from outside the session, and none of them
could become ownership: registering the work needed the session's cooperation,
which is exactly what is missing when a session never attached. Measured on
2026-08-31 in one live tenant: 31 open operator-authored PRs and 66 session
directories against three chartered lanes, all of it unlinked, while every
instrument the substrate owns reported ok.

`org adopt` writes four records on an UNHELD lane: attach -> note -> assign ->
release. No claim.

The note is why this is a verb and not a documented recipe. `assign` takes no
body and `attach` rejects one, so an adopter's identity has nowhere to live
except a record of its own — and in a recipe that record is the optional step,
which means it is the step missing from the tick where it mattered. An
unattributed assign is indistinguishable from the lane's own coverage sweep,
and that indistinguishability is the entire cost of adopting at all. `-by` is
therefore required.

Two properties come from the kernel rather than from this verb, and both are
what make adoption safe to put in a watcher's hands:

- It cannot displace. `attach` on a held lane is refused `already_held`, whose
  message already says "displacing a holder is a takeover". So granting adopt
  authority cannot leak into takeover authority — the kernel draws that line
  for free, and `checkTakeover` still gates the other side of it on
  `Terms.Supervisors`.
- It never claims. Adoption writes Held, never Active. A lane holds many items
  and acts on exactly one (`claim_active`), so putting work on a plate and
  starting it are different acts, and adoption is only ever the first. That is
  what makes `held 31 · active 0` a truthful state rather than a broken one.

Refuses `work_already_held` when another lane in the tenant already holds the
work, rather than manufacturing the `assign_conflict` sweep reports: which of
two lanes should own something is a routing question no mechanical verb gets to
answer. An unreadable peer chain refuses too — it is not evidence of absence.
Warns on scope drift and proceeds, matching `transfer`.

Both refusals name frozen reasons, so the exit-code seam holds: 1 for the
substrate declining, not 4 for the command failing.

It manufactures no authority. `attach` carries no authorization check, so any
process able to read the state directory could already write these four appends
by hand; the composite adds only that the trace is not optional and the pin is
not hand-assembled. WHO may invoke it is a charter question for the invoking
role — for `/task-supervisor` specifically it is an open operator decision,
recorded in cc-skills `docs/supervisor-as-manager.md` §4.

The handoff is the assertion worth reading in the tests: adopted work is picked
up with `org begin -work <uri>` and no pin, because the pin is already on the
chain. Adoption is only worth anything if a successor can resume the work
without knowing an adoption happened.

Not exposed on org-mcp, matching begin/done/intake/transfer — that surface
lags the CLI deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@itsHabib

itsHabib commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex @claude Review this PR, pinned to 327045af.

Sweep of stale open PRs: this one has been open without a complete review panel. Triggering the repository roster on the exact head above so it can be gated.

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown

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


Review: feat(org): adopt — 327045af

  • Load tools
  • Read cmd/org/main.go — adopt implementation
  • Read cmd/org/adopt_test.go — test coverage
  • Read doc changes (AGENTS.md, CLAUDE.md, README.md)
  • Cross-check invariants against stated properties
  • Post findings

Reviewed at 327045af. Five files, 434 additions.

Summary

The implementation is correct and the two safety properties hold by construction. Logic is clean, Go idioms are well-applied throughout, and test coverage matches the four refusal cases in the PR table. One real gap worth recording in FOLLOWUPS; two minor issues below that.


P2 — Partial-failure recovery leaves the lane stuck held

File: cmd/org/main.go:516–544 (cmdAdopt)

The PR description's refusal table claims idempotency "like transfer" but the guarantee is narrower than transfer's. transfer is idempotent across partial failures: a crash between the two chain writes leaves both chains with the item and re-running finishes the unassign. adopt is idempotent only for the success case — re-adopting work the lane already holds is a no-op.

If the verb crashes between attach and release (e.g. after the note but before the assign), the lane is left in phase: held. On retry, adoptable checks state.Holder != "" → returns already_held → exit 1. The caller cannot complete the adoption by re-running; a manual org release is required to unblock.

For an autonomous watcher that re-runs adopt after an uncertain result, this is a blocked state with no mechanical exit. The recovery path (manual release) is not documented.

Suggested path: either document the recovery explicitly (like transfer documents its surviving window), or implement a partial-recovery check — if the lane is held but no incarnation has been committed to the work yet, treat it as a resume rather than a conflict. A FOLLOWUPS entry would also be acceptable given the PR's stated scope.

Fix this →


P3 — Note body omits digest when -digest is supplied without -pin

File: cmd/org/main.go:601–608 (adoptionNote)

adoptionNote renders pin: — when pin == "". A caller who supplies -digest sha256:abc… without -pin gets a note that says pin: — and does not mention the digest at all. The assign record holds the digest, but the note — the one record meant to make an adoption legible six weeks later — gives no human-readable trace of what was pinned.

func adoptionNote(by, work, pin, evidence string) []byte {
    // pin is "" when caller passed -digest only; the digest is not forwarded here
    fmt.Fprintf(&sb, "pin: %s\n", orDashText(pin))
    ...
}

The fix is either to pass digest through and append digest: %s when pin == "", or require -pin unconditionally (eliminating the ambiguity). The latter aligns with the note's stated purpose as the human-readable attribution.

Fix this →


Nit — PhaseRetired/PhaseSealed not pre-checked in adoptable

File: cmd/org/main.go:552–569 (adoptable)

PhaseVoid gets a clear error message ("adoption puts work on an existing lane's plate, it does not charter one"). Retired and sealed lanes also cannot be adopted into (the kernel's attach will refuse them), but the error that surfaces is a kernel refusal without a guiding message. Low priority — the refusal is correct — but the pattern of the PhaseVoid guard makes it noticeable.


What holds

Two safety properties — both enforced by the kernel, correctly non-duplicated here:

  • Cannot displace: attach on a held lane is already_held at the substrate layer; adoptable's pre-check (state.Holder != "") is defense-in-depth, the kernel is the authority. ✓
  • Never claims: the four records are attach/note/assign/release. No claim record is written. ✓

Idempotent for the success case (adopted_work/already_held no-op at line 507): ✓

work_already_held refusal with conservative semantics (unreadable peer chain = refuse, not skip): ✓

Scope drift warning fires after all four records land, uses pre-adopt state.Terms.Scope which is correct since Terms are immutable within the adoption. ✓

Attribution chain: inc is captured from the attach's st.Holder and threaded through note, assign, and release. The test at adopt_test.go:133 verifies all three share one incarnation. ✓

Test coverage: Four tests covering the four scenarios from the PR description table. TestAdoptPutsWorkOnAPlateWithoutStartingIt is the right framing — it proves the handoff, not just the writes. ✓

Go idioms: Line-of-sight throughout. adoptable and otherHolder are small, sharp, single-responsibility functions. No else. ✓

Doc consistency: AGENTS.md, CLAUDE.md, and README.md all accurately describe the verb; the three copies are in sync and match the implementation.


The P2 is worth a FOLLOWUPS entry — not a blocker for gating, but the "idempotent by state, like transfer" claim in the PR table should be bounded in the docs so a future reader doesn't walk into the stuck-held case expecting the same recovery surface transfer has.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T12:32:33.570900Z 327045a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 327045af7b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmd/org/main.go
The otherHolder scan runs before the role lock is taken and appends lock
one chain at a time, so two adoptions into two idle lanes can both pass
and sweep reports the assign_conflict. Say so in the guide pair and the
code, pointing at the FOLLOWUPS cross-chain-transaction entry, instead of
implying the preflight is an admission law.

Co-Authored-By: Claude Fable 5.1 <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