muse-crew 0.6.6 → 0.6.8

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 CHANGED
@@ -5,7 +5,7 @@ Muse Crew source repository. The repo is the product; the personal instance (`$C
5
5
  ## Structure
6
6
 
7
7
  - `API.md` — the Crew API contract: every action a task service must implement
8
- - `.orchestration/` — crew configuration notes (deploy config lives in the dashboard's project record — the single source of truth)
8
+ - `.orchestration/` — crew configuration notes (deploy config lives in the crew's project record — the single source of truth, read via the Crew API)
9
9
  - `identities/` — crew member character files and portraits
10
10
  - `lib/` — shell scripts for release, merge, worktree, and cleanup
11
11
  - `personas/` — QA perspective costumes for Hazel
package/API.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The contract between Muse Crew and any task service implementation. The dispatcher and task workflows call these actions. A conforming task service must implement all of them.
4
4
 
5
- The current implementation is the `orchestra-dashboard` web artifact.
5
+ The crew owns its state. The reference implementation is `lib/crew-api.js` a Node CLI backed by SQLite at `$CREW_HOME/crew-state.db`. The dashboard is a pure client of this API; it never owns crew state.
6
6
 
7
7
  ---
8
8
 
@@ -101,6 +101,16 @@ Create or update an agent session record. Sessions track which identity is worki
101
101
  | `failure_reason` | string (≤ 3000) or null | no | |
102
102
  | `caveats` | array of `{text, severity}` | no | |
103
103
 
104
+ ### `heartbeat-session`
105
+
106
+ Update the `last_heartbeat` timestamp for a running session. Workflows call this periodically during long phases to signal liveness and prevent the zombie sweep from marking the session as `timed_out`. The sweep (in `getdispatchstate`) marks a `running` session as `timed_out` only if its `last_heartbeat` (or `started_at` for pre-migration sessions) is older than one hour.
107
+
108
+ | Field | Type | Required | Notes |
109
+ |-------|------|----------|-------|
110
+ | `id` | uuid | yes | Session ID; must be `running` |
111
+
112
+ Returns the updated session. Fails with `not_found` if the session does not exist, `conflict` if the session is not `running`.
113
+
104
114
  ---
105
115
 
106
116
  ## Events
@@ -111,7 +121,7 @@ Log an event to the activity timeline.
111
121
 
112
122
  | Field | Type | Required | Notes |
113
123
  |-------|------|----------|-------|
114
- | `type` | `dispatched` · `completed` · `failed` · `blocked` · `created` · `note` · `release_activated` · `deployed` | yes | |
124
+ | `type` | `dispatched` · `completed` · `failed` · `blocked` · `created` · `note` · `release_activated` · `deployed` · `rejected` | yes | `rejected` records a Review/QA rejection as an event, the same word the workflow stores as the session's `status` — one vocabulary for both, so a rejection can never store the session but fail the event (canary 2026-09-11). |
115
125
  | `message` | string (1–1000) | yes | |
116
126
  | `task_id` | uuid or null | no | Associate with a specific task |
117
127
  | `identity` | slug or null | no | Which crew member logged it |
@@ -126,6 +136,22 @@ Read the event timeline.
126
136
  | `task_id` | uuid | no | Return only events for this task |
127
137
  | `limit` | integer (1–100) | no | Defaults to 50 |
128
138
 
139
+ ### `recordphase` (composite)
140
+
141
+ Atomically record a workflow phase outcome: writes the agent session (with its verdict status) and the corresponding event in a single transaction. This is the only safe way to record a phase result — writing the session and event as two separate calls can leave the session stored but the event rejected (or vice versa), crashing the workflow mid-run. The `rejected` vocabulary is valid for both the session `status` and the event `type`.
142
+
143
+ | Field | Type | Required | Notes |
144
+ |-------|------|----------|-------|
145
+ | `task_id` | uuid | yes | |
146
+ | `identity` | slug | yes | The crew identity that ran the phase |
147
+ | `step` | string (1–120) | yes | Workflow phase (e.g. `Review`, `QA`) |
148
+ | `status` | `completed` · `failed` · `rejected` · `timed_out` · `stalled` | yes | The phase verdict |
149
+ | `notes` | string (≤ 3000) | no | Phase summary |
150
+ | `event_type` | event type | no | Defaults to the `status` value; use `note` for non-verdict events |
151
+ | `event_message` | string (1–1000) | no | Defaults to "`<identity>` `<step>` `<status>`" |
152
+
153
+ The CLI exposes this as `record-phase`. All CLI commands use kebab-case (`create-task`, `get-dispatch-state`, etc.) mapping to the action names documented here.
154
+
129
155
  ---
130
156
 
131
157
  ## Dispatch
@@ -182,6 +208,7 @@ Register a new project.
182
208
  | `description` | string (≤ 3000) | no | |
183
209
  | `simultaneity` | integer (1–100) | no | Max concurrent tasks; defaults to 2 |
184
210
  | `quiesced` | boolean | no | Start paused; defaults to false |
211
+ | `visual_protocol` | boolean or null | no | Tri-state: `null` = inherit crew default (off), `true` = enable for this project, `false` = explicit off. Defaults to null. |
185
212
 
186
213
  ### `updateproject`
187
214
 
@@ -189,6 +216,8 @@ Update project fields. Only `id` is required; all others are optional patch fiel
189
216
 
190
217
  **Context-change guard:** changing `repo_path`, `deploy_type`, or `deploy_slug` is blocked while any task in the project has an active run (an agent session with status `running` that started within the last hour): the call throws, because live runs keep the old project config and the change would split the project context mid-run. Wait for the runs to finish, or recover/park those tasks first, then retry.
191
218
 
219
+ **`visual_protocol`:** accepted values are `true`, `false`, `null`; anything else is a validation error. `null` clears the override and returns the project to inherit. It is **not** covered by the context-change guard: the dispatcher reads the value once into launch args at dispatch time, so changing it mid-run only affects future launches. Effective value rule: `null` (inherit) and `false` (explicit off) both resolve to off — the visual protocol only runs when the project record carries `true`. The dispatcher passes the resolved boolean to launched workflows as the `visual_protocol` launch arg; workflows receive a plain boolean and never see the tri-state.
220
+
192
221
  ### `deleteproject`
193
222
 
194
223
  Remove a project registration.
@@ -269,7 +298,7 @@ Stamp publication provenance. Called by the crew Publish phase after rebuilding
269
298
 
270
299
  ### `getprovenance`
271
300
 
272
- Read the last publication provenance. Returns `{ "provenance": { "source_commit", "crew_release", "published_at" } }`, or `{ "provenance": null }` when nothing has been published yet.
301
+ Read the last publication provenance. Returns `{ "provenance": { "source_commit", "crew_release", "published_at", "task_id" } }`, or `{ "provenance": null }` when nothing has been published yet. `task_id` is the publishing task, for traceability — present when `setprovenance` was called with one, absent otherwise.
273
302
 
274
303
  ---
275
304
 
@@ -287,12 +316,27 @@ Phases whose outcome drives control flow (Build, Review, QA, Reproduce, Integrat
287
316
 
288
317
  ### Artifact publish verification
289
318
 
290
- Artifact Publish passes only when both are true:
291
-
292
- 1. The worker declares success.
293
- 2. Mechanical verification confirms the artifact's provenance matches the integrated Git commit: the workflow reads `getprovenance` and compares `provenance.source_commit` against `git rev-parse HEAD` of the project repo.
294
-
295
- Verification fails closed. If the provenance is missing, unreadable, or points at a different commit than the integrated HEAD, the workflow parks the task — it does not trust the worker's prose. Narrative remains useful but cannot independently establish deployment truth.
319
+ Artifact Publish passes only when the parent has verified content independently:
320
+
321
+ 1. The workflow carries the merged diff to the artifact builder, polls the
322
+ build to completion, triggers an independent `artifact_inspect` read-back
323
+ of the changed regions, and parks with
324
+ `publish: verification-requested <commit>` — it never stamps provenance
325
+ and never treats the builder's applied-changes report as verification.
326
+ The applied report is derived from the carried diff and demonstrated an
327
+ unreliable false-negative mode (2026-09-12: `applied:[]` for a diff the
328
+ builder had applied); it is logged as observation only. The read-back
329
+ request carries the observed builder build identifier (`build.agent_id`)
330
+ so the read-back can be correlated to the exact builder run that built
331
+ this attempt.
332
+ 2. The parent compares the read-back against the merged diff mechanically
333
+ (every added line present, every removed line absent), correlates the
334
+ builder build identifier, and stamps provenance via `set-provenance` only
335
+ on a match. A mismatch, a build-id mismatch, or an unobtainable read-back
336
+ never stamps; the task stays parked.
337
+
338
+ Verification fails closed. The loop never retries a verification and never
339
+ issues a blind re-publish on an unknown outcome.
296
340
 
297
341
  ### Integrate merge verification
298
342
 
package/docs/guide.md CHANGED
@@ -17,7 +17,7 @@ This is the full setup and operations reference. If you're new, start with the [
17
17
  - A **release** — the first immutable snapshot of the crew's runtime code.
18
18
  - An **`.orchestration/` directory** in the crew home with identities, personas, workflow docs, and feedback conventions.
19
19
  - A **project registration** — the task service registered as its own first project.
20
- - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 3 minutes via Muse's scheduling, even when nobody's in the conversation) and an orphan sweep (every 30 minutes, cleaning merged worktrees and breaking stale merge locks; it never touches worktrees belonging to tasks with a running dashboard session). Owner is `space:<slug>`, so deleting the task service also removes the crons.
20
+ - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 3 minutes via Muse's scheduling, even when nobody's in the conversation). Owner is `space:<slug>`, so deleting the task service also removes the crons.
21
21
 
22
22
  The agent running in the main chat receives the dispatcher's claims and launches each task workflow. Workflows can't launch workflows, so this handoff is structural.
23
23
 
@@ -55,7 +55,7 @@ Required arguments:
55
55
 
56
56
  Optional arguments:
57
57
  - `dashboardName` — display name for the project registration (default: `"Muse Crew"`).
58
- - `cronIds` — manifest id → live id map for parallel instances on one account (default: `{}`). The manifest's ids are used as-is unless overridden, e.g. `{"crew-poll": "crew-poll-canary", "crew-orphan-sweep": "crew-sweep-canary"}`.
58
+ - `cronIds` — manifest id → live id map for parallel instances on one account (default: `{}`). The manifest's ids are used as-is unless overridden, e.g. `{"crew-poll": "crew-poll-canary"}`.
59
59
 
60
60
  ### What init creates
61
61
 
@@ -67,7 +67,7 @@ Init runs four phases, each idempotent — re-running converges anything that dr
67
67
 
68
68
  3. **Project registration** — registers the task service as a project in its own database via `createproject`. The dashboard becomes its own first project, so the crew can work on the dashboard itself.
69
69
 
70
- 4. **Crons** — reads the manifest at `seed/crons.json` and makes the scheduler match it: missing jobs are created from the entry (title, enabled, mode, schedule, owner, timeout_secs, and the body built from the entry's template with `crewHome` and the dashboard slug substituted in); existing jobs are viewed and converged — drifted fields are updated, unchanged jobs are left alone. One exception: init never touches `enabled` on an existing job. `enabled` is a creation-time default only — a disabled job is a deliberate human decision (the orphan sweep ships disabled as a holding state), and re-init must not silently resurrect it. Jobs removed from the manifest are left alone; deletion is a human decision.
70
+ 4. **Crons** — reads the manifest at `seed/crons.json` and makes the scheduler match it: missing jobs are created from the entry (title, enabled, mode, schedule, owner, timeout_secs, and the body built from the entry's template with `crewHome` and the dashboard slug substituted in); existing jobs are viewed and converged — drifted fields are updated, unchanged jobs are left alone. One exception: init never touches `enabled` on an existing job. `enabled` is a creation-time default only — a disabled job is a deliberate human decision, and re-init must not silently resurrect it. Jobs removed from the manifest are left alone; deletion is a human decision.
71
71
 
72
72
  ### Verification
73
73
 
@@ -83,8 +83,7 @@ After init completes, it returns a summary:
83
83
  "scaffold": { "created": 23, "skipped": 0 },
84
84
  "project": "registered",
85
85
  "crons": [
86
- { "id": "crew-poll", "action": "created", "updated_fields": [] },
87
- { "id": "crew-orphan-sweep", "action": "created", "updated_fields": [] }
86
+ { "id": "crew-poll", "action": "created", "updated_fields": [] }
88
87
  ]
89
88
  }
90
89
  ```
@@ -268,6 +267,10 @@ Configuration (per project, via the existing `getconfig` / `updateconfig` action
268
267
 
269
268
  For tasks that change anything rendered and visible in the project's artifact ("experiential" tasks), QA owns the visual verdict. Capture runs right after Triage to collect pre-change baseline evidence (parent-driven — see `docs/visual-verdict.md`); Map cannot be written without it; final QA covers mechanical checks only and the task does not complete until a `visual_verdict: PASS` is recorded. A FAIL reworks at Build within the shared budget; a `rendering impossible:` FAIL parks for human attention. Hazel judges against the artist-eye rubric (alignment, spacing, hierarchy, composition, balance, finish, taste) in `identities/hazel.md`, from rendered evidence only — never from prose descriptions.
270
269
 
270
+ ### Publish content verification
271
+
272
+ 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.
273
+
271
274
  ## Identities
272
275
 
273
276
  Each phase has an assigned identity — a character with a defined personality:
@@ -280,7 +283,7 @@ Each phase has an assigned identity — a character with a defined personality:
280
283
  | Build | **Wren** | Quietest one, trusts the plan |
281
284
  | Review | **Cass** | Fair but exacting — holds the spec as the contract |
282
285
  | Integrate | **Wren** | Merges the work, pushes `main` to the repo (succeeds vacuously when the task branch is empty — runtime-state deliverable) |
283
- | Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow verifies the side effect mechanically — npm via registry version, artifact via `getprovenance` matching the integrated commit — and fails closed if the worker's report and system state disagree |
286
+ | Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow verifies the side effect mechanically — npm via registry version; artifact via an independent content read-back before the parent stamps provenance (see `docs/publish-verification.md`) — and fails closed if the worker's report and system state disagree |
284
287
  | QA | **Hazel** | Code-blind, persistent, wears persona costumes |
285
288
  | Reproduce | **Hazel** | Reproduces bugs before fixing |
286
289
  | Write | **Tate** | Docs writer, observational voice |
@@ -0,0 +1,205 @@
1
+ # Publish content verification — parent protocol
2
+
3
+ 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.
7
+
8
+ ## Why the parent stamps
9
+
10
+ The builder's `applied` report is derived from the diff the workflow carries
11
+ to it — so `verifyAppliedChanges` (report vs. diff) is circular: a fabricated
12
+ report passes by construction. Canary run 8 (2026-09-11) proved it: the build
13
+ was hollow, the report matched the diff, the workflow stamped provenance, and
14
+ all eight phases went green on stale content. The old post-hoc check
15
+ (get-provenance vs. HEAD) only verified the stamp, not the content.
16
+
17
+ Task `23ca8f3f` (2026-09-12, canary `orchestra-dashboard-2`) proved the report
18
+ is unreliable in the other direction too: the builder applied the one-line
19
+ diff, the applied-report came back `applied: []`, and the workflow's old
20
+ applied-report gate parked the task fail-closed as unverified — on a publish
21
+ that had actually landed. A later independent read-back of the live artifact
22
+ showed the change present.
23
+
24
+ The rule: **the builder's applied report is never a verification signal, in
25
+ either direction.** A matching report certifies nothing (it is derived from
26
+ the carried diff — circular by construction, canary run 8). A mismatching or
27
+ empty report blocks nothing (false-negative mode demonstrated by `23ca8f3f`).
28
+ The workflow computes the report comparison as an observation only — it
29
+ never parks on it. The parent ignores the applied report entirely when
30
+ deciding whether to stamp: an `applied:[]` or mismatched report with a
31
+ matching independent read-back stamps normally.
32
+
33
+ The contract is split on purpose:
34
+
35
+ - **Workflow-owned:** carrying the merged diff to the builder, the
36
+ 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
40
+ 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.
49
+
50
+ No artifact publish completes without parent-stamped provenance. A missing or
51
+ mismatched read-back never stamps.
52
+
53
+ ## The park
54
+
55
+ When the artifact build lands, the workflow parks the task with the message:
56
+
57
+ ```
58
+ 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).
68
+ ```
69
+
70
+ The parked message is stored as `Parked: publish: verification-requested
71
+ <commit> …`. Match on the contained exact string
72
+ `publish: verification-requested`. The `<commit>` is the merged commit whose
73
+ content must be verified. The `(build …)` suffix carries the
74
+ `build.agent_id` the workflow observed for this publish attempt (the
75
+ artifact system's durable build identifier — the parent uses it for the
76
+ build-ID correlation in step 4b); `agent_id unobserved` means the edit was
77
+ accepted but the workflow never correlated it to a builder run. The merge
78
+ lock is already released (post-deploy ran before the park), so the parked
79
+ task holds no resources.
80
+
81
+ ## Parent verification procedure
82
+
83
+ For a task parked with `publish: verification-requested <commit>`:
84
+
85
+ 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
87
+ 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.
106
+ 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:
112
+ 1. **Expected** = the agent_id in the park message's `(build …)` suffix.
113
+ If the suffix says `agent_id unobserved`, look up the workflow's
114
+ durable publish ledger at `$CREW_HOME/.publish-ledger/<slug>.jsonl`
115
+ for the `submitted` entry with this `<commit>` and use its
116
+ `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.
129
+ 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:**
138
+ - **Match** — stamp provenance with the Crew API CLI `set-provenance`
139
+ (the crew-owned store). Do NOT use the artifact's `setprovenance`
140
+ action — it writes a different, non-authoritative store that QA never
141
+ 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`
148
+ restarts Triage and resets retry accounting). The dispatcher resumes
149
+ at QA from the completed Publish session (standard/bugfix); chore has
150
+ no QA — it proceeds to terminal completion. QA's provenance check
151
+ (standard/bugfix) enforces the stamp mechanically.
152
+ - **Mismatch** — do NOT stamp. Log the exact FAIL evidence with
153
+ `publish: content-mismatch <commit> <details>` (quote the observed
154
+ lines from the read-back) and leave the task parked. Never stamp
155
+ provenance on a mismatch; never re-queue a mismatched publish into
156
+ QA — QA would fail it and burn rework budget communicating a Publish
157
+ problem that is not QA's to solve. The task stays parked for human
158
+ attention; the loop does not retry the verification.
159
+ - **Stamp failure** — if `set-provenance` fails after a matched read-back,
160
+ log `publish: stamp-failed <commit> <reason>` and leave the task parked
161
+ for human attention. Never re-queue an unstamped-but-verified task into
162
+ QA — QA would fail it and burn rework budget on a stamping problem.
163
+
164
+ ## Exact note-event prefixes
165
+
166
+ Case-sensitive, exact-prefix matches — match on prefixes, never on English
167
+ meaning:
168
+
169
+ - `publish: verification-requested <commit>` — workflow park; contained in
170
+ the stored `Parked: …` message.
171
+ - `publish: verified <commit> (<inspection_id>)` — parent, after stamping
172
+ AND reading the stamp back exactly; re-queued to `in_progress` (never
173
+ `todo`).
174
+ - `publish: content-mismatch <commit> <details>` — parent; exact FAIL
175
+ evidence quoted; stays parked, never stamped, never re-queued to QA.
176
+ - `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.
181
+ - `publish: verification-blocked <commit> <reason>` — parent; no read-back
182
+ obtainable; stays parked for a human.
183
+ - `publish: stamp-failed <commit> <reason>` — parent; read-back matched but
184
+ the stamp call failed; stays parked for a human.
185
+
186
+ ## Workflow differences
187
+
188
+ - **standard / bugfix:** after `publish: verified`, the dispatcher resumes
189
+ at QA from the completed Publish session. QA's provenance check enforces
190
+ the stamp mechanically — an unstamped publish fails loudly there.
191
+ - **chore:** there is no QA phase. After `publish: verified` (stamp +
192
+ exact stamp read-back), the dispatcher proceeds to terminal completion.
193
+ The parent's stamp read-back is the final gate — no downstream phase
194
+ 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).
package/lib/AGENTS.md CHANGED
@@ -3,14 +3,14 @@
3
3
  Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, and the release system.
4
4
 
5
5
  - `build-registry.js` — deterministic extractor that generates `workflows/registry.json` (workflow step registry) from the workflow files' `meta` blocks at release time; invoked by `crew-release.sh` deploy
6
+ - `crew-api.js` — the crew-owned task-service API (dependency inversion, 2026-09-11): a zero-dependency Node CLI implementing the API.md contract against `$CREW_HOME/crew-state.db` (schema in `schema.sql`). Workflows call it through their agents' shell; the dashboard delegates to it. All state-machine invariants live as CHECK constraints in the schema, never in client prose. Includes the `record-phase` composite (session + event in one transaction) and a one-time `migrate` import from a dashboard app.db.
7
+ - `schema.sql` — the crew-owned state schema: projects, tasks, poll_state, config, agent_sessions, events. Vocabularies enforced by CHECK constraints; `rejected` is a valid event type (the 2026-09-11 crash was a stored session whose event was rejected). Column names match the historical dashboard tables for a verbatim migration.
6
8
  - `crew-release.sh` — immutable release manager: deploy, rollback, prune
7
- - `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
8
- - `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/refresh-lock/lock-status over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving. `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or key=value: locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds — always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
9
- - `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight) on a scratch repo
9
+ - `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
10
+ - `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (`integrate` reconciles `origin/main` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — so the push is a fast-forward; fail-soft with no remote, fail closed on conflict) (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving. `terminal-cleanup` is the run's last act at every park/fail boundary (called from `parkTask` in standard/bugfix/chore): releases the merge lock unconditionally and reclaims the worktree+branch only when the task branch is fully merged into main — unmerged work is preserved for the human by design, and a dirty worktree is reported, never force-removed. `post-deploy` reports worktree removal honestly (a lying "removed" echo hid real leftovers — canary run 9, 2026-09-12). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset); `CREW_REPO` is exported so internal merge-lock.sh calls inherit the repo being worked on; `LIB_DIR` defaults to `$CREW_HOME/lib` (`CREW_LIB` override). `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or key=value: locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds — always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
11
+ - `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight, integrate remote-reconcile with fixture sensitivity + no-remote fail-soft) on scratch repos
10
12
  - `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \\u2014 escape; extracts the shipped block by anchor)
11
13
  - `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
12
14
  - `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
13
- - `orphan-sweep.sh` — find and clean stale worktrees and merge locks: parses `merge-lock.sh status` key=value fields (never a PID); a lock whose task has a running session is untouched regardless of lease age; a lock with no running session is broken only when its lease is expired (via merge-lock.sh release so the break lands in the audit log); reaps a lifecycle pin only after N consecutive clean-mode observations (default 3, `ORPHAN_SWEEP_REAP_AFTER`) with the task absent from the active-run list — a single negative observation only plants a tombstone, so a daemon-restart window can't reap a live task's pin
14
15
  - `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
15
16
  - `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
16
- - `test-orphan-sweep.sh` — regression tests for orphan-sweep.sh (active-run guard, verified removal, fail-closed)