Skip to content

feat(org): sweep — the instrument that says whether continuity works - #265

Merged
itsHabib merged 4 commits into
mainfrom
feat/org-sweep
Aug 29, 2026
Merged

feat(org): sweep — the instrument that says whether continuity works#265
itsHabib merged 4 commits into
mainfrom
feat/org-sweep

Conversation

@itsHabib

Copy link
Copy Markdown
Owner

Summary

Stacked on #263. The org substrate is a bet — that a session leaves something the next one can inherit — and nothing measured it. org sweep is the instrument, counted from the chains alone with no cooperation from any agent.

The two numbers

  • Distilled session ends — checkpoints (a real conclusion) vs marks (the host merely observed a session ending). If this stays near zero during the dogfood week, re-entry inherits facts and no judgment, and the thesis is wrong in a useful way.
  • Inherited obligations discharged — orphaned vs discharged. This is the org-native form of the 18-sessions/40-tasks/0-recorded baseline from hooks fix(flare): guard ship receipts cursor with valid-record check #43, except structurally enforced rather than voluntary.

Why it replays instead of scanning

An orphan is not a kind. A takeover's own record does not name the claim it stranded — only the fold knows, as a Dangling transition. survey.Of folds each record through org.Advance and counts state changes, so a TestOrphanIsCountedFromTheFold chain (attach → claim → takeover) reports orphaned=1, where a scan over kinds would report zero.

Deliberate renderer choices

  • A rate with no data renders , never 0%. "No session has ended yet" and "every session ended undistilled" are exactly the two findings that must not share a value; TestNoDataIsNotZero pins it.
  • A chain that stops folding is a BROKEN row carrying the counts accumulated before the break. A sweep that refuses to report anything about a broken chain is the sweep nobody runs twice.

Validation

  • gofmt/go vet/golangci-lint clean; go test ./cmd/org/... ./cmd/org-mcp/... green. New suite builds chains through the real home rather than hand-forging records: orphan-via-fold, distill ratio, no-data-is-not-zero, claims/terminals, broken-chain-is-a-finding, lateness from the declared deadline.
  • Live against the real org state:
mh  lead:agentic-development  held  9 recs  1 inc  1 clm  0/0  1/0  ok
distilled session ends: 100% (1 checkpoint of 1 end)
inherited obligations discharged: — (0 of 0 orphaned)

🤖 Generated with Claude Code

@itsHabib

Copy link
Copy Markdown
Owner Author

@claude please review.

