muse-crew 0.14.5 → 0.14.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/API.md +106 -12
  2. package/docs/decisions/AGENTS.md +5 -0
  3. package/docs/decisions/publish-path.md +247 -9
  4. package/docs/decisions/qa-reproduce.md +30 -0
  5. package/docs/guide.md +41 -5
  6. package/docs/publish-unknown-recovery.md +28 -120
  7. package/docs/publish-verification.md +148 -501
  8. package/lib/AGENTS.md +9 -13
  9. package/lib/advance-publish-base.js +3 -3
  10. package/lib/compose-evidence-caption.js +1 -2
  11. package/lib/compute-publish-diff.js +82 -5
  12. package/lib/crew-api.js +1134 -1027
  13. package/lib/merge-lock.sh +153 -41
  14. package/lib/publish-note-vocabulary.js +135 -42
  15. package/lib/qa-db.js +132 -0
  16. package/lib/schema.sql +69 -0
  17. package/lib/serve-artifact.js +46 -2
  18. package/lib/test-detached-integrate.sh +122 -0
  19. package/lib/test-merge-lock.sh +30 -1
  20. package/lib/worktree-lifecycle.sh +284 -34
  21. package/package.json +1 -1
  22. package/seed/AGENTS.md +1 -0
  23. package/seed/cron-body-ack-scan.md +44 -0
  24. package/seed/cron-body-template.md +55 -89
  25. package/seed/crons.json +12 -0
  26. package/workflows/AGENTS.md +1 -1
  27. package/workflows/bugfix.js +489 -224
  28. package/workflows/chore.js +306 -226
  29. package/workflows/crew-dispatch.js +57 -4
  30. package/workflows/crew-init.js +23 -0
  31. package/workflows/crew-uninstall.js +7 -4
  32. package/workflows/docs.js +24 -2
  33. package/workflows/standard.js +494 -251
  34. package/workflows/upgrade.js +13 -1
  35. package/lib/build-readback-request.js +0 -140
  36. package/lib/check-intent-freshness.js +0 -101
  37. package/lib/classify-publish-absence.js +0 -462
  38. package/lib/publish-content.js +0 -154
  39. package/lib/readback-disk.js +0 -195
  40. package/lib/retry-publish.js +0 -417
  41. package/lib/verify-publish.js +0 -416
package/API.md CHANGED
@@ -34,7 +34,7 @@ Update an existing task's fields. Only `id` is required; all other fields are op
34
34
  | `id` | uuid | yes | |
35
35
  | `title` | string (1–200) | no | |
36
36
  | `description` | string (≤ 3000) | no | |
37
- | `state` | `todo` · `in_progress` · `parked` · `done` | no | Only the `parked` → `todo` transition stamps `retry_reset_at` to the current server time (UTC, ISO 8601), mechanically restarting the consecutive-failure streak — a re-queued task never inherits its old failures. No other transition stamps it (`parked` → `done`, `todo` → `todo`, and patches without a `state` field do not). |
37
+ | `state` | `todo` · `in_progress` · `parked` · `done` | no | Only the `parked` → `todo` transition stamps `retry_reset_at` to the current server time (UTC, ISO 8601), mechanically restarting the consecutive-failure streak — a re-queued task never inherits its old failures. No other transition stamps it (`parked` → `done`, `todo` → `todo`, and patches without a `state` field do not). Any transition INTO `parked` closes the task's open `workflow_runs` rows to `parked` and fires the dependent cascade-park, exactly like `parktask` (room #26 blocker 37, 2026-09-21: a parked task must never leave a stale `running` telemetry row). |
38
38
  | `priority` | `high` · `normal` · `low` | no | |
39
39
  | `project` | slug | no | **Project-move guard:** changing this to a different project throws while the task has an active run (an agent session with status `running` that started within the last hour), because the live run keeps the old project's repo context and moving it mid-phase would work on the wrong repo. Wait for the run to finish, or recover/park the task first, then move it. |
40
40
  | `workflow` | slug or null | no | |
@@ -92,17 +92,76 @@ Atomically park a task for human attention: sets `state` to `parked`, writes the
92
92
  | `task_id` | uuid | yes | |
93
93
  | `message` | string (1–1000) | yes | Why the task needs a human; written to the activity feed as a `note` |
94
94
 
