muse-crew 0.7.9 → 0.7.11

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/API.md CHANGED
@@ -80,6 +80,16 @@ Atomically park a task for human attention: sets `state` to `parked`, writes the
80
80
 
81
81
  Returns `{ "ok": true, "task": {...}, "settled_sessions": <n> }` with the updated task record and the count of sessions settled (`0` when none were running).
82
82
 
83
+ ### `resolvepublishunknown`
84
+
85
+ Recovery contract for publish attempts parked with an `unknown` outcome (2026-09-14): the rebuild trigger's child failed structured closeout and the in-flight-only build-state poll could not see the completed build, so the workflow parked fail-closed — but the edit may still have gone through. When a platform audit build completed inside the publish window (between the Integrate-completion event and the unknown-outcome park), this action routes the task to the parent's independent content verification WITHOUT re-issuing the edit and WITHOUT stamping provenance. The original unknown ledger entry and park event are preserved; the resolution is appended to the publish ledger (`outcome: "unknown-resolved"`) and to the event log, and a `publish: verification-requested <commit>` note is written so `scan-verification-pending` claims the task on the next tick. The parent's read-back ([publish verification](docs/publish-verification.md)) remains the real verification and can still fail terminally. Cases that stay parked with `{ "resolved": false, ... }`: task not parked, latest ledger outcome not `unknown`, no usable commit on the unknown entry, no audit build inside the publish window, unobservable publish window (missing Integrate-completion or park event), or an already-resolved attempt (idempotent).
86
+
87
+ | Field | Type | Required | Notes |
88
+ |-------|------|----------|-------|
89
+ | `task_id` | uuid | yes | Must be parked with a latest publish-ledger outcome of `unknown` |
90
+
91
+ Returns `{ "resolved": true, "task_id", "commit", "evidence_dir", "audit_report_ok" }` on success, or `{ "resolved": false, "reason" }` when the recovery preconditions are not met.
92
+
83
93
  ---
84
94
 
85
95
  ## Sessions
@@ -477,21 +487,19 @@ Phases whose outcome drives control flow (Build, Review, QA, Reproduce, Integrat
477
487
  Artifact Publish passes only when the parent has verified content independently:
478
488
 
479
489
  1. The workflow carries the merged diff to the artifact builder, polls the
480
- build to completion, triggers an independent `artifact_inspect` read-back
481
- of the changed regions, and parks with
490
+ build to completion, and parks with
482
491
  `publish: verification-requested <commit>` — it never stamps provenance
483
492
  and never treats the builder's applied-changes report as verification.
484
493
  The applied report is derived from the carried diff and demonstrated an
485
494
  unreliable false-negative mode (2026-09-12: `applied:[]` for a diff the
486
- builder had applied); it is logged as observation only. The read-back
487
- request carries the observed builder build identifier (`build.agent_id`)
488
- so the read-back can be correlated to the exact builder run that built
489
- this attempt.
490
- 2. The parent compares the read-back against the merged diff mechanically
491
- (every added line present, every removed line absent), correlates the
492
- builder build identifier, and stamps provenance via `set-provenance` only
493
- on a match. A mismatch, a build-id mismatch, or an unobtainable read-back
494
- never stamps; the task stays parked.
495
+ builder had applied); it is logged as observation only.
496
+ 2. The parent's verification protocol (docs/publish-verification.md) owns
497
+ the independent content confirmation. The independent read-back step is
498
+ currently unavailable: `artifact_inspect` was removed by the platform
499
+ (2026-09-14) and no agent-callable replacement exists (`artifact.inspect`
500
+ is malfunction diagnosis, not a read-back tool). Until a read-back path
501
+ exists, the parent cannot confirm content independently and the task
502
+ stays parked at `publish: verification-requested`.
495
503
 
496
504
  Verification fails closed. The loop never retries a verification and never
497
505
  issues a blind re-publish on an unknown outcome.
package/docs/guide.md CHANGED
@@ -302,7 +302,7 @@ For tasks that change anything rendered and visible in the project's artifact ("
302
302
 
303
303
  ### Publish content verification
304
304
 
305
- The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself: after the build lands it triggers an independent `artifact_inspect` read-back of the changed regions and parks with `publish: verification-requested <commit> (build <agent_id|agent_id unobserved>)`. The parent compares the read-back against the merged diff and stamps provenance only on a match (parent-driven — see `docs/publish-verification.md`); the park message records the observed builder build identifier as `(build <agent_id|agent_id unobserved>)`, and the parent correlates the read-back's live build agent_id against it — a mismatch logs `publish: build-mismatch <commit> …`, stays parked, and is never stamped (parent-driven — see `docs/publish-verification.md` step 4b). QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
305
+ The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself: after the build lands it parks with `publish: verification-requested <commit> (build <agent_id|agent_id unobserved>)`. The parent protocol owns the independent content confirmation (parent-driven — see `docs/publish-verification.md`); the park message records the observed builder build identifier as `(build <agent_id|agent_id unobserved>)`, and the parent correlates the read-back's live build agent_id against it — a mismatch logs `publish: build-mismatch <commit> …`, stays parked, and is never stamped (parent-driven — see `docs/publish-verification.md` step 4b). The independent read-back step is currently unavailable: `artifact_inspect` was removed by the platform (2026-09-14) and no agent-callable replacement exists (`artifact.inspect` is malfunction diagnosis, not a read-back tool), so the parent cannot confirm content independently and tasks stay parked at verification-requested until a read-back path exists. QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
306
306
 
