@junghanacs/entwurf 0.24.0 → 0.25.1

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +564 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/DELIVERY.md +24 -1
  4. package/README.md +2 -2
  5. package/VERIFY.md +20 -2
  6. package/demo/README.md +5 -2
  7. package/docs/acp-backend-rail.md +49 -2
  8. package/docs/setup-clean-host.md +4 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +5 -4
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +13 -3
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +11 -0
  18. package/mcp/entwurf-bridge/dist/protocol.js +0 -7
  19. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
  20. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
  21. package/mcp/entwurf-bridge/src/index.ts +5 -4
  22. package/package.json +10 -9
  23. package/pi-extensions/entwurf-control.ts +27 -154
  24. package/pi-extensions/lib/compaction-send-guard.ts +5 -3
  25. package/pi-extensions/lib/control-send-receipt.ts +108 -0
  26. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  27. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
  28. package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
  29. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
  30. package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
  31. package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
  32. package/pi-extensions/lib/herdr-fresh-call.ts +13 -3
  33. package/pi-extensions/lib/herdr-placement.ts +11 -0
  34. package/protocol.js +0 -8
  35. package/run.sh +202 -33
  36. package/scripts/check-entwurf-v2-surface.ts +289 -10
  37. package/scripts/check-gate-qualification.ts +2354 -11
  38. package/scripts/check-herdr-fresh-call.ts +10 -2
  39. package/scripts/check-herdr-placement.ts +51 -0
  40. package/scripts/check-herdr-plugin-build.ts +11 -4
  41. package/scripts/check-herdr-plugin.ts +745 -38
  42. package/scripts/check-mux-launcher-fence.ts +37 -0
  43. package/scripts/check-pi-launch.ts +7 -4
  44. package/scripts/check-release-gate-outcomes.ts +165 -0
  45. package/scripts/doctor-pi-provider.ts +44 -4
  46. package/scripts/herdr-rails.mjs +75 -0
  47. package/scripts/lib/qualification-acceptance.ts +146 -0
  48. package/scripts/lib/qualification-artifact.ts +359 -0
  49. package/scripts/lib/qualification-collection.ts +384 -0
  50. package/scripts/lib/qualification-gh.ts +231 -0
  51. package/scripts/lib/qualification-ledger.ts +669 -0
  52. package/scripts/lib/qualification-oracle-core.ts +279 -0
  53. package/scripts/lib/qualification-receipt.ts +561 -0
  54. package/scripts/lib/qualification-release.ts +271 -0
  55. package/scripts/meta-bridge-state.py +18 -1
  56. package/scripts/mutants/bridge-command-boot.json +38 -0
  57. package/scripts/mutants/control-send-receipt.json +235 -0
  58. package/scripts/mutants/herdr-fresh-call.json +4 -4
  59. package/scripts/mutants/herdr-plugin-build.json +7 -11
  60. package/scripts/mutants/herdr-plugin.json +177 -9
  61. package/scripts/mutants/meta-retire.json +22 -0
  62. package/scripts/mutants/pack-install.json +4 -4
  63. package/scripts/mutants/pi-floor.json +17 -0
  64. package/scripts/mutants/qualification-ledger.json +489 -0
  65. package/scripts/mutants/release-gate.json +48 -0
  66. package/scripts/mutants/retired-sent-surface.json +32 -0
  67. package/scripts/mutants/v2-surface.json +7 -7
  68. package/scripts/qualification-oracle.ts +124 -0
  69. package/scripts/smoke-codex-fresh-live.ts +94 -13
  70. package/scripts/smoke-herdr-fresh-call-live.ts +15 -8
  71. package/scripts/smoke-herdr-raw-install-live.sh +66 -4
  72. package/scripts/smoke-meta-install-state.sh +107 -1
  73. package/scripts/smoke-mux-lifecycle-live.ts +9 -2
  74. package/scripts/smoke-pi-provider-state.sh +106 -0
  75. package/scripts/smoke-setup-verdict.sh +18 -2
