muse-crew 0.14.4 → 0.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -18,4 +18,4 @@ Muse Crew source repository. The repo is the product; the personal instance (`$C
18
18
  - Git source is authoritative.
19
19
  - Never expose this repo publicly.
20
20
  - Ship implementation and documentation together.
21
- - After editing `workflows/*.js`, run `bash tests/run.sh` — `node --check` does NOT catch syntax errors inside function bodies (V8 lazy preparsing), so it cannot validate workflow edits alone. Worse (2026-09-19): raw `node --check` on the unstripped file is a FALSE NEGATIVE — the file parses as a module (top-level `export`), which masks breakage the release gate's export-strip + async-wrap transform exposes (module/script goal confusion). An unterminated string passed raw `node --check` and was caught only by `publish-verdict-first.test.js`'s loader emulation. After any workflow edit, verify with the true gate: `{ echo "async function __crew_workflow__(args) {"; sed 's/^export //' workflows/<f>.js; echo "}"; } > /tmp/w.js && node --check /tmp/w.js` — or just run the suite.
21
+ - After editing `workflows/*.js`, run `bash tests/run.sh` — `node --check` does NOT catch syntax errors inside function bodies (V8 lazy preparsing), so it cannot validate workflow edits alone. Worse (2026-09-19): raw `node --check` on the unstripped file is a FALSE NEGATIVE — the file parses as a module (top-level `export`), which masks breakage the release gate's export-strip + async-wrap transform exposes (module/script goal confusion). An unterminated string passed raw `node --check` and was caught only by the suite's loader emulation (then `publish-verdict-first.test.js`, now the `_validate_workflows` behavioral drive in `tests/workflow-size.test.js`). After any workflow edit, verify with the true gate: `{ echo "async function __crew_workflow__(args) {"; sed 's/^export //' workflows/<f>.js; echo "}"; } > /tmp/w.js && node --check /tmp/w.js` — or just run the suite.
package/API.md CHANGED
@@ -94,15 +94,68 @@ Atomically park a task for human attention: sets `state` to `parked`, writes the
94
94
 
95
95
  Returns `{ "ok": true, "task": {...}, "settled_sessions": <n> }` with the updated task record and the count of sessions settled (`0` when none were running).
96
96
 
97
- ### `resolvepublishunknown`
97
+ ### `scan-publish-intent`
98
98
 
99
- Recovery contract for publish attempts parked with an `unknown` outcome (2026-09-14): the rebuild trigger's child failed structured closeout and the in-flight-only build-state poll could not see the completed build, so the workflow parked fail-closed — but the edit may still have gone through. When a platform audit build completed inside the publish window (between the Integrate-completion event and the unknown-outcome park), this action routes the task to the parent's independent content verification WITHOUT re-issuing the edit and WITHOUT stamping provenance. The original unknown ledger entry and park event are preserved; the resolution is appended to the publish ledger (`outcome: "unknown-resolved"`) and to the event log, and a `publish: verification-requested <commit>` note is written so `scan-verification-pending` claims the task on the next tick. The parent's read-back ([publish verification](docs/publish-verification.md)) remains the real verification and can still fail terminally. Cases that stay parked with `{ "resolved": false, ... }`: task not parked, latest ledger outcome not `unknown`, no usable commit on the unknown entry, no audit build inside the publish window, unobservable publish window (missing Integrate-completion or park event), or an already-resolved attempt (idempotent).
99
+ 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.
100
+
101
+ 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`.
102
+
103
+ ### `record-intent-issuance`
104
+
105
+ Records the tick worker's direct issuance outcome (0.14.6). Compare-and-swap on `claim_expiry` — a stale tick writes nothing.
106
+
107
+ | Field | Type | Required | Notes |
108
+ |-------|------|----------|-------|
109
+ | `task_id` | uuid | yes | Must have an unexpired intent claim |
110
+ | `claim_expiry` | ISO-8601 | yes | Must match the claim's expiry (CAS) |
111
+ | `edit_outcome` | `accepted`\|`refused` | yes | The direct `artifact_edit` outcome |
112
+ | `refusal_text` | string | no | Required when `refused` |
113
+
114
+ `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
+
116
+ ### `record-builder-report`
117
+
118
+ 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.
119
+
120
+ | Field | Type | Required | Notes |
121
+ |-------|------|----------|-------|
122
+ | `task_id` | uuid | yes | |
123
+ | `version` | `<commit>:<attempt>` | yes | Must name an issued attempt |
124
+ | `outcome` | `refused` | yes | (2026-09-20 REVIEW) `acknowledged` is rejected — the positive builder-report path was circular; disk is the sole positive evidence |
125
+ | `report_text` | string | yes | Must contain the exact version |
126
+
127
+ 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.
128
+
129
+ ### `record-version-ack`
130
+
131
+ Stamps a version acknowledgement (0.14.6). The version must name an issued attempt for the task/commit.
132
+
133
+ | Field | Type | Required | Notes |
134
+ |-------|------|----------|-------|
135
+ | `task_id` | uuid | yes | |
136
+ | `version` | `<commit>:<attempt>` | yes | Must match an issued attempt's derived version |
137
+ | `via` | string | yes | `disk` only (2026-09-20 REVIEW) |
138
+
139
+ 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).
140
+
141
+ ### `scan-ack-pending`
142
+
143
+ 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).
144
+
145
+ Returns `{ "acknowledged": [...], "refused": [...], "reissued": [...], "timeouts": [...], "skipped": [...] }`.
146
+
147
+ 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.
148
+
149
+ ### `record-publish-note`
150
+
151
+ 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.
100
152
 
101
153
  | Field | Type | Required | Notes |
102
154
  |-------|------|----------|-------|
103
- | `task_id` | uuid | yes | Must be parked with a latest publish-ledger outcome of `unknown` |
155
+ | `task_id` | uuid | yes | |
156
+ | `note` | string | yes | Must start with a registered `publish: <verb>` |
104
157
 
105
- Returns `{ "resolved": true, "task_id", "commit", "evidence_dir", "audit_report_ok" }` on success, or `{ "resolved": false, "reason" }` when the recovery preconditions are not met.
158
+ 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
159
 
107
160
  ---
108
161
 
@@ -483,9 +536,10 @@ project row carries its own publish base (`provenance_source_commit`,
483
536
  a crew home with N artifact projects carries N independent bases. There are
484
537
  two stamp call paths, and they are different operations on the same row:
485
538
 
486
- - **Full stamp** — called by the parent's publish verification (`lib/verify-publish.js`)
487
- after independently verifying the rebuilt artifact: `source_commit` is the
488
- verified build commit, `crew_release` the active release at publish time.
539
+ - **Full stamp** — called by the ack scan's `record-version-ack` (the 0.14.6
540
+ version machine) after the exact per-attempt version is observed on the
541
+ artifact's disk: `source_commit` is the acknowledged build commit,
542
+ `crew_release` the active release at publish time.
489
543
  - **Refresh** — called by the release installer (`lib/advance-publish-base.js`)
490
544
  after installing/activating a crew release: `crew_release` is refreshed to
491
545
  the live release identity while preserving the existing `source_commit`.
@@ -57,6 +57,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
57
57
  - `#d1-issuance-time` — D1 issuance-time field (`issued_at`; `entry_kind` cut 2026-09-20)
58
58
  - `#detached-head-audit` — Detached-HEAD audit (0.14.2)
59
59
  - `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
60
+ - `#version-acknowledgement` — Version acknowledgement replaces content verification (0.14.6)
61
+ - `#version-review-dispositions` — 2026-09-20 amended REVIEW dispositions (14 mechanical fixes)
62
+ - `#version-not-hash` — 2026-09-20: version number replaces the sha256 nonce
60
63
 
61
64
  ## qa-reproduce.md — Decision history: qa reproduce
62
65
 
@@ -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>
@@ -1361,7 +1374,7 @@ Consumers (one anchor, shared): `lib/verify-publish.js`,
1361
1374
  `lib/crew-api.js::findTriggerEntry`, the classifier
1362
1375
  (`lib/classify-publish-absence.js`), and the retry writer
1363
1376
  (`lib/retry-publish.js`) all take the oldest matching "submitted" by `ts`
1364
- and bind `issued_at || ts`. Platform nonce: deferred, not filed in this
1377
+ and bind `issued_at || ts`. Platform version: deferred, not filed in this
1365
1378
  change.
1366
1379
 
1367
1380
  Edge cases:
@@ -1507,3 +1520,233 @@ push-destination, do_push refspec), lib/publish-npm.sh
1507
1520
  (PUSH-TARGET-ANCHOR refspec), workflows/{standard,bugfix,chore}.js
1508
1521
  (STEP-2/R5-PUSH wording), lib/test-detached-integrate.sh (fixture H),
1509
1522
  lib/test-publish-preflight.sh (cases 6/7).
1523
+
1524
+ ## One-party worker-owned publish (0.14.5, room #24 → blockers 22/23)
1525
+
1526
+ **Problem.** Room #24's four journeys all died at Publish on one mechanical
1527
+ fact: workflow children cannot call `artifact_edit`. The tool requires a
1528
+ parent-conversation session ID that `agent()` children do not have — every
1529
+ trigger-child attempt failed with `missing session id`, and the runtime's
1530
+ JSON-candidate scan threw on the refusal before the workflow could parse it.
1531
+ A second, independent defect: the retry harness (`lib/retry-publish.js`)
1532
+ imported the unavailable `better-sqlite3` and pointed at `crew.db` instead
1533
+ of `crew-state.db`, so no retry could ever run.
1534
+
1535
+ **Decision.** One party owns issuance: the session-carrying tick worker.
1536
+ The workflow prepares (preflight, provenance base, checksummed diff via
1537
+ `lib/compute-publish-diff.js`, toolcheck, pre-trigger manifest baseline),
1538
+ records ONE issuer-stamped ledger entry (`outcome: "publish-intent"`,
1539
+ `issuer: "workflow"`, carrying `diff_path`/`diff_sha256`), and parks with
1540
+ `publish: publish-requested <commit> <attempt>`. The tick claims the intent
1541
+ (`publish: publish-intent-claimed <expiry>`, 1-hour lease, via
1542
+ `scan-publish-unknown`'s `intent` bucket), verifies the staged diff's sha256
1543
+ (regenerating deterministically from `base..commit` when the file is
1544
+ missing), runs the manifest-freshness check on re-claimed intents (the dead
1545
+ tick may have issued and died before recording — never blindly re-issue),
1546
+ calls `artifact_edit` directly in its own turn, and records the outcome
1547
+ through `record-intent-issuance` (CAS on the claim): `accepted`/`recovered`
1548
+ → issuer-stamped `submitted` (`issuer: "tick-worker"`) + mirrored
1549
+ `publish: verification-requested`; `refused` → `rejected` + terminal
1550
+ `publish: publish-refused`.
1551
+
1552
+ **Retired, not reverted.** The trigger child, build-state observation
1553
+ machinery, refusal parser, phantom workflow-owned `submitted`, and the
1554
+ receipt-less unknown machinery's issuance side are deleted. The
1555
+ unknown-recovery classifier survives for legacy parks; its trigger anchor
1556
+ is now issuer-bound (only `issuer: "tick-worker"` binds). The retry
1557
+ protocol survives ported to `node:sqlite` + `crew-state.db` with the
1558
+ missing `CREW_REPO` for the merge lock supplied, and its issuance step is
1559
+ the tick worker's direct call. Verified re-entry: a `submitted` without a
1560
+ mirrored request mirrors `verification-requested` — never re-issues.
1561
+
1562
+ **What it is not.** Not a ledger-as-request queue (no new queue
1563
+ abstraction — the intent IS the ledger entry plus the park note). Not a
1564
+ revert of room #22's trigger-timing work (that machinery was deleted, and
1565
+ room #24 disproved its premise). The new path is proven in parts; the next
1566
+ clean room proves it end-to-end.
1567
+
1568
+ Applies to: workflows/{standard,bugfix,chore}.js (Publish exits at intent),
1569
+ lib/crew-api.js (scan-publish-unknown intent bucket, record-intent-issuance,
1570
+ issuer-bound findTriggerEntry), lib/retry-publish.js (node:sqlite port,
1571
+ CREW_REPO, direct tick issuance), lib/verify-publish.js (issuer-bound
1572
+ trigger anchor), lib/publish-note-vocabulary.js (publish-requested,
1573
+ publish-intent-claimed, publish-refused), seed/cron-body-template.md
1574
+ (steps 4.4b, 4.4 retry direct-issuance), docs/publish-verification.md,
1575
+ docs/publish-unknown-recovery.md.
1576
+
1577
+ <a id="version-acknowledgement"></a>
1578
+ ## Version acknowledgement replaces content verification (0.14.6, 2026-09-20)
1579
+
1580
+ **Eric's contract:** "If we hear that the artifact acknowledges our version,
1581
+ that's it. We don't verify against content."
1582
+
1583
+ The content-verdict machine (read-back, `verify-publish.js`, the unknown-
1584
+ recovery classifier, the retry harness) is deleted. Exact per-attempt version
1585
+ acknowledgement is the sole positive completion criterion.
1586
+
1587
+ **The version:** `sha256("publish-version:v1:" + task_id + ":" + commit + ":" +
1588
+ publish_attempt)` — deterministic, 64 hex. Synthesized into the staged diff
1589
+ as `.crew-publish-version` (checksum covers it; never committed). The workflow
1590
+ replay key (`attempt`, a string) is separate from the numeric
1591
+ `publish_attempt`.
1592
+
1593
+ **The loop:** workflow stages intent (attempt 1) → tick worker claims via
1594
+ `scan-publish-intent` → issues directly → `record-intent-issuance` writes
1595
+ `submitted` + `publish: edit-issued` → `scan-ack-pending` watches for the
1596
+ exact version on disk or in a builder report. Any issued version for the
1597
+ task/commit counts (late earlier attempts included). Refusal is terminal.
1598
+ 45-min window per attempt, 3 attempts max, 15/45-min backoff. Third expiry
1599
+ → `publish: version-timeout`.
1600
+
1601
+ **Provenance certifies request acknowledgement, not byte equality.**
1602
+
1603
+ Applies to: lib/compute-publish-diff.js (--task-id/--attempt, version hunk),
1604
+ lib/crew-api.js (scan-publish-intent, record-intent-issuance,
1605
+ record-builder-report, record-version-ack, scan-ack-pending,
1606
+ record-publish-note), lib/publish-note-vocabulary.js (WRITABLE split:
1607
+ legacy terminals recognized, not writable), workflows/{standard,bugfix,
1608
+ chore}.js (Publish exits at intent with `attempt=1`), seed/cron-body-
1609
+ template.md (tick protocol), docs/publish-verification.md,
1610
+ docs/publish-unknown-recovery.md (retired).
1611
+
1612
+ ## 2026-09-20 (post-REVIEW amendment) — bounds re-set, observation split from mutation
1613
+
1614
+ Before PUBLISH, Eric challenged the bounds as unmeasured placeholders and
1615
+ approved a replacement:
1616
+
1617
+ - **Observation is cheap and read-only; mutation is not.** The old bounds
1618
+ coupled them: the 45-minute window + 15/45-minute backoffs + 3-attempt cap
1619
+ were all guesses, and the scan ran inside the 15-minute poll tick, so a
1620
+ verdict could never land sooner than the tick cadence.
1621
+ - **New shape:** the ack scan runs on its own 5-minute `crew-ack-scan` cron
1622
+ (observation-only — it never issues edits); the 15-minute poll tick keeps
1623
+ the claim-and-issue path. The scan cadence no longer constrains verdict
1624
+ latency: with 6 scans per window, the verdict lands within one cadence of
1625
+ the ack or the expiry.
1626
+ - **New bounds:** 30-minute acknowledgement window per attempt (anchored at
1627
+ the LATEST issuance — evidence-set: room #25's one healthy ack landed
1628
+ ~15-17 minutes after issuance, so the window is ~2x that plus margin),
1629
+ immediate reissue at expiry (`not_before: null` — no backoff), and a
1630
+ 2-hour total budget from the FIRST issuance, after which the scan parks
1631
+ terminal `publish: version-timeout`. The fixed 3-attempt cap is gone — a
1632
+ slow-but-healthy artifact no longer false-timeouts, and a dead one still
1633
+ terminates on the budget. The ledger records issued_at and first-seen ack
1634
+ per attempt, so the window is tunable from measured data.
1635
+
1636
+ <a id="version-review-dispositions"></a>
1637
+ ## 2026-09-20 (amended REVIEW dispositions) — 14 mechanical fixes
1638
+
1639
+ A five-critic read-only review (Architect, Subtractor, Reliability, Room
1640
+ Operator + step-back) of the amended 0.14.6 tree returned
1641
+ ACCEPT-WITH-CONCERNS, no REJECTs. All 14 dispositions applied before the
1642
+ amended BUILD commit:
1643
+
1644
+ 1. **Torn reissue staging is note-keyed.** The ack scan's reissue
1645
+ idempotency guard is keyed on the `publish: publish-requested <commit>
1646
+ attempt=N` note — the same anchor `scan-publish-intent` claims on —
1647
+ not the ledger line. A crash between the ledger append and the note
1648
+ write (or a rolled-back COMMIT) leaves an orphan line; the note-keyed
1649
+ guard self-heals by re-appending the content-identical line. Write
1650
+ order stays line-then-note: note-first would park permanently on a
1651
+ tear.
1652
+ 2. **`publish: version-timeout` vocabulary** now reads the 2-hour
1653
+ acknowledgement-budget meaning, not "three issuance attempts
1654
+ exhausted".
1655
+ 3. **Dead backoff guard deleted** from `scan-publish-intent` (the amended
1656
+ design never writes `not_before`).
1657
+ 4. **`not_before` plumbing removed**: no longer on claimed intents,
1658
+ reissue results, or ledger lines — the field is gone entirely, not
1659
+ even null-inline (nothing reads it; a dead field is the opposite of
1660
+ the disposition).
1661
+ 5. **Version-mismatch is per-entry**: a corrupt submitted entry is
1662
+ quarantined; valid derivation-verified entries still drive the windows
1663
+ and the budget. Only when NO entry verifies does the record park as
1664
+ `version-mismatch`.
1665
+ 6. **Unparseable latest issuance is an unknown window**: never reported
1666
+ as "window-open"; the budget check still runs (a corrupt latest
1667
+ timestamp cannot defer the terminal timeout), but reissue is withheld
1668
+ until the budget decides.
1669
+ 7. **Ack-scan failure is mechanical**: nonzero exit → report stderr
1670
+ verbatim, take no other action, no rerun, no other Crew API command,
1671
+ stop.
1672
+ 8. **Tests extended** in the existing `tests/scan-publish-intent.test.js`
1673
+ (no new file): reissue idempotency across repeated scans, exact `>=`
1674
+ budget boundary (inside reissues, at-boundary times out), unparseable
1675
+ first issuance (`first-issuance-unparseable` park), unparseable latest
1676
+ issuance (reissue withheld), duplicate submitted entries (deduped),
1677
+ corrupt version beside a valid entry (quarantined), all-invalid
1678
+ versions (`version-mismatch` park), on-disk version naming no issued
1679
+ attempt (ignored), per-task receipts never cross-attributed, torn
1680
+ reissue prepare self-heals via the note-keyed guard.
1681
+ 9. **Receipt scoped per task** (verified possible): per-project
1682
+ simultaneity defaults to 2 and parked tasks don't count as in-flight,
1683
+ so two same-project tasks CAN sit in `edit-issued` concurrently — a
1684
+ shared per-slug `.crew-publish-version` would let each issuance
1685
+ overwrite the other's acknowledgement (receipt ping-pong). The
1686
+ receipt is now `.crew-publish-version-<task_id>` in the staged diff
1687
+ hunk and on disk.
1688
+ 10. **Cron id overrides validated at init**: an override must start with
1689
+ `<manifest-id>-`, so uninstall's prefix-based discovery fallback can
1690
+ always find the job even if `.cron-registry.json` is lost.
1691
+ 11. **`crew-burn-room.sh`** (outside the repo): `--crons-removed` caller
1692
+ attestation is REQUIRED — the script refuses to burn without it
1693
+ (fail-closed; it cannot reach the scheduler, so the contract is
1694
+ honest: the caller removes poll/ack-scan/update-watch/journey-watch
1695
+ via cron.remove first and attests the ids). `roomNN-journey-watch`
1696
+ added to the convention list. `--ts-spaces <slugs>` moves the room's
1697
+ artifact records into the trash (already-gone is fine).
1698
+ 12. **Stuck-publish runbook** in `docs/guide.md`: the ack scan stages
1699
+ reissues, the poll tick issues them — if the poll tick is down,
1700
+ reissues stay staged and the budget eventually parks
1701
+ `publish: version-timeout`.
1702
+ 13. **Receipt scope documented**: the version proves adoption into the
1703
+ artifact's on-disk source tree — not a successful rebuild or
1704
+ redeployment.
1705
+ 14. **Result taxonomy fixed**: `timeouts[]` is terminal-budget exhaustion
1706
+ only; window expiries are `reissued[]`.
1707
+
1708
+ **Follow-up fix found in final pre-commit inspection (same day):** the
1709
+ version-not-hash cut left the old 64-hex nonce regex in the Publish
1710
+ phase of `workflows/standard.js`, `bugfix.js`, and `chore.js` — the
1711
+ format gate would have rejected every `<commit>:<attempt>` version and
1712
+ parked every publish. All three now mirror `isPublishVersion()` from
1713
+ `lib/crew-api.js`, and `tests/compute-publish-diff.test.js` carries a
1714
+ mechanical guard asserting the workflow regex matches the library's
1715
+ (and the stale 64-hex shape is gone). The full suite passed before this
1716
+ was found because no test executes the workflow's Publish gate — the
1717
+ guard closes that hole.
1718
+
1719
+ Step-back conclusions recorded: the five-minute observer costs ~192
1720
+ marginal task-mode turns/day/room (accepted for current VERIFY
1721
+ turnaround); the machine is a compensating control for the platform's
1722
+ missing real completion receipt and should be deleted the day the
1723
+ platform issues one (see `docs/publish-verification.md`
1724
+ § Decommissioning). Non-blocking backlog: at final timeout, a
1725
+ machine-owned audit-directory check could split the park note into
1726
+ "landed-but-receiptless" vs "platform-silent".
1727
+
1728
+ <a id="version-not-hash"></a>
1729
+ ## 2026-09-20 — Version number replaces the sha256 nonce
1730
+
1731
+ Eric: a nonce is a hash plus a version number — why not just the version
1732
+ number? He was right. The sha256 defended against a forger that doesn't
1733
+ exist: the builder's failure mode is omission (zero-tool runs), never
1734
+ forging our receipts. The artifact's real failure mode — an unfaithful
1735
+ carrier that mangles bytes or ignores the request — defeats a hash and a
1736
+ plain counter identically. We own the code, so we count our own attempts;
1737
+ we don't own the artifact, so verification stays observation of its state,
1738
+ never its attestation. The hash pretended to buy attestation.
1739
+
1740
+ The per-attempt version is now `<commit>:<attempt>` (e.g.
1741
+ `4b825dc6…:2`) — no hash, no task_id (the receipt path
1742
+ `.crew-publish-version-<task_id>` and the per-task ledger scoping already
1743
+ carry the task). The commit binds the version to the exact code, so a stale
1744
+ receipt from an earlier commit can never equal a fresh version — the binding
1745
+ the hash used to provide, kept without the cryptography. Derivation is a
1746
+ plain concatenation, byte-identical between `compute-publish-diff.js` and
1747
+ `derivePublishVersion()` in `crew-api.js`; well-formedness is
1748
+ `/^[0-9a-f]{40}:[1-9][0-9]*$/` (`isPublishVersion`). The retry machine,
1749
+ windows, budget, and terminal vocabulary are unchanged — only the token got
1750
+ honest. Side benefit, and Eric's original motive: the version is
1751
+ human-readable observability ("roughly what's deployed") where the hash was
1752
+ opaque.
package/docs/guide.md CHANGED
@@ -17,7 +17,7 @@ This is the full setup and operations reference. If you're new, start with the [
17
17
  - A **release** — the first immutable snapshot of the crew's runtime code.
18
18
  - An **`.orchestration/` directory** in the crew home with identities, personas, workflow docs, and feedback conventions.
19
19
  - A **project registration** — the task service registered as its own first project.
20
- - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 15 minutes via Muse's scheduling, even when nobody's in the conversation). The scheduler identity is dashboard-independent — chosen once at first init (no owner; the platform rejects `cli:` owners) — so deleting the dashboard artifact does not stop the crew. Removing a crew entirely is `crew-uninstall`'s job.
20
+ - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 15 minutes via Muse's scheduling, even when nobody's in the conversation), a 5-minute acknowledgement scan (observation-only — runs `scan-ack-pending` so publish verdicts land quickly; it never issues edits), and a daily update watcher. The scheduler identity is dashboard-independent — chosen once at first init (no owner; the platform rejects `cli:` owners) — so deleting the dashboard artifact does not stop the crew. Removing a crew entirely is `crew-uninstall`'s job.
21
21
 
22
22
  The agent running in the main chat receives the dispatcher's claims and launches each task workflow. Workflows can't launch workflows, so this handoff is structural.
23
23
 
@@ -126,7 +126,7 @@ The run has four phases:
126
126
 
127
127
  1. **Gates** — the crew home must be inside the workspace (uninstall refuses to delete anything outside it), and `confirm: true` must be present. Either missing blocks the run before anything is touched.
128
128
  2. **Safety** — if any task is `in_progress`, the run blocks unless `force: true`. Uninstalling under live workflows would strand them.
129
- 3. **Crons** — removes the crew's scheduler jobs, and only those: the exact ids from `$CREW_HOME/.cron-registry.json` (written by init; covers id overrides), plus discovery of `crew-poll-*` jobs whose body contains the crew home path (covers installs from before the registry existed). Anything not on that union is never touched — shared crons are safe by construction, and a missing home still gets its orphaned crons removed.
129
+ 3. **Crons** — removes the crew's scheduler jobs, and only those: the exact ids from `$CREW_HOME/.cron-registry.json` (written by init; covers id overrides), plus discovery of `crew-poll-*` / `crew-ack-scan-*` / `crew-update-watch-*` jobs whose body contains the crew home path (covers installs from before the registry existed). Anything not on that union is never touched — shared crons are safe by construction, and a missing home still gets its orphaned crons removed.
130
130
  4. **Home** — best-effort `git worktree prune` on the crew's project repos, then deletes the crew home directory and verifies it is gone.
131
131
 
132
132
  Dashboard artifacts are left untouched — uninstall removes the crew instance (its crons and its home), never the user's artifacts.
@@ -230,7 +230,10 @@ A task in `todo` state becomes eligible for dispatch on the next polling tick. A
230
230
 
231
231
  ## How the loop works
232
232
 
233
- Every 15 minutes, the `crew-poll` cron fires:
233
+ Every 15 minutes, the `crew-poll` cron fires. (Publish verdicts don't wait
234
+ for it: a separate 5-minute `crew-ack-scan` cron runs the observation-only
235
+ `scan-ack-pending` pass so acknowledgements, refusals, and timeouts land
236
+ within one scan cadence — issuance stays in the poll tick below.)
234
237
 
235
238
  1. **Cron runs the dispatcher** — `crew-dispatch.js` with the task service slug and `crewHome` as arguments.
236
239
 
@@ -364,9 +367,42 @@ Bugfix tasks reproduce at the layer where the bug lives — not always in the br
364
367
 
365
368
  The workflow dispatches the Reproduce strategy mechanically on the classification. A garbled or missing marker degrades to `artifact` (the old behavior) — a task is never parked on it. The experiential loop is forbidden for engine/docs tasks: if the reproducer drives the browser anyway, the closeout fails the phase loudly for retry, so wrong-layer verdicts never reach Map.
366
369
 
367
- ### Publish content verification
370
+ ### Publish acknowledgement
368
371
 
369
- The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself: after the build lands it parks with `publish: verification-requested <commit> (build <agent_id|agent_id unobserved>)`. "Landed" requires positive evidence (canary 2026-09-15, task `1d692d91`): the build poll must have positively observed our build — a running build with the receipt `agent_id`, or a completed-build record matching it. Absence of a running build is not evidence our build ran; an unobserved "done" is an unknown outcome, parked fail-closed with an append-only `unknown` ledger entry — never parked as verification-requested. The parent protocol owns the independent content confirmation (parent-driven — see `docs/publish-verification.md`); the park message records the observed builder build identifier as `(build <agent_id|agent_id unobserved>)`, and the parent correlates the read-back's live build agent_id against it — a mismatch logs `publish: build-mismatch <commit> …`, stays parked, and is never stamped (parent-driven — see `docs/publish-verification.md` step 4b). The independent read-back step is currently unavailable: `artifact_inspect` was removed by the platform (2026-09-14) and no agent-callable replacement exists (`artifact.inspect` is malfunction diagnosis, not a read-back tool), so the parent cannot confirm content independently and tasks stay parked at verification-requested until a read-back path exists. QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
372
+ Eric's publication contract (2026-09-20): "If we hear that the artifact acknowledges our version, that's it. We don't verify against content."
373
+
374
+ The workflow never stamps provenance itself, and it doesn't issue the edit: workflow children cannot call `artifact_edit` (the tool requires a parent-conversation session id they don't have), so publish is one-party. The workflow prepares — preflight, provenance base, checksummed diff with the deterministic per-attempt version — writes one issuer-stamped `publish-intent` ledger entry, and parks with `publish: publish-requested <commit> attempt=N`. The session-carrying tick worker (the only caller class that can reach `artifact_edit`) claims the intent via `scan-publish-intent`, re-derives the version, verifies the diff's sha256, issues the edit directly in its own turn, and records the issuer-stamped `submitted` entry plus `publish: edit-issued` via `record-intent-issuance`.
375
+
376
+ The ack scan (`scan-ack-pending`) owns the verdict from there: the exact per-attempt version on the artifact's disk is the sole positive completion criterion (2026-09-20 REVIEW removed the circular builder-report positive path; builder reports carry explicit refusals only). Bounds: 30-minute window per attempt, 2-hour total budget from first issuance, no backoff (reissue immediately claimable), fresh version per attempt. Explicit refusal parks terminally (`publish: publish-refused`); budget exhaustion parks `publish: version-timeout`. Silence is unknown and fails closed. See `docs/publish-verification.md` for the full protocol. QA's provenance check then enforces the stamp mechanically, so an unacknowledged publish fails loudly in QA instead of passing silently.
377
+
378
+ ### When publish is stuck
379
+
380
+ Two different workers move a publish forward, and they own different halves:
381
+
382
+ - The **5-minute `crew-ack-scan`** is observation-only: it watches the
383
+ on-disk version receipt and, when a 30-minute window expires, stages the
384
+ next attempt's intent (fresh version, immediately claimable). It never
385
+ issues an edit.
386
+ - The **15-minute poll tick** is the only half that issues: its
387
+ `scan-publish-intent` claims staged intents and its session-carrying
388
+ worker calls `artifact_edit`.
389
+
390
+ If the poll tick is down, reissues stay staged and unclaimed — the task
391
+ sits at `publish: edit-issued` (or `publish: publish-requested` for a
392
+ staged reissue) with no new issuance, and eventually the 2-hour total
393
+ budget expires and the ack scan writes the terminal
394
+ `publish: version-timeout`. The timeout is honest: the budget is measured
395
+ from the first issuance, not from the last successful observation, so a
396
+ dead issuer cannot stretch it.
397
+
398
+ **Runbook:** check the poll tick first — is `crew-poll-*` enabled and
399
+ producing recent tick reports? If yes, the stuck task is a publish-path
400
+ problem (diagnose via `docs/publish-verification.md`'s Operator recovery).
401
+ If no, the ack scan's staged reissues are waiting on an issuer that isn't
402
+ running — fix the poll tick, not the publish machine. In either case, do
403
+ not hand-issue the edit: issuance belongs to the tick worker's
404
+ claim-and-issue path, and an out-of-band edit has no ledger anchor for
405
+ the ack scan to verify against.
370
406
 
371
407
  ## Identities
372
408
 
@@ -380,7 +416,7 @@ Each phase has an assigned identity — a character with a defined personality:
380
416
  | Build | **Wren** | Quietest one, trusts the plan |
381
417
  | Review | **Cass** | Fair but exacting — holds the spec as the contract |
382
418
  | Integrate | **Wren** | Merges the work into the integration target and pushes it (succeeds vacuously when the task branch is empty — runtime-state deliverable) |
383
- | Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow verifies the side effect mechanically — npm via registry version; artifact via an independent content read-back before the parent stamps provenance (see `docs/publish-verification.md`) — and fails closed if the worker's report and system state disagree |
419
+ | Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow prepares the publish but never issues it — artifact issuance is tick-worker-owned (one-party, blocker 22); npm via registry version; artifact via the tick worker's direct edit plus an independent content read-back before the parent stamps provenance (see `docs/publish-verification.md`) — and fails closed on any unverified outcome |
384
420
  | QA | **Hazel** | Code-blind, persistent, wears persona costumes |
385
421
  | Reproduce | **Hazel** | Reproduces bugs before fixing |
386
422
  | Write | **Tate** | Docs writer, observational voice |
@@ -1,113 +1,28 @@
1
- # Publish-unknown recovery (blocker 15, 2026-09-18)
2
-
3
- When a standard/bugfix Publish parks with "Publish outcome unknown", the
4
- artifact-edit trigger went out fire-and-forget and no receipt came back —
5
- async was planned for, receipt-less was not. The unknown-recovery loop
6
- closes that gap without re-issuing blindly.
7
-
8
- ## The note is the state machine
9
-
10
- Recovery state lives in the task's `note` events, keyed on machine-written
11
- `publish: <transition>` markers. Deterministic code (`lib/crew-api.js`) owns
12
- every transition; the cron tick (Step 4.4) is only the ferry between the
13
- deterministic steps. The initial park note is the workflow's
14
- "Publish outcome unknown …" note (no `publish:` marker — the scan matches it
15
- explicitly); every transition after that is machine-written.
16
-
17
- State diagram (latest `publish:` note wins; history is the retry budget):
18
-
19
- ```
20
- "Publish outcome unknown" park
21
- (< 30m) ──waiting──> scan: waiting (leave alone)
22
- (>= 30m) ──scan──> publish: unknown-recovery-claimed <expiry> (1h lease)
23
- │
24
- ├─ tick runs lib/classify-publish-absence.js, then
25
- │ record-unknown-classification (CAS on the claim expiry):
26
- │
27
- ├─ verified ──────────> publish: verification-requested <commit>
28
- │ (content IS present; Step 4.5 verifies;
29
- │ ledger: unknown-resolved)
30
- ├─ provably-dropped ──> publish: dropped <commit>
31
- │ (queues the retry protocol, next tick)
32
- ├─ applied-not-built / ambiguous ──> publish: ambiguous <commit>
33
- │ (terminal; ledger: unknown-classified)
34
- ├─ superseded ────────> publish: superseded <commit> (terminal)
35
- └─ deferred ──────────> (no note; the claim expires; the next scan
36
- re-claims and re-classifies)
37
- ```
38
-
39
- Retry protocol (Step 4.4, for `publish: dropped` with a free budget):
40
-
41
- ```
42
- publish: dropped
43
- ── tick: HEAD == commit? no ──> publish: retry-superseded (terminal)
44
- ── tick: acquire merge lock publish-retry:<task> (600s); held ──> stop, retry next tick
45
- ── tick: re-read content in the lock (classifier)
46
- ├─ not provably-dropped ──> record-retry-recheck routes it
47
- │ (verified → verification-requested; ambiguous → terminal;
48
- │ superseded → terminal; deferred → no-op)
49
- └─ provably-dropped ──> publish: retry-intended <commit> <ts>
50
- ── trigger (same child shape as the first attempt)
51
- ├─ ARTIFACT_EDIT_REFUSED ──> publish: retry-refused (terminal)
52
- └─ no refusal ──> publish: retry-issued <commit>
53
- ──> publish: verification-requested <commit> not-before=<ts+20m>
54
- (Step 4.5 skips not-before entries until the window passes)
55
- ──> one -retry1 ledger entry (best-effort)
56
- ── release the merge lock (every path)
57
- ```
58
-
59
- Crash recovery (fail closed, never re-trigger blind):
60
-
61
- - `publish: retry-intended` without `retry-issued` → the next scan mirrors
62
- `verification-requested` with `not-before=intended+20m`. The trigger may
63
- or may not have gone out; content verification is the arbiter.
64
- - `publish: retry-issued` without a mirrored request → the next scan writes
65
- the missing mirror.
66
- - Exactly one retry per task, enforced from note history: the scan emits
67
- `retry_due` only when no `publish: retry-issued` exists in the task's
68
- history — including a retry for an earlier unknown attempt on a reworked
69
- task. When the budget is spent, the drop is terminal: `publish: ambiguous`.
70
-
71
- ## Commands
72
-
73
- - `scan-publish-unknown` — the cron scan (Step 4.5 of the old numbering).
74
- Returns `{ waiting, due, retry_due, mirrored, skipped }`. `due` entries
75
- ferry the classifier inputs: `commit`, `trigger_ts` (from the `submitted`
76
- ledger entry — never a time window), `park_ts`, `slug`, `repo_path`,
77
- `base` (provenance `source_commit`, else the empty tree), and
78
- `claim_expiry` for the record CAS. `retry_due` entries ferry the retry
79
- inputs (`commit`, `attempt`, `slug`, `repo_path`, `base`, `ledger_path`).
80
- - `record-unknown-classification --json '{task_id, claim_expiry, decision}'`
81
- — routes the classifier's decision; CAS on the claim expiry (a stale tick
82
- records nothing).
83
- - `record-retry-recheck --json '{task_id, decision}'` — routes the retry
84
- protocol's in-lock content re-read; CAS on latest being `publish: dropped`.
85
- - `resolve-publish-unknown` — the manual one-shot for a single parked task
86
- (unchanged; the audit-window contract, not the classifier).
87
-
88
- ## Classifier (lib/classify-publish-absence.js)
89
-
90
- Decides, from the task's repo and the platform's on-disk state, whether the
91
- dropped edit is proven:
92
-
93
- - `verified` — the content IS present (the platform applied it; the receipt
94
- was the only thing lost). Never re-issue.
95
- - `provably-dropped` — the old source is live AND the manifest shows no
96
- build since the trigger. Only this decision may retry.
97
- - `applied-not-built` — the new source is live but no build ran (the
98
- trigger reached the platform but the build didn't). Ambiguous outcome,
99
- terminal: a retry would double-apply.
100
- - `ambiguous` — the content check is inconclusive. Never retry blind.
101
- - `deferred` — not yet quiesced; re-check next tick.
102
- - `superseded` — HEAD moved past the attempt's commit. Terminal.
103
-
104
- The classifier's verified path requires the CURRENT manifest to be a new
105
- build identity: `built_at` advanced past the trigger AND `content_sha256`
106
- differs from the pre-trigger baseline the workflow snapshots into the
107
- submitted ledger entry (design §1.9 — defeats a replayed manifest). When
108
- the baseline is unavailable the classifier falls back to the time-based
109
- advance check and notes it; the Step 4.5 verifier (`lib/verify-publish.js`)
110
- is the strict gate and fails closed without a baseline before stamping.
111
- Build-in-flight is checked twice bracketing the content read via manifest
112
- state change (no mtime heuristics), and the suite carries add-only and
113
- removal-only regression fixtures.
1
+ # Publish-unknown recovery — RETIRED (0.14.6)
2
+
3
+ The unknown-recovery machinery (the `publish: unknown-*` note state machine,
4
+ `lib/classify-publish-absence.js`, `lib/retry-publish.js`,
5
+ `lib/resolve-publish-unknown` flow) is RETIRED as of 0.14.6 (2026-09-20).
6
+
7
+ It is subsumed by the version attempt loop: a 30-minute acknowledgement
8
+ window per attempt, fresh per-attempt versions with immediate reissue (no
9
+ backoff), a 2-hour total budget from first issuance, and `scan-ack-pending`
10
+ as the sole verdict. There is no separate unknown state — silence within
11
+ the window is unknown and fails closed; expiry re-issues with a fresh version;
12
+ budget exhaustion parks `publish: version-timeout`.
13
+
14
+ See `docs/publish-verification.md` for the current protocol.
15
+
16
+ ## Historical note
17
+
18
+ The unknown path existed because the old two-party publish fired the
19
+ artifact edit through a trigger child and got no receipt back — async was
20
+ planned for, receipt-less was not. The one-party publish (2026-09-20,
21
+ blocker 22) retired the trigger child: the workflow parks at intent, the
22
+ session-carrying tick worker issues directly, and the ack scan attributes
23
+ by exact version. The `publish: ambiguous`, `publish: retry-superseded`,
24
+ `publish: retry-refused`, `publish: verified`, `publish: superseded`,
25
+ `publish: verification-failed`, and `publish: intent-unverifiable` verbs
26
+ remain in the vocabulary's legacy registry — the exact list in
27
+ `lib/publish-note-vocabulary.js` (no wildcard families). Recognized so old
28
+ note history skips as terminal, never written anew.