307
307
  ## Identities
308
308
 
@@ -430,4 +430,4 @@ crew-release.sh current
430
430
 
431
431
  5. **No backoff on retry.** Failed phases retry on the next tick with no delay. The consecutive-failure cap (default 3, then park — see "Failure handling and retry") bounds the retries, but there's no exponential backoff between attempts.
432
432
 
433
- 6. **QA is code-blind.** Hazel uses `artifact_inspect` for visual/functional testing. She cannot read source code — this is by design (context isolation), but it means QA catches only what's visible or inspectable through the artifact's public interface.
433
+ 6. **QA is code-blind.** Hazel never reads source code — this is by design (context isolation), so QA covers the mechanical checks only: data-level effects via the Crew API, the docs gate, and the provenance check. There is currently no agent-callable visual-inspection tool (`artifact_inspect` was removed by the platform 2026-09-14; `artifact.inspect` is malfunction diagnosis, not a substitute), so visual/functional inspection of rendered output is unavailable until a capture path exists (see `docs/visual-verdict.md`).
@@ -0,0 +1,123 @@
1
+ # OODA Report
2
+
3
+ The experiential report produced by the Reproduce and QA phases' see-act loop
4
+ (2026-09-14; hardened 2026-09-15). When an agent drives the browser one step
5
+ at a time — observing each frame, deciding the next action — the complete
6
+ record of that session is the **OODA report**: the ordered
7
+ Observe/Orient/Decide/Act trace, the archived frames, and a machine-readable
8
+ terminal verdict.
9
+
10
+ ## Location
11
+
12
+ ```
13
+ $CREW_HOME/task-evidence/<taskId>/<phase>/
14
+ ooda-log.jsonl # ordered step records (every attempt preserved)
15
+ verdict.json # latest terminal verdict (what the workflow reads)
16
+ verdicts.jsonl # append-only ledger: every attempt's verdict, never overwritten
17
+ 001-shot-desktop.png
18
+ 002-click-desktop.png
19
+ ...
20
+ ```
21
+
22
+ `<phase>` is `repro` (bugfix Reproduce), `postchange` (standard QA, bugfix QA).
23
+
24
+ ## ooda-log.jsonl
25
+
26
+ One JSON object per line, appended after every browser action via
27
+ `lib/append-ooda-step.js`:
28
+
29
+ ```json
30
+ {"step":1,"attempt":"1","action":"aria","args":{},"exit":0,"screenshot":null,"observation":"Login button found in header.","ts":"..."}
31
+ {"step":2,"attempt":"1","action":"shot","args":{},"exit":0,"screenshot":"/abs/.../001-shot-desktop.png","observation":"Header renders, no console errors.","ts":"..."}
32
+ ```
33
+
34
+ Schema: `step` (positive int), `attempt` (the attempt/run identity —
35
+ `"1"`, `"2"` for successive QA loops, `"repro-1"` for a reproduce run),
36
+ `action`, `args` (object), `exit` (the see-act exit code), `screenshot`
37
+ (absolute path or null), `observation` (the agent's 1–2 sentences: what was
38
+ seen and what it implies — this is the payload, not the filename), `ts`
39
+ (optional, agent-supplied).
40
+
41
+ **Attempts never overwrite.** Step numbers are strictly monotonic *per
42
+ attempt*: an attempt's first step is 1 and each following step is exactly
43
+ previous+1. A retry starts a NEW `--attempt` in the same log — it never
44
+ renumbers or overwrites. A non-monotonic step, a gap, or a corrupt line fails
45
+ loudly (exit 2) and appends nothing.
46
+
47
+ **Actions** (2026-09-15): `aria|shot|click|scroll|type` (see-act driver) plus
48
+ the image tools — `crop|zoom|label|nup` (`lib/edit-image.py`: pixel-exact
49
+ crop, pixel-crisp zoom, caption bars, n-up grids) and `compose`
50
+ (`lib/render-html.js`: Chromium-rendered HTML compositions — the reef-qa
51
+ pattern, real typography and layout for before/after and annotated evidence).
52
+ Every frame-producing action (`shot|crop|zoom|label|nup|compose`) requires
53
+ `--screenshot`: the frame the agent looked at must be the frame it logs. A
54
+ captured-but-unlogged frame is invisible to everyone after the agent.
55
+
56
+ The writer validates and fails loudly (exit 2) on bad input. The agent never
57
+ hand-writes JSON — it supplies fields as flags.
58
+
59
+ ## verdict.json and verdicts.jsonl
60
+
61
+ Written at phase end via `lib/write-ooda-verdict.js --dir <phase-dir>
62
+ --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE>`:
63
+
64
+ ```json
65
+ {
66
+ "verdict": "PASS",
67
+ "attempt": "1",
68
+ "summary": "Footer fix verified on desktop and mobile.",
69
+ "expected": "published N hr ago, no Last polled mislabel",
70
+ "actual": "published 6 hr ago on both viewports",
71
+ "missing_evidence": []
72
+ }
73
+ ```
74
+
75
+ `verdict` is `PASS`, `FAIL`, or `NOT_POSSIBLE`. `missing_evidence` lists what
76
+ was not checked — an honest gap is recorded, never hidden. Unknown/
77
+ inconclusive is neither PASS nor FAIL.
78
+
79
+ Two records are written:
80
+
81
+ - `verdict.json` — the LATEST verdict, what the workflow closeout reads.
82
+ Overwritten on each call.
83
+ - `verdicts.jsonl` — the append-only ledger. One JSON line per verdict,
84
+ NEVER overwritten: `{seq, attempt, verdict, summary, expected, actual,
85
+ missing_evidence[], reason?, ts?}`. `seq` is assigned mechanically
86
+ (existing lines + 1). A QA retry that FAILs after an earlier PASS keeps
87
+ both — a later attempt can never silently erase an earlier verdict.
88
+
89
+ ## Screenshot archival
90
+
91
+ `SEE_ACT_ARCHIVE_DIR=<phase-dir>` (set on every see-act invocation — shell env
92
+ vars do not persist between the agent's commands) funnels every screenshot
93
+ into the phase dir automatically:
94
+
95
+ - `--out` becomes optional; the archived path is the durable record.
96
+ - Sequential names: `001-shot-desktop.png`, `002-click-mobile.png`, ...
97
+ (counter in `<dir>/.seq` — stable across the driver's one-process-per-invocation shape).
98
+ - The JSON response carries `screenshot` (what to READ) and `archived` (the durable copy).
99
+ - Explicit `--out` + archive: the file is captured to `--out` AND copied into the archive.
100
+ - An unusable archive dir is `NOT POSSIBLE` (exit 3) — lost evidence fails loudly, never silently.
101
+
102
+ No frame can be lost: Reproduce, QA, and exploratory hunting all funnel through
103
+ the same wrapper.
104
+
105
+ ## Workflow integration
106
+
107
+ - **Standard QA** (artifact): archive to `task-evidence/<task>/postchange/`,
108
+ bounded at 8 steps, logs every step, writes verdict.
109
+ - **Bugfix Reproduce**: archive to `task-evidence/<task>/repro/`, bounded at 8
110
+ steps, logs every step, writes verdict (the bug is confirmed or not).
111
+ - **Bugfix QA**: archive to `task-evidence/<task>/postchange/`, bounded at 8
112
+ steps, re-runs the reproduction steps plus surrounding views, logs every
113
+ step, writes verdict.
114
+
115
+ No nested agents — the depth-1 work agent runs the browser steps itself.
116
+
117
+ ## Reading a report
118
+
119
+ 1. Read `verdict.json` for the terminal claim.
120
+ 2. Read `ooda-log.jsonl` for the ordered trace — each observation next to its frame.
121
+ 3. Open the archived PNGs and compare against the written observations. Never trust prose alone.
122
+
123
+ The report is the evidence. A QA claim without its OODA report is an unverified claim.
@@ -1,9 +1,16 @@
1
1
  # Publish content verification — parent protocol
2
2
 
3
+ > **BLOCKED (2026-09-14):** `artifact_inspect` was removed by the platform.
4
+ > No agent-callable replacement exists (`artifact.inspect` is malfunction
5
+ > diagnosis, not a read-back tool), so the ferry step below cannot currently
6
+ > run. Tasks park at `publish: verification-requested` and stay parked until
7
+ > a read-back path exists. The rest of this document describes the protocol
8
+ > as designed, so the shape is preserved for when the capability returns.
9
+
3
10
  Provenance is the artifact's claim that its live content came from a specific
4
- repo commit. The workflow used to stamp it; it no longer does. This document
5
- is the parent-side runbook. The workflow script handles the mechanical
6
- halves; the parent handles the stamp.
11
+ repo commit. The workflow never stamps it. This document is the parent-side
12
+ protocol. Deterministic code detects, claims, and certifies; the tick worker
13
+ (the live root agent) is only the async ferry for the inspection.
7
14
 
8
15
  ## Why the parent stamps
9
16
 
@@ -34,37 +41,126 @@ The contract is split on purpose:
34
41
 
35
42
  - **Workflow-owned:** carrying the merged diff to the builder, the
36
43
  applied-report observation (logged, never a park), the build-completion
37
- poll, post-deploy cleanup, triggering an independent `artifact_inspect`
38
- read-back of the changed regions (carrying the observed builder build
39
- identifier for correlation), recording the Publish session completed, and
44
+ poll, post-deploy cleanup, recording the Publish session completed, and
40
45
  parking with `publish: verification-requested <commit>` instead of
41
- stamping.
42
- - **Parent-owned:** reading back the artifact's actual content, comparing it
43
- mechanically against the merged diff, checking for supersession, stamping
44
- provenance only on a match, reading the stamp back exactly, and re-queuing
45
- the task to `in_progress`. In standard/bugfix, QA's provenance check then
46
- enforces the stamp an unstamped publish fails loudly in QA instead of
47
- passing silently. Chore has no QA: the parent's stamp read-back is the
48
- final gate.
46
+ stamping. The workflow does NOT trigger the read-back inspection — an
47
+ async inspection triggered from inside a workflow run delivers its result
48
+ to the root agent, never back into the run, so a workflow-side trigger is
49
+ an orphan the verifier cannot consume. The parent triggers the one
50
+ inspection it can actually receive.
51
+ - **Parent-owned (deterministic code, ferried by the tick worker):**
52
+ scanning for verification-pending parks, atomically claiming them,
53
+ building the read-back request, triggering the inspection, waiting for the
54
+ result, comparing it mechanically against the merged diff, checking
55
+ build-ID correlation and supersession, stamping provenance only on a
56
+ match, reading the stamp back exactly, logging the terminal verdict, and
57
+ re-queuing the task to `in_progress`.
49
58
 
50
59
  No artifact publish completes without parent-stamped provenance. A missing or
51
60
  mismatched read-back never stamps.
52
61
 
62
+ ## The carried diff: BASE..HEAD from the stamped provenance
63
+
64
+ The "merged diff" the workflow carries is `BASE..HEAD` where `BASE` is the
65
+ previously-stamped provenance `source_commit` — the artifact's actual
66
+ content — never `HEAD^1`. (Task `0c53af4e`, 2026-09-14: a push-time
67
+ reconcile merge put the task's own changes behind an intermediate merge, so
68
+ `HEAD^1..HEAD` carried only the reconcile delta and silently omitted the
69
+ task's fix; the artifact built without it. The stamped base is the only
70
+ ground truth for what the artifact already has; `BASE..HEAD` is the complete
71
+ unpublished delta.)
72
+
73
+ The workflow reads the base via `get-provenance` before computing the diff,
74
+ and the computation is guarded mechanically:
75
+
76
+ - Empty base (no provenance stamped) → the empty tree
77
+ `4b825dc642cb6eb9a060e54bf8d69288fbee4904`, and only then. A present but
78
+ malformed base SHA parks fail-closed.
79
+ - `git merge-base --is-ancestor BASE HEAD` must pass; a non-ancestor base
80
+ parks fail-closed (the stamped provenance must lead to the integrated
81
+ commit, otherwise the artifact has drifted or the stamp is wrong).
82
+ - The agent-reported base must equal the stamped base; a mismatch parks.
83
+ - The expected base content hashes (pre-publish observation) are computed at
84
+ the stamped base, not the merge parent — the artifact's tree should match
85
+ the stamp, and the observation is only meaningful against it.
86
+
87
+ The parent verifier (`lib/verify-publish.js`, `--base`) and the read-back
88
+ request builder (`lib/build-readback-request.js`, `--base`) use the identical
89
+ base: the previously-stamped provenance, or the empty tree for a genuine
90
+ first publish. Request builder and verifier never disagree on the base.
91
+
92
+ ## The shape: code detects, the tick ferries, code certifies
93
+
94
+ A standalone verification workflow cannot work with the async inspection
95
+ model: async inspection results are delivered to the root agent of the
96
+ agent tree, never into a workflow run — so a verify workflow would wait
97
+ forever for a result it can never receive. The tick worker IS the live
98
+ root agent, so it is the only component that can both trigger an
99
+ inspection and receive its result. (The inspection tool itself,
100
+ `artifact_inspect`, was removed by the platform 2026-09-14 — see the
101
+ BLOCKED notice at the top.)
102
+
103
+ But the tick worker is a generalist LLM, and the certification decision is
104
+ safety-critical: a misjudged "match" stamps unverified content, and nothing
105
+ downstream can ever detect it (QA checks the stamp, not the content). So the
106
+ LLM never judges. The division:
107
+
108
+ 1. **Scan (code):** `scan-verification-pending` finds parked tasks whose
109
+ latest parent note is `publish: verification-requested`, with no terminal
110
+ verdict and no unexpired claim. It atomically claims each one by logging
111
+ `publish: verification-claimed <expiry>` (1-hour lease) — the task stays
112
+ parked, so the dispatcher never dispatches QA mid-verification, and a
113
+ second tick cannot start a duplicate verification. It also reconciles the
114
+ verified-but-still-parked gap (verdict recorded, re-queue lost to a crash)
115
+ back to `in_progress`.
116
+ 2. **Build (code):** `build-readback-request.js` builds the EXACT inspection
117
+ request from the publish delta — `git diff <base> <commit>` where
118
+ `<base>` is the previously-stamped provenance `source_commit` (or the
119
+ empty tree for a first publish). The tick never hand-writes the request,
120
+ and never uses `commit^1` as the base: push-time reconcile merges put
121
+ the task's own changes behind an intermediate merge, so `commit^1`
122
+ covers only the reconcile delta (2026-09-14, task `0c53af4e`).
123
+ 3. **Ferry (tick worker):** when a read-back tool is available, calls it
124
+ with the built request, waits for the async handoff, saves the full
125
+ result JSON to a file. (Currently blocked — no agent-callable tool
126
+ exists.)
127
+ 4. **Certify (code):** `verify-publish.js` parses the inspector's
128
+ machine-readable findings block, compares every added/removed diff line
129
+ against the reported present/absent verdicts, checks build-ID correlation
130
+ and supersession via git, and only then stamps provenance, reads the
131
+ stamp back exactly, logs the terminal verdict, and re-queues to
132
+ `in_progress`. Unparseable findings, mismatches, supersession, and stamp
133
+ failures all fail CLOSED with a terminal `publish: verification-failed`
134
+ verdict — never a stamp.
135
+ - **Envelope:** the tick saves the COMPLETE handoff — the full prose
136
+ report AND the full JSON result, both verbatim (raw prose, JSON, or
137
+ both concatenated are all accepted). Observed 2026-09-14: the
138
+ platform's JSON envelope carries NO machine-readable findings
139
+ block; the block lives in the prose handoff. The verifier locates
140
+ the findings block in prose text and JSON string values (including
141
+ double-encoded ones) and prefers the block whose file paths cover
142
+ the expected diff — an echoed request template or stray prose never
143
+ outranks the real block. No covering block => `unreadable-result`,
144
+ fail closed. Saving JSON-only strands verification.
145
+ fail closed.
146
+ - **Release identity:** `scan-verification-pending` resolves
147
+ `crew_release` through the crew home's `current` symlink (the immutable
148
+ active release) and cross-checks it against the running code's own
149
+ realpath. Unresolvable or disputed => the scan throws fail-closed
150
+ BEFORE writing any claim — provenance is never stamped `unknown`, and
151
+ a stale cron body running an old release cannot certify.
152
+
153
+ The tick body (seed/cron-body-template.md, step 4.5) wires these together.
154
+ The publisher never certifies itself, and the LLM never makes the
155
+ safety-critical match decision.
156
+
53
157
  ## The park
54
158
 
55
159
  When the artifact build lands, the workflow parks the task with the message:
56
160
 
57
161
  ```
58
162
  publish: verification-requested <commit> (build <agent_id|agent_id unobserved>) — artifact build landed, post-deploy
59
- finalized, provenance NOT stamped. Parent: run docs/publish-verification.md
60
- (content read-back inspection <inspection_id> already triggered).
61
- ```
62
-
63
- or, if the workflow's inspect trigger failed:
64
-
65
- ```
66
- publish: verification-requested <commit> (build <agent_id|agent_id unobserved>) — ... (read-back inspect trigger
67
- failed: <reason> — parent: trigger artifact_inspect manually).
163
+ finalized, provenance NOT stamped. Parent: run docs/publish-verification.md.
68
164
  ```
69
165
 
70
166
  The parked message is stored as `Parked: publish: verification-requested
@@ -80,71 +176,87 @@ task holds no resources.
80
176
 
81
177
  ## Parent verification procedure
82
178
 
179
+ The automated path is the tick body's step 4.5 (scan → build → ferry →
180
+ verify). The manual fallback below is the same protocol run by hand; it
181
+ exists for when the artifact namespace is unavailable to the tick worker.
182
+
83
183
  For a task parked with `publish: verification-requested <commit>`:
84
184
 
85
185
  1. **Resolve the project.** Read the task's project via the Crew API
86
- (`getproject`); you need `repo_path` (the git checkout) and the artifact
186
+ (`get-project`); you need `repo_path` (the git checkout) and the artifact
87
187
  slug (the project's publish target).
88
- 2. **Expected change.** Run `git show <commit>` (or
89
- `git diff <commit>^1 <commit>`) in `repo_path`. This is the expected
90
- content the same diff the workflow carried to the builder.
91
- 3. **Actual content.** Get the read-back:
92
- - If the park message names an `inspection_id`, read that inspection's
93
- result (async `artifact_inspect` results are delivered to the parent,
94
- like the visual-verdict inspections).
95
- - If the trigger failed, call `artifact_inspect` yourself with
96
- `repair_authorized: false` and the `verbatim_request` rebuilt from the
97
- workflow source: `buildPublishReadbackRequest(taskId, commit, diff,
98
- buildAgentId)` in `workflows/standard.js` (identical in `bugfix.js`
99
- and `chore.js`) pass the task id, the commit, the diff from step 2,
100
- and the agent_id from the park message's `(build …)` suffix (ledger
101
- lookup from step 4b if the suffix says `agent_id unobserved`; null if
102
- neither is available).
103
- - If no read-back can be obtained at all, log
104
- `publish: verification-blocked <commit> <reason>` and leave the task
105
- parked for human attention. Never stamp without a read-back.
188
+ 2. **Expected change.** The publish delta is `git diff <base> <commit>`
189
+ in `repo_path`, where `<base>` is the previously-stamped provenance
190
+ `source_commit` (read it via `get-provenance`; use the empty-tree sha
191
+ `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when no provenance is
192
+ stamped yet a first publish). Never use `commit^1` as the base and
193
+ never take the expected change from the builder's report: push-time
194
+ reconcile merges violate the `merge^1 == previously-published tree`
195
+ invariant, so `commit^1..commit` can omit the task's own fix
196
+ (2026-09-14, task `0c53af4e`).
197
+ 3. **Actual content.** When a read-back tool is available, call it with
198
+ `repair_authorized: false` and the `verbatim_request` built by
199
+ `lib/build-readback-request.js` (pass `--repo-path`, `--commit`,
200
+ `--base` (the same base as step 2),
201
+ `--task-id`, `--slug`, and `--build-agent-id` from the park message's
202
+ `(build …)` suffix when it is not `agent_id unobserved`). Currently
203
+ blocked `artifact_inspect` was removed by the platform (2026-09-14)
204
+ and no agent-callable replacement exists. The request demands a
205
+ machine-readable findings block:
206
+ ```
207
+ FILE: <path>
208
+ ADDED: <exact added line> :: PRESENT|ABSENT
209
+ REMOVED: <exact removed line> :: PRESENT|ABSENT
210
+ END_FILE
211
+ ```
212
+ If no read-back can be obtained at all, log
213
+ `publish: verification-blocked <commit> <reason>` and leave the task
214
+ parked for human attention. Never stamp without a read-back.
106
215
  4. **Compare mechanically.** For every added (`+`) line in the diff, the