@@ -0,0 +1,271 @@
1
+ /**
2
+ * qualification-release — compose a RELEASE ledger from CI collections along a
3
+ * first-parent chain (#124 step 2). PURE: no network, no git, no fs. The caller hands
4
+ * in what it has already fetched and verified elsewhere; this function never claims its
5
+ * inputs came from anywhere in particular, and it produces no acceptance class.
6
+ *
7
+ * Inputs:
8
+ * - finalSha and firstParent (`git rev-list --first-parent finalSha`, finalSha first);
9
+ * - runs: one entry per CI run attempt — runId, runAttempt, headSha, and the unpacked
10
+ * collection files (name → bytes);
11
+ * - independent: the candidate recomputed by the caller from the final tree ITSELF
12
+ * (surface, groups, manifest claims, CORE_FILES fingerprints). Its node/node_modules
13
+ * part cannot be recomputed off the CI runner and is not used;
14
+ * - operatorDeclared: extra groups a human requires to be measured on the final SHA.
15
+ *
16
+ * Contract errors (any → RED): a run that is not CI provenance (runId/runAttempt not
17
+ * digits), a headSha off the first-parent chain, the same (runId, runAttempt) twice, a
18
+ * collection whose own runId/runAttempt/head disagree with the run, no collection at the
19
+ * final SHA, an incomplete final collection, or a final candidate the independent
20
+ * recomputation contradicts.
21
+ *
22
+ * Per SHA only the newest (runId, then runAttempt) run counts; older ones are listed as
23
+ * superseded. An incomplete ancestor collection is excluded from the basis with its
24
+ * reason. Order across runs is first-parent position, then collection index — never a
25
+ * clock. Release blockers on top of the local ones: every group needs a usable pass
26
+ * receipt somewhere in the chain (UNRUN blocks), and the runner group, every group whose
27
+ * newest receipt in the chain is red, and every operator-declared group must be
28
+ * exact-MEASURED on the final SHA. A red on an ancestor that the final SHA re-measured
29
+ * green stays in history and contributes no FAIL.
30
+ */
31
+
32
+ import { type Collection, checkCollectionData, mapReader } from "./qualification-collection.ts";
33
+ import {
34
+ type ContentCandidate,
35
+ type CurrentCandidate,
36
+ composeLedger,
37
+ type Ledger,
38
+ type LoadedReceipt,
39
+ } from "./qualification-ledger.ts";
40
+ import { canonicalJson } from "./qualification-receipt.ts";
41
+
42
+ export interface CiRun {
43
+ runId: string;
44
+ runAttempt: string;
45
+ headSha: string;
46
+ files: Record<string, Buffer>;
47
+ }
48
+
49
+ export interface ReleaseInput {
50
+ finalSha: string;
51
+ firstParent: string[];
52
+ runs: CiRun[];
53
+ /** The final tree's content projection — no C0 environment part (see ContentCandidate). */
54
+ independent: ContentCandidate;
55
+ operatorDeclared: string[][];
56
+ }
57
+
58
+ export interface RunRef {
59
+ runId: string;
60
+ runAttempt: string;
61
+ headSha: string;
62
+ }
63
+
64
+ export interface ReleaseLedger extends Ledger {
65
+ release: {
66
+ finalSha: string;
67
+ contractErrors: string[];
68
+ crossCheck: string[];
69
+ used: RunRef[];
70
+ excluded: (RunRef & { reason: string })[];
71
+ required: string[][];
72
+ denominator: {
73
+ groups: number;
74
+ claims: number;
75
+ measuredOnFinal: { groups: number; claims: number; list: string[] };
76
+ inheritedCore: { gate: string; from: RunRef | null; reason: string }[];
77
+ flagged: string[];
78
+ unrun: number;
79
+ fail: number;
80
+ };
81
+ /** What this ledger can and cannot vouch for. */
82
+ provenance: string[];
83
+ };
84
+ }
85
+
86
+ const DIGITS = /^[0-9]+$/;
87
+ const refOf = (r: CiRun): RunRef => ({ runId: r.runId, runAttempt: r.runAttempt, headSha: r.headSha });
88
+ const newer = (a: CiRun, b: CiRun): boolean =>
89
+ BigInt(a.runId) !== BigInt(b.runId) ? BigInt(a.runId) > BigInt(b.runId) : BigInt(a.runAttempt) > BigInt(b.runAttempt);
90
+
91
+ /** Content fields a CI-claimed candidate must share with the independent recomputation. */
92
+ export function crossCheckCandidate(claimed: CurrentCandidate, independent: ContentCandidate): string[] {
93
+ const out: string[] = [];
94
+ if (claimed.head !== independent.head) out.push(`head: CI ${claimed.head} vs tree ${independent.head}`);
95
+ if (claimed.workSurfaceSha256 !== independent.workSurfaceSha256) out.push("workSurface");
96
+ if (canonicalJson(claimed.manifestClaims) !== canonicalJson(independent.manifestClaims)) out.push("manifestClaims");
97
+ if (canonicalJson(claimed.groups) !== canonicalJson(independent.groups)) out.push("groups");
98
+ if (!claimed.coreFiles || !independent.coreFiles) out.push("coreFiles: missing on one side");
99
+ else if (canonicalJson(claimed.coreFiles) !== canonicalJson(independent.coreFiles)) out.push("coreFiles");
100
+ return out;
101
+ }
102
+
103
+ export function composeRelease(input: ReleaseInput): ReleaseLedger {
104
+ const contractErrors: string[] = [];
105
+ const excluded: ReleaseLedger["release"]["excluded"] = [];
106
+ const chainPos = new Map(input.firstParent.map((sha, i) => [sha, i]));
107
+ if (input.firstParent[0] !== input.finalSha) contractErrors.push("firstParent must start with finalSha");
108
+
109
+ // 1. Admission: provenance shape, chain membership, uniqueness, collection integrity.
110
+ const seenAttempt = new Set<string>();
111
+ const admitted: { run: CiRun; collection: Collection; complete: boolean; errors: string[] }[] = [];
112
+ for (const run of input.runs) {
113
+ const tag = `run ${run.runId}#${run.runAttempt}@${run.headSha.slice(0, 12)}`;
114
+ if (!DIGITS.test(run.runId) || !DIGITS.test(run.runAttempt)) {
115
+ contractErrors.push(`${tag}: runId/runAttempt are not CI provenance`);
116
+ continue;
117
+ }
118
+ const key = `${run.runId}#${run.runAttempt}`;
119
+ if (seenAttempt.has(key)) {
120
+ contractErrors.push(`${tag}: the same run attempt is supplied twice`);
121
+ continue;
122
+ }
123
+ seenAttempt.add(key);
124
+ if (!chainPos.has(run.headSha)) {
125
+ contractErrors.push(`${tag}: headSha is not on the first-parent chain of ${input.finalSha.slice(0, 12)}`);
126
+ continue;
127
+ }
128
+ const { errors, collection } = checkCollectionData(mapReader(run.files), Object.keys(run.files).sort());
129
+ if (!collection) {
130
+ contractErrors.push(`${tag}: ${errors.join("; ")}`);
131
+ continue;
132
+ }
133
+ if (collection.runId !== run.runId || collection.runAttempt !== run.runAttempt) {
134
+ contractErrors.push(`${tag}: the collection records run ${collection.runId}#${collection.runAttempt}`);
135
+ continue;
136
+ }
137
+ if (collection.candidate.head !== run.headSha) {
138
+ contractErrors.push(`${tag}: the collection measured head ${collection.candidate.head}`);
139
+ continue;
140
+ }
141
+ admitted.push({ run, collection, complete: errors.length === 0, errors });
142
+ }
143
+
144
+ // 2. One run per SHA: the newest attempt wins; the rest are superseded, never mixed in.
145
+ const perSha = new Map<string, (typeof admitted)[number]>();
146
+ for (const a of admitted) {
147
+ const cur = perSha.get(a.run.headSha);
148
+ if (!cur || newer(a.run, cur.run)) {
149
+ if (cur) excluded.push({ ...refOf(cur.run), reason: `superseded by run ${a.run.runId}#${a.run.runAttempt}` });
150
+ perSha.set(a.run.headSha, a);
151
+ } else {
152
+ excluded.push({ ...refOf(a.run), reason: `superseded by run ${cur.run.runId}#${cur.run.runAttempt}` });
153
+ }
154
+ }
155
+ const finalEntry = perSha.get(input.finalSha);
156
+ if (!finalEntry) contractErrors.push(`no CI collection at the final SHA ${input.finalSha.slice(0, 12)}`);
157
+ else if (!finalEntry.complete)
158
+ contractErrors.push(`final-SHA collection incomplete: ${finalEntry.errors.join("; ")}`);
159
+ const used: (typeof admitted)[number][] = [];
160
+ for (const [sha, a] of perSha) {
161
+ if (sha !== input.finalSha && !a.complete) {
162
+ excluded.push({ ...refOf(a.run), reason: `incomplete ancestor collection, not a basis: ${a.errors.join("; ")}` });
163
+ continue;
164
+ }
165
+ used.push(a);
166
+ }
167
+
168
+ // 3. The final candidate: content from the independent recomputation, C0 environment
169
+ // part (node, node_modules) as the final run witnessed it.
170
+ const crossCheck = finalEntry ? crossCheckCandidate(finalEntry.collection.candidate, input.independent) : [];
171
+ const current: CurrentCandidate = {
172
+ ...input.independent,
173
+ head: input.finalSha,
174
+ // porcelainClean stays the INDEPENDENT candidate's own: a clean final tree is the
175
+ // oracle's to produce and prove, never this function's to assume.
176
+ coreSha256: finalEntry ? finalEntry.collection.candidate.coreSha256 : "unwitnessed",
177
+ };
178
+
179
+ // 4. Receipts in chain order (older first-parent position first), collection index next.
180
+ const maxPos = input.firstParent.length;
181
+ const loaded: LoadedReceipt[] = [];
182
+ const receiptRun = new Map<string, RunRef>();
183
+ const finalSources = new Set<string>();
184
+ for (const a of used) {
185
+ const base = (maxPos - (chainPos.get(a.run.headSha) ?? maxPos)) * 1_000_000;
186
+ a.collection.attempted.forEach((att, i) => {
187
+ if (att.outcome !== "receipt-written" || !att.file) return;
188
+ const bytes = a.run.files[att.file];
189
+ if (!bytes) return;
190
+ if (a.run.headSha === input.finalSha) finalSources.add(`run ${a.run.runId}#${a.run.runAttempt}/${att.file}`);
191
+ loaded.push({
192
+ source: `run ${a.run.runId}#${a.run.runAttempt}/${att.file}`,
193
+ text: bytes.toString("utf8"),
194
+ seq: base + i,
195
+ });
196
+ try {
197
+ receiptRun.set(JSON.parse(bytes.toString("utf8")).receiptSha256, refOf(a.run));
198
+ } catch {
199
+ // unparseable: composeLedger reports it as INVALID
200
+ }
201
+ });
202
+ }
203
+
204
+ // 5. The required set on the final SHA: runner, every group whose newest receipt among
205
+ // the ANCESTOR runs is red (the section that broke — computed before the final run,
206
+ // so re-measuring it green does not erase the requirement), operator-declared.
207
+ const newestPassed = new Map<string, boolean>();
208
+ for (const l of [...loaded].filter((x) => !finalSources.has(x.source)).sort((x, y) => (x.seq ?? 0) - (y.seq ?? 0))) {
209
+ try {
210
+ const r = JSON.parse(l.text);
211
+ newestPassed.set(JSON.stringify(r.group.gate), r.result.passed === true);
212
+ } catch {
213
+ // INVALID, reported by composeLedger
214
+ }
215
+ }
216
+ const requiredKeys = new Set<string>([JSON.stringify(input.independent.coreGroupGate)]);
217
+ for (const [k, passed] of newestPassed) if (!passed) requiredKeys.add(k);
218
+ for (const argv of input.operatorDeclared) requiredKeys.add(JSON.stringify(argv));
219
+ const required = [...requiredKeys].map((k) => JSON.parse(k) as string[]);
220
+
221
+ const ledger = composeLedger(loaded, current, undefined, { mode: "release", required });
222
+ const red = contractErrors.length > 0 || crossCheck.length > 0;
223
+ const verdict = red ? "RED" : ledger.verdict;
224
+ const summary = [
225
+ ...(red ? [`[qualification-release] RED on ${input.finalSha.slice(0, 12)}`] : []),
226
+ ...contractErrors.map((e) => ` CONTRACT ${e}`),
227
+ ...crossCheck.map((e) => ` CROSS-CHECK ${e}: the CI-claimed candidate contradicts the final tree`),
228
+ ...excluded.map((e) => ` EXCLUDED run ${e.runId}#${e.runAttempt}@${e.headSha.slice(0, 12)}: ${e.reason}`),
229
+ ...ledger.summary,
230
+ ];
231
+ const measured = ledger.groups.filter((g) => g.state === "MEASURED" && g.provenance === "exact");
232
+ return {
233
+ ...ledger,
234
+ verdict,
235
+ summary,
236
+ release: {
237
+ finalSha: input.finalSha,
238
+ contractErrors,
239
+ crossCheck,
240
+ used: used.map((a) => refOf(a.run)),
241
+ excluded,
242
+ required,
243
+ denominator: {
244
+ groups: ledger.counts.groups.total,
245
+ claims: ledger.counts.claims.total,
246
+ measuredOnFinal: {
247
+ groups: measured.length,
248
+ claims: measured.reduce((n, g) => n + g.claims.length, 0),
249
+ list: measured.map((g) => g.gate.join(" ")),
250
+ },
251
+ inheritedCore: ledger.groups
252
+ .filter((g) => g.state === "INHERITED-UNVERIFIED" && g.inheritKind === "c-core")
253
+ .map((g) => ({
254
+ gate: g.gate.join(" "),
255
+ from: (g.decidedBy && receiptRun.get(g.decidedBy)) || null,
256
+ reason: g.reasons[0],
257
+ })),
258
+ flagged: ledger.groups
259
+ .filter((g) => g.flags.length > 0)
260
+ .map((g) => `${g.gate.join(" ")}: ${g.flags.join("; ")}`),
261
+ unrun: ledger.counts.groups.UNRUN,
262
+ fail: ledger.counts.groups.FAIL,
263
+ },
264
+ provenance: [
265
+ "inputs are whatever the caller supplied; this function does not fetch or authenticate them",
266
+ "node/node_modules identity is the final run's own witness, not recomputed here",
267
+ "no acceptance class is produced: RISK-PENDING awaits a human decision",
268
+ ],
269
+ },
270
+ };
271
+ }
@@ -81,13 +81,18 @@ LEGACY_PERMISSION_ALLOW = ["mcp__pi-tools-bridge__*"]
81
81
  # native rail stops owning `autoCompactEnabled` / `env.DISABLE_AUTOCOMPACT` too.
