muse-crew 0.7.18 → 0.7.20

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.
@@ -42,18 +42,25 @@ The rule: **the builder's applied report is never a verification signal, in
42
42
  either direction.** A matching report certifies nothing (it is derived from
43
43
  the carried diff — circular by construction, canary run 8). A mismatching or
44
44
  empty report blocks nothing (false-negative mode demonstrated by `23ca8f3f`).
45
- The workflow computes the report comparison as an observation only — it
46
- never parks on it. The parent ignores the applied report entirely when
47
- deciding whether to stamp: an `applied:[]` or mismatched report with a
48
- matching independent read-back stamps normally.
45
+ The workflow dropped the report entirely on 2026-09-16 (clean-room task
46
+ `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
47
+ stochastic text channel and the runtime's JSON-candidate heuristic misfired
48
+ on its prose ("workflow agent output was not JSON"), parking a task whose
49
+ edit may have gone through. The trigger is now awaited-but-unconsumed — no schema,
50
+ no consumed return value — and there is no builder report to observe:
51
+ `applied_report` is `missing-report` on ledger lines for issued triggers
52
+ (pre-trigger parks and unattributed-unknown parks write null — no trigger
53
+ was observed, so there is nothing to report). The
54
+ parent ignores the (absent) report entirely when deciding whether to stamp.
49
55
 
50
56
  The contract is split on purpose:
51
57
 
52
- - **Workflow-owned:** carrying the merged diff to the builder, the
53
- applied-report observation (logged, never a park), the build-completion
54
- poll, post-deploy cleanup, recording the Publish session completed, and
55
- parking with `publish: verification-requested <commit>` instead of
56
- stamping. The workflow does NOT trigger the read-back inspection — an
58
+ - **Workflow-owned:** carrying the merged diff to the builder, attributing
59
+ the edit itself (fire-and-forget trigger no builder report — via
60
+ pre-trigger toolcheck, pre-trigger build-state baseline, and post-trigger
61
+ build-state diff), the build-completion poll, post-deploy cleanup,
62
+ recording the Publish session completed, and parking with `publish:
63
+ verification-requested <commit>` instead of stamping. The workflow does NOT trigger the read-back inspection — an
57
64
  async inspection triggered from inside a workflow run delivers its result
58
65
  to the root agent, never back into the run, so a workflow-side trigger is
59
66
  an orphan the verifier cannot consume. The parent triggers the one
@@ -313,24 +320,40 @@ For a task parked with `publish: verification-requested <commit>`:
313
320
  second scan sees the unexpired `publish: verification-claimed` note and
314
321
  skips. The lease expiry bounds the damage if a claimer dies.
315
322
 
316
- ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7)
323
+ ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; fire-and-forget 2026-09-16)
317
324
 
318
325
  Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
319
326
  child failed structured closeout and the in-flight-only build-state poll
320
327
  could not see the completed build — even though the build HAD run (a fresh
321
- platform audit directory existed). Two mechanisms close this gap.
322
-
323
- **1. Workflow-side durable evidence.** Before the rebuild trigger, the
328
+ platform audit directory existed). 2026-09-16 (clean-room task `e2a8d9f8`)
329
+ showed the failure is worse than a catchable throw: the runtime's
330
+ JSON-candidate heuristic rejects the trigger call itself ("workflow agent
331
+ output was not JSON") whenever the child returns prose, whether or not the
332
+ edit went through. The trigger is therefore fire-and-forget — no schema, no
333
+ consumed return value — and the workflow always attributes the edit itself.
334
+ Two mechanisms close the gap.
335
+
336
+ **1. Pre-trigger toolcheck + baseline.** Before the trigger, a tiny schema'd
337
+ child proves the artifact tool namespace is available (one bounded retry on
338
+ explicit negative evidence — the only safe retry on the publish path:
339
+ without the tools the edit provably did not go through) and captures a
340
+ pre-trigger build-state baseline. After the trigger, the workflow diffs the
341
+ post-trigger build state against the baseline: a build whose `agent_id` is
342
+ new relative to the baseline is this edit's receipt. The baseline build's
343
+ `agent_id` is never substituted — a build already in flight at baseline
344
+ predates the trigger and is never attributed to this edit.
345
+
346
+ **2. Workflow-side durable evidence.** Before the rebuild trigger, the
324
347
  workflow snapshots the artifact's audit-directory listing
325
- (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). On a
326
- structured-output failure with no in-flight build observed, it re-lists and
327
- diffs: a timestamped directory that appeared during the trigger window is
328
- positive evidence the edit went through and the build completed. The
329
- fallback never re-issues the edit, never stamps provenance, and only routes
330
- to the parent's independent content read-back. No new directory still parks
331
- `unknown` fail-closed. The ledger distinguishes the two confirmations: `edit
332
- confirmed via durable audit evidence …` vs `edit confirmed via build-state
333
- poll …`.
348
+ (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). When
349
+ no in-flight receipt was observed, it re-lists and diffs: a timestamped
350
+ directory that appeared during the trigger window is positive evidence the
351
+ edit went through and the build completed. The fallback never re-issues the
352
+ edit, never stamps provenance, and only routes to the parent's independent
353
+ content read-back. No new directory still parks `unknown` fail-closed. The
354
+ ledger's `detail` line distinguishes the two confirmations: `… edit
355
+ confirmed via durable audit evidence (new audit dir …)` vs `… build receipt
356
+ captured by workflow-owned build-state observation (pre/post-trigger diff)`.
334
357
 
335
358
  The fallback's known limitation: audit directories are not attributed to
336
359
  tasks, so two concurrent publishes to the same artifact could cross-read.
package/lib/AGENTS.md CHANGED
@@ -14,7 +14,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
14
14
  - `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
15
15
  - `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
16
  - `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.
17
- - `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 latest `visual_verdict:` note, the `publish: verified` commit (or the global 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. Honest labels: "provenance not yet stamped" when no commit is known, "no QA verdict recorded" when none exists, and the visual-protocol-unavailable note when the task's own run recorded `baseline: none`. Exits non-zero on missing input.
17
+ - `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 global 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 and the newest `baseline:` note is `baseline: none` (a `baseline: none` note means no baseline captures to compare against — it never overrules a real QA verdict). "Latest" is by event timestamp, sorted explicitly. Exits non-zero on missing input.
18
18
  - `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.
19
19
  - `render-html.js` — deterministic HTML evidence composition (2026-09-15, headless Chromium): renders a local HTML layout to PNG (fixed viewport width, device scale 1, full-page screenshot). Hermetic: remote HTTP(S) assets are blocked and fail the render loudly; relative image paths resolve against the HTML file; local/system fonts only. Exit 3 reports NOT POSSIBLE when Chromium is unavailable. The composition layer above edit-image.py's pixel layer.
20
20
  - `see-act.js` — single-step browser driver for experiential QA (2026-09-14): one browser action per invocation (`aria`, `shot`, `click`, `scroll`, `type`), one JSON line on stdout, exit 0/2/3 (3 = NOT POSSIBLE: missing playwright-core or Chromium). Spawns its own loopback forward proxy on an ephemeral port (Chromium blocks direct loopback). The QA work agent closes the OODA loop: run a step, read the screenshot/aria, decide the next. Determinism: no wall-clock reads, no randomness. `SEE_ACT_ARCHIVE_DIR=<phase-dir>` (2026-09-14): every screenshot is archived automatically as `001-shot-desktop.png`, `002-click-mobile.png`, ... (counter in `<dir>/.seq`); `--out` becomes optional; JSON carries `screenshot` + `archived`; explicit `--out` + archive copies into the archive; unusable dir is NOT POSSIBLE exit 3.
@@ -22,3 +22,6 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
22
22
  - `write-ooda-verdict.js` — deterministic writer for the OODA terminal verdict (2026-09-14, append-only ledger 2026-09-15): `node write-ooda-verdict.js --dir <phase-dir> --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE> --summary <text> --expected <text> --actual <text> --missing <json-array> [--reason <text>]` writes `<phase-dir>/verdict.json` (the latest verdict) and appends one JSON line to `<phase-dir>/verdicts.jsonl` — the append-only ledger: every attempt's verdict is preserved with a mechanical `seq`, never overwritten; corrupt or non-contiguous ledgers fail loudly. `--reason` is REQUIRED and must be non-empty for `FAIL` and `NOT_POSSIBLE` — a reason-less negative verdict fails with exit 2 before anything is written (2026-09-15). Exit 2 on bad input. See `docs/ooda-report.md`.
23
23
  - `read-ooda-verdict.js` — deterministic cross-checker for the OODA terminal verdict (2026-09-15): `node read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>` reads `<dir>/verdict.json`, prints one JSON line to stdout, exits 0 with `{ok:true, verdict, reason, summary, expected, actual, attempt}` when the record agrees with the prose expectation and a FAIL carries a non-empty reason, or exits 2 with `{ok:false, code}` — `missing|corrupt|contradiction|no_reason`. No wall-clock reads, no randomness. The bugfix QA closeout runs it against the prose `VERDICT:` line before any rework routing: a failed cross-check records the phase as failed for retry, never routes to rework.
24
24
  - `serve-artifact.js` — local server for a built TS space for experiential QA (2026-09-14): serves `<space-dir>/client/dist` statically and dispatches POST `*/actions` to the compiled server actions with a locally-built Ctx. Prints `READY port=<n>` then serves until killed. Read-only w.r.t. the space directory. Fidelity: the served client and action handlers are the artifact's own built code; the Ctx is locally built (privileged handlers run from the space's own `server/dist/privileged.js` when present; blobs are stored in a per-run temp dir and served back at `/__blobs/<key>`); environment is inherited from the caller. It is not the hosted runtime — tasks that cannot be judged under it must report `NOT POSSIBLE: <reason>`.
25
+ - `readback-disk.js` — deterministic publish content sensor (2026-09-16): reads the on-disk tree the artifact is built/served from and emits the machine-readable findings block (`FILE:`/`ADDED:`/`REMOVED:`/`END_FILE`) that `verify-publish.js` judges. The primary sensor — the LLM-inspector path (`build-readback-request.js`) is manual-fallback only since `artifact_inspect` was removed by the platform 2026-09-14.
26
+ - `build-readback-request.js` — builds the LLM-inspector read-back `verbatim_request` from the merge commit's diff (2026-09-14): carries the merged diff as the expected change and asks for an independent read of the artifact's actual source. Retained as the manual fallback; the deterministic `readback-disk.js` is the primary sensor.
27
+ - `verify-publish.js` — mechanical publish verification judge (2026-09-14/16): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every added line PRESENT / every removed line ABSENT, HEAD==commit supersession check) and only then stamps provenance. Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp. Collision exemption (2026-09-15): a removed line occurring in the old tree strictly more times than the diff removes is exempt from ABSENT (zero discriminating power); computed from `git show <base>:<path>`, never the working tree.
@@ -18,6 +18,7 @@
18
18
  // Prints the caption to stdout. Exits non-zero on any missing input.
19
19
 
20
20
  const { execFileSync } = require("node:child_process");
21
+ const fs = require("node:fs");
21
22
  const path = require("node:path");
22
23
 
23
24
  function usage(msg) {
@@ -83,19 +84,51 @@ try {
83
84
  usage("cannot read task events: " + (e.message || e));
84
85
  }
85
86
  const notes = events.filter((e) => e.type === "note");
87
+ // Newest-first by timestamp. The API orders DESC, but the sort is explicit:
88
+ // array order is not a contract this script should depend on, and the old
89
+ // [...notes].reverse().find() pattern silently returned the OLDEST match.
90
+ const byNewest = (a, b) => {
91
+ const ta = a.timestamp || "", tb = b.timestamp || "";
92
+ return tb < ta ? -1 : tb > ta ? 1 : 0;
93
+ };
94
+ const notesNewest = [...notes].sort(byNewest);
86
95
 
87
- // QA verdict: the workflow's own exact-prefix contract the latest note
88
- // whose message starts exactly "visual_verdict: PASS" / "visual_verdict: FAIL".
89
- const verdictNote = [...notes].reverse()
90
- .find((e) => /^visual_verdict:\s*(PASS|FAIL)/.test(e.message || ""));
91
- const verdict = verdictNote
92
- ? verdictNote.message.replace(/^visual_verdict:\s*/, "").trim()
93
- : null;
96
+ // QA verdict: Hazel owns the verdict<phase-dir>/verdict.json is
97
+ // authoritative (2026-09-15: the parent visual-verdict protocol was retired,
98
+ // so the workflows no longer emit `visual_verdict:` notes; the old note
99
+ // contract is legacy fallback only). Prefer verdict.json, then the last line
100
+ // of the append-only verdicts.jsonl ledger, then the newest legacy note.
101
+ let verdict = null; // { status, summary }
102
+ const qaDir = path.join(crewHome, "task-evidence", taskId, "postchange");
103
+ function readVerdictFile(p) {
104
+ try {
105
+ const v = JSON.parse(fs.readFileSync(p, "utf8"));
106
+ if (v && v.verdict) return { status: String(v.verdict), summary: v.summary || "" };
107
+ } catch { /* fall through */ }
108
+ return null;
109
+ }
110
+ verdict = readVerdictFile(path.join(qaDir, "verdict.json"));
111
+ if (!verdict) {
112
+ // Append-only ledger: the last line is the latest attempt's verdict.
113
+ try {
114
+ const lines = fs.readFileSync(path.join(qaDir, "verdicts.jsonl"), "utf8").trim().split("\n");
115
+ const last = JSON.parse(lines[lines.length - 1]);
116
+ if (last && last.verdict) verdict = { status: String(last.verdict), summary: last.summary || "" };
117
+ } catch { /* fall through to the legacy note contract */ }
118
+ }
119
+ if (!verdict) {
120
+ const verdictNote = notesNewest
121
+ .find((e) => /^visual_verdict:\s*(PASS|FAIL)/.test(e.message || ""));
122
+ if (verdictNote) {
123
+ const m = /^visual_verdict:\s*(PASS|FAIL)\s*[—–-]?\s*([\s\S]*)$/.exec(verdictNote.message || "");
124
+ verdict = { status: m[1], summary: (m[2] || "").trim() };
125
+ }
126
+ }
94
127
 
95
- // Provenance: this task's own `publish: verified` event first; the global
96
- // provenance record only when it belongs to this task.
128
+ // Provenance: this task's own newest `publish: verified` event first; the
129
+ // global provenance record only when it belongs to this task.
97
130
  let commit = null;
98
- const verifiedNote = [...notes].reverse()
131
+ const verifiedNote = notesNewest
99
132
  .find((e) => (e.message || "").includes("publish: verified"));
100
133
  if (verifiedNote) {
101
134
  const m = /\b([0-9a-f]{40})\b/.exec(verifiedNote.message);
@@ -122,9 +155,19 @@ if (commit) {
122
155
  if (!solution) solution = task.title;
123
156
 
124
157
  const problem = trimWords(task.description || task.title, 280);
125
- // The task's own run record is authoritative for the honest label: a
126
- // `baseline: none` note means QA ran without the visual protocol.
127
- const visualUnavailable = notes.some((e) => /baseline:\s*none/i.test(e.message || ""));
158
+ // The "visual protocol unavailable" honest label answers "did QA get to
159
+ // look?" and the only honest signal for that is the ABSENCE of any QA
160
+ // verdict evidence. A `baseline: none` note means the Capture phase had no
161
+ // baseline screenshots to compare against; it does NOT mean QA couldn't
162
+ // inspect. Room #5 (2026-09-16) logged `baseline: none (visual protocol
163
+ // unavailable)` as its newest baseline note, yet Hazel still ran the full
164
+ // experiential see-act loop and stamped PASS in verdict.json — the old
165
+ // .some() check mislabeled that run. So: any verdict evidence (file,
166
+ // ledger, legacy note) suppresses the label; without it, the newest
167
+ // `baseline: none` note earns it.
168
+ const newestBaselineNote = notesNewest.find((e) => /^baseline:\s*/i.test(e.message || ""));
169
+ const visualUnavailable = !verdict && !!newestBaselineNote &&
170
+ /^baseline:\s*none/i.test(newestBaselineNote.message || "");
128
171
 
129
172
  const lines = [];
130
173
  lines.push("These screenshots are dropping in out of context — quick transition.");
@@ -134,7 +177,7 @@ lines.push("Problem: " + problem);
134
177
  lines.push("Shipped: " + trimWords(solution, 160) +
135
178
  (commit ? " (commit `" + commit.slice(0, 12) + "`, parent-verified)" : " (provenance not yet stamped)"));
136
179
  lines.push("Evidence: desktop + mobile captures below, taken by the platform audit harness after deploy (`" + auditDir + "`)." +
137
- (verdict ? " QA verdict: " + trimWords(verdict, 200) + "." : " No QA verdict recorded."));
180
+ (verdict ? " QA verdict: " + verdict.status + (verdict.summary ? " — " + trimWords(verdict.summary, 200) : "") + "." : " No QA verdict recorded."));
138
181
  if (visualUnavailable) {
139
182
  lines.push("Note: the visual protocol is unavailable — these are the post-deploy capture plus QA's mechanical checks, not a QA visual inspection.");
140
183
  }
@@ -2,17 +2,22 @@
2
2
  // verify-publish.js — deterministic parent publish verifier.
3
3
  //
4
4
  // The parent never eyeballs an inspection report. This script takes the
5
- // async read-back inspection's result JSON and MECHANICALLY decides the
6
- // verdict: it parses the inspector's machine-readable findings block,
7
- // compares every added/removed diff line against the reported
8
- // present/absent verdicts (with a mechanically computed collision
9
- // exemption for removed lines that also occur in untouched code),
10
- // checks supersession via git, and only then stamps provenance, logs
11
- // the terminal event, and re-queues the task.
5
+ // read-back inspection's result JSON and MECHANICALLY decides the verdict:
6
+ // it parses the machine-readable findings block, compares every
7
+ // added/removed diff line against the reported present/absent verdicts
8
+ // (with mechanically computed collision exemptions for lines that also
9
+ // occur in untouched code and therefore have zero discriminating power),
10
+ // checks supersession via git, and only then stamps provenance, logs the
11
+ // terminal event, and re-queues the task.
12
12
  //
13
- // The LLM is the sensor (it reads the artifact source); this code is the
14
- // judge. Unparseable findings, content mismatches, supersession, and stamp
15
- // failures all fail CLOSED with a terminal parent verdict — never a stamp.
13
+ // The sensor is the deterministic lib/readback-disk.js (it reads the
14
+ // on-disk tree the artifact is built/served from); this code is the judge.
15
+ // The LLM inspector driven by lib/build-readback-request.js is retained
16
+ // only as the manual fallback for environments without disk access.
17
+ // Unparseable findings, content mismatches, unverifiable changes (binary
18
+ // files, mode-only changes, renames, fully-colliding hunks), supersession,
19
+ // and stamp failures all fail CLOSED with a terminal parent verdict —
20
+ // never a stamp.
16
21
  //
17
22
  // Usage:
18
23
  // node verify-publish.js --crew-home <path> --task-id <uuid>
@@ -177,13 +182,15 @@ try {
177
182
  } catch (e) {
178
183
  terminal("read-back-unavailable", `git diff failed: ${e.message}`);
179
184
  }
180
- const expected = new Map(); // path -> { added: [], removed: [] }
185
+ const expected = new Map(); // path -> { added: [], removed: [], isBinary: bool }
181
186
  let curFile = null;
182
187
  for (const line of diff.split("\n")) {
183
188
  if (line.startsWith("diff --git")) {
184
189
  const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
185
190
  curFile = m ? m[2] : "unknown";
186
- expected.set(curFile, { added: [], removed: [] });
191
+ expected.set(curFile, { added: [], removed: [], isBinary: false });
192
+ } else if (curFile && line.startsWith("Binary files ")) {
193
+ expected.get(curFile).isBinary = true;
187
194
  } else if (curFile && line.startsWith("+") && !line.startsWith("+++")) {
188
195
  expected.get(curFile).added.push(line.slice(1));
189
196
  } else if (curFile && line.startsWith("-") && !line.startsWith("---")) {
@@ -257,11 +264,42 @@ let exempted = 0;
257
264
  for (const [path, exp] of expected) {
258
265
  const found = findings.get(path);
259
266
  if (!found) terminal("content-mismatch", `no findings for changed file ${path}`);
267
+ // (2026-09-16, critic finding 1) Binary files, mode-only changes, and
268
+ // renames produce zero added/removed lines: the loops below would
269
+ // iterate over empty arrays and the stamp would issue with "all added
270
+ // lines PRESENT, all removed lines ABSENT" — vacuously true, content
271
+ // never read. The line-based judge cannot verify these changes, so they
272
+ // fail closed as unverifiable (never stamped); a human verifies.
273
+ if (exp.added.length === 0 && exp.removed.length === 0) {
274
+ terminal("unverifiable-content",
275
+ `${exp.isBinary ? "binary file" : "no content lines (mode-only change or rename)"} ${path}: ` +
276
+ "the diff carries no added/removed lines for this file, so the line-based read-back checked nothing — " +
277
+ "provenance NOT stamped; human verification needed");
278
+ }
279
+ // (2026-09-16, critic finding 5) Added-line collisions: an added line
280
+ // that already occurs verbatim in the old tree is reported PRESENT
281
+ // whether or not the new hunk actually landed — zero discriminating
282
+ // power (the sensor is membership-only, not count-sensitive). Exempt
283
+ // such lines from the pass criteria, symmetric to the removed-side
284
+ // exemption below — and require at least one discriminating added line
285
+ // per file, or the added-side check is vacuous (finding 1's class).
286
+ let discriminatingAdded = 0;
260
287
  for (const line of exp.added) {
261
288
  const v = found.added.get(line);
262
289
  if (v === undefined) terminal("unreadable-result", `no ADDED finding for line in ${path}: ${line.slice(0, 60)}`);
290
+ if (oldCount(path, line) > 0) {
291
+ exempted += 1; // colliding pre-existing line: zero signal, cannot fail a good publish
292
+ continue;
293
+ }
294
+ discriminatingAdded += 1;
263
295
  if (v !== "PRESENT") terminal("content-mismatch", `added line ABSENT in ${path}: ${line.slice(0, 80)}`);
264
296
  }
297
+ if (exp.added.length > 0 && discriminatingAdded === 0) {
298
+ terminal("unverifiable-content",
299
+ `all ${exp.added.length} added line(s) in ${path} already occur in the old tree: ` +
300
+ "their PRESENT findings cannot tell \"hunk landed\" from \"hunk dropped\" — " +
301
+ "provenance NOT stamped; human verification needed");
302
+ }
265
303
  // The diff may remove the same line more than once; the old tree must
266
304
  // account for every removal before a line counts as a collision.
267
305
  const removedBudget = new Map();
@@ -316,7 +354,7 @@ if (!p || p.source_commit !== commit || p.task_id !== taskId || p.crew_release !
316
354
  // --- 8. Terminal verified event + re-queue -----------------------------------
317
355
  const verifiedMsg =
318
356
  `publish: verified ${commit} (${inspectionId}) — read-back: all added lines PRESENT, all removed lines ABSENT` +
319
- (exempted > 0 ? ` (${exempted} colliding removed line${exempted === 1 ? "" : "s"} exempted)` : "") +
357
+ (exempted > 0 ? ` (${exempted} colliding line${exempted === 1 ? "" : "s"} exempted as non-discriminating)` : "") +
320
358
  `; ${buildNote}; no supersession (HEAD=${commit}).`;
321
359
  api("log-event", { task_id: taskId, type: "note", message: verifiedMsg.slice(0, 1000) });
322
360
  api("update-task", { id: taskId, state: "in_progress" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,