muse-crew 0.14.4 → 0.14.5

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.
@@ -1507,3 +1507,56 @@ push-destination, do_push refspec), lib/publish-npm.sh
1507
1507
  (PUSH-TARGET-ANCHOR refspec), workflows/{standard,bugfix,chore}.js
1508
1508
  (STEP-2/R5-PUSH wording), lib/test-detached-integrate.sh (fixture H),
1509
1509
  lib/test-publish-preflight.sh (cases 6/7).
1510
+
1511
+ ## One-party worker-owned publish (0.14.5, room #24 → blockers 22/23)
1512
+
1513
+ **Problem.** Room #24's four journeys all died at Publish on one mechanical
1514
+ fact: workflow children cannot call `artifact_edit`. The tool requires a
1515
+ parent-conversation session ID that `agent()` children do not have — every
1516
+ trigger-child attempt failed with `missing session id`, and the runtime's
1517
+ JSON-candidate scan threw on the refusal before the workflow could parse it.
1518
+ A second, independent defect: the retry harness (`lib/retry-publish.js`)
1519
+ imported the unavailable `better-sqlite3` and pointed at `crew.db` instead
1520
+ of `crew-state.db`, so no retry could ever run.
1521
+
1522
+ **Decision.** One party owns issuance: the session-carrying tick worker.
1523
+ The workflow prepares (preflight, provenance base, checksummed diff via
1524
+ `lib/compute-publish-diff.js`, toolcheck, pre-trigger manifest baseline),
1525
+ records ONE issuer-stamped ledger entry (`outcome: "publish-intent"`,
1526
+ `issuer: "workflow"`, carrying `diff_path`/`diff_sha256`), and parks with
1527
+ `publish: publish-requested <commit> <attempt>`. The tick claims the intent
1528
+ (`publish: publish-intent-claimed <expiry>`, 1-hour lease, via
1529
+ `scan-publish-unknown`'s `intent` bucket), verifies the staged diff's sha256
1530
+ (regenerating deterministically from `base..commit` when the file is
1531
+ missing), runs the manifest-freshness check on re-claimed intents (the dead
1532
+ tick may have issued and died before recording — never blindly re-issue),
1533
+ calls `artifact_edit` directly in its own turn, and records the outcome
1534
+ through `record-intent-issuance` (CAS on the claim): `accepted`/`recovered`
1535
+ → issuer-stamped `submitted` (`issuer: "tick-worker"`) + mirrored
1536
+ `publish: verification-requested`; `refused` → `rejected` + terminal
1537
+ `publish: publish-refused`.
1538
+
1539
+ **Retired, not reverted.** The trigger child, build-state observation
1540
+ machinery, refusal parser, phantom workflow-owned `submitted`, and the
1541
+ receipt-less unknown machinery's issuance side are deleted. The
1542
+ unknown-recovery classifier survives for legacy parks; its trigger anchor
1543
+ is now issuer-bound (only `issuer: "tick-worker"` binds). The retry
1544
+ protocol survives ported to `node:sqlite` + `crew-state.db` with the
1545
+ missing `CREW_REPO` for the merge lock supplied, and its issuance step is
1546
+ the tick worker's direct call. Verified re-entry: a `submitted` without a
1547
+ mirrored request mirrors `verification-requested` — never re-issues.
1548
+
1549
+ **What it is not.** Not a ledger-as-request queue (no new queue
1550
+ abstraction — the intent IS the ledger entry plus the park note). Not a
1551
+ revert of room #22's trigger-timing work (that machinery was deleted, and
1552
+ room #24 disproved its premise). The new path is proven in parts; the next
1553
+ clean room proves it end-to-end.
1554
+
1555
+ Applies to: workflows/{standard,bugfix,chore}.js (Publish exits at intent),
1556
+ lib/crew-api.js (scan-publish-unknown intent bucket, record-intent-issuance,
1557
+ issuer-bound findTriggerEntry), lib/retry-publish.js (node:sqlite port,
1558
+ CREW_REPO, direct tick issuance), lib/verify-publish.js (issuer-bound
1559
+ trigger anchor), lib/publish-note-vocabulary.js (publish-requested,
1560
+ publish-intent-claimed, publish-refused), seed/cron-body-template.md
1561
+ (steps 4.4b, 4.4 retry direct-issuance), docs/publish-verification.md,
1562
+ docs/publish-unknown-recovery.md.
package/docs/guide.md CHANGED
@@ -366,7 +366,7 @@ The workflow dispatches the Reproduce strategy mechanically on the classificatio
366
366
 
367
367
  ### Publish content verification
368
368
 
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.
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.
370
370
 
371
371
  ## Identities
372
372
 
@@ -380,7 +380,7 @@ Each phase has an assigned identity — a character with a defined personality:
380
380
  | Build | **Wren** | Quietest one, trusts the plan |
381
381
  | Review | **Cass** | Fair but exacting — holds the spec as the contract |
382
382
  | 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 |
383
+ | 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
384
  | QA | **Hazel** | Code-blind, persistent, wears persona costumes |
385
385
  | Reproduce | **Hazel** | Reproduces bugs before fixing |
386
386
  | Write | **Tate** | Docs writer, observational voice |
@@ -1,10 +1,16 @@
1
- # Publish-unknown recovery (blocker 15, 2026-09-18)
1
+ # Publish-unknown recovery (blocker 15, 2026-09-18; one-party 2026-09-20)
2
2
 
3
3
  When a standard/bugfix Publish parks with "Publish outcome unknown", the
4
4
  artifact-edit trigger went out fire-and-forget and no receipt came back —
5
5
  async was planned for, receipt-less was not. The unknown-recovery loop
6
6
  closes that gap without re-issuing blindly.
7
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
+
8
14
  ## The note is the state machine
9
15
 
10
16
  Recovery state lives in the task's `note` events, keyed on machine-written
@@ -47,8 +53,9 @@ publish: dropped
47
53
  │ (verified → verification-requested; ambiguous → terminal;
48
54
  │ superseded → terminal; deferred → no-op)
49
55
  └─ provably-dropped ──> publish: retry-intended <commit> <ts>
50
- ── trigger (same child shape as the first attempt)
51
- ├─ ARTIFACT_EDIT_REFUSED ──> publish: retry-refused (terminal)
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)
52
59
  └─ no refusal ──> publish: retry-issued <commit>