82
82
  # Backend-native context management is allowed; we add no guard and only measure.
83
83
  # Both keys live in RETIRED_SETTINGS_SCALARS below.
84
+ #
85
+ # NEITHER IS `showTurnDuration`. It reports how long a turn took, which is
86
+ # OPERATOR INFORMATION — it opens no background autonomy surface and drives no
87
+ # single-driver behaviour, so the policy reason the rest of this list stands on
88
+ # never applied to it. GLG's ruling: the operator owns that scalar and entwurf
89
+ # must not force it to false. It joins RETIRED_SETTINGS_SCALARS below.
84
90
  MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
85
91
  ("cleanupPeriodDays", ["cleanupPeriodDays"], 365),
86
92
  ("promptSuggestionEnabled", ["promptSuggestionEnabled"], False),
87
93
  ("awaySummaryEnabled", ["awaySummaryEnabled"], False),
88
94
  ("autoMemoryEnabled", ["autoMemoryEnabled"], False),
89
95
  ("verbose", ["verbose"], False),
90
- ("showTurnDuration", ["showTurnDuration"], False),
91
96
  ("terminalProgressBarEnabled", ["terminalProgressBarEnabled"], False),
92
97
  ("useAutoModeDuringPlan", ["useAutoModeDuringPlan"], False),