95
- Returns `{ "ok": true, "task": {...}, "settled_sessions": <n> }` with the updated task record and the count of sessions settled (`0` when none were running).
95
+ Returns `{ "ok": true, "task": {...}, "settled_sessions": <n>, "closed_workflow_runs": <n>, "cascade_parked": [{ "id", "dep_id" }] }` with the updated task record, the count of sessions settled (`0` when none were running), the count of open `workflow_runs` rows closed to `parked` (`0` when none were open — room #26 blocker 37, 2026-09-21: park never closed telemetry rows, leaving stale `running` rows), and the cascade-parked dependents.
96
96
 
97
- ### `resolvepublishunknown`
97
+ **Cascade-park (room #26, 2026-09-21):** parking a task also parks every `todo` task that depends on it — transitively, to a fixpoint — with structured attribution on the existing `parked` state (`park_reason: "dep_parked"`, `park_dep_id` naming the parked dep): never a new state, never silent `todo`, never auto-waived. Each cascade-parked task gets a `blocked` event naming the dependent and the parked dep, and its running sessions are settled. An `in_progress` task is never cascade-parked — a running workflow owns its verdict. The same cascade fires when a task parks through `updatetask` → `parked` or `retry-platform-failure`. When a task leaves `parked` for any other state, its cascade-parked dependents are re-evaluated: re-attributed to another still-parked dep, or returned to `todo` with the attribution cleared. The `parked` → `todo` transition is the designed human waiver — explicit, attributable, never automatic.
98
98
 
99
- Recovery contract for publish attempts parked with an `unknown` outcome (2026-09-14): the rebuild trigger's child failed structured closeout and the in-flight-only build-state poll could not see the completed build, so the workflow parked fail-closed — but the edit may still have gone through. When a platform audit build completed inside the publish window (between the Integrate-completion event and the unknown-outcome park), this action routes the task to the parent's independent content verification WITHOUT re-issuing the edit and WITHOUT stamping provenance. The original unknown ledger entry and park event are preserved; the resolution is appended to the publish ledger (`outcome: "unknown-resolved"`) and to the event log, and a `publish: verification-requested <commit>` note is written so `scan-verification-pending` claims the task on the next tick. The parent's read-back ([publish verification](docs/publish-verification.md)) remains the real verification and can still fail terminally. Cases that stay parked with `{ "resolved": false, ... }`: task not parked, latest ledger outcome not `unknown`, no usable commit on the unknown entry, no audit build inside the publish window, unobservable publish window (missing Integrate-completion or park event), or an already-resolved attempt (idempotent).
99
+ ### `scan-publish-intent`
100
+
101
+ Claims parked publish intents for the tick worker's direct issuance (0.14.6). For each task parked with `publish: publish-requested <commit> attempt=N`, re-derives the version deterministically (never trusts the ledger's), verifies the staged diff's sha256, checks the intent base against stamped provenance, and claims with a 1-hour lease (CAS on `claim_expiry`). Skips: no intent entry, unparseable note, terminal note, unexpired claim (another tick owns it), base-stale (re-queues to the workflow's Publish), backoff not elapsed.
102
+
103
+ Returns `{ "intent": [...], "skipped": [...], "requeued": [...] }`. Each intent entry carries `task_id`, `commit`, `attempt` (numeric `publish_attempt`), `version` (`<commit>:<attempt>`), `claim_expiry`, `diff_path`, `diff_sha256`, `ledger_path`.
104
+
105
+ ### `record-intent-issuance`
106
+
107
+ Records the tick worker's direct issuance outcome (0.14.6). Compare-and-swap on `claim_expiry` — a stale tick writes nothing.
108
+
109
+ | Field | Type | Required | Notes |
110
+ |-------|------|----------|-------|
111
+ | `task_id` | uuid | yes | Must have an unexpired intent claim |
112
+ | `claim_expiry` | ISO-8601 | yes | Must match the claim's expiry (CAS) |
113
+ | `edit_outcome` | `accepted`\|`refused` | yes | The direct `artifact_edit` outcome |
114
+ | `refusal_text` | string | no | Required when `refused` |
115
+ | `reclaimed` | boolean | no | Copy verbatim from the `scan-publish-intent` entry — true when a previous tick died mid-issuance |
116
+ | `receipt_check` | `different`\|`unreadable`\|`match` | required when `reclaimed` is true, forbidden otherwise | The tick worker's step-2 version-receipt check outcome for the reclaimed claim |
117
+
118
+ `accepted` writes the issuer-stamped `submitted` ledger entry + `publish: edit-issued` note. `refused` writes `rejected` + terminal `publish: publish-refused`. Returns `{ "recorded": true, "edit_outcome" }` or `{ "recorded": false }` on CAS mismatch.
119
+
120
+ Reclaimed-claim receipt guard (2026-09-21 R-3): a reclaimed claim means a previous tick may have issued the edit without recording it, so `record-intent-issuance` requires the explicit `receipt_check` — the mechanical receipt-check outcome, not a prose judgment. A missing or unknown value is a usage error; `"match"` refuses to record fail-closed (the edit landed — acknowledge via `record-version-ack`, never re-issue); `"different"`/`"unreadable"` record normally with the check carried on the ledger entry. A differing or unreadable receipt is NOT proof the prior attempt's edit did not land — the acknowledgement scan owns that verdict from here.
121
+
122
+ ### `record-builder-report`
123
+
124
+ Records a builder's refusal report (0.14.6). Mechanically requires the EXACT version in `report_text` — a report that does not echo the version is rejected.
125
+
126
+ | Field | Type | Required | Notes |
127
+ |-------|------|----------|-------|
128
+ | `task_id` | uuid | yes | |
129
+ | `version` | `<commit>:<attempt>` | yes | Must name an issued attempt |
130
+ | `outcome` | `refused` | yes | (2026-09-20 REVIEW) `acknowledged` is rejected — the positive builder-report path was circular; disk is the sole positive evidence |
131
+ | `report_text` | string | yes | Must contain the exact version |
132
+
133
+ Returns `{ "ok": true, "publish_attempt" }`. The ack scan consumes refusal reports (terminal `publish: publish-refused`); positive acknowledgements come only from the on-disk version receipt.
134
+
135
+ ### `record-version-ack`
136
+
137
+ Stamps a version acknowledgement (0.14.6). The version must name an issued attempt for the task/commit.
100
138
 
101
139
  | Field | Type | Required | Notes |
102
140
  |-------|------|----------|-------|
103
- | `task_id` | uuid | yes | Must be parked with a latest publish-ledger outcome of `unknown` |
141
+ | `task_id` | uuid | yes | |
142
+ | `version` | `<commit>:<attempt>` | yes | Must match an issued attempt's derived version |
143
+ | `via` | string | yes | `disk` only (2026-09-20 REVIEW) |
144
+
145
+ Stamps provenance, writes terminal `publish: version-acknowledged`, re-queues the task to `in_progress`. Returns `{ "ok": true, "commit", "publish_attempt", "via" }`. The reclaim path accepts the exact disk version for the currently claimed intent when no `submitted` entry exists (dead tick issued but died before recording).
146
+
147
+ ### `scan-ack-pending`
148
+
149
+ Evaluates version acknowledgements (0.14.6). For each `edit-issued` task: checks the on-disk `.crew-publish-version-<task_id>` receipt (per-task scoped; the SOLE positive evidence — 2026-09-20 REVIEW removed the circular builder-report positive path). Any exact on-disk acknowledgement for an issued version on the task/commit counts (late earlier attempts included). On ack: provenance + terminal `publish: version-acknowledged` + requeue. On explicit refusal (via builder report): terminal `publish: publish-refused`. On 30-minute window expiry (per attempt, anchored at the LATEST issuance): re-issues with a FRESH version, immediately claimable (no backoff). When the 2-hour total budget from the FIRST issuance is exhausted: terminal `publish: version-timeout` (unobserved within budget — the edit may still have landed).
150
+
151
+ Returns `{ "acknowledged": [...], "refused": [...], "reissued": [...], "timeouts": [...], "skipped": [...] }`.
152
+
153
+ A dedicated 5-minute `crew-ack-scan` cron (seed/crons.json) runs this command on an observation-only tick so verdicts land quickly; it never issues edits — issuance stays in the 15-minute poll tick's claim-and-issue path.
104
154
 
105
- Returns `{ "resolved": true, "task_id", "commit", "evidence_dir", "audit_report_ok" }` on success, or `{ "resolved": false, "reason" }` when the recovery preconditions are not met.
155
+ ### `record-publish-note`
156
+
157
+ The closed publish-note writer (0.14.6 §1.5). Asserts the note's verb against the writable registry before writing — prose cannot mint new states.
158
+
159
+ | Field | Type | Required | Notes |
160
+ |-------|------|----------|-------|
161
+ | `task_id` | uuid | yes | |
162
+ | `note` | string | yes | Must start with a registered `publish: <verb>` |
163
+
164
+ Writable verbs (tick prose via `record-publish-note`): `publish-requested`, `publish-intent-claimed`, `publish-base-stale`, `edit-issued` (transitional only — prose can never mint a terminal). Deterministic code writes terminals via the code-writable registry: `version-acknowledged`, `publish-refused`, `version-timeout`, `publish-unissuable`. Legacy terminals are recognized but NOT writable by anyone. Returns `{ "ok": true, "verb" }` or a usage error.
106
165
 
107
166
  ---
