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.
- package/AGENTS.md +1 -1
- package/API.md +61 -7
- package/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +244 -1
- package/docs/guide.md +42 -6
- package/docs/publish-unknown-recovery.md +28 -113
- package/docs/publish-verification.md +148 -445
- package/docs/release-integrity.md +5 -3
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +4 -8
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +113 -15
- package/lib/crew-api.js +855 -725
- package/lib/publish-note-vocabulary.js +137 -24
- package/lib/schema.sql +19 -0
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-ack-scan.md +44 -0
- package/seed/cron-body-template.md +65 -65
- package/seed/crons.json +12 -0
- package/workflows/bugfix.js +146 -740
- package/workflows/chore.js +146 -740
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/standard.js +146 -763
- package/lib/build-readback-request.js +0 -140
- package/lib/classify-publish-absence.js +0 -462
- package/lib/publish-content.js +0 -154
- package/lib/readback-disk.js +0 -195
- package/lib/retry-publish.js +0 -405
- package/lib/verify-publish.js +0 -414
|
@@ -1,453 +1,156 @@
|
|
|
1
|
-
# Publish
|
|
1
|
+
# Publish acknowledgement — the version protocol (0.14.6)
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
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
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
`
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
`
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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.
|