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
@@ -1,509 +1,156 @@
1
- # Publish content verification — parent protocol
1
+ # Publish acknowledgement — the version protocol (0.14.6)
2
2
 
3
- > **UNBLOCKED (2026-09-15):** the platform's `artifact_inspect` is still
4
- > gone, but no platform tool is needed anymore. The platform's artifact
5
- > edits land in its on-disk working copy of the artifact source
6
- > (`~/workspace/ts-spaces/<slug>/` — verified empirically 2026-09-15:
7
- > added lines present, removed lines absent across real platform commits),
8
- > so `lib/readback-disk.js` performs the read-back deterministically: it
9
- > reads the working copy and emits the exact machine-readable findings
10
- > block `lib/verify-publish.js` already parses. No LLM, no async handoff,
11
- > no prose to parse. The verifier is unchanged — the sensor changed, the
12
- > judge didn't.
3
+ > **Eric's publication contract (2026-09-20):** "If we hear that the artifact
4
+ > acknowledges our version, that's it. We don't verify against content."
13
5
  >
14
- > Authority boundary: the sensor reads the platform's working copy — the
15
- > tree the hosted artifact is built/served from. A working copy that is
16
- > stale relative to a just-applied edit yields honest ABSENT findings and
17
- > the verifier fails CLOSED (parked). Staleness can only park a task,
18
- > never stamp provenance.
6
+ > The content-verification architecture (read-back, `verify-publish.js`,
7
+ > the unknown-recovery classifier) is RETIRED. Exact per-attempt version
8
+ > acknowledgement is the SOLE positive completion criterion. Provenance
9
+ > certifies that the issuance request was acknowledged — never byte
10
+ > equality with the staged diff.
19
11
 
20
- ## One-party publish (2026-09-20, blocker 22)
12
+ ## The version
21
13
 