108
167
 
@@ -124,9 +183,9 @@ Create or update an agent session record. Sessions track which identity is worki
124
183
  | `notes` | string (≤ 3000) | no | |
125
184
  | `failure_reason` | string (≤ 3000) or null | no | |
126
185
  | `caveats` | array of `{text, severity}` | no | |
127
- | `already_merged_sha` | 7–40 lowercase hex or null | no | **Do not set this yourself.** The workflow sets it automatically when it verifies that a task's work is already on the main branch (e.g., landed by a prior run). Written by workflow code (not by the agent's text output) after the `rev-parse` + `merge-base --is-ancestor` check passes; a bad value is a usage error. Review loads it from the field instead of parsing the notes. Null semantics: omitting the field preserves the existing value; passing explicit `null` clears it. `record-phase` always preserves (it never clears). |
186
+ | `already_merged_sha` | 7–40 lowercase hex or null | no | **Superseded (0.14.7, blocker 34):** no workflow writes or reads this field; attribution is classifier-only (the `classify-branch` lifecycle command reports the merge sha on its own `BRANCH_STATE:` line, and Review adopts it from there). Column retained; null on all new rows. Null semantics: omitting the field preserves the existing value; passing explicit `null` clears it. `record-phase` always preserves (it never clears). |
128
187
 
129
- The `already_merged_sha` field exists because session `notes` are hard-capped at 3000 chars: a truthful already-merged declaration at the tail of a long Build report was silently truncated, and Review could not recover it. The sha is managed by the workflow — the workflow writes it after verification, and downstream phases read the field, never the agent's text.
188
+ **Superseded (0.14.7, blocker 34):** no workflow writes or reads this field; attribution is classifier-only. The field's original reason is gone — the pre-Review classifier now answers "already merged, by which merge" in git, before Review, so there is no longer a declaration to carry through session notes. Column retained; null on all new rows.
130
189
 
131
190
  ### `heartbeat-session`
132
191
 