93
98
  ("enableWorkflows", ["enableWorkflows"], False),
@@ -108,6 +113,18 @@ RETIRED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
108
113
  # reach it.
109
114
  ("env.DISABLE_AUTOCOMPACT", ["env", "DISABLE_AUTOCOMPACT"], "1"),
110
115
  ("autoCompactEnabled", ["autoCompactEnabled"], False),
116
+ # `showTurnDuration` returns to the operator for the same structural reason
117
+ # and by the same one-shot path. It is retired rather than deleted because an
118
+ # existing install-state ALREADY carries its entry — measured on the source
119
+ # host 2026-09-23: `~/.claude/entwurf.install-state.json` records
120
+ # `original {existed: true, value: false}` while live settings say `true`,
121
+ # i.e. the operator turned it on after our install. Deleting the constant
122
+ # would orphan that entry, and uninstall() walks every ledger entry through
123
+ # restore_entry(), so it would write `false` back over the operator's own
124
+ # choice. relinquish_retired_scalar() is what makes the inverse honest: the
125
+ # live `true` is not what we last managed, so it is left untouched and the
126
+ # entry is consumed, after which no code path can reach that key again.
127
+ ("showTurnDuration", ["showTurnDuration"], False),
111
128
  ]
112
129
 
113
130
  _managed_scalar_names = {name for name, _path, _desired in MANAGED_SETTINGS_SCALARS}
@@ -37,6 +37,44 @@
37
37
  "signature": "[QK:PI-DOCTOR-PROBES-OVERRIDE]",
38
38
  "signatureSource": "scripts/smoke-pi-provider-state.sh"
39
39
  },
