muse-crew 0.14.2 → 0.14.3
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/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +189 -0
- package/docs/publish-verification.md +5 -2
- package/docs/reviews/critic-0143-redo.md +94 -0
- package/docs/reviews/critic-0143.md +108 -0
- package/lib/AGENTS.md +4 -3
- package/lib/crew-api.js +8 -6
- package/lib/publish-npm.sh +39 -10
- package/lib/retry-publish.js +5 -2
- package/lib/test-detached-integrate.sh +79 -3
- package/lib/test-publish-preflight.sh +94 -5
- package/lib/verify-publish.js +3 -4
- package/lib/worktree-lifecycle.sh +67 -25
- package/package.json +1 -1
- package/workflows/bugfix.js +27 -26
- package/workflows/chore.js +27 -26
- package/workflows/standard.js +27 -26
package/docs/decisions/AGENTS.md
CHANGED
|
@@ -53,6 +53,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
|
|
|
53
53
|
- `#diff-computation` — Diff computation
|
|
54
54
|
- `#already-merged-hydra` — Already-merged hydration
|
|
55
55
|
- `#already-merged-idem2` — Already-merged idempotency
|
|
56
|
+
- `#submitted-on-issuance` — Submitted at trigger issuance
|
|
57
|
+
- `#detached-head-audit` — Detached-HEAD audit (0.14.2)
|
|
58
|
+
- `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
|
|
56
59
|
|
|
57
60
|
## qa-reproduce.md — Decision history: qa reproduce
|
|
58
61
|
|
|
@@ -1278,3 +1278,192 @@ Applies to: standard, bugfix, chore.
|
|
|
1278
1278
|
// fails on the extra hex char). Pure — pinned byte-identical across
|
|
1279
1279
|
// standard/bugfix/chore.
|
|
1280
1280
|
```
|
|
1281
|
+
|
|
1282
|
+
<a id="submitted-on-issuance"></a>
|
|
1283
|
+
## Submitted-on-issuance (room #22)
|
|
1284
|
+
|
|
1285
|
+
Invariant: every non-refused trigger lands `outcome: "submitted"` at
|
|
1286
|
+
issuance — immediately after the trigger-agent call/catch, before any
|
|
1287
|
+
receipt/audit observation. The entry asserts ONLY that the workflow invoked
|
|
1288
|
+
the rebuild-trigger agent and observed no explicit refusal; it does NOT
|
|
1289
|
+
assert `artifact_edit` was called or a build was accepted.
|
|
1290
|
+
|
|
1291
|
+
Applies to: standard, bugfix, chore.
|
|
1292
|
+
|
|
1293
|
+
History: the workflow contract wrote "submitted" only beside a captured receipt
|
|
1294
|
+
agent_id, so receipt-less successful triggers (build-observed audit evidence
|
|
1295
|
+
from STEP 1b, unknown outcomes) left `lib/verify-publish.js` and the
|
|
1296
|
+
unknown-recovery cron with no trigger instant to bind to — the verifier
|
|
1297
|
+
requires the oldest matching `submitted` entry as the trigger-issuance
|
|
1298
|
+
anchor, and all four room #22 journeys parked at Publish on its absence. The
|
|
1299
|
+
gap was in the three workflow files, not the vocabulary: the recovery writer
|
|
1300
|
+
(`lib/retry-publish.js:349`, pre-existing) already wrote
|
|
1301
|
+
issuance-without-receipt "submitted" lines (`agent_id: null`,
|
|
1302
|
+
`workflow: "unknown-recovery"`); the workflow writers never did.
|
|
1303
|
+
|
|
1304
|
+
Outcome vocabulary (one place — the word "submitted" has three meanings;
|
|
1305
|
+
discriminate on `agent_id` / `workflow`, never on the outcome word alone):
|
|
1306
|
+
- "submitted" (a) issuance: workflow writer, `agent_id: null`. The trigger
|
|
1307
|
+
instant — the verifier's anchor. Asserts invocation only.
|
|
1308
|
+
- "submitted" (b) receipt-bearing: workflow writer, `agent_id` non-null. A
|
|
1309
|
+
build with a new agent_id was observed after the trigger — a further
|
|
1310
|
+
observation, not a second issuance.
|
|
1311
|
+
- "submitted" (c) recovery re-issue: `workflow: "unknown-recovery"`,
|
|
1312
|
+
`agent_id: null`. The retry trigger re-issued after a provably-dropped
|
|
1313
|
+
classification.
|
|
1314
|
+
- "build-observed": audit-dir evidence proved the build completed with no
|
|
1315
|
+
captured receipt (H2 verdict-first path).
|
|
1316
|
+
- "unknown": the trigger went out but no build could be attributed to the
|
|
1317
|
+
attempt — fail-closed park; the unknown-recovery scan may retry it.
|
|
1318
|
+
- "failed": a build ran and failed during the poll window (explicit negative
|
|
1319
|
+
evidence, audit report ok=false) — preserved verbatim, never auto-retried.
|
|
1320
|
+
- "rejected": conclusive negative — artifact_edit explicitly refused; the
|
|
1321
|
+
edit provably did not go through. Terminal park, human attention.
|
|
1322
|
+
|
|
1323
|
+
The verifier binds the OLDEST matching "submitted" as the anchor, so the
|
|
1324
|
+
issuance entry must sort first and the observation entries stay as
|
|
1325
|
+
additional lines.
|
|
1326
|
+
|
|
1327
|
+
Edge cases:
|
|
1328
|
+
- Explicit ARTIFACT_EDIT_REFUSED still parks rejected BEFORE this site and
|
|
1329
|
+
never writes "submitted" — conclusive negative evidence is unchanged.
|
|
1330
|
+
- A throwing trigger-agent call lands here too: the catch swallows the throw
|
|
1331
|
+
because the edit may still have gone through, and this entry is then the
|
|
1332
|
+
recovery's trigger timestamp. It still does not claim the call succeeded.
|
|
1333
|
+
- `recordPublishLedger` is best-effort and non-gating; a write failure does
|
|
1334
|
+
not break the publish flow.
|
|
1335
|
+
- Distinct agent-call keys per write (2026-09-19 REVIEW): the issuance write
|
|
1336
|
+
and the later receipt-bearing "submitted" write execute in the same pass
|
|
1337
|
+
with the same task/outcome/rework key base. The runtime replays cached
|
|
1338
|
+
agent() calls by key, so the two writes pass keyTag "issuance" / "receipt"
|
|
1339
|
+
and the key construction disambiguates — a shared key would silently drop
|
|
1340
|
+
the second write and its ledger line. They are the only same-pass,
|
|
1341
|
+
same-key pair: the other same-outcome writes are mutually exclusive by
|
|
1342
|
+
verdict arm (rebuildTrigger is non-null only in the receipt arm), and the
|
|
1343
|
+
null-safe "unknown" write is guarded by `if (!publishUnknownFields)`.
|
|
1344
|
+
- Deferred vocabulary (2026-09-19 critic review): a distinct outcome word
|
|
1345
|
+
like `trigger-issued` for the issuance write would make `findTriggerEntry`
|
|
1346
|
+
an exact match and give the two writes naturally distinct agent-call keys.
|
|
1347
|
+
Not this release — the vocabulary is frozen across consumers; renaming
|
|
1348
|
+
widens the blast radius for no behavioral gain.
|
|
1349
|
+
- Crash between the issuance write and the observation (accepted risk,
|
|
1350
|
+
2026-09-19 REVIEW): the ledger tail is an issuance-only "submitted" with
|
|
1351
|
+
no "unknown" follow-up — indistinguishable from "observation still
|
|
1352
|
+
running" by the ledger alone, so scan/retry do nothing and the task waits
|
|
1353
|
+
for human attention (fail-closed, safe). A Publish-entry "submitted
|
|
1354
|
+
already exists" guard would change resume semantics; deferred — it needs
|
|
1355
|
+
an explicit call, not smuggling into this release. The hazard it would
|
|
1356
|
+
guard: re-running Publish re-issues the trigger with the same
|
|
1357
|
+
rebuildAttemptKey (keyed by totalReworkCount, unchanged) and writes a
|
|
1358
|
+
second issuance "submitted" for the same attempt key.
|
|
1359
|
+
|
|
1360
|
+
<a id="detached-head-audit"></a>
|
|
1361
|
+
## Detached-HEAD audit (0.14.2, room #22)
|
|
1362
|
+
|
|
1363
|
+
Mandated before room #22 but not performed; performed 2026-09-19 during the
|
|
1364
|
+
0.14.3 BUILD. Scope: lib/worktree-lifecycle.sh, lib/publish-npm.sh,
|
|
1365
|
+
workflows/{standard,bugfix,chore}.js, lib/test-detached-integrate.sh,
|
|
1366
|
+
tests/integrate-reconcile.test.js.
|
|
1367
|
+
|
|
1368
|
+
Findings:
|
|
1369
|
+
|
|
1370
|
+
1. `integration_target()` resolves the branch name, or the literal "HEAD"
|
|
1371
|
+
when detached. Nothing in the lifecycle ever checks out a branch
|
|
1372
|
+
(verified by grep — only comments name the old `checkout main` defect).
|
|
1373
|
+
2. `cmd_integrate` merges into the current checkout — `git merge` advances a
|
|
1374
|
+
detached HEAD directly. The CREW_STAGED_BASE guard (merge-base against
|
|
1375
|
+
HEAD) preserves the reviewed-SHA and crew-init ancestry; the merge record
|
|
1376
|
+
names `integration_target=HEAD`; `verify-merge` checks the record against
|
|
1377
|
+
the LIVE tip (never the record's word for the tip — fixture C).
|
|
1378
|
+
3. **[SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.]**
|
|
1379
|
+
`do_push` on a detached HEAD reports PUSH_SKIPPED and returns 0. This is
|
|
1380
|
+
deliberate policy, not a git limitation: no remote ref exists for the
|
|
1381
|
+
detached line, and pushing `HEAD:main` would claim a branch the room does
|
|
1382
|
+
not own. The workflow's STEP 2 verdict treats PUSH_SKIPPED as PASS with
|
|
1383
|
+
the merge commit + record + VERIFIED as the evidence. This is honest, not
|
|
1384
|
+
hollow: the merge landed (MERGED, 2-parent commit, VERIFIED) — only
|
|
1385
|
+
propagation is skipped, loudly. In Gate 1 rooms the sample project has no
|
|
1386
|
+
origin (fresh `git init` from the tarball), so "detached + remote" cannot
|
|
1387
|
+
arise; fixture A step 9 pins the skip as the expected behavior.
|
|
1388
|
+
4. publish-npm.sh resolves its push destination through the same
|
|
1389
|
+
`integration-target` resolver (2026-09-19 REVIEW removed the old
|
|
1390
|
+
hardcoded `git push origin main`).
|
|
1391
|
+
5. **[SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.]**
|
|
1392
|
+
publish-npm.sh's detached refusal, corrected in place: the refusal itself
|
|
1393
|
+
stands (there is nowhere to push a detached version-bump commit), but it
|
|
1394
|
+
sat at step 13 — AFTER the registry publish — so a detached invocation
|
|
1395
|
+
left a shipped version with a failed phase and a retry loop. Moved to the
|
|
1396
|
+
preflight (PUSH-TARGET-ANCHOR, before any mutation): a detached checkout
|
|
1397
|
+
now fails fast with PUBLISH_FAILED=env and zero side effects. Verified
|
|
1398
|
+
behaviorally: detached → exit 1, no commit, clean tree; branch →
|
|
1399
|
+
proceeds. The npm tarball publish itself was never refused — only the
|
|
1400
|
+
unpushable git record.
|
|
1401
|
+
|
|
1402
|
+
Applies to: lib/publish-npm.sh (preflight), lib/worktree-lifecycle.sh
|
|
1403
|
+
(unchanged — policy confirmed), workflows (unchanged — main STEP 2 verdict
|
|
1404
|
+
wording confirmed accurate; the R5-PUSH line's "nothing to push" wording was
|
|
1405
|
+
corrected separately, 2026-09-19 REVIEW).
|
|
1406
|
+
|
|
1407
|
+
<a id="detached-head-audit-redo"></a>
|
|
1408
|
+
## Detached-HEAD audit REDO (0.14.3)
|
|
1409
|
+
|
|
1410
|
+
Supersedes findings 3 and 5 of the 0.14.2 `#detached-head-audit` above —
|
|
1411
|
+
finding 3's "PUSH_SKIPPED-as-policy is honest" stance and finding 5's
|
|
1412
|
+
"detached npm publication refuses" stance. The 0.14.3 critic panel blessed
|
|
1413
|
+
the old refusal/PUSH_SKIPPED→PASS takes; those dispositions were REJECTED —
|
|
1414
|
+
they contradict the mandatory contract (Eric, 2026-09-19): **a detached
|
|
1415
|
+
HEAD with a remote pushes as explicit `HEAD:<destination>`; detached npm
|
|
1416
|
+
publication works, it does not refuse; fail closed only when the
|
|
1417
|
+
destination is genuinely unknowable; never check out a local branch;
|
|
1418
|
+
preserve the reviewed-SHA → staged-base → crew-init ancestry.** A critic's
|
|
1419
|
+
blessing is checked against the contract, not taken as truth.
|
|
1420
|
+
|
|
1421
|
+
The contract defines the destination as the remote's default branch; the
|
|
1422
|
+
mechanism implements it by reading `refs/remotes/origin/HEAD` (which
|
|
1423
|
+
`git push` never resolves on its own, so the mechanism names it
|
|
1424
|
+
explicitly). The 0.14.2 audit's error: it treated git's no-automatic-ref
|
|
1425
|
+
behavior as a property of the contract ("no remote ref exists for the
|
|
1426
|
+
detached line"), and made policy out of a gap the mechanism was allowed to
|
|
1427
|
+
close. Room repos (the clean-room proof environments) live detached
|
|
1428
|
+
(reviewed SHA + crew-init commits); rooms #18–#22 carried that shape, so a
|
|
1429
|
+
refusal is a refusal of the normal room. Residual fib (2026-09-19 critic
|
|
1430
|
+
review): `refs/remotes/origin/HEAD` is a *cached* answer, not a live one —
|
|
1431
|
+
a renamed remote default branch makes it stale. Accepted residual risk in
|
|
1432
|
+
room-owned remotes (the deliberate no-network-fallback stance).
|
|
1433
|
+
|
|
1434
|
+
Behavior:
|
|
1435
|
+
|
|
1436
|
+
- `lib/worktree-lifecycle.sh`: `resolve_push_destination()` — attached
|
|
1437
|
+
resolves to the branch name; detached resolves to the remote's default
|
|
1438
|
+
branch (bare name) via `refs/remotes/origin/HEAD`; fails closed
|
|
1439
|
+
("genuinely unknowable") when origin/HEAD is unset, and the reason is
|
|
1440
|
+
surfaced (2026-09-19 critic finding F-R1 — the loop must see the
|
|
1441
|
+
remediation hint). `push-destination` is the thin command wrapper.
|
|
1442
|
+
`do_push` runs the NO_REMOTE_PUSH check first (unchanged), then pushes
|
|
1443
|
+
the explicit refspec — `<branch>` attached, `HEAD:<dest>` detached —
|
|
1444
|
+
printing `PUSHED: origin/<dest> (refspec <refspec>)`; the reconcile path
|
|
1445
|
+
uses `$dest`. An unknowable destination fails closed — never a silent
|
|
1446
|
+
skip, never a refusal of a knowable push. PUSH_SKIPPED means exactly one
|
|
1447
|
+
thing: no push attempted (no merge record + no lock held, the R5
|
|
1448
|
+
manual path only).
|
|
1449
|
+
- `lib/publish-npm.sh`: the PUSH-TARGET-ANCHOR resolves `PUSH_REFSPEC` in
|
|
1450
|
+
the preflight, before any mutation; step 13 pushes it (never a bare
|
|
1451
|
+
HEAD). Detached + resolvable origin passes through; no origin at all
|
|
1452
|
+
(attached or detached) fails fast with `PUBLISH_FAILED=env` before any
|
|
1453
|
+
mutation (2026-09-19 critic finding F-A1); detached + origin with no
|
|
1454
|
+
origin/HEAD fails fast the same way, repo untouched.
|
|
1455
|
+
- workflows/{standard,bugfix,chore}.js: STEP-2 + R5-PUSH wording —
|
|
1456
|
+
PUSH_SKIPPED names only the nothing-merged path ("no push attempted (no
|
|
1457
|
+
record + no lock)"), and both PUSHED clauses name the detached form (a
|
|
1458
|
+
detached checkout's line prints e.g. `PUSHED: origin/main (refspec
|
|
1459
|
+
HEAD:main)` — that is the normal expected form).
|
|
1460
|
+
- Tests: fixture H in `lib/test-detached-integrate.sh` and cases 6/7 in
|
|
1461
|
+
`lib/test-publish-preflight.sh` pin the contract behaviorally.
|
|
1462
|
+
|
|
1463
|
+
No force-push anywhere; nothing in the lifecycle checks out a branch.
|
|
1464
|
+
|
|
1465
|
+
Applies to: lib/worktree-lifecycle.sh (resolve_push_destination,
|
|
1466
|
+
push-destination, do_push refspec), lib/publish-npm.sh
|
|
1467
|
+
(PUSH-TARGET-ANCHOR refspec), workflows/{standard,bugfix,chore}.js
|
|
1468
|
+
(STEP-2/R5-PUSH wording), lib/test-detached-integrate.sh (fixture H),
|
|
1469
|
+
lib/test-publish-preflight.sh (cases 6/7).
|
|
@@ -273,8 +273,11 @@ For a task parked with `publish: verification-requested <commit>`:
|
|
|
273
273
|
1. **Expected** = the agent_id in the park message's `(build …)` suffix.
|
|
274
274
|
If the suffix says `agent_id unobserved`, look up the workflow's
|
|
275
275
|
durable publish ledger at `$CREW_HOME/.publish-ledger/<slug>.jsonl`
|
|
276
|
-
for the `submitted`
|
|
277
|
-
|
|
276
|
+
for the `submitted` entries with this `<commit>`: the OLDEST is the
|
|
277
|
+
trigger issuance (its `agent_id` is always null — the trigger instant,
|
|
278
|
+
not a build identity); the receipt-bearing `submitted`, if present, is
|
|
279
|
+
a LATER entry carrying the observed `agent_id` — use that one here
|
|
280
|
+
(if it is absent or null, this step is vacuous).
|
|
278
281
|
2. **Live** = whether the expected agent_id appears anywhere in the
|
|
279
282
|
read-back result (the live artifact status exposes no durable
|
|
280
283
|
agent_id — only an in-flight correlation ID that expires with the
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Critic review — detached-HEAD audit REDO + submitted-on-issuance (0.14.3)
|
|
2
|
+
|
|
3
|
+
Change set: uncommitted, on top of `d619f2a` (545 insertions, 138 deletions, 18 files). Full suite green on this tree (EXIT=0); `test-publish-preflight.sh` 12/12 incl. the two new detached cases.
|
|
4
|
+
|
|
5
|
+
Contract checked against (Eric, 2026-09-19): a detached HEAD with a remote pushes as explicit `HEAD:<destination>`; destination = the remote's default branch via refs/remotes/origin/HEAD — the only in-repo mechanical answer; detached npm publication works, never refuses; fail closed only when the destination is genuinely unknowable; never check out a local branch; never force-push; preserve reviewed-SHA → staged-base → crew-init ancestry. The prior panel's blessing of the old refusal / PUSH_SKIPPED→PASS policy was REJECTED against this contract; every disposition below is checked against the contract, not the old policy.
|
|
6
|
+
|
|
7
|
+
## Verdicts
|
|
8
|
+
|
|
9
|
+
**Architect: CONCERN** — resolver matches the contract on every detached path traced; one real mechanism gap keeps it from ACCEPT.
|
|
10
|
+
|
|
11
|
+
- F-A1 (moderate) — `lib/publish-npm.sh`: the anchor's no-origin check is nested inside the detached branch only (:101–103). An attached checkout with no origin resolves `PUSH_REFSPEC=<branch>` cleanly in the preflight, then step 13 pushes directly (`git push origin "$PUSH_REFSPEC"`, :311, no `do_push`, no NO_REMOTE_PUSH soft path) and fails `PUBLISH_FAILED=push` *after* the version-bump commit (step 8) and the registry publish (step 11) — the "shipped version with a failed phase and a retry loop" the REDO moved to the preflight for detached, still live for attached. Violates the anchor's own rule: "never after a side effect you cannot roll back."
|
|
12
|
+
- F-A2 (low) — `docs/decisions/publish-path.md#submitted-on-issuance`: "Other ledger writes in the pass are each followed by an immediate park/return, so they cannot collide" is false — the `unknown` write continues to post-deploy. The code is safe via a finer mechanism (verdict arms mutually exclusive; the null-safe `unknown` write guarded by `if (!publishUnknownFields)`); the doc's stated mechanism is wrong and this codebase treats decision docs as load-bearing.
|
|
13
|
+
|
|
14
|
+
**Subtractor: ACCEPT** — with the cuts below; nothing is hollow, cuts are prose and duplicated statements, no behavior.
|
|
15
|
+
|
|
16
|
+
- F-S1 — Flag the superseded findings inline: `#detached-head-audit` findings 3 ("PUSH_SKIPPED-as-policy is honest, not hollow") and 5 ("the refusal itself stands") read as live policy to anyone who stops there; the "Supersedes findings 3 and 5" sentence sits one section down. Findings 1, 2, 4 stay unflagged (still true).
|
|
17
|
+
- F-S2 — Cut process narration: REDO "Behavior" workflows bullet (`the "room-local" / "declined by policy" language is deleted`; `Net deletions (workflow size budget holds)` — keep only `PUSH_SKIPPED names only the nothing-merged path`); delete the submitted-on-issuance final paragraph entirely (narration about call-site placement and size budget); compress the fixture bullet to one sentence.
|
|
18
|
+
- F-S3 — `lib/worktree-lifecycle.sh:371–375`: the inline comment restates the `do_push` header comment nearly verbatim. Cut to one line (`# Attached: branch name; detached: remote default branch via resolve_push_destination().`).
|
|
19
|
+
- F-S4 — `lib/verify-publish.js:317–318`: duplicate of the block comment at :298–301. Delete the two inner lines.
|
|
20
|
+
- F-S5 (deferred, not a cut now) — A distinct outcome word (`trigger-issued`) for the issuance write would delete both the three-meanings doc and the keyTag parameter; rejected for this release — renaming now widens the blast radius for no behavioral gain. One line in the doc records the deferred shape.
|
|
21
|
+
- F-S6 (info) — keyTag is load-bearing and the pair is the only one: checked all `recordPublishLedger` call sites; every other same-outcome write is mutually exclusive by verdict arm or immediately parked. Honest fix under the frozen vocabulary.
|
|
22
|
+
- F-S7 (weak, optional) — the preflight calls the lifecycle twice; the explicit `remote get-url` check is subsumed by push-destination's unknowable branch. Not worth the churn; the more precise no-origin error message is worth one subprocess spawn.
|
|
23
|
+
|
|
24
|
+
**Reliability: CONCERN** — mechanism-level fail-closed holds on all paths; concerns are diagnosability and stale rationale.
|
|
25
|
+
|
|
26
|
+
- F-R1 (medium) — `lib/worktree-lifecycle.sh:377`: `dest=$(resolve_push_destination) || return 1` swallows the resolver's reason into `$dest` and discards it. Callers then misreport: `:542` prints `"ERROR: push failed after merge ... retry will recover the push"` — false for unknowable destinations (retry hits the same unknowable destination; only human `git remote set-head origin -a` recovers). `cmd_push_target` (:899/:914) exits 1 with no reason at all. Fail-closed holds (exit 1, no push), but the autonomous loop loses the only remediation hint and gets a false recovery promise.
|
|
27
|
+
- F-R2 (low) — `lib/worktree-lifecycle.sh:502–506` and `lib/AGENTS.md` (NO_REMOTE_RECONCILE line): `"A detached HEAD is local by design (no remote ref is claimed for the detached line), so there is nothing to reconcile."` — the exact rationale the REDO rejected. The skip itself is fine (do_push reconciles lazily on failure); the rationale contradicts the contract and will mislead readers that treat comments as policy.
|
|
28
|
+
- F-R3 (low) — `lib/worktree-lifecycle.sh:345`: the `do_push` header still claims it prints PUSH_SKIPPED; only `cmd_push_target:902` emits it. Same root as F-F1.
|
|
29
|
+
- F-R4 (low) — `lib/publish-npm.sh:97,104`: `bash "${LIFECYCLE:?LIFECYCLE is required}"` under `set -u` dies during word expansion with a raw bash error before `|| fail` is reached — no `PUBLISH_FAILED=<reason>` line, violating the script's header contract. Fail-closed (no mutation before :108), but the marker contract is the script's own.
|
|
30
|
+
- F-R5 (info) — No network fallback (`git ls-remote --symref`); deliberate. Over-conservative fail-closed matches the standing bias. Fixture fidelity confirmed: `crew-standup-room.sh` clones (clone sets origin/HEAD), so H2–H5 model the representative path; H1/case-7 cover the non-clone shape.
|
|
31
|
+
- F-R6 (info) — Reconcile merge on detached preserves ancestry (ancestor-based guards); incorporating origin/`<dest>`'s tip is required by the contract (fast-forward the remote, no force-push) and announced in the merge message.
|
|
32
|
+
- F-R7 (info) — NO_REMOTE_PUSH preserved for attached and detached in `do_push`/`push-target` (checked before target resolution, return 0). The npm-side divergence (detached+no-origin fails hard `PUBLISH_FAILED=env`) is correct: step 13 unconditionally pushes, while Integrate's merge is the deliverable.
|
|
33
|
+
- F-R8 (info) — The deferred "submitted already exists" guard is an honest deferral: oldest-binds across all three consumers neutralizes misanchoring.
|
|
34
|
+
- F-R9 (info) — DRY_RUN now fails `PUBLISH_FAILED=env` on unknowable destinations instead of printing `PUBLISH_DRY_RUN=1` (case 7 pins this): sanctioned by "fail closed only when genuinely unknowable." The DRY_RUN comment should note the preflight now runs first.
|
|
35
|
+
|
|
36
|
+
**First-Time User: CONCERN** — text is honest; misreading hazards are real and fixable.
|
|
37
|
+
|
|
38
|
+
- F-F1 — STEP-2 line (`workflows/standard.js:1188`, ×3): `"PUSH_SKIPPED — nothing merged (no record, no lock), VERDICT: PASS"` sits under `"- MERGED: the merge landed; the script pushed inline. Read the push line:"` — but PUSH_SKIPPED is unreachable on the inline path (`cmd_integrate` calls `do_push` directly; only `cmd_push_target` emits it). And `"nothing merged"` collides with the adjacent `"merged empty: no repo changes"` (MERGED_EMPTY).
|
|
39
|
+
- F-F2 — R5-PUSH line (:1201): `"PUSHED — report the merged hash, VERDICT: PASS"` never names the now-normal detached form `PUSHED: origin/main (refspec HEAD:main)`, and the line prints no hash.
|
|
40
|
+
- F-F3 — publish-path.md:1405: `"Supersedes findings 3 and 5 of the 0.14.2 #detached-head-audit above."` — an anchor-link landing names the findings but doesn't summarize them; the stranger must read the whole old section to learn what changed.
|
|
41
|
+
- F-F4 — :1415–1417: presents the destination as a mechanical discovery, but the contract quoted above already defines it. The mechanism implements a mandated answer, it doesn't derive it.
|
|
42
|
+
- F-F5 — :1418: `"The 0.14.2 audit's fib:"` — "fib" is insider shorthand opaque to a stranger.
|
|
43
|
+
- F-F6 — PUSH-TARGET-ANCHOR comment doesn't explain why npm is stricter on no-remote than the integrate path (fail-closed vs fail-soft) — the contract says "fail closed only when genuinely unknowable," and a stranger can't tell whether detached+no-origin is "unknowable" or just "absent."
|
|
44
|
+
- F-F7 — `"Room"` is never defined in the decision file; the stranger meets it four times with no gloss.
|
|
45
|
+
|
|
46
|
+
Submitted-on-issuance vocabulary: ACCEPT — the (a)/(b)/(c) list is a mechanical discriminator (discriminate on `agent_id`/`workflow`, never the word alone), and the doc tells the reader which meaning to use when reading a ledger.
|
|
47
|
+
|
|
48
|
+
## Step-back round (run by the coordinator, not delegated)
|
|
49
|
+
|
|
50
|
+
Question: is there a better way altogether — should detached work ever be pushed to origin by name, or should rooms always publish onto a branch? Is the destination-resolution question itself an arrogance?
|
|
51
|
+
|
|
52
|
+
Answer: the current shape stands. The candidates:
|
|
53
|
+
|
|
54
|
+
- **Branch-publishing** (check out a branch, push it): forbidden by the contract, and it would reintroduce the room #21 root cause — branch-checkout confusion between the integration line and the checkout. Detached is the architecture.
|
|
55
|
+
- **Per-room remote branches** (`room/<id>`): invents a naming convention the crew would have to track, and the remote's integration target would no longer carry the line — verify-merge, "remote ref advances" evidence, and downstream room consumers all assume the line is visible at the remote integration target.
|
|
56
|
+
- **No push at all** (old policy): rejected by the contract; hid merged work.
|
|
57
|
+
|
|
58
|
+
The destination question is not a new arrogance: the attached same-name push already asks "who guarantees origin/<branch> is the intended target?" — it's convention in both cases. The repo's integration target is the current checkout (room #21 J3); the remote's integration target is origin/HEAD; the refspec connects the two, making explicit where `git push` refuses to guess.
|
|
59
|
+
|
|
60
|
+
The residual fib: **refs/remotes/origin/HEAD is a cached answer, not a live one.** If the remote's default branch is renamed after clone, the resolver pushes to a stale name — creating or updating a dead branch on the remote. Accepted residual risk (room-owned remotes; F-R5's deliberate no-network stance), but worth one line in the decision doc so the next reader doesn't treat the cache as truth.
|
|
61
|
+
|
|
62
|
+
## Dispositions
|
|
63
|
+
|
|
64
|
+
Accepted (apply before PUBLISH):
|
|
65
|
+
|
|
66
|
+
1. **F-A1** — Hoist the no-origin check out of the detached branch in the PUSH-TARGET-ANCHOR: `fail "env"` pre-mutation whenever origin is absent, attached or detached. Rationale: step 13 pushes unconditionally; the anchor's own "never after a side effect you cannot roll back" rule applies to both shapes. (Compatible with F-R7: integrate keeps fail-soft NO_REMOTE_PUSH; npm stays strict.)
|
|
67
|
+
2. **F-R1** — Surface the unknowable reason: `dest=$(resolve_push_destination) || { echo "$dest"; return 1; }` (or emit on stderr); fix the `:542` retry-promise message to not promise recovery for the unknowable mode; ensure `cmd_push_target` doesn't exit silently.
|
|
68
|
+
3. **F-R2** — Reword the stale rationale at `worktree-lifecycle.sh:502–506` and the `lib/AGENTS.md` NO_REMOTE_RECONCILE line in mechanical terms (the skip is fine — do_push reconciles lazily — but "local by design" is the rejected rationale).
|
|
69
|
+
4. **F-R3/F-F1** — Remove PUSH_SKIPPED from the `do_push` header print list; drop PUSH_SKIPPED from the STEP-2 inline line (it belongs only to R5-PUSH); reword the R5-PUSH gloss to `"PUSH_SKIPPED — no push attempted (no merge record + no lock held)"` so it can't be confused with MERGED_EMPTY's "merged empty."
|
|
70
|
+
5. **F-F2** — Name the detached form in both PUSHED clauses: `"(a detached checkout prints e.g. PUSHED: origin/main (refspec HEAD:main) — that is the normal expected form)"`.
|
|
71
|
+
6. **F-F6** — Add to the PUSH-TARGET-ANCHOR comment: `"A version-bump commit with nowhere to record it would ship an npm version with no git provenance, so no-remote fails closed here rather than skipping like the integrate path."`
|
|
72
|
+
7. **F-F3** — Expand the supersedes sentence: `"Supersedes findings 3 and 5 of the 0.14.2 #detached-head-audit above — finding 3's 'PUSH_SKIPPED-as-policy is honest' stance and finding 5's 'detached npm publication refuses' stance."`
|
|
73
|
+
8. **F-F4** — Reword: `"The contract defines the destination as the remote's default branch; the mechanism implements it by reading refs/remotes/origin/HEAD (which git push never resolves on its own, so the mechanism names it explicitly)."`
|
|
74
|
+
9. **F-F5** — `"The 0.14.2 audit's fib:"` → `"The 0.14.2 audit's error:"`.
|
|
75
|
+
10. **F-F7** — First use: `"Room repos (the clean-room proof environments) live detached ..."`.
|
|
76
|
+
11. **F-S1** — Prefix findings 3 and 5 of the preserved 0.14.2 section with `**SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.**` (findings 1, 2, 4 stay unflagged; the section stays as alignment archeology).
|
|
77
|
+
12. **F-S2** — Cut the two process-narration paragraphs; compress the fixture bullet to: `fixture H (test-detached-integrate.sh) and cases 6/7 (test-publish-preflight.sh) pin the contract behaviorally.`
|
|
78
|
+
13. **F-S3** — Cut the `do_push` inline comment to one line; the header owns the contract statement.
|
|
79
|
+
14. **F-S4** — Delete the duplicate inner comment lines in `lib/verify-publish.js:317–318`.
|
|
80
|
+
15. **F-A2** — Correct the doc's keyTag mechanism sentence to the real one: other same-outcome writes are mutually exclusive by verdict arm (rebuildTrigger non-null only in the receipt arm) and the null-safe `unknown` write is guarded by `if (!publishUnknownFields)`; the issuance/receipt pair is the only same-pass, same-key pair.
|
|
81
|
+
16. **F-R4** — Add an explicit markered LIFECYCLE check ahead of the anchor (e.g. `: "${LIFECYCLE:?}"` early, or route through `fail "env"`) so the script's `PUBLISH_FAILED=<reason>` header contract holds.
|
|
82
|
+
17. **F-R9** — Note in the DRY_RUN comment that the preflight now runs first (DRY_RUN requires a push-resolvable environment).
|
|
83
|
+
|
|
84
|
+
Rejected / deferred (with reason):
|
|
85
|
+
|
|
86
|
+
- **F-S5** (distinct outcome word for issuance) — deferred, not this release. The vocabulary is frozen across consumers; renaming now widens the blast radius for no behavioral gain. Record the one-line deferred shape in the doc: `trigger-issued` would make `findTriggerEntry` an exact match and give the two writes naturally distinct agent-call keys.
|
|
87
|
+
- **F-S7** (collapse the double lifecycle call) — rejected: the explicit no-origin branch's more precise error message is worth one subprocess spawn; not worth the churn.
|
|
88
|
+
- **F-R5** (network fallback for origin/HEAD) — rejected: the contract sanctions "the only in-repo mechanical answer"; a network call is outside it, and over-conservative fail-closed is the standing bias.
|
|
89
|
+
- **F-R6** (reconcile incorporates foreign line) — accepted as information, no change: inherent to the contract (fast-forward the remote, never force-push), same semantics as the attached path, announced in the merge message.
|
|
90
|
+
- **F-S6** (keyTag) — confirmed load-bearing, no change.
|
|
91
|
+
- **F-R8** (deferred "submitted already exists" guard) — honest deferral stands; oldest-binds neutralizes misanchoring.
|
|
92
|
+
- **Step-back alternative (branch publishing / per-room branches / no push)** — rejected per the contract and room #21's root cause, recorded above.
|
|
93
|
+
|
|
94
|
+
Panel consensus: no REJECT anywhere; no fail-open path found by any critic. After the 17 accepted fixes are applied and the suite re-run green, this change set is publishable.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Critic review — prospective release 0.14.3
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-19. **Scope:** uncommitted diff in `~/workspace/muse-crew` (submitted-on-issuance fix for the room #22 Publish-tail defect, detached-HEAD audit follow-up in `lib/publish-npm.sh`, byte-tightening, test updates). **Method:** four parallel critic subagents (Architect, Subtractor, Reliability, Room Operator), read-only; coordinator spot-verified the two most load-bearing claims (`keyTag` dead code, oldest/newest anchor divergence) against the code before dispositioning. Full suite green (EXIT=0, 571 assertions) before review.
|
|
4
|
+
|
|
5
|
+
**Overall verdict: CONCERN.** All four critics returned CONCERN; none returned REJECT. No critic could construct a fail-open path — the core mechanism ("submitted = invoked with no refusal observed; verifier binds oldest; every gate still requires a real new build plus read-back") holds up adversarially. But the mechanism does not do what its own comments and decision doc claim in several places, and the ledger vocabulary now has three meanings under one word with none of them written down in one place. Fix the ACCEPTED items below, then ship. Nothing here requires redesigning the change.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Architect — CONCERN
|
|
10
|
+
|
|
11
|
+
**A1 (strong). `keyTag` is dead code; the two same-pass `submitted` writes share one agent-call key.** `recordPublishLedger(entry, rework, keyTag)` accepts `keyTag` (`workflows/standard.js:435`; bugfix `:423`; chore `:482`) and the comment at `standard.js:431–434` states the key "must be unique per write within a pass, or a platform call cache could silently drop the second write without appending its line" — but the key is built as `attemptKey("publish-ledger-" + taskId + "-" + entry.outcome, rework)` (`standard.js:457`), ignoring `keyTag`. Zero call sites pass a third argument. This change creates the first same-pass duplicate-outcome pair (issuance `submitted` at `standard.js:1563`, receipt-bearing `submitted` at `:1684`); per the repo's own incident record (bug 1b8bb875, `tests/replay-key.test.js:9`) a reused explicit key trips the runtime's duplicate-key constraint, and the throw is swallowed by `recordPublishLedger`'s catch (`standard.js:463–466`) — so the failure mode is a **silently lost receipt-bearing line**, including the only ledger record of the receipt `agent_id`. Coordinator verified: parameter present, unused in key, no 3-arg callers.
|
|
12
|
+
|
|
13
|
+
**A2 (moderate). Verifier binds oldest, scan binds newest — divergent now that two `submitted` entries exist.** `lib/verify-publish.js:318–319` sorts oldest-first and takes `candidates[0]` (the issuance entry). `findTriggerEntry` (`lib/crew-api.js:1729–1740`) iterates from the end — newest-first — while its comment claims it returns the entry "recorded when the trigger went out." Pre-change at most one `submitted` existed per attempt, so the divergence was latent; the decision doc blesses only the oldest rule. Reachable misclassification: receipt captured → poll inconclusive → unknown parked → scan binds the receipt-bearing entry (T1) → classifier computes `manifestAdvanced = built_at > T1`; a build completing in (T0, T1] reads as stale → `applied-not-built` (terminal, no retry) where the T0 anchor would yield `verified`. Coordinator verified the newest-first loop at `crew-api.js:1731`.
|
|
14
|
+
|
|
15
|
+
**A3 (minor). Thrown vs returned trigger are indistinguishable in the durable ledger.** Both paths land the identical entry — same fields, same constant `detail`. The disposition exists only in unstructured `log()` lines. No machine consumer needs the distinction, but a human investigator cannot tell from the ledger whether the trigger call succeeded or threw. The test pins ("trigger issuance" + "receipt/observation pending" present; "receipt captured"/"build went through" absent) allow appending a disposition token.
|
|
16
|
+
|
|
17
|
+
**Checked and clean:** composition with all consumers (`verify-publish.js` §5b reads only `ts` + `manifest_before.content_sha256` — both present on the issuance entry; `scan-publish-unknown`/`findTriggerEntry`/`retry-publish.js` read `trigger.ts`/`manifest_before` — both entries carry both; `classify-publish-absence.js` receives issuance values in receipt-less cases, exactly the intended fix; `applied_report` is write-only with constant value `"missing-report"`, so the name cannot mislead); `rejected`/`submitted` exclusivity (every `rejected` write returns before the issuance site: `standard.js:1295, 1322, 1501, 1512, 1548–1556`; the issuance write is unconditional after the try/catch and cannot throw past the site).
|
|
18
|
+
|
|
19
|
+
## 2. Subtractor — CONCERN
|
|
20
|
+
|
|
21
|
+
**S1. The issuance comment restates its own code and detail string.** `workflows/standard.js:1561–1562` (identical in bugfix/chore): "every non-refused trigger lands `submitted`" duplicates `outcome: "submitted"` two lines below; "invoked, no refusal" duplicates the `detail` string. Only the contract name/date/room earn their keep. Delete the second sentence (also frees bytes — see implementation constraints).
|
|
22
|
+
|
|
23
|
+
**S2. The R5-PUSH verdict wording is not accurate, and the audit claims it is.** `workflows/standard.js:1202`: `"PUSH_SKIPPED or NO_REMOTE_PUSH — nothing to push, VERDICT: PASS"`. For detached-with-remote there *is* a commit; the policy is "we choose not to push the detached line" (`lib/worktree-lifecycle.sh:334–341`, whose own comment is more honest than the workflow text). "Nothing to push" collapses PUSH_SKIPPED with NO_REMOTE_PUSH and erases the loud skip/pass distinction the audit celebrates. The audit's "STEP 2 verdict wording confirmed accurate" is overbroad — scope it to the main STEP 2 path (`standard.js:1189`, which does distinguish via its parenthetical). Policy itself stands: PASS-with-loud-skip is defensible over refuse/park (in Gate 1 rooms the sample project has no origin; a park would stall the room on an unresolvable human decision for a merge that genuinely landed).
|
|
24
|
+
|
|
25
|
+
**S3. `PUSH_SKIPPED` is an overloaded token; the verdict template assumes one meaning.** `lib/worktree-lifecycle.sh:342` = "detached HEAD — declined by policy"; `:862` = "no merge record and no lock held — nothing was merged." The STEP 2 template (`standard.js:1189`) reads PUSH_SKIPPED unconditionally as "the detached line is room-local (the merge commit + record are the evidence)" — in the `:862` case there is no merge commit and no record. Reachable only via push-target/R5 past MERGED_EMPTY, so blast radius is small, but two meanings behind one token is the kind of ambiguity this codebase refuses.
|
|
26
|
+
|
|
27
|
+
**S4. `docs/publish-verification.md` §4b.1 went stale inside this change.** Line 276 says to use "the `submitted` entry with this `<commit>`" and its `agent_id` field ("it may still be null — then this step is vacuous"). Under submitted-on-issuance the oldest matching `submitted` is the issuance entry, whose `agent_id` is *always* null — a literal reading now makes build-ID correlation permanently vacuous, missing the later receipt-bearing entry that carries the observed agent_id. Fix: say "the receipt-bearing `submitted` entry" and distinguish oldest from latest.
|
|
28
|
+
|
|
29
|
+
**S5 (minor). Step 13's comment half-restates the preflight.** `lib/publish-npm.sh:300–302`: the first sentence ("PUSH_TARGET was resolved in the preflight above") is load-bearing cross-reference; the second restates the PUSH-TARGET-ANCHOR contract. Trim the second (frees bytes). Verified: resolution exists in exactly one place (`:95`), consumed at `:303`; the anchor sits after PREFLIGHT-END before DRY_RUN with only read-only probes ahead — "before ANY mutation" holds. The split is justified (resolution+refusal pre-mutation, push post-verify); merging recreates the original bug or a premature push.
|
|
30
|
+
|
|
31
|
+
**Checked:** no test in `tests/publish-submitted-on-issuance.test.js` is exactly redundant with an existing test (closest overlaps are subsumed-but-not-duplicated); no doc duplication (the detached-HEAD audit section is genuinely new material).
|
|
32
|
+
|
|
33
|
+
## 3. Reliability — CONCERN
|
|
34
|
+
|
|
35
|
+
**R1. Same as A1** (`keyTag` dead code). Independent corroboration — treat as one finding (F1).
|
|
36
|
+
|
|
37
|
+
**R2. Same as A2** (anchor divergence), plus: the verifier's own comment (`lib/verify-publish.js:302` and `:317`) claims "audit observations are now `build-observed`, not `submitted`" — factually wrong while the receipt path still writes a second `outcome: "submitted"`. Fix or remove the stale comment.
|
|
38
|
+
|
|
39
|
+
**R3 (benign, verified).** The verifier's field selection handles the issuance entry correctly: only `e.ts` and `e.manifest_before` are read (`verify-publish.js:320–321`); null `agent_id` irrelevant; null `manifest_before` fails closed at `:329–330`; 1-second timestamp ties resolve by stable sort → issuance first. No action.
|
|
40
|
+
|
|
41
|
+
**R4 (benign).** Thrown-trigger anchor timestamp: stamped by the ledger-write child *after* the trigger call settled, so "build before anchor" reads as stale → `unverifiable-content`/`ambiguous`/`applied-not-built`/`provably-dropped` — all non-stamping. No false pass. One genuine weakening to name: the anchor moved *earlier* than the old receipt-time anchor, widening the `built_at > trigger` window — bounded by the traveling stronger gates (`content_sha256` must differ from the pre-trigger baseline; read-back must match the diff). The decision doc does not acknowledge this relaxation. No action beyond noting it here.
|
|
42
|
+
|
|
43
|
+
**R5 (benign).** No fail-open found. The newly-enabled recovery path is strictly stronger than the old fallback: previously a receipt-less unknown fell back to `--trigger-ts` = park-note timestamp with no `--manifest-before`, and the classifier's `verified` branch ran on time-only freshness; now the issuance entry supplies a real baseline enabling the strict sha-identity branch. The "no submitted ledger entry" fail-closed (`verify-publish.js:327`) still fires exactly where it should (explicit refusal, pre-trigger parks, issuance-write failure). No action.
|
|
44
|
+
|
|
45
|
+
**R6. Crash between issuance write and observation leaves recovery with nothing to act on (pre-existing hazard, newly visible).** Ledger tail is an issuance-only `submitted` with no `unknown` follow-up — indistinguishable from "observation still running" by the ledger alone. `derivePublishUnknownAttempt` requires an unresolved `unknown` entry, so scan/retry do nothing; the task stays parked for human attention (fail-closed, safe). The asymmetry: the change's purpose is giving recovery a trigger anchor, yet the crash-during-observation window — the most likely crash to need recovery — still has no recovery path, because the anchor is only reachable via the `unknown` entry that was never written. Worse, the actual recovery path (workflow resume re-running Publish from the top) re-issues the trigger with the **same** `rebuildAttemptKey` (keyed by `totalReworkCount`, unchanged) — the pre-existing duplicate-edit hazard from 2026-09-12 — and writes a **second** issuance `submitted` for the same attempt key, which now looks intentional. No "submitted already exists for this attempt" guard at the Publish entry.
|
|
46
|
+
|
|
47
|
+
## 4. Room Operator — CONCERN
|
|
48
|
+
|
|
49
|
+
**O1/O7. The detail string actively negates but never positively states its claim; the double negative dies under fatigue.** `workflows/standard.js:1570`: `"trigger issuance: rebuild-trigger agent invoked, no refusal — receipt/observation pending; asserts neither artifact_edit nor build acceptance"`. A 2am skim lands on `outcome: "submitted"` + "no refusal" — one saccade from "accepted." "Asserts neither X nor Y" requires holding two negations to recover the actual claim. Proposed replacement (pins-safe: keeps "trigger issuance" + "receipt/observation pending"; avoids "receipt captured"/"build went through"): `"trigger issuance: rebuild-trigger agent invoked (no explicit refusal observed); receipt/observation pending. This entry proves ONLY invocation — it is not evidence that artifact_edit ran or that any build was accepted."`
|
|
50
|
+
|
|
51
|
+
**O2. The decision doc's History is factually incomplete.** `docs/decisions/publish-path.md:1293` claims "the old contract wrote `submitted` only beside a captured receipt agent_id." Wrong: `lib/retry-publish.js:349` (pre-existing, untouched) already writes `outcome: "submitted"`, `agent_id: null`, `workflow: "unknown-recovery"`, detail `"retry trigger re-issued after provably-dropped classification"` — asserting something *stronger* than the new issuance entry. After this change, `submitted` + null `agent_id` has two meanings distinguished only by the `workflow` field, and the doc never says so. The "one broken contract in three workflow files" narrative (`publish-path.md:1298`) is overstated: the recovery writer already knew how to write issuance-without-receipt; the workflow writers were the gap. Coordinator verified `retry-publish.js:349`.
|
|
52
|
+
|
|
53
|
+
**O3. No read-site comment warns about `agent_id: null`; both ledger readers ignore `agent_id` entirely.** `lib/verify-publish.js` has zero references to `agent_id`; `lib/retry-publish.js:185` filters on `outcome`/`commit`/`attempt` only. The three-way ambiguity of `submitted` (issuance-null / receipt-bearing / recovery-reissue-null) is documented at zero read sites.
|
|
54
|
+
|
|
55
|
+
**O4. Same as A2/R2** (oldest/newest binding). Independent corroboration.
|
|
56
|
+
|
|
57
|
+
**O5. The decision section does not give the full outcome vocabulary in one place.** `docs/decisions/publish-path.md:1301–1306` lists issuance `submitted`, receipt-bearing `submitted`, `build-observed`, `unknown`, `failed` — but `rejected` appears only in Edge cases with no definition (from the code it is conclusive negative evidence: no trigger issued, park, human attention — the one outcome where "parked" is *terminal*); the receipt-bearing `submitted` is named but never defined in the section; `unknown` is listed but not defined; and nowhere is the **triple meaning of `submitted`** enumerated: (a) issuance-only, null agent_id, workflow writer; (b) receipt-bearing, non-null agent_id, workflow writer; (c) re-issue after provably-dropped, null agent_id, `unknown-recovery` writer.
|
|
58
|
+
|
|
59
|
+
**O6. The old receipt-bearing `submitted` detail now reads as the strong claim under a weakened word.** `workflows/standard.js:1691` (pre-existing): `"fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff)"`. An operator scanning two `submitted` lines reads the second as the stronger confirmation of the first — backwards from the intended semantics (the first is the anchor; the second is just another observation). Symmetric honesty wants a scope clause on this line too (careful: `publish-ledger.test.js` pins a whole-block substring here — append, don't rewrite).
|
|
60
|
+
|
|
61
|
+
**O8. The detached-HEAD preflight message diagnoses but does not prescribe.** `lib/publish-npm.sh:98`: `"detached HEAD: publish-npm cannot push a version-bump commit from a detached checkout — no remote ref exists for the detached line"`. As an operator seeing `PUBLISH_FAILED=env`: no statement that nothing was mutated, no next step, no retryability signal. Proposed append: `" — no version-bump commit was created and nothing was published; attach the checkout to a branch (git checkout <branch>) and re-run."` (`test-publish-preflight.sh` pins only markers, not message text — safe.)
|
|
62
|
+
|
|
63
|
+
**Checked and sound:** issuance call-site position genuinely after the trigger try/catch and before observation; explicit-refusal path parks `rejected` before the issuance site (no double-record); the new test's structural coverage (position, fields, refusal, first-submitted ordering, symmetry) is the right shape. Note: the pre-existing `rejected` writers already used `agent_id: null`, so null agent_id was never the novelty — only its pairing with `submitted` in the *workflow* was, which makes O2's doc fix the thing to repair, not the code.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 5. Step-back round (coordinator)
|
|
68
|
+
|
|
69
|
+
**The question the panel didn't ask:** *Should the issuance entry have been a new outcome value (e.g. `trigger-issued`) instead of overloading `submitted` — accepting the cost of updating every consumer filter for an unambiguous vocabulary?*
|
|
70
|
+
|
|
71
|
+
**Honest answer:** The overload was load-bearing, not lazy. Four consumers key anchor-finding on `outcome === "submitted"`: `verify-publish.js`'s oldest-match filter, `crew-api.js` `findTriggerEntry`, `retry-publish.js`'s trigger-entry lookup, and the "no submitted ledger entry for commit" fail-closed that was itself the room #22 park reason. A new outcome value would have required touching all four filters plus their tests, and any missed consumer would silently reintroduce the exact gap this change closes (anchor not found → fail closed → parked room). The overload guarantees every existing anchor-reader finds the anchor on day one. The cost — three meanings of `submitted` — is concentrated in *human* legibility, which is repairable with prose (the vocabulary enumeration in F5/F11 below) without touching the machine contract. The one durable consequence: any future consumer that needs "was a receipt observed" must discriminate on `agent_id`/`workflow` fields, never on the outcome word — that rule belongs in the vocabulary doc. **No design change.** The overload stands; the documentation repair is the disposition.
|
|
72
|
+
|
|
73
|
+
(Considered and rejected as the step-back: moving the anchor off the agent-written ledger into workflow-owned state. The workflow scripts cannot read the wall clock — `tests/determinism.test.js` bans it — so the ledger child's `date -u` stamp is the only clock available. The current placement is forced by that constraint, not chosen.)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 6. Synthesis — explicit dispositions
|
|
78
|
+
|
|
79
|
+
Deduped across critics. **ACCEPTED** = fix before 0.14.3 ships. **DEFERRED** = acknowledged, scheduled later or needs the parent's call. **NOTED** = verified benign, no action.
|
|
80
|
+
|
|
81
|
+
| # | Finding (sources) | Disposition | Reason / implementation note |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| F1 | `keyTag` dead code; same-pass duplicate agent-call key risks silent drop of the receipt-bearing line (A1, R1) | **ACCEPTED** | Wire `keyTag` into the key at `standard.js:457` (and bugfix/chore twins), pass `"issuance"` / `"receipt"` at the two call sites, and extend `tests/publish-submitted-on-issuance.test.js` to pin within-pass key disjointness. Two independent critics + coordinator verification; the comment describes a mechanism the code lacks, and the silent-drop failure mode is exactly what the comment warns about. |
|
|
84
|
+
| F2 | Anchor-selection divergence: doc mandates oldest; `findTriggerEntry` (`lib/crew-api.js:1729`) and `retry-publish.js` newest-pick implement newest (A2, R2, O4) | **ACCEPTED** | Unify on oldest in all three to match `verify-publish.js:318–319` and the decision doc. Four-way corroboration; the divergence is newly *active* (two `submitted` lines per attempt is now the normal receipt path) with a reachable misclassification (`applied-not-built` where `verified`). |
|
|
85
|
+
| F3 | Stale verifier comment (`lib/verify-publish.js:302,317`: "audit observations are now `build-observed`, not `submitted`") (R2) | **ACCEPTED** | Factually wrong while the receipt path writes a second `outcome: "submitted"`. One-line fix. |
|
|
86
|
+
| F4 | Decision-doc history inaccurate re pre-existing recovery `submitted` (`lib/retry-publish.js:349`); "one broken contract" narrative overstated (O2) | **ACCEPTED** | Correct `publish-path.md:1293`; coordinator verified the pre-existing writer. A decision doc that misstates history will mislead the next root-cause session. |
|
|
87
|
+
| F5 | Triple meaning of `submitted` undocumented; outcome vocabulary incomplete in one place; no read-site comments (O3, O5) | **ACCEPTED** | Enumerate the three meanings + full outcome vocabulary (`submitted` ×3, `build-observed`, `unknown`, `failed`, `rejected` — with `rejected` defined as conclusive-negative/terminal-park) in `docs/decisions/publish-path.md`, and add one-line `agent_id`/binding comments at `lib/verify-publish.js` ~308 and `lib/retry-publish.js` ~184. |
|
|
88
|
+
| F6 | Issuance detail double-negative; "no refusal" skimmable as "accepted" (O1, O7) | **ACCEPTED** | Replace with the positive-claim wording proposed in O1 (pins-safe: retains "trigger issuance" + "receipt/observation pending", avoids banned phrases). |
|
|
89
|
+
| F7 | Receipt-bearing `submitted` detail lacks scope clause under the weakened word (O6) | **ACCEPTED** (minor) | Append a short scope clause (e.g. "; observation only, not a second issuance") — append-only, to preserve the `publish-ledger.test.js` whole-block substring pin. |
|
|
90
|
+
| F8 | Thrown vs returned trigger indistinguishable in ledger (A3) | **ACCEPTED** (minor) | Append the disposition ("trigger-returned" / "trigger-threw") to the issuance `detail`; test pins allow appending. Cheap durability for the next investigator. |
|
|
91
|
+
| F9 | `docs/publish-verification.md` §4b.1 stale — oldest `submitted` now always null-`agent_id` (S4) | **ACCEPTED** | Direct the reader to the receipt-bearing `submitted` entry and distinguish oldest from latest. The operational doc drifted in the same change that changed the semantics it documents. |
|
|
92
|
+
| F10 | R5-PUSH "nothing to push" wording inaccurate; audit accuracy claim overbroad (S2) | **ACCEPTED** (wording) | Fix the `standard.js:1202` wording to the honest policy ("detached line is room-local by policy — merge landed, not pushed"), and scope the audit's "confirmed accurate" claim in `publish-path.md` to the main STEP 2 path. Mechanism (PASS-with-loud-skip) stands. |
|
|
93
|
+
| F11 | `PUSH_SKIPPED` overloaded token; verdict template assumes the detached meaning (S3) | **ACCEPTED** (minimal) | Narrow the template or disambiguate the token. Small blast radius (R5 path only); keep the fix byte-cheap and check for pinned strings before editing prompt text. |
|
|
94
|
+
| F12 | Issuance comment restates code + detail; delete second sentence (S1) | **ACCEPTED** | Pure deletion; also funds the byte budget (see constraints). |
|
|
95
|
+
| F13 | Step-13 comment half-restates preflight; trim second sentence (S5) | **ACCEPTED** | Trivial; preserves the load-bearing cross-reference. |
|
|
96
|
+
| F14 | Detached-HEAD preflight message not actionable (O8) | **ACCEPTED** | Append the no-mutation assurance + next step (`git checkout <branch>`, re-run). `test-publish-preflight.sh` pins markers only — safe. |
|
|
97
|
+
| F15 | Crash between issuance write and observation: no recovery path; resume re-issues the same attempt key (R6) | **ACCEPTED** (document) / **DEFERRED** (mechanical guard) | Document as accepted risk in the decision doc now. A Publish-entry "submitted already exists" guard changes resume semantics and needs the parent's explicit call — do not smuggle it into this release. |
|
|
98
|
+
| F16 | Reliability benigns: verifier field selection correct; thrown-trigger timestamp fail-closed; no fail-open; new recovery path strictly stronger (R3, R4, R5) | **NOTED** | No action. Recorded so the next review doesn't re-litigate. |
|
|
99
|
+
| F17 | Architect composition otherwise clean: `applied_report` write-only, `manifest_before` honest, `rejected`/`submitted` exclusivity (A brief 1/2/4) | **NOTED** | No action. |
|
|
100
|
+
| F18 | Subtractor: no test redundancy, no doc duplication, preflight/step-13 split justified (S5, Q4, Q5) | **NOTED** | No action. |
|
|
101
|
+
|
|
102
|
+
**Implementation constraints for the parent:**
|
|
103
|
+
1. **Byte budget.** `workflows/bugfix.js` is at 245,736 of 245,760 (24 bytes headroom). The accepted wording additions (F6, F7, F8, F10, F11) must be byte-neutral or funded by the accepted deletions (F12 ≈ −100, F13 ≈ −60). Re-run `node tests/workflow-size.test.js` after editing.
|
|
104
|
+
2. **Test pins constraining workflow wording.** The issuance `detail` must keep "trigger issuance" + "receipt/observation pending" and must not contain "receipt captured"/"build went through" (`tests/publish-submitted-on-issuance.test.js`); the anchor comment `// (2026-09-19, room #22) Submitted-on-issuance` is pinned; "Attribution is by window, not by build identity" is pinned verbatim (`tests/publish-audit-fallback.test.js`).
|
|
105
|
+
3. **New test pin.** Extend `tests/publish-submitted-on-issuance.test.js` with within-pass agent-call-key disjointness for the two `submitted` writes (F1 follow-up).
|
|
106
|
+
4. **Decision-doc updates** (F4, F5, F15-doc) belong in the same commit as the code fixes — the doc already drifted once this cycle (F9).
|
|
107
|
+
|
|
108
|
+
**Bottom line:** ship-blocking items are F1–F15 (wording, comments, one real code fix in F1, one selection-logic fix in F2, doc repairs). No redesign, no new mechanism, no fail-open anywhere. After the fixes: re-run loader gate + full suite, then proceed to PUBLISH 0.14.3.
|
package/lib/AGENTS.md
CHANGED
|
@@ -7,13 +7,14 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
7
7
|
- `schema.sql` — the crew-owned state schema: projects, tasks, poll_state, config, agent_sessions, events. Vocabularies enforced by CHECK constraints; `rejected` is a valid event type (the 2026-09-11 crash was a stored session whose event was rejected). Column names match the historical dashboard tables for a verbatim migration.
|
|
8
8
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
9
9
|
- `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
|
|
10
|
-
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (the integration target is the repo's current checkout — a branch name, or HEAD when detached — resolved by `integration_target()`; nothing in the lifecycle ever checks out a branch. `integrate` merges into the target, reconciles `origin/<target>` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — then pushes inline via `do_push` (no agent round-trip; 2026-09-19 REVIEW collapsed the old STEP-2 agent run, which added only a lock refresh). `do_push`: PUSHED
|
|
11
|
-
- `test-detached-integrate.sh` — behavioral regression tests for the detached-HEAD Integrate fix (room #21 J3, 2026-09-19; extended 2026-09-19 REVIEW): fixture A stages a detached HEAD (reviewed SHA + crew-init commit) and proves integrate leaves HEAD detached, keeps the reviewed SHA and crew-init as ancestors, lands the task merge, never moves main, records `integration_target=HEAD`, verify-merge VERIFIEDs, and push-target
|
|
10
|
+
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (the integration target is the repo's current checkout — a branch name, or HEAD when detached — resolved by `integration_target()`; nothing in the lifecycle ever checks out a branch. `integrate` merges into the target, reconciles `origin/<target>` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — then pushes inline via `do_push` (no agent round-trip; 2026-09-19 REVIEW collapsed the old STEP-2 agent run, which added only a lock refresh). `do_push`: PUSHED (the refspec is always explicit — a branch pushes as `<branch>`, a detached HEAD as `HEAD:<destination>` where the destination is the remote's default branch from `resolve_push_destination()`; 2026-09-19 detached-HEAD audit REDO) / NO_REMOTE_PUSH (no origin — fail-soft, 2026-09-19 REVIEW: the old unconditional push exited 128 and mislabeled it); an unresolvable detached destination fails closed (never a silent skip, never a refusal of a knowable push); on push failure one fetch+merge retry while the lock is held, then fail closed without asserting a cause (never force-push). PUSH_SKIPPED survives only for the nothing-merged R5 path (no merge record + no lock held). Merged-but-unpushed retry recovery: a record whose merge commit IS the live HEAD takes the lock and pushes instead of reporting MERGED_EMPTY. `CREW_STAGED_BASE` guard (ferried from get-provenance): the line must descend from the staged publish base — STAGED_BASE_MISMATCH fails closed before the merge (room #21 J3 shape). `verify-merge` always re-resolves the target from the current checkout and checks the recorded commit against the LIVE tip — never the record's word for the tip (2026-09-19 REVIEW: the old read-back compared the record to itself for target=HEAD). A detached HEAD skips the inline reconcile loudly (NO_REMOTE_RECONCILE) — the inline reconcile needs a static branch target, so on detached the destination is resolved explicitly in do_push, which reconciles lazily on push failure (2026-09-19 detached-HEAD audit REDO; the old "local by design" rationale is rejected); `push-target` is the thin R5 wrapper over `do_push`: refreshes the lock, distinguishes nothing-merged (no record + no lock → loud skip) from lock-lost (fail closed). (`.worktrees/<id>`, branch `task/<id>`). Branch resolution order: the crew registry (`<repo>/.worktrees/.registry/<id>`), then the canonical `task/<full-id>` ref, then a `task/<id-prefix>` ref matched by strict prefix enumeration (task 4e1a1bba — a Build agent may create the branch with raw git from an abbreviated id, bypassing prepare; prefix names are tolerated, never created; ambiguous prefixes fail closed). `resolve-branch` prints the resolved branch for agent-side one-liners. Nothing reconstructs the branch name — every command resolves it. Prepare fails closed on a dirty integration target; cleanup is forgiving. `terminal-cleanup` is the run's last act at every park/fail boundary (called from `parkTask` in standard/bugfix/chore): releases the merge lock unconditionally and reclaims the worktree+branch only when the task branch is fully merged into the integration target — unmerged work is preserved for the human by design, and a dirty worktree is reported, never force-removed. `post-deploy` reports worktree removal honestly (a lying "removed" echo hid real leftovers — canary run 9, 2026-09-12). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset); `CREW_REPO` is exported so internal merge-lock.sh calls inherit the repo being worked on; `LIB_DIR` defaults to `$CREW_HOME/lib` (`CREW_LIB` override). `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or key=value: locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds — always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure. post-deploy's `.worktrees/` guard is add-then-reset, never an all-negative pathspec: `git add -A -- ':!.worktrees/'` still exits 1 on git 2.43.0 against the crew's own gitignored non-empty `.worktrees/` (room #15 J3), killing post-deploy under `set -e` — a plain `add -A` never errors on ignored paths and the follow-up `git reset -q -- .worktrees/` keeps stray lock files out of the integration target even on pre-gitignore-entry repos.
|
|
11
|
+
- `test-detached-integrate.sh` — behavioral regression tests for the detached-HEAD Integrate fix (room #21 J3, 2026-09-19; extended 2026-09-19 REVIEW): fixture A stages a detached HEAD (reviewed SHA + crew-init commit) and proves integrate leaves HEAD detached, keeps the reviewed SHA and crew-init as ancestors, lands the task merge, never moves main, records `integration_target=HEAD`, verify-merge VERIFIEDs, and push-target reports NO_REMOTE_PUSH (fixture A has no origin); fixture B proves the branch path still merges, reconciles, and pushes inline to a local origin; fixture C proves verify-merge checks the record against the LIVE tip (a detached line reset past the merge does NOT verify; the branch-gone path verifies only when the recorded commit is an ancestor of the live tip); fixture D proves the no-remote branch path fails soft with NO_REMOTE_PUSH (exit 0); fixture E proves the CREW_STAGED_BASE guard (non-ancestor base → STAGED_BASE_MISMATCH before any merge; true base → STAGED_BASE_OK); fixture F proves merged-but-unpushed retry recovery (re-running integrate after a crashed push takes the lock and pushes instead of parking on MERGED_EMPTY); fixture H proves the detached-HEAD audit REDO contract (2026-09-19) on a detached checkout with a local origin: push-destination fails closed when origin/HEAD is unset (genuinely unknowable destination), otherwise resolves the remote default branch; integrate pushes inline as `HEAD:main` (`PUSHED: origin/main (refspec HEAD:main)`), the remote ref advances to the merge commit, local main stays at the reviewed base, HEAD stays detached, verify-merge VERIFIEDs, and push-target re-pushes idempotently. Scratch dirs under /tmp only; CREW_HOME lives outside the scratch repos (as in real rooms) so lock files never trip the integrate preflight.
|
|
12
12
|
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight, integrate remote-reconcile with fixture sensitivity + no-remote fail-soft) on scratch repos
|
|
13
13
|
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \\u2014 escape; extracts the shipped block by anchor)
|
|
14
14
|
- `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
|
|
15
15
|
- `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
|
|
16
|
-
- `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
16
|
+
- `publish-npm.sh` — deterministic npm publish: preflight (npm capability + push-destination resolution — the push refspec is resolved before any mutation: a branch name, or `HEAD:<destination>` on a detached checkout via the lifecycle's `push-destination` command; a genuinely unknowable destination fails fast here with `PUBLISH_FAILED=env`, before any mutation — 2026-09-19 detached-HEAD audit REDO: detached publication is legitimate and must work, not refuse), lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
17
|
+
- `test-publish-preflight.sh` — regression tests for the npm publish capability preflight: the PREFLIGHT block extracted by anchor, helper -f/-r checks, placement, full-script runs (helper missing / credential missing / credential present + DRY_RUN / old helper without --check / real-helper probe), plus the detached-HEAD audit REDO cases (2026-09-19): a detached fixture repo with a local origin — origin/HEAD set + DRY_RUN=1 passes through to PUBLISH_DRY_RUN=1 (no refusal), origin/HEAD unset fails closed with PUBLISH_FAILED=env before any mutation, repo untouched; and the attached+no-origin case (critic F-A1: the anchor used to check no-origin only on the detached branch — attached+no-origin now fails closed pre-mutation too). Runs against scratch repos; the real-helper probe is credential-dependent (author machine only).
|
|
17
18
|
- `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
|
|
18
19
|
- `compose-evidence-caption.js` — deterministic QA-evidence caption composer (2026-09-14): given `--crew-home`, `--task-id`, and the resolved `--audit-dir` name, extracts the task title/description, the QA verdict, the `publish: verified` commit (or the task's project-scoped provenance record when it belongs to this task), and the merge commit's subject line from the project repo — and prints the out-of-context transition Eric asked for (problem → solution → evidence handoff) to stdout. The caller appends the desktop/mobile attachment lines after this caption. Verdict source (2026-09-16): Hazel's `task-evidence/<taskId>/postchange/verdict.json` is authoritative, then the last line of the append-only `verdicts.jsonl` ledger, then the newest legacy `visual_verdict:` note (the parent visual-verdict protocol was retired 2026-09-15). Honest labels: "provenance not yet stamped" when no commit is known, "no QA verdict recorded" when none exists, and the visual-protocol-unavailable note only when no verdict evidence exists at all, the newest `baseline:` note is `baseline: none`, AND captures exist (a `baseline: none` note means no baseline captures to compare against — it never overrules a real QA verdict; the note's premise is the post-deploy capture, so without captures it would fabricate evidence again). Capture guard (2026-09-17, room #14): the composer resolves the audit dir itself (`<home>/workspace/ts-spaces/<deploy_slug>/audits/<auditDir>/`, the same root crew-api.js and readback-disk.js resolve) and verifies `screenshot.png` + `screenshot-mobile.png` are files before claiming "desktop + mobile captures below" — a bare `--audit-dir` name is never trusted. Missing deploy_slug, missing dir, or missing PNGs gets the honest no-captures opener and Evidence line (terminal surfaces named explicitly: "terminal surface — QA judges the CLI, not pixels"); the caller only appends attachment lines when its own freshness check passes. "Latest" is by event timestamp, sorted explicitly. Exits non-zero on missing input.
|
|
19
20
|
- `edit-image.py` — deterministic pixel-level evidence editing (2026-09-15, Pillow): `crop --region x,y,w,h` (pixel-exact, rejects out-of-bounds), `zoom --factor <f> [--center x,y]` (nearest-neighbor, returns to source frame size), `label --text <caption>` (caption bar), `nup --cols <n> --in <a.png> --in <b.png> [--labels "a|b"]` (side-by-side grid). Byte-deterministic PNG output; no network, no time, no randomness. Derivatives supplement raw frames — the source frame stays archived.
|
package/lib/crew-api.js
CHANGED
|
@@ -1721,13 +1721,15 @@ function latestPublishNote(db, taskId) {
|
|
|
1721
1721
|
).get(taskId) || null;
|
|
1722
1722
|
}
|
|
1723
1723
|
|
|
1724
|
-
// The trigger anchor for an unknown attempt: the "submitted" ledger
|
|
1725
|
-
// for the same attempt
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1724
|
+
// The trigger anchor for an unknown attempt: the OLDEST "submitted" ledger
|
|
1725
|
+
// entry for the same attempt (the trigger issuance). Oldest binds the trigger
|
|
1726
|
+
// instant — verify-publish.js and the decision doc bind oldest; binding newest
|
|
1727
|
+
// misclassifies builds that complete between issuance and receipt (A2/R2/O4).
|
|
1728
|
+
// Returns the entry or null. A null means the trigger instant is unknowable
|
|
1729
|
+
// (or the trigger was never issued) — classification cannot run; the task
|
|
1730
|
+
// stays parked for human attention. Fail closed, never guess the anchor.
|
|
1729
1731
|
function findTriggerEntry(entries, commit, attempt) {
|
|
1730
|
-
for (let i =
|
|
1732
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1731
1733
|
const e = entries[i];
|
|
1732
1734
|
if (e.outcome === "submitted" && e.commit === commit &&
|
|
1733
1735
|
(e.attempt || null) === (attempt || null)) {
|