@@ -176,9 +235,35 @@ Atomically record a workflow phase outcome: writes the agent session (with its v
176
235
  | `notes` | string (≤ 3000) | no | Phase summary |
177
236
  | `event_type` | event type | no | Defaults to the `status` value; use `note` for non-verdict events |
178
237
  | `event_message` | string (1–1000) | no | Defaults to "`<identity>` `<step>` `<status>`" |
238
+ | `verdict` | object | no | Optional structured Review verdict record (see below) — the non-lossy verdict grounds |
179
239
 
180
240
  The CLI exposes this as `record-phase`. All CLI commands use kebab-case (`create-task`, `get-dispatch-state`, etc.) mapping to the action names documented here.
181
241
 
242
+ #### Structured verdict records
243
+
244
+ Session notes are intentionally lossy (truncated at 3000 chars) — Review verdict grounds were destroyed by truncation before reaching `rejectionNotes`, making parks on verified-correct implementations unauditable. `recordphase` optionally carries the full worker report as a verdict payload, validated at the API boundary and written to the `verdicts` table in the same transaction as the session and event, so the lossless record can never be missing when the note exists.
245
+
246
+ | Field | Type | Required | Notes |
247
+ |-------|------|----------|-------|
248
+ | `step` | `Review` | yes | Verdict records are Review-scoped |
249
+ | `attempt` | integer ≥ 0 | yes | The rework round (0 = first Review) |
250
+ | `reviewer` | string (1–200) | yes | The reviewing identity (e.g. `cass`) |
251
+ | `verdict` | `PASS` · `FAIL` · `INDETERMINATE` | yes | The machine-extracted verdict; `INDETERMINATE` is the fail-closed verdict-extraction failure (bounded re-ask exhausted) — the grounds are still preserved |
252
+ | `grounds` | string (1–1000000) | yes | The full worker report, uncapped |
253
+ | `review_basis` | `mechanical-fail` · `branch-diff` · `runtime-state-none` · `frozen-merge:<40-hex-sha>` · null | no | What the review actually examined (blocker-34 branch classifier). `null` = not branch-classified (docs.js path). Shape-validated at the boundary; malformed values fail the whole `recordphase` call |
254
+
255
+ `summary` is derived from the session note actually recorded (one source of truth — never a second caller-supplied truncation). One verdict per recording session: a re-recorded phase for the same session is the same evidence, so the first write wins and rows are never revised; a new Review execution claims a new session and gets its own row. A malformed payload fails the whole `recordphase` call (fail closed) — no session, no event, no row. The response carries the recorded (or pre-existing) row as `verdict`, or `null` when no payload was sent.
256
+
257
+ ### `getverdicts`
258
+
259
+ Return all structured verdict records for a task, ordered by recording time (rework rounds and retries accumulate as separate rows, never overwrite).
260
+
261
+ | Field | Type | Required | Notes |
262
+ |-------|------|----------|-------|
263
+ | `task_id` | uuid | yes | |
264
+
265
+ The CLI exposes this as `get-verdicts`.
266
+
182
267
  ---
183
268
 
184
269
  ## Dispatch
@@ -187,6 +272,8 @@ The CLI exposes this as `record-phase`. All CLI commands use kebab-case (`create
187
272
 
188
273
  Read the full board state for dispatch decisions. Takes no arguments. Returns all projects with their tasks, simultaneity limits, quiesce state, and active sessions — everything the dispatcher needs to decide what to claim.
189
274
 
275
+ Like the zombie-session sweep, this call writes: it first reconciles stranded dependents — any `todo` task whose dep is currently `parked` is cascade-parked with `park_reason: "dep_parked"` attribution (see `parktask`), so no task sits `todo` behind a parked dep for more than one dispatcher tick. Idempotent across ticks; reconciliation parks, never dispatches (cascade-parked tasks carry zero sessions).
276
+
190
277
  Each task in `ready_tasks` carries an explicit retry object:
191
278
 
192
279
  | Field | Type | Notes |
@@ -209,6 +296,8 @@ A dashboard that omits `retry` or returns an invalid `consecutive_failures` (not
209
296
 
210
297
  Task `state` vocabulary: `todo` = ready for dispatch; `in_progress` = a workflow run is active or awaiting its next phase; `parked` = human hold, the dispatcher skips it; `done` = all phases finished. `blocked` is never a stored task state — it is derived mechanically from unmet dependencies (surfaced as a `blocked` flag on the task) and never authored by hand.
211
298
 
299
+ Park attribution: when a task parks because one of its dependencies parked (cascade-park, see `parktask`), the task record carries `park_reason: "dep_parked"` and `park_dep_id` (the parked dep's id) — machine-queryable attribution on the existing `parked` state, never prose-matched. `null` on both means an ordinary park (human hold); legacy tasks read `null` (no backfill by design). Leaving `parked` clears the attribution.
300
+
212
301
  Session `status` vocabulary: `failed` = a phase was attempted but did not finish (retryable); `rejected` = an explicit phase verdict (Review/QA said no), routed through rework. Workflows never record `blocked` for operational failures — that word is reserved for the dependency signal.
213
302
 
214
303
  Two namespaces, not one: the vocabulary above is the **dashboard API** (task states, session statuses, event types). Workflow scripts additionally return a **workflow result envelope** to their launcher (`{status: "ok" | "failed" | "parked", ...}`) and may return `{__hatchWorkflowControl: "blocked"}` as a runtime halt signal. The `"blocked"` inside `__hatchWorkflowControl` is a workflow-runtime mechanism, unrelated to the dashboard's dependency-derived `blocked` — same word, different namespace. `parked` as a workflow return means the workflow parked the task via `parktask` and the launcher should treat the run as complete.
@@ -271,6 +360,8 @@ Record the start of a workflow run. The API mints the `run_id` (UUID, SQLite-sid
271
360
 
272
361
  Returns `{ ok: true, run_id }`.
273
362
 
363
+ Refuses (2026-09-21, fail closed): starting a run for a task already in a terminal or parked state is rejected — a late start after an external park must not resurrect a `running` row.
364
+
274
365
  ### `record-run-event`
275
366
 
276
367
  Record a single telemetry event. Prefer `record-run-events-batch` for multiple events.
@@ -303,6 +394,8 @@ Record the terminal state of a workflow run.
303
394
  | `run_id` | string | Required. |
304
395
  | `status` | string | Required. One of `completed`, `failed`, `parked`, `timed_out`. |
305
396
 
397
+ Late-write guard (2026-09-21, fail closed): the end update only lands when the run's status is still `running`. A zombie/duplicate worker's late end after the row was closed to `parked` (or any other terminal state) is a silent no-op — parking is the designed terminal state for the run, and a late writer must never overwrite it.
398
+
306
399
  ### `get-run-timeline`
307
400
 
308
401
  Get a workflow run (or all runs for a task) with its events.
@@ -360,7 +453,7 @@ Retry a task whose workflow died on a platform failure. Clears the stale reserva
360
453
  - on `requeued`: running sessions → `stalled` (the dispatcher already treats `stalled` as a retry candidate);
361
454
  - on `parked`: running sessions → `failed` (what `recover-task` accepts), same as `park-task`.
362
455
 
363
- Returns `{ ok: true, action: "requeued"|"parked"|"skipped", ..., settled_sessions: <n> }`.
456
+ Returns `{ ok: true, action: "requeued"|"parked"|"skipped", ..., settled_sessions: <n>, closed_workflow_runs: <n> }` — the parked path also closes the task's open `workflow_runs` rows to `parked` in the same transaction (room #26 blocker 37, 2026-09-21).
364
457
 
365
458
  **Supersede guard:** if a newer platform run has been linked for this task since this failure's run, the task is already owned by the successor — the retry is skipped instead of clobbering live work (a late detection of an old dead run never resets a redispatched task).
366
459
 
@@ -483,9 +576,10 @@ project row carries its own publish base (`provenance_source_commit`,
483
576
  a crew home with N artifact projects carries N independent bases. There are
484
577
  two stamp call paths, and they are different operations on the same row:
485
578
 
486
- - **Full stamp** — called by the parent's publish verification (`lib/verify-publish.js`)
487
- after independently verifying the rebuilt artifact: `source_commit` is the
488
- verified build commit, `crew_release` the active release at publish time.
579
+ - **Full stamp** — called by the ack scan's `record-version-ack` (the 0.14.6
580
+ version machine) after the exact per-attempt version is observed on the
581
+ artifact's disk: `source_commit` is the acknowledged build commit,
582
+ `crew_release` the active release at publish time.
489
583
  - **Refresh** — called by the release installer (`lib/advance-publish-base.js`)
490
584
  after installing/activating a crew release: `crew_release` is refreshed to
491
585
  the live release identity while preserving the existing `source_commit`.
@@ -28,7 +28,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
28
28
  - `#provenance-refresh` — Provenance refresh for self-publishes
29
29
  - `#parent-owned-verification` — Publish content verification — parent-owned
30
30
  - `#already-merged-corrective` — Already-merged corrective
31
+ - `#false-negative-review-guard` — False-negative Review guard (room #26 blocker 34 — deleted, redesigned as the pre-Review branch-state classifier)
31
32
  - `#merge-record-helpers` — Merge-record helpers
33
+ - `#stale-merge-recovery-identity` — Stale-merge recovery identity (room #26 blocker 35)
32
34
  - `#publish-diff-base` — Publish diff base (BASE..HEAD)
33
35
  - `#attribution-limitation` — Attribution timing limitation
34
36
  - `#read-back-request` — Publish read-back request (currently unavailable)
@@ -57,6 +59,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
57
59
  - `#d1-issuance-time` — D1 issuance-time field (`issued_at`; `entry_kind` cut 2026-09-20)
58
60
  - `#detached-head-audit` — Detached-HEAD audit (0.14.2)
59
61
  - `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
62
+ - `#version-acknowledgement` — Version acknowledgement replaces content verification (0.14.6)
63
+ - `#version-review-dispositions` — 2026-09-20 amended REVIEW dispositions (14 mechanical fixes)
64
+ - `#version-not-hash` — 2026-09-20: version number replaces the sha256 nonce
60
65
 
61
66
  ## qa-reproduce.md — Decision history: qa reproduce
62
67
 
@@ -1,5 +1,18 @@
1
1
  # Decision history: publish path
2
2
 
3
+ > **SUPERSESSION NOTICE (2026-09-20, 0.14.6 version machine REVIEW):** This
4
+ > document records the decision history of the RETIRED publish machinery
5
+ > (two-party trigger, fire-and-forget observation, content verification,
6
+ > pre-trigger baselines, artifact toolcheck). The sections below describe
7
+ > that machinery in present tense as "Invariants" — they are HISTORICAL,
8
+ > not current. The current publish path is the one-party worker-owned version
9
+ > acknowledgement machine: the workflow stages a checksummed diff and
10
+ > parks with `publish: publish-requested`; the session-carrying tick
11
+ > worker claims the intent, issues the edit via `artifact_edit`, and the
12
+ > ack scan observes the exact per-attempt version on the artifact's disk as
13
+ > the sole positive evidence. See `docs/publish-verification.md` for the
14
+ > current contract.
15
+
3
16
  Relocated from workflow source comments during H5 (2026-09-18). The workflows keep only the relied-upon invariant inline; the full decision history lives here.
4
17
 
5
18
  <a id="fire-and-forget-trigger"></a>
@@ -694,16 +707,39 @@ Applies to: chore.
694
707
  <a id="already-merged-corrective"></a>
695
708
  ## Already-merged corrective
696
709
 
697
- Invariant: when Review rejects an empty branch but the work is already on main (workflow-verified sha), Wren must declare it — not re-implement or re-commit already-landed work. Scoped to the empty-branch rejection.
710
+ Invariant: whether the branch's work is already on the line is a git fact, answered BEFORE Review in git, never in prose and never by the reviewer. The pre-Review branch-state classifier (`classify-branch <task_id>` in `lib/worktree-lifecycle.sh`, pure git) emits exactly one stdout line — `BRANCH_STATE: has-work | already-merged:<40-hex> | empty-no-work` — and exits 1 with diagnostics on stderr when the branch is unclassifiable (fail closed: an unclassifiable branch is treated as empty-no-work, a mechanical FAIL).
698
711
 
699
- Applies to: standard, bugfix, chore.
712
+ Attribution is identity-first, never declared: (1) the live merge carrying the branch's CURRENT tip (`current_branch_merge`, blocker 35's identity primitive) — if the tip is on the live line via a merge, that merge IS the deliverable; (2) the task's own durable merge records (`$CREW_HOME/.merge-records/<task_id>`, append-only, preserved by post-deploy), newest first, at most 20 candidates — the first that resolves to a MERGE commit and is an ancestor of the live integration target wins, canonicalized to full 40-hex. An arbitrary non-merge ancestor is never the task's merge. The newest stale-but-resolvable record poisons older fallback.
713
+
714
+ Routing: `has-work` → Cass reviews the normal inspect; `already-merged:<sha>` → Cass reviews the frozen first-parent diff (`git diff <sha>^1 <sha>`) — she judges quality/spec compliance, never emptiness; `empty-no-work` → mechanical FAIL, Cass never dispatched — unless the plain `repo_diff: none` runtime-state marker survives in the Build notes, in which case Cass gets a plausibility judgment (the only empty-branch case she ever sees). The old empty-branch rule, the Build's SHA-bearing `repo_diff: none (already-merged: <sha>)` declaration, the declared-sha classifier input, Review hydration, and the post-rejection corrective are all deleted. The structured `review_basis` verdict field records what the review actually examined (`mechanical-fail` | `branch-diff` | `runtime-state-none` | `frozen-merge:<sha>`).
715
+
716
+ Applies to: standard, bugfix, chore. docs.js has no empty-branch rule and carries no classifier.
717
+
718
+ ```
719
+ // Already-merged corrective (room #26 blocker 34, redesign 2026-09-21):
720
+ // branch state is classified in git before Review (classify-branch):
721
+ // has-work | already-merged:<sha> | empty-no-work. The reviewer never
722
+ // adjudicates git identity.
723
+ ```
724
+
725
+ <a id="false-negative-review-guard"></a>
726
+ ## False-negative Review guard (deleted — redesigned as the pre-Review branch-state classifier)
727
+
728
+ Incident: room #26 J2 parked after cass's Review false-rejected an empty branch. The shuffle deliverable had already landed on main via the task's own prior Integrate (merge e69ae33e), so the rework Build committed nothing — and at 06:25:49Z the Build had in fact declared `repo_diff: none (already-merged: e69ae33ea11d696e428a17290fce6e33d1833312)` in its report (earlier notes omitted this declaration — the correction matters: the evidence WAS there). Cass rejected at 06:30:34Z anyway, claiming no declaration existed and confusing the verified sha with git's empty-tree object (4b825dc642cb6eb9a060e54bf8d69288fbee4904). The shared rework budget burned on the false negative and the journey parked.
729
+
730
+ First fix (deleted): a post-rejection safeguard — the workflow checked the task's merge record after Review rejected, recovered the sha, skipped the budget check for guard-fired rounds, and parsed reviewer prose for the empty-branch sentence to drive git state. It worked but answered the question AFTER the rejection, in prose, at the wrong layer.
731
+
732
+ Redesign (2026-09-21, blocker 34, v2.1): the question is answered BEFORE Review, in git, never in prose — see `#already-merged-corrective` for the canonical design. The declared-sha path was deleted from the classifier per the v2.1 reconciliation: the classifier is a pure function of (branch, merge records, target), `classify-branch <task_id>`, one argument. Merge records survive deployment (post-deploy no longer deletes them — they are the durable per-task delivery log). Routing summary: `has-work` → Cass on the normal inspect; `already-merged:<sha>` → Cass reviews the frozen first-parent diff; `empty-no-work` → mechanical FAIL without dispatching Cass, with the `REVIEW_BASIS:` session-note marker naming the basis (mechanical-fail / frozen-merge / branch-diff / runtime-state-none). The plain `repo_diff: none` runtime-state claim is captured at Build closeout from the full report (same-process Review never depends on the 2000-char session-note truncation); there is no hydration fallback for the plain claim. The rework budget applies uniformly to every rejection; no prose regex ever drives git state. The post-rejection guard, its test file, and the Case A/B budget exceptions are deleted.
733
+
734
+ Follow-up (2026-09-21, same shape): the npm `package.json` version check is also a mechanical git fact, not a reviewer judgment. `lib/worktree-lifecycle.sh` gains `version-check <task_id>` (pure git, no LLM): stdout carries only `VERSION_TOUCHED` (the branch's three-dot diff against the integration target touches the `version` field — versions are assigned at publish time, never in branches) or `VERSION_CLEAN` — anything else is a resolution failure (stderr, exit 1, fail closed). For npm projects the workflow runs it before Review; anything but an explicit `VERSION_CLEAN` joins the mechanical FAIL gate (no Cass dispatch). Cass keeps judging release necessity and version-bump scope — never whether the branch touched the field.
735
+
736
+ Applies to: standard, bugfix, chore. docs.js has no empty-branch rule and carries no classifier.
700
737
 
701
738
  ```
702
- // Already-merged corrective (room #16 blocker 11): when Review rejected
703
- // an empty branch but the work is already on main (the workflow verified
704
- // the sha), Wren must declare it — not re-implement or re-commit
705
- // already-landed work. Scoped to the empty-branch rejection; any other
706
- // rejection already carries its own specific notes.
739
+ // Room #26 blocker 34 (redesign): the classifier is pure git
740
+ // (classify-branch in the lifecycle script): has-work |
741
+ // already-merged:<sha> | empty-no-work. Git facts are never
742
+ // adjudicated by a reviewer.
707
743
  ```
708
744
 
709
745
  <a id="merge-record-helpers"></a>
@@ -721,6 +757,15 @@ Applies to: standard, bugfix, chore.
721
757
  // no clock. Byte-identical in standard.js, bugfix.js, chore.js.
722
758
  ```
723
759
 
760
+ <a id="stale-merge-recovery-identity"></a>
761
+ ## Stale-merge recovery identity (blocker 35, 2026-09-21)
762
+
763
+ Room #26 J3 parked after 3 consecutive Integrate failures. Mechanical: Integrate #1 merged locally but the push failed on an SSH host-key flake; the ERROR-after-MERGED retry path later pushed a stale merge; meanwhile rework had changed the task branch, so subsequent Integrates collided with the task's own stale merge (CONFLICT in theme.css). Two fibs: (a) the push environment is trusted but flaked; (b) "retry the push" assumed branch state == pushed state.
764
+
765
+ The fix is in the lifecycle, not the prompt. The old merged-but-unpushed recovery checked only that the RECORDED merge is an ancestor of live HEAD — ancestry proves containment, not identity. Both recovery paths (`cmd_integrate`'s ahead==0 branch and `cmd_push_target`'s record path) now prove identity via `current_branch_merge()`: the newest merge on live HEAD's first-parent chain whose branch-side parent (^2) is the branch's CURRENT tip. cmd_integrate always merges with --no-ff, so M^2 is the branch tip at merge time. The first-parent walk also covers do_push's reconcile merges above the recorded commit and the manual R5 merge (scratch-worktree merge, ff'd onto the line). No such merge plus a record means the branch moved since the merge — STALE_MERGE fails closed (never pushed, never MERGED_EMPTY: an unpushed merge may sit on the line and MERGED_EMPTY would report PASS while the publish diff still carries the abandoned work). Branch gone (merge-lease reclaim): no rework could have moved it, so the recorded merge on the line is still the deliverable — the old ancestry check is kept for exactly that case. The STALE_MERGE marker is documented in the Integrate instructions as VERDICT: FAIL (standard/bugfix/chore).
766
+
767
+ Applies to: lib/worktree-lifecycle.sh; standard, bugfix, chore (one marker line).
768
+
724
769
  <a id="publish-diff-base"></a>
725
770
  ## Publish diff base (BASE..HEAD)
726
771
 
@@ -911,6 +956,8 @@ Applies to: standard, bugfix, chore.
911
956
  <a id="already-merged-hydration"></a>
912
957
  ## Already-merged hydration
913
958
 
959
+ **Superseded (2026-09-21, blocker 34 redesign):** the hydration ferry (structured field first, notes fallback) is deleted. See `#already-merged-corrective` for the current design.
960
+
914
961
  Invariant: when the run did not execute Build itself, recover the workflow-verified sha; the structured session field is read FIRST, the notes line is only a fallback (session notes are hard-capped at 3000 chars).
915
962
 
916
963
  Applies to: standard, bugfix, chore.
@@ -932,6 +979,8 @@ Applies to: standard, bugfix, chore.
932
979
  <a id="already-merged-declaration"></a>
933
980
  ## Already-merged declaration
934
981
 
982
+ **Superseded (2026-09-21, blocker 34 redesign):** the agent-authored `repo_diff: none (already-merged: <sha>)` declaration is deleted. See `#already-merged-corrective` for the current design.
983
+
935
984
  Invariant: when the builder correctly makes no commit because the deliverable is already on main, it declares repo_diff: none (already-merged: <sha>) naming the main commit that carries the work.
936
985
 
937
986
  Applies to: standard, bugfix, chore.
@@ -1065,6 +1114,8 @@ Applies to: standard, bugfix, chore.
1065
1114
  <a id="already-merged"></a>
1066
1115
  ## Already-merged attestation
1067
1116
 
1117
+ **Superseded (2026-09-21, blocker 34 redesign):** the Build-gate attestation (`already_merged_verified:`) is deleted. See `#already-merged-corrective` for the current design.
1118
+
1068
1119
  Invariant: when the Build gate verifies already-merged, attestation is recorded.
1069
1120
 
1070
1121
  Applies to: standard, bugfix, chore.
@@ -1142,6 +1193,8 @@ Applies to: standard, bugfix, chore.
1142
1193
  <a id="already-merged-idem"></a>
1143
1194
  ## Already-merged idempotency
1144
1195
 
1196
+ **Superseded (2026-09-21, blocker 34 redesign):** the agent-authored declaration + mechanical verification is deleted. See `#already-merged-corrective` for the current design.
1197
+
1145
1198
  Invariant: an already-merged repo_diff is idempotent; no rebuild.
1146
1199
 
1147
1200
  Applies to: standard, bugfix, chore.
@@ -1239,6 +1292,8 @@ Applies to: standard, bugfix, chore.
1239
1292
  <a id="already-merged-hydra"></a>
1240
1293
  ## Already-merged hydration
1241
1294
 
1295
+ **Superseded (2026-09-21, blocker 34 redesign):** the hydration ferry is deleted. See `#already-merged-corrective` for the current design.
1296
+
1242
1297
  Invariant: when this run did not execute, hydration uses the existing merge.
1243
1298
 
1244
1299
  Applies to: standard, bugfix, chore.
@@ -1260,7 +1315,13 @@ Applies to: standard, bugfix, chore.
1260
1315
  <a id="already-merged-idem2"></a>
1261
1316
  ## Already-merged idempotency
1262
1317
 
1263
- Invariant: idempotency for already-merged tasks.
1318
+ **Superseded (2026-09-21, blocker 34 redesign):** the agent-authored
1319
+ `repo_diff: none (already-merged: <sha>)` declaration is deleted. Attribution
1320
+ is now classifier-only — see `#already-merged-corrective` for the current
1321
+ design (pre-Review `classify-branch`, identity-first from git, no declared
1322
+ sha, no hydration, no Build-closeout verification).
1323
+
1324
+ Invariant (historical): idempotency for already-merged tasks.
1264
1325
 
1265
1326
  Applies to: standard, bugfix, chore.
1266
1327
 
@@ -1361,7 +1422,7 @@ Consumers (one anchor, shared): `lib/verify-publish.js`,
1361
1422
  `lib/crew-api.js::findTriggerEntry`, the classifier
1362
1423
  (`lib/classify-publish-absence.js`), and the retry writer
1363
1424
  (`lib/retry-publish.js`) all take the oldest matching "submitted" by `ts`
1364
- and bind `issued_at || ts`. Platform nonce: deferred, not filed in this
1425
+ and bind `issued_at || ts`. Platform version: deferred, not filed in this
1365
1426
  change.
1366
1427
 
1367
1428
  Edge cases:
@@ -1560,3 +1621,180 @@ trigger anchor), lib/publish-note-vocabulary.js (publish-requested,
1560
1621
  publish-intent-claimed, publish-refused), seed/cron-body-template.md
1561
1622
  (steps 4.4b, 4.4 retry direct-issuance), docs/publish-verification.md,
1562
1623
  docs/publish-unknown-recovery.md.
1624
+
1625
+ <a id="version-acknowledgement"></a>
1626
+ ## Version acknowledgement replaces content verification (0.14.6, 2026-09-20)
1627
+
1628
+ **Eric's contract:** "If we hear that the artifact acknowledges our version,
1629
+ that's it. We don't verify against content."
1630
+
1631
+ The content-verdict machine (read-back, `verify-publish.js`, the unknown-
1632
+ recovery classifier, the retry harness) is deleted. Exact per-attempt version
1633
+ acknowledgement is the sole positive completion criterion.
1634
+
1635
+ **The version:** `sha256("publish-version:v1:" + task_id + ":" + commit + ":" +
1636
+ publish_attempt)` — deterministic, 64 hex. Synthesized into the staged diff
1637
+ as `.crew-publish-version` (checksum covers it; never committed). The workflow
1638
+ replay key (`attempt`, a string) is separate from the numeric
1639
+ `publish_attempt`.
1640
+
1641
+ **The loop:** workflow stages intent (attempt 1) → tick worker claims via
1642
+ `scan-publish-intent` → issues directly → `record-intent-issuance` writes
1643
+ `submitted` + `publish: edit-issued` → `scan-ack-pending` watches for the
1644
+ exact version on disk or in a builder report. Any issued version for the
1645
+ task/commit counts (late earlier attempts included). Refusal is terminal.
1646
+ 45-min window per attempt, 3 attempts max, 15/45-min backoff. Third expiry
1647
+ → `publish: version-timeout`.
1648
+
1649
+ **Provenance certifies request acknowledgement, not byte equality.**
1650
+
1651
+ Applies to: lib/compute-publish-diff.js (--task-id/--attempt, version hunk),
1652
+ lib/crew-api.js (scan-publish-intent, record-intent-issuance,
1653
+ record-builder-report, record-version-ack, scan-ack-pending,
1654
+ record-publish-note), lib/publish-note-vocabulary.js (WRITABLE split:
1655
+ legacy terminals recognized, not writable), workflows/{standard,bugfix,
1656
+ chore}.js (Publish exits at intent with `attempt=1`), seed/cron-body-
1657
+ template.md (tick protocol), docs/publish-verification.md,
1658
+ docs/publish-unknown-recovery.md (retired).
1659
+
1660
+ ## 2026-09-20 (post-REVIEW amendment) — bounds re-set, observation split from mutation
1661
+
1662
+ Before PUBLISH, Eric challenged the bounds as unmeasured placeholders and
1663
+ approved a replacement:
1664
+
1665
+ - **Observation is cheap and read-only; mutation is not.** The old bounds
1666
+ coupled them: the 45-minute window + 15/45-minute backoffs + 3-attempt cap
1667
+ were all guesses, and the scan ran inside the 15-minute poll tick, so a
1668
+ verdict could never land sooner than the tick cadence.
1669
+ - **New shape:** the ack scan runs on its own 5-minute `crew-ack-scan` cron
1670
+ (observation-only — it never issues edits); the 15-minute poll tick keeps
1671
+ the claim-and-issue path. The scan cadence no longer constrains verdict
1672
+ latency: with 6 scans per window, the verdict lands within one cadence of
1673
+ the ack or the expiry.
1674
+ - **New bounds:** 30-minute acknowledgement window per attempt (anchored at
1675
+ the LATEST issuance — evidence-set: room #25's one healthy ack landed
1676
+ ~15-17 minutes after issuance, so the window is ~2x that plus margin),
1677
+ immediate reissue at expiry (`not_before: null` — no backoff), and a
1678
+ 2-hour total budget from the FIRST issuance, after which the scan parks
1679
+ terminal `publish: version-timeout`. The fixed 3-attempt cap is gone — a
1680
+ slow-but-healthy artifact no longer false-timeouts, and a dead one still
1681
+ terminates on the budget. The ledger records issued_at and first-seen ack
1682
+ per attempt, so the window is tunable from measured data.
1683
+
1684
+ <a id="version-review-dispositions"></a>
1685
+ ## 2026-09-20 (amended REVIEW dispositions) — 14 mechanical fixes
1686
+
1687
+ A five-critic read-only review (Architect, Subtractor, Reliability, Room
1688
+ Operator + step-back) of the amended 0.14.6 tree returned
1689
+ ACCEPT-WITH-CONCERNS, no REJECTs. All 14 dispositions applied before the
1690
+ amended BUILD commit:
1691
+
1692
+ 1. **Torn reissue staging is note-keyed.** The ack scan's reissue
1693
+ idempotency guard is keyed on the `publish: publish-requested <commit>
1694
+ attempt=N` note — the same anchor `scan-publish-intent` claims on —
1695
+ not the ledger line. A crash between the ledger append and the note
1696
+ write (or a rolled-back COMMIT) leaves an orphan line; the note-keyed
1697
+ guard self-heals by re-appending the content-identical line. Write
1698
+ order stays line-then-note: note-first would park permanently on a
1699
+ tear.
1700
+ 2. **`publish: version-timeout` vocabulary** now reads the 2-hour
1701
+ acknowledgement-budget meaning, not "three issuance attempts
1702
+ exhausted".
1703
+ 3. **Dead backoff guard deleted** from `scan-publish-intent` (the amended
1704
+ design never writes `not_before`).
1705
+ 4. **`not_before` plumbing removed**: no longer on claimed intents,
1706
+ reissue results, or ledger lines — the field is gone entirely, not
1707
+ even null-inline (nothing reads it; a dead field is the opposite of
1708
+ the disposition).
1709
+ 5. **Version-mismatch is per-entry**: a corrupt submitted entry is
1710
+ quarantined; valid derivation-verified entries still drive the windows
1711
+ and the budget. Only when NO entry verifies does the record park as
1712
+ `version-mismatch`.
1713
+ 6. **Unparseable latest issuance is an unknown window**: never reported
1714
+ as "window-open"; the budget check still runs (a corrupt latest
1715
+ timestamp cannot defer the terminal timeout), but reissue is withheld
1716
+ until the budget decides.
1717
+ 7. **Ack-scan failure is mechanical**: nonzero exit → report stderr
1718
+ verbatim, take no other action, no rerun, no other Crew API command,
1719
+ stop.
1720
+ 8. **Tests extended** in the existing `tests/scan-publish-intent.test.js`
1721
+ (no new file): reissue idempotency across repeated scans, exact `>=`
1722
+ budget boundary (inside reissues, at-boundary times out), unparseable
1723
+ first issuance (`first-issuance-unparseable` park), unparseable latest
1724
+ issuance (reissue withheld), duplicate submitted entries (deduped),
1725
+ corrupt version beside a valid entry (quarantined), all-invalid
1726
+ versions (`version-mismatch` park), on-disk version naming no issued
1727
+ attempt (ignored), per-task receipts never cross-attributed, torn
1728
+ reissue prepare self-heals via the note-keyed guard.
1729
+ 9. **Receipt scoped per task** (verified possible): per-project
1730
+ simultaneity defaults to 2 and parked tasks don't count as in-flight,
1731
+ so two same-project tasks CAN sit in `edit-issued` concurrently — a
1732
+ shared per-slug `.crew-publish-version` would let each issuance
1733
+ overwrite the other's acknowledgement (receipt ping-pong). The
1734
+ receipt is now `.crew-publish-version-<task_id>` in the staged diff
1735
+ hunk and on disk.
1736
+ 10. **Cron id overrides validated at init**: an override must start with
1737
+ `<manifest-id>-`, so uninstall's prefix-based discovery fallback can
1738
+ always find the job even if `.cron-registry.json` is lost.
1739
+ 11. **`crew-burn-room.sh`** (outside the repo): `--crons-removed` caller
1740
+ attestation is REQUIRED — the script refuses to burn without it
1741
+ (fail-closed; it cannot reach the scheduler, so the contract is
1742
+ honest: the caller removes poll/ack-scan/update-watch/journey-watch
1743
+ via cron.remove first and attests the ids). `roomNN-journey-watch`
1744
+ added to the convention list. `--ts-spaces <slugs>` moves the room's
1745
+ artifact records into the trash (already-gone is fine).
1746
+ 12. **Stuck-publish runbook** in `docs/guide.md`: the ack scan stages
1747
+ reissues, the poll tick issues them — if the poll tick is down,
1748
+ reissues stay staged and the budget eventually parks
1749
+ `publish: version-timeout`.
1750
+ 13. **Receipt scope documented**: the version proves adoption into the
1751
+ artifact's on-disk source tree — not a successful rebuild or
1752
+ redeployment.
1753
+ 14. **Result taxonomy fixed**: `timeouts[]` is terminal-budget exhaustion
1754
+ only; window expiries are `reissued[]`.
1755
+
1756
+ **Follow-up fix found in final pre-commit inspection (same day):** the
1757
+ version-not-hash cut left the old 64-hex nonce regex in the Publish
1758
+ phase of `workflows/standard.js`, `bugfix.js`, and `chore.js` — the
1759
+ format gate would have rejected every `<commit>:<attempt>` version and
1760
+ parked every publish. All three now mirror `isPublishVersion()` from
1761
+ `lib/crew-api.js`, and `tests/compute-publish-diff.test.js` carries a
1762
+ mechanical guard asserting the workflow regex matches the library's
1763
+ (and the stale 64-hex shape is gone). The full suite passed before this
1764
+ was found because no test executes the workflow's Publish gate — the
1765
+ guard closes that hole.
1766
+
1767
+ Step-back conclusions recorded: the five-minute observer costs ~192
1768
+ marginal task-mode turns/day/room (accepted for current VERIFY
1769
+ turnaround); the machine is a compensating control for the platform's
1770
+ missing real completion receipt and should be deleted the day the
1771
+ platform issues one (see `docs/publish-verification.md`
1772
+ § Decommissioning). Non-blocking backlog: at final timeout, a
1773
+ machine-owned audit-directory check could split the park note into
1774
+ "landed-but-receiptless" vs "platform-silent".
1775
+
1776
+ <a id="version-not-hash"></a>
1777
+ ## 2026-09-20 — Version number replaces the sha256 nonce
1778
+
1779
+ Eric: a nonce is a hash plus a version number — why not just the version
1780
+ number? He was right. The sha256 defended against a forger that doesn't
1781
+ exist: the builder's failure mode is omission (zero-tool runs), never
1782
+ forging our receipts. The artifact's real failure mode — an unfaithful
1783
+ carrier that mangles bytes or ignores the request — defeats a hash and a
1784
+ plain counter identically. We own the code, so we count our own attempts;
1785
+ we don't own the artifact, so verification stays observation of its state,
1786
+ never its attestation. The hash pretended to buy attestation.
1787
+
1788
+ The per-attempt version is now `<commit>:<attempt>` (e.g.
1789
+ `4b825dc6…:2`) — no hash, no task_id (the receipt path
1790
+ `.crew-publish-version-<task_id>` and the per-task ledger scoping already
1791
+ carry the task). The commit binds the version to the exact code, so a stale
1792
+ receipt from an earlier commit can never equal a fresh version — the binding
1793
+ the hash used to provide, kept without the cryptography. Derivation is a
1794
+ plain concatenation, byte-identical between `compute-publish-diff.js` and
1795
+ `derivePublishVersion()` in `crew-api.js`; well-formedness is
1796
+ `/^[0-9a-f]{40}:[1-9][0-9]*$/` (`isPublishVersion`). The retry machine,
1797
+ windows, budget, and terminal vocabulary are unchanged — only the token got
1798
+ honest. Side benefit, and Eric's original motive: the version is
1799
+ human-readable observability ("roughly what's deployed") where the hash was
1800
+ opaque.
@@ -484,3 +484,33 @@ Applies to: standard, bugfix, chore.
484
484
  // tests/pin-location.test.js.
485
485
  ```
486
486
 
487
+
488
+ <a id="content-finding-attribution"></a>
489
+ ## Content-finding attribution (blocker 38, 2026-09-21)
490
+
491
+ Invariant: Hazel reports user-visible content observations in a machine-readable CONTENT-FINDINGS block; the workflow classifies each against the task's publish-diff file set by mechanical set membership; attributable findings become workflow-filed bugfixes; environment-attributable findings are recorded as note events with their attribution — never a bugfix; unknown attribution (diff unavailable) fails closed as "unknown", never assumed environment-attributable. The QA verdict is never overridden by finding attribution — if Hazel reports FAIL, it stands; the recorded attributions give the human evidence to distinguish environment residue from task failure.
492
+
493
+ Applies to: standard, bugfix.
494
+
495
+ ```
496
+ // Room #26 (2026-09-21): an earlier hosted audit created a "Timer
497
+ // verification deck" through the live artifact; a later shared-concurrent
498
+ // audit observed that persistent row as user-facing content and criticized
499
+ // it. The live production app.db persisted through redeploy while isolated
500
+ // audit DBs sat unused — the shared-concurrent path wrote production.
501
+ // Delphi panel converged 4–0: the required platform fix is isolation using
502
+ // the incumbent isolated workspaces; the crew-side guard classifies
503
+ // findings by attribution. Platform-side isolation is out of crew scope —
504
+ // the crew does not control the hosted shared-concurrent audit write path
505
+ // (zero repo hits; audit data lives under ~/workspace/ts-spaces/<slug>/).
506
+ // Crew-side guard (this decision): publish diff file set persisted at
507
+ // Publish; CONTENT-FINDINGS protocol in the QA prompt (Hazel never files
508
+ // content bugfixes directly); deterministic classifyContentFinding at QA
509
+ // closeout; findings recorded with attribution, never suppressed for
510
+ // looking audit-y. Unknown attribution fails closed — the verdict stands.
511
+ // "live-data" is never in a git publish diff, so it always classifies
512
+ // environment-attributable: the crew has no live-DB write path (blocker 32:
513
+ // QA writes go to a fresh per-run temp DB), so live content is never the
514
+ // task's change. Functional and non-content failures are untouched — they
515
+ // remain actionable.
516
+ ```