107
- read-back must report it PRESENT in the artifact's current source. For
108
- every removed (`-`) line, the read-back must report it ABSENT. Quote the
109
- observed lines; do not infer from prose.
110
- 4b. **Build-ID correlation.** Prove the read-back inspected the live build
111
- of THIS publish attempt — not a different build's output:
216
+ read-back's machine-readable block must report it PRESENT in the
217
+ artifact's current source. For every removed (`-`) line, it must report
218
+ it ABSENT. The comparison is computed by `lib/verify-publish.js` — never
219
+ by eyeballing prose. A missing or malformed findings block fails closed
220
+ as `unreadable-result`, never as a pass.
221
+ 4b. **Build-ID correlation.** The read-back may have inspected a different
222
+ build's output than this publish attempt's:
112
223
  1. **Expected** = the agent_id in the park message's `(build …)` suffix.
113
224
  If the suffix says `agent_id unobserved`, look up the workflow's
114
225
  durable publish ledger at `$CREW_HOME/.publish-ledger/<slug>.jsonl`
115
226
  for the `submitted` entry with this `<commit>` and use its
116
227
  `agent_id` field (it may still be null — then this step is vacuous).
117
- 2. **Live** = the build agent_id the read-back reports for the live
118
- build (the request asks for it explicitly; the inspector may state
119
- that no build/agent_id is visible).
120
- 3. If expected and live are both non-null and DIFFER, the read-back may
121
- have inspected a different build's output — log
122
- `publish: build-mismatch <commit> expected <expected> observed <live>`,
123
- stay parked, never stamp, never re-queue. This is not a retry signal:
124
- a later inspection of the right build can be recorded by the human
125
- if desired; the loop never re-drives the verification itself.
126
- 4. Otherwise the content match from step 4 decides; log the correlation
127
- outcome (expected/live compared, or vacuous) in the `publish:
128
- verified` note.
228
+ 2. **Live** = whether the expected agent_id appears anywhere in the
229
+ read-back result (the live artifact status exposes no durable
230
+ agent_id — only an in-flight correlation ID that expires with the
231
+ publish attempt, so absence is the common case, not evidence of a
232
+ mismatch).
233
+ 3. If expected is non-null and the read-back positively reports a
234
+ DIFFERENT live build identity for this attempt's output, log
235
+ `publish: build-mismatch <commit> expected <expected> observed
236
+ <live>`, stay parked, never stamp, never re-queue.
237
+ 4. Otherwise the content match from step 4 decides the stamp certifies
238
+ CONTENT, not the builder's identity. Log the correlation outcome
239
+ (correlated / unobserved) in the `publish: verified` note.
129
240
  5. **Supersession check.** Before stamping, prove the inspected live
