muse-crew 0.14.5 → 0.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +106 -12
- package/docs/decisions/AGENTS.md +5 -0
- package/docs/decisions/publish-path.md +247 -9
- package/docs/decisions/qa-reproduce.md +30 -0
- package/docs/guide.md +41 -5
- package/docs/publish-unknown-recovery.md +28 -120
- package/docs/publish-verification.md +148 -501
- package/lib/AGENTS.md +9 -13
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +82 -5
- package/lib/crew-api.js +1134 -1027
- package/lib/merge-lock.sh +153 -41
- package/lib/publish-note-vocabulary.js +135 -42
- package/lib/qa-db.js +132 -0
- package/lib/schema.sql +69 -0
- package/lib/serve-artifact.js +46 -2
- package/lib/test-detached-integrate.sh +122 -0
- package/lib/test-merge-lock.sh +30 -1
- package/lib/worktree-lifecycle.sh +284 -34
- 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 +55 -89
- package/seed/crons.json +12 -0
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +489 -224
- package/workflows/chore.js +306 -226
- package/workflows/crew-dispatch.js +57 -4
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/docs.js +24 -2
- package/workflows/standard.js +494 -251
- package/workflows/upgrade.js +13 -1
- package/lib/build-readback-request.js +0 -140
- package/lib/check-intent-freshness.js +0 -101
- 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 -417
- package/lib/verify-publish.js +0 -416
package/API.md
CHANGED
|
@@ -34,7 +34,7 @@ Update an existing task's fields. Only `id` is required; all other fields are op
|
|
|
34
34
|
| `id` | uuid | yes | |
|
|
35
35
|
| `title` | string (1–200) | no | |
|
|
36
36
|
| `description` | string (≤ 3000) | no | |
|
|
37
|
-
| `state` | `todo` · `in_progress` · `parked` · `done` | no | Only the `parked` → `todo` transition stamps `retry_reset_at` to the current server time (UTC, ISO 8601), mechanically restarting the consecutive-failure streak — a re-queued task never inherits its old failures. No other transition stamps it (`parked` → `done`, `todo` → `todo`, and patches without a `state` field do not). |
|
|
37
|
+
| `state` | `todo` · `in_progress` · `parked` · `done` | no | Only the `parked` → `todo` transition stamps `retry_reset_at` to the current server time (UTC, ISO 8601), mechanically restarting the consecutive-failure streak — a re-queued task never inherits its old failures. No other transition stamps it (`parked` → `done`, `todo` → `todo`, and patches without a `state` field do not). Any transition INTO `parked` closes the task's open `workflow_runs` rows to `parked` and fires the dependent cascade-park, exactly like `parktask` (room #26 blocker 37, 2026-09-21: a parked task must never leave a stale `running` telemetry row). |
|
|
38
38
|
| `priority` | `high` · `normal` · `low` | no | |
|
|
39
39
|
| `project` | slug | no | **Project-move guard:** changing this to a different project throws while the task has an active run (an agent session with status `running` that started within the last hour), because the live run keeps the old project's repo context and moving it mid-phase would work on the wrong repo. Wait for the run to finish, or recover/park the task first, then move it. |
|
|
40
40
|
| `workflow` | slug or null | no | |
|
|
@@ -92,17 +92,76 @@ Atomically park a task for human attention: sets `state` to `parked`, writes the
|
|
|
92
92
|
| `task_id` | uuid | yes | |
|
|
93
93
|
| `message` | string (1–1000) | yes | Why the task needs a human; written to the activity feed as a `note` |
|
|
94
94
|
|
|
95
|
-
Returns `{ "ok": true, "task": {...}, "settled_sessions": <n
|
|
95
|
+
Returns `{ "ok": true, "task": {...}, "settled_sessions": <n>, "closed_workflow_runs": <n>, "cascade_parked": [{ "id", "dep_id" }] }` with the updated task record, the count of sessions settled (`0` when none were running), the count of open `workflow_runs` rows closed to `parked` (`0` when none were open — room #26 blocker 37, 2026-09-21: park never closed telemetry rows, leaving stale `running` rows), and the cascade-parked dependents.
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
**Cascade-park (room #26, 2026-09-21):** parking a task also parks every `todo` task that depends on it — transitively, to a fixpoint — with structured attribution on the existing `parked` state (`park_reason: "dep_parked"`, `park_dep_id` naming the parked dep): never a new state, never silent `todo`, never auto-waived. Each cascade-parked task gets a `blocked` event naming the dependent and the parked dep, and its running sessions are settled. An `in_progress` task is never cascade-parked — a running workflow owns its verdict. The same cascade fires when a task parks through `updatetask` → `parked` or `retry-platform-failure`. When a task leaves `parked` for any other state, its cascade-parked dependents are re-evaluated: re-attributed to another still-parked dep, or returned to `todo` with the attribution cleared. The `parked` → `todo` transition is the designed human waiver — explicit, attributable, never automatic.
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
### `scan-publish-intent`
|
|
100
|
+
|
|
101
|
+
Claims parked publish intents for the tick worker's direct issuance (0.14.6). For each task parked with `publish: publish-requested <commit> attempt=N`, re-derives the version deterministically (never trusts the ledger's), verifies the staged diff's sha256, checks the intent base against stamped provenance, and claims with a 1-hour lease (CAS on `claim_expiry`). Skips: no intent entry, unparseable note, terminal note, unexpired claim (another tick owns it), base-stale (re-queues to the workflow's Publish), backoff not elapsed.
|
|
102
|
+
|
|
103
|
+
Returns `{ "intent": [...], "skipped": [...], "requeued": [...] }`. Each intent entry carries `task_id`, `commit`, `attempt` (numeric `publish_attempt`), `version` (`<commit>:<attempt>`), `claim_expiry`, `diff_path`, `diff_sha256`, `ledger_path`.
|
|
104
|
+
|
|
105
|
+
### `record-intent-issuance`
|
|
106
|
+
|
|
107
|
+
Records the tick worker's direct issuance outcome (0.14.6). Compare-and-swap on `claim_expiry` — a stale tick writes nothing.
|
|
108
|
+
|
|
109
|
+
| Field | Type | Required | Notes |
|
|
110
|
+
|-------|------|----------|-------|
|
|
111
|
+
| `task_id` | uuid | yes | Must have an unexpired intent claim |
|
|
112
|
+
| `claim_expiry` | ISO-8601 | yes | Must match the claim's expiry (CAS) |
|
|
113
|
+
| `edit_outcome` | `accepted`\|`refused` | yes | The direct `artifact_edit` outcome |
|
|
114
|
+
| `refusal_text` | string | no | Required when `refused` |
|
|
115
|
+
| `reclaimed` | boolean | no | Copy verbatim from the `scan-publish-intent` entry — true when a previous tick died mid-issuance |
|
|
116
|
+
| `receipt_check` | `different`\|`unreadable`\|`match` | required when `reclaimed` is true, forbidden otherwise | The tick worker's step-2 version-receipt check outcome for the reclaimed claim |
|
|
117
|
+
|
|
118
|
+
`accepted` writes the issuer-stamped `submitted` ledger entry + `publish: edit-issued` note. `refused` writes `rejected` + terminal `publish: publish-refused`. Returns `{ "recorded": true, "edit_outcome" }` or `{ "recorded": false }` on CAS mismatch.
|
|
119
|
+
|
|
120
|
+
Reclaimed-claim receipt guard (2026-09-21 R-3): a reclaimed claim means a previous tick may have issued the edit without recording it, so `record-intent-issuance` requires the explicit `receipt_check` — the mechanical receipt-check outcome, not a prose judgment. A missing or unknown value is a usage error; `"match"` refuses to record fail-closed (the edit landed — acknowledge via `record-version-ack`, never re-issue); `"different"`/`"unreadable"` record normally with the check carried on the ledger entry. A differing or unreadable receipt is NOT proof the prior attempt's edit did not land — the acknowledgement scan owns that verdict from here.
|
|
121
|
+
|
|
122
|
+
### `record-builder-report`
|
|
123
|
+
|
|
124
|
+
Records a builder's refusal report (0.14.6). Mechanically requires the EXACT version in `report_text` — a report that does not echo the version is rejected.
|
|
125
|
+
|
|
126
|
+
| Field | Type | Required | Notes |
|
|
127
|
+
|-------|------|----------|-------|
|
|
128
|
+
| `task_id` | uuid | yes | |
|
|
129
|
+
| `version` | `<commit>:<attempt>` | yes | Must name an issued attempt |
|
|
130
|
+
| `outcome` | `refused` | yes | (2026-09-20 REVIEW) `acknowledged` is rejected — the positive builder-report path was circular; disk is the sole positive evidence |
|
|
131
|
+
| `report_text` | string | yes | Must contain the exact version |
|
|
132
|
+
|
|
133
|
+
Returns `{ "ok": true, "publish_attempt" }`. The ack scan consumes refusal reports (terminal `publish: publish-refused`); positive acknowledgements come only from the on-disk version receipt.
|
|
134
|
+
|
|
135
|
+
### `record-version-ack`
|
|
136
|
+
|
|
137
|
+
Stamps a version acknowledgement (0.14.6). The version must name an issued attempt for the task/commit.
|
|
100
138
|
|
|
101
139
|
| Field | Type | Required | Notes |
|
|
102
140
|
|-------|------|----------|-------|
|
|
103
|
-
| `task_id` | uuid | yes |
|
|
141
|
+
| `task_id` | uuid | yes | |
|
|
142
|
+
| `version` | `<commit>:<attempt>` | yes | Must match an issued attempt's derived version |
|
|
143
|
+
| `via` | string | yes | `disk` only (2026-09-20 REVIEW) |
|
|
144
|
+
|
|
145
|
+
Stamps provenance, writes terminal `publish: version-acknowledged`, re-queues the task to `in_progress`. Returns `{ "ok": true, "commit", "publish_attempt", "via" }`. The reclaim path accepts the exact disk version for the currently claimed intent when no `submitted` entry exists (dead tick issued but died before recording).
|
|
146
|
+
|
|
147
|
+
### `scan-ack-pending`
|
|
148
|
+
|
|
149
|
+
Evaluates version acknowledgements (0.14.6). For each `edit-issued` task: checks the on-disk `.crew-publish-version-<task_id>` receipt (per-task scoped; the SOLE positive evidence — 2026-09-20 REVIEW removed the circular builder-report positive path). Any exact on-disk acknowledgement for an issued version on the task/commit counts (late earlier attempts included). On ack: provenance + terminal `publish: version-acknowledged` + requeue. On explicit refusal (via builder report): terminal `publish: publish-refused`. On 30-minute window expiry (per attempt, anchored at the LATEST issuance): re-issues with a FRESH version, immediately claimable (no backoff). When the 2-hour total budget from the FIRST issuance is exhausted: terminal `publish: version-timeout` (unobserved within budget — the edit may still have landed).
|
|
150
|
+
|
|
151
|
+
Returns `{ "acknowledged": [...], "refused": [...], "reissued": [...], "timeouts": [...], "skipped": [...] }`.
|
|
152
|
+
|
|
153
|
+
A dedicated 5-minute `crew-ack-scan` cron (seed/crons.json) runs this command on an observation-only tick so verdicts land quickly; it never issues edits — issuance stays in the 15-minute poll tick's claim-and-issue path.
|
|
104
154
|
|
|
105
|
-
|
|
155
|
+
### `record-publish-note`
|
|
156
|
+
|
|
157
|
+
The closed publish-note writer (0.14.6 §1.5). Asserts the note's verb against the writable registry before writing — prose cannot mint new states.
|
|
158
|
+
|
|
159
|
+
| Field | Type | Required | Notes |
|
|
160
|
+
|-------|------|----------|-------|
|
|
161
|
+
| `task_id` | uuid | yes | |
|
|
162
|
+
| `note` | string | yes | Must start with a registered `publish: <verb>` |
|
|
163
|
+
|
|
164
|
+
Writable verbs (tick prose via `record-publish-note`): `publish-requested`, `publish-intent-claimed`, `publish-base-stale`, `edit-issued` (transitional only — prose can never mint a terminal). Deterministic code writes terminals via the code-writable registry: `version-acknowledged`, `publish-refused`, `version-timeout`, `publish-unissuable`. Legacy terminals are recognized but NOT writable by anyone. Returns `{ "ok": true, "verb" }` or a usage error.
|
|
106
165
|
|
|
107
166
|
---
|
|
108
167
|
|
|
@@ -124,9 +183,9 @@ Create or update an agent session record. Sessions track which identity is worki
|
|
|
124
183
|
| `notes` | string (≤ 3000) | no | |
|
|
125
184
|
| `failure_reason` | string (≤ 3000) or null | no | |
|
|
126
185
|
| `caveats` | array of `{text, severity}` | no | |
|
|
127
|
-
| `already_merged_sha` | 7–40 lowercase hex or null | no | **
|
|
186
|
+
| `already_merged_sha` | 7–40 lowercase hex or null | no | **Superseded (0.14.7, blocker 34):** no workflow writes or reads this field; attribution is classifier-only (the `classify-branch` lifecycle command reports the merge sha on its own `BRANCH_STATE:` line, and Review adopts it from there). Column retained; null on all new rows. Null semantics: omitting the field preserves the existing value; passing explicit `null` clears it. `record-phase` always preserves (it never clears). |
|
|
128
187
|
|
|
129
|
-
|
|
188
|
+
**Superseded (0.14.7, blocker 34):** no workflow writes or reads this field; attribution is classifier-only. The field's original reason is gone — the pre-Review classifier now answers "already merged, by which merge" in git, before Review, so there is no longer a declaration to carry through session notes. Column retained; null on all new rows.
|
|
130
189
|
|
|
131
190
|
### `heartbeat-session`
|
|
132
191
|
|
|
@@ -176,9 +235,35 @@ Atomically record a workflow phase outcome: writes the agent session (with its v
|
|
|
176
235
|
| `notes` | string (≤ 3000) | no | Phase summary |
|
|
177
236
|
| `event_type` | event type | no | Defaults to the `status` value; use `note` for non-verdict events |
|
|
178
237
|
| `event_message` | string (1–1000) | no | Defaults to "`<identity>` `<step>` `<status>`" |
|
|
238
|
+
| `verdict` | object | no | Optional structured Review verdict record (see below) — the non-lossy verdict grounds |
|
|
179
239
|
|
|
180
240
|
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.
|
|
181
241
|
|
|
242
|
+
#### Structured verdict records
|
|
243
|
+
|
|
244
|
+
Session notes are intentionally lossy (truncated at 3000 chars) — Review verdict grounds were destroyed by truncation before reaching `rejectionNotes`, making parks on verified-correct implementations unauditable. `recordphase` optionally carries the full worker report as a verdict payload, validated at the API boundary and written to the `verdicts` table in the same transaction as the session and event, so the lossless record can never be missing when the note exists.
|
|
245
|
+
|
|
246
|
+
| Field | Type | Required | Notes |
|
|
247
|
+
|-------|------|----------|-------|
|
|
248
|
+
| `step` | `Review` | yes | Verdict records are Review-scoped |
|
|
249
|
+
| `attempt` | integer ≥ 0 | yes | The rework round (0 = first Review) |
|
|
250
|
+
| `reviewer` | string (1–200) | yes | The reviewing identity (e.g. `cass`) |
|
|
251
|
+
| `verdict` | `PASS` · `FAIL` · `INDETERMINATE` | yes | The machine-extracted verdict; `INDETERMINATE` is the fail-closed verdict-extraction failure (bounded re-ask exhausted) — the grounds are still preserved |
|
|
252
|
+
| `grounds` | string (1–1000000) | yes | The full worker report, uncapped |
|
|
253
|
+
| `review_basis` | `mechanical-fail` · `branch-diff` · `runtime-state-none` · `frozen-merge:<40-hex-sha>` · null | no | What the review actually examined (blocker-34 branch classifier). `null` = not branch-classified (docs.js path). Shape-validated at the boundary; malformed values fail the whole `recordphase` call |
|
|
254
|
+
|
|
255
|
+
`summary` is derived from the session note actually recorded (one source of truth — never a second caller-supplied truncation). One verdict per recording session: a re-recorded phase for the same session is the same evidence, so the first write wins and rows are never revised; a new Review execution claims a new session and gets its own row. A malformed payload fails the whole `recordphase` call (fail closed) — no session, no event, no row. The response carries the recorded (or pre-existing) row as `verdict`, or `null` when no payload was sent.
|
|
256
|
+
|
|
257
|
+
### `getverdicts`
|
|
258
|
+
|
|
259
|
+
Return all structured verdict records for a task, ordered by recording time (rework rounds and retries accumulate as separate rows, never overwrite).
|
|
260
|
+
|
|
261
|
+
| Field | Type | Required | Notes |
|
|
262
|
+
|-------|------|----------|-------|
|
|
263
|
+
| `task_id` | uuid | yes | |
|
|
264
|
+
|
|
265
|
+
The CLI exposes this as `get-verdicts`.
|
|
266
|
+
|
|
182
267
|
---
|
|
183
268
|
|
|
184
269
|
## Dispatch
|
|
@@ -187,6 +272,8 @@ The CLI exposes this as `record-phase`. All CLI commands use kebab-case (`create
|
|
|
187
272
|
|
|
188
273
|
Read the full board state for dispatch decisions. Takes no arguments. Returns all projects with their tasks, simultaneity limits, quiesce state, and active sessions — everything the dispatcher needs to decide what to claim.
|
|
189
274
|
|
|
275
|
+
Like the zombie-session sweep, this call writes: it first reconciles stranded dependents — any `todo` task whose dep is currently `parked` is cascade-parked with `park_reason: "dep_parked"` attribution (see `parktask`), so no task sits `todo` behind a parked dep for more than one dispatcher tick. Idempotent across ticks; reconciliation parks, never dispatches (cascade-parked tasks carry zero sessions).
|
|
276
|
+
|
|
190
277
|
Each task in `ready_tasks` carries an explicit retry object:
|
|
191
278
|
|
|
192
279
|
| Field | Type | Notes |
|
|
@@ -209,6 +296,8 @@ A dashboard that omits `retry` or returns an invalid `consecutive_failures` (not
|
|
|
209
296
|
|
|
210
297
|
Task `state` vocabulary: `todo` = ready for dispatch; `in_progress` = a workflow run is active or awaiting its next phase; `parked` = human hold, the dispatcher skips it; `done` = all phases finished. `blocked` is never a stored task state — it is derived mechanically from unmet dependencies (surfaced as a `blocked` flag on the task) and never authored by hand.
|
|
211
298
|
|
|
299
|
+
Park attribution: when a task parks because one of its dependencies parked (cascade-park, see `parktask`), the task record carries `park_reason: "dep_parked"` and `park_dep_id` (the parked dep's id) — machine-queryable attribution on the existing `parked` state, never prose-matched. `null` on both means an ordinary park (human hold); legacy tasks read `null` (no backfill by design). Leaving `parked` clears the attribution.
|
|
300
|
+
|
|
212
301
|
Session `status` vocabulary: `failed` = a phase was attempted but did not finish (retryable); `rejected` = an explicit phase verdict (Review/QA said no), routed through rework. Workflows never record `blocked` for operational failures — that word is reserved for the dependency signal.
|
|
213
302
|
|
|
214
303
|
Two namespaces, not one: the vocabulary above is the **dashboard API** (task states, session statuses, event types). Workflow scripts additionally return a **workflow result envelope** to their launcher (`{status: "ok" | "failed" | "parked", ...}`) and may return `{__hatchWorkflowControl: "blocked"}` as a runtime halt signal. The `"blocked"` inside `__hatchWorkflowControl` is a workflow-runtime mechanism, unrelated to the dashboard's dependency-derived `blocked` — same word, different namespace. `parked` as a workflow return means the workflow parked the task via `parktask` and the launcher should treat the run as complete.
|
|
@@ -271,6 +360,8 @@ Record the start of a workflow run. The API mints the `run_id` (UUID, SQLite-sid
|
|
|
271
360
|
|
|
272
361
|
Returns `{ ok: true, run_id }`.
|
|
273
362
|
|
|
363
|
+
Refuses (2026-09-21, fail closed): starting a run for a task already in a terminal or parked state is rejected — a late start after an external park must not resurrect a `running` row.
|
|
364
|
+
|
|
274
365
|
### `record-run-event`
|
|
275
366
|
|
|
276
367
|
Record a single telemetry event. Prefer `record-run-events-batch` for multiple events.
|
|
@@ -303,6 +394,8 @@ Record the terminal state of a workflow run.
|
|
|
303
394
|
| `run_id` | string | Required. |
|
|
304
395
|
| `status` | string | Required. One of `completed`, `failed`, `parked`, `timed_out`. |
|
|
305
396
|
|
|
397
|
+
Late-write guard (2026-09-21, fail closed): the end update only lands when the run's status is still `running`. A zombie/duplicate worker's late end after the row was closed to `parked` (or any other terminal state) is a silent no-op — parking is the designed terminal state for the run, and a late writer must never overwrite it.
|
|
398
|
+
|
|
306
399
|
### `get-run-timeline`
|
|
307
400
|
|
|
308
401
|
Get a workflow run (or all runs for a task) with its events.
|
|
@@ -360,7 +453,7 @@ Retry a task whose workflow died on a platform failure. Clears the stale reserva
|
|
|
360
453
|
- on `requeued`: running sessions → `stalled` (the dispatcher already treats `stalled` as a retry candidate);
|
|
361
454
|
- on `parked`: running sessions → `failed` (what `recover-task` accepts), same as `park-task`.
|
|
362
455
|
|
|
363
|
-
Returns `{ ok: true, action: "requeued"|"parked"|"skipped", ..., settled_sessions: <n> }
|
|
456
|
+
Returns `{ ok: true, action: "requeued"|"parked"|"skipped", ..., settled_sessions: <n>, closed_workflow_runs: <n> }` — the parked path also closes the task's open `workflow_runs` rows to `parked` in the same transaction (room #26 blocker 37, 2026-09-21).
|
|
364
457
|
|
|
365
458
|
**Supersede guard:** if a newer platform run has been linked for this task since this failure's run, the task is already owned by the successor — the retry is skipped instead of clobbering live work (a late detection of an old dead run never resets a redispatched task).
|
|
366
459
|
|
|
@@ -483,9 +576,10 @@ project row carries its own publish base (`provenance_source_commit`,
|
|
|
483
576
|
a crew home with N artifact projects carries N independent bases. There are
|
|
484
577
|
two stamp call paths, and they are different operations on the same row:
|
|
485
578
|
|
|
486
|
-
- **Full stamp** — called by the
|
|
487
|
-
|
|
488
|
-
|
|
579
|
+
- **Full stamp** — called by the ack scan's `record-version-ack` (the 0.14.6
|
|
580
|
+
version machine) after the exact per-attempt version is observed on the
|
|
581
|
+
artifact's disk: `source_commit` is the acknowledged build commit,
|
|
582
|
+
`crew_release` the active release at publish time.
|
|
489
583
|
- **Refresh** — called by the release installer (`lib/advance-publish-base.js`)
|
|
490
584
|
after installing/activating a crew release: `crew_release` is refreshed to
|
|
491
585
|
the live release identity while preserving the existing `source_commit`.
|
package/docs/decisions/AGENTS.md
CHANGED
|
@@ -28,7 +28,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
|
|
|
28
28
|
- `#provenance-refresh` — Provenance refresh for self-publishes
|
|
29
29
|
- `#parent-owned-verification` — Publish content verification — parent-owned
|
|
30
30
|
- `#already-merged-corrective` — Already-merged corrective
|
|
31
|
+
- `#false-negative-review-guard` — False-negative Review guard (room #26 blocker 34 — deleted, redesigned as the pre-Review branch-state classifier)
|
|
31
32
|
- `#merge-record-helpers` — Merge-record helpers
|
|
33
|
+
- `#stale-merge-recovery-identity` — Stale-merge recovery identity (room #26 blocker 35)
|
|
32
34
|
- `#publish-diff-base` — Publish diff base (BASE..HEAD)
|
|
33
35
|
- `#attribution-limitation` — Attribution timing limitation
|
|
34
36
|
- `#read-back-request` — Publish read-back request (currently unavailable)
|
|
@@ -57,6 +59,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
|
|
|
57
59
|
- `#d1-issuance-time` — D1 issuance-time field (`issued_at`; `entry_kind` cut 2026-09-20)
|
|
58
60
|
- `#detached-head-audit` — Detached-HEAD audit (0.14.2)
|
|
59
61
|
- `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
|
|
62
|
+
- `#version-acknowledgement` — Version acknowledgement replaces content verification (0.14.6)
|
|
63
|
+
- `#version-review-dispositions` — 2026-09-20 amended REVIEW dispositions (14 mechanical fixes)
|
|
64
|
+
- `#version-not-hash` — 2026-09-20: version number replaces the sha256 nonce
|
|
60
65
|
|
|
61
66
|
## qa-reproduce.md — Decision history: qa reproduce
|
|
62
67
|
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Decision history: publish path
|
|
2
2
|
|
|
3
|
+
> **SUPERSESSION NOTICE (2026-09-20, 0.14.6 version machine REVIEW):** This
|
|
4
|
+
> document records the decision history of the RETIRED publish machinery
|
|
5
|
+
> (two-party trigger, fire-and-forget observation, content verification,
|
|
6
|
+
> pre-trigger baselines, artifact toolcheck). The sections below describe
|
|
7
|
+
> that machinery in present tense as "Invariants" — they are HISTORICAL,
|
|
8
|
+
> not current. The current publish path is the one-party worker-owned version
|
|
9
|
+
> acknowledgement machine: the workflow stages a checksummed diff and
|
|
10
|
+
> parks with `publish: publish-requested`; the session-carrying tick
|
|
11
|
+
> worker claims the intent, issues the edit via `artifact_edit`, and the
|
|
12
|
+
> ack scan observes the exact per-attempt version on the artifact's disk as
|
|
13
|
+
> the sole positive evidence. See `docs/publish-verification.md` for the
|
|
14
|
+
> current contract.
|
|
15
|
+
|
|
3
16
|
Relocated from workflow source comments during H5 (2026-09-18). The workflows keep only the relied-upon invariant inline; the full decision history lives here.
|
|
4
17
|
|
|
5
18
|
<a id="fire-and-forget-trigger"></a>
|
|
@@ -694,16 +707,39 @@ Applies to: chore.
|
|
|
694
707
|
<a id="already-merged-corrective"></a>
|
|
695
708
|
## Already-merged corrective
|
|
696
709
|
|
|
697
|
-
Invariant:
|
|
710
|
+
Invariant: whether the branch's work is already on the line is a git fact, answered BEFORE Review in git, never in prose and never by the reviewer. The pre-Review branch-state classifier (`classify-branch <task_id>` in `lib/worktree-lifecycle.sh`, pure git) emits exactly one stdout line — `BRANCH_STATE: has-work | already-merged:<40-hex> | empty-no-work` — and exits 1 with diagnostics on stderr when the branch is unclassifiable (fail closed: an unclassifiable branch is treated as empty-no-work, a mechanical FAIL).
|
|
698
711
|
|
|
699
|
-
|
|
712
|
+
Attribution is identity-first, never declared: (1) the live merge carrying the branch's CURRENT tip (`current_branch_merge`, blocker 35's identity primitive) — if the tip is on the live line via a merge, that merge IS the deliverable; (2) the task's own durable merge records (`$CREW_HOME/.merge-records/<task_id>`, append-only, preserved by post-deploy), newest first, at most 20 candidates — the first that resolves to a MERGE commit and is an ancestor of the live integration target wins, canonicalized to full 40-hex. An arbitrary non-merge ancestor is never the task's merge. The newest stale-but-resolvable record poisons older fallback.
|
|
713
|
+
|
|
714
|
+
Routing: `has-work` → Cass reviews the normal inspect; `already-merged:<sha>` → Cass reviews the frozen first-parent diff (`git diff <sha>^1 <sha>`) — she judges quality/spec compliance, never emptiness; `empty-no-work` → mechanical FAIL, Cass never dispatched — unless the plain `repo_diff: none` runtime-state marker survives in the Build notes, in which case Cass gets a plausibility judgment (the only empty-branch case she ever sees). The old empty-branch rule, the Build's SHA-bearing `repo_diff: none (already-merged: <sha>)` declaration, the declared-sha classifier input, Review hydration, and the post-rejection corrective are all deleted. The structured `review_basis` verdict field records what the review actually examined (`mechanical-fail` | `branch-diff` | `runtime-state-none` | `frozen-merge:<sha>`).
|
|
715
|
+
|
|
716
|
+
Applies to: standard, bugfix, chore. docs.js has no empty-branch rule and carries no classifier.
|
|
717
|
+
|
|
718
|
+
```
|
|
719
|
+
// Already-merged corrective (room #26 blocker 34, redesign 2026-09-21):
|
|
720
|
+
// branch state is classified in git before Review (classify-branch):
|
|
721
|
+
// has-work | already-merged:<sha> | empty-no-work. The reviewer never
|
|
722
|
+
// adjudicates git identity.
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
<a id="false-negative-review-guard"></a>
|
|
726
|
+
## False-negative Review guard (deleted — redesigned as the pre-Review branch-state classifier)
|
|
727
|
+
|
|
728
|
+
Incident: room #26 J2 parked after cass's Review false-rejected an empty branch. The shuffle deliverable had already landed on main via the task's own prior Integrate (merge e69ae33e), so the rework Build committed nothing — and at 06:25:49Z the Build had in fact declared `repo_diff: none (already-merged: e69ae33ea11d696e428a17290fce6e33d1833312)` in its report (earlier notes omitted this declaration — the correction matters: the evidence WAS there). Cass rejected at 06:30:34Z anyway, claiming no declaration existed and confusing the verified sha with git's empty-tree object (4b825dc642cb6eb9a060e54bf8d69288fbee4904). The shared rework budget burned on the false negative and the journey parked.
|
|
729
|
+
|
|
730
|
+
First fix (deleted): a post-rejection safeguard — the workflow checked the task's merge record after Review rejected, recovered the sha, skipped the budget check for guard-fired rounds, and parsed reviewer prose for the empty-branch sentence to drive git state. It worked but answered the question AFTER the rejection, in prose, at the wrong layer.
|
|
731
|
+
|
|
732
|
+
Redesign (2026-09-21, blocker 34, v2.1): the question is answered BEFORE Review, in git, never in prose — see `#already-merged-corrective` for the canonical design. The declared-sha path was deleted from the classifier per the v2.1 reconciliation: the classifier is a pure function of (branch, merge records, target), `classify-branch <task_id>`, one argument. Merge records survive deployment (post-deploy no longer deletes them — they are the durable per-task delivery log). Routing summary: `has-work` → Cass on the normal inspect; `already-merged:<sha>` → Cass reviews the frozen first-parent diff; `empty-no-work` → mechanical FAIL without dispatching Cass, with the `REVIEW_BASIS:` session-note marker naming the basis (mechanical-fail / frozen-merge / branch-diff / runtime-state-none). The plain `repo_diff: none` runtime-state claim is captured at Build closeout from the full report (same-process Review never depends on the 2000-char session-note truncation); there is no hydration fallback for the plain claim. The rework budget applies uniformly to every rejection; no prose regex ever drives git state. The post-rejection guard, its test file, and the Case A/B budget exceptions are deleted.
|
|
733
|
+
|
|
734
|
+
Follow-up (2026-09-21, same shape): the npm `package.json` version check is also a mechanical git fact, not a reviewer judgment. `lib/worktree-lifecycle.sh` gains `version-check <task_id>` (pure git, no LLM): stdout carries only `VERSION_TOUCHED` (the branch's three-dot diff against the integration target touches the `version` field — versions are assigned at publish time, never in branches) or `VERSION_CLEAN` — anything else is a resolution failure (stderr, exit 1, fail closed). For npm projects the workflow runs it before Review; anything but an explicit `VERSION_CLEAN` joins the mechanical FAIL gate (no Cass dispatch). Cass keeps judging release necessity and version-bump scope — never whether the branch touched the field.
|
|
735
|
+
|
|
736
|
+
Applies to: standard, bugfix, chore. docs.js has no empty-branch rule and carries no classifier.
|
|
700
737
|
|
|
701
738
|
```
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
// rejection already carries its own specific notes.
|
|
739
|
+
// Room #26 blocker 34 (redesign): the classifier is pure git
|
|
740
|
+
// (classify-branch in the lifecycle script): has-work |
|
|
741
|
+
// already-merged:<sha> | empty-no-work. Git facts are never
|
|
742
|
+
// adjudicated by a reviewer.
|
|
707
743
|
```
|
|
708
744
|
|
|
709
745
|
<a id="merge-record-helpers"></a>
|
|
@@ -721,6 +757,15 @@ Applies to: standard, bugfix, chore.
|
|
|
721
757
|
// no clock. Byte-identical in standard.js, bugfix.js, chore.js.
|
|
722
758
|
```
|
|
723
759
|
|
|
760
|
+
<a id="stale-merge-recovery-identity"></a>
|
|
761
|
+
## Stale-merge recovery identity (blocker 35, 2026-09-21)
|
|
762
|
+
|
|
763
|
+
Room #26 J3 parked after 3 consecutive Integrate failures. Mechanical: Integrate #1 merged locally but the push failed on an SSH host-key flake; the ERROR-after-MERGED retry path later pushed a stale merge; meanwhile rework had changed the task branch, so subsequent Integrates collided with the task's own stale merge (CONFLICT in theme.css). Two fibs: (a) the push environment is trusted but flaked; (b) "retry the push" assumed branch state == pushed state.
|
|
764
|
+
|
|
765
|
+
The fix is in the lifecycle, not the prompt. The old merged-but-unpushed recovery checked only that the RECORDED merge is an ancestor of live HEAD — ancestry proves containment, not identity. Both recovery paths (`cmd_integrate`'s ahead==0 branch and `cmd_push_target`'s record path) now prove identity via `current_branch_merge()`: the newest merge on live HEAD's first-parent chain whose branch-side parent (^2) is the branch's CURRENT tip. cmd_integrate always merges with --no-ff, so M^2 is the branch tip at merge time. The first-parent walk also covers do_push's reconcile merges above the recorded commit and the manual R5 merge (scratch-worktree merge, ff'd onto the line). No such merge plus a record means the branch moved since the merge — STALE_MERGE fails closed (never pushed, never MERGED_EMPTY: an unpushed merge may sit on the line and MERGED_EMPTY would report PASS while the publish diff still carries the abandoned work). Branch gone (merge-lease reclaim): no rework could have moved it, so the recorded merge on the line is still the deliverable — the old ancestry check is kept for exactly that case. The STALE_MERGE marker is documented in the Integrate instructions as VERDICT: FAIL (standard/bugfix/chore).
|
|
766
|
+
|
|
767
|
+
Applies to: lib/worktree-lifecycle.sh; standard, bugfix, chore (one marker line).
|
|
768
|
+
|
|
724
769
|
<a id="publish-diff-base"></a>
|
|
725
770
|
## Publish diff base (BASE..HEAD)
|
|
726
771
|
|
|
@@ -911,6 +956,8 @@ Applies to: standard, bugfix, chore.
|
|
|
911
956
|
<a id="already-merged-hydration"></a>
|
|
912
957
|
## Already-merged hydration
|
|
913
958
|
|
|
959
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the hydration ferry (structured field first, notes fallback) is deleted. See `#already-merged-corrective` for the current design.
|
|
960
|
+
|
|
914
961
|
Invariant: when the run did not execute Build itself, recover the workflow-verified sha; the structured session field is read FIRST, the notes line is only a fallback (session notes are hard-capped at 3000 chars).
|
|
915
962
|
|
|
916
963
|
Applies to: standard, bugfix, chore.
|
|
@@ -932,6 +979,8 @@ Applies to: standard, bugfix, chore.
|
|
|
932
979
|
<a id="already-merged-declaration"></a>
|
|
933
980
|
## Already-merged declaration
|
|
934
981
|
|
|
982
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the agent-authored `repo_diff: none (already-merged: <sha>)` declaration is deleted. See `#already-merged-corrective` for the current design.
|
|
983
|
+
|
|
935
984
|
Invariant: when the builder correctly makes no commit because the deliverable is already on main, it declares repo_diff: none (already-merged: <sha>) naming the main commit that carries the work.
|
|
936
985
|
|
|
937
986
|
Applies to: standard, bugfix, chore.
|
|
@@ -1065,6 +1114,8 @@ Applies to: standard, bugfix, chore.
|
|
|
1065
1114
|
<a id="already-merged"></a>
|
|
1066
1115
|
## Already-merged attestation
|
|
1067
1116
|
|
|
1117
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the Build-gate attestation (`already_merged_verified:`) is deleted. See `#already-merged-corrective` for the current design.
|
|
1118
|
+
|
|
1068
1119
|
Invariant: when the Build gate verifies already-merged, attestation is recorded.
|
|
1069
1120
|
|
|
1070
1121
|
Applies to: standard, bugfix, chore.
|
|
@@ -1142,6 +1193,8 @@ Applies to: standard, bugfix, chore.
|
|
|
1142
1193
|
<a id="already-merged-idem"></a>
|
|
1143
1194
|
## Already-merged idempotency
|
|
1144
1195
|
|
|
1196
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the agent-authored declaration + mechanical verification is deleted. See `#already-merged-corrective` for the current design.
|
|
1197
|
+
|
|
1145
1198
|
Invariant: an already-merged repo_diff is idempotent; no rebuild.
|
|
1146
1199
|
|
|
1147
1200
|
Applies to: standard, bugfix, chore.
|
|
@@ -1239,6 +1292,8 @@ Applies to: standard, bugfix, chore.
|
|
|
1239
1292
|
<a id="already-merged-hydra"></a>
|
|
1240
1293
|
## Already-merged hydration
|
|
1241
1294
|
|
|
1295
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the hydration ferry is deleted. See `#already-merged-corrective` for the current design.
|
|
1296
|
+
|
|
1242
1297
|
Invariant: when this run did not execute, hydration uses the existing merge.
|
|
1243
1298
|
|
|
1244
1299
|
Applies to: standard, bugfix, chore.
|
|
@@ -1260,7 +1315,13 @@ Applies to: standard, bugfix, chore.
|
|
|
1260
1315
|
<a id="already-merged-idem2"></a>
|
|
1261
1316
|
## Already-merged idempotency
|
|
1262
1317
|
|
|
1263
|
-
|
|
1318
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the agent-authored
|
|
1319
|
+
`repo_diff: none (already-merged: <sha>)` declaration is deleted. Attribution
|
|
1320
|
+
is now classifier-only — see `#already-merged-corrective` for the current
|
|
1321
|
+
design (pre-Review `classify-branch`, identity-first from git, no declared
|
|
1322
|
+
sha, no hydration, no Build-closeout verification).
|
|
1323
|
+
|
|
1324
|
+
Invariant (historical): idempotency for already-merged tasks.
|
|
1264
1325
|
|
|
1265
1326
|
Applies to: standard, bugfix, chore.
|
|
1266
1327
|
|
|
@@ -1361,7 +1422,7 @@ Consumers (one anchor, shared): `lib/verify-publish.js`,
|
|
|
1361
1422
|
`lib/crew-api.js::findTriggerEntry`, the classifier
|
|
1362
1423
|
(`lib/classify-publish-absence.js`), and the retry writer
|
|
1363
1424
|
(`lib/retry-publish.js`) all take the oldest matching "submitted" by `ts`
|
|
1364
|
-
and bind `issued_at || ts`. Platform
|
|
1425
|
+
and bind `issued_at || ts`. Platform version: deferred, not filed in this
|
|
1365
1426
|
change.
|
|
1366
1427
|
|
|
1367
1428
|
Edge cases:
|
|
@@ -1560,3 +1621,180 @@ trigger anchor), lib/publish-note-vocabulary.js (publish-requested,
|
|
|
1560
1621
|
publish-intent-claimed, publish-refused), seed/cron-body-template.md
|
|
1561
1622
|
(steps 4.4b, 4.4 retry direct-issuance), docs/publish-verification.md,
|
|
1562
1623
|
docs/publish-unknown-recovery.md.
|
|
1624
|
+
|
|
1625
|
+
<a id="version-acknowledgement"></a>
|
|
1626
|
+
## Version acknowledgement replaces content verification (0.14.6, 2026-09-20)
|
|
1627
|
+
|
|
1628
|
+
**Eric's contract:** "If we hear that the artifact acknowledges our version,
|
|
1629
|
+
that's it. We don't verify against content."
|
|
1630
|
+
|
|
1631
|
+
The content-verdict machine (read-back, `verify-publish.js`, the unknown-
|
|
1632
|
+
recovery classifier, the retry harness) is deleted. Exact per-attempt version
|
|
1633
|
+
acknowledgement is the sole positive completion criterion.
|
|
1634
|
+
|
|
1635
|
+
**The version:** `sha256("publish-version:v1:" + task_id + ":" + commit + ":" +
|
|
1636
|
+
publish_attempt)` — deterministic, 64 hex. Synthesized into the staged diff
|
|
1637
|
+
as `.crew-publish-version` (checksum covers it; never committed). The workflow
|
|
1638
|
+
replay key (`attempt`, a string) is separate from the numeric
|
|
1639
|
+
`publish_attempt`.
|
|
1640
|
+
|
|
1641
|
+
**The loop:** workflow stages intent (attempt 1) → tick worker claims via
|
|
1642
|
+
`scan-publish-intent` → issues directly → `record-intent-issuance` writes
|
|
1643
|
+
`submitted` + `publish: edit-issued` → `scan-ack-pending` watches for the
|
|
1644
|
+
exact version on disk or in a builder report. Any issued version for the
|
|
1645
|
+
task/commit counts (late earlier attempts included). Refusal is terminal.
|
|
1646
|
+
45-min window per attempt, 3 attempts max, 15/45-min backoff. Third expiry
|
|
1647
|
+
→ `publish: version-timeout`.
|
|
1648
|
+
|
|
1649
|
+
**Provenance certifies request acknowledgement, not byte equality.**
|
|
1650
|
+
|
|
1651
|
+
Applies to: lib/compute-publish-diff.js (--task-id/--attempt, version hunk),
|
|
1652
|
+
lib/crew-api.js (scan-publish-intent, record-intent-issuance,
|
|
1653
|
+
record-builder-report, record-version-ack, scan-ack-pending,
|
|
1654
|
+
record-publish-note), lib/publish-note-vocabulary.js (WRITABLE split:
|
|
1655
|
+
legacy terminals recognized, not writable), workflows/{standard,bugfix,
|
|
1656
|
+
chore}.js (Publish exits at intent with `attempt=1`), seed/cron-body-
|
|
1657
|
+
template.md (tick protocol), docs/publish-verification.md,
|
|
1658
|
+
docs/publish-unknown-recovery.md (retired).
|
|
1659
|
+
|
|
1660
|
+
## 2026-09-20 (post-REVIEW amendment) — bounds re-set, observation split from mutation
|
|
1661
|
+
|
|
1662
|
+
Before PUBLISH, Eric challenged the bounds as unmeasured placeholders and
|
|
1663
|
+
approved a replacement:
|
|
1664
|
+
|
|
1665
|
+
- **Observation is cheap and read-only; mutation is not.** The old bounds
|
|
1666
|
+
coupled them: the 45-minute window + 15/45-minute backoffs + 3-attempt cap
|
|
1667
|
+
were all guesses, and the scan ran inside the 15-minute poll tick, so a
|
|
1668
|
+
verdict could never land sooner than the tick cadence.
|
|
1669
|
+
- **New shape:** the ack scan runs on its own 5-minute `crew-ack-scan` cron
|
|
1670
|
+
(observation-only — it never issues edits); the 15-minute poll tick keeps
|
|
1671
|
+
the claim-and-issue path. The scan cadence no longer constrains verdict
|
|
1672
|
+
latency: with 6 scans per window, the verdict lands within one cadence of
|
|
1673
|
+
the ack or the expiry.
|
|
1674
|
+
- **New bounds:** 30-minute acknowledgement window per attempt (anchored at
|
|
1675
|
+
the LATEST issuance — evidence-set: room #25's one healthy ack landed
|
|
1676
|
+
~15-17 minutes after issuance, so the window is ~2x that plus margin),
|
|
1677
|
+
immediate reissue at expiry (`not_before: null` — no backoff), and a
|
|
1678
|
+
2-hour total budget from the FIRST issuance, after which the scan parks
|
|
1679
|
+
terminal `publish: version-timeout`. The fixed 3-attempt cap is gone — a
|
|
1680
|
+
slow-but-healthy artifact no longer false-timeouts, and a dead one still
|
|
1681
|
+
terminates on the budget. The ledger records issued_at and first-seen ack
|
|
1682
|
+
per attempt, so the window is tunable from measured data.
|
|
1683
|
+
|
|
1684
|
+
<a id="version-review-dispositions"></a>
|
|
1685
|
+
## 2026-09-20 (amended REVIEW dispositions) — 14 mechanical fixes
|
|
1686
|
+
|
|
1687
|
+
A five-critic read-only review (Architect, Subtractor, Reliability, Room
|
|
1688
|
+
Operator + step-back) of the amended 0.14.6 tree returned
|
|
1689
|
+
ACCEPT-WITH-CONCERNS, no REJECTs. All 14 dispositions applied before the
|
|
1690
|
+
amended BUILD commit:
|
|
1691
|
+
|
|
1692
|
+
1. **Torn reissue staging is note-keyed.** The ack scan's reissue
|
|
1693
|
+
idempotency guard is keyed on the `publish: publish-requested <commit>
|
|
1694
|
+
attempt=N` note — the same anchor `scan-publish-intent` claims on —
|
|
1695
|
+
not the ledger line. A crash between the ledger append and the note
|
|
1696
|
+
write (or a rolled-back COMMIT) leaves an orphan line; the note-keyed
|
|
1697
|
+
guard self-heals by re-appending the content-identical line. Write
|
|
1698
|
+
order stays line-then-note: note-first would park permanently on a
|
|
1699
|
+
tear.
|
|
1700
|
+
2. **`publish: version-timeout` vocabulary** now reads the 2-hour
|
|
1701
|
+
acknowledgement-budget meaning, not "three issuance attempts
|
|
1702
|
+
exhausted".
|
|
1703
|
+
3. **Dead backoff guard deleted** from `scan-publish-intent` (the amended
|
|
1704
|
+
design never writes `not_before`).
|
|
1705
|
+
4. **`not_before` plumbing removed**: no longer on claimed intents,
|
|
1706
|
+
reissue results, or ledger lines — the field is gone entirely, not
|
|
1707
|
+
even null-inline (nothing reads it; a dead field is the opposite of
|
|
1708
|
+
the disposition).
|
|
1709
|
+
5. **Version-mismatch is per-entry**: a corrupt submitted entry is
|
|
1710
|
+
quarantined; valid derivation-verified entries still drive the windows
|
|
1711
|
+
and the budget. Only when NO entry verifies does the record park as
|
|
1712
|
+
`version-mismatch`.
|
|
1713
|
+
6. **Unparseable latest issuance is an unknown window**: never reported
|
|
1714
|
+
as "window-open"; the budget check still runs (a corrupt latest
|
|
1715
|
+
timestamp cannot defer the terminal timeout), but reissue is withheld
|
|
1716
|
+
until the budget decides.
|
|
1717
|
+
7. **Ack-scan failure is mechanical**: nonzero exit → report stderr
|
|
1718
|
+
verbatim, take no other action, no rerun, no other Crew API command,
|
|
1719
|
+
stop.
|
|
1720
|
+
8. **Tests extended** in the existing `tests/scan-publish-intent.test.js`
|
|
1721
|
+
(no new file): reissue idempotency across repeated scans, exact `>=`
|
|
1722
|
+
budget boundary (inside reissues, at-boundary times out), unparseable
|
|
1723
|
+
first issuance (`first-issuance-unparseable` park), unparseable latest
|
|
1724
|
+
issuance (reissue withheld), duplicate submitted entries (deduped),
|
|
1725
|
+
corrupt version beside a valid entry (quarantined), all-invalid
|
|
1726
|
+
versions (`version-mismatch` park), on-disk version naming no issued
|
|
1727
|
+
attempt (ignored), per-task receipts never cross-attributed, torn
|
|
1728
|
+
reissue prepare self-heals via the note-keyed guard.
|
|
1729
|
+
9. **Receipt scoped per task** (verified possible): per-project
|
|
1730
|
+
simultaneity defaults to 2 and parked tasks don't count as in-flight,
|
|
1731
|
+
so two same-project tasks CAN sit in `edit-issued` concurrently — a
|
|
1732
|
+
shared per-slug `.crew-publish-version` would let each issuance
|
|
1733
|
+
overwrite the other's acknowledgement (receipt ping-pong). The
|
|
1734
|
+
receipt is now `.crew-publish-version-<task_id>` in the staged diff
|
|
1735
|
+
hunk and on disk.
|
|
1736
|
+
10. **Cron id overrides validated at init**: an override must start with
|
|
1737
|
+
`<manifest-id>-`, so uninstall's prefix-based discovery fallback can
|
|
1738
|
+
always find the job even if `.cron-registry.json` is lost.
|
|
1739
|
+
11. **`crew-burn-room.sh`** (outside the repo): `--crons-removed` caller
|
|
1740
|
+
attestation is REQUIRED — the script refuses to burn without it
|
|
1741
|
+
(fail-closed; it cannot reach the scheduler, so the contract is
|
|
1742
|
+
honest: the caller removes poll/ack-scan/update-watch/journey-watch
|
|
1743
|
+
via cron.remove first and attests the ids). `roomNN-journey-watch`
|
|
1744
|
+
added to the convention list. `--ts-spaces <slugs>` moves the room's
|
|
1745
|
+
artifact records into the trash (already-gone is fine).
|
|
1746
|
+
12. **Stuck-publish runbook** in `docs/guide.md`: the ack scan stages
|
|
1747
|
+
reissues, the poll tick issues them — if the poll tick is down,
|
|
1748
|
+
reissues stay staged and the budget eventually parks
|
|
1749
|
+
`publish: version-timeout`.
|
|
1750
|
+
13. **Receipt scope documented**: the version proves adoption into the
|
|
1751
|
+
artifact's on-disk source tree — not a successful rebuild or
|
|
1752
|
+
redeployment.
|
|
1753
|
+
14. **Result taxonomy fixed**: `timeouts[]` is terminal-budget exhaustion
|
|
1754
|
+
only; window expiries are `reissued[]`.
|
|
1755
|
+
|
|
1756
|
+
**Follow-up fix found in final pre-commit inspection (same day):** the
|
|
1757
|
+
version-not-hash cut left the old 64-hex nonce regex in the Publish
|
|
1758
|
+
phase of `workflows/standard.js`, `bugfix.js`, and `chore.js` — the
|
|
1759
|
+
format gate would have rejected every `<commit>:<attempt>` version and
|
|
1760
|
+
parked every publish. All three now mirror `isPublishVersion()` from
|
|
1761
|
+
`lib/crew-api.js`, and `tests/compute-publish-diff.test.js` carries a
|
|
1762
|
+
mechanical guard asserting the workflow regex matches the library's
|
|
1763
|
+
(and the stale 64-hex shape is gone). The full suite passed before this
|
|
1764
|
+
was found because no test executes the workflow's Publish gate — the
|
|
1765
|
+
guard closes that hole.
|
|
1766
|
+
|
|
1767
|
+
Step-back conclusions recorded: the five-minute observer costs ~192
|
|
1768
|
+
marginal task-mode turns/day/room (accepted for current VERIFY
|
|
1769
|
+
turnaround); the machine is a compensating control for the platform's
|
|
1770
|
+
missing real completion receipt and should be deleted the day the
|
|
1771
|
+
platform issues one (see `docs/publish-verification.md`
|
|
1772
|
+
§ Decommissioning). Non-blocking backlog: at final timeout, a
|
|
1773
|
+
machine-owned audit-directory check could split the park note into
|
|
1774
|
+
"landed-but-receiptless" vs "platform-silent".
|
|
1775
|
+
|
|
1776
|
+
<a id="version-not-hash"></a>
|
|
1777
|
+
## 2026-09-20 — Version number replaces the sha256 nonce
|
|
1778
|
+
|
|
1779
|
+
Eric: a nonce is a hash plus a version number — why not just the version
|
|
1780
|
+
number? He was right. The sha256 defended against a forger that doesn't
|
|
1781
|
+
exist: the builder's failure mode is omission (zero-tool runs), never
|
|
1782
|
+
forging our receipts. The artifact's real failure mode — an unfaithful
|
|
1783
|
+
carrier that mangles bytes or ignores the request — defeats a hash and a
|
|
1784
|
+
plain counter identically. We own the code, so we count our own attempts;
|
|
1785
|
+
we don't own the artifact, so verification stays observation of its state,
|
|
1786
|
+
never its attestation. The hash pretended to buy attestation.
|
|
1787
|
+
|
|
1788
|
+
The per-attempt version is now `<commit>:<attempt>` (e.g.
|
|
1789
|
+
`4b825dc6…:2`) — no hash, no task_id (the receipt path
|
|
1790
|
+
`.crew-publish-version-<task_id>` and the per-task ledger scoping already
|
|
1791
|
+
carry the task). The commit binds the version to the exact code, so a stale
|
|
1792
|
+
receipt from an earlier commit can never equal a fresh version — the binding
|
|
1793
|
+
the hash used to provide, kept without the cryptography. Derivation is a
|
|
1794
|
+
plain concatenation, byte-identical between `compute-publish-diff.js` and
|
|
1795
|
+
`derivePublishVersion()` in `crew-api.js`; well-formedness is
|
|
1796
|
+
`/^[0-9a-f]{40}:[1-9][0-9]*$/` (`isPublishVersion`). The retry machine,
|
|
1797
|
+
windows, budget, and terminal vocabulary are unchanged — only the token got
|
|
1798
|
+
honest. Side benefit, and Eric's original motive: the version is
|
|
1799
|
+
human-readable observability ("roughly what's deployed") where the hash was
|
|
1800
|
+
opaque.
|
|
@@ -484,3 +484,33 @@ Applies to: standard, bugfix, chore.
|
|
|
484
484
|
// tests/pin-location.test.js.
|
|
485
485
|
```
|
|
486
486
|
|
|
487
|
+
|
|
488
|
+
<a id="content-finding-attribution"></a>
|
|
489
|
+
## Content-finding attribution (blocker 38, 2026-09-21)
|
|
490
|
+
|
|
491
|
+
Invariant: Hazel reports user-visible content observations in a machine-readable CONTENT-FINDINGS block; the workflow classifies each against the task's publish-diff file set by mechanical set membership; attributable findings become workflow-filed bugfixes; environment-attributable findings are recorded as note events with their attribution — never a bugfix; unknown attribution (diff unavailable) fails closed as "unknown", never assumed environment-attributable. The QA verdict is never overridden by finding attribution — if Hazel reports FAIL, it stands; the recorded attributions give the human evidence to distinguish environment residue from task failure.
|
|
492
|
+
|
|
493
|
+
Applies to: standard, bugfix.
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
// Room #26 (2026-09-21): an earlier hosted audit created a "Timer
|
|
497
|
+
// verification deck" through the live artifact; a later shared-concurrent
|
|
498
|
+
// audit observed that persistent row as user-facing content and criticized
|
|
499
|
+
// it. The live production app.db persisted through redeploy while isolated
|
|
500
|
+
// audit DBs sat unused — the shared-concurrent path wrote production.
|
|
501
|
+
// Delphi panel converged 4–0: the required platform fix is isolation using
|
|
502
|
+
// the incumbent isolated workspaces; the crew-side guard classifies
|
|
503
|
+
// findings by attribution. Platform-side isolation is out of crew scope —
|
|
504
|
+
// the crew does not control the hosted shared-concurrent audit write path
|
|
505
|
+
// (zero repo hits; audit data lives under ~/workspace/ts-spaces/<slug>/).
|
|
506
|
+
// Crew-side guard (this decision): publish diff file set persisted at
|
|
507
|
+
// Publish; CONTENT-FINDINGS protocol in the QA prompt (Hazel never files
|
|
508
|
+
// content bugfixes directly); deterministic classifyContentFinding at QA
|
|
509
|
+
// closeout; findings recorded with attribution, never suppressed for
|
|
510
|
+
// looking audit-y. Unknown attribution fails closed — the verdict stands.
|
|
511
|
+
// "live-data" is never in a git publish diff, so it always classifies
|
|
512
|
+
// environment-attributable: the crew has no live-DB write path (blocker 32:
|
|
513
|
+
// QA writes go to a fresh per-run temp DB), so live content is never the
|
|
514
|
+
// task's change. Functional and non-content failures are untouched — they
|
|
515
|
+
// remain actionable.
|
|
516
|
+
```
|