muse-crew 0.14.4 → 0.14.6

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.
@@ -1,453 +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
- Provenance is the artifact's claim that its live content came from a specific
21
- repo commit. The workflow never stamps it. This document is the parent-side
22
- protocol. Deterministic code detects, claims, and certifies; the tick worker
23
- (the live root agent) is only the async ferry for the inspection.
12
+ ## The version
24
13
 
25
- ## Why the parent stamps
26
-
27
- The builder's `applied` report is derived from the diff the workflow carries
28
- to it — so `verifyAppliedChanges` (report vs. diff) is circular: a fabricated
29
- report passes by construction. Canary run 8 (2026-09-11) proved it: the build
30
- was hollow, the report matched the diff, the workflow stamped provenance, and
31
- all eight phases went green on stale content. The old post-hoc check
32
- (get-provenance vs. HEAD) only verified the stamp, not the content.
33
-
34
- Task `23ca8f3f` (2026-09-12, canary `orchestra-dashboard-2`) proved the report
35
- is unreliable in the other direction too: the builder applied the one-line
36
- diff, the applied-report came back `applied: []`, and the workflow's old
37
- applied-report gate parked the task fail-closed as unverified — on a publish
38
- that had actually landed. A later independent read-back of the live artifact
39
- showed the change present.
40
-
41
- The rule: **the builder's applied report is never a verification signal, in
42
- either direction.** A matching report certifies nothing (it is derived from
43
- the carried diff — circular by construction, canary run 8). A mismatching or
44
- empty report blocks nothing (false-negative mode demonstrated by `23ca8f3f`).
45
- The workflow dropped the report entirely on 2026-09-16 (clean-room task
46
- `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
47
- stochastic text channel and the runtime's JSON-candidate heuristic misfired
48
- on its prose ("workflow agent output was not JSON"), parking a task whose
49
- edit may have gone through. The trigger is now awaited and scanned for a
50
- single explicit refusal signal — `ARTIFACT_EDIT_REFUSED: <text>` as the
51
- entire trimmed turn output — and nothing else is consumed from the return.
52
- An exact refusal is conclusive negative evidence (parks `rejected`, skips
53
- observation polling); any other output (including prose quoting the signal)
54
- is inconclusive and follows the existing fail-closed observation path.
55
- `applied_report` is `missing-report` on ledger lines for issued triggers
56
- (pre-trigger parks and unattributed-unknown parks write null — no trigger
57
- was observed, so there is nothing to report). The
58
- parent ignores the (absent) report entirely when deciding whether to stamp.
59
-
60
- The contract is split on purpose:
61
-
62
- - **Workflow-owned:** carrying the merged diff to the builder, attributing
63
- the edit itself (fire-and-forget trigger — no builder report — via
64
- pre-trigger toolcheck, pre-trigger build-state baseline, and post-trigger
65
- build-state diff), the build-completion poll, post-deploy cleanup,
66
- recording the Publish session completed, and parking with `publish:
67
- verification-requested <commit>` instead of stamping. The workflow does NOT trigger the read-back inspection — an
68
- async inspection triggered from inside a workflow run delivers its result
69
- to the root agent, never back into the run, so a workflow-side trigger is
70
- an orphan the verifier cannot consume. The parent triggers the one
71
- inspection it can actually receive.
72
- - **Parent-owned (deterministic code, ferried by the tick worker):**
73
- scanning for verification-pending parks, atomically claiming them,
74
- building the read-back request, triggering the inspection, waiting for the
75
- result, comparing it mechanically against the merged diff, checking
76
- build-ID correlation and supersession, stamping provenance only on a
77
- match, reading the stamp back exactly, logging the terminal verdict, and
78
- re-queuing the task to `in_progress`.
79
-
80
- No artifact publish completes without parent-stamped provenance. A missing or
81
- mismatched read-back never stamps.
82
-
83
- ## The carried diff: BASE..HEAD from the stamped provenance
84
-
85
- The "merged diff" the workflow carries is `BASE..HEAD` where `BASE` is the
86
- previously-stamped provenance `source_commit` — the artifact's actual
87
- content — never `HEAD^1`. (Task `0c53af4e`, 2026-09-14: a push-time
88
- reconcile merge put the task's own changes behind an intermediate merge, so
89
- `HEAD^1..HEAD` carried only the reconcile delta and silently omitted the
90
- task's fix; the artifact built without it. The stamped base is the only
91
- ground truth for what the artifact already has; `BASE..HEAD` is the complete
92
- unpublished delta.)
93
-
94
- The workflow reads the base via `get-provenance` before computing the diff,
95
- and the computation is guarded mechanically:
96
-
97
- - Empty base (no provenance stamped) → the empty tree
98
- `4b825dc642cb6eb9a060e54bf8d69288fbee4904`, and only then. A present but
99
- malformed base SHA parks fail-closed.
100
- - `git merge-base --is-ancestor BASE HEAD` must pass; a non-ancestor base
101
- parks fail-closed (the stamped provenance must lead to the integrated
102
- commit, otherwise the artifact has drifted or the stamp is wrong).
103
- - The agent-reported base must equal the stamped base; a mismatch parks.
104
- - The expected base content hashes (pre-publish observation) are computed at
105
- the stamped base, not the merge parent — the artifact's tree should match
106
- the stamp, and the observation is only meaningful against it.
107
-
108
- The parent verifier (`lib/verify-publish.js`, `--base`) and the read-back
109
- request builder (`lib/build-readback-request.js`, `--base`) use the identical
110
- base: the previously-stamped provenance, or the empty tree for a genuine
111
- first publish. Request builder and verifier never disagree on the base.
112
-
113
- **Diff transport (room #14, 2026-09-17):** the diff is computed by the
114
- deterministic `lib/compute-publish-diff.js` (pinned per-run alongside the
115
- other lifecycle files), which writes the raw diff bytes to
116
- `$CREW_HOME/.publish-diffs/<taskId>[-rN].diff` and returns only a small JSON
117
- summary (`sha256`, `changed_lines` = added+removed, `has_binary`,
118
- `has_rename`). The LLM never carries diff bytes — an earlier shape asked an
119
- agent to return the raw diff as a JSON string field, and the JSON ferry
120
- dropped a valid 700-line diff ("Publish diff parsed to zero files"). The
121
- trigger agent verifies the file with `sha256sum` before any `artifact_edit`
122
- call and pastes the verified content into the edit. The 200 changed-line
123
- budget counts added+removed lines from the summary, never raw diff output
124
- lines (context lines inflated the old count ~2x).
125
-
126
- ## The shape: code detects, the tick ferries, code certifies
127
-
128
- A standalone verification workflow cannot work with the async inspection
129
- model: async inspection results are delivered to the root agent of the
130
- agent tree, never into a workflow run — so a verify workflow would wait
131
- forever for a result it can never receive. The tick worker IS the live
132
- root agent, so it is the only component that can both trigger an
133
- inspection and receive its result. (The inspection tool itself,
134
- `artifact_inspect`, was removed by the platform 2026-09-14 — see the
135
- BLOCKED notice at the top.)
136
-
137
- But the tick worker is a generalist LLM, and the certification decision is
138
- safety-critical: a misjudged "match" stamps unverified content, and nothing
139
- downstream can ever detect it (QA checks the stamp, not the content). So the
140
- LLM never judges. The division:
141
-
142
- 1. **Scan (code):** `scan-verification-pending` finds parked tasks whose
143
- latest parent note is `publish: verification-requested`, with no terminal
144
- verdict and no unexpired claim. It atomically claims each one by logging
145
- `publish: verification-claimed <expiry>` (1-hour lease) — the task stays
146
- parked, so the dispatcher never dispatches QA mid-verification, and a
147
- second tick cannot start a duplicate verification. It also reconciles the
148
- verified-but-still-parked gap (verdict recorded, re-queue lost to a crash)
149
- back to `in_progress`.
150
- 2. **Build (code):** `build-readback-request.js` builds the EXACT inspection
151
- request from the publish delta — `git diff <base> <commit>` where
152
- `<base>` is the previously-stamped provenance `source_commit` (or the
153
- empty tree for a first publish). The tick never hand-writes the request,
154
- and never uses `commit^1` as the base: push-time reconcile merges put
155
- the task's own changes behind an intermediate merge, so `commit^1`
156
- covers only the reconcile delta (2026-09-14, task `0c53af4e`).
157
- 3. **Ferry (tick worker):** runs the deterministic sensor
158
- `lib/readback-disk.js` (`--repo-path`, `--commit`, `--base` — the same
159
- base as step 2 — `--slug` from the project's `deploy_slug`, `--task-id`)
160
- and saves its stdout to the result file. The sensor exits 0 only when it
161
- actually read the working copy; on a non-zero exit the tick must NOT
162
- save stdout — log `publish: verification-procedural-error <commit>
163
- <stderr>` and leave the task parked for the next tick to retry (a sensor
164
- failure is procedural — the read could not be performed — not a content
165
- verdict). `build-readback-request.js` is retained for the manual LLM
166
- fallback below.
167
- 4. **Certify (code):** `verify-publish.js` parses the inspector's
168
- machine-readable findings block, compares every added/removed diff line
169
- against the reported present/absent verdicts, checks build-ID correlation
170
- and supersession via git, and only then stamps provenance, reads the
171
- stamp back exactly, logs the terminal verdict, and re-queues to
172
- `in_progress`. Unparseable findings, mismatches, supersession, and stamp
173
- failures all fail CLOSED with a terminal `publish: verification-failed`
174
- verdict — never a stamp. The terminal vocabulary is a closed registry in
175
- `lib/publish-note-vocabulary.js` (D7, 2026-09-19): `scan-publish-unknown`
176
- skips a recognized terminal note as terminal with its meaning named (never
177
- as `unrecognized-publish-note`), and `verify-publish.js`'s `terminal()`
178
- asserts its emitted verb is in the registry before writing.
179
- `tests/publish-note-vocabulary.test.js` closes the enum structurally:
180
- every `publish: <verb>` literal in lib/ must be declared in the terminal
181
- registry or the pinned transitional set, so no future terminal verb ships
182
- unrecognized.
183
- - **Envelope:** the tick saves the COMPLETE handoff — the full prose
184
- report AND the full JSON result, both verbatim (raw prose, JSON, or
185
- both concatenated are all accepted). Observed 2026-09-14: the
186
- platform's JSON envelope carries NO machine-readable findings
187
- block; the block lives in the prose handoff. The verifier locates
188
- the findings block in prose text and JSON string values (including
189
- double-encoded ones) and prefers the block whose file paths cover
190
- the expected diff — an echoed request template or stray prose never
191
- outranks the real block. No covering block => `unreadable-result`,
192
- fail closed. Saving JSON-only strands verification.
193
- fail closed.
194
- - **Release identity:** `scan-verification-pending` resolves
195
- `crew_release` through the crew home's `current` symlink (the immutable
196
- active release) and cross-checks it against the running code's own
197
- realpath. Unresolvable or disputed => the scan throws fail-closed
198
- BEFORE writing any claim — provenance is never stamped `unknown`, and
199
- a stale cron body running an old release cannot certify.
200
-
201
- The tick body (seed/cron-body-template.md, step 4.5) wires these together.
202
- The publisher never certifies itself, and the LLM never makes the
203
- safety-critical match decision.
204
-
205
- ## The park
206
-
207
- When the artifact build lands, the workflow parks the task with the message:
14
+ Each publish attempt carries a deterministic version:
208
15
 
209
16
  ```
210
- publish: verification-requested <commit> (build <agent_id|agent_id unobserved>) — artifact build landed, post-deploy
211
- finalized, provenance NOT stamped. Parent: run docs/publish-verification.md.
17
+ sha256("publish-version:v1:" + task_id + ":" + commit + ":" + publish_attempt)
212
18
  ```
213
19
 
214
- The parked message is stored as `Parked: publish: verification-requested
215
- <commit> …`. Match on the contained exact string
216
- `publish: verification-requested`. The `<commit>` is the merged commit whose
217
- content must be verified. The `(build …)` suffix carries the
218
- `build.agent_id` the workflow observed for this publish attempt (the
219
- artifact system's in-flight build correlation ID — the parent uses it for the
220
- build-ID correlation in step 4b); `agent_id unobserved` means the edit was
221
- accepted but the workflow never correlated it to a builder run. The merge
222
- lock is already released (post-deploy ran before the park), so the parked
223
- task holds no resources.
224
-
225
- ## Parent verification procedure
226
-
227
- The automated path is the tick body's step 4.5 (scan → build → ferry →
228
- verify). The manual fallback below is the same protocol run by hand; it
229
- exists for when the artifact namespace is unavailable to the tick worker.
230
-
231
- For a task parked with `publish: verification-requested <commit>`:
232
-
233
- 1. **Resolve the project.** Read the task's project via the Crew API
234
- (`get-project`); you need `repo_path` (the git checkout) and the artifact
235
- slug (the project's publish target).
236
- 2. **Expected change.** The publish delta is `git diff <base> <commit>`
237
- in `repo_path`, where `<base>` is the previously-stamped provenance
238
- `source_commit` for this project (read it via
239
- `get-provenance --json '{"project_id":"<id>"}'` — provenance is
240
- per-project; without `project_id` the call exits 2; use the empty-tree sha
241
- `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when no provenance is
242
- stamped yet — a first publish). Never use `commit^1` as the base and
243
- never take the expected change from the builder's report: push-time
244
- reconcile merges violate the `merge^1 == previously-published tree`
245
- invariant, so `commit^1..commit` can omit the task's own fix
246
- (2026-09-14, task `0c53af4e`).
247
- 3. **Actual content.** Run `lib/readback-disk.js` with `--repo-path`,
248
- `--commit`, `--base` (the same base as step 2), `--slug`, and
249
- `--task-id`, and save its stdout to the result file — this is the
250
- deterministic read-back; it emits the machine-readable findings block
251
- directly. (LLM fallback: if the disk working copy is unavailable, call
252
- the artifact inspector with `repair_authorized: false` and the
253
- `verbatim_request` built by `lib/build-readback-request.js`, passing
254
- `--build-agent-id` from the park message's `(build …)` suffix when it
255
- is not `agent_id unobserved`.) The findings block grammar:
256
- ```
257
- FILE: <path>
258
- ADDED: <exact added line> :: PRESENT|ABSENT
259
- REMOVED: <exact removed line> :: PRESENT|ABSENT
260
- END_FILE
261
- ```
262
- If no read-back can be obtained at all, log
263
- `publish: verification-blocked <commit> <reason>` and leave the task
264
- parked for human attention. Never stamp without a read-back.
265
- 4. **Compare mechanically.** For every added (`+`) line in the diff, the
266
- read-back's machine-readable block must report it PRESENT in the
267
- artifact's current source. For every removed (`-`) line, it must report
268
- it ABSENT — with one mechanical exemption: a removed line that also
269
- occurs verbatim in untouched code has zero discriminating power (its
270
- presence proves nothing about whether the old block survived), so the
271
- verifier exempts it instead of failing a good publish. The exemption is
272
- computed, never judged: a removed line L in file F is exempt iff L
273
- occurs in F's old tree (at `<base>`) strictly more times than the diff
274
- removes it (2026-09-15, task `00bca4b8` — a valid publish parked because
275
- two removed lines occurred identically in the untouched WorkflowSteps
276
- component). The comparison is computed by `lib/verify-publish.js` —
277
- never by eyeballing prose. A missing or malformed findings block fails
278
- closed as `unreadable-result`, never as a pass.
279
- 4b. **Build-ID correlation.** The read-back may have inspected a different
280
- build's output than this publish attempt's:
281
- 1. **Expected** = the agent_id in the park message's `(build …)` suffix.
282
- If the suffix says `agent_id unobserved`, look up the workflow's
283
- durable publish ledger at `$CREW_HOME/.publish-ledger/<slug>.jsonl`
284
- for the `submitted` entries with this `<commit>`: the OLDEST is the
285
- trigger issuance (its `agent_id` is always null — the trigger instant,
286
- not a build identity); the receipt-bearing `submitted`, if present, is
287
- a LATER entry carrying the observed `agent_id` — use that one here
288
- (if it is absent or null, this step is vacuous).
289
- 2. **Live** = whether the expected agent_id appears anywhere in the
290
- read-back result (the live artifact status exposes no durable
291
- agent_id — only an in-flight correlation ID that expires with the
292
- publish attempt, so absence is the common case, not evidence of a
293
- mismatch).
294
- 3. If expected is non-null and the read-back positively reports a
295
- DIFFERENT live build identity for this attempt's output, log
296
- `publish: build-mismatch <commit> expected <expected> observed
297
- <live>`, stay parked, never stamp, never re-queue.
298
- 4. Otherwise the content match from step 4 decides — the stamp certifies
299
- CONTENT, not the builder's identity. Log the correlation outcome
300
- (correlated / unobserved) in the `publish: verified` note.
301
- 5. **Supersession check.** Before stamping, prove the inspected live
302
- artifact still represents the commit being certified: `git rev-parse
303
- HEAD` in `repo_path` must equal `<commit>`. If HEAD has moved (a later
304
- Publish landed), the read-back is stale — log
305
- `publish: superseded <commit> by <head>` and leave the task parked for
306
- human attention. Never stamp a superseded commit.
307
- 6. **Stamp, verify the stamp, then re-queue** (all in `lib/verify-publish.js`;
308
- the manual equivalent):
309
- - **Match** — stamp provenance with the Crew API CLI `set-provenance`
310
- (the crew-owned store). Do NOT use the artifact's `setprovenance`
311
- action — it writes a different, non-authoritative store that QA never
312
- reads, so the stamp would be invisible to every gate:
313
- `set-provenance --json '{"project_id":"<id>","source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
314
- (crew_release is the basename of the active release, e.g.
315
- `pkg-0.7.10`). This is the full publish-verification stamp — it
316
- records a verified publish. Do not confuse it with the
317
- provenance *refresh* (re-pointing `crew_release` at the active
318
- release after a crew upgrade without a new publish), which also goes
319
- through `set-provenance` but keeps the existing `source_commit`.
320
- Then read the stamp back with
321
- `get-provenance --json '{"project_id":"<id>"}'`
322
- and confirm source_commit, crew_release, and task_id match exactly
323
- what was sent — a stamp that cannot be read back is not a stamp. Only
324
- then log the task note event
325
- `publish: verified <commit> (<inspection_id>)` and re-queue with
326
- `update-task` → state `in_progress` (never `todo` — `todo`
327
- restarts Triage and resets retry accounting). The dispatcher resumes
328
- at QA from the completed Publish session (standard/bugfix); chore has
329
- no QA — it proceeds to terminal completion. QA's provenance check
330
- (standard/bugfix) enforces the stamp mechanically.
331
- - **Mismatch** — do NOT stamp. Log the exact FAIL evidence with
332
- `publish: content-mismatch <commit> <details>` (quote the observed
333
- lines from the read-back) and leave the task parked. Never stamp
334
- provenance on a mismatch; never re-queue a mismatched publish into
335
- QA — QA would fail it and burn rework budget communicating a Publish
336
- problem that is not QA's to solve. The task stays parked for human
337
- attention; the loop does not retry the verification.
338
- - **Stamp failure** — if `set-provenance` fails after a matched read-back,
339
- log `publish: stamp-failed <commit> <reason>` and leave the task parked
340
- for human attention. Never re-queue an unstamped-but-verified task into
341
- QA — QA would fail it and burn rework budget on a stamping problem.
342
-
343
- ## Crash recovery
344
-
345
- - **Tick dies before triggering the inspection:** the claim expires after
346
- 1 hour; the next scan re-claims and re-verifies from scratch. The stamp
347
- is an idempotent upsert, so a duplicate verification cannot corrupt it.
348
- - **Tick dies after the inspection but before the stamp:** same as above —
349
- the next scan re-runs the whole verification (new inspection, new
350
- comparison). Wasteful but correct.
351
- - **Crash between stamp and re-queue:** the next scan sees
352
- `publish: verified` on a still-parked task and reconciles it to
353
- `in_progress`. Failure verdicts are never reconciled — they stay parked
354
- for human attention.
355
- - **Two ticks verify concurrently:** impossible — the atomic claim means the
356
- second scan sees the unexpired `publish: verification-claimed` note and
357
- skips. The lease expiry bounds the damage if a claimer dies.
358
-
359
- ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; fire-and-forget 2026-09-16)
360
-
361
- Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
362
- child failed structured closeout and the in-flight-only build-state poll
363
- could not see the completed build — even though the build HAD run (a fresh
364
- platform audit directory existed). 2026-09-16 (clean-room task `e2a8d9f8`)
365
- showed the failure is worse than a catchable throw: the runtime's
366
- JSON-candidate heuristic rejects the trigger call itself ("workflow agent
367
- output was not JSON") whenever the child returns prose, whether or not the
368
- edit went through. The trigger is therefore fire-and-forget — no schema, no
369
- consumed return value — and the workflow always attributes the edit itself.
370
- Two mechanisms close the gap.
371
-
372
- **1. Pre-trigger toolcheck + baseline.** Before the trigger, a tiny schema'd
373
- child proves the artifact tool namespace is available (one bounded retry on
374
- explicit negative evidence — the only safe retry on the publish path:
375
- without the tools the edit provably did not go through) and captures a
376
- pre-trigger build-state baseline. After the trigger, the workflow diffs the
377
- post-trigger build state against the baseline: a build whose `agent_id` is
378
- new relative to the baseline is this edit's receipt. The baseline build's
379
- `agent_id` is never substituted — a build already in flight at baseline
380
- predates the trigger and is never attributed to this edit.
381
-
382
- **2. Workflow-side durable evidence.** Before the rebuild trigger, the
383
- workflow snapshots the artifact's audit-directory listing
384
- (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). When
385
- no in-flight receipt was observed, it re-lists and diffs: a timestamped
386
- directory that appeared during the trigger window is positive evidence the
387
- edit went through and the build completed. The fallback never re-issues the
388
- edit, never stamps provenance, and only routes to the parent's independent
389
- content read-back. No new directory still parks `unknown` fail-closed. The
390
- ledger's `detail` line distinguishes the two confirmations: `… edit
391
- confirmed via durable audit evidence (new audit dir …)` vs `… build receipt
392
- captured by workflow-owned build-state observation (pre/post-trigger diff)`.
393
-
394
- The fallback's known limitation: audit directories are not attributed to
395
- tasks, so two concurrent publishes to the same artifact could cross-read.
396
- The consequence is bounded — the fallback only routes to the parent
397
- read-back, and the parent still certifies the exact commit's content
398
- mechanically (a wrong build's content fails closed as `publish:
399
- content-mismatch` / `publish: build-mismatch`, never stamps).
400
-
401
- **2. `resolve-publish-unknown` (Crew API).** For attempts already parked
402
- `unknown` before this fix: given a task parked with a latest ledger outcome
403
- of `unknown`, it derives the publish window (Integrate-completion event →
404
- unknown-outcome park event) and checks for a timestamped audit build inside
405
- that window. On evidence, it appends `unknown-resolved` to the ledger
406
- (never rewriting the original entry), writes `publish: unknown-resolved`
407
- and a mirrored `publish: verification-requested <commit>` note (the mirror
408
- is timestamped strictly later so the scan sees it as the latest), and leaves
409
- the task parked for the normal scan. Still-unknown cases stay parked:
410
- unparked task, non-`unknown` latest ledger outcome, missing commit, no audit
411
- build in the window, unobservable window, or an already-resolved attempt
412
- (idempotent).
413
-
414
- ## Exact note-event prefixes
415
-
416
- Case-sensitive, exact-prefix matches — match on prefixes, never on English
417
- meaning:
418
-
419
- - `publish: verification-requested <commit>` — workflow park; contained in
420
- the stored `Parked: …` message.
421
- - `publish: verification-claimed <ISO-expiry>` — parent scan; atomic claim
422
- with lease. Not a verdict.
423
- - `publish: verified <commit> (<inspection_id>)` — parent, after stamping
424
- AND reading the stamp back exactly; re-queued to `in_progress` (never
425
- `todo`).
426
- - `publish: content-mismatch <commit> <details>` — parent; exact FAIL
427
- evidence quoted; stays parked, never stamped, never re-queued to QA.
428
- - `publish: build-mismatch <commit> expected <expected> observed <live>` —
429
- parent; the read-back positively identified a different build's output
430
- (step 4b); stays parked, never stamped, never re-queued.
431
- - `publish: superseded <commit> by <head>` — parent; HEAD moved past the
432
- commit; stays parked for human attention.
433
- - `publish: verification-blocked <commit> <reason>` — parent; no read-back
434
- obtainable; stays parked for a human.
435
- - `publish: unknown-resolved <commit>` — recovery; durable build evidence
436
- found inside the publish window for a previously-unknown attempt (see
437
- "Unknown-outcome recovery"). The original `unknown` outcome is preserved;
438
- the mirrored `verification-requested` note (written strictly later) is
439
- what the scan claims.
440
- - `publish: stamp-failed <commit> <reason>` — parent; read-back matched but
441
- the stamp call failed; stays parked for a human.
442
- - `publish: reconciled verified-but-parked -> in_progress` — parent scan;
443
- the verified verdict was recorded but the re-queue was lost.
444
-
445
- ## Workflow differences
446
-
447
- - **standard / bugfix:** after `publish: verified`, the dispatcher resumes
448
- at QA from the completed Publish session. QA's provenance check enforces
449
- the stamp mechanically — an unstamped publish fails loudly there.
450
- - **chore:** there is no QA phase. After `publish: verified` (stamp +
451
- exact stamp read-back), the dispatcher proceeds to terminal completion.
452
- The parent's stamp read-back is the final gate — no downstream phase
453
- 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.