muse-crew 0.14.6 → 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 +45 -5
- package/docs/decisions/AGENTS.md +2 -0
- package/docs/decisions/publish-path.md +56 -8
- package/docs/decisions/qa-reproduce.md +30 -0
- package/lib/AGENTS.md +6 -5
- package/lib/crew-api.js +403 -7
- package/lib/merge-lock.sh +153 -41
- package/lib/qa-db.js +132 -0
- package/lib/schema.sql +50 -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/cron-body-template.md +6 -6
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +434 -106
- package/workflows/chore.js +251 -108
- package/workflows/crew-dispatch.js +57 -4
- package/workflows/docs.js +24 -2
- package/workflows/standard.js +439 -111
- package/workflows/upgrade.js +13 -1
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,7 +92,9 @@ 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
|
+
|
|
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.
|
|
96
98
|
|
|
97
99
|
### `scan-publish-intent`
|
|
98
100
|
|
|
@@ -110,9 +112,13 @@ Records the tick worker's direct issuance outcome (0.14.6). Compare-and-swap on
|
|
|
110
112
|
| `claim_expiry` | ISO-8601 | yes | Must match the claim's expiry (CAS) |
|
|
111
113
|
| `edit_outcome` | `accepted`\|`refused` | yes | The direct `artifact_edit` outcome |
|
|
112
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 |
|
|
113
117
|
|
|
114
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.
|
|
115
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
|
+
|
|
116
122
|
### `record-builder-report`
|
|
117
123
|
|
|
118
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.
|
|
@@ -177,9 +183,9 @@ Create or update an agent session record. Sessions track which identity is worki
|
|
|
177
183
|
| `notes` | string (≤ 3000) | no | |
|
|
178
184
|
| `failure_reason` | string (≤ 3000) or null | no | |
|
|
179
185
|
| `caveats` | array of `{text, severity}` | no | |
|
|
180
|
-
| `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). |
|
|
181
187
|
|
|
182
|
-
|
|
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.
|
|
183
189
|
|
|
184
190
|
### `heartbeat-session`
|
|
185
191
|
|
|
@@ -229,9 +235,35 @@ Atomically record a workflow phase outcome: writes the agent session (with its v
|
|
|
229
235
|
| `notes` | string (≤ 3000) | no | Phase summary |
|
|
230
236
|
| `event_type` | event type | no | Defaults to the `status` value; use `note` for non-verdict events |
|
|
231
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 |
|
|
232
239
|
|
|
233
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.
|
|
234
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
|
+
|
|
235
267
|
---
|
|
236
268
|
|
|
237
269
|
## Dispatch
|
|
@@ -240,6 +272,8 @@ The CLI exposes this as `record-phase`. All CLI commands use kebab-case (`create
|
|
|
240
272
|
|
|
241
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.
|
|
242
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
|
+
|
|
243
277
|
Each task in `ready_tasks` carries an explicit retry object:
|
|
244
278
|
|
|
245
279
|
| Field | Type | Notes |
|
|
@@ -262,6 +296,8 @@ A dashboard that omits `retry` or returns an invalid `consecutive_failures` (not
|
|
|
262
296
|
|
|
263
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.
|
|
264
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
|
+
|
|
265
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.
|
|
266
302
|
|
|
267
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.
|
|
@@ -324,6 +360,8 @@ Record the start of a workflow run. The API mints the `run_id` (UUID, SQLite-sid
|
|
|
324
360
|
|
|
325
361
|
Returns `{ ok: true, run_id }`.
|
|
326
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
|
+
|
|
327
365
|
### `record-run-event`
|
|
328
366
|
|
|
329
367
|
Record a single telemetry event. Prefer `record-run-events-batch` for multiple events.
|
|
@@ -356,6 +394,8 @@ Record the terminal state of a workflow run.
|
|
|
356
394
|
| `run_id` | string | Required. |
|
|
357
395
|
| `status` | string | Required. One of `completed`, `failed`, `parked`, `timed_out`. |
|
|
358
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
|
+
|
|
359
399
|
### `get-run-timeline`
|
|
360
400
|
|
|
361
401
|
Get a workflow run (or all runs for a task) with its events.
|
|
@@ -413,7 +453,7 @@ Retry a task whose workflow died on a platform failure. Clears the stale reserva
|
|
|
413
453
|
- on `requeued`: running sessions → `stalled` (the dispatcher already treats `stalled` as a retry candidate);
|
|
414
454
|
- on `parked`: running sessions → `failed` (what `recover-task` accepts), same as `park-task`.
|
|
415
455
|
|
|
416
|
-
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).
|
|
417
457
|
|
|
418
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).
|
|
419
459
|
|
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)
|
|
@@ -707,16 +707,39 @@ Applies to: chore.
|
|
|
707
707
|
<a id="already-merged-corrective"></a>
|
|
708
708
|
## Already-merged corrective
|
|
709
709
|
|
|
710
|
-
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).
|
|
711
711
|
|
|
712
|
-
|
|
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.
|
|
713
737
|
|
|
714
738
|
```
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// 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.
|
|
720
743
|
```
|
|
721
744
|
|
|
722
745
|
<a id="merge-record-helpers"></a>
|
|
@@ -734,6 +757,15 @@ Applies to: standard, bugfix, chore.
|
|
|
734
757
|
// no clock. Byte-identical in standard.js, bugfix.js, chore.js.
|
|
735
758
|
```
|
|
736
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
|
+
|
|
737
769
|
<a id="publish-diff-base"></a>
|
|
738
770
|
## Publish diff base (BASE..HEAD)
|
|
739
771
|
|
|
@@ -924,6 +956,8 @@ Applies to: standard, bugfix, chore.
|
|
|
924
956
|
<a id="already-merged-hydration"></a>
|
|
925
957
|
## Already-merged hydration
|
|
926
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
|
+
|
|
927
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).
|
|
928
962
|
|
|
929
963
|
Applies to: standard, bugfix, chore.
|
|
@@ -945,6 +979,8 @@ Applies to: standard, bugfix, chore.
|
|
|
945
979
|
<a id="already-merged-declaration"></a>
|
|
946
980
|
## Already-merged declaration
|
|
947
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
|
+
|
|
948
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.
|
|
949
985
|
|
|
950
986
|
Applies to: standard, bugfix, chore.
|
|
@@ -1078,6 +1114,8 @@ Applies to: standard, bugfix, chore.
|
|
|
1078
1114
|
<a id="already-merged"></a>
|
|
1079
1115
|
## Already-merged attestation
|
|
1080
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
|
+
|
|
1081
1119
|
Invariant: when the Build gate verifies already-merged, attestation is recorded.
|
|
1082
1120
|
|
|
1083
1121
|
Applies to: standard, bugfix, chore.
|
|
@@ -1155,6 +1193,8 @@ Applies to: standard, bugfix, chore.
|
|
|
1155
1193
|
<a id="already-merged-idem"></a>
|
|
1156
1194
|
## Already-merged idempotency
|
|
1157
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
|
+
|
|
1158
1198
|
Invariant: an already-merged repo_diff is idempotent; no rebuild.
|
|
1159
1199
|
|
|
1160
1200
|
Applies to: standard, bugfix, chore.
|
|
@@ -1252,6 +1292,8 @@ Applies to: standard, bugfix, chore.
|
|
|
1252
1292
|
<a id="already-merged-hydra"></a>
|
|
1253
1293
|
## Already-merged hydration
|
|
1254
1294
|
|
|
1295
|
+
**Superseded (2026-09-21, blocker 34 redesign):** the hydration ferry is deleted. See `#already-merged-corrective` for the current design.
|
|
1296
|
+
|
|
1255
1297
|
Invariant: when this run did not execute, hydration uses the existing merge.
|
|
1256
1298
|
|
|
1257
1299
|
Applies to: standard, bugfix, chore.
|
|
@@ -1273,7 +1315,13 @@ Applies to: standard, bugfix, chore.
|
|
|
1273
1315
|
<a id="already-merged-idem2"></a>
|
|
1274
1316
|
## Already-merged idempotency
|
|
1275
1317
|
|
|
1276
|
-
|
|
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.
|
|
1277
1325
|
|
|
1278
1326
|
Applies to: standard, bugfix, chore.
|
|
1279
1327
|
|
|
@@ -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
|
+
```
|
package/lib/AGENTS.md
CHANGED
|
@@ -9,12 +9,12 @@ Shipped library: ESM JavaScript CLIs and import-safe modules, shell scripts for
|
|
|
9
9
|
- **Release entry gate.** `_validate_lib_entries` in `crew-release.sh` (wired into `cmd_deploy` after `_validate_workflows`) executes every shipped JS entry for real through a `$CREW_HOME/current`-shaped symlink: shebang'd JS as `node <file> --help`, shebang-less JS as bare `node <file>`; `lib/*.sh` (minus `test-*.sh`) via `bash -n` and `lib/*.py` via `python3 -m py_compile` are parse-checked only (real `.sh` execution risks side effects), with bytecode kept out of the staging dir. `node --check` is banned — it can check under a different parse goal than the real loader uses (blocker 21), and V8's preparser skips function bodies; only real execution uses the loader's goal. Per-entry `ENTRY-GATE pass/FAIL` lines (FAIL reasons fold the first 10 stderr lines), one row per entry in `<staging>/entry-gate.json` (`test-*.sh` exclusions get `skipped` rows), exit 30 on any entry failure, deploy fails closed naming the failing entries and preserving the evidence in `$CREW_HOME`. Contract page: `docs/release-integrity.md`; behavioral pins: `tests/entry-gate.test.js`.
|
|
10
10
|
|
|
11
11
|
- `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
|
|
12
|
-
- `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. Active-release resolution is split in two (room #15): `resolveActiveReleaseName` (symlink-only — writers like the initial-provenance stamp record the active release without proving they are it) and `resolveActiveRelease` (symlink + self-path cross-check — verifiers like `scan-ack-pending` refuse to stamp claims when the running code isn't the active release's own). Provenance is per-project (2026-09-18, room #15 blocker 8): nullable `provenance_*` columns on the projects row, a single `stampProvenance()` writer, `set-provenance`/`get-provenance` require `project_id` (no silent global fallback), and a watermarked openDb backfill that attributes the legacy `config.provenance.*` triple to exactly-one ancestor match — never fabricated, otherwise deferred.
|
|
12
|
+
- `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; 2026-09-21 optionally carries a structured Review verdict payload — validated at the API boundary, written to the `verdicts` table in the same transaction, session-keyed first-write-wins idempotency — plus the `get-verdicts` read surface) and a one-time `migrate` import from a dashboard app.db. Active-release resolution is split in two (room #15): `resolveActiveReleaseName` (symlink-only — writers like the initial-provenance stamp record the active release without proving they are it) and `resolveActiveRelease` (symlink + self-path cross-check — verifiers like `scan-ack-pending` refuse to stamp claims when the running code isn't the active release's own). Provenance is per-project (2026-09-18, room #15 blocker 8): nullable `provenance_*` columns on the projects row, a single `stampProvenance()` writer, `set-provenance`/`get-provenance` require `project_id` (no silent global fallback), and a watermarked openDb backfill that attributes the legacy `config.provenance.*` triple to exactly-one ancestor match — never fabricated, otherwise deferred.
|
|
13
13
|
- `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.
|
|
14
14
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
15
|
-
- `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
|
|
16
|
-
- `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 (the integration target is the repo's current checkout — a branch name, or HEAD when detached — resolved by `integration_target()`; nothing in the lifecycle ever checks out a branch. `integrate` merges into the target, reconciles `origin/<target>` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — then pushes inline via `do_push` (no agent round-trip; 2026-09-19 REVIEW collapsed the old STEP-2 agent run, which added only a lock refresh). `do_push`: PUSHED (the refspec is always explicit — a branch pushes as `<branch>`, a detached HEAD as `HEAD:<destination>` where the destination is the remote's default branch from `resolve_push_destination()`; 2026-09-19 detached-HEAD audit REDO) / NO_REMOTE_PUSH (no origin — fail-soft, 2026-09-19 REVIEW: the old unconditional push exited 128 and mislabeled it); an unresolvable detached destination fails closed (never a silent skip, never a refusal of a knowable push); on push failure one fetch+merge retry while the lock is held, then fail closed without asserting a cause (never force-push). PUSH_SKIPPED survives only for the nothing-merged R5 path (no merge record + no lock held). Merged-but-unpushed retry recovery:
|
|
17
|
-
- `test-detached-integrate.sh` — behavioral regression tests for the detached-HEAD Integrate fix (room #21 J3, 2026-09-19; extended 2026-09-19 REVIEW): fixture A stages a detached HEAD (reviewed SHA + crew-init commit) and proves integrate leaves HEAD detached, keeps the reviewed SHA and crew-init as ancestors, lands the task merge, never moves main, records `integration_target=HEAD`, verify-merge VERIFIEDs, and push-target reports NO_REMOTE_PUSH (fixture A has no origin); fixture B proves the branch path still merges, reconciles, and pushes inline to a local origin; fixture C proves verify-merge checks the record against the LIVE tip (a detached line reset past the merge does NOT verify; the branch-gone path verifies only when the recorded commit is an ancestor of the live tip); fixture D proves the no-remote branch path fails soft with NO_REMOTE_PUSH (exit 0); fixture E proves the CREW_STAGED_BASE guard (non-ancestor base → STAGED_BASE_MISMATCH before any merge; true base → STAGED_BASE_OK); fixture F proves merged-but-unpushed retry recovery (re-running integrate after a crashed push takes the lock and pushes instead of parking on MERGED_EMPTY); fixture H proves the detached-HEAD audit REDO contract (2026-09-19) on a detached checkout with a local origin: push-destination fails closed when origin/HEAD is unset (genuinely unknowable destination), otherwise resolves the remote default branch; integrate pushes inline as `HEAD:main` (`PUSHED: origin/main (refspec HEAD:main)`), the remote ref advances to the merge commit, local main stays at the reviewed base, HEAD stays detached, verify-merge VERIFIEDs, and push-target re-pushes idempotently. Scratch dirs under /tmp only; CREW_HOME lives outside the scratch repos (as in real rooms) so lock files never trip the integrate preflight.
|
|
15
|
+
- `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. Stale-lease breaks serialize on a sidecar `$LOCK_FILE.flock` with an in-critical-section lease re-read (R-B1, 2026-09-21); refresh rewrites via temp-file + atomic rename so readers never see a torn file, and takes the same sidecar flock with an identity-only re-check inside the critical section — a reclaim always changes `task_id`, so identity alone closes the clobber (no expiry check on refresh: a long build that outran the lease legitimately revives its lock); release takes the same flock with the identity re-check inside (review pass 2, 2026-09-21) so a release can never `rm` a reclaimer's fresh lock.
|
|
16
|
+
- `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 (the integration target is the repo's current checkout — a branch name, or HEAD when detached — resolved by `integration_target()`; nothing in the lifecycle ever checks out a branch. `integrate` merges into the target, reconciles `origin/<target>` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — then pushes inline via `do_push` (no agent round-trip; 2026-09-19 REVIEW collapsed the old STEP-2 agent run, which added only a lock refresh). `do_push`: PUSHED (the refspec is always explicit — a branch pushes as `<branch>`, a detached HEAD as `HEAD:<destination>` where the destination is the remote's default branch from `resolve_push_destination()`; 2026-09-19 detached-HEAD audit REDO) / NO_REMOTE_PUSH (no origin — fail-soft, 2026-09-19 REVIEW: the old unconditional push exited 128 and mislabeled it); an unresolvable detached destination fails closed (never a silent skip, never a refusal of a knowable push); on push failure one fetch+merge retry while the lock is held, then fail closed without asserting a cause (never force-push). PUSH_SKIPPED survives only for the nothing-merged R5 path (no merge record + no lock held). Merged-but-unpushed retry recovery (blocker 35, 2026-09-21): identity-based, not ancestry-based — `current_branch_merge()` finds the newest merge on live HEAD's first-parent chain whose branch-side parent (^2) is the branch's CURRENT tip (the recorded merge, a do_push reconcile merge above it, or the manual R5 merge); that merge takes the lock and pushes instead of reporting MERGED_EMPTY. A record with no such merge means the branch moved since the merge — STALE_MERGE fails closed (never pushed, never MERGED_EMPTY). Branch gone (merge-lease reclaim): the recorded merge on the line is still the deliverable (no rework could have moved it). `push-target` proves the same identity before the ERROR-after-MERGED retry push. `CREW_STAGED_BASE` guard (ferried from get-provenance): the line must descend from the staged publish base — STAGED_BASE_MISMATCH fails closed before the merge (room #21 J3 shape). `verify-merge` always re-resolves the target from the current checkout and checks the recorded commit against the LIVE tip — never the record's word for the tip (2026-09-19 REVIEW: the old read-back compared the record to itself for target=HEAD). A detached HEAD skips the inline reconcile loudly (NO_REMOTE_RECONCILE) — the inline reconcile needs a static branch target, so on detached the destination is resolved explicitly in do_push, which reconciles lazily on push failure (2026-09-19 detached-HEAD audit REDO; the old "local by design" rationale is rejected); `push-target` is the thin R5 wrapper over `do_push`: refreshes the lock, distinguishes nothing-merged (no record + no lock → loud skip) from lock-lost (fail closed). (`.worktrees/<id>`, branch `task/<id>`). Branch resolution order: the crew registry (`<repo>/.worktrees/.registry/<id>`), then the canonical `task/<full-id>` ref, then a `task/<id-prefix>` ref matched by strict prefix enumeration (task 4e1a1bba — a Build agent may create the branch with raw git from an abbreviated id, bypassing prepare; prefix names are tolerated, never created; ambiguous prefixes fail closed). `resolve-branch` prints the resolved branch for agent-side one-liners. Nothing reconstructs the branch name — every command resolves it. Prepare fails closed on a dirty integration target; 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 the integration target — 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. post-deploy's `.worktrees/` guard is add-then-reset, never an all-negative pathspec: `git add -A -- ':!.worktrees/'` still exits 1 on git 2.43.0 against the crew's own gitignored non-empty `.worktrees/` (room #15 J3), killing post-deploy under `set -e` — a plain `add -A` never errors on ignored paths and the follow-up `git reset -q -- .worktrees/` keeps stray lock files out of the integration target even on pre-gitignore-entry repos.
|
|
17
|
+
- `test-detached-integrate.sh` — behavioral regression tests for the detached-HEAD Integrate fix (room #21 J3, 2026-09-19; extended 2026-09-19 REVIEW): fixture A stages a detached HEAD (reviewed SHA + crew-init commit) and proves integrate leaves HEAD detached, keeps the reviewed SHA and crew-init as ancestors, lands the task merge, never moves main, records `integration_target=HEAD`, verify-merge VERIFIEDs, and push-target reports NO_REMOTE_PUSH (fixture A has no origin); fixture B proves the branch path still merges, reconciles, and pushes inline to a local origin; fixture C proves verify-merge checks the record against the LIVE tip (a detached line reset past the merge does NOT verify; the branch-gone path verifies only when the recorded commit is an ancestor of the live tip); fixture D proves the no-remote branch path fails soft with NO_REMOTE_PUSH (exit 0); fixture E proves the CREW_STAGED_BASE guard (non-ancestor base → STAGED_BASE_MISMATCH before any merge; true base → STAGED_BASE_OK); fixture F proves merged-but-unpushed retry recovery (re-running integrate after a crashed push takes the lock and pushes instead of parking on MERGED_EMPTY); fixture F3 proves the blocker-35 stale-record case (branch moved backward after the recorded merge → STALE_MERGE fails closed: never pushed, never MERGED_EMPTY); fixture H proves the detached-HEAD audit REDO contract (2026-09-19) on a detached checkout with a local origin: push-destination fails closed when origin/HEAD is unset (genuinely unknowable destination), otherwise resolves the remote default branch; integrate pushes inline as `HEAD:main` (`PUSHED: origin/main (refspec HEAD:main)`), the remote ref advances to the merge commit, local main stays at the reviewed base, HEAD stays detached, verify-merge VERIFIEDs, and push-target re-pushes idempotently; fixture I proves the push-target identity contract on a branch with a local origin: I1 stale record → STALE_MERGE and the remote ref does not move, I2 branch restored to the merged tip → the legitimate retry still PUSHEDs, I3 the manual R5 shape (stale record plus a newer manual merge of the current tip) → PUSHEDs. Scratch dirs under /tmp only; CREW_HOME lives outside the scratch repos (as in real rooms) so lock files never trip the integrate preflight.
|
|
18
18
|
- `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
|
|
19
19
|
- `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)
|
|
20
20
|
- `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
|
|
@@ -30,7 +30,8 @@ Shipped library: ESM JavaScript CLIs and import-safe modules, shell scripts for
|
|
|
30
30
|
- `write-ooda-verdict.js` — deterministic writer for the OODA terminal verdict (2026-09-14, append-only ledger 2026-09-15): `node write-ooda-verdict.js --dir <phase-dir> --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE> --summary <text> --expected <text> --actual <text> --missing <json-array> [--reason <text>]` writes `<phase-dir>/verdict.json` (the latest verdict) and appends one JSON line to `<phase-dir>/verdicts.jsonl` — the append-only ledger: every attempt's verdict is preserved with a mechanical `seq`, never overwritten; corrupt or non-contiguous ledgers fail loudly. `--reason` is REQUIRED and must be non-empty for `FAIL` and `NOT_POSSIBLE` — a reason-less negative verdict fails with exit 2 before anything is written (2026-09-15). Exit 2 on bad input. See `docs/ooda-report.md`.
|
|
31
31
|
- `read-ooda-verdict.js` — deterministic cross-checker for the OODA terminal verdict (2026-09-15): `node read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>` reads `<dir>/verdict.json`, prints one JSON line to stdout, exits 0 with `{ok:true, verdict, reason, summary, expected, actual, attempt}` when the record agrees with the prose expectation and a FAIL carries a non-empty reason, or exits 2 with `{ok:false, code}` — `missing|corrupt|contradiction|no_reason`. No wall-clock reads, no randomness. The bugfix QA closeout runs it against the prose `VERDICT:` line before any rework routing: a failed cross-check records the phase as failed for retry, never routes to rework.
|
|
32
32
|
- `ux-doctrine.js` — UX-surface doctrine page resolution (2026-09-17): the canonical map from `environment_type` to the crew's shared UX bar (`artifact` → `docs/artifact-ux.md`, `terminal` → `docs/terminal-ux.md`; null/unknown → no page). Pure and deterministic: `doctrinePage(env)`, `doctrinePath(crewHome, env)`, `doctrinePageExists(crewHome, env)`; CLI `--page <env>` / `--path <crewHome> <env>`. Workflows mirror the map inline (one line — the workflow runtime's relative-import support is unverified) and tests pin the mirror against this file.
|
|
33
|
-
- `
|
|
33
|
+
- `qa-db.js` — fresh per-run QA database for the local artifact server (2026-09-21, blocker 32): `openQaDb(spaceDir)` opens a drizzle db over a fresh per-run SQLite database (temp dir, `node:sqlite`), migrated from the space's own `drizzle/` migrations in journal order — the same migration path a fresh production install takes — and never a copy of the shipped `app.db`, so QA writes can never contaminate production data (the B38 class). Driver: `drizzle-orm/sqlite-proxy` resolved from the space's own `node_modules` (the driver matches the artifact's drizzle version) over a `node:sqlite` proxy callback; zero extra crew-release dependencies. The proxy contract is verified against drizzle-orm 0.45.2's compiled runtime (positional value arrays for `all`/`values`, falsy `rows` on `get`-miss, per-item `{rows}` for batch). Throws with a clear reason when the space has no drizzle migrations or drizzle-orm is unresolvable — the caller degrades gracefully. Import-safe module (no shebang, no side effects, bare `node` exits 0).
|
|
34
|
+
- `serve-artifact.js` — local server for a built TS space for experiential QA (2026-09-14): serves `<space-dir>/client/dist` statically and dispatches POST `*/actions` to the compiled server actions with a locally-built Ctx. Prints `READY port=<n>` then serves until killed. SIGTERM/SIGINT close the listener and the QA db and exit (1.5s fallback). Read-only w.r.t. the space directory. Fidelity: the served client and action handlers are the artifact's own built code; the Ctx is locally built (`ctx.db` is a drizzle db over a fresh per-run database from `lib/qa-db.js` — never the shipped `app.db`; privileged handlers run from the space's own `server/dist/privileged.js` when present; blobs are stored in a per-run temp dir and served back at `/__blobs/<key>`); environment is inherited from the caller. It is not the hosted runtime — tasks that cannot be judged under it must report `NOT POSSIBLE: <reason>`.
|
|
34
35
|
- `publish-verification retired (0.14.6, 2026-09-20)` — the content-verdict/readback architecture is retired: `verify-publish.js`, `readback-disk.js`, `publish-content.js`, `classify-publish-absence.js`, `build-readback-request.js`, `check-intent-freshness.js`, and `retry-publish.js` are deleted. Eric's publication contract replaced it: exact per-attempt version acknowledgement is the sole positive completion criterion ("If we hear that the artifact acknowledges our version, that's it. We don't verify against content."). The version is derived deterministically (`compute-publish-diff.js --task-id/--attempt`), the tick worker issues the edit directly (`scan-publish-intent` → `record-intent-issuance`), and `scan-ack-pending` evaluates the version receipt with bounded retries. Provenance now certifies the issuance request was acknowledged, never byte equality.
|
|
35
36
|
- `update-watch.js` — deterministic automatic update watcher (2026-09-16, zero deps): `node update-watch.js --crew-home <path>` (missing arg → usage, exit 2; every other path exits 0). Watches the public npm registry (`npm view muse-crew version` pinned to `https://registry.npmjs.org/`) vs `crew-release.sh current` and files a `workflow: "upgrade"` task with `source: npm@<version>` when policy (`auto_update_crew`, `update_channel`) and channel gating allow; watches `git ls-remote origin HEAD` on the first `deploy_type=artifact` project vs `$CREW_HOME/.update-watch.json` and files a `workflow: "chore"` task carrying the mechanical dashboard-upgrade journey. Reads the `.crew-version` compatibility anchor at the new ref via `git fetch` + `git show <sha>:.crew-version` (never the working tree) and orders dashboard-led: a declared newer crew files the crew upgrade task FIRST and the dashboard task notes it follows the crew upgrade (declaration bypasses `update_channel`, not the `auto_update_crew=false` opt-out); a declared older crew skips the dashboard leg entirely as a human decision; a missing/invalid/unfetchable anchor fails open to the dashboard leg as today. Idempotency via the same state file (records at file time); check failures log to `$CREW_HOME/update-watch.log` and are never thrown. Safety: only files tasks — never deploys, never touches the artifact/config/scheduler. Run by the daily `crew-update-watch` cron through the `current` symlink (latest release); deliberately NOT in the lib-pinning `PIN_BASENAMES`.
|
|
36
37
|
- `gitignore.js` — deterministic .gitignore management for crew-owned paths (2026-09-17): the crew touches exactly one user-owned file outside `.orchestration/` — the repo's `.gitignore`. `ensureGitignoreEntries(repoPath, entries)` creates the file when missing, appends missing entries (exact line match, no duplicates), preserves existing content byte-for-byte, and is idempotent. `describeGitignoreChange(repoPath, entries)` renders the exact diff for the setup consent conversation. Crew-owned entries: `.worktrees/`, `.orchestration/user/`. CLI: `--repo <path> [--dry-run]`.
|