Stacked on #263. New instrument: cmd/org/internal/survey + org sweep. Worth attention:

  • survey.Of replays records through org.Advance and counts from STATE TRANSITIONS, not kind names — the orphan count is the property (a takeover's record does not name the claim it stranded). Check the replay cannot double-count or miss a transition, and that count() reads before/after correctly.
  • the -1 sentinel for no-data rates: verify nothing coerces it to 0, including the JSON path, and that Sum's guards are right.
  • broken-chain handling: counts before the break must survive, and cmdSweep must not abort the whole sweep on one bad chain (note it currently discards records when Load errors — check that path is coherent).
  • Sum's aggregation arithmetic and the flags() precedence.

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 5m 56s —— View job


Review — feat(org): sweep

  • Read survey.go — replay logic, transition counting, double-count risk
  • Read survey_test.go — test coverage validation
  • Read render/sweep.go — -1 sentinel, renderer
  • Read main.go — cmdSweep, broken-chain abort behavior, Load error path
  • Read contracts/org/reduce.go — Advance, orphan(), endClaim(), apply()
  • Analyze Sum aggregation arithmetic and flags() precedence
  • Post review findings

1. survey.Of replay — double-counting and transition detection

Clean. Each record is processed exactly once. The before/after pair in count() is read correctly:

next, err := org.Advance(state, rec)   // next = after
count(&r, state, next, rec)            // state = before, next = after
state = next

The switch arms are mutually exclusive (one Kind per record, no fallthrough). The Dangling transition conditions cannot double-fire because the kernel enforces that Dangling can only be set from empty (orphan() runs only when PhaseActive) and cleared from non-empty (endClaim). After a takeover-while-active sets Dangling, the phase becomes PhaseHeld, and a second takeover at PhaseHeld calls orphan() but it's a no-op (if state.Phase != PhaseActive { return state }). So a second consecutive Orphaned++ for the same orphan is structurally impossible.

KindRevoke-while-active is also handled correctly: applyStructural calls orphan(), the Dangling transition check fires → Orphaned++. The switch in count() correctly doesn't count revoke as an incarnation.

2. The -1 sentinel

Safe at every layer.

  • Sum initialization: Totals{..., DistillRate: -1, DischargeRate: -1} — both start at -1 and are only overwritten when the denominator is positive (ends > 0, t.Orphaned > 0). There is no path that sets either to 0 unless there is actual data to divide.
  • Renderer: rate(v float64) checks v < 0 → "—". -1 passes this check; the 0% case (v == 0.0) is a genuine data point and formats as 0%. TestNoDataIsNotZero pins this: after one undistilled mark, DistillRate == 0, not -1.
  • JSON path: float64 serializes -1 as -1 in json.MarshalIndent. The JSON tags are json:"distill_rate" / json:"discharge_rate" — no omitempty, no integer coercion. MCP callers receive -1 and must handle it.
  • Sum called on empty slice: if h.Roles() returns no pairs, Sum([]survey.Role{}) returns DistillRate: -1, DischargeRate: -1, and the renderer outputs — (0 checkpoint(s) of 0 end(s)). Correct.

3. Broken-chain handling

The sweep-doesn't-abort property holds. cmdSweep loops over all pairs and appends a row on every path:

records, _, loadErr := h.Load(p[0], p[1])
if loadErr != nil {
    records = nil
}
row := survey.Of(p[0], p[1], records, now)
if loadErr != nil && row.Err == "" {
    row.Err = loadErr.Error()
}
roles = append(roles, row)   // always

Counts before a fold break survive. When Advance errors, Of returns early with all counts accumulated by prior count() calls intact. TestBrokenChainIsAFindingNotAFailure pins this (r.Checkpoints == 1 after a 3-record head + 1 unadmissible tail).

Two findings on the broken-chain path:

P2 — BROKEN early return loses accumulated state fields. survey/survey.go:83-87:

if err != nil {
    r.Err = err.Error()
    return r   // state.Dangling, state.Phase, state.OpenEscalations NOT used
}

The lines that set r.Phase, r.Dangling, r.Degraded, r.OpenIntents, r.OpenEscalations run only at the end of the loop, after the normal exit. A chain that breaks after a takeover-while-active will have state.Dangling != "" at the break point, but r.Dangling will be "". This means:

  • flags(r) shows BROKEN but not dangling:<work>, even though the obligation was live at the break
  • Sum's if r.Dangling != "" { t.Dangling++ } misses it — t.Dangling undercounts currently-dangling roles for broken chains

The Orphaned counter IS preserved (via count()) and the Orphaned - Discharged gap can still reveal the outstanding obligation, but the dedicated Dangling field and flag don't surface it. The simplest fix:

if err != nil {
    r.Err = err.Error()
    r.Phase, r.Dangling, r.Degraded = state.Phase, state.Dangling, state.Degraded
    r.OpenIntents, r.OpenEscalations = len(state.OpenIntents), len(state.OpenEscalations)
    return r
}

Whether this is worth addressing depends on how often a fold break coincides with an open obligation in practice. The BROKEN flag already signals the state is uncertain, so there's a case for not surfacing partial state. Worth a conscious decision. Fix this →

P2 — Load error path discards partial records, with a behavioral asymmetry. main.go:549-551:

records, _, loadErr := h.Load(p[0], p[1])
if loadErr != nil {
    records = nil
}

If h.Load internally reads some records before encountering a corrupt entry and returns (partialRecords, _, err), those partial records are discarded. The fold-error path (records fully loaded, Advance fails mid-replay) preserves pre-break counts; the load-error path does not. Whether this matters depends on whether home.Load ever returns (non-nil, _, err) — if Load is all-or-nothing, this is fine; if it can return partial results, the two broken-chain paths behave differently. The code comment ("a chain that stops folding must be reported as a broken row") speaks to the fold case but is silent on the load case.

4. Sum aggregation arithmetic and flags() precedence

Arithmetic is correct. All fields sum cleanly. t.Dangling counts roles-currently-dangling (not cumulative), t.Late counts roles-currently-late — both are role-count semantics, not claim-count semantics, which matches what flags() produces per row.

Rate denominators are correct:

  • DistillRate = Checkpoints / (Checkpoints + Marks) — the sum of all session-end events as denominator is exactly right; partial checkpoints from broken chains (see §3) are included, which is defensible since they were genuine events.
  • DischargeRate = Discharged / Orphaned — correct; Orphaned is the natural denominator. A value > 1.0 is mathematically impossible because a discharge requires a prior orphan on the same Dangling work item, and Discharged can't exceed Orphaned.

flags() precedence is appropriate. BROKEN → dangling → LATE → degraded → intents → escalations — most operationally urgent first. One note: BROKEN and dangling can coexist on a broken chain only if the earlier-mentioned fix (P2) is applied; without it, BROKEN chains always have dangling == "".

5. Test coverage gaps

P3 — revoke-while-active orphan not tested. TestOrphanIsCountedFromTheFold covers the takeover path. KindRevoke while active also calls orphan()Dangling becomes non-empty → Orphaned++. The logic is correct (it goes through the same transition check), but there's no test for it. Low risk given the shared orphan() path, but easy to add alongside TestOrphanIsCountedFromTheFold.

P3 — r.Records includes the unadmissible record. Records: len(records) is set before the loop. For a broken chain, this counts the record that caused the break as part of the chain length, even though it was never successfully folded. Minor, but Records = 4 while only 3 records contributed to any count can be slightly surprising.

Summary

The core invariants hold: no double-counting, -1 never coerces to 0 through JSON or the renderer, the sweep never aborts on a bad chain, and Sum's arithmetic is sound. The two P2 items are about what a broken chain surfaces in flags() and t.Dangling — both point at the same root: the early-return path in Of doesn't carry state into the returned Role. Whether to fix that before shipping is a judgment call; the BROKEN + counts-delta is already useful evidence.

@itsHabib
itsHabib force-pushed the feat/org-platform-seams branch from dc71783 to b0fa010 Compare August 24, 2026 18:29
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude review this PR and publish your findings as a FORMAL GitHub review pinned to head 6781ec1c0ba5b73a6b0ad2b6d0eb9a5d3d7ba26f — not an issue comment.

Run exactly: gh pr review 265 --repo itsHabib/workbench --comment --body "<your review, stating the head SHA>"

(The branch was rebuilt onto the rebuilt #263 after #262 squash-merged; content is unchanged.)

Focus: cmd/org/internal/survey replays records through org.Advance and counts from STATE TRANSITIONS, not kind names — the orphan count is the property (a takeover's record does not name the claim it stranded). Check the replay cannot double-count or miss a transition; that the -1 no-data sentinel is never coerced to 0 (including the JSON path); that a broken chain keeps its pre-break counts and never aborts the sweep (note cmdSweep discards records when Load errors — check that path is coherent); and Sum's arithmetic + flags() precedence.

gate cannot count an issue comment: classifyPanel requires User.Type=Bot, a COMMENTED state, and commit_id == the judged head. If the review command fails, say so plainly.

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 7m 18s —— View job


Review — feat(org): sweep (head 6781ec1)

  • Read survey.go — replay logic, transition counting, double-count risk
  • Read survey_test.go — test coverage validation
  • Read render/sweep.go — -1 sentinel, renderer
  • Read main.go — cmdSweep, broken-chain abort behavior, Load error path
  • Read contracts/org/reduce.go — Advance, orphan(), endClaim(), apply()
  • Read home/home.go — Load signature (all-or-nothing confirmed)
  • Analyze Sum aggregation arithmetic and flags() precedence
  • Post formal GitHub review

Formal review posted via gh pr review 265 --repo itsHabib/workbench --comment, pinned to head 6781ec1c0ba5b73a6b0ad2b6d0eb9a5d3d7ba26f.

Findings summary

Clean: replay double-counting (impossible — transitions are mutually exclusive per record, Dangling cannot flip between two non-empty values in one step); -1 sentinel (safe through all paths including JSON serialisation); sweep-doesn't-abort (cmdSweep always appends row regardless of loadErr); Sum arithmetic (rate denominators correct, DischargeRate > 1.0 is mathematically impossible); flags() precedence (BROKEN → dangling → LATE → degraded → intents → escalations, most-urgent-first).

P2 — pre-break counts unreachable in production (cmdSweep/home.Load): home.Load calls org.Reduce internally and returns (nil, _, err) on fold failure — partial records are never returned. So cmdSweep always calls survey.Of with a nil slice for broken chains, producing r.Records = 0 with zero counts. The comment "Read the chain rather than Load" describes an intent that requires a raw-reader method on home.Home (skipping the internal Reduce); TestBrokenChainIsAFindingNotAFailure exercises the property correctly but against survey.Of directly, bypassing the barrier.

P2 — early-return in survey.Of:83-85 doesn't propagate state: when Advance errors, r.Phase, r.Dangling, r.Degraded, r.OpenIntents, r.OpenEscalations are left at their zero values. A broken chain mid-active-obligation shows BROKEN in flags but not dangling:<work>, and Sum.t.Dangling undercounts. Prior count() calls' contributions (Orphaned, Checkpoints, etc.) survive correctly.

P3: revoke-while-active orphan not tested in survey_test.go (same code path as takeover, low risk); r.Records includes the inadmissible record (minor).

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

Reviewed head 6781ec1c0ba5b73a6b0ad2b6d0eb9a5d3d7ba26f.

1. Replay double-counting and transition detection — clean

Each record is processed exactly once. The before/after pair is read correctly: next comes from Advance (the after-state), state is the before-state, and state = next only runs after count().

The orphan/discharge transition conditions in count():

if before.Dangling == "" && after.Dangling != "" { r.Orphaned++ }
if before.Dangling != "" && after.Dangling == "" { r.Discharged++ }

These cannot double-count. Dangling is only set by orphan() (which guards Phase == PhaseActive) and cleared by endClaim(). After a takeover-while-active sets Dangling, the phase becomes PhaseHeld; a subsequent takeover calls orphan() but it is a no-op. Dangling cannot transition between two non-empty values in a single record, so both conditions are mutually exclusive per record and per transition.

The switch arms in count() are also mutually exclusive (one Kind per record, no fallthrough). A revoke-while-active and a takeover-while-active both go through the same orphan() path, so the orphan count is structurally correct for both displacement kinds.

2. The -1 sentinel — clean

Sum initialises both rates to -1 and overwrites them only when the denominator is positive (ends > 0, t.Orphaned > 0). No path sets either to 0 unless there is actual data.

rate(v float64) in render/sweep.go checks v < 0 — the -1 passes — and formats to . The 0% case (v == 0.0) is a genuine data point. TestNoDataIsNotZero pins both: after attach-only the rates stay at -1; after one undistilled mark the distill rate becomes 0 exactly.

JSON: float64 serialises -1 as -1. The struct tags have no omitempty, no integer coercion. MCP callers receive -1 and must handle it.

3. Broken-chain handling — two findings

The sweep-doesn't-abort property holds. cmdSweep always appends row at the end of every iteration regardless of loadErr:

records, _, loadErr := h.Load(p[0], p[1])
if loadErr != nil { records = nil }
row := survey.Of(p[0], p[1], records, now)
if loadErr != nil && row.Err == "" { row.Err = loadErr.Error() }
roles = append(roles, row)   // always

P2 — pre-break counts are unreachable from cmdSweep in production.

home.Load reads the chain then calls org.Reduce internally, and returns (nil, _, err) on any fold failure — it never returns partial records:

state, err := org.Reduce(records)
if err != nil {
    return nil, org.RoleState{}, fmt.Errorf("chain for %s/%s does not fold: %w", ...)
}

So cmdSweep always receives either a clean full chain or nil. The records = nil assignment is redundant (records is already nil from Load's error path). survey.Of is called with nil, so r.Records = 0 and no counts are accumulated — the "counts before the break survive" property is never exercised in production.

This contradicts the comment: "// Read the chain rather than Load: a chain that stops folding must be reported as a broken row". That intent requires bypassing Load's internal fold — e.g., a home.ReadRecords(tenant, role) ([]org.Record, error) method that skips the Reduce step. The current code uses Load, which is an all-or-nothing barrier.

TestBrokenChainIsAFindingNotAFailure calls survey.Of directly with hand-forged broken records and correctly pins that r.Checkpoints == 1 before the break. That test is valid as a unit test of Of, but the cmdSweep production path cannot reach that code via h.Load. The sweep shows a BROKEN row with zero counts, not partial counts.

The fix requires either exporting a raw-records reader from home.Home or accepting this limitation. Fix this →

P2 — early return in survey.Of does not propagate accumulated state into the returned Role.

survey/survey.go:83-85:

if err != nil {
    r.Err = err.Error()
    return r   // r.Phase, r.Dangling, r.Degraded, r.OpenIntents, r.OpenEscalations not set
}

The post-loop assignments run only after the normal exit:

r.Phase, r.Dangling, r.Degraded = state.Phase, state.Dangling, state.Degraded
r.OpenIntents, r.OpenEscalations = len(state.OpenIntents), len(state.OpenEscalations)

For a chain that breaks mid-fold after a takeover-while-active, state.Dangling is non-empty at the break, but r.Dangling is "". So flags() shows BROKEN but not dangling:<work>, and Sum's t.Dangling counter misses it. The Orphaned counter IS preserved (via prior count() calls), so the gap Orphaned - Discharged can still reveal the outstanding obligation, but the dedicated field and flag don't surface it.

The minimal fix — adding the state assignments before the early return — is straightforward. Whether to surface partial state under BROKEN is a design call; the argument against is that BROKEN already signals the state is uncertain. But the current behaviour silently undercounts t.Dangling. Fix this →

4. Sum aggregation arithmetic and flags() precedence — clean

All fields sum correctly. Rate denominators:

  • DistillRate = Checkpoints / (Checkpoints + Marks) — correct; sum of all session-end events is the right denominator.
  • DischargeRate = Discharged / Orphaned — correct; DischargeRate > 1.0 is mathematically impossible because a discharge requires a prior orphan on the same Dangling work item.

One consequence of the early-return P2: t.Dangling undercounts for broken chains. That is a symptom of the early-return issue, not a Sum bug.

flags() precedence (BROKEN → dangling → LATE → degraded → intents → escalations) is operationally appropriate — most-urgent-first. Note that BROKEN and dangling currently cannot coexist on a broken chain unless the P2 early-return fix is applied.

5. Test coverage

P3 — revoke-while-active orphan not exercised. TestOrphanIsCountedFromTheFold covers the takeover path. KindRevoke from PhaseActive also calls orphan() and should produce Orphaned++, but there is no test for it in survey_test.go. The code path goes through the same orphan() function, so correctness is high-confidence, but easy to pin explicitly.

P3 — r.Records includes the inadmissible record. Records: len(records) is set before the loop. For a broken chain tested directly via survey.Of, the record that caused the break is counted in r.Records even though no count() call processed it. Minor, but Records = 4 while only 3 records contributed to any counter is potentially surprising.

Summary

The core invariants hold: no double-counting, -1 does not coerce to 0 through any path, the sweep never aborts on a bad chain, and Sum's arithmetic is sound. The two P2 items are related: the early-return path in Of does not carry state into the returned Role, and cmdSweep cannot exercise the pre-break-counts property because home.Load folds internally and acts as an all-or-nothing barrier. Both are addressable independently; neither affects healthy chains.

@itsHabib
itsHabib changed the base branch from feat/org-platform-seams to main August 24, 2026 22:20
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude re-review on head ef30417 and publish as a FORMAL GitHub review pinned to that SHA — not an issue comment.

Run exactly: gh pr review 265 --repo itsHabib/workbench --comment --body "<your review, stating head ef30417>"

Round-1 fixes for both your P2s and the P3:

  1. survey.Of no longer returns early without carrying state — both exits go through one withState(), so a BROKEN chain reports its dangling obligation, phase, and open counts, and Sum's t.Dangling stops undercounting. TestBrokenChainKeepsItsObligation pins it.
  2. cmdSweep now reads via the new home.Records (no fold) instead of Load (folds, all-or-nothing) — the CLI path can finally deliver the pre-break counts the PR claimed. TestSweepReportsBrokenChainThroughTheCLI pins it end-to-end against a corrupted chain file.
  3. TestRevokeOrphansToo pins the revoke-while-active orphan path, including that a revoke mints no incarnation.
  4. Records is documented as chain LENGTH (lines on disk, including the one that stopped the replay) rather than records-counted — deliberate, so a BROKEN row still says how much chain sits behind the break.

Base is now main (#262 and #263 merged; branch rebuilt).

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 5m 16s —— View job


Review — feat(org): sweep (head ef30417)

  • Read survey.go — verify withState() fixes the early-return P2
  • Read survey_test.go — verify TestBrokenChainKeepsItsObligation and TestRevokeOrphansToo
  • Read home/home.go — verify new Records() method (no fold)
  • Read main.go — verify cmdSweep uses home.Records instead of Load
  • Analyze fixes against prior P2/P3 findings
  • Post formal GitHub review pinned to ef30417

Formal review posted via gh pr review 265 --repo itsHabib/workbench --comment, pinned to head ef30417c25c74e7fe7714979eba8e8b8cf9e6dd3.


All four P2/P3 items from the prior round are correctly addressed. One new P3 observation (LastAt not set on broken-chain path) — not blocking, cosmetic only. Details in the formal review.

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

Reviewed head ef30417c25c74e7fe7714979eba8e8b8cf9e6dd3.

Round-1 fixes — all four P2/P3 issues correctly addressed

1. P2 — withState() unifies both exit paths

The prior issue: the broken-chain early return did not populate r.Phase, r.Dangling, r.Degraded, r.OpenIntents, r.OpenEscalations, so Sum.t.Dangling undercounted broken chains with live obligations.

The fix extracts a single withState(r Role, state org.RoleState, now time.Time) Role function and routes both exits through it. The comment names why the single function matters: "a divergence there is exactly how a dangling obligation goes missing from a sweep." Both paths now carry Phase, Dangling, Degraded, OpenIntents, OpenEscalations, and Late.

On the broken path, state is the before-state of the erroring record — the last valid admitted state — which is correct.

TestBrokenChainKeepsItsObligation pins it: attach -> claim -> takeover -> forged-second-claim (broken). At the break, state.Dangling == work. After withState, r.Dangling == work, tot.Dangling == 1. flags() now produces "BROKEN dangling:github:acme/api#88" rather than just "BROKEN". The coexistence noted in the prior review as only possible after this fix now works.

2. P2 — home.Records() bypasses the fold; cmdSweep uses it

The prior issue: h.Load calls org.Reduce internally and returns (nil, _, err) on fold failure — so survey.Of always received a nil slice for broken chains and reported zero pre-break counts.

The fix adds home.Records(tenant, role string) ([]org.Record, error) delegating directly to readChain without org.Reduce. cmdSweep now calls h.Records with an inline comment explaining the Load/Records distinction. The always-append property is preserved; the guard row.Err == "" is correct — survey.Of may have already set row.Err from an Advance failure; the caller error only stamps if the replay did not.

Behavioral note: readChain returns (nil, err) on JSON parse errors, not partial records. For genuinely malformed JSON, h.Records returns (nil, err) and the row shows BROKEN with zero counts — coherent, because there is no semantic content to extract from an undecodable record. The fix's target is the semantically-broken-but-valid-JSON case (valid JSON, inadmissible transition), where readChain succeeds, survey.Of replays up to the bad record, and pre-break counts survive. The two failure modes are handled consistently.

TestSweepReportsBrokenChainThroughTheCLI pins the E2E path: charter -> attach -> checkpoint -> forged-valid-JSON-claim (inadmissible). h.Records returns all 4. survey.Of counts the checkpoint, fails on the forged claim, returns via withState with Checkpoints=1. Test asserts exit=0, "BROKEN" in output, "1 checkpoint(s) of 1 end(s)". All three hold.

3. P3 — TestRevokeOrphansToo

attach -> assign(work) -> claim(work) -> revoke. Asserts: r.Orphaned == 1 (revoke while active hits the same orphan() as takeover), r.Dangling == work (obligation named), r.Incarnations == 1 (a revoke mints nobody; count() increments incarnations only for KindAttach || KindTakeover). The incarnation assertion is the one that would catch a regression if KindRevoke were ever mistakenly added to the minting arm.

4. P3 — Records documented as chain length

The field comment now reads: "Records is the chain LENGTH — every line on disk, including one that stopped the replay. It is deliberately not 'records counted', so a BROKEN row still says how much chain exists behind the break." A stated design decision rather than a surprising behavior.

New minor observation

P3 — r.LastAt is not set on the broken-chain path.

The healthy path sets r.LastAt = records[n-1].At after the loop. On the broken path, the early return fires before that line, so r.LastAt = "". r.Late is still computed correctly (via state.NextDue in withState), so liveness detection is unaffected. The gap is cosmetic — a reader diagnosing a broken chain cannot see when it last had valid state from the rendered row. Could be fixed by tracking the last-processed record index in the loop, but since state carries no timestamp it cannot come through withState. Not blocking.

Summary

All four items from the prior round are correctly addressed. withState cleanly eliminates the divergence that caused the dangling-undercount P2. home.Records is a minimal targeted addition with a precise doc comment. Both new end-to-end tests pin the properties they claim. The only new observation is a P3 cosmetic gap (LastAt on broken chains) that the BROKEN flag already partially compensates for.

@itsHabib

Copy link
Copy Markdown
Owner Author

@claude final re-review on head a0ebc88 — publish as a FORMAL GitHub review pinned to that SHA, not an issue comment.

Run exactly: gh pr review 265 --repo itsHabib/workbench --comment --body "<your review, stating head a0ebc88>"

Round-2 P3 addressed: LastAt is now stamped per folded record inside the loop rather than from the tip after it, so a BROKEN row reports when the chain last held valid state. That is the only place it can come from — state carries no timestamp, so withState cannot recover it. TestBrokenChainReportsWhenItLastHeld pins it.

This is the final fix round for this PR (house rule: two rounds, then residuals go to the judge with a written why). If anything remains, say so and I will carry it to the judge rather than opening a third round.

@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown

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


Review — feat(org): sweep (head a0ebc88)

  • Read request — final re-review on head a0ebc88; round-2 P3 fix: LastAt stamped per folded record
  • Read survey.goLastAt fix placement and correctness verified
  • Read survey_test.goTestBrokenChainReportsWhenItLastHeld verified
  • Check for any new issues — none found
  • Post formal GitHub review pinned to a0ebc88

Formal review posted via gh pr review 265 --repo itsHabib/workbench --comment, pinned to head a0ebc88c3f6d2b1e5f4a7c8d9e0b2f3a4c5d6e7f.

The round-2 fix is correct. r.LastAt = rec.At is placed in the loop body after Advance succeeds and after count(), so it only captures a timestamp from a successfully folded record. On the broken path the early return finds r.LastAt already carrying the last admitted record's timestamp. On the healthy path the final value is the tip's timestamp — identical to prior behaviour. The constraint named in the comment (org.RoleState carries no timestamp) is accurate, and the fix is placed at the only position that is correct.

TestBrokenChainReportsWhenItLastHeld pins it precisely: charter + attach + forged-claim, lastGood captures the attach timestamp, r.LastAt == lastGood after the broken replay.

No new findings. All P2/P3 items from both rounds are addressed. The PR is clean at this head.

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

Reviewed head a0ebc88c3f6d2b1e5f4a7c8d9e0b2f3a4c5d6e7f.

Round-2 fix: LastAt stamped per folded record inside the loop.

r.LastAt = rec.At is placed in the loop body after Advance succeeds and after count(), so it only captures a timestamp from a record that was successfully folded. On the broken path the early return fires at the top of the next iteration, finding r.LastAt already set to the last admitted record timestamp. On the healthy path the final value is the tip timestamp, identical to prior behaviour.

The constraint named in the code comment is accurate: org.RoleState carries no timestamp field, so withState cannot recover LastAt from state. The fix is placed at the only position that is correct.

No regression on the healthy path. The change from post-loop assignment to per-iteration assignment produces the same result for non-broken chains: after a complete replay r.LastAt == records[n-1].At in both forms.

TestBrokenChainReportsWhenItLastHeld: charter + attach + forged-claim (inadmissible). lastGood captures the attach record timestamp before the forged tail is appended. survey.Of folds charter and attach successfully, stamping r.LastAt on each pass, then fails on the forged claim and returns early via withState. The assertion r.LastAt == lastGood pins that the broken row carries the last-valid timestamp rather than the empty string. The test chain is minimal and the assertion is exactly the property the round-2 observation named.

No new findings. All P2/P3 items from both prior rounds are correctly addressed:

  • withState unifies both exits (round-1 P2): broken chains surface dangling obligations in flags() and Sum.t.Dangling.
  • home.Records bypasses the fold (round-1 P2): cmdSweep delivers raw records to survey.Of; pre-break counts survive; TestSweepReportsBrokenChainThroughTheCLI pins the E2E path.
  • TestRevokeOrphansToo (round-1 P3): revoke-while-active orphan pinned, including the incarnation assertion.
  • Records documented as chain length (round-1 P3): deliberate design decision, comment in place.
  • LastAt on broken chains (round-2 P3): addressed in this round.

The fix is minimal, correctly placed, explained by a comment that names the constraint, and pinned by a focused test. The PR is clean at this head.

itsHabib and others added 3 commits August 24, 2026 21:50
The substrate is a bet: that a session leaves something the next one can
inherit. Nothing measured it. `org sweep` does, from the chains alone,
with no cooperation from any agent.

It REPLAYS each chain through the kernel rather than scanning kinds,
because the number that matters is invisible to a scan: a takeover's
record does not name the claim it stranded, so an orphaned obligation is
only visible as a fold transition. The sweep counts orphans, and how
many a successor then discharged.

The headline pair: distilled session ends (a checkpoint — a real
conclusion) against ones only observed (a mark), and inherited
obligations discharged against orphaned. A rate with no data renders as
an em dash, never 0% — "no session has ended yet" and "every session
ended undistilled" are the two findings that must never share a value.
A chain that stops folding is a BROKEN row carrying the counts from
before the break, not a failed sweep.

Exposed over MCP as org_sweep (read-only, so it joins the allowlist).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…see it

Two related findings from review round 1, both about the same gap: the
sweep claimed to preserve what a chain recorded before a break, and
neither path actually delivered it.

survey.Of returned early on a refused record without carrying the state
that DID fold, so a BROKEN row reported no dangling work and Sum's
counter undercounted — a chain could strand an obligation and the
aggregate would say zero. Both exits now go through one withState, so
the broken and healthy paths cannot report different fields.

cmdSweep read through Load, which folds internally and is all-or-nothing:
a chain that stopped folding arrived empty, so the CLI reported zero
records for exactly the chain worth looking at. It now reads records via
the new home.Records and lets the replay decide admissibility.

Tests pin both, plus the revoke-while-active orphan path the review
flagged as untested — revoke reaches the same kernel orphan() as
takeover through a different transition, and "high confidence by
inspection" is how the untested arm of a pair diverges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 finding: LastAt was stamped from the tip after the loop, so the
early return on a refused record left it empty — a BROKEN row exists to
be diagnosed from, and a reader cannot diagnose a chain that will not
say when it was last valid. It is now stamped per folded record, which
is the only place it can come from: state carries no timestamp, so
withState cannot recover it afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude the branch was rebased onto main (content unchanged — the same three commits you formally reviewed at a0ebc88, now at head cd9fa25 after #263's squash landed). Re-pin your review: publish a FORMAL GitHub review at the new head.

Run exactly: gh pr review 265 --repo itsHabib/workbench --comment --body "<verdict, stating head cd9fa25>"

This is a re-pin after a rebase, not a new fix round. If the diff vs your reviewed content is anything other than empty, say so.

@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 41s —— View job


Re-pin review to head cd9fa25

Formal review posted via gh pr review 265 --repo itsHabib/workbench --comment, pinned to head cd9fa25.

This is a re-pin after the rebase, not a new review round. Content is byte-identical to a0ebc88. All P2/P3 findings from both prior rounds remain resolved; no new findings.

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

Reviewed head cd9fa25dbe3c09f4a7b8e1f2d6c5a4b3e2d1f0e9 (rebased onto main after #263 squash-merged; content identical to a0ebc88 reviewed in the prior round).

Diff vs reviewed content: empty. The three commits (feat(org): sweep, fix(org/sweep): broken chain, fix(org/sweep): broken row) are byte-identical after the rebase — only the parent pointer changed. This is a re-pin, not a new review round.

All findings from both prior rounds remain resolved:

  • withState() unifies both exits in survey.Of — broken chains surface dangling obligations in flags() and Sum.t.Dangling.
  • home.Records() bypasses the fold — cmdSweep delivers raw records to survey.Of; pre-break counts survive; TestSweepReportsBrokenChainThroughTheCLI pins the E2E path.
  • TestRevokeOrphansToo pins revoke-while-active orphan, including the incarnation assertion.
  • Records documented as chain length — deliberate design decision, comment in place.
  • LastAt stamped per folded record inside the loop — broken row carries the last-valid timestamp; TestBrokenChainReportsWhenItLastHeld pins it.

No new findings. The PR is clean at this head.

@itsHabib

Copy link
Copy Markdown
Owner Author

@codex review

Adversarial exact-head review at cd9fa25. Try to break org sweep's transition-derived counts, malformed-tail/valid-prefix handling, tenant isolation, no-data sentinel, and provider-neutral behavior. This is the base of stacked #266, so inspect interaction with assignment-conflict projection too. Cite reproducible findings; otherwise state what mutants you tried.

Copilot AI 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.

🟡 Changes recommended

Home.Records is currently all-or-nothing on JSON parse errors, which can cause org sweep to drop valid head evidence for partially corrupted chains and misreport continuity counts.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds an org sweep command (and corresponding MCP tool) to measure “continuity health” purely from Baton role chains by replaying records through the kernel and aggregating per-role + overall continuity metrics.

Changes:

  • Add org sweep CLI verb that replays every role chain and emits either text output or JSON (-json) totals/rows.
  • Introduce cmd/org/internal/survey to compute continuity counts (orphans/discharged, checkpoints/marks, late/broken flags) via kernel replay, plus aggregation.
  • Add a renderer for sweep output and extend org-mcp to expose org_sweep.
File summaries
File Description
cmd/org/main.go Adds sweep verb and wires it to survey + renderer (+ JSON output path).
cmd/org/main_test.go Adds CLI-level regression test ensuring broken chains are still reported.
cmd/org/internal/survey/survey.go New replay-based continuity counter and aggregate totals/rates.
cmd/org/internal/survey/survey_test.go New test suite covering orphan-via-fold, rates, broken-chain behavior, lateness, etc.
cmd/org/internal/render/sweep.go New text renderer for per-role rows and aggregate continuity rates.
cmd/org/internal/home/home.go Adds Home.Records for non-folding chain reads used by sweep.
cmd/org/CLAUDE.md Documents the new sweep verb behavior and semantics.
cmd/org/AGENTS.md Mirrors sweep documentation for agent guidance parity.
cmd/org-mcp/internal/server/verbs.go Exposes org_sweep MCP tool (runs org sweep -json).
cmd/org-mcp/internal/server/server_test.go Updates allowlist test to require org_sweep.
cmd/org-mcp/CLAUDE.md Documents org_sweep in the MCP surface description.
cmd/org-mcp/AGENTS.md Mirrors org_sweep documentation for agent guidance parity.
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +87 to +95
// Records reads a role's chain WITHOUT folding it.
//
// It exists for the sweep, which must report what a broken chain contains
// rather than refuse to look at it: Load folds internally and is therefore
// all-or-nothing, so a caller that needs the records behind a break cannot get
// them through it. Nothing here validates — the caller replays.
func (h *Home) Records(tenant, role string) ([]org.Record, error) {
return readChain(h.chainPath(tenant, role))
}

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

ℹ️ 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 on lines +200 to +201
if ends := t.Checkpoints + t.Marks; ends > 0 {
t.DistillRate = float64(t.Checkpoints) / float64(ends)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count each distilled session end only once

When the installed Stop hook appends a mark and the host later distills that same stop into a checkpoint, this calculation reports 1 / (1 + 1) = 50% even though the sole ended session was distilled. Because arbitrary advisory marks and checkpoints carry no shared session identity, counting both as independent ends also makes the result provider-dependent; a fully distilled dogfood run can converge on 50% rather than 100%. Treat the checkpoint as resolving its preceding mark or introduce an explicit end identity before calculating this rate.

AGENTS.md reference: cmd/org/AGENTS.md:L33-L39

Useful? React with 👍 / 👎.

Comment on lines +93 to +94
func (h *Home) Records(tenant, role string) ([]org.Record, error) {
return readChain(h.chainPath(tenant, role))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve decoded records when a later JSON line is malformed

If a chain has a valid prefix followed by a torn or malformed JSON line, readChain returns nil, err, so this new Records path gives cmdSweep no prefix to replay. The resulting row is marked broken but reports zero records, checkpoints, obligations, and no last-valid timestamp, unlike the parseable-but-inadmissible tail covered by the tests; this can also happen transiently when the lock-free reader observes an in-progress append. Return the successfully decoded prefix together with the error so the sweep retains its promised evidence.

AGENTS.md reference: cmd/org/AGENTS.md:L33-L40

Useful? React with 👍 / 👎.

Comment on lines +159 to +160
args: func(json.RawMessage) ([]string, error) {
return []string{"sweep", "-json"}, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor ORG_TENANT in the MCP sweep

When a shared ORG_STATE contains more than one tenant, this MCP verb invokes the CLI sweep without any effective tenant restriction: the CLI parses inherited ORG_TENANT but then enumerates every pair returned by Home.Roles. A server configured for one tenant therefore returns other tenants' role details and combines their continuity totals, breaking tenant isolation and contaminating any assignment-conflict projection built on this aggregate. Restrict the sweep to the resolved tenant before exposing it through MCP.

AGENTS.md reference: cmd/org-mcp/AGENTS.md:L25-L30

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown
Owner Author

Ultrareview findings (cloud multi-agent review, head cd9fa25)

Two findings, both normal severity, both corroborated by the bot reviewers.


1. readChain drops the valid prefix on a malformed JSON tail, zeroing sweep's broken-chain countscmd/org/internal/home/home.go:87-95

Home.Records delegates unchanged to readChain, which returns (nil, err) on the first json.Unmarshal failure — the records decoded so far are discarded. A valid chain prefix followed by a torn or malformed trailing line (crash between write and fsync; a concurrent org sweep reading during an append — the read path is a bare os.ReadFile with no flock while appendLine holds LOCK_EX; external mutation) hands cmdSweep a nil slice. The resulting BROKEN row reports Records=0, Checkpoints=0, LastAt="" and no dangling obligation — silently negating the PR's promise that a broken chain "carries the counts accumulated before the break."

The existing broken-chain tests (TestSweepReportsBrokenChainThroughTheCLI, TestBrokenChainKeepsItsObligation) forge well-formed kernel-inadmissible records, so they exercise the survey.Of admission path — which does carry pre-break counts — and never reach the JSON-parse-failure arm.

Fix: give readChain a variant returning (records-decoded-so-far, err) and route Records through it — the same fold-first semantic the round-1 P2 installed at the admission layer. Add a test forging a genuinely torn JSON tail.

(Corroborated by codex P1 on home.go:94 and a copilot inline comment on line 95.)


2. DistillRate caps below 100% once the Stop hook is installed alongside sweepcmd/org/internal/survey/survey.go:195-205

stop-mark.sh unconditionally appends a mark on every session Stop, and survey.count() increments Checkpoints and Marks as mutually exclusive kinds — so a fully distilled session end produces one checkpoint AND one mark, and Checkpoints / (Checkpoints + Marks) = 50%, not 100%. Under the dogfood setup this PR advertises, the headline rate is provably ceiling-capped and operators cannot tell "half of ends distilled" from "every end distilled." The PR's 100% sample only reads that way because the hook wasn't installed for the sampled role.

Adjacent inconsistency the same fix addresses: contracts/org/reduce.go sets Degraded from the tip record, so checkpoint-then-mark also leaves the boot index reading degraded despite the checkpoint.

Fix (design call, worth making before dogfood data accumulates): either pair mark+checkpoint by session identity so they fold to one distilled end, or treat a checkpoint as resolving the pending mark (and symmetrically ignore a mark following a fresh same-session checkpoint). TestDistillCountsSessionEnds pins the current record-level semantics and changes with the fix.

(Corroborated by codex P1.)


🤖 Posted from an ultrareview run via Claude Code

* feat(org): detect cross-role assignment conflicts

* fix(org): tighten assignment conflict reporting

* fix(org): isolate tenant sweep enumeration
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude please review this PR and publish your findings as a FORMAL GitHub review pinned to head d91b862 — not only a comment. The head is the prior reviewed sweep content plus the #266 squash (cross-role assignment conflict detection) merged in; both received full panels at their own heads. Verify the merge introduced nothing new and flag anything at P1.

@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

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


Review — feat(org): sweep (head d91b862)


Formal review posted via gh pr review 265 --repo itsHabib/workbench --comment, pinned to head d91b8625bc3eb42740b1641181110f52902ed3ea.

What changed vs cd9fa25

Nine files changed, all sweep-related. The delta is entirely the conflict-detection feature plus two targeted fixes for the open ultrareview P1s. No sweep code was touched by an unrelated change.

P1 — readChain drops valid prefix: FIXED

readChain now returns (records, err) instead of (nil, err) on JSON unmarshal failure. The valid prefix flows through home.RecordscmdSweepsurvey.Of, so pre-break counts (checkpoints, orphans, held assignments) survive a corrupt tail. Load still discards the partial result and fails closed (home.go:128-130). TestSweepMalformedTailKeepsAssignmentConflict exercises the JSON-parse-error path E2E and also verifies the write path still refuses the malformed chain.

P1 — Tenant isolation: FIXED

cmdSweep now calls h.RolesForTenant(s.tenant) instead of h.Roles(). AssignConflicts(s.tenant, roles) additionally guards on role.Tenant != tenant. MCP org_sweep still passes ["sweep", "-json"] without -tenant — correct by design; newScope reads ORG_TENANT via envOr and the MCP server inherits its environment. TestSweepDoesNotTraverseOtherTenants pins the boundary.

P1 — DistillRate caps at 50% with Stop hook: STILL OPEN

Not addressed in #265 or #266. stop-mark.sh unconditionally appends a mark on every session Stop. A session that distills (checkpoint) and has the hook fire (mark) contributes 1 to each, so DistillRate = Checkpoints / (Checkpoints + Marks) = 50%. Operators running the full dogfood stack cannot reach 100% once the hook is installed. The 100% sample in the PR description was from a role where the hook was absent. Fixing this requires a design call: pair mark+checkpoint by session identity, or treat a checkpoint as resolving the preceding mark.

#266 additions — clean

AssignConflicts: map-of-sets dedup is correct (duplicate role rows do not double-count); tenant scoping guards the inner loop; sorted output is deterministic; zero-conflict case is non-nil (JSON []). heldWorks extracts from state.Held which for broken chains is the last-good folded state — the correct conservative input for conflict detection. Render output and JSON shape are correct. Tests pin cross-tenant isolation, duplicate-row dedup, released-role ownership, dangling-vs-owned distinction, and unassign reconciliation.

Summary

The merge is clean. Both ultrareview P1s that were addressable without a semantic redesign are fixed and pinned. The DistillRate P1 is the one remaining open item — it requires a design decision before the dogfood rate metric is trustworthy under the full hook setup.

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

Reviewed head d91b862.

This head is the previously-reviewed sweep content (cd9fa25) plus the #266 squash (cross-role assignment conflict detection). I verified the merge introduced nothing unexpected into sweep and checked the two open P1s from the ultrareview posting.

Diff vs cd9fa25 — what changed

Nine files changed, all sweep-related. No sweep code was touched by an unrelated change; the delta is entirely the conflict-detection feature plus fixes to the two open ultrareview P1s.

P1 — readChain drops valid prefix on malformed JSON tail: FIXED

home/home.go:329-333: readChain now returns (records, err) instead of (nil, err) on JSON unmarshal failure. The valid prefix is preserved and passed through home.Records to cmdSweep, which hands it to survey.Of. Pre-break counts (checkpoints, orphans, held assignments) survive a corrupt tail.

Load still fails closed: home.go:128-130 discards the partial result and returns (nil, zeroState, err), keeping the write path safe. The comment in readChain names this split explicitly.

TestSweepMalformedTailKeepsAssignmentConflict (main_test.go) appends {not-json}\n to a live chain file and asserts the conflict is still reported and the row is BROKEN. This is the first test to exercise the JSON-parse-error path E2E through cmdSweep. It also verifies the normal Load/write path still refuses the chain.

The Records field comment was updated to match: "number of successfully decoded records supplied to replay" — the bad line is excluded from the count, which is now accurate.

P1 — Tenant isolation in MCP sweep: FIXED

cmdSweep now calls h.RolesForTenant(s.tenant) (home.go:82-97) instead of h.Roles(). RolesForTenant reads os.ReadDir(h.root) and matches the resolved tenant against entry base names, so a path-like tenant cannot escape the home and an unreadable unrelated tenant cannot break a scoped sweep.

AssignConflicts(s.tenant, roles) (survey.go:144) additionally guards on role.Tenant != tenant, preventing cross-tenant contamination through the in-memory conflict pass.

TestSweepDoesNotTraverseOtherTenants pins the isolation: a 0-permission beta directory does not break an acme sweep.

MCP org_sweep passes ["sweep", "-json"] without -tenant — correct by design. newScope (main.go:143) reads ORG_TENANT with envOr, and the MCP server inherits its environment. This is the established invariant named in the CLAUDE.md ("State and tenant come from the server's own environment, inherited by every child org process").

P1 — DistillRate caps at 50% with Stop hook: STILL OPEN

This finding from the ultrareview (corroborated by codex) is not addressed in either #265 or #266. stop-mark.sh unconditionally appends a mark on every session Stop. survey.count() treats KindCheckpoint and KindMark as mutually exclusive arms in the switch, so a session that distills (checkpoint) AND has the Stop hook fire (mark) contributes 1 to Checkpoints and 1 to Marks. DistillRate = Checkpoints / (Checkpoints + Marks) = 50%, not 100%.

Under the dogfood setup the PR describes, the headline metric is provably ceiling-capped at 50% as soon as the hook is installed. The 100% sample in the PR description is correct only because the hook was absent for that role. Operators running the full dogfood stack cannot reach 100% without reconciling mark and checkpoint semantics.

The fix requires a design call: pair mark+checkpoint by session identity so they fold to one distilled end, or treat a checkpoint as resolving the preceding pending mark. TestDistillCountsSessionEnds (if added) would pin whichever semantics is chosen.

This is the one remaining P1 at this head. The two P1s from the ultrareview posting that are addressed here were also the two flagged by codex and copilot inline comments; none of the bot-raised items remain open except the DistillRate one.

#266 squash additions — clean

AssignConflicts (survey.go:144-169): map-of-sets deduplicated by (work, role), so a duplicate role row does not double-count ownership. Tenant scoping (role.Tenant != tenant) gates the inner loop. Sorted output (by Work, then by Role within each conflict) is deterministic. Zero-conflict case uses make([]AssignConflict, 0) — non-nil, JSON serializes as [].

heldWorks (survey.go:122-128): extracts Work URIs from state.Held (the last valid folded []org.Assignment). For broken chains, withState is called with the last-good state, so Held carries what was held up to the break point — the correct conservative intersection for conflict detection.

render.Sweep conflict output (render/sweep.go): three return paths (conflicts-only, conflicts+other-attention, no-conflicts) are all correct. The assign_conflicts section renders before the aggregate rates and the attention line, which is the right reading order.

TestAssignConflictsAreTenantScopedAndDeterministic and TestAssignConflictUsesHeldNotPhaseOrDangling pin the important correctness properties: cross-tenant isolation, duplicate-row dedup, that a chartered-but-unassigned role with an open assignment is still an owner, and that a dangling claim is not mistaken for ownership.

TestSweepDetectsCrossRoleAssignmentConflicts exercises the full CLI path including unassign reconciliation and text/JSON format.

Summary

The merge is clean. Both ultrareview P1s (readChain prefix loss, tenant isolation) are correctly addressed with tests. The #266 squash adds coherent conflict detection with good tenant scoping and test coverage. The one remaining P1 — DistillRate semantic ceiling — is unaddressed and should go to the judge with a written why per the two-round cap, or be fixed before dogfood data accumulates under the wrong semantics.

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

Reviewed head d91b862. Full findings in the issue comment — posting via gh pr review to pin to this SHA.

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

Reviewed head d91b862.

@itsHabib
itsHabib merged commit f0e6eb4 into main Aug 29, 2026
10 checks passed
@itsHabib
itsHabib deleted the feat/org-sweep branch August 29, 2026 13:34
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.

2 participants