40
+ {
41
+ "claim": "PI-DOCTOR-PLUGIN-COMMAND-IS-NOT-DRIFT",
42
+ "title": "the recorded-command comparison degenerates to 'always drifted', so a Herdr-plugin host whose effective command IS exactly the absolute bridge this install wrote is red again — the #116 M3-b2 false-red restored in the new wording. It is the always-red half of a symmetric pair with PI-DOCTOR-REAL-DRIFT-STILL-RED's always-green half; a mutation that instead re-scoped the comparison under `!isBare` would also kill that claim's bare cell, so this one deliberately does not touch the managedState predicate",
43
+ "subject": "scripts/doctor-pi-provider.ts",
44
+ "find": ["\t\tif (cmd !== stateCommand) {"],
45
+ "replace": ["\t\tif (true) {"],
46
+ "gate": ["bash", "scripts/smoke-pi-provider-state.sh"],
47
+ "timeoutSeconds": 120,
48
+ "signature": "[QK:PI-DOCTOR-PLUGIN-COMMAND-IS-NOT-DRIFT]",
49
+ "signatureSource": "scripts/smoke-pi-provider-state.sh"
50
+ },
51
+ {
52
+ "claim": "PI-DOCTOR-REAL-DRIFT-STILL-RED",
53
+ "title": "the drift comparison is dropped entirely, so a managed key repointed at a command entwurf never wrote reads as 'the managed command this install recorded' — the ownership axis silently stops existing",
54
+ "subject": "scripts/doctor-pi-provider.ts",
55
+ "find": ["\t\tif (cmd !== stateCommand) {"],
56
+ "replace": ["\t\tif (false) {"],
57
+ "gate": ["bash", "scripts/smoke-pi-provider-state.sh"],
58
+ "timeoutSeconds": 120,
59
+ "signature": "[QK:PI-DOCTOR-REAL-DRIFT-STILL-RED]",
60
+ "signatureSource": "scripts/smoke-pi-provider-state.sh"
61
+ },
62
+ {
63
+ "claim": "PI-DOCTOR-LEGACY-STATE-UNCHANGED",
64
+ "title": "the plugin-mode repair is turned into a schema amnesty: a managed record written before `command` existed stops being red and its unrecorded, uncomparable entry is adopted as a note — a green nobody measured, handed out by the doctor instead of by ./run.sh setup",
65
+ "subject": "scripts/doctor-pi-provider.ts",
66
+ "find": [
67
+ "\t\t\tlog(` FAIL: state owns entwurf-bridge (ownership=${ownership}) but the effective command drifted to '${cmd}'.`);",
68
+ "\t\t\thardFail = 1;"
69
+ ],
70
+ "replace": [
71
+ "\t\t\tlog(` note: legacy record (ownership=${ownership}) — no recorded command, adopting '${cmd}'.`);"
72
+ ],
73
+ "gate": ["bash", "scripts/smoke-pi-provider-state.sh"],
74
+ "timeoutSeconds": 120,
75
+ "signature": "[QK:PI-DOCTOR-LEGACY-STATE-UNCHANGED]",
76
+ "signatureSource": "scripts/smoke-pi-provider-state.sh"
77
+ },
40
78
  {
41
79
  "claim": "AGY-DOCTOR-PROBES-ARGS",
42
80
  "title": "the agy invocation parser drops configured args, so the doctor boots command defaults while agy executes a failing argv",
@@ -0,0 +1,235 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "control-send-receipt",
4
+ "mutants": [
5
+ {
6
+ "claim": "SEND-RECEIPT-IDLE-IS-SENT",
7
+ "title": "an idle receiver's DIRECT trigger is reported as a queue it never touched, so the one boundary that actually started a turn becomes indistinguishable from the two that did not",
8
+ "subject": "pi-extensions/lib/control-send-receipt.ts",
9
+ "find": ["\tif (input.idle) return \"sent\";"],
10
+ "replace": ["\tif (false) return \"sent\";"],
11
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
12
+ "timeoutSeconds": 600,
13
+ "signature": "[QK:SEND-RECEIPT-IDLE-IS-SENT]",
14
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
15
+ },
16
+ {
17
+ "claim": "SEND-RECEIPT-BUSY-STEER-IS-QUEUED",
18
+ "title": "a busy receiver's steering queue is reported as `sent`, which is the exact false claim #120 was opened about: the sender is told the message landed while it lies in volatile process memory behind whatever else is there",
19
+ "subject": "pi-extensions/lib/control-send-receipt.ts",
20
+ "find": ["\treturn input.mode === \"follow_up\" ? \"queued-follow-up\" : \"queued-steer\";"],
21
+ "replace": ["\treturn input.mode === \"follow_up\" ? \"queued-follow-up\" : \"sent\";"],
22
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
23
+ "timeoutSeconds": 600,
24
+ "signature": "[QK:SEND-RECEIPT-BUSY-STEER-IS-QUEUED]",
25
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
26
+ },
27
+ {
28
+ "claim": "SEND-RECEIPT-BUSY-FOLLOWUP-IS-QUEUED",
29
+ "title": "the follow-up queue is reported under the steering queue's name, collapsing the only fact that explains a reorder after it happens — a later steer overtakes every earlier follow_up, and one name for both makes that unaccountable",
30
+ "subject": "pi-extensions/lib/control-send-receipt.ts",
31
+ "find": ["? \"queued-follow-up\" : \"queued-steer\";"],
32
+ "replace": ["? \"queued-steer\" : \"queued-steer\";"],
33
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
34
+ "timeoutSeconds": 600,
35
+ "signature": "[QK:SEND-RECEIPT-BUSY-FOLLOWUP-IS-QUEUED]",
36
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
37
+ },
38
+ {
39
+ "claim": "SEND-RECEIPT-UNKNOWN-IS-ACCEPTED-NOT-SENT",
40
+ "title": "an answer this build cannot classify is reported as `sent`, so a version-skewed peer's acceptance is dressed as a direct injection nobody observed",
41
+ "subject": "pi-extensions/lib/control-send-receipt.ts",
42
+ "find": ["\treturn isObservedBoundary(value) ? value : \"accepted-unknown-boundary\";"],
43
+ "replace": ["\treturn isObservedBoundary(value) ? value : \"sent\";"],
44
+ "gate": ["bash", "run.sh", "check-control-send-receipt"],
45
+ "timeoutSeconds": 600,
46
+ "signature": "[QK:SEND-RECEIPT-UNKNOWN-IS-ACCEPTED-NOT-SENT]",
47
+ "signatureSource": "pi-extensions/lib/control-send-receipt.test.ts"
48
+ },
49
+ {
50
+ "claim": "SEND-BOUNDARY-CARRIED",
51
+ "title": "the primary leg reports a fixed `sent` instead of the boundary the receiver actually named, so every busy acceptance reads as a direct trigger again",
52
+ "subject": "pi-extensions/lib/entwurf-v2-send.ts",
53
+ "find": [
54
+ "\treturn result.success ? { outcome: \"sent\", boundary: requireBoundary(result) } : inBandRejected(result);"
55
+ ],
56
+ "replace": ["\treturn result.success ? { outcome: \"sent\", boundary: \"sent\" } : inBandRejected(result);"],
57
+ "gate": ["bash", "run.sh", "check-entwurf-v2-send"],
58
+ "timeoutSeconds": 600,
59
+ "signature": "[QK:SEND-BOUNDARY-CARRIED]",
60
+ "signatureSource": "pi-extensions/lib/entwurf-v2-send.test.ts"
61
+ },
62
+ {
63
+ "claim": "SEND-FALLBACK-KEEPS-BOUNDARY",
64
+ "title": "a re-resolved socket drops the boundary it was handed, so a first socket that died and a second that was BUSY come out reading as a delivery — the route axis erasing the receipt axis",
65
+ "subject": "pi-extensions/lib/entwurf-v2-send.ts",
66
+ "find": [
67
+ "\t\t\t\treturn r.success ? { outcome: \"fallback-sent\", boundary: requireBoundary(r) } : inBandRejected(r);"
68
+ ],
69
+ "replace": ["\t\t\t\treturn r.success ? { outcome: \"fallback-sent\", boundary: \"sent\" } : inBandRejected(r);"],
70
+ "gate": ["bash", "run.sh", "check-entwurf-v2-send"],
71
+ "timeoutSeconds": 600,
72
+ "signature": "[QK:SEND-FALLBACK-KEEPS-BOUNDARY]",
73
+ "signatureSource": "pi-extensions/lib/entwurf-v2-send.test.ts"
74
+ },
75
+ {
76
+ "claim": "SEND-SOCKET-SUCCESS-NEEDS-BOUNDARY",
77
+ "title": "a successful socket answer with no boundary is accepted silently, so a bypassed wire seam — the one shape that must never pass for a direct trigger — reads exactly like one",
78
+ "subject": "pi-extensions/lib/entwurf-v2-send.ts",
79
+ "find": ["\tif (result.boundary === undefined) {"],
80
+ "replace": ["\tif (false) {"],
81
+ "gate": ["bash", "run.sh", "check-entwurf-v2-send"],
82
+ "timeoutSeconds": 600,
83
+ "signature": "[QK:SEND-SOCKET-SUCCESS-NEEDS-BOUNDARY]",
84
+ "signatureSource": "pi-extensions/lib/entwurf-v2-send.test.ts"
85
+ },
86
+ {
87
+ "claim": "SEND-RECEIVER-EMITS-BOUNDARY",
88
+ "title": "the receiver goes back to answering with a bare `delivered:true`, which was equally true of a direct trigger and of a message rotting in a queue — the bare `delivered` DELIVERY.md refuses by name, and the sender is left with nothing to render",
89
+ "subject": "pi-extensions/entwurf-control.ts",
90
+ "find": ["\t\t\tboundary: sendReceiptBoundary({ idle: isIdle, mode }),"],
91
+ "replace": ["\t\t\tdelivered: true,"],
92
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
93
+ "timeoutSeconds": 300,
94
+ "signature": "[QK:SEND-RECEIVER-EMITS-BOUNDARY]",
95
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
96
+ },
97
+ {
98
+ "claim": "SEND-SURFACE-RENDERS-BOUNDARY",
99
+ "title": "the sender surface renders the internal route word again instead of the receiver's boundary, putting `control-socket → sent` back on a message that is sitting in a queue",
100
+ "subject": "pi-extensions/lib/entwurf-v2-surface.ts",
101
+ "find": [
102
+ "\t\t\t\t\treturn { text: `entwurf_v2 control-socket → ${boundary}${route}${enqueued}`, isError: false };"
103
+ ],
104
+ "replace": [
105
+ "\t\t\t\t\treturn { text: `entwurf_v2 control-socket → ${o.outcome}${route}${enqueued}`, isError: false };"
106
+ ],
107
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
108
+ "timeoutSeconds": 300,
109
+ "signature": "[QK:SEND-SURFACE-RENDERS-BOUNDARY]",
110
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
111
+ },
112
+ {
113
+ "claim": "SEND-SURFACE-UNKNOWN-NOT-SENT",
114
+ "title": "an unclassifiable acceptance is rendered with the word `sent`, which is the claim the whole axis exists to stop making — it was accepted, and nobody observed a direct injection",
115
+ "subject": "pi-extensions/lib/entwurf-v2-surface.ts",
116
+ "find": [
117
+ "\t\t\t\t\t\to.boundary ?? (o.outcome === \"fallback-sent\" ? \"mailbox-enqueued\" : \"accepted-unknown-boundary\");"
118
+ ],
119
+ "replace": [
120
+ "\t\t\t\t\t\to.boundary === \"accepted-unknown-boundary\" ? \"sent\" : (o.boundary ?? \"mailbox-enqueued\");"
121
+ ],
122
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
123
+ "timeoutSeconds": 300,
124
+ "signature": "[QK:SEND-SURFACE-UNKNOWN-NOT-SENT]",
125
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
126
+ },
127
+ {
128
+ "claim": "SEND-SURFACE-FALLBACK-KEEPS-BOUNDARY",
129
+ "title": "the re-resolved route annotation is dropped, so a caller cannot tell that its first socket was dead — the two legs render identically and a dead-socket host looks healthy",
130
+ "subject": "pi-extensions/lib/entwurf-v2-surface.ts",
131
+ "find": ["\t\t\t\t\tconst route = o.outcome === \"fallback-sent\" ? \" (via a re-resolved route)\" : \"\";"],
132
+ "replace": ["\t\t\t\t\tconst route = \"\";"],
133
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
134
+ "timeoutSeconds": 300,
135
+ "signature": "[QK:SEND-SURFACE-FALLBACK-KEEPS-BOUNDARY]",
136
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
137
+ },
138
+ {
139
+ "claim": "SEND-PROSE-NO-INTERRUPT",
140
+ "title": "the caller-facing description of `mode` goes back to promising that steer interrupts the current turn — a caller picks it for urgency and gets a queue drained only after the turn ends, with no order against the other queue",
141
+ "subject": "pi-extensions/lib/entwurf-v2-contract-schema.ts",
142
+ "find": ["\t\t\t\t\"delivery mode — NOT the ownership axis (F1) nor liveness routing."],
143
+ "replace": [
144
+ "\t\t\t\t\"delivery mode (steer = interrupt current turn, follow_up = queue) — NOT the ownership axis (F1) nor liveness routing."
145
+ ],
146
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
147
+ "timeoutSeconds": 300,
148
+ "signature": "[QK:SEND-PROSE-NO-INTERRUPT]",
149
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
150
+ },
151
+ {
152
+ "claim": "SEND-DIRTY-LOCK-NAMES-ACCEPTANCE",
153
+ "title": "the dirty-lock line goes back to `DELIVERED (<route>)` for all three shapes that reach it — a refusal that delivered nothing, a message lying in a volatile queue, and a durable mailbox file all read as one word, at the exact moment an operator is deciding BY HAND whether anything still needs sending",
154
+ "subject": "pi-extensions/lib/entwurf-v2-surface.ts",
155
+ "find": [
156
+ "\t\t\t\t\t\t? `rejected${result.finalizedRejectReason ? ` (${result.finalizedRejectReason})` : \"\"} — the receiver refused it; nothing was accepted`"
157
+ ],
158
+ "replace": ["\t\t\t\t\t\t? `DELIVERED (${result.finalizedOutcome})`"],
159
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
160
+ "timeoutSeconds": 300,
161
+ "signature": "[QK:SEND-DIRTY-LOCK-NAMES-ACCEPTANCE]",
162
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
163
+ },
164
+ {
165
+ "claim": "VITEST-TEST-DIR-ON-THE-FLOOR",
166
+ "title": "the pi queue oracle's public coordinate is dropped from `check:vitest`, so the file that proves every claim the receipt makes about a receiver's queues is green only when somebody runs `pnpm test` by hand — the discovery door deliberately does not select `test/**`, so nothing else would notice",
167
+ "subject": "package.json",
168
+ "find": [
169
+ "\"check:vitest\": \"./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior\""
170
+ ],
171
+ "replace": [
172
+ "\"check:vitest\": \"./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-tests-beside-behavior\""
173
+ ],
174
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
175
+ "timeoutSeconds": 600,
176
+ "signature": "[QK:VITEST-TEST-DIR-ON-THE-FLOOR]",
177
+ "signatureSource": "run.sh"
178
+ },
179
+ {
180
+ "claim": "PI-CORE-PIN-TRACKS-FLOOR",
181
+ "title": "the queue oracle's pi-agent-core pin drifts off the floor the rest of the pi runtime is pinned to, so the oracle keeps answering honestly — about a pi nobody in this repo runs",
182
+ "subject": "package.json",
183
+ "find": ["\"@earendil-works/pi-agent-core\": \"0.87.1\","],
184
+ "replace": ["\"@earendil-works/pi-agent-core\": \"0.86.0\","],
185
+ "gate": ["bash", "run.sh", "check-dep-versions"],
186
+ "timeoutSeconds": 300,
187
+ "signature": "[QK:PI-CORE-PIN-TRACKS-FLOOR]",
188
+ "signatureSource": "run.sh"
189
+ },
190
+ {
191
+ "claim": "SEND-DIRTY-LOCK-ERROR-CARRIES-ACCEPTANCE",
192
+ "title": "the structured dirty-lock error stops carrying the refusal's reason, so a receiver that said WHY it refused loses that word at the one moment an operator is deciding by hand whether the message still needs sending — the render cell cannot see this, because it builds its own result object",
193
+ "subject": "pi-extensions/lib/entwurf-v2-send.ts",
194
+ "find": ["\t\t\t\trejectReason: drive.rejectReason,"],
195
+ "replace": ["\t\t\t\trejectReason: undefined,"],
196
+ "gate": ["bash", "run.sh", "check-entwurf-v2-send"],
197
+ "timeoutSeconds": 600,
198
+ "signature": "[QK:SEND-DIRTY-LOCK-ERROR-CARRIES-ACCEPTANCE]",
199
+ "signatureSource": "pi-extensions/lib/entwurf-v2-send.test.ts"
200
+ },
201
+ {
202
+ "claim": "SEND-DIRTY-LOCK-RUNNER-MOVES-ACCEPTANCE",
203
+ "title": "the runner drops the acceptance fields on the way from the thrown error to the result, so the hand proves it fills them and the surface proves it renders them while the operator still sees nothing — the classic middle-seam hole two端 tests cannot cover",
204
+ "subject": "pi-extensions/lib/entwurf-v2-runner.ts",
205
+ "find": ["\t\t\t\t\t\tfinalizedRejectReason: err.finalizedRejectReason,"],
206
+ "replace": ["\t\t\t\t\t\tfinalizedRejectReason: undefined,"],
207
+ "gate": ["bash", "run.sh", "check-entwurf-v2-runner"],
208
+ "timeoutSeconds": 600,
209
+ "signature": "[QK:SEND-DIRTY-LOCK-RUNNER-MOVES-ACCEPTANCE]",
210
+ "signatureSource": "pi-extensions/lib/entwurf-v2-runner.test.ts"
211
+ },
212
+ {
213
+ "claim": "VITEST-TEST-DIR-HAS-COORDINATE",
214
+ "title": "the only line that EXECUTES the pi queue oracle is deleted while its verb and its explanatory comment stay, so the file is run by nothing at all — and a checker that accepted a mention instead of a call would keep reporting the floor as complete",
215
+ "subject": "run.sh",
216
+ "find": [" run_vitest test/pi-queue.oracle.test.ts"],
217
+ "replace": [" : # the oracle is no longer executed here"],
218
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
219
+ "timeoutSeconds": 600,
220
+ "signature": "[QK:VITEST-TEST-DIR-HAS-COORDINATE]",
221
+ "signatureSource": "run.sh"
222
+ },
223
+ {
224
+ "claim": "SEND-OUTCOME-LIST-NAMES-EVERY-RAIL",
225
+ "title": "the caller-facing outcome list drops the native-push rail, so a caller reading the one blurb it reads before dispatching is told the control rail's vocabulary covers a rail that answers with a direct injection instead",
226
+ "subject": "mcp/entwurf-bridge/src/index.ts",
227
+ "find": ["\t\t\"native-push INJECTION, a rejection, or lock-dirty. EXISTING targets only; discover with \" +"],
228
+ "replace": ["\t\t\"a rejection, or lock-dirty. EXISTING targets only; discover with \" +"],
229
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
230
+ "timeoutSeconds": 300,
231
+ "signature": "[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL]",
232
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
233
+ }
234
+ ]
235
+ }
@@ -15,10 +15,10 @@
15
15
  },
