@junghanacs/entwurf 0.23.1 → 0.25.0

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 (107) hide show
  1. package/BASELINE.md +1 -0
  2. package/CHANGELOG.md +628 -0
  3. package/DELIVERY.md +24 -1
  4. package/FAQ.md +47 -0
  5. package/README.md +5 -3
  6. package/VERIFY.md +20 -2
  7. package/demo/README.md +5 -2
  8. package/docs/acp-backend-rail.md +49 -2
  9. package/docs/setup-clean-host.md +8 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
  23. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
  24. package/mcp/entwurf-bridge/dist/protocol.js +0 -7
  25. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
  26. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
  27. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  28. package/mcp/entwurf-bridge/src/index.ts +46 -5
  29. package/mcp/entwurf-bridge/test.sh +2 -2
  30. package/package.json +11 -9
  31. package/pi-extensions/entwurf-control.ts +83 -154
  32. package/pi-extensions/lib/acp/backend.ts +16 -6
  33. package/pi-extensions/lib/acp/context.ts +30 -10
  34. package/pi-extensions/lib/acp/session-store.ts +2 -2
  35. package/pi-extensions/lib/callback-env.ts +78 -0
  36. package/pi-extensions/lib/compaction-send-guard.ts +6 -1
  37. package/pi-extensions/lib/control-send-receipt.ts +108 -0
  38. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  39. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
  40. package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
  42. package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
  43. package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
  44. package/pi-extensions/lib/fresh-call-composition.ts +56 -22
  45. package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
  46. package/pi-extensions/lib/herdr-placement.ts +24 -4
  47. package/pi-extensions/lib/mux-fresh-call.ts +23 -8
  48. package/pi-extensions/meta-bridge-omp.ts +35 -48
  49. package/protocol.js +0 -8
  50. package/run.sh +306 -53
  51. package/scripts/check-acp-backend-preflight.ts +117 -9
  52. package/scripts/check-acp-carrier-augment.ts +6 -4
  53. package/scripts/check-acp-event-mapper.ts +17 -6
  54. package/scripts/check-acp-prompt-builder.ts +17 -9
  55. package/scripts/check-acp-prompt-lifecycle.ts +23 -6
  56. package/scripts/check-acp-session-reuse.ts +64 -51
  57. package/scripts/check-acp-session-store.ts +31 -8
  58. package/scripts/check-acp-stop-reason.ts +20 -2
  59. package/scripts/check-acp-stream-hooks.ts +22 -5
  60. package/scripts/check-acp-usage-accounting.ts +33 -9
  61. package/scripts/check-entwurf-bridge-boot.ts +16 -1
  62. package/scripts/check-entwurf-v2-surface.ts +289 -10
  63. package/scripts/check-fresh-call-dispatch.ts +4 -5
  64. package/scripts/check-gate-qualification.ts +51 -9
  65. package/scripts/check-herdr-activation.ts +73 -0
  66. package/scripts/check-herdr-fresh-call.ts +30 -6
  67. package/scripts/check-herdr-placement.ts +51 -0
  68. package/scripts/check-herdr-plugin-build.ts +11 -4
  69. package/scripts/check-herdr-plugin.ts +745 -38
  70. package/scripts/check-herdr-sandbox.ts +14 -2
  71. package/scripts/check-install-container.sh +1 -1
  72. package/scripts/check-mux-launcher-fence.ts +37 -0
  73. package/scripts/check-pi-launch.ts +8 -2
  74. package/scripts/check-release-gate-outcomes.ts +165 -0
  75. package/scripts/doctor-pi-provider.ts +44 -4
  76. package/scripts/herdr-rails.mjs +75 -0
  77. package/scripts/lib/mutation-qualify.ts +1 -1
  78. package/scripts/meta-bridge-state.py +18 -1
  79. package/scripts/mutants/acp-backend-preflight.json +20 -0
  80. package/scripts/mutants/bridge-boot-resume.json +26 -0
  81. package/scripts/mutants/bridge-command-boot.json +38 -0
  82. package/scripts/mutants/control-send-receipt.json +235 -0
  83. package/scripts/mutants/herdr-activation.json +16 -0
  84. package/scripts/mutants/herdr-fresh-call.json +15 -4
  85. package/scripts/mutants/herdr-plugin-build.json +7 -11
  86. package/scripts/mutants/herdr-plugin.json +177 -9
  87. package/scripts/mutants/meta-retire.json +22 -0
  88. package/scripts/mutants/mux-fresh-call.json +21 -8
  89. package/scripts/mutants/omp-fresh.json +5 -9
  90. package/scripts/mutants/pack-install.json +4 -4
  91. package/scripts/mutants/pi-floor.json +17 -0
  92. package/scripts/mutants/release-gate.json +48 -0
  93. package/scripts/mutants/retired-sent-surface.json +32 -0
  94. package/scripts/mutants/v2-surface.json +7 -7
  95. package/scripts/probe-bridge-command.ts +1 -0
  96. package/scripts/smoke-acp-session-reuse-live.ts +23 -5
  97. package/scripts/smoke-agy-install-state.sh +2 -2
  98. package/scripts/smoke-codex-fresh-live.ts +94 -13
  99. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  100. package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
  101. package/scripts/smoke-herdr-raw-install-live.sh +90 -5
  102. package/scripts/smoke-meta-install-state.sh +107 -1
  103. package/scripts/smoke-mux-lifecycle-live.ts +9 -2
  104. package/scripts/smoke-omp-fresh-live.ts +4 -2
  105. package/scripts/smoke-omp-mcp-state.sh +1 -1
  106. package/scripts/smoke-pi-provider-state.sh +108 -2
  107. package/scripts/smoke-setup-verdict.sh +18 -2
@@ -44,6 +44,9 @@ import fs from "node:fs";
44
44
  import os from "node:os";
45
45
  import path from "node:path";
46
46
  import { parse as parseToml } from "smol-toml";
47
+ import * as HERDR_RAILS from "../scripts/herdr-rails.mjs";
48
+ import * as ACTIVATION from "./herdr-activation.mjs";
49
+ import * as RUNTIME from "./herdr-runtime.mjs";
47
50
  import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
48
51
 
49
52
  let passed = 0;