22
- The publish path used to be two-party: the workflow spawned a trigger
23
- child to call `artifact_edit`, then observed the outcome. Room #24 proved
24
- the child cannot reach `artifact_edit` — the tool requires a
25
- parent-conversation session ID that `agent()` children do not have, and all
26
- four journeys parked at Publish on exactly that failure.
27
-
28
- The path is now one-party: the workflow prepares the publish and parks at
29
- **intent**; the session-carrying tick worker — the only caller class that
30
- can reach `artifact_edit` — issues the edit directly in its own turn.
31
-
32
- - **Workflow-owned (Publish phase, read-only):** preflight, provenance
33
- base, checksummed diff (`lib/compute-publish-diff.js` → staged at
34
- `$CREW_HOME/.publish-diffs/<taskId>.diff`), artifact toolcheck,
35
- pre-trigger manifest baseline. It writes ONE issuer-stamped ledger entry
36
- (`outcome: "publish-intent"`, `issuer: "workflow"`, carrying `diff_path`
37
- and `diff_sha256`) and parks with
38
- `publish: publish-requested <commit> <attempt>`. It performs no edit and
39
- writes no issuance.
40
- - **Tick-worker-owned (seed/cron-body-template.md, step 4.4b):** the tick
41
- claims the intent (`publish: publish-intent-claimed <expiry>`, 1-hour
42
- lease, via `scan-publish-unknown`'s `intent` bucket), verifies the staged
43
- diff's sha256 (regenerating deterministically from `base..commit` via
44
- `lib/compute-publish-diff.js --commit` when the file is missing — a diff
45
- that cannot be (re)generated byte-identically is recorded terminally via
46
- `record-intent-unissuable` as `publish: publish-unissuable`, never
47
- re-claimed in a loop; a mismatched diff never becomes an edit), and on a
48
- re-claimed intent runs the deterministic manifest-freshness check first
49
- (`lib/check-intent-freshness.js` compares the on-disk manifest's
50
- `content_sha256`/`built_at` against the intent's `manifest_before` — the
51
- dead tick may have issued and died before recording, and a re-claim never
52
- blindly re-issues; the check's JSON evidence is recorded on a `recovered`
53
- entry). It calls `artifact_edit` directly in its own turn, then
54
- records the outcome through `record-intent-issuance` (compare-and-swap on
55
- the claim): `accepted`/`recovered` writes the issuer-stamped
56
- `submitted` ledger entry (`issuer: "tick-worker"`; a `recovered` entry's
57
- `issued_at` is the dead tick's original claim time — the lower bound on
58
- the unobserved issuance) and mirrors `publish: verification-requested`;
59
- `refused` writes `rejected` and the terminal `publish: publish-refused`
60
- note. An inconclusive edit call (tool unavailable, timeout, ambiguous
61
- result) is never recorded — the tick logs it and lets the claim expire.
62
- - **Verified re-entry:** if the tick died between issuing and mirroring,
63
- the next scan mirrors the missing `publish: verification-requested`
64
- from the issuer-stamped `submitted` entry — it never re-issues. The
65
- workflow that parked at intent never acts on the park note again: its
66
- Publish session already ended. When a verification-pending task is later
67
- re-queued and the dispatcher resumes Publish, the workflow's provenance
68
- re-entry guard (stamp has this `task_id` and `source_commit === HEAD`)
69
- returns PASS without any publish action.
70
-
71
- Everything below about parent verification (read-back, mechanical
72
- comparison, supersession, stamping) is unchanged: the one-party model
73
- changes WHO issues the edit, not what certifies it.
74
-
75
- Provenance is the artifact's claim that its live content came from a specific
76
- repo commit. The workflow never stamps it. This document is the parent-side
77
- protocol. Deterministic code detects, claims, and certifies; the tick worker
78
- (the live root agent) is only the async ferry for the inspection.
79
-
80
- ## Why the parent stamps
81
-
82
- The builder's `applied` report is derived from the diff the workflow carries
83
- to it — so `verifyAppliedChanges` (report vs. diff) is circular: a fabricated
84
- report passes by construction. Canary run 8 (2026-09-11) proved it: the build
85
- was hollow, the report matched the diff, the workflow stamped provenance, and
86
- all eight phases went green on stale content. The old post-hoc check
87
- (get-provenance vs. HEAD) only verified the stamp, not the content.
88
-
89
- Task `23ca8f3f` (2026-09-12, canary `orchestra-dashboard-2`) proved the report
90
- is unreliable in the other direction too: the builder applied the one-line
91
- diff, the applied-report came back `applied: []`, and the workflow's old
92
- applied-report gate parked the task fail-closed as unverified — on a publish
93
- that had actually landed. A later independent read-back of the live artifact
94
- showed the change present.
95
-
96
- The rule: **the builder's applied report is never a verification signal, in
97
- either direction.** A matching report certifies nothing (it is derived from
98
- the carried diff — circular by construction, canary run 8). A mismatching or
99
- empty report blocks nothing (false-negative mode demonstrated by `23ca8f3f`).
100
- The workflow dropped the report entirely on 2026-09-16 (clean-room task
101
- `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
102
- stochastic text channel and the runtime's JSON-candidate heuristic misfired
103
- on its prose ("workflow agent output was not JSON"), parking a task whose
104
- edit may have gone through. 2026-09-20 retired the trigger child outright
105
- (blocker 22: children cannot reach `artifact_edit`): the session-carrying
106
- tick worker issues the edit directly in its own turn and records the
107
- outcome itself — there is no trigger call to close out, and no report for
108
- the parent to ignore. `applied_report` survives on ledger lines only as
109
- `missing-report` (legacy) or null.
110
-
111
- The contract is split on purpose:
112
-
113
- - **Workflow-owned:** preparing the publish read-only (preflight,
114
- provenance base, checksummed diff, toolcheck, pre-trigger manifest
115
- baseline), recording the `publish-intent` ledger entry, and parking with
116
- `publish: publish-requested <commit> <attempt>` instead of stamping. The
117
- workflow never issues the edit — issuance belongs to the session-carrying
118
- tick worker (one-party publish, 2026-09-20).
119
- - **Parent-owned (deterministic code, ferried by the tick worker):**
120
- scanning for verification-pending parks, atomically claiming them,
121
- building the read-back request, triggering the inspection, waiting for the
122
- result, comparing it mechanically against the merged diff, checking
123
- build-ID correlation and supersession, stamping provenance only on a
124
- match, reading the stamp back exactly, logging the terminal verdict, and
125
- re-queuing the task to `in_progress`.
126
-
127
- No artifact publish completes without parent-stamped provenance. A missing or
128
- mismatched read-back never stamps.
129
-
130
- ## The carried diff: BASE..HEAD from the stamped provenance
131
-
132
- The "merged diff" the workflow carries is `BASE..HEAD` where `BASE` is the
133
- previously-stamped provenance `source_commit` — the artifact's actual
134
- content — never `HEAD^1`. (Task `0c53af4e`, 2026-09-14: a push-time
135
- reconcile merge put the task's own changes behind an intermediate merge, so
136
- `HEAD^1..HEAD` carried only the reconcile delta and silently omitted the
137
- task's fix; the artifact built without it. The stamped base is the only
138
- ground truth for what the artifact already has; `BASE..HEAD` is the complete
139
- unpublished delta.)
140
-
141
- The workflow reads the base via `get-provenance` before computing the diff,
142
- and the computation is guarded mechanically:
143
-
144
- - Empty base (no provenance stamped) → the empty tree
145
- `4b825dc642cb6eb9a060e54bf8d69288fbee4904`, and only then. A present but
146
- malformed base SHA parks fail-closed.
147
- - `git merge-base --is-ancestor BASE HEAD` must pass; a non-ancestor base
148
- parks fail-closed (the stamped provenance must lead to the integrated
149
- commit, otherwise the artifact has drifted or the stamp is wrong).
150
- - The agent-reported base must equal the stamped base; a mismatch parks.
151
- - The expected base content hashes (pre-publish observation) are computed at
152
- the stamped base, not the merge parent — the artifact's tree should match
153
- the stamp, and the observation is only meaningful against it.
154
-
155
- The parent verifier (`lib/verify-publish.js`, `--base`) and the read-back
156
- request builder (`lib/build-readback-request.js`, `--base`) use the identical
157
- base: the previously-stamped provenance, or the empty tree for a genuine
158
- first publish. Request builder and verifier never disagree on the base.
159
-
160
- **Diff transport (room #14, 2026-09-17):** the diff is computed by the
161
- deterministic `lib/compute-publish-diff.js` (pinned per-run alongside the
162
- other lifecycle files), which writes the raw diff bytes to
163
- `$CREW_HOME/.publish-diffs/<taskId>[-rN].diff` and returns only a small JSON
164
- summary (`sha256`, `changed_lines` = added+removed, `has_binary`,
165
- `has_rename`). The LLM never carries diff bytes — an earlier shape asked an
166
- agent to return the raw diff as a JSON string field, and the JSON ferry
167
- dropped a valid 700-line diff ("Publish diff parsed to zero files"). The
168
- trigger agent verifies the file with `sha256sum` before any `artifact_edit`
169
- call and pastes the verified content into the edit. The 200 changed-line
170
- budget counts added+removed lines from the summary, never raw diff output
171
- lines (context lines inflated the old count ~2x).
172
-
173
- ## The shape: code detects, the tick ferries, code certifies
174
-
175
- A standalone verification workflow cannot work with the async inspection
176
- model: async inspection results are delivered to the root agent of the
177
- agent tree, never into a workflow run — so a verify workflow would wait
178
- forever for a result it can never receive. The tick worker IS the live
179
- root agent, so it is the only component that can both trigger an
180
- inspection and receive its result. (The inspection tool itself,
181
- `artifact_inspect`, was removed by the platform 2026-09-14 — see the
182
- BLOCKED notice at the top.)
183
-
184
- But the tick worker is a generalist LLM, and the certification decision is
185
- safety-critical: a misjudged "match" stamps unverified content, and nothing
186
- downstream can ever detect it (QA checks the stamp, not the content). So the
187
- LLM never judges. The division:
188
-
189
- 1. **Scan (code):** `scan-verification-pending` finds parked tasks whose
190
- latest parent note is `publish: verification-requested`, with no terminal
191
- verdict and no unexpired claim. It atomically claims each one by logging
192
- `publish: verification-claimed <expiry>` (1-hour lease) — the task stays
193
- parked, so the dispatcher never dispatches QA mid-verification, and a
194
- second tick cannot start a duplicate verification. It also reconciles the
195
- verified-but-still-parked gap (verdict recorded, re-queue lost to a crash)
196
- back to `in_progress`.
197
- 2. **Build (code):** `build-readback-request.js` builds the EXACT inspection
198
- request from the publish delta — `git diff <base> <commit>` where
199
- `<base>` is the previously-stamped provenance `source_commit` (or the
200
- empty tree for a first publish). The tick never hand-writes the request,
201
- and never uses `commit^1` as the base: push-time reconcile merges put
202
- the task's own changes behind an intermediate merge, so `commit^1`
203
- covers only the reconcile delta (2026-09-14, task `0c53af4e`).
204
- 3. **Ferry (tick worker):** runs the deterministic sensor
205
- `lib/readback-disk.js` (`--repo-path`, `--commit`, `--base` — the same
206
- base as step 2 — `--slug` from the project's `deploy_slug`, `--task-id`)
207
- and saves its stdout to the result file. The sensor exits 0 only when it
208
- actually read the working copy; on a non-zero exit the tick must NOT
209
- save stdout — log `publish: verification-procedural-error <commit>
210
- <stderr>` and leave the task parked for the next tick to retry (a sensor
211
- failure is procedural — the read could not be performed — not a content
212
- verdict). `build-readback-request.js` is retained for the manual LLM
213
- fallback below.
214
- 4. **Certify (code):** `verify-publish.js` parses the inspector's
215
- machine-readable findings block, compares every added/removed diff line
216
- against the reported present/absent verdicts, checks build-ID correlation
217
- and supersession via git, and only then stamps provenance, reads the
218
- stamp back exactly, logs the terminal verdict, and re-queues to
219
- `in_progress`. Unparseable findings, mismatches, supersession, and stamp
220
- failures all fail CLOSED with a terminal `publish: verification-failed`
221
- verdict — never a stamp. The terminal vocabulary is a closed registry in
222
- `lib/publish-note-vocabulary.js` (D7, 2026-09-19): `scan-publish-unknown`
223
- skips a recognized terminal note as terminal with its meaning named (never
224
- as `unrecognized-publish-note`), and `verify-publish.js`'s `terminal()`
225
- asserts its emitted verb is in the registry before writing.
226
- `tests/publish-note-vocabulary.test.js` closes the enum structurally:
227
- every `publish: <verb>` literal in lib/ must be declared in the terminal
228
- registry or the pinned transitional set, so no future terminal verb ships
229
- unrecognized.
230
- - **Envelope:** the tick saves the COMPLETE handoff — the full prose
231
- report AND the full JSON result, both verbatim (raw prose, JSON, or
232
- both concatenated are all accepted). Observed 2026-09-14: the
233
- platform's JSON envelope carries NO machine-readable findings
234
- block; the block lives in the prose handoff. The verifier locates
235
- the findings block in prose text and JSON string values (including
236
- double-encoded ones) and prefers the block whose file paths cover
237
- the expected diff — an echoed request template or stray prose never
238
- outranks the real block. No covering block => `unreadable-result`,
239
- fail closed. Saving JSON-only strands verification.
240
- fail closed.
241
- - **Release identity:** `scan-verification-pending` resolves
242
- `crew_release` through the crew home's `current` symlink (the immutable
243
- active release) and cross-checks it against the running code's own
244
- realpath. Unresolvable or disputed => the scan throws fail-closed
245
- BEFORE writing any claim — provenance is never stamped `unknown`, and
246
- a stale cron body running an old release cannot certify.
247
-
248
- The tick body (seed/cron-body-template.md, step 4.5) wires these together.
249
- The publisher never certifies itself, and the LLM never makes the
250
- safety-critical match decision.
251
-
252
- ## The park
253
-
254
- When the artifact build lands, the workflow parks the task with the message:
14
+ Each publish attempt carries a deterministic version:
255
15
 
256
16
  ```
257
- publish: verification-requested <commit> (build <agent_id|agent_id unobserved>) — artifact build landed, post-deploy
258
- finalized, provenance NOT stamped. Parent: run docs/publish-verification.md.
17
+ sha256("publish-version:v1:" + task_id + ":" + commit + ":" + publish_attempt)
259
18
  ```
260
19
 
261
- The parked message is stored as `Parked: publish: verification-requested
262
- <commit> …`. Match on the contained exact string
263
- `publish: verification-requested`. The `<commit>` is the merged commit whose
264
- content must be verified. The `(build …)` suffix carries the
265
- `build.agent_id` the workflow observed for this publish attempt (the
266
- artifact system's in-flight build correlation ID — the parent uses it for the
267
- build-ID correlation in step 4b); `agent_id unobserved` means the edit was
268
- accepted but the workflow never correlated it to a builder run. The merge
269
- lock is already released (post-deploy ran before the park), so the parked
270
- task holds no resources.
271
-
272
- ## Parent verification procedure
273
-
274
- The automated path is the tick body's step 4.5 (scan → build → ferry →
275
- verify). The manual fallback below is the same protocol run by hand; it
276
- exists for when the artifact namespace is unavailable to the tick worker.
277
-
278
- For a task parked with `publish: verification-requested <commit>`:
279
-
280
- 1. **Resolve the project.** Read the task's project via the Crew API
281
- (`get-project`); you need `repo_path` (the git checkout) and the artifact
282
- slug (the project's publish target).
283
- 2. **Expected change.** The publish delta is `git diff <base> <commit>`
284
- in `repo_path`, where `<base>` is the previously-stamped provenance
285
- `source_commit` for this project (read it via
286
- `get-provenance --json '{"project_id":"<id>"}'` — provenance is
287
- per-project; without `project_id` the call exits 2; use the empty-tree sha
288
- `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when no provenance is
289
- stamped yet — a first publish). Never use `commit^1` as the base and
290
- never take the expected change from the builder's report: push-time
291
- reconcile merges violate the `merge^1 == previously-published tree`
292
- invariant, so `commit^1..commit` can omit the task's own fix
293
- (2026-09-14, task `0c53af4e`).
294
- 3. **Actual content.** Run `lib/readback-disk.js` with `--repo-path`,
295
- `--commit`, `--base` (the same base as step 2), `--slug`, and
296
- `--task-id`, and save its stdout to the result file — this is the
297
- deterministic read-back; it emits the machine-readable findings block
298
- directly. (LLM fallback: if the disk working copy is unavailable, call
299
- the artifact inspector with `repair_authorized: false` and the
300
- `verbatim_request` built by `lib/build-readback-request.js`, passing
301
- `--build-agent-id` from the park message's `(build …)` suffix when it
302
- is not `agent_id unobserved`.) The findings block grammar:
303
- ```
304
- FILE: <path>
305
- ADDED: <exact added line> :: PRESENT|ABSENT
306
- REMOVED: <exact removed line> :: PRESENT|ABSENT
307
- END_FILE
308
- ```
309
- If no read-back can be obtained at all, log
310
- `publish: verification-blocked <commit> <reason>` and leave the task
311
- parked for human attention. Never stamp without a read-back.
312
- 4. **Compare mechanically.** For every added (`+`) line in the diff, the
313
- read-back's machine-readable block must report it PRESENT in the
314
- artifact's current source. For every removed (`-`) line, it must report
315
- it ABSENT — with one mechanical exemption: a removed line that also
316
- occurs verbatim in untouched code has zero discriminating power (its
317
- presence proves nothing about whether the old block survived), so the
318
- verifier exempts it instead of failing a good publish. The exemption is
319
- computed, never judged: a removed line L in file F is exempt iff L
320
- occurs in F's old tree (at `<base>`) strictly more times than the diff
321
- removes it (2026-09-15, task `00bca4b8` — a valid publish parked because
322
- two removed lines occurred identically in the untouched WorkflowSteps
323
- component). The comparison is computed by `lib/verify-publish.js` —
324
- never by eyeballing prose. A missing or malformed findings block fails
325
- closed as `unreadable-result`, never as a pass.
326
- 4b. **Build-ID correlation.** The read-back may have inspected a different
327
- build's output than this publish attempt's:
328
- 1. **Expected** = the agent_id in the park message's `(build …)` suffix.
329
- If the suffix says `agent_id unobserved`, look up the workflow's
330
- durable publish ledger at `$CREW_HOME/.publish-ledger/<slug>.jsonl`
331
- for the `submitted` entries with this `<commit>`: the OLDEST is the
332
- trigger issuance (its `agent_id` is always null — the trigger instant,
333
- not a build identity); the receipt-bearing `submitted`, if present, is
334
- a LATER entry carrying the observed `agent_id` — use that one here
335
- (if it is absent or null, this step is vacuous).
336
- 2. **Live** = whether the expected agent_id appears anywhere in the
337
- read-back result (the live artifact status exposes no durable
338
- agent_id — only an in-flight correlation ID that expires with the
339
- publish attempt, so absence is the common case, not evidence of a
340
- mismatch).
341
- 3. If expected is non-null and the read-back positively reports a
342
- DIFFERENT live build identity for this attempt's output, log
343
- `publish: build-mismatch <commit> expected <expected> observed
344
- <live>`, stay parked, never stamp, never re-queue.
345
- 4. Otherwise the content match from step 4 decides — the stamp certifies
346
- CONTENT, not the builder's identity. Log the correlation outcome
347
- (correlated / unobserved) in the `publish: verified` note.
348
- 5. **Supersession check.** Before stamping, prove the inspected live
349
- artifact still represents the commit being certified: `git rev-parse
350
- HEAD` in `repo_path` must equal `<commit>`. If HEAD has moved (a later
351
- Publish landed), the read-back is stale — log
352
- `publish: superseded <commit> by <head>` and leave the task parked for
353
- human attention. Never stamp a superseded commit.
354
- 6. **Stamp, verify the stamp, then re-queue** (all in `lib/verify-publish.js`;
355
- the manual equivalent):
356
- - **Match** — stamp provenance with the Crew API CLI `set-provenance`
357
- (the crew-owned store). Do NOT use the artifact's `setprovenance`
358
- action — it writes a different, non-authoritative store that QA never
359
- reads, so the stamp would be invisible to every gate:
360
- `set-provenance --json '{"project_id":"<id>","source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
361
- (crew_release is the basename of the active release, e.g.
362
- `pkg-0.7.10`). This is the full publish-verification stamp — it
363
- records a verified publish. Do not confuse it with the
364
- provenance *refresh* (re-pointing `crew_release` at the active
365
- release after a crew upgrade without a new publish), which also goes
366
- through `set-provenance` but keeps the existing `source_commit`.
367
- Then read the stamp back with
368
- `get-provenance --json '{"project_id":"<id>"}'`
369
- and confirm source_commit, crew_release, and task_id match exactly
370
- what was sent — a stamp that cannot be read back is not a stamp. Only
371
- then log the task note event
372
- `publish: verified <commit> (<inspection_id>)` and re-queue with
373
- `update-task` → state `in_progress` (never `todo` — `todo`
374
- restarts Triage and resets retry accounting). The dispatcher resumes
375
- at QA from the completed Publish session (standard/bugfix); chore has
376
- no QA — it proceeds to terminal completion. QA's provenance check
377
- (standard/bugfix) enforces the stamp mechanically.
378
- - **Mismatch** — do NOT stamp. Log the exact FAIL evidence with
379
- `publish: content-mismatch <commit> <details>` (quote the observed
380
- lines from the read-back) and leave the task parked. Never stamp
381
- provenance on a mismatch; never re-queue a mismatched publish into
382
- QA — QA would fail it and burn rework budget communicating a Publish
383
- problem that is not QA's to solve. The task stays parked for human
384
- attention; the loop does not retry the verification.
385
- - **Stamp failure** — if `set-provenance` fails after a matched read-back,
386
- log `publish: stamp-failed <commit> <reason>` and leave the task parked
387
- for human attention. Never re-queue an unstamped-but-verified task into
388
- QA — QA would fail it and burn rework budget on a stamping problem.
389
-
390
- ## Crash recovery
391
-
392
- - **Tick dies before triggering the inspection:** the claim expires after
393
- 1 hour; the next scan re-claims and re-verifies from scratch. The stamp
394
- is an idempotent upsert, so a duplicate verification cannot corrupt it.
395
- - **Tick dies after the inspection but before the stamp:** same as above —
396
- the next scan re-runs the whole verification (new inspection, new
397
- comparison). Wasteful but correct.
398
- - **Crash between stamp and re-queue:** the next scan sees
399
- `publish: verified` on a still-parked task and reconciles it to
400
- `in_progress`. Failure verdicts are never reconciled — they stay parked
401
- for human attention.
402
- - **Two ticks verify concurrently:** impossible — the atomic claim means the
403
- second scan sees the unexpired `publish: verification-claimed` note and
404
- skips. The lease expiry bounds the damage if a claimer dies.
405
-
406
- ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; one-party 2026-09-20)
407
-
408
- Attempt 7 parked at Publish with outcome `unknown`: the old two-party
409
- path's rebuild-trigger child failed structured closeout and the
410
- in-flight-only build-state poll could not see the completed build — even
411
- though the build HAD run. The two-party machinery (trigger child,
412
- build-state observation, receipt attribution) was retired 2026-09-20:
413
- blocker 22 proved children cannot reach `artifact_edit`, so there is no
414
- trigger child anymore and no observation gap to close.
415
-
416
- What remains of unknown-recovery is the legacy classifier for parks that
417
- predate one-party publish, plus the one-party crash windows:
418
-
419
- - **Legacy `due` parks** (`publish outcome unknown` parks from the old
420
- path): `scan-publish-unknown` claims them for the deterministic
421
- six-way classifier (`lib/classify-publish-absence.js`) exactly as
422
- before. The only accepted trigger anchor is an issuer-stamped
423
- `submitted` ledger entry (`issuer: "tick-worker"` — the only issuance
424
- class that exists now); a `submitted` without issuer never binds.
425
- - **One-party crash windows:** the tick dies after issuing but before
426
- `record-intent-issuance` → the next scan mirrors
427
- `publish: verification-requested` from the issuer-stamped `submitted`
428
- entry (never re-issues). The tick dies before issuing → the intent
429
- claim expires and the next scan re-claims with `reclaimed: true`, and
430
- the manifest-freshness check decides between `recovered` and a fresh
431
- issuance — a re-claim never blindly re-issues.
432
-
433
- ### Retired two-party machinery (kept for the record)
434
-
435
- **1. Pre-trigger toolcheck + baseline.** In the two-party path, before the
436
- trigger, a tiny schema'd child proved the artifact tool namespace was
437
- available and captured a pre-trigger build-state baseline; after the
438
- trigger, the workflow diffed the post-trigger build state against the
439
- baseline for a receipt. In the one-party path the workflow still performs
440
- the read-only preflight (toolcheck + manifest baseline — the baseline is
441
- carried in the `publish-intent` ledger entry's `manifest_before`), but
442
- there is no trigger child and no receipt attribution: the tick worker
443
- issues the edit directly and records the outcome itself.
444
-
445
- **2. Workflow-side durable evidence.** In the two-party path, before the
446
- rebuild trigger, the workflow snapshotted the artifact's audit-directory
447
- listing and re-diffed it after the trigger as fallback evidence. Retired
448
- with the trigger child — the observation gap it closed no longer exists.
449
-
450
- **2. `resolve-publish-unknown` (Crew API).** For attempts already parked
451
- `unknown` before this fix: given a task parked with a latest ledger outcome
452
- of `unknown`, it derives the publish window (Integrate-completion event →
453
- unknown-outcome park event) and checks for a timestamped audit build inside
454
- that window. On evidence, it appends `unknown-resolved` to the ledger
455
- (never rewriting the original entry), writes `publish: unknown-resolved`
456
- and a mirrored `publish: verification-requested <commit>` note (the mirror
457
- is timestamped strictly later so the scan sees it as the latest), and leaves
458
- the task parked for the normal scan. Still-unknown cases stay parked:
459
- unparked task, non-`unknown` latest ledger outcome, missing commit, no audit
460
- build in the window, unobservable window, or an already-resolved attempt
461
- (idempotent).
462
-
463
- ## Exact note-event prefixes
464
-
465
- Case-sensitive, exact-prefix matches — match on prefixes, never on English
466
- meaning:
467
-
468
- - `publish: publish-requested <commit> <attempt>` — workflow park at
469
- intent (one-party publish, 2026-09-20): the merged change is staged as a
470
- checksummed diff; the tick worker owns issuance. Not a verdict.
471
- - `publish: publish-intent-claimed <ISO-expiry>` — tick-worker scan;
472
- atomic claim with 1-hour lease. Not a verdict.
473
- - `publish: publish-refused <commit>` — tick worker; the platform refused
474
- the directly-issued edit. Terminal: parked for human attention.
475
- - `publish: verification-requested <commit>` — workflow park; contained in
476
- the stored `Parked: …` message.
477
- - `publish: verification-claimed <ISO-expiry>` — parent scan; atomic claim
478
- with lease. Not a verdict.
479
- - `publish: verified <commit> (<inspection_id>)` — parent, after stamping
480
- AND reading the stamp back exactly; re-queued to `in_progress` (never
481
- `todo`).
482
- - `publish: content-mismatch <commit> <details>` — parent; exact FAIL
483
- evidence quoted; stays parked, never stamped, never re-queued to QA.
484
- - `publish: build-mismatch <commit> expected <expected> observed <live>` —
485
- parent; the read-back positively identified a different build's output
486
- (step 4b); stays parked, never stamped, never re-queued.
487
- - `publish: superseded <commit> by <head>` — parent; HEAD moved past the
488
- commit; stays parked for human attention.
489
- - `publish: verification-blocked <commit> <reason>` — parent; no read-back
490
- obtainable; stays parked for a human.
491
- - `publish: unknown-resolved <commit>` — recovery; durable build evidence
492
- found inside the publish window for a previously-unknown attempt (see
493
- "Unknown-outcome recovery"). The original `unknown` outcome is preserved;
494
- the mirrored `verification-requested` note (written strictly later) is
495
- what the scan claims.
496
- - `publish: stamp-failed <commit> <reason>` — parent; read-back matched but
497
- the stamp call failed; stays parked for a human.
498
- - `publish: reconciled verified-but-parked -> in_progress` — parent scan;
499
- the verified verdict was recorded but the re-queue was lost.
500
-
501
- ## Workflow differences
502
-
503
- - **standard / bugfix:** after `publish: verified`, the dispatcher resumes
504
- at QA from the completed Publish session. QA's provenance check enforces
505
- the stamp mechanically — an unstamped publish fails loudly there.
506
- - **chore:** there is no QA phase. After `publish: verified` (stamp +
507
- exact stamp read-back), the dispatcher proceeds to terminal completion.
508
- The parent's stamp read-back is the final gate — no downstream phase
509
- re-checks it.
20
+ - `task_id` — the crew task
21
+ - `commit` — the merge commit being published
22
+ - `publish_attempt` — 1, 2, 3 (numeric; separate from the workflow's
23
+ string replay key `attempt`)
24
+
25
+ `lib/compute-publish-diff.js --task-id <id> --attempt <n>` derives the version
26
+ and synthesizes it into the staged diff as a `.crew-publish-version-<task_id>`
27
+ new-file hunk. The receipt is scoped per task, not per slug: two tasks of
28
+ the same project can sit in `edit-issued` concurrently, and a shared
29
+ per-slug receipt would let each issuance overwrite the other's
30
+ acknowledgement. The diff checksum covers the version hunk. The version is
31
+ NEVER committed to the repo — it exists only in the staged diff and the
32
+ ledger.
33
+
34
+ ## The attempt loop
35
+
36
+ 1. **Workflow (Publish phase):** computes the diff with `--task-id` /
37
+ `--attempt 1`, records ONE issuer-stamped `publish-intent` ledger entry
38
+ (`outcome: "publish-intent"`, `issuer: "workflow"`, carrying `diff_path`,
39
+ `diff_sha256`, `version`, `base`), parks with
40
+ `publish: publish-requested <commit> attempt=1`. The workflow NEVER issues
41
+ the edit — its children cannot reach `artifact_edit`.
42
+
43
+ 2. **Tick worker (`scan-publish-intent`):** claims the parked intent (1-hour
44
+ lease, CAS on `claim_expiry`), re-derives the version (never trusts the
45
+ ledger's), verifies the staged diff's sha256, checks the base against
46
+ stamped provenance. Then issues the edit DIRECTLY in its own turn —
47
+ the tick worker is the only caller class that can reach `artifact_edit`.
48
+
49
+ 3. **Tick worker (`record-intent-issuance`):** records the outcome:
50
+ - `accepted` → issuer-stamped `submitted` ledger entry +
51
+ `publish: edit-issued` note. The ack scan owns the task from here.
52
+ - `refused` → `rejected` entry + terminal `publish: publish-refused`.
53
+ The platform said no; parked for human attention.
54
+
55
+ 4. **Ack scan (`scan-ack-pending`):** every tick, for each `edit-issued` task:
56
+ - Reads the artifact's on-disk `.crew-publish-version-<task_id>` receipt — the
57
+ SOLE positive evidence (2026-09-20 REVIEW removed the circular
58
+ builder-report positive path).
59
+ - A builder report that echoes the EXACT version with outcome `refused`
60
+ (`record-builder-report` mechanically requires the version in
61
+ `report_text`) is the explicit-refusal channel — never an ack.
62
+ - **Any exact on-disk acknowledgement for an issued version on the same
63
+ task/commit counts** — including a late earlier attempt's version.
64
+ - On ack: stamps provenance, writes terminal
65
+ `publish: version-acknowledged`, re-queues the task.
66
+ - On explicit refusal: terminal `publish: publish-refused`.
67
+ - On window expiry (30 minutes per attempt, anchored at the LATEST
68
+ issuance): stages attempt N+1 with a FRESH version, immediately
69
+ claimable (no backoff).
70
+ - When the 2-hour total budget from the FIRST issuance is exhausted:
71
+ terminal `publish: version-timeout`.
72
+
73
+ ## Bounds (Eric's, re-set 2026-09-20)
74
+
75
+ - 30-minute acknowledgement window per attempt (anchored at the latest issuance)
76
+ - 2-hour total budget from the first issuance — then terminal `publish: version-timeout`
77
+ - No backoff: a reissue is immediately claimable
78
+ - The window is evidence-set: room #25's one healthy ack landed ~15-17
79
+ minutes after issuance — the window is ~2x that plus margin. The ledger
80
+ records issued_at and first-seen ack per attempt, so the distribution is
81
+ measurable and the window is tunable from real data
82
+ - Explicit refusal is terminal (never re-issued)
83
+ - Silence is unknown — fails closed
84
+
85
+ ## What the version proves
86
+
87
+ The version on the artifact's disk proves the platform applied THIS attempt's
88
+ edit request. It does NOT prove the content matches the staged diff byte-
89
+ for-byte — and per Eric's contract, we don't check. The builder is trusted
90
+ to apply the diff it was given; the version proves the request landed.
91
+
92
+ **Receipt scope.** The version receipt proves the edit was adopted into the
93
+ artifact's on-disk source tree — NOT that a rebuild succeeded, and NOT
94
+ that a redeployment picked it up. A version acknowledgement followed by a
95
+ failed build leaves the served bundle stale with a green publish verdict;
96
+ that gap belongs to the artifact platform's build pipeline, not to this
97
+ machine.
98
+
99
+ ## Operator recovery
100
+
101
+ A task parked at Publish with a terminal note (`publish: version-timeout`,
102
+ `publish: publish-refused`, `publish: publish-unissuable`) is the designed
103
+ human decision point — parked→Todo is a human call, never automatic.
104
+
105
+ **Diagnose before requeue.** The park note names the reason; the evidence
106
+ lives in two places:
107
+
108
+ - `$CREW_HOME/.publish-ledger/<deploy-slug>.jsonl` — the append-only
109
+ ledger: every intent, issuance, acknowledgement, and refusal for the
110
+ task, with commits, attempts, versions, and timestamps.
111
+ - `$CREW_HOME/.publish-diffs/<task-id>-a<attempt>.diff` — the staged
112
+ checksummed diff that was (or was to be) issued.
113
+
114
+ **Timeout semantics.** `publish: version-timeout` means the version was
115
+ unobserved within the 2-hour total budget — NOT "not deployed." The edit may
116
+ still have landed (the platform applied it but the receipt was never
117
+ observed, or the observation raced the window). Before requeueing:
118
+
119
+ 1. Check the artifact's current on-disk source for the version. If present,
120
+ the edit landed — stamp provenance manually or requeue to let the ack
121
+ scan observe it.
122
+ 2. If absent, the edit likely did not land. Fix the cause (tool
123
+ availability, platform refusal, stale base), then move the task to Todo.
124
+ The next Publish run stages a fresh diff with a fresh version.
125
+
126
+ **Never requeue blindly.** A timeout without diagnosis risks a duplicate
127
+ edit (the unobserved edit may have landed). The machine fails closed for
128
+ exactly this reason — the human opens it only with evidence.
129
+
130
+ ## Decommissioning
131
+
132
+ This machine is a compensating control for the platform's missing real
133
+ completion receipt: `artifact_edit` returns no build handle and no
134
+ deployment verdict, so the crew attributes completion by content (the
135
+ version) instead. The day the platform issues a trustworthy receipt — one
136
+ that names the edit, its outcome, and its build — delete this machine:
137
+ the version derivation, the receipt file, both scans' publish paths, and
138
+ the `publish:` note verbs that belong to them. Until then, the
139
+ compensating control IS the receipt.
140
+
141
+ ## Retired
142
+
143
+ - `lib/verify-publish.js` — content verifier (deleted)
144
+ - `lib/readback-disk.js` — disk read-back sensor (deleted)
145
+ - `lib/build-readback-request.js` — read-back request builder (deleted)
146
+ - `lib/classify-publish-absence.js` — unknown-recovery classifier (deleted)
147
+ - `lib/check-intent-freshness.js` — intent freshness check (deleted)
148
+ - `lib/retry-publish.js` — retry harness (deleted)
149
+ - `lib/publish-content.js` — publish content helper (deleted)
150
+
151
+ The `publish: verification-requested`, `publish: verification-claimed`,
152
+ `publish: ambiguous`, `publish: retry-superseded`, `publish: retry-refused`,
153
+ `publish: verified`, `publish: superseded`, `publish: verification-failed`,
154
+ and `publish: intent-unverifiable` note verbs are legacy — the exact
155
+ registry in `lib/publish-note-vocabulary.js` (no wildcard families).
156
+ Recognized so old history skips as terminal, never written anew.