16
16
  {
17
17
  "claim": "HFC-PILOT-CLOSED",
18
- "title": "a backend nobody measured on herdr is admitted to the pilot, so a caller gets a pane running something this rail never proved it can open",
19
- "subject": "pi-extensions/lib/herdr-fresh-call.ts",
20
- "find": ["export const HERDR_FRESH_CALL_BACKENDS = [\"pi\", \"claude-code\"] as const;"],
21
- "replace": ["export const HERDR_FRESH_CALL_BACKENDS = [\"pi\", \"claude-code\", \"codex\"] as const;"],
18
+ "title": "a backend nobody measured on herdr is admitted to the pilot, so a caller gets a pane running something this rail never proved it can open (#116 A7: the tuple moved to the shared rails leaf, so the mutant follows its subject — widening the pilot set is still the mutation, it is simply written in one place now)",
19
+ "subject": "scripts/herdr-rails.mjs",
20
+ "find": ["export const HERDR_FRESH_CALL_BACKENDS = Object.freeze([\"pi\", \"claude-code\"]);"],
21
+ "replace": ["export const HERDR_FRESH_CALL_BACKENDS = Object.freeze([\"pi\", \"claude-code\", \"codex\"]);"],
22
22
  "gate": ["bash", "run.sh", "check-herdr-fresh-call"],
23
23
  "timeoutSeconds": 120,
24
24
  "signature": "[QK:HFC-PILOT-CLOSED]",
@@ -132,14 +132,10 @@
132
132
  },
