muse-crew 0.14.5 → 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/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:
@@ -1560,3 +1573,180 @@ trigger anchor), lib/publish-note-vocabulary.js (publish-requested,
1560
1573
  publish-intent-claimed, publish-refused), seed/cron-body-template.md
1561
1574
  (steps 4.4b, 4.4 retry direct-issuance), docs/publish-verification.md,
1562
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, and since 2026-09-20 (blocker 22) it doesn't even 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, pre-trigger manifest baseline — writes one issuer-stamped `publish-intent` ledger entry, and parks with `publish: publish-requested <commit> <attempt>`. The session-carrying tick worker (the only caller class that can reach `artifact_edit`) claims the intent, verifies the diff's sha256, issues the edit directly in its own turn, and records the issuer-stamped `submitted` entry plus `publish: verification-requested`. An unobserved outcome is an unknown outcome, parked fail-closed with an append-only `unknown` ledger entry — never stamped. The parent protocol owns the independent content confirmation: `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source and `lib/verify-publish.js` compares it mechanically against the base..commit diff (see `docs/publish-verification.md`); only a match stamps provenance. 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
 
@@ -1,120 +1,28 @@
1
- # Publish-unknown recovery (blocker 15, 2026-09-18; one-party 2026-09-20)
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
- (2026-09-20, blocker 22: the trigger child is retired. New publishes park
9
- at intent, not unknown — see the one-party section of
10
- docs/publish-verification.md. This document's unknown path is the legacy
11
- recovery for pre-one-party parks, plus the retry protocol, which now issues
12
- through the session-carrying tick worker directly — never a child.)
13
-
14
- ## The note is the state machine
15
-
16
- Recovery state lives in the task's `note` events, keyed on machine-written
17
- `publish: <transition>` markers. Deterministic code (`lib/crew-api.js`) owns
18
- every transition; the cron tick (Step 4.4) is only the ferry between the
19
- deterministic steps. The initial park note is the workflow's
20
- "Publish outcome unknown …" note (no `publish:` marker — the scan matches it
21
- explicitly); every transition after that is machine-written.
22
-
23
- State diagram (latest `publish:` note wins; history is the retry budget):
24
-
25
- ```
26
- "Publish outcome unknown" park
27
- (< 30m) ──waiting──> scan: waiting (leave alone)
28
- (>= 30m) ──scan──> publish: unknown-recovery-claimed <expiry> (1h lease)
29
- │
30
- ├─ tick runs lib/classify-publish-absence.js, then
31
- │ record-unknown-classification (CAS on the claim expiry):
32
- │
33
- ├─ verified ──────────> publish: verification-requested <commit>
34
- │ (content IS present; Step 4.5 verifies;
35
- │ ledger: unknown-resolved)
36
- ├─ provably-dropped ──> publish: dropped <commit>
37
- │ (queues the retry protocol, next tick)
38
- ├─ applied-not-built / ambiguous ──> publish: ambiguous <commit>
39
- │ (terminal; ledger: unknown-classified)
40
- ├─ superseded ────────> publish: superseded <commit> (terminal)
41
- └─ deferred ──────────> (no note; the claim expires; the next scan
42
- re-claims and re-classifies)
43
- ```
44
-
45
- Retry protocol (Step 4.4, for `publish: dropped` with a free budget):
46
-
47
- ```
48
- publish: dropped
49
- ── tick: HEAD == commit? no ──> publish: retry-superseded (terminal)
50
- ── tick: acquire merge lock publish-retry:<task> (600s); held ──> stop, retry next tick
51
- ── tick: re-read content in the lock (classifier)
52
- ├─ not provably-dropped ──> record-retry-recheck routes it
53
- │ (verified → verification-requested; ambiguous → terminal;
54
- │ superseded → terminal; deferred → no-op)
55
- └─ provably-dropped ──> publish: retry-intended <commit> <ts>
56
- ── tick issues the edit DIRECTLY in its own turn (never a child;
57
- blocker 22 — children cannot reach artifact_edit)
58
- ├─ explicit refusal ──> publish: retry-refused (terminal)
59
- └─ no refusal ──> publish: retry-issued <commit>
60
- ──> publish: verification-requested <commit> not-before=<ts+20m>
61
- (Step 4.5 skips not-before entries until the window passes)
62
- ──> one -retry1 ledger entry (best-effort)
63
- ── release the merge lock (every path)
64
- ```
65
-
66
- Crash recovery (fail closed, never re-trigger blind):
67
-
68
- - `publish: retry-intended` without `retry-issued` → the next scan mirrors
69
- `verification-requested` with `not-before=intended+20m`. The trigger may
70
- or may not have gone out; content verification is the arbiter.
71
- - `publish: retry-issued` without a mirrored request → the next scan writes
72
- the missing mirror.
73
- - Exactly one retry per task, enforced from note history: the scan emits
74
- `retry_due` only when no `publish: retry-issued` exists in the task's
75
- history — including a retry for an earlier unknown attempt on a reworked
76
- task. When the budget is spent, the drop is terminal: `publish: ambiguous`.
77
-
78
- ## Commands
79
-
80
- - `scan-publish-unknown` — the cron scan (Step 4.5 of the old numbering).
81
- Returns `{ waiting, due, retry_due, mirrored, skipped }`. `due` entries
82
- ferry the classifier inputs: `commit`, `trigger_ts` (from the `submitted`
83
- ledger entry — never a time window), `park_ts`, `slug`, `repo_path`,
84
- `base` (provenance `source_commit`, else the empty tree), and
85
- `claim_expiry` for the record CAS. `retry_due` entries ferry the retry
86
- inputs (`commit`, `attempt`, `slug`, `repo_path`, `base`, `ledger_path`).
87
- - `record-unknown-classification --json '{task_id, claim_expiry, decision}'`
88
- — routes the classifier's decision; CAS on the claim expiry (a stale tick
89
- records nothing).
90
- - `record-retry-recheck --json '{task_id, decision}'` — routes the retry
91
- protocol's in-lock content re-read; CAS on latest being `publish: dropped`.
92
- - `resolve-publish-unknown` — the manual one-shot for a single parked task
93
- (unchanged; the audit-window contract, not the classifier).
94
-
95
- ## Classifier (lib/classify-publish-absence.js)
96
-
97
- Decides, from the task's repo and the platform's on-disk state, whether the
98
- dropped edit is proven:
99
-
100
- - `verified` — the content IS present (the platform applied it; the receipt
101
- was the only thing lost). Never re-issue.
102
- - `provably-dropped` — the old source is live AND the manifest shows no
103
- build since the trigger. Only this decision may retry.
104
- - `applied-not-built` — the new source is live but no build ran (the
105
- trigger reached the platform but the build didn't). Ambiguous outcome,
106
- terminal: a retry would double-apply.
107
- - `ambiguous` — the content check is inconclusive. Never retry blind.
108
- - `deferred` — not yet quiesced; re-check next tick.
109
- - `superseded` — HEAD moved past the attempt's commit. Terminal.
110
-
111
- The classifier's verified path requires the CURRENT manifest to be a new
112
- build identity: `built_at` advanced past the trigger AND `content_sha256`
113
- differs from the pre-trigger baseline the workflow snapshots into the
114
- submitted ledger entry (design §1.9 — defeats a replayed manifest). When
115
- the baseline is unavailable the classifier falls back to the time-based
116
- advance check and notes it; the Step 4.5 verifier (`lib/verify-publish.js`)
117
- is the strict gate and fails closed without a baseline before stamping.
118
- Build-in-flight is checked twice bracketing the content read via manifest
119
- state change (no mtime heuristics), and the suite carries add-only and
120
- 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.