@@ -128,21 +131,42 @@ const manifest = parseToml(fs.readFileSync(path.join(PLUGIN_DIR, "herdr-plugin.t
128
131
  code.includes('placement.kind === "herdr-pane"') && code.includes("placement.paneId"),
129
132
  );
130
133
  // Not a word scan — the closing line of the report NAMES entwurf_v2 on purpose, to tell
131
- // the reader where delivery actually lives. What must be absent is the ability to DO it:
132
- // one spawn helper, and only the two read argvs anywhere in the file.
134
+ // the reader where delivery actually lives. What must be absent is the ability to DO it.
135
+ //
136
+ // #116 A2/P1: the READS grew to three and the CALL SITE stayed at one. herdr's `integration
137
+ // status` has no machine format at all (integration-profile.mjs: `--json`/`--format=json`/
138
+ // `-o json` each exit 2), so its bytes go to a prose parser instead of `JSON.parse` — that
139
+ // buys a second PARSER, which is a different thing from a second way to start a process. One
140
+ // `spawnOnce` serves both, and the argv SET below is what proves the pane cannot install,
141
+ // configure or deliver.
133
142
  ok("there is exactly one child-process call site", code.split("spawnSync(").length - 1 === 1);
134
143
  // The SET of argvs, not the sequence: how many times each read happens is counted for
135
144
  // real below, and duplicating that check here would steal that claim's kill.
136
145
  const argvs = [
137
- ...new Set([...code.matchAll(/readJson\([^,]+,\s*(\[[^\]]*\])/g)].map((m) => (m[1] ?? "").replace(/\s+/g, ""))),
146
+ ...new Set(
147
+ [...code.matchAll(/(?:readJson|spawnOnce)\([^,]+,\s*(\[[^\]]*\])/g)]
148
+ .map((m) => (m[1] ?? "").replace(/\s+/g, ""))
149
+ .filter((a) => a !== ""),
150
+ ),
138
151
  ].sort();
139
152
  ok(
140
- "the only argvs it can spawn are the two reads — nothing installs, configures, or delivers",
141
- JSON.stringify(argvs) === JSON.stringify(['["agent","list"]', '["peer-facts"]']),
153
+ "the only argvs it can spawn are the three reads — nothing installs, configures, or delivers",
154
+ JSON.stringify(argvs) === JSON.stringify(['["agent","list"]', '["integration","status"]', '["peer-facts"]']),
142
155
  );
143
156
  }
144
157
 
145
158
  // ── behavioural: the real entry, stubbed inputs, counted calls ──────────────
159
+ const agentList = (agents: unknown[]): string =>
160
+ JSON.stringify({ id: "cli:agent:list", result: { agents, type: "agent_list" } });
161
+
162
+ /** Herdr's own listing shape, both selected atoms current. Measured grammar, not invented. */
163
+ const INTEGRATION_BOTH_CURRENT = [
164
+ "pi: current (v8) (/h/pi)",
165
+ "claude: current (v8) (/h/claude)",
166
+ "opencode: not installed (/h/oc)",
167
+ "",
168
+ ].join("\n");
169
+
146
170
  interface World {
147
171
  dir: string;
148
172
  callLog: string;
@@ -156,6 +180,123 @@ function world(prefix: string): World {
156
180
  return { dir, callLog: path.join(dir, "calls"), binDir };
157
181
  }
158
182
 
183
+ /**
184
+ * A herdr stub that answers DIFFERENTLY per verb, which the pane now requires: `integration
185
+ * status` returns a prose listing and `agent list` returns JSON, and a single-payload stub
186
+ * would feed one of them the other's bytes. Both answers are still recorded in one call log,
187
+ * so the read-count claim below is unchanged in kind.
188
+ */
189
+ function stubHerdr(
190
+ w: World,
191
+ opts: { integration?: string; agents?: string; agentsExit?: number; integrationExit?: number },
192
+ ): string {
193
+ // The payloads are EMBEDDED and emitted with `printf`, a shell builtin, rather than read back
194
+ // with `cat`. The skip cell runs with PATH set to this bin dir alone — that is the whole point
195
+ // of it — and a stub that shells out to an external tool answers an empty body there while
196
+ // still exiting 0, which reads as "herdr said nothing" instead of "the stub is broken".
197
+ const p = path.join(w.binDir, "herdr");
198
+ fs.writeFileSync(
199
+ p,
200
+ `#!/bin/sh\nprintf '%s %s\\n' "herdr" "$*" >> "${w.callLog}"\n` +
201
+ `case "$1 $2" in\n` +
202
+ ` "integration status") printf '%s' ${shq(opts.integration ?? INTEGRATION_BOTH_CURRENT)}; exit ${opts.integrationExit ?? 0} ;;\n` +
203
+ ` *) printf '%s' ${shq(opts.agents ?? agentList([]))}; exit ${opts.agentsExit ?? 0} ;;\n` +
204
+ `esac\n`,
205
+ );
206
+ fs.chmodSync(p, 0o755);
207
+ return p;
208
+ }
209
+
210
+ /**
211
+ * A `ready` npm artifact identity, in the EXACT key set `herdr-runtime.mjs` certifies
212
+ * (`ARTIFACT_KEYS.npm.ready`), with `observedDigest` in the exact `sha256-<64 hex>` form the reader
213
+ * enforces. Two earlier fixtures — a short key set, then a `sha512-` digest — each produced
214
+ * `activation-ledger-uncertified`, which is the reader doing its job; a ledger the real reader
215
+ * rejects proves nothing about a healthy host.
216
+ */
217
+ const READY_NPM_IDENTITY = {
218
+ kind: "npm",
219
+ name: "@junghanacs/entwurf",
220
+ version: "0.24.0",
221
+ expectedIntegrity: "sha512-AAAA",
222
+ observedDigest: `sha256-${"a".repeat(64)}`,
223
+ };
224
+
225
+ /**
226
+ * Turn a world into a host that HAS activated: the installed reader, a certified ledger whose
227
+ * `runtimeRoot` is the one this host derives, and an executable runtime bin. Returns the env
228
+ * overrides that point the pane at it, so a cell can opt in without repeating the fixture.
229
+ */
230
+ function activateHost(w: World, opts: { runtimeBinPayload?: string; logAs?: string } = {}): Record<string, string> {
231
+ const home = path.join(w.dir, "activated-home");
232
+ const dataHome = path.join(home, ".local", "share");
233
+ const stateHome = path.join(home, ".local", "state");
234
+ const activeDir = path.join(dataHome, "entwurf", "herdr-plugin", "runtime", "active");
235
+ fs.mkdirSync(path.join(activeDir, "node_modules", ".bin"), { recursive: true });
236
+ const readerDir = path.join(activeDir, "node_modules", "@junghanacs", "entwurf", "scripts");
237
+ fs.mkdirSync(readerDir, { recursive: true });
238
+ installReader(readerDir);
239
+ const bin = path.join(activeDir, "node_modules", ".bin", "entwurf");
240
+ fs.writeFileSync(
241
+ bin,
242
+ `#!/bin/sh\nprintf '%s %s\\n' "${opts.logAs ?? "entwurf"}" "$*" >> "${w.callLog}"\nprintf '%s' ${shq(opts.runtimeBinPayload ?? PEERS)}\n`,
243
+ );
244
+ fs.chmodSync(bin, 0o755);
245
+ writeLedgerAt({ HOME: home, XDG_STATE_HOME: stateHome }, ledgerBody(activeDir));
246
+ return { HOME: home, XDG_DATA_HOME: dataHome, XDG_STATE_HOME: stateHome };
247
+ }
248
+
249
+ /**
250
+ * Put a ledger where the WRITER would put it, and never by spelling the path out here.
251
+ *
252
+ * `[측정]` `scripts/herdr-activation.mjs:85-96` resolves the ledger under XDG **state**, while
253
+ * `scripts/herdr-runtime.mjs:221-242` installs the runtime under XDG **data**. A fixture that wrote
254
+ * its own `<data>/entwurf/herdr-plugin/activation.json` therefore agreed with a READER that looked
255
+ * in the wrong root and disagreed with every real host — a green cell certifying a pane that, on a
256
+ * correctly activated host, died with `TypeError: Cannot read properties of null (reading
257
+ * 'runtimeRoot')` before drawing a single block. So the fixture calls the writer's own resolver: a
258
+ * locator that drifts again cannot be matched by a gate that drifted with it.
259
+ */
260
+ function writeLedgerAt(env: { HOME: string; XDG_STATE_HOME: string }, body: Record<string, unknown>): string {
261
+ const ledgerPath = ACTIVATION.resolveActivationLayout(env).ledgerPath as string;
262
+ fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
263
+ fs.writeFileSync(ledgerPath, `${JSON.stringify(body, null, 2)}\n`);
264
+ return ledgerPath;
265
+ }
266
+
267
+ /** A certified ledger body for a given runtime root. */
268
+ function ledgerBody(activeDir: string): Record<string, unknown> {
269
+ return {
270
+ schemaVersion: 2,
271
+ phase: "active",
272
+ runtimeRoot: activeDir,
273
+ artifactIdentity: READY_NPM_IDENTITY,
274
+ piAgentDir: { path: "/p", source: "default" },
275
+ claudeConfigDir: { path: "/c", source: "default" },
276
+ claudeUserConfig: { path: "/c/u", source: "HOME" },
277
+ activatedBackends: ["pi"],
278
+ components: [{ backend: "pi", state: "active" }],
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Lay down the INSTALLED runtime's own copy of the activation reader — the artifact that WROTE the
284
+ * ledger (build.mjs:70,113 resolve the activation verb inside the installed package, never in this
285
+ * checkout). It is a two-file module: `herdr-activation.mjs` value-imports `herdr-runtime.mjs`, and
286
+ * copying only the first produced `activation-reader-unavailable` — correct behaviour against a
287
+ * broken install, and the wrong fixture for a healthy one.
288
+ */
289
+ function installReader(readerScripts: string): void {
290
+ for (const f of ["herdr-activation.mjs", "herdr-runtime.mjs"]) {
291
+ fs.copyFileSync(path.join(REPO, "scripts", f), path.join(readerScripts, f));
292
+ }
293
+ }
294
+
295
+ /** Single-quote a payload for /bin/sh. The only escape a single-quoted word needs is its own quote. */
296
+ function shq(value: string): string {
297
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
298
+ }
299
+
159
300
  /** A stub that records its argv and then answers with the given bytes / exit code. */
160
301
  function stub(w: World, name: string, payload: string, exitCode = 0): string {
161
302
  const payloadFile = path.join(w.dir, `${name}.payload`);
@@ -198,6 +339,10 @@ function run(w: World, env: Record<string, string | undefined> = {}): Run {
198
339
  PATH: `${w.binDir}:${process.env.PATH ?? ""}`,
199
340
  HOME: home,
200
341
  XDG_DATA_HOME: path.join(home, ".local", "share"),
342
+ // The ledger's root, and it is NOT the runtime's. `herdr-activation.mjs` writes under XDG
343
+ // **state**; `herdr-runtime.mjs` installs under XDG **data**. Both are spelled out here so a
344
+ // cell that overrides one and forgets the other cannot silently read the operator's real host.
345
+ XDG_STATE_HOME: path.join(home, ".local", "state"),
201
346
  HERDR_ENV: "1",
202
347
  HERDR_PLUGIN_ID: "junghan0611.entwurf",
203
348
  HERDR_PLUGIN_ROOT: PLUGIN_DIR,
@@ -258,36 +403,145 @@ const PEERS = JSON.stringify({
258
403
  ],
259
404
  });
260
405
 
261
- const agentList = (agents: unknown[]): string =>
262
- JSON.stringify({ id: "cli:agent:list", result: { agents, type: "agent_list" } });
406
+ // ── #116 A-P1: two artifacts, two jobs — location here, certification there ──
407
+ {
408
+ const src = fs.readFileSync(path.join(PLUGIN_DIR, "lib", "activation-evidence.mjs"), "utf8");
409
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
410
+ // The checkout's activation module may answer WHERE, and only where. The BODY is certified by
411
+ // the installed runtime's own copy, because the pane and the writer are two artifacts that can
412
+ // ship two `ACTIVATION_SCHEMA_VERSION`s and a checkout-side parser would report a newer
413
+ // runtime's ledger as uncertified on a perfectly healthy host. Location is not schema-versioned;
414
+ // certification is. So: the checkout module is reached for `resolveActivationLayout` and for
415
+ // nothing else.
416
+ const checkoutCalls = [...code.matchAll(/activationModule\.([A-Za-z_$][\w$]*)/g)].map((m) => m[1]);
417
+ ok(
418
+ "the checkout activation module is asked only WHERE the ledger is, never to certify it",
419
+ checkoutCalls.length > 0 && checkoutCalls.every((name) => name === "resolveActivationLayout"),
420
+ );
421
+ // EXACTLY ONE parse of the ledger body, and it is on the dynamically imported installed reader.
422
+ // Two parses would be two chances to disagree about one file; a parse on the checkout module
423
+ // would be the false red above wearing the installed reader's name.
424
+ const reads = [...code.matchAll(/([A-Za-z_$][\w$]*)\.readCertifiedLedger\(/g)].map((m) => m[1]);
425
+ ok(
426
+ "the ledger body is parsed exactly once, by the installed runtime's own reader",
427
+ reads.length === 1 && reads[0] === "reader" && !code.includes("activationModule.readCertifiedLedger"),
428
+ );
429
+ }
430
+
431
+ // ── #120: the package name is the CHECKOUT MANIFEST's, on BOTH lock carriers ──
432
+ {
433
+ // THE PREMISE IS MEASURED HERE, NOT QUOTED. `runtime-lock.json` is a discriminated union and
434
+ // only its `npm` arm carries a `name`; the `herdr-checkout` arm is `{source, repository}` and
435
+ // nothing more. So this cell writes that arm and asks the real reader what `.name` is, rather
436
+ // than asserting a sentence about it — if a future lock schema grows a name on that arm, the
437
+ // premise stops being true HERE instead of quietly somewhere else.
438
+ const lockDir = tmp("lock-arm");
439
+ fs.writeFileSync(
440
+ path.join(lockDir, "runtime-lock.json"),
441
+ `${JSON.stringify({ schemaVersion: 2, source: "herdr-checkout", repository: RUNTIME.CHECKOUT_REPOSITORY })}\n`,
442
+ );
443
+ const checkoutArm = RUNTIME.readRuntimeLock(lockDir) as { name?: unknown };
444
+ // AND THE CONSEQUENCE IS STRUCTURAL, because the behavioural half is only true while the
445
+ // committed lock happens to BE the checkout carrier. Every candidate window rides that carrier
446
+ // and every published cut re-pins to npm afterwards, so a cell that merely spawned the pane
447
+ // today would silently stop testing this the moment the npm pin returns. What must hold on both
448
+ // carriers is that this module never asks the lock at all: the manifest is the one name both
449
+ // arms already agree about (`certifyLockCoherence` binds the npm arm to it, and the checkout arm
450
+ // packs that very manifest). Comments are stripped first, so prose about the lock cannot satisfy
451
+ // the claim — and prose about it is exactly what the retired doctrine left behind.
452
+ const code = fs
453
+ .readFileSync(path.join(PLUGIN_DIR, "lib", "activation-evidence.mjs"), "utf8")
454
+ .replace(/\/\*[\s\S]*?\*\//g, "")
455
+ .replace(/^\s*\/\/.*$/gm, "");
456
+ ok(
457
+ "[QK:HPL-PACKAGE-NAME-FROM-MANIFEST] the installed reader's package directory name comes from the " +
458
+ "CHECKOUT MANIFEST and the lock is never asked for it — measured, the `herdr-checkout` arm carries no " +
459
+ "`name` at all, so reading one off it yields `undefined` and `path.join` answers that with a TypeError " +
460
+ "rather than any refusal this state machine can name: the pane died before drawing a single block on " +
461
+ "exactly the carrier both v0.23.1 and v0.24.0 were tagged on " +
462
+ `(checkout-arm-name=${JSON.stringify(checkoutArm.name)} asks-manifest=${code.includes("runtimeModule.readCheckoutPackageSpec(")} asks-lock=${/readRuntimeLock\s*\(/.test(code)})`,
463
+ checkoutArm.name === undefined &&
464
+ code.includes("runtimeModule.readCheckoutPackageSpec(") &&
465
+ !/readRuntimeLock\s*\(/.test(code),
466
+ );
467
+ }
468
+
469
+ // ── #116 A-P1 [Blocker]: the ledger is read where the WRITER puts it ───────
470
+ {
471
+ // THIS CELL IS FIRST BECAUSE EVERY OTHER ACTIVATION CLAIM STANDS ON IT. The writer puts the
472
+ // ledger under XDG **state** (`herdr-activation.mjs:85-96`); the runtime tree lives under XDG
473
+ // **data** (`herdr-runtime.mjs:221-242`). Reading it from the data root finds nothing on a
474
+ // correctly activated host, and `readCertifiedLedger` answers a missing file with `null` —
475
+ // measured before this cell existed, the pane died on that null with
476
+ // `TypeError: Cannot read properties of null (reading 'runtimeRoot')` and drew ZERO blocks.
477
+ const w = world("locator");
478
+ const env = activateHost(w);
479
+ stubHerdr(w, {});
480
+ // A DECOY at the RUNTIME's data root: bytes the real reader refuses. Nothing writes a ledger
481
+ // there, so its only reader is a locator that has drifted back — and then this pane says
482
+ // `activation-ledger-uncertified` about a host whose ledger is certified and sitting elsewhere.
483
+ fs.mkdirSync(path.join(env.XDG_DATA_HOME, "entwurf", "herdr-plugin"), { recursive: true });
484
+ fs.writeFileSync(path.join(env.XDG_DATA_HOME, "entwurf", "herdr-plugin", "activation.json"), '{"schemaVersion":1}\n');
485
+
486
+ const r = run(w, env);
487
+ const activation = r.stdout.slice(r.stdout.indexOf("[2] ENTWURF ACTIVATION"), r.stdout.indexOf("[3] RAILS"));
488
+ const writerLedger = ACTIVATION.resolveActivationLayout({
489
+ HOME: env.HOME,
490
+ XDG_STATE_HOME: env.XDG_STATE_HOME,
491
+ }).ledgerPath as string;
492
+ assert.ok(
493
+ fs.existsSync(writerLedger) &&
494
+ activation.includes("phase: active") &&
495
+ !activation.includes("activation-ledger-uncertified") &&
496
+ !activation.includes("activation-ledger-missing"),
497
+ `[QK:HPL-LEDGER-AT-WRITER-PATH] the activation block reads the ledger from the path its WRITER owns (XDG state, \`resolveActivationLayout\`), not from the runtime's data root (\`resolveRuntimeLayout().pluginRoot\`) — the two roots differ on every host, so a pane that looks in the second one reports a correctly activated host as having no ledger, or reports whatever unrelated bytes are sitting there. A decoy the real reader refuses is planted at the data root precisely so a drifted locator cannot pass quietly (ledger at ${writerLedger}, block was ${JSON.stringify(activation.split("\n").slice(0, 3).join(" / "))})`,
498
+ );
499
+ console.log(" ok the ledger is read from the writer's own XDG state path, not the runtime data root");
500
+ passed++;
501
+ }
263
502
 
264
503
  // ── the ordinary open ───────────────────────────────────────────────────────
265
504
  {
266
505
  const w = world("ok");
506
+ // A world that HAS activated. Without it every unrelated cell also depended on the
507
+ // activation block's `absent` arm, and a mutant that turned absence into an error was killed
508
+ // by "the pane renders and exits 0" instead of by the claim that owns that arm — a kill
509
+ // nobody could attribute. Measured, then fixed here rather than by loosening the claim.
510
+ const ledgerHome = activateHost(w);
267
511
  stub(w, "entwurf", PEERS);
268
- stub(w, "herdr", agentList([{ pane_id: "w1:p3", agent_status: "working", terminal_id: "t1" }]));
269
- const r = run(w);
512
+ stubHerdr(w, { agents: agentList([{ pane_id: "w1:p3", agent_status: "working", terminal_id: "t1" }]) });
513
+ const r = run(w, ledgerHome);
270
514
 
271
515
  ok("the pane renders and exits 0", r.status === 0);
272
516
  ok("the citizen a placement owner reported is listed", r.stdout.includes("20260915T111111-aaaaaa"));
273
517
  ok("the four identity columns are present", /garden id\s+backend\s+cwd\s+placement/.test(r.stdout));
274
518
  ok("a joined citizen shows its pane, attributed to herdr", r.stdout.includes("herdr w1:p3"));
275
519
 
276
- // THE ONE READ PER OPEN. This is counted from the stubs' own log, so it cannot be
277
- // satisfied by a source that merely looks like it reads once.
520
+ // THE READS PER OPEN. Counted from the stubs' own log, so it cannot be satisfied by a source
521
+ // that merely looks like it reads once.
522
+ //
523
+ // #116 A2 AMENDMENT, visible on purpose: the SET grew from two reads to three when the
524
+ // integration block landed. The CADENCE did not move — still at most one of each, still only
525
+ // during this one open, still no refresh/retry/watcher. The claim asserts the exact sequence
526
+ // so a fourth read, a repeat, or a reorder all land here.
278
527
  assert.deepEqual(
279
528
  r.calls,
280
- ["entwurf peer-facts", "herdr agent list"],
281
- `[QK:HPL-ONE-OPEN-ONE-READ] one pane open issues exactly one \`entwurf peer-facts\` and one \`herdr agent list\`, in that order and never again — peer-facts observes every citizen unbounded (measured 4.1s on a 1,150-record store), and herdr publishes no "the session reference landed" event, so any refresh, retry or poll here is both the slow path and the discovery watcher docs/mux-launch-rail.md §7 refuses (got ${JSON.stringify(r.calls)})`,
529
+ ["herdr integration status", "entwurf peer-facts", "herdr agent list"],
530
+ `[QK:HPL-ONE-OPEN-ONE-READ] one pane open issues exactly one \`herdr integration status\`, one \`entwurf peer-facts\` and one \`herdr agent list\`, in that order and never again — peer-facts observes every citizen unbounded (measured 4.1s on a 1,150-record store), and herdr publishes no "the session reference landed" event, so any refresh, retry or poll here is both the slow path and the discovery watcher docs/mux-launch-rail.md §7 refuses (got ${JSON.stringify(r.calls)})`,
282
531
  );
283
- console.log(" ok one pane open issues exactly one peer-facts and one agent list, and never again");
532
+ console.log(" ok one pane open issues exactly one of each of the three reads, and never again");
284
533
  passed++;
285
534
 
286
535
  // ACTIVITY IS HERDR'S, IN ITS OWN COLUMN, UNDER ITS OWN NAME.
536
+ // The TABLE HEADER ROW, found by its first column rather than by a line index: the pane's
537
+ // closing guidance also contains the phrase `herdr-reported activity` (it explains what the
538
+ // column means), so a whole-stdout containment test passes even when the header itself has
539
+ // been renamed — measured, by a mutant that renamed it to `liveness` and survived.
540
+ const header = r.stdout.split("\n").find((l) => l.startsWith("garden id")) ?? "";
287
541
  assert.ok(
288
- r.stdout.includes("herdr-reported activity") &&
289
- r.stdout.indexOf("placement") < r.stdout.indexOf("herdr-reported activity") &&
290
- !/liveness/.test(r.stdout.split("\n")[2] ?? ""),
542
+ header.includes("herdr-reported activity") &&
543
+ header.indexOf("placement") < header.indexOf("herdr-reported activity") &&
544
+ !/liveness/.test(header),
291
545
  "[QK:HPL-ACTIVITY-IS-SEPARATE] herdr's agent_status is rendered in its own column under the name `herdr-reported activity`, after placement and never merged with it — it is herdr's judgement about a screen, not delivery evidence (docs/herdr-launch-rail.md §9), and a column that blurs the two is how it would start being read as liveness",
292
546
  );
293
547
  console.log(" ok herdr activity has its own named column, separate from placement");
@@ -320,6 +574,7 @@ const agentList = (agents: unknown[]): string =>
320
574
  // ── the render budget: a whole read, a table that stays readable ────────────
321
575
  {
322
576
  const w = world("many");
577
+ const manyHome = activateHost(w);
323
578
  // 1,000 historical citizens the placement owner does not have, one that it does, one
324
579
  // nobody could look at. This is the operator's own store in miniature: measured
325
580
  // 2026-09-15, 1,162 records, all unobserved, 17,437 lines of peer-facts output.
@@ -355,8 +610,8 @@ const agentList = (agents: unknown[]): string =>
355
610
  "entwurf",
356
611
  JSON.stringify({ schemaVersion: 1, storeDir: "/s", peers: [...many, visible, blind], diagnostics: [] }),
357
612
  );
358
- stub(w, "herdr", agentList([{ pane_id: "w1:p9", agent_status: "idle" }]));
359
- const r = run(w);
613
+ stubHerdr(w, { agents: agentList([{ pane_id: "w1:p9", agent_status: "idle" }]) });
614
+ const r = run(w, manyHome);
360
615
 
361
616
  const drawn = r.stdout.split("\n").filter((l) => /^20\d{6}T/.test(l)).length;
362
617
  assert.ok(
@@ -377,9 +632,10 @@ const agentList = (agents: unknown[]): string =>
377
632
 
378
633
  // Nothing visible at all is its own answer, not an empty frame.
379
634
  const w2 = world("empty-table");
635
+ const emptyHome = activateHost(w2);
380
636
  stub(w2, "entwurf", JSON.stringify({ schemaVersion: 1, storeDir: "/s", peers: many, diagnostics: [] }));
381
- stub(w2, "herdr", agentList([]));
382
- const r2 = run(w2);
637
+ stubHerdr(w2, {});
638
+ const r2 = run(w2, emptyHome);
383
639
  ok(
384
640
  "no visible citizen renders `(none)`, not a bare header",
385
641
  r2.status === 0 && r2.stdout.includes("(none)") && !r2.stdout.includes("garden id backend"),
@@ -389,16 +645,15 @@ const agentList = (agents: unknown[]): string =>
389
645
  // ── two agents on one pane: label it, never pick ────────────────────────────
390
646
  {
391
647
  const w = world("ambiguous");
648
+ const ambiguousHome = activateHost(w);
392
649
  stub(w, "entwurf", PEERS);
393
- stub(
394
- w,
395
- "herdr",
396
- agentList([
650
+ stubHerdr(w, {
651
+ agents: agentList([
397
652
  { pane_id: "w1:p3", agent_status: "working", terminal_id: "t1" },
398
653
  { pane_id: "w1:p3", agent_status: "idle", terminal_id: "t2" },
399
654
  ]),
400
- );
401
- const r = run(w);
655
+ });
656
+ const r = run(w, ambiguousHome);
402
657
  assert.ok(
403
658
  r.status === 0 &&
404
659
  /20260915T111111-aaaaaa.*ambiguous/.test(r.stdout) &&
@@ -409,17 +664,29 @@ const agentList = (agents: unknown[]): string =>
409
664
  passed++;
410
665
  }
411
666
 
412
- // ── a host without Entwurf is a SKIP, not a failure ─────────────────────────
667
+ // ── a host without Entwurf skips the CITIZEN axis, and only that axis ───────
413
668
  {
414
669
  const w = world("skip");
415
- stub(w, "herdr", agentList([]));
416
- // PATH holds the herdr stub but no `entwurf`, and ENTWURF_BIN is unset.
670
+ stubHerdr(w, {});
671
+ // PATH holds the herdr stub but no `entwurf`, ENTWURF_BIN is unset, and the sandbox HOME has
672
+ // no activation ledger and no runtime — so the D3=C fallback has nothing to offer either.
417
673
  const r = run(w, { PATH: w.binDir });
418
674
  assert.ok(
419
- r.status === 0 && r.stdout.trim() === "entwurf-not-found" && r.calls.length === 0,
420
- `[QK:HPL-SKIP-NOT-FAILURE] a host with no Entwurf prints exactly \`entwurf-not-found\` and exits 0, reading nothing — absence is a skip and only a BROKEN thing is red, because installing Entwurf is not a plugin's business (AGENTS.md Hard Rule 17) (got status ${r.status}, stdout ${JSON.stringify(r.stdout)})`,
675
+ r.status === 0 &&
676
+ r.stdout.includes("entwurf-not-found") &&
677
+ !r.calls.some((c) => c.startsWith("entwurf ")) &&
678
+ /\[4\] CITIZENS/.test(r.stdout) &&
679
+ /\[1\] HERDR INTEGRATION/.test(r.stdout) &&
680
+ /\[2\] ENTWURF ACTIVATION/.test(r.stdout) &&
681
+ /\[3\] RAILS/.test(r.stdout) &&
682
+ // The same world says the other half of this fact: a host with no ledger AND no runtime
683
+ // reports activation as ABSENT, not as a failure. It is one world, so it gets one claim —
684
+ // two claims owning it would make every mutant on that arm unattributable.
685
+ r.stdout.includes("this host has not activated Entwurf") &&
686
+ !r.stdout.includes("activation-reader-unavailable"),
687
+ `[QK:HPL-SKIP-NOT-FAILURE] a host with no Entwurf names \`entwurf-not-found\` on the CITIZEN block, exits 0, and spawns no entwurf at all — absence is a skip and only a BROKEN thing is red, because installing Entwurf is not a plugin's business and the ACTIVATION block reports ABSENT rather than an error — never activating is a state, not a defect, and calling it red would make "I have not installed this yet" indistinguishable from "your install is broken" (AGENTS.md Hard Rule 17). #116 A-D3=C AMENDMENT, narrowed visibly: this used to require the WHOLE pane to be that one token with zero reads. Blocks 1-3 do not need an entwurf binary and withholding them said less than we knew, so the skip is now scoped to the axis that actually cannot be answered (got status ${r.status}, calls ${JSON.stringify(r.calls)})`,
421
688
  );
422
- console.log(" ok a host with no Entwurf skips by name at exit 0, reading nothing");
689
+ console.log(" ok no Entwurf: the citizen block alone skips by name, blocks 1-3 still render, exit 0");
423
690
  passed++;
424
691
  }
425
692
 
@@ -470,18 +737,28 @@ const agentList = (agents: unknown[]): string =>
470
737
  // would fail first and certify nothing.
471
738
  const verdicts = cases.map((c) => {
472
739
  const w = world("red");
740
+ const redHome = activateHost(w);
473
741
  stub(w, "entwurf", c.peerPayload, c.peerExit);
474
- stub(w, "herdr", c.herdrPayload, c.herdrExit);
475
- const r = run(w);
476
- const named = r.stdout.startsWith(c.name);
742
+ stubHerdr(w, { agents: c.herdrPayload, agentsExit: c.herdrExit });
743
+ const r = run(w, redHome);
744
+ // #116 A1: the name now lives in the CITIZENS block rather than at the top of the pane —
745
+ // each block reports its own outcome in its own voice, so the failure has to be found in
746
+ // the block that owns it. Asserting the section, not merely the whole stdout, is what keeps
747
+ // this from passing on a name that leaked into some other block's prose.
748
+ const citizens = r.stdout.slice(r.stdout.indexOf("[4] CITIZENS"));
749
+ const named = citizens.includes(c.name);
477
750
  const red = r.status !== 0;
478
- const noTable = !r.stdout.includes("garden id");
751
+ // The TABLE, not the word: the pane's closing guidance names "garden id" on every open
752
+ // (it tells the reader where delivery lives), so the absence being asserted here is the
753
+ // column HEADER — the thing that would make a failed read look like a read that found
754
+ // nothing. Matching the bare word instead would have made this cell unfailable.
755
+ const noTable = !r.stdout.includes("garden id backend");
479
756
  return {
480
757
  c,
481
758
  named,
482
759
  red,
483
760
  noTable,
484
- shown: `${c.label}: status=${r.status} first=${JSON.stringify(r.stdout.split("\n")[0])}`,
761
+ shown: `${c.label}: status=${r.status} citizens=${JSON.stringify(citizens.split("\n").slice(0, 2).join(" / "))}`,
485
762
  };
486
763
  });
487
764
  assert.ok(
@@ -520,4 +797,434 @@ const agentList = (agents: unknown[]): string =>
520
797
  );
521
798
  }
522
799
 
800
+ // ── #116 A: four blocks, four owners, four times, and NO aggregate ──────────
801
+ {
802
+ const w = world("blocks");
803
+ const blocksHome = activateHost(w);
804
+ stub(w, "entwurf", PEERS);
805
+ stubHerdr(w, { agents: agentList([{ pane_id: "w1:p3", agent_status: "working" }]) });
806
+ const r = run(w, blocksHome);
807
+
808
+ // POSITIVE and STRUCTURAL: every block must SAY who owns it and when its fact was true.
809
+ // This is the primary oracle for the no-merged-green contract — a surface that states four
810
+ // provenances cannot also be reduced to one word without contradicting itself on screen.
811
+ const blockLine = (n: number): string => r.stdout.split("\n").find((l) => l.startsWith(`[${n}] `)) ?? "";
812
+ assert.ok(
813
+ /^\[1\] HERDR INTEGRATION — owner: herdr; read now, this open$/.test(blockLine(1)) &&
814
+ /^\[2\] ENTWURF ACTIVATION — owner: entwurf; recorded at install time — not re-checked now$/.test(blockLine(2)) &&
815
+ /^\[3\] RAILS — owner: entwurf \(this package\); declared by this package — not a fact about this host$/.test(
816
+ blockLine(3),
817
+ ) &&
818
+ /^\[4\] CITIZENS — owner: entwurf; read now, this open$/.test(blockLine(4)),
819
+ `[QK:HPL-BLOCK-PROVENANCE] each of the four blocks renders its OWN owner and its OWN observation time, and the three times stay distinct — herdr's listing is true NOW, the activation ledger is an INSTALL-TIME receipt nobody re-checked, the rails table is STATIC and is a property of this package rather than of this host. Collapsing any of them into "wired" would have to pick one time and be wrong about the other two (got ${JSON.stringify([1, 2, 3, 4].map(blockLine))})`,
820
+ );
821
+ console.log(" ok four blocks, each naming its own owner and observation time");
822
+ passed++;
823
+
824
+ // The SECONDARY tripwire, and it is deliberately narrow: a summary token. It backs the
825
+ // structural claim above rather than standing in for it, and it does NOT ban the word `ok`,
826
+ // which every block legitimately needs for its own outcome.
827
+ assert.ok(
828
+ !/\b(overall|all ready|all green|everything is ready|fully wired)\b/i.test(r.stdout),
829
+ `[QK:HPL-NO-AGGREGATE-VERDICT] no aggregate verdict token is rendered anywhere — no "overall", no "all ready", no readiness word over the four blocks. The render leaf has no reducer to offer, and this tripwire is what keeps one from being added as prose later (stdout carried one)`,
830
+ );
831
+ console.log(" ok no aggregate verdict token over the four blocks");
832
+ passed++;
833
+
834
+ // RAILS come from the shared declaration leaf, not from prose in this pane.
835
+ assert.ok(
836
+ HERDR_RAILS.HERDR_FRESH_CALL_BACKENDS.every(
837
+ (b) => r.stdout.includes(` ${b}: `) && r.stdout.includes(HERDR_RAILS.MODEL_SYNTAX_EXAMPLE[b]),
838
+ ) &&
839
+ !/provider|quota|subscription|openrouter|billing/i.test(
840
+ r.stdout.slice(r.stdout.indexOf("[3] RAILS"), r.stdout.indexOf("[4] CITIZENS")),
841
+ ),
842
+ `[QK:HPL-RAILS-SHARED-SOURCE] the RAILS block renders exactly the backends and the one model SYNTAX example that scripts/herdr-rails.mjs declares, and names no provider, quota, subscription or billing — a model example that mentioned a provider would read as an answer to #76, which is a rail-policy question this pane does not decide`,
843
+ );
844
+ console.log(" ok rails and model examples come from the shared leaf, with no provider policy");
845
+ passed++;
846
+ }
847
+
848
+ // ── #116 A-D3=C: the ledger fallback, and the precedence it may never out-vote ──
849
+ {
850
+ // A certified activation ledger, an aligned runtime root, and a real executable bin — the
851
+ // only state in which a fallback exists at all.
852
+ const w = world("fallback");
853
+ const home = path.join(w.dir, "home");
854
+ const dataHome = path.join(home, ".local", "share");
855
+ const stateHome = path.join(home, ".local", "state");
856
+ const activeDir = path.join(dataHome, "entwurf", "herdr-plugin", "runtime", "active");
857
+ const binDir2 = path.join(activeDir, "node_modules", ".bin");
858
+ fs.mkdirSync(binDir2, { recursive: true });
859
+ const readerDir = path.join(activeDir, "node_modules", "@junghanacs", "entwurf", "scripts");
860
+ fs.mkdirSync(readerDir, { recursive: true });
861
+ installReader(readerDir);
862
+ const ledgerPath = writeLedgerAt({ HOME: home, XDG_STATE_HOME: stateHome }, ledgerBody(activeDir));
863
+ // The runtime's own `entwurf` bin, answering peer-facts exactly as the PATH stub would.
864
+ const payloadFile = path.join(w.dir, "runtime-entwurf.payload");
865
+ fs.writeFileSync(payloadFile, PEERS);
866
+ const runtimeBin = path.join(binDir2, "entwurf");
867
+ fs.writeFileSync(
868
+ runtimeBin,
869
+ `#!/bin/sh\nprintf '%s %s\\n' "runtime-entwurf" "$*" >> "${w.callLog}"\nprintf '%s' ${shq(PEERS)}\n`,
870
+ );
871
+ fs.chmodSync(runtimeBin, 0o755);
872
+ stubHerdr(w, { agents: agentList([{ pane_id: "w1:p3", agent_status: "working" }]) });
873
+
874
+ // PATH carries the herdr stub and NO entwurf: exactly the state a correct plugin install
875
+ // leaves behind (README: the plugin puts nothing on PATH).
876
+ const r = run(w, { PATH: w.binDir, HOME: home, XDG_DATA_HOME: dataHome, XDG_STATE_HOME: stateHome });
877
+ assert.ok(
878
+ r.status === 0 &&
879
+ r.stdout.includes("20260915T111111-aaaaaa") &&
880
+ r.stdout.includes("(via activation-ledger)") &&
881
+ r.calls.some((c) => c.startsWith("runtime-entwurf peer-facts")),
882
+ `[QK:HPL-LEDGER-FALLBACK-READS-CITIZENS] with nothing named entwurf on PATH and no ENTWURF_BIN, a CERTIFIED, root-aligned ledger whose runtime bin is executable lets the citizen axis be read from that bin — this is the state a correct plugin install actually leaves a clean host in, and before it the very first open closed itself with entwurf-not-found (status ${r.status}, calls ${JSON.stringify(r.calls)})`,
883
+ );
884
+ console.log(" ok a certified, aligned ledger supplies the runtime bin when PATH has none");
885
+ passed++;
886
+
887
+ // The fallback WRITES NOTHING and rewrites nothing: still a read-only surface.
888
+ assert.ok(
889
+ fs.readdirSync(r.stateDir).length === 0 &&
890
+ fs.readdirSync(r.configDir).length === 0 &&
891
+ fs.readFileSync(ledgerPath, "utf8").includes('"phase": "active"'),
892
+ "[QK:HPL-FALLBACK-READ-ONLY] taking the ledger fallback writes nothing — the plugin state and config dirs stay empty and the ledger is untouched; a pane that repaired what it read would be installing, which is not a plugin's business (AGENTS.md Hard Rule 17)",
893
+ );
894
+ console.log(" ok the ledger fallback writes nothing and repairs nothing");
895
+ passed++;
896
+
897
+ // PRECEDENCE: an operator override wins outright, and the ledger never shadows it.
898
+ const overrideBin = path.join(w.binDir, "my-entwurf");
899
+ fs.writeFileSync(
900
+ overrideBin,
901
+ `#!/bin/sh\nprintf '%s %s\\n' "override-entwurf" "$*" >> "${w.callLog}"\nprintf '%s' ${shq(PEERS)}\n`,
902
+ );
903
+ fs.chmodSync(overrideBin, 0o755);
904
+ fs.writeFileSync(w.callLog, "");
905
+ const r2 = run(w, {
906
+ PATH: w.binDir,
907
+ HOME: home,
908
+ XDG_DATA_HOME: dataHome,
909
+ XDG_STATE_HOME: stateHome,
910
+ ENTWURF_BIN: overrideBin,
911
+ });
912
+ assert.ok(
913
+ r2.status === 0 &&
914
+ r2.calls.some((c) => c.startsWith("override-entwurf peer-facts")) &&
915
+ !r2.calls.some((c) => c.startsWith("runtime-entwurf")) &&
916
+ r2.stdout.includes("(via ENTWURF_BIN)"),
917
+ `[QK:HPL-OVERRIDE-BEATS-LEDGER] ENTWURF_BIN wins outright even when a certified ledger is sitting right there, and the ledger's bin is never spawned — an operator override is the operator speaking, and a receipt must not out-vote it (calls ${JSON.stringify(r2.calls)})`,
918
+ );
919
+ console.log(" ok ENTWURF_BIN beats a certified ledger, which is never spawned");
920
+ passed++;
921
+ }
922
+
923
+ // ── #116 A4: every activation-evidence state has its own name, and absence is not red ──
924
+ {
925
+ const cases: {
926
+ label: string;
927
+ mutate: (dirs: { dataHome: string; activeDir: string; ledgerPath: string }) => void;
928
+ code: string;
929
+ red: boolean;
930
+ }[] = [
931
+ {
932
+ label: "ledger present, installed reader absent",
933
+ mutate: ({ activeDir }) =>
934
+ fs.rmSync(path.join(activeDir, "node_modules", "@junghanacs"), { recursive: true, force: true }),
935
+ code: "activation-reader-unavailable",
936
+ red: true,
937
+ },
938
+ {
939
+ label: "ledger body not certifiable",
940
+ mutate: ({ ledgerPath }) => fs.writeFileSync(ledgerPath, '{"schemaVersion":1}\n'),
941
+ code: "activation-ledger-uncertified",
942
+ red: true,
943
+ },
944
+ {
945
+ label: "certified ledger describing another root",
946
+ mutate: ({ ledgerPath }) => {
947
+ const l = JSON.parse(fs.readFileSync(ledgerPath, "utf8")) as { runtimeRoot: string };
948
+ l.runtimeRoot = "/somewhere/else/active";
949
+ fs.writeFileSync(ledgerPath, `${JSON.stringify(l, null, 2)}\n`);
950
+ },
951
+ code: "activation-runtime-root-mismatch",
952
+ red: true,
953
+ },
954
+ {
955
+ label: "certified and aligned, runtime bin gone",
956
+ mutate: ({ activeDir }) => fs.rmSync(path.join(activeDir, "node_modules", ".bin", "entwurf"), { force: true }),
957
+ code: "runtime-bin-missing",
958
+ red: true,
959
+ },
960
+ ];
961
+ const verdicts = cases.map((c) => {
962
+ const w = world("evidence");
963
+ const home = path.join(w.dir, "home");
964
+ const dataHome = path.join(home, ".local", "share");
965
+ const stateHome = path.join(home, ".local", "state");
966
+ const activeDir = path.join(dataHome, "entwurf", "herdr-plugin", "runtime", "active");
967
+ const readerDir = path.join(activeDir, "node_modules", "@junghanacs", "entwurf", "scripts");
968
+ fs.mkdirSync(readerDir, { recursive: true });
969
+ fs.mkdirSync(path.join(activeDir, "node_modules", ".bin"), { recursive: true });
970
+ installReader(readerDir);
971
+ const bin = path.join(activeDir, "node_modules", ".bin", "entwurf");
972
+ fs.writeFileSync(bin, `#!/bin/sh\nprintf '%s' ${shq(PEERS)}\n`);
973
+ fs.chmodSync(bin, 0o755);
974
+ const ledgerPath = writeLedgerAt({ HOME: home, XDG_STATE_HOME: stateHome }, ledgerBody(activeDir));
975
+ c.mutate({ dataHome, activeDir, ledgerPath });
976
+ stubHerdr(w, {});
977
+ const r = run(w, { PATH: w.binDir, HOME: home, XDG_DATA_HOME: dataHome, XDG_STATE_HOME: stateHome });
978
+ const activation = r.stdout.slice(r.stdout.indexOf("[2] ENTWURF ACTIVATION"), r.stdout.indexOf("[3] RAILS"));
979
+ return {
980
+ c,
981
+ named: activation.includes(c.code),
982
+ red: (r.status !== 0) === c.red,
983
+ shown: `${c.label}: status=${r.status} block=${JSON.stringify(activation.split("\n")[1] ?? "")}`,
984
+ };
985
+ });
986
+ assert.ok(
987
+ verdicts.every((v) => v.named && v.red),
988
+ `[QK:HPL-ACTIVATION-NAMED-REDS] every way the activation evidence can be broken has its OWN name on screen and its own red — a reader we cannot import, a body the writer's own reader refuses, a receipt describing a different runtime root, and a certified receipt whose bin has since gone. None of them crashes and none silently skips: a pane that went quiet on a receipt it could not read would be reporting the absence of a failure as health (${verdicts.map((v) => v.shown).join(" | ")})`,
989
+ );
990
+ console.log(" ok the four broken-evidence states are named reds, each in its own voice");
991
+ passed++;
992
+ }
993
+
994
+ // ── #116 A-P1: the remaining activation arms — named, and none of them a crash ──
995
+ {
996
+ /** A host with a runtime and its bin — its installed reader and its ledger both optional. */
997
+ function runtimeOnly(
998
+ w: World,
999
+ ledger: Record<string, unknown> | null,
1000
+ opts: { withReader?: boolean; readerSource?: string } = {},
1001
+ ): Record<string, string> {
1002
+ const home = path.join(w.dir, "ro-home");
1003
+ const dataHome = path.join(home, ".local", "share");
1004
+ const stateHome = path.join(home, ".local", "state");
1005
+ const activeDir = path.join(dataHome, "entwurf", "herdr-plugin", "runtime", "active");
1006
+ const readerDir = path.join(activeDir, "node_modules", "@junghanacs", "entwurf", "scripts");
1007
+ fs.mkdirSync(readerDir, { recursive: true });
1008
+ fs.mkdirSync(path.join(activeDir, "node_modules", ".bin"), { recursive: true });
1009
+ // A CUSTOM installed reader stands for an install whose bytes are not the ones we shipped —
1010
+ // the two shapes below are measured API answers, not inventions: a module that loads without
1011
+ // the export this leaf calls, and the `null` the real reader returns for a ledger that is
1012
+ // gone (herdr-activation.mjs:159-162).
1013
+ if (opts.readerSource !== undefined) {
1014
+ fs.mkdirSync(readerDir, { recursive: true });
1015
+ fs.writeFileSync(path.join(readerDir, "herdr-activation.mjs"), opts.readerSource);
1016
+ } else if (opts.withReader !== false) installReader(readerDir);
1017
+ const bin = path.join(activeDir, "node_modules", ".bin", "entwurf");
1018
+ fs.writeFileSync(bin, `#!/bin/sh\nprintf '%s %s\\n' "entwurf" "$*" >> "${w.callLog}"\nprintf '%s' ${shq(PEERS)}\n`);
1019
+ fs.chmodSync(bin, 0o755);
1020
+ if (ledger !== null) writeLedgerAt({ HOME: home, XDG_STATE_HOME: stateHome }, ledger);
1021
+ return { HOME: home, XDG_DATA_HOME: dataHome, XDG_STATE_HOME: stateHome, ACTIVE_DIR: activeDir };
1022
+ }
1023
+ const blockOf = (stdout: string, open: string, close: string): string =>
1024
+ stdout.slice(stdout.indexOf(open), stdout.indexOf(close));
1025
+
1026
+ // (1) A RUNTIME STANDS THERE AND NO LEDGER DESCRIBES IT. `readCertifiedLedger` answers a
1027
+ // missing file with `null` (herdr-activation.mjs:159-162), and the candidate dereferenced that
1028
+ // null — so a partial install, a finished teardown, or a ledger removed between the check and
1029
+ // the read all closed the pane with a stack trace and no blocks at all. The whole pane still
1030
+ // has to render; only this receipt is missing, and it says so by name.
1031
+ {
1032
+ const w = world("no-ledger");
1033
+ const { ACTIVE_DIR, ...env } = runtimeOnly(w, null);
1034
+ stubHerdr(w, {});
1035
+ const r = run(w, { PATH: w.binDir, ...env });
1036
+ const activation = blockOf(r.stdout, "[2] ENTWURF ACTIVATION", "[3] RAILS");
1037
+ const citizens = r.stdout.slice(r.stdout.indexOf("[4] CITIZENS"));
1038
+ // AND THE SAME HOST WITH NO READER EITHER. Knowing that no receipt exists takes no reader:
1039
+ // the ledger's address is a pure function of the environment. Deciding this one only AFTER
1040
+ // locating the installed reader would report a broken install on a host whose only real
1041
+ // fact is that nothing ever wrote a ledger here.
1042
+ const w2 = world("no-ledger-no-reader");
1043
+ const { ACTIVE_DIR: _unusedRoot, ...env2 } = runtimeOnly(w2, null, { withReader: false });
1044
+ stubHerdr(w2, {});
1045
+ const r2 = run(w2, { PATH: w2.binDir, ...env2 });
1046
+ const activation2 = blockOf(r2.stdout, "[2] ENTWURF ACTIVATION", "[3] RAILS");
1047
+ assert.ok(
1048
+ r2.status !== 0 &&
1049
+ activation2.includes("activation-ledger-missing") &&
1050
+ !activation2.includes("activation-reader-unavailable") &&
1051
+ r.status !== 0 &&
1052
+ activation.includes("activation-ledger-missing") &&
1053
+ /\[1\] HERDR INTEGRATION/.test(r.stdout) &&
1054
+ /\[3\] RAILS/.test(r.stdout) &&
1055
+ citizens.includes("entwurf-not-found") &&
1056
+ citizens.includes("activation-ledger-missing") &&
1057
+ !r.calls.some((c) => c.startsWith("entwurf ")),
1058
+ `[QK:HPL-LEDGER-MISSING-IS-NAMED] a runtime with no ledger behind it is a NAMED red (\`activation-ledger-missing\`) that still draws all four blocks — the reader answers a missing ledger with \`null\`, and dereferencing that null crashed the pane before any block was written, which is the one failure a status surface cannot report about itself. The citizen skip names the same code rather than claiming there was simply no runtime, and the runtime's own bin is never spawned on a receipt we do not have. The same answer holds with no installed reader present: no reader is needed to know that nothing wrote a receipt here, and filing that as a broken install would name the wrong repair (status ${r.status}/${r2.status}, blocks ${JSON.stringify([activation.split("\n")[1], activation2.split("\n")[1]])})`,
1059
+ );
1060
+ console.log(" ok a runtime with no ledger is a named red, with four blocks and no crash");
1061
+ passed++;
1062
+ }
1063
+
1064
+ // (1b) THE IMPORT SUCCEEDS AND THE READER IS NOT THERE. A module can load perfectly and carry
1065
+ // none of the export this leaf calls — a partial or mismatched install. Without an explicit
1066
+ // check that is a TypeError raised inside the CERTIFICATION try, and it lands as "the writer's
1067
+ // own reader refused this body" when no body was ever read: the operator is sent to inspect a
1068
+ // ledger that is fine, instead of to the install that is not.
1069
+ {
1070
+ const w = world("reader-export");
1071
+ const { ACTIVE_DIR, ...env } = runtimeOnly(w, ledgerBody(""), {
1072
+ readerSource: "export const NOT_THE_READER = 1;\n",
1073
+ });
1074
+ stubHerdr(w, {});
1075
+ const r = run(w, { PATH: w.binDir, ...env });
1076
+ const activation = blockOf(r.stdout, "[2] ENTWURF ACTIVATION", "[3] RAILS");
1077
+ assert.ok(
1078
+ r.status !== 0 &&
1079
+ activation.includes("activation-reader-unavailable") &&
1080
+ !activation.includes("activation-ledger-uncertified") &&
1081
+ /\[4\] CITIZENS/.test(r.stdout) &&
1082
+ !r.calls.some((c) => c.startsWith("entwurf ")),
1083
+ `[QK:HPL-READER-EXPORT-IS-REQUIRED] an installed reader that imports but exports no \`readCertifiedLedger\` is \`activation-reader-unavailable\`, never \`activation-ledger-uncertified\` — the second name says the writer's own reader looked at this body and refused it, and here no body was read at all. The two names carry two different repairs (reinstall the runtime vs inspect the ledger), and the TypeError this would otherwise raise lands inside the certification try where it would wear the wrong one (status ${r.status}, block ${JSON.stringify(activation.split("\n").slice(0, 2).join(" / "))})`,
1084
+ );
1085
+ console.log(" ok a reader that imports without its export is reader-unavailable, not uncertified");
1086
+ passed++;
1087
+ }
1088
+
1089
+ // (1c) THE LEDGER WAS THERE AND IS NOT ANY MORE. The presence check and the read are two
1090
+ // syscalls with a gap between them, and the writer's reader answers that gap with `null`
1091
+ // (herdr-activation.mjs:159-162). The EARLY branch above never sees this one — it already
1092
+ // decided while the file was there — so the late guard is the only thing standing between that
1093
+ // null and a dereference. The fixture reader returns exactly the answer the real one returns.
1094
+ {
1095
+ const w = world("ledger-vanished");
1096
+ const { ACTIVE_DIR, ...env } = runtimeOnly(w, ledgerBody(""), {
1097
+ readerSource: "export function readCertifiedLedger() {\n\treturn null;\n}\n",
1098
+ });
1099
+ stubHerdr(w, {});
1100
+ const r = run(w, { PATH: w.binDir, ...env });
1101
+ const activation = blockOf(r.stdout, "[2] ENTWURF ACTIVATION", "[3] RAILS");
1102
+ const citizens = r.stdout.slice(r.stdout.indexOf("[4] CITIZENS"));
1103
+ assert.ok(
1104
+ r.status !== 0 &&
1105
+ activation.includes("activation-ledger-missing") &&
1106
+ /\[1\] HERDR INTEGRATION/.test(r.stdout) &&
1107
+ /\[3\] RAILS/.test(r.stdout) &&
1108
+ citizens.includes("entwurf-not-found") &&
1109
+ !r.calls.some((c) => c.startsWith("entwurf ")),
1110
+ `[QK:HPL-LEDGER-VANISHED-IS-NAMED] a ledger that passed the presence check and is gone by the time the installed reader reads it comes back as \`null\`, and that null is NAMED \`activation-ledger-missing\` with all four blocks still drawn — not dereferenced. The early branch cannot cover this: it already answered while the file existed, so removing this guard puts a TypeError between a race nobody controls and the operator's only view of the host (status ${r.status}, block ${JSON.stringify(activation.split("\n").slice(0, 2).join(" / "))})`,
1111
+ );
1112
+ console.log(" ok a ledger that vanishes after the presence check is named, not dereferenced");
1113
+ passed++;
1114
+ }
1115
+
1116
+ // (2) NO XDG ROOTS AT ALL. Both resolvers throw a named error when there is no user root to
1117
+ // own; the candidate called them outside any guard, so the pane exited on an unhandled
1118
+ // rejection. An environment this surface cannot resolve is still something it must be able to
1119
+ // SAY, and blocks 1, 3 and 4 do not need a ledger root to answer.
1120
+ {
1121
+ const w = world("no-root");
1122
+ stubHerdr(w, {});
1123
+ const r = run(w, { PATH: w.binDir, HOME: undefined, XDG_DATA_HOME: undefined, XDG_STATE_HOME: undefined });
1124
+ const activation = blockOf(r.stdout, "[2] ENTWURF ACTIVATION", "[3] RAILS");
1125
+ assert.ok(
1126
+ r.status !== 0 &&
1127
+ activation.includes("activation-env-root-unresolvable") &&
1128
+ /\[1\] HERDR INTEGRATION/.test(r.stdout) &&
1129
+ /\[3\] RAILS/.test(r.stdout) &&
1130
+ /\[4\] CITIZENS/.test(r.stdout),
1131
+ `[QK:HPL-ENV-ROOT-IS-NAMED] with no HOME and none of the XDG roots the two layouts need, the activation block reports \`activation-env-root-unresolvable\` and the other three blocks still render — the layout resolvers throw by design — each root they need must be resolvable on its own once HOME is gone — and a gatherer that lets that throw past its caller turns "I cannot tell where your ledger would live" into a pane that prints nothing at all (status ${r.status}, stdout ${JSON.stringify(r.stdout.slice(0, 120))})`,
1132
+ );
1133
+ console.log(" ok unresolvable XDG roots are a named red, not an unhandled rejection");
1134
+ passed++;
1135
+ }
1136
+
1137
+ // (3) A CERTIFIED LEDGER MID-TEARDOWN IS A RECEIPT, NOT AN AUTHORITY. `deactivating` (and
1138
+ // `activating`, a transaction that never checkpointed) are legitimate certified states, so the
1139
+ // block reports them; what they are not is evidence that a runtime is standing there ready to
1140
+ // answer. Install-time wiring and runtime-live are two different questions and this is the
1141
+ // seam where merging them would start.
1142
+ {
1143
+ const drivePhase = (phase: string, state: string) => {
1144
+ const w = world(`phase-${phase}`);
1145
+ const { ACTIVE_DIR, ...env } = runtimeOnly(w, {
1146
+ schemaVersion: 2,
1147
+ phase,
1148
+ runtimeRoot: "",
1149
+ artifactIdentity: READY_NPM_IDENTITY,
1150
+ piAgentDir: { path: "/p", source: "default" },
1151
+ claudeConfigDir: { path: "/c", source: "default" },
1152
+ claudeUserConfig: { path: "/c/u", source: "HOME" },
1153
+ activatedBackends: ["pi"],
1154
+ components: [{ backend: "pi", state }],
1155
+ });
1156
+ // The root the ledger describes is this host's own, so nothing here is a mismatch —
1157
+ // the phase is the only thing standing between the receipt and the fallback.
1158
+ const ledgerFile = ACTIVATION.resolveActivationLayout(env).ledgerPath as string;
1159
+ const body = JSON.parse(fs.readFileSync(ledgerFile, "utf8")) as { runtimeRoot: string };
1160
+ body.runtimeRoot = ACTIVE_DIR;
1161
+ fs.writeFileSync(ledgerFile, `${JSON.stringify(body, null, 2)}\n`);
1162
+ stubHerdr(w, {});
1163
+ const r = run(w, { PATH: w.binDir, ...env });
1164
+ return {
1165
+ phase,
1166
+ status: r.status,
1167
+ activation: blockOf(r.stdout, "[2] ENTWURF ACTIVATION", "[3] RAILS"),
1168
+ citizens: r.stdout.slice(r.stdout.indexOf("[4] CITIZENS")),
1169
+ calls: r.calls,
1170
+ };
1171
+ };
1172
+ // BOTH non-active phases, because the claim names both: `activating` is a transaction that
1173
+ // never checkpointed, `deactivating` is a teardown in flight. `PHASE_ALLOWS` admits a
1174
+ // different component state under each, so each drive carries its own.
1175
+ const drives = [drivePhase("deactivating", "removed"), drivePhase("activating", "pending")];
1176
+ assert.ok(
1177
+ drives.every(
1178
+ (d) =>
1179
+ d.status === 0 &&
1180
+ d.activation.includes(`phase: ${d.phase}`) &&
1181
+ d.activation.includes("activation-phase-not-active") &&
1182
+ d.citizens.includes("activation-phase-not-active") &&
1183
+ !d.calls.some((c) => c.startsWith("entwurf ")),
1184
+ ),
1185
+ `[QK:HPL-PHASE-GATES-FALLBACK] only a ledger in phase \`active\` grants the D3=C runtime fallback: an \`activating\` or \`deactivating\` ledger is still a certified install-time receipt and is reported as one (exit 0, not a red), but its runtime bin is never spawned and the citizen skip names \`activation-phase-not-active\` instead of implying this host simply never activated. A receipt written while a teardown was in progress is not evidence that anything is standing there now (${drives.map((d) => `${d.phase}: status=${d.status} calls=${JSON.stringify(d.calls)}`).join(" | ")})`,
1186
+ );
1187
+ console.log(" ok only an `active` ledger grants the fallback; a teardown receipt is reported, not spent");
1188
+ passed++;
1189
+ }
1190
+ }
1191
+
1192
+ // ── #116 A-P1: block [1] owns its own failure, and a failure is not prose ──
1193
+ {
1194
+ // `buildActivationProfile` CLASSIFIES: `outdated` and `needs-repair` go to `profile.fail` and
1195
+ // the build runner refuses them, so "herdr's integration is not current here" is a verdict this
1196
+ // plugin already reached — not a row an operator has to notice. "No aggregate green" means the
1197
+ // four axes keep their OWN receipts; it does not mean an axis that failed leaves at zero.
1198
+ // `not-installed` is the other side of that line and stays a reported skip: nothing is wrong
1199
+ // with a host that has simply not integrated a harness.
1200
+ const drive = (integration: string) => {
1201
+ const w = world("integration-verdict");
1202
+ const env = activateHost(w);
1203
+ stubHerdr(w, { integration });
1204
+ const r = run(w, env);
1205
+ return {
1206
+ status: r.status,
1207
+ block: r.stdout.slice(r.stdout.indexOf("[1] HERDR INTEGRATION"), r.stdout.indexOf("[2] ENTWURF ACTIVATION")),
1208
+ stdout: r.stdout,
1209
+ };
1210
+ };
1211
+ const failing = drive(["pi: outdated (v7 < v8) (/h/pi)", "claude: needs repair (v8) (/h/c)", ""].join("\n"));
1212
+ const skipping = drive(["pi: current (v8) (/h/pi)", "claude: not installed (/h/c)", ""].join("\n"));
1213
+ assert.ok(
1214
+ failing.status !== 0 &&
1215
+ failing.block.includes("herdr-integration-not-current") &&
1216
+ failing.block.includes("outdated — herdr-integration-outdated") &&
1217
+ failing.block.includes("needs-repair — herdr-integration-needs-repair") &&
1218
+ /\[2\] ENTWURF ACTIVATION/.test(failing.stdout) &&
1219
+ /\[3\] RAILS/.test(failing.stdout) &&
1220
+ /\[4\] CITIZENS/.test(failing.stdout) &&
1221
+ skipping.status === 0 &&
1222
+ !skipping.block.includes("herdr-integration-not-current") &&
1223
+ skipping.block.includes("not-installed — herdr-integration-not-installed"),
1224
+ `[QK:HPL-INTEGRATION-NONCURRENT-IS-RED] a selected atom herdr reports as \`outdated\` or \`needs repair\` makes block [1] a NAMED error (\`herdr-integration-not-current\`) and the pane leave non-zero, while every row and every other block still renders — the profile leaf already files both into \`profile.fail\` and the build runner already refuses them, so rendering that as a \`reported\` block was this surface disagreeing with its own classifier. \`not-installed\` stays a reported skip at exit 0, because a harness nobody integrated is not a fault (failing: status=${failing.status} ${JSON.stringify(failing.block.split("\n").slice(0, 3).join(" / "))} | not-installed: status=${skipping.status})`,
1225
+ );
1226
+ console.log(" ok an outdated/needs-repair atom makes block [1] a named red; not-installed stays a skip");
1227
+ passed++;
1228
+ }
1229
+
523
1230
  console.log(`\ncheck-herdr-plugin: ${passed} assertions passed`);