133
133
  {
134
134
  "claim": "HPB-USAGE-NOTE-NAMES-A-MODEL",
135
- "title": "the install names a model id in the wrong harness's grammar, so the first fresh call an operator copies is refused",
136
- "subject": "plugins/herdr/lib/build.mjs",
137
- "find": [
138
- "\t\tnotes.push(\"claude-code: a model id looks like `claude-sonnet-5` — the vendor id, not a provider path.\");"
139
- ],
140
- "replace": [
141
- "\t\tnotes.push(\"claude-code: a model id looks like `anthropic/claude-sonnet-5` — provider-qualified.\");"
142
- ],
135
+ "title": "the install names a model id in the wrong harness's grammar, so the first fresh call an operator copies is refused (#116 A7: the example moved to the shared rails leaf; the mutation still gives claude-code a provider-qualified id, which is the grammar confusion this claim exists to catch)",
136
+ "subject": "scripts/herdr-rails.mjs",
137
+ "find": ["\t\"claude-code\": \"claude-sonnet-5\","],
138
+ "replace": ["\t\"claude-code\": \"anthropic/claude-sonnet-5\","],
143
139
  "gate": ["bash", "run.sh", "check-herdr-plugin-build"],
144
140
  "timeoutSeconds": 180,
145
141
  "signature": "[QK:HPB-USAGE-NOTE-NAMES-A-MODEL]",
@@ -147,10 +143,10 @@
147
143
  },