53
60
  ──> publish: verification-requested <commit> not-before=<ts+20m>
54
61
  (Step 4.5 skips not-before entries until the window passes)
@@ -17,6 +17,61 @@
17
17
  > the verifier fails CLOSED (parked). Staleness can only park a task,
18
18
  > never stamp provenance.
19
19
 
20
+ ## One-party publish (2026-09-20, blocker 22)
21
+
22
+ The publish path used to be two-party: the workflow spawned a trigger
23
+ child to call `artifact_edit`, then observed the outcome. Room #24 proved
24
+ the child cannot reach `artifact_edit` — the tool requires a
25
+ parent-conversation session ID that `agent()` children do not have, and all
26
+ four journeys parked at Publish on exactly that failure.
27
+
28
+ The path is now one-party: the workflow prepares the publish and parks at
29
+ **intent**; the session-carrying tick worker — the only caller class that
30
+ can reach `artifact_edit` — issues the edit directly in its own turn.
31
+
32
+ - **Workflow-owned (Publish phase, read-only):** preflight, provenance
33
+ base, checksummed diff (`lib/compute-publish-diff.js` → staged at
34
+ `$CREW_HOME/.publish-diffs/<taskId>.diff`), artifact toolcheck,
35
+ pre-trigger manifest baseline. It writes ONE issuer-stamped ledger entry
36
+ (`outcome: "publish-intent"`, `issuer: "workflow"`, carrying `diff_path`
37
+ and `diff_sha256`) and parks with
38
+ `publish: publish-requested <commit> <attempt>`. It performs no edit and
39
+ writes no issuance.
40
+ - **Tick-worker-owned (seed/cron-body-template.md, step 4.4b):** the tick
41
+ claims the intent (`publish: publish-intent-claimed <expiry>`, 1-hour
42
+ lease, via `scan-publish-unknown`'s `intent` bucket), verifies the staged
43
+ diff's sha256 (regenerating deterministically from `base..commit` via
44
+ `lib/compute-publish-diff.js --commit` when the file is missing — a diff
45
+ that cannot be (re)generated byte-identically is recorded terminally via
46
+ `record-intent-unissuable` as `publish: publish-unissuable`, never
47
+ re-claimed in a loop; a mismatched diff never becomes an edit), and on a
48
+ re-claimed intent runs the deterministic manifest-freshness check first
49
+ (`lib/check-intent-freshness.js` compares the on-disk manifest's
50
+ `content_sha256`/`built_at` against the intent's `manifest_before` — the
51
+ dead tick may have issued and died before recording, and a re-claim never
52
+ blindly re-issues; the check's JSON evidence is recorded on a `recovered`
53
+ entry). It calls `artifact_edit` directly in its own turn, then
54
+ records the outcome through `record-intent-issuance` (compare-and-swap on
55
+ the claim): `accepted`/`recovered` writes the issuer-stamped
56
+ `submitted` ledger entry (`issuer: "tick-worker"`; a `recovered` entry's
57
+ `issued_at` is the dead tick's original claim time — the lower bound on
58
+ the unobserved issuance) and mirrors `publish: verification-requested`;
59
+ `refused` writes `rejected` and the terminal `publish: publish-refused`
60
+ note. An inconclusive edit call (tool unavailable, timeout, ambiguous
61
+ result) is never recorded — the tick logs it and lets the claim expire.
62
+ - **Verified re-entry:** if the tick died between issuing and mirroring,
63
+ the next scan mirrors the missing `publish: verification-requested`
64
+ from the issuer-stamped `submitted` entry — it never re-issues. The
65
+ workflow that parked at intent never acts on the park note again: its
66
+ Publish session already ended. When a verification-pending task is later
67
+ re-queued and the dispatcher resumes Publish, the workflow's provenance
68
+ re-entry guard (stamp has this `task_id` and `source_commit === HEAD`)
69
+ returns PASS without any publish action.
70
+
71
+ Everything below about parent verification (read-back, mechanical
72
+ comparison, supersession, stamping) is unchanged: the one-party model
73
+ changes WHO issues the edit, not what certifies it.
74
+
20
75
  Provenance is the artifact's claim that its live content came from a specific
21
76
  repo commit. The workflow never stamps it. This document is the parent-side
22
77
  protocol. Deterministic code detects, claims, and certifies; the tick worker