130
- artifact still represents the commit being certified. Read the crew's
131
- current provenance record (`get-provenance`): if `source_commit` names a
132
- commit NEWER than `<commit>` (a later Publish already landed), the
133
- read-back is stale do NOT stamp `<commit>` on top of it. Log
134
- `publish: superseded <commit> by <newer_commit>` and leave the task
135
- parked for human attention. Never claim commit A after commit B changed
136
- the artifact.
137
- 6. **Stamp, verify the stamp, then re-queue:**
241
+ artifact still represents the commit being certified: `git rev-parse
242
+ HEAD` in `repo_path` must equal `<commit>`. If HEAD has moved (a later
243
+ Publish landed), the read-back is stale log
244
+ `publish: superseded <commit> by <head>` and leave the task parked for
245
+ human attention. Never stamp a superseded commit.
246
+ 6. **Stamp, verify the stamp, then re-queue** (all in `lib/verify-publish.js`;
247
+ the manual equivalent):
138
248
  - **Match** — stamp provenance with the Crew API CLI `set-provenance`
139
249
  (the crew-owned store). Do NOT use the artifact's `setprovenance`
140
250
  action — it writes a different, non-authoritative store that QA never
141
251
  reads, so the stamp would be invisible to every gate:
142
- `set-provenance --json '{"source_commit":"<commit>","crew_release":"<basename of $crewHome/current>","published_at":"<date -u +%Y-%m-%dT%H:%M:%SZ>","task_id":"<task>"}'`.
143
- Then read the stamp back with `get-provenance`
144
- and confirm all four fields match exactly what was sent — a stamp
145
- that cannot be read back is not a stamp. Only then log the task note
146
- event `publish: verified <commit> (<inspection_id>)` and re-queue
147
- with `update-task` state `in_progress` (never `todo` — `todo`
252
+ `set-provenance --json '{"source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
253
+ (crew_release is the basename of the active release, e.g.
254
+ `pkg-0.7.10`). Then read the stamp back with `get-provenance`
255
+ and confirm source_commit, crew_release, and task_id match exactly
256
+ what was sent a stamp that cannot be read back is not a stamp. Only
257
+ then log the task note event
258
+ `publish: verified <commit> (<inspection_id>)` and re-queue with
259
+ `update-task` → state `in_progress` (never `todo` — `todo`
148
260
  restarts Triage and resets retry accounting). The dispatcher resumes
149
261
  at QA from the completed Publish session (standard/bugfix); chore has
150
262
  no QA — it proceeds to terminal completion. QA's provenance check
@@ -161,6 +273,61 @@ For a task parked with `publish: verification-requested <commit>`:
161
273
  for human attention. Never re-queue an unstamped-but-verified task into
162
274
  QA — QA would fail it and burn rework budget on a stamping problem.
163
275
 
276
+ ## Crash recovery
277
+
278
+ - **Tick dies before triggering the inspection:** the claim expires after
279
+ 1 hour; the next scan re-claims and re-verifies from scratch. The stamp
280
+ is an idempotent upsert, so a duplicate verification cannot corrupt it.
281
+ - **Tick dies after the inspection but before the stamp:** same as above —
282
+ the next scan re-runs the whole verification (new inspection, new
283
+ comparison). Wasteful but correct.
284
+ - **Crash between stamp and re-queue:** the next scan sees
285
+ `publish: verified` on a still-parked task and reconciles it to
286
+ `in_progress`. Failure verdicts are never reconciled — they stay parked
287
+ for human attention.
288
+ - **Two ticks verify concurrently:** impossible — the atomic claim means the
289
+ second scan sees the unexpired `publish: verification-claimed` note and
290
+ skips. The lease expiry bounds the damage if a claimer dies.
291
+
292
+ ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7)
293
+
294
+ Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
295
+ child failed structured closeout and the in-flight-only build-state poll
296
+ could not see the completed build — even though the build HAD run (a fresh
297
+ platform audit directory existed). Two mechanisms close this gap.
298
+
299
+ **1. Workflow-side durable evidence.** Before the rebuild trigger, the
300
+ workflow snapshots the artifact's audit-directory listing
301
+ (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). On a
302
+ structured-output failure with no in-flight build observed, it re-lists and
303
+ diffs: a timestamped directory that appeared during the trigger window is
304
+ positive evidence the edit went through and the build completed. The
305
+ fallback never re-issues the edit, never stamps provenance, and only routes
306
+ to the parent's independent content read-back. No new directory still parks
307
+ `unknown` fail-closed. The ledger distinguishes the two confirmations: `edit
308
+ confirmed via durable audit evidence …` vs `edit confirmed via build-state
309
+ poll …`.
310
+
311
+ The fallback's known limitation: audit directories are not attributed to
312
+ tasks, so two concurrent publishes to the same artifact could cross-read.
313
+ The consequence is bounded — the fallback only routes to the parent
314
+ read-back, and the parent still certifies the exact commit's content
315
+ mechanically (a wrong build's content fails closed as `publish:
316
+ content-mismatch` / `publish: build-mismatch`, never stamps).
317
+
318
+ **2. `resolve-publish-unknown` (Crew API).** For attempts already parked
319
+ `unknown` before this fix: given a task parked with a latest ledger outcome
320
+ of `unknown`, it derives the publish window (Integrate-completion event →
321
+ unknown-outcome park event) and checks for a timestamped audit build inside
322
+ that window. On evidence, it appends `unknown-resolved` to the ledger
323
+ (never rewriting the original entry), writes `publish: unknown-resolved`
324
+ and a mirrored `publish: verification-requested <commit>` note (the mirror
325
+ is timestamped strictly later so the scan sees it as the latest), and leaves
326
+ the task parked for the normal scan. Still-unknown cases stay parked:
327
+ unparked task, non-`unknown` latest ledger outcome, missing commit, no audit
328
+ build in the window, unobservable window, or an already-resolved attempt
329
+ (idempotent).
330
+
164
331
  ## Exact note-event prefixes
165
332
 
166
333
  Case-sensitive, exact-prefix matches — match on prefixes, never on English
@@ -168,20 +335,29 @@ meaning:
168
335
 
169
336
  - `publish: verification-requested <commit>` — workflow park; contained in
170
337
  the stored `Parked: …` message.
338
+ - `publish: verification-claimed <ISO-expiry>` — parent scan; atomic claim
339
+ with lease. Not a verdict.
171
340
  - `publish: verified <commit> (<inspection_id>)` — parent, after stamping
172
341
  AND reading the stamp back exactly; re-queued to `in_progress` (never
173
342
  `todo`).
174
343
  - `publish: content-mismatch <commit> <details>` — parent; exact FAIL
175
344
  evidence quoted; stays parked, never stamped, never re-queued to QA.
176
345
  - `publish: build-mismatch <commit> expected <expected> observed <live>` —
177
- parent; the read-back inspected a different build's output than the
178
- publish attempt (step 4b); stays parked, never stamped, never re-queued.
179
- - `publish: superseded <commit> by <newer_commit>` — parent; a later Publish
180
- already landed; stays parked for human attention.
346
+ parent; the read-back positively identified a different build's output
347
+ (step 4b); stays parked, never stamped, never re-queued.
348
+ - `publish: superseded <commit> by <head>` — parent; HEAD moved past the
349
+ commit; stays parked for human attention.
181
350
  - `publish: verification-blocked <commit> <reason>` — parent; no read-back
182
351
  obtainable; stays parked for a human.
352
+ - `publish: unknown-resolved <commit>` — recovery; durable build evidence
353
+ found inside the publish window for a previously-unknown attempt (see
354
+ "Unknown-outcome recovery"). The original `unknown` outcome is preserved;
355
+ the mirrored `verification-requested` note (written strictly later) is
356
+ what the scan claims.
183
357
  - `publish: stamp-failed <commit> <reason>` — parent; read-back matched but
184
358
  the stamp call failed; stays parked for a human.
359
+ - `publish: reconciled verified-but-parked -> in_progress` — parent scan;
360
+ the verified verdict was recorded but the re-queue was lost.
185
361
 
186
362
  ## Workflow differences
187
363
 
@@ -192,14 +368,3 @@ meaning:
192
368
  exact stamp read-back), the dispatcher proceeds to terminal completion.
193
369
  The parent's stamp read-back is the final gate — no downstream phase
194
370
  re-checks it.
195
-
196
- ## Recovery: rebuilding the read-back request
197
-
198
- If the park message names no inspection and you must trigger the read-back
199
- manually, the request is deterministic — rebuild it from the workflow source
200
- (`buildPublishReadbackRequest` in `workflows/standard.js`, identical in
201
- `bugfix.js` and `chore.js`) with the task id, the `<commit>` from the park
202
- message, the diff from `git show <commit>` in the project's `repo_path`,
203
- and the agent_id from the park message's `(build …)` suffix (use the ledger
204
- lookup from step 4b if the suffix says `agent_id unobserved`; pass null if
205
- neither is available).