148
144
  {
149
145
  "claim": "HPB-INSTALL-USAGE-NOTE",
150
- "title": "dropping the pi line leaves the one backend whose wiring is invisible from the operator's chair unexplained: claude-code picks the tools up through MCP with nothing added, while a plain pi loads the extension on every session of the host and is still not a citizen — a green install followed by a silent, tool-less pi is indistinguishable from an install that did nothing",
146
+ "title": "dropping the pi line leaves the one backend whose wiring is invisible from the operator's chair unexplained: claude-code picks the tools up through MCP with nothing added, while a plain pi loads the extension on every session of the host and is still not a citizen — a green install followed by a silent, tool-less pi is indistinguishable from an install that did nothing (#116 A7: usageNotes now loops the shared backend tuple instead of a literal pair, so the mutation suppresses pi's notes at the loop guard rather than at a per-backend `if`)",
151
147
  "subject": "plugins/herdr/lib/build.mjs",
152
- "find": ["\tif (activated.includes(\"pi\")) {"],
153
- "replace": ["\tif (false && activated.includes(\"pi\")) {"],
148
+ "find": ["\t\tif (!activated.includes(backend)) continue;"],
149
+ "replace": ["\t\tif (backend === \"pi\" || !activated.includes(backend)) continue;"],
154
150
  "gate": ["bash", "run.sh", "check-herdr-plugin-build"],
155
151
  "timeoutSeconds": 300,
156
152
  "signature": "[QK:HPB-INSTALL-USAGE-NOTE]",