@@ -46,29 +101,21 @@ The workflow dropped the report entirely on 2026-09-16 (clean-room task
46
101
  `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
47
102
  stochastic text channel and the runtime's JSON-candidate heuristic misfired
48
103
  on its prose ("workflow agent output was not JSON"), parking a task whose
49
- edit may have gone through. The trigger is now awaited and scanned for a
50
- single explicit refusal signal — `ARTIFACT_EDIT_REFUSED: <text>` as the
51
- entire trimmed turn output — and nothing else is consumed from the return.
52
- An exact refusal is conclusive negative evidence (parks `rejected`, skips
53
- observation polling); any other output (including prose quoting the signal)
54
- is inconclusive and follows the existing fail-closed observation path.
55
- `applied_report` is `missing-report` on ledger lines for issued triggers
56
- (pre-trigger parks and unattributed-unknown parks write null — no trigger
57
- was observed, so there is nothing to report). The
58
- parent ignores the (absent) report entirely when deciding whether to stamp.
104
+ edit may have gone through. 2026-09-20 retired the trigger child outright
105
+ (blocker 22: children cannot reach `artifact_edit`): the session-carrying
106
+ tick worker issues the edit directly in its own turn and records the
107
+ outcome itself — there is no trigger call to close out, and no report for
108
+ the parent to ignore. `applied_report` survives on ledger lines only as
109
+ `missing-report` (legacy) or null.
59
110
 
60
111
  The contract is split on purpose:
61
112
 
62
- - **Workflow-owned:** carrying the merged diff to the builder, attributing
63
- the edit itself (fire-and-forget trigger — no builder report — via
64
- pre-trigger toolcheck, pre-trigger build-state baseline, and post-trigger
65
- build-state diff), the build-completion poll, post-deploy cleanup,
66
- recording the Publish session completed, and parking with `publish:
67
- verification-requested <commit>` instead of stamping. The workflow does NOT trigger the read-back inspection — an
68
- async inspection triggered from inside a workflow run delivers its result
69
- to the root agent, never back into the run, so a workflow-side trigger is
70
- an orphan the verifier cannot consume. The parent triggers the one
71
- inspection it can actually receive.
113
+ - **Workflow-owned:** preparing the publish read-only (preflight,
114
+ provenance base, checksummed diff, toolcheck, pre-trigger manifest
115
+ baseline), recording the `publish-intent` ledger entry, and parking with
116
+ `publish: publish-requested <commit> <attempt>` instead of stamping. The
117
+ workflow never issues the edit — issuance belongs to the session-carrying
118
+ tick worker (one-party publish, 2026-09-20).
72
119
  - **Parent-owned (deterministic code, ferried by the tick worker):**
73
120
  scanning for verification-pending parks, atomically claiming them,
74
121
  building the read-back request, triggering the inspection, waiting for the
@@ -356,47 +403,49 @@ For a task parked with `publish: verification-requested <commit>`:
356
403
  second scan sees the unexpired `publish: verification-claimed` note and
357
404
  skips. The lease expiry bounds the damage if a claimer dies.
358
405
 
359
- ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; fire-and-forget 2026-09-16)
360
-
361
- Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
362
- child failed structured closeout and the in-flight-only build-state poll
363
- could not see the completed build — even though the build HAD run (a fresh
364
- platform audit directory existed). 2026-09-16 (clean-room task `e2a8d9f8`)
365
- showed the failure is worse than a catchable throw: the runtime's
366
- JSON-candidate heuristic rejects the trigger call itself ("workflow agent
367
- output was not JSON") whenever the child returns prose, whether or not the
368
- edit went through. The trigger is therefore fire-and-forget — no schema, no
369
- consumed return value — and the workflow always attributes the edit itself.
370
- Two mechanisms close the gap.
371
-
372
- **1. Pre-trigger toolcheck + baseline.** Before the trigger, a tiny schema'd
373
- child proves the artifact tool namespace is available (one bounded retry on
374
- explicit negative evidence — the only safe retry on the publish path:
375
- without the tools the edit provably did not go through) and captures a
376
- pre-trigger build-state baseline. After the trigger, the workflow diffs the
377
- post-trigger build state against the baseline: a build whose `agent_id` is
378
- new relative to the baseline is this edit's receipt. The baseline build's
379
- `agent_id` is never substituted — a build already in flight at baseline
380
- predates the trigger and is never attributed to this edit.
381
-
382
- **2. Workflow-side durable evidence.** Before the rebuild trigger, the
383
- workflow snapshots the artifact's audit-directory listing
384
- (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). When
385
- no in-flight receipt was observed, it re-lists and diffs: a timestamped
386
- directory that appeared during the trigger window is positive evidence the
387
- edit went through and the build completed. The fallback never re-issues the
388
- edit, never stamps provenance, and only routes to the parent's independent
389
- content read-back. No new directory still parks `unknown` fail-closed. The
390
- ledger's `detail` line distinguishes the two confirmations: `… edit
391
- confirmed via durable audit evidence (new audit dir …)` vs `… build receipt
392
- captured by workflow-owned build-state observation (pre/post-trigger diff)`.
393
-
394
- The fallback's known limitation: audit directories are not attributed to
395
- tasks, so two concurrent publishes to the same artifact could cross-read.
396
- The consequence is bounded — the fallback only routes to the parent
397
- read-back, and the parent still certifies the exact commit's content
398
- mechanically (a wrong build's content fails closed as `publish:
399
- content-mismatch` / `publish: build-mismatch`, never stamps).
406
+ ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; one-party 2026-09-20)
407
+
408
+ Attempt 7 parked at Publish with outcome `unknown`: the old two-party
409
+ path's rebuild-trigger child failed structured closeout and the
410
+ in-flight-only build-state poll could not see the completed build — even
411
+ though the build HAD run. The two-party machinery (trigger child,
412
+ build-state observation, receipt attribution) was retired 2026-09-20:
413
+ blocker 22 proved children cannot reach `artifact_edit`, so there is no
414
+ trigger child anymore and no observation gap to close.
415
+
416
+ What remains of unknown-recovery is the legacy classifier for parks that
417
+ predate one-party publish, plus the one-party crash windows:
418
+
419
+ - **Legacy `due` parks** (`publish outcome unknown` parks from the old
420
+ path): `scan-publish-unknown` claims them for the deterministic
421
+ six-way classifier (`lib/classify-publish-absence.js`) exactly as
422
+ before. The only accepted trigger anchor is an issuer-stamped
423
+ `submitted` ledger entry (`issuer: "tick-worker"` — the only issuance
424
+ class that exists now); a `submitted` without issuer never binds.
425
+ - **One-party crash windows:** the tick dies after issuing but before
426
+ `record-intent-issuance` → the next scan mirrors
427
+ `publish: verification-requested` from the issuer-stamped `submitted`
428
+ entry (never re-issues). The tick dies before issuing → the intent
429
+ claim expires and the next scan re-claims with `reclaimed: true`, and
430
+ the manifest-freshness check decides between `recovered` and a fresh
431
+ issuance — a re-claim never blindly re-issues.
432
+
433
+ ### Retired two-party machinery (kept for the record)
434
+
435
+ **1. Pre-trigger toolcheck + baseline.** In the two-party path, before the
436
+ trigger, a tiny schema'd child proved the artifact tool namespace was
437
+ available and captured a pre-trigger build-state baseline; after the
438
+ trigger, the workflow diffed the post-trigger build state against the
439
+ baseline for a receipt. In the one-party path the workflow still performs
440
+ the read-only preflight (toolcheck + manifest baseline — the baseline is
441
+ carried in the `publish-intent` ledger entry's `manifest_before`), but
442
+ there is no trigger child and no receipt attribution: the tick worker
443
+ issues the edit directly and records the outcome itself.
444
+
445
+ **2. Workflow-side durable evidence.** In the two-party path, before the
446
+ rebuild trigger, the workflow snapshotted the artifact's audit-directory
447
+ listing and re-diffed it after the trigger as fallback evidence. Retired
448
+ with the trigger child — the observation gap it closed no longer exists.
400
449
 
401
450
  **2. `resolve-publish-unknown` (Crew API).** For attempts already parked
402
451
  `unknown` before this fix: given a task parked with a latest ledger outcome
@@ -416,6 +465,13 @@ build in the window, unobservable window, or an already-resolved attempt
416
465
  Case-sensitive, exact-prefix matches — match on prefixes, never on English
417
466
  meaning:
418
467
 
468
+ - `publish: publish-requested <commit> <attempt>` — workflow park at
469
+ intent (one-party publish, 2026-09-20): the merged change is staged as a
470
+ checksummed diff; the tick worker owns issuance. Not a verdict.
471
+ - `publish: publish-intent-claimed <ISO-expiry>` — tick-worker scan;
472
+ atomic claim with 1-hour lease. Not a verdict.
473
+ - `publish: publish-refused <commit>` — tick worker; the platform refused
474
+ the directly-issued edit. Terminal: parked for human attention.
419
475
  - `publish: verification-requested <commit>` — workflow park; contained in
420
476
  the stored `Parked: …` message.
421
477
  - `publish: verification-claimed <ISO-expiry>` — parent scan; atomic claim
@@ -47,9 +47,11 @@ nearest `package.json`'s `type` field.
47
47
  Boundaries, stated plainly: `--help` short-circuits before argument
48
48
  parsing, so the gate proves an entry *loads*, not that its main path
49
49
  *behaves* (the suite covers behavior); `workflows/*.js` are not gated
50
- here — the suite's loader emulation (`publish-verdict-first.test.js`) is
51
- their true gate; the import-safe half of the shebang-less contract (no
52
- side effects on import) is unchecked — an accepted residual with no
50
+ here — the suite's loader emulation (`workflow-size.test.js`, which
51
+ drives the release script's `_validate_workflows` behaviorally: export-strip
52
+ + async-function-wrap parse plus the size budget) is their true gate; the
53
+ import-safe half of the shebang-less contract (no side effects on import)
54
+ is unchecked — an accepted residual with no
53
55
  cheap mechanism.
54
56
 
55
57
  Verdicts are aggregated and every entry gets one row in
@@ -0,0 +1,106 @@
1
+ # Critic review — 0.14.5 release candidate (one-party worker-owned publish)
2
+
3
+ Change set: `v0.14.4..HEAD` worktree (32 files, +1444/−4013). Full suite green on the final tree ("All test suites passed.", exit 0, 90 suites). Read-only review; no repo modifications made by the panel. Coordinator empirically verified the load-bearing claims before disposition.
4
+
5
+ Contracts checked against (Eric's standing rules): (a) wording is not a mechanism — prefer mechanical guards over hardened prose; (b) fix the cause first, then the symptom; (c) unknown outcomes fail closed — never stamp/verify on inconclusive evidence; (d) don't proliferate tests — reuse the existing suite; (e) implementation and documentation ship together; (f) only `submitted` ledger entries with `issuer: "tick-worker"` bind as real issuance; (g) take critic blessings skeptically — every disposition below is checked against the rules and the code, not the panel's enthusiasm. Two panel claims were verified empirically by the coordinator before acceptance (F-O1, F-O2); one high claim was rejected on the code (F-R1).
6
+
7
+ ## Verdicts
8
+
9
+ **Architect: CONCERN** — the one-party shape is the right architecture and trust boundaries are clean in code (no workflow path writes `submitted`; no tick path issues without a claim/lock; the scan never emits `retry_due` with `retry-issued` in history; the verified re-entry guard is sound — no TOCTOU constructible). Four concrete defects: one stale string reintroducing blocker 22 into the retry path (F-A1), one false absolute guarantee (F-A2), one forced outcome ternary (F-A3), one new park→issuance base-staleness window (F-A4). None is fail-open at the stamp — the content read-back backstop holds everywhere — hence CONCERN, not REJECT.
10
+
11
+ **Subtractor: ACCEPT** — the change set does what it claims; the new code is mechanically disciplined; contracts (a), (d), (e), (f) hold. Three low-severity deletable leftovers (F-S1–S3), two info notes (F-S4–S5). Notably, the Subtractor did NOT claim the retry protocol as a must-cut — the step-back disposition (keep for 0.14.5) stands unchallenged.
12
+
13
+ **Reliability: CONCERN** — sequential operation is fail-closed and well-tested (10 intent-path probes + 22 validateRegistry probes green; scan-publish-unknown.test.js 45/45). The headline "never double-issue" guarantee is carried by prose (F-R2) and has a null-baseline hole with no terminal exit (F-R3). F-R1 (non-atomic lease) was REJECTED by the coordinator on the code — see below.
14
+
15
+ **Room Operator: REJECT** — two high findings in the 4.4b crash-recovery path (F-O1: the `recovered` outcome can never pass verification — self-defeating; F-O2: `--commit` silently ignored by the diff regenerator, and the mismatch path loops forever with no terminal exit). Both are cause-level and both were confirmed by the coordinator. The REJECT converts to ship-with-fixes: both have small mechanical fixes.
16
+
17
+ ## Findings and dispositions
18
+
19
+ ### Must-fix before ship
20
+
21
+ **F-O1 (high) — ACCEPTED. The `recovered` outcome can never pass verification.**
22
+ `record-intent-issuance` writes `issued_at = null` for `recovered` (lib/crew-api.js:2363); the verifier binds `triggerTs = issued_at || ts` = the *recovery-record* time T2 (lib/verify-publish.js:356), then requires `built_at > T2` (:376-378). The build completed at T1 < T2, so every genuine recovery ends terminal `unverifiable-content` with the false message "no new build for this attempt". Coordinator-verified on the code. The author's comment ("the verifier falls back to ts") shows the confusion: the recovery-record ts is necessarily *after* the build — it is not a usable bound.
23
+ *Fix:* for `recovered`, set `issued_at` to the dead tick's claim time (the earliest `publish: publish-intent-claimed` note's ts — `writePublishNote` takes an explicit claimedAt, and the intent-claimed note carries it). Issuance ∈ [claim, expiry] and built_at > issuance, so built_at > claim_time is a sound lower bound. Small, mechanical, testable.
24
+
25
+ **F-O2 (high) — ACCEPTED. 4.4b's regeneration passes `--commit`; `compute-publish-diff.js` silently ignores it.**
26
+ Only `--repo-path`/`--base`/`--out` are parsed (lib/compute-publish-diff.js:60-75); regeneration is `base..HEAD`, not the documented `base..commit` (docs/publish-verification.md:43). Coordinator-ran the command: exit 0, `--commit deadbeef…` ignored, stdout reports HEAD. Worse: on sha256 mismatch the template says only "log loudly and STOP" — the claim expires, the next scan re-claims, identical result, forever. The outcome enum has no terminal branch for this case.
27
+ *Fix:* teach `compute-publish-diff.js` a `--commit` flag (diff `base..commit`, default HEAD) so the template command does what the doc claims; add a terminal park (`publish: publish-unissuable` or equivalent vocabulary + parked for human attention) for the unresolvable-regeneration case instead of the silent reclaim loop.
28
+
29
+ **F-A1 (medium) — ACCEPTED. Stale "spawn the artifact_edit child" message in `retry-publish.js:323`.**
30
+ The prepare output — the actual handoff the tick reads — still instructs the blocker-22-forbidden shape, while the cron body (4.4) and the recovery doc were updated to tick-mediated direct issuance. Coordinator-confirmed verbatim. A tick following the tool message spawns a child → `missing session id` → the "otherwise `--edit-outcome accepted`" contract mints a phantom issuer-stamped `submitted` for an edit that never went out, spending the single retry budget on a non-attempt. The verification backstop fails closed (no stamp), but the ledger is poisoned.
31
+ *Fix:* one line — change the message to the tick-mediated direct-issuance instruction. Contract (a) violation at the exact handoff the tick reads.
32
+
33
+ ### Should-fix for 0.14.5
34
+
35
+ **F-A4 (medium) — ACCEPTED. No base-currency check between intent-park and issuance.**
36
+ The intent entry carries `diff_path`/`diff_sha256` but no base; the lock releases at the intent park, so issuance happens ≥1 tick later, lock-free. If provenance moved in between (another task's publish verified), the tick issues a diff against a superseded base: overlapping hunks → spurious terminal `publish: publish-refused`; partial application → `content-mismatch` park. Not stamp-unsafe, but a liveness regression in the multi-task pipeline the dispatcher supports. New in 0.14.5 (0.14.4 computed and issued in one lock-held session).
37
+ *Fix:* record `base` in the intent ledger entry; the scan's intent branch (or the tick's step 1) fails the claim loudly when `base !==` current stamped provenance — re-queue the task (the workflow's Publish re-prepares against the new base), never issue the stale diff.
38
+
39
+ **F-O3 / F-R2 (medium) — ACCEPTED (merged). The reclaim freshness check is prose, not mechanism.**
40
+ Template 4.4b step 2 tells the tick to compare `artifact_status` fields the repo never establishes against `manifest_before`, but `record-intent-issuance` takes no freshness evidence and records `accepted` unconditionally. The crash-window guarantee therefore rests on tick discipline — the "wording is not a mechanism" class. A null `manifest_before` (best-effort baseline, explicitly never-gating) leaves the worker guessing between blind re-issue and false `recovered`.
41
+ *Fix:* a deterministic freshness helper (lib script reading the on-disk manifest at the path the verifier already uses, comparing `content_sha256`/`built_at` against `manifest_before`) whose evidence is passed to / recorded by `record-intent-issuance`; the template names the manifest path and the exact comparison instead of `artifact_status` fields.
42
+
43
+ **F-A3 / F-O5 (low-medium) — ACCEPTED (merged). The outcome enum has no "unknown": inconclusive issuance is forced into `accepted`.**
44
+ Template 4.4b never defines `accepted` (the definition lives only in a code comment), and 4.4b lacks the tool-unavailable clause 4.4 has. Transport timeouts, apply-failures on already-applied hunks, namespace-load failures — none are "explicit refusal", so they land in `accepted` → issuer-stamped `submitted` on unproven issuance. Contained at the stamp (verification still requires a content match), but it weakens contract (f)'s meaning.
45
+ *Fix:* define `accepted` precisely in the template ("the direct call completed without error or explicit refusal"); add the inconclusive branch — on ambiguous outcome or tool-unavailable, do NOT record, log loudly, and exit: the claim expires and the reclaim path (with the mechanized freshness check) is exactly the designed handler for "maybe issued". Add 4.4's tool-unavailable clause to 4.4b.
46
+
47
+ **F-O4 (medium) — ACCEPTED. The known-blockers snapshot shipped already stale; no refresh mechanism.**
48
+ The template's blocker-23 snapshot says "re-anchor or remove the retry machinery" while the authoritative file says "Fixed in BUILD (0.14.5): ported to node:sqlite + crew-state.db". Grep over docs/, workflows/upgrade.js, tests/run.sh: no script, release step, or test performs the promised "refreshed at release time" — prose only. A fixed blocker keeps telling pollers to "log one line and move on".
49
+ *Fix for 0.14.5:* update the snapshot text to match the file now. Follow-up: a mechanical refresh (release-checklist step or a test that diffs the template snapshot against known-blockers.md's settled list).
50
+
51
+ **F-R3 (medium) — ACCEPTED. Null `manifest_before` on reclaim has no terminal exit.**
52
+ First claim + null baseline is safe to issue (no prior issuance possible under a fresh claim). Reclaim + null baseline is not: the freshness comparison is unperformable, the tick's safe move (stop without recording) yields an hourly reclaim loop with no human escalation, the unsafe move is a blind re-issue.
53
+ *Fix:* on reclaim with null baseline, fail closed with a terminal human-attention park — never blind-issue, never silent-loop. Contract (c).
54
+
55
+ **F-R5 (low) — ACCEPTED. Intent bucket can carry `repo_path: null` without failing closed.**
56
+ The scan fails closed on missing `deploy_slug` but emits `repo_path: null`; the tick then fails at sha256sum/regen and the task loops on hourly re-claims.
57
+ *Fix:* fail closed at emission — terminal/human park, same as the slug check.
58
+
59
+ **F-S1 / F-S2 / F-S3 (low) — ACCEPTED. Trivial cleanups.**
60
+ Dead `repo` param on `releaseLock` (lib/retry-publish.js:192, :356 — all 8 call sites pass nothing); dead `notes` param on `claimForIntent` (lib/crew-api.js:2014); four stray `echo "` debris lines in tests/run.sh (:698, :700, :702, :704) from the deleted-test-block removals.
61
+
62
+ ### Downgraded / rejected / deferred
63
+
64
+ **F-R1 (high) — REJECTED as stated. The "non-atomic lease" does not survive the code.**
65
+ The claim write is a read-then-write inside a single `BEGIN`/`COMMIT` (lib/crew-api.js:2115/2264) with `ROLLBACK` + throw on any error, under WAL + `busy_timeout = 5000` (:252-254). Two overlapping scans cannot both commit: the loser's write waits, then its COMMIT fails on the stale snapshot (`SQLITE_BUSY_SNAPSHOT`) and rolls back entirely. Coordinator-verified against the transaction code; the Architect's I-3 (fail-closed, not silent) was the correct read. The residual double-issuance risk is F-A2's *sequential* lease-expiry path only — no advisory-lock fix needed.
66
+
67
+ **F-A2 (medium) — ACCEPTED as a documentation fix.**
68
+ Slow-but-alive tick past the 1h lease + manifest lag (build normally still in flight exactly when the check matters — fire-and-forget, no receipt) = double *physical* issuance of the identical diff. Consequence is bounded: at most one *recorded* issuance (CAS), the verifier certifies content not provenance-of-issue, and identical bytes are idempotent-ish. Exactly-once physical issuance is not mechanically achievable with receipt-less semantics — the honest disposition is to reword the change set's "the diff is never issued twice" to what the mechanism provides: at-most-one recorded issuance; duplicate physical issuance possible in the lease-expiry window, content-identical, verifier-bounded. The F-O3/F-R2 freshness mechanization narrows the window; it cannot close it.
69
+
70
+ **F-R4 (low) — ACCEPTED as follow-up.** The exactly-one-retry `historyHas("publish: retry-issued")` scans only the last 40 publish notes (pre-existing shape, also in v0.14.4). Unbound it when convenient; not ship-blocking.
71
+
72
+ **F-O6 (low) — ACCEPTED as follow-up.** Skip/record-failure reasons are stdout-only; DB-alone debugging has blind spots. Nice-to-have.
73
+
74
+ **F-S4 / F-S5 (info) — noted, no action.** Pre-existing duplicated comment; legacy fixtures mixing the new issuer stamp with old workflow labels are cosmetic (the bound fields are exactly producible).
75
+
76
+ ## Coordinator's empirical verifications
77
+
78
+ 1. **F-O1 confirmed on the code:** `issued_at = null` for `recovered` (crew-api.js:2363) → verifier binds `triggerTs = issued_at || ts` = recovery-record time (verify-publish.js:356) → `built_at > triggerTs` (:376-378) strictly false in the normal case. Every genuine recovery ends terminal `unverifiable-content`. The dead tick's claim time is recoverable from note history (`writePublishNote` takes an explicit claimedAt), giving the fix a sound lower bound.
79
+ 2. **F-O2 confirmed by execution:** `node lib/compute-publish-diff.js --repo-path . --base <empty-tree> --commit deadbeef… --out /tmp/x.diff` → exit 0, `--commit` silently ignored, stdout reports HEAD. The template's regeneration command does not do what the doc claims.
80
+ 3. **F-R1 rejected on the code:** single BEGIN/COMMIT + ROLLBACK/throw, WAL, busy_timeout=5000 — the concurrent-scan double-claim cannot commit; the loser rolls back. The critic's sequential probes were valid; the concurrent mechanism was not.
81
+ 4. **F-A1 confirmed verbatim:** `lib/retry-publish.js:323` still reads "spawn the artifact_edit child with the diff".
82
+ 5. **F-A4's premise confirmed:** `parkTask` → `terminalCleanup` releases the merge lock at the intent park (prior BUILD proof) — the park→issuance window is lock-free and new in 0.14.5.
83
+
84
+ ## Step-back round (run by the coordinator, not delegated)
85
+
86
+ Question: is one-party tick-worker-owned publish the right shape, or is there a simpler architecture that dissolves it?
87
+
88
+ **A. Synchronous publish inside the workflow via a new platform capability.** Requires a platform change that doesn't exist. The current shape is the minimal adaptation to the platform as it is. Not actionable — external dependency, not a design flaw. *Coordinator: agree. Nothing in the review changes this.*
89
+
90
+ **B. Tick does everything including diff computation; workflow parks with bare coordinates.** Not simpler, just relocated: moves deterministic, testable preparation into the 90-minute tick where failures are harder to attribute. *Coordinator: agree, and the review reinforces it — F-O3/F-R2's fix is to mechanize the freshness check as a lib script, i.e. keep deterministic work in code, not move it into the tick's prose.*
91
+
92
+ **C. Tick applies the diff directly to `~/workspace/ts-spaces/<slug>/` via filesystem, bypassing `artifact_edit`.** Rejected: bypasses the supported product interface, and nothing proves the platform rebuilds on raw filesystem writes. *Coordinator: agree firmly. The read-back sensor reads the working copy, but writing to it directly would be the same cleverness-in-the-wrong-direction the loop keeps burning rooms on.*
93
+
94
+ **D. Route verified tasks straight to QA; delete the verified re-entry guard.** The guard is ~30 lines, mechanical, proven non-gating; the alternative is dispatcher special-casing. *Coordinator: agree — and the Architect verified the guard sound (no TOCTOU constructible; QA is an independent backstop).*
95
+
96
+ **E. Delete the retry protocol as subsumed by intent recovery.** The one live candidate. Under one-party, every new issuance is synchronous with immediate accepted/refused; the dropped shape is handled by intent reclaim + freshness → `recovered`. *Coordinator: KEEP for 0.14.5.* The review reinforces this: F-A1 lives in the retry path (fix the message, keep the path), F-R4 notes the retry budget, and the retry protocol is the only backstop for legacy "Publish outcome unknown" parks. Deleting it now, on zero clean-room evidence, would remove defense-in-depth for aesthetics. Filed as designed follow-up after the first clean room runs, when `retry_due` can be proven never to fire for new-model publishes. The Subtractor did not claim it as a must-cut — the guard held.
97
+
98
+ **F. Collapse the publish-intent ledger entry and the park note into one record.** Reject: the ledger is the audit trail, the note is the state machine — different consumers; merging tangles the two. *Coordinator: agree.*
99
+
100
+ **Step-back conclusion:** the one-party shape is the right architecture given the platform constraint; the workflow-prepares/tick-issues split isolates the privileged act minimally; the lease/reclaim machinery is proportional to the exactly-once-recorded requirement (overlapping ticks are possible: 90-min timeout vs 15-min interval); the retry protocol is the sole redundancy candidate and stays as a backstop for 0.14.5. One gap the step-back missed and the panel caught: F-A4's park→issuance base-staleness window — a genuine liveness hole in the multi-task pipeline, with a small mechanical fix. No better architecture found — the design stands, with the retry-deletion filed as post-room follow-up.
101
+
102
+ ## Recommendation: SHIP WITH FIXES
103
+
104
+ Fix the 3 must-fix (F-O1, F-O2, F-A1) and the 7 should-fix (F-A4, F-O3/F-R2, F-A3/F-O5, F-O4-text, F-R3, F-R5, F-S1/S2/S3), re-run the full suite, then publish. No finding is fail-open at the stamp — all four critics agree the content read-back backstop holds on every path — the architecture is sound, and every fix is small and mechanical. F-R4 and F-O6 ride as follow-ups; the retry-protocol deletion rides as a post-room follow-up.
105
+
106
+ What "done" looks like before PUBLISH: F-O1's `issued_at` bound in `record-intent-issuance` + test; `--commit` in `compute-publish-diff.js` + terminal `publish-unissuable` vocabulary + test; F-A1's one-line message fix; intent entry carries `base` + stale-base re-queue + test; mechanized freshness helper + template rewrite of step 2; `accepted` defined + inconclusive branch + 4.4b tool-unavailable clause; blocker-23 snapshot text corrected; null-baseline-reclaim and null-repo_path terminal parks + tests; dead params and stray echo lines removed. Then one fresh uninterrupted `bash tests/run.sh`, then the release sequence.
package/lib/AGENTS.md CHANGED
@@ -42,7 +42,8 @@ Shipped library: ESM JavaScript CLIs and import-safe modules, shell scripts for
42
42
  - `setup-project-repo.js` — repo-local setup composer for a registered project (2026-09-17, setup-commit 2026-09-18): runs the `.orchestration/` scaffold plus the `.gitignore` update (only with explicit `--consent`), then COMMITS both via `commit-scaffold.js` so the repo's primary checkout is clean when setup finishes (room #13: an uncommitted scaffold deterministically broke the first task's Integrate). Refuses loudly when `.gitignore` has pre-existing uncommitted changes (never sweeps the user's edits into the setup commit); `.orchestration/` is crew-owned so pre-existing content there is committed, not refused. The CLI-path equivalent of crew-init's dashboard-mode repo setup; the dashboard calls the same script. No new API endpoint — composition of existing deterministic modules. CLI: `--repo <path> --crew-repo <path> [--consent]`; prints `{ orchestration, gitignore, commit }`, exits non-zero on any failure.
43
43
  - `commit-scaffold.js` — deterministic setup-scaffold commit (2026-09-18): stages only `.orchestration/` (+ `.gitignore` with `--gitignore`) and commits with a message naming exactly what was staged. Idempotent (`nothing-to-commit`, never an empty commit); not-a-git-repo is a clean no-op; missing `.orchestration/` and commit failures (e.g. no git identity) throw loudly — the commit belongs to the user, so no identity is ever invented here. Exports `isPathDirty` (porcelain check for one path) for the composer's pre-mutation guard. CLI: `--repo <path> [--gitignore]`.
44
44
  - `sample-project.js` — optional sample-project setup (2026-09-17): extracts `seed/sample-project.tar.gz` (stripping the `sample-project/` prefix), `git init`, initial commit. No remote required. Registration happens via the existing `create-project` action — no sample-specific endpoint. CLI: `--tarball <path> --dest <dir> --name <project-name>`.
45
- - `compute-publish-diff.js` — deterministic publish-diff computer (2026-09-18, room #14): replaces the agent-ferried `git diff` JSON string field (the JSON ferry dropped a valid 700-line diff — "Publish diff parsed to zero files"). Runs `git diff <base> <HEAD>` via argv (no shell), writes the raw diff bytes to `--out`, and prints only a small JSON summary (`commit`, `base`, `diff_file`, `sha256`, `bytes`, `file_count`, `changed_lines` = added+removed, never raw diff output lines, `files`, `has_binary`, `has_rename`). Empty-tree base is the first-publish ancestor (hardcoded, never `merge-base` on the empty tree); non-ancestor is semantic JSON (`ok:false, error:"not-ancestor"`, exit 0); usage errors exit 2 with nothing on stdout. Pinned per-run alongside the other lifecycle files (`COMPUTE_DIFF` in `PIN_BASENAMES`) so a mid-run release swap cannot change it under the workflow.
45
+ - `compute-publish-diff.js` — deterministic publish-diff computer (2026-09-18, room #14): replaces the agent-ferried `git diff` JSON string field (the JSON ferry dropped a valid 700-line diff — "Publish diff parsed to zero files"). Runs `git diff <base> <commit>` via argv (no shell) — `--commit <40-hex>` (2026-09-20, critic-0145 F-O2) pins the diff target deterministically; omitting it defaults to HEAD for the workflow's initial preparation. Writes the raw diff bytes to `--out`, and prints only a small JSON summary (`commit`, `base`, `diff_file`, `sha256`, `bytes`, `file_count`, `changed_lines` = added+removed, never raw diff output lines, `files`, `has_binary`, `has_rename`). Empty-tree base is the first-publish ancestor (hardcoded, never `merge-base` on the empty tree); non-ancestor is semantic JSON (`ok:false, error:"not-ancestor"`, exit 0); usage errors exit 2 with nothing on stdout. Pinned per-run alongside the other lifecycle files (`COMPUTE_DIFF` in `PIN_BASENAMES`) so a mid-run release swap cannot change it under the workflow.
46
+ - `check-intent-freshness.js` — deterministic intent-freshness helper (2026-09-20, critic-0145 F-O3/F-R2): on a re-claimed publish intent, the tick must decide whether the dead tick's edit already landed before re-issuing — prose judgment is forbidden. `node check-intent-freshness.js --slug <slug> --manifest-before '<json>' [--spaces-root <path>]` reads `<spaces-root>/<slug>/.space-build/manifest.json` (the same path the verifier uses) and prints exactly one JSON object `{ok, advanced, baseline:{built_at,content_sha256}, current:{built_at,content_sha256}}`; `advanced` is true when the content hash changed or `built_at` advanced past the baseline. Unreadable/missing manifest is `ok:false` (exit 1) — the freshness question is unanswerable, so the tick records nothing and lets the claim expire. The helper's stdout JSON is passed verbatim as `freshness_evidence` to `record-intent-issuance ... edit_outcome=recovered`; the command refuses a recovery without `advanced:true` evidence.
46
47
  - `classify-surface.js` — deterministic project-surface classifier (2026-09-18, room #14 defect): a pure, never-throwing scan of the registered repo — `space.json` present → `artifact` surface (deploy_slug suggestion = `space.json`'s valid slug, verbatim — presence alone still means "web app"), `package.json` with a non-empty `bin` → `terminal` surface, otherwise null (unclassified); artifact wins conflicts (the deployed artifact is what users see, same rule as workflows/standard.js). CLI `--repo-path` prints one JSON object (`{surface, deploy_slug, signals}`), always exit 0 (classification failure is a null result, exit 2 only for usage). `create-project` fills omitted `environment_type`/`deploy_type`/`deploy_slug` from the classification; explicit values (including explicit null) always win and drive the effective surface.
47
48
  - `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14; testimony-free 2026-09-19 REVIEW): `create-project` stamps provenance at the pre-scaffold repo HEAD, then crew-init commits `.orchestration/` (+ the consented `.gitignore`) on top — the first task's publish diff would carry the whole scaffold (517 insertions) and blow the 200-line diff-transport budget. The script resolves the scaffold commit itself (`git rev-parse HEAD` in `--repo`; `--scaffold-sha` is an explicit override only) and moves `source_commit` to it only when every fail-closed condition holds deterministically: the existing stamp is not a real publish's (provenance.task_id is set by every real publish and null for initial stamps/base advances — the task_id invariant documented on stampProvenance; the old `--project-created` testimony flag is gone), the scaffold commit's parent is exactly the stamped base, and the scaffold touches only crew-owned paths (`.orchestration/`, `.gitignore`); no provenance + root scaffold commit stamps fresh. Every other case skips without touching provenance; `published_at` is preserved on advance. Every stdout JSON object also carries `repo_clean` (informational — whether `git status --porcelain` is empty — so the caller can warn loudly about a dirty tree; the first task's Integrate refuses to merge onto one). Called from crew-init.js (best-effort, non-fatal). Provenance is per-project (room #15 blocker 8): `--project-id` is required and every read/stamp is project-scoped. CLI: `--crew-home <path> --repo <path> --project-id <id> [--scaffold-sha <sha>]`.
48
49
  - `scaffold-crew.js` — deterministic crew-home scaffold composer (2026-09-19, Room #19 D1): replaces the agent-authored scaffold report (the agent returned prose instead of the schema'd JSON and the platform rejected it, killing room #19's init on minute one — the copying itself always succeeded). The script copies the fixed file sets (identities *.md+*.png, personas *.md, seed/workflows *.md, seed/feedback/README.md) into `<orchDir>/{identities,personas,workflows,feedback}/` with mkdir -p + no-clobber semantics (existing files never overwritten), verifies every expected file on disk (the decision rule), then writes `<orchDir>/.scaffold-receipt.json` `{created, skipped}` (integer counts, durable evidence) and prints the same JSON to stdout. Idempotent: all-skipped is exit 0. Exit 1 with the reason on stderr on any failure; exit 2 on usage errors. crew-init's scaffold-1 call is a byte courier only — it runs this script and prints the receipt's exact bytes; a paraphrased ferry fails the mechanical parse loudly as a blocked init (tripwire, never a fabricated 0/0). The receipt bytes are the agent's report of script output, not proof the script ran: crew-init corroborates with a witness re-run of this same idempotent composer (a witness that still has files to create proves the ferry's work didn't land; the witness's own disk-verification then holds the end state). Fail-closed source guards (2026-09-19 REVIEW): a missing