@junghanacs/entwurf 0.45.0 → 0.51.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.
- package/AGENTS.md +5 -3
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +249 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +63 -15
- package/FAQ.md +4 -4
- package/README.md +132 -188
- package/VERIFY.md +18 -11
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +4 -4
- package/docs/setup-clean-host.md +94 -22
- package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +28 -8
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +16 -20
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +327 -57
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/carrier/subagent.js +5 -2
- package/pi/pi-durable/carrier/tui.js +60 -22
- package/pi/pi-durable/overlay/upstream-pin.json +26 -19
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +73 -69
- package/pi-extensions/lib/entwurf-v2-contract.ts +4 -4
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-resume-call.ts +77 -4
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +30 -16
- package/run.sh +552 -699
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-v2-contract.ts +4 -4
- package/scripts/check-entwurf-v2-surface.ts +2 -2
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +19 -14
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +28 -0
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +4 -4
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-resume-call.json +63 -2
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +1167 -3
- package/scripts/mutants/pi-floor.json +22 -5
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +255 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- package/team.json.example +39 -0
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* HFC-ENCODE-NO-CONTROL the encoded argv carries zero Unicode Cc — including the C1
|
|
27
27
|
* block and DEL, which JSON.stringify leaves literal
|
|
28
28
|
* HFC-ENCODE-MAX-TASK a maximum public task encodes and stays Cc-free
|
|
29
|
+
* HFC-CODEX-CWD-NO-FALLBACK a known Codex caller with no record cwd is refused on the agent-start
|
|
30
|
+
* branch — its process directory is never read, no herdr call is made
|
|
29
31
|
* HFC-PREMUTATION-ONLY every refusal happens before the first herdr call
|
|
30
32
|
* HFC-CALLER-WORKSPACE the caller's workspace comes from herdr's own answer about the
|
|
31
33
|
* caller's own pane, never from parsing a pane id, and an
|
|
@@ -51,7 +53,9 @@
|
|
|
51
53
|
* anybody has reported that agent's session id
|
|
52
54
|
* HFC-ORPHAN-NAMED every failed reclaim names its reason instead of going quiet
|
|
53
55
|
* HFC-INPUT-PARITY the caller-facing input contract is the SHARED one, word for word
|
|
54
|
-
* HFC-CWD-
|
|
56
|
+
* HFC-CWD-CALLER-DEFAULT an omitted cwd is sent as the CALLER's directory (process dir, or a
|
|
57
|
+
* known Codex caller's record dir), never left to herdr's default
|
|
58
|
+
* HFC-CWD-EMPTY-IS-OMITTED `cwd: ""` means "no cwd requested", as it does on the other rail
|
|
55
59
|
* HFC-CWD-HASH-IS-DATA a `#` in the start directory is DATA here — the tmux rail's
|
|
56
60
|
* format-token refusal is not imported, because its reason is
|
|
57
61
|
* false on a rail with no format expansion
|
|
@@ -98,9 +102,11 @@ import {
|
|
|
98
102
|
encodeBirthPrompt,
|
|
99
103
|
HERDR_AGENT_SESSION_POLL_MS,
|
|
100
104
|
HERDR_AGENT_SESSION_SETTLE_MS,
|
|
105
|
+
HERDR_AGENT_START_BACKENDS,
|
|
101
106
|
HERDR_CLI_TIMEOUT_MS,
|
|
102
107
|
HERDR_FRESH_CALL_BACKENDS,
|
|
103
108
|
HERDR_FRESH_CALL_OPENING_LINE,
|
|
109
|
+
HERDR_LAYOUT_LAUNCH_BACKENDS,
|
|
104
110
|
HERDR_MAX_OUTPUT_BYTES,
|
|
105
111
|
HERDR_START_READY_MS,
|
|
106
112
|
HERDR_START_TIMEOUT_MS,
|
|
@@ -121,6 +127,8 @@ import {
|
|
|
121
127
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
122
128
|
const MODULE = "pi-extensions/lib/herdr-fresh-call.ts";
|
|
123
129
|
const MODULE_SRC = fs.readFileSync(path.join(REPO_DIR, MODULE), "utf8");
|
|
130
|
+
/** #157 A-L1: the two pi-durable leaves the herdr layout branch imports, by their import specifier. */
|
|
131
|
+
const PI_DURABLE_LEAVES: readonly string[] = ["./pi-durable-fresh-preflight.ts", "./pi-durable-runtime.ts"];
|
|
124
132
|
|
|
125
133
|
let passed = 0;
|
|
126
134
|
function ok(label: string, cond: boolean): void {
|
|
@@ -344,7 +352,7 @@ async function main(): Promise<void> {
|
|
|
344
352
|
const code = MODULE_SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
345
353
|
const specifiers = [...MODULE_SRC.matchAll(/^import\s[^;]*?from\s+"([^"]+)";$/gm)].map((m) => m[1]);
|
|
346
354
|
ok(
|
|
347
|
-
"[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins, the neutral composition leaf, the callback-env pair,
|
|
355
|
+
"[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins, the neutral composition leaf, the callback-env pair, the dependency-free rails DECLARATION leaf, and the two pi-durable preflight/runtime leaves whose own imports are node builtins or each other — one mux or entwurf import here would re-couple the two rails and make either one undeletable",
|
|
348
356
|
specifiers.length > 0 &&
|
|
349
357
|
specifiers.every(
|
|
350
358
|
(s) =>
|
|
@@ -355,7 +363,18 @@ async function main(): Promise<void> {
|
|
|
355
363
|
// example are written, shared with the status pane and the install narration.
|
|
356
364
|
// It imports nothing itself, so admitting it couples this rail to a constant
|
|
357
365
|
// rather than to another rail; the mux/entwurf ban below is untouched.
|
|
358
|
-
s === "../../scripts/herdr-rails.mjs"
|
|
366
|
+
s === "../../scripts/herdr-rails.mjs" ||
|
|
367
|
+
// #157 A-L1 — the herdr layout branch runs pi-durable's own unit/carrier/SDK
|
|
368
|
+
// preflight and package-layout probe. Admitted as LEAVES only: their own imports
|
|
369
|
+
// are checked below, so neither can carry a mux or entwurf import in transitively.
|
|
370
|
+
PI_DURABLE_LEAVES.includes(s),
|
|
371
|
+
) &&
|
|
372
|
+
PI_DURABLE_LEAVES.every((leaf) =>
|
|
373
|
+
[
|
|
374
|
+
...fs
|
|
375
|
+
.readFileSync(path.join(REPO_DIR, "pi-extensions/lib", leaf), "utf8")
|
|
376
|
+
.matchAll(/^import\s[^;]*?from\s+"([^"]+)";$/gm),
|
|
377
|
+
].every((m) => m[1].startsWith("node:") || PI_DURABLE_LEAVES.includes(m[1])),
|
|
359
378
|
) &&
|
|
360
379
|
!/from\s+"\.\/(mux-|entwurf-)/.test(code),
|
|
361
380
|
);
|
|
@@ -371,10 +390,13 @@ async function main(): Promise<void> {
|
|
|
371
390
|
.split("\n")
|
|
372
391
|
.filter((line) => /tmux/i.test(line));
|
|
373
392
|
ok(
|
|
374
|
-
"[QK:HFC-PILOT-CLOSED] the
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
393
|
+
"[QK:HFC-PILOT-CLOSED] the agent-start set is exactly pi + claude-code, the layout set exactly pi-durable (#157 A-L1), and the admitted aggregate exactly their union in that order — and tmux appears in this module ONLY as the refusal of a tmux seat, never as a fallback launch",
|
|
394
|
+
HERDR_AGENT_START_BACKENDS.length === 2 &&
|
|
395
|
+
HERDR_AGENT_START_BACKENDS.includes("pi") &&
|
|
396
|
+
HERDR_AGENT_START_BACKENDS.includes("claude-code") &&
|
|
397
|
+
HERDR_LAYOUT_LAUNCH_BACKENDS.length === 1 &&
|
|
398
|
+
HERDR_LAYOUT_LAUNCH_BACKENDS[0] === "pi-durable" &&
|
|
399
|
+
JSON.stringify(HERDR_FRESH_CALL_BACKENDS) === JSON.stringify(["pi", "claude-code", "pi-durable"]) &&
|
|
378
400
|
tmuxLines.length > 0 &&
|
|
379
401
|
tmuxLines.every((line) => /tmux-rejected|tmuxSession|rejectTmuxPlacementInHerdrContext|tmuxPlacement/.test(line)),
|
|
380
402
|
);
|
|
@@ -531,12 +553,46 @@ async function main(): Promise<void> {
|
|
|
531
553
|
rejectTmuxPlacementInHerdrContext(undefined) === null,
|
|
532
554
|
);
|
|
533
555
|
const base = { backend: "pi", model: "openai-codex/gpt-5.6-sol", task: "do it", callerGardenId: CALLER } as const;
|
|
556
|
+
// #157 A-L1x: placed BEFORE the pre-mutation loop on purpose — `ok` throws at the first failure, and
|
|
557
|
+
// a fallback in the shared cwd chooser must be named by THIS claim. The loop's own Codex row below is
|
|
558
|
+
// kept; without this cell it would see the same defect first, as a herdr call.
|
|
559
|
+
ok(
|
|
560
|
+
"[QK:HFC-CODEX-CWD-NO-FALLBACK] an agent-start call from a known Codex caller whose record carries no cwd — absent or empty — and with no cwd requested is refused as `codex-caller-cwd-unavailable` with this process's directory (the app-server's) never read and the herdr CLI never invoked, never opened wherever this process happens to be",
|
|
561
|
+
await (async () => {
|
|
562
|
+
const processDir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-herdr-codex."));
|
|
563
|
+
let processCwdReads = 0;
|
|
564
|
+
const callerProcessCwd = (): string => {
|
|
565
|
+
processCwdReads += 1;
|
|
566
|
+
return processDir;
|
|
567
|
+
};
|
|
568
|
+
const variants: Parameters<typeof herdrFreshCall>[0][] = [
|
|
569
|
+
{ ...base, callerIsCodex: true },
|
|
570
|
+
{ ...base, callerIsCodex: true, callerCwd: "" },
|
|
571
|
+
];
|
|
572
|
+
try {
|
|
573
|
+
for (const params of variants) {
|
|
574
|
+
const { run, calls } = neverRun();
|
|
575
|
+
const result = await herdrFreshCall(params, run, HERDR_ENV, NONCE, undefined, { callerProcessCwd });
|
|
576
|
+
if (result.ok || result.reason !== "codex-caller-cwd-unavailable" || calls.length !== 0) return false;
|
|
577
|
+
}
|
|
578
|
+
return processCwdReads === 0;
|
|
579
|
+
} catch {
|
|
580
|
+
// A throw here must still end the gate on THIS claim's token, never before it.
|
|
581
|
+
return false;
|
|
582
|
+
} finally {
|
|
583
|
+
fs.rmSync(processDir, { recursive: true, force: true });
|
|
584
|
+
}
|
|
585
|
+
})(),
|
|
586
|
+
);
|
|
534
587
|
const refusals: [string, Parameters<typeof herdrFreshCall>[0], NodeJS.ProcessEnv, string][] = [
|
|
535
588
|
["herdr-context-missing", { ...base }, {}, "herdr-context-missing"],
|
|
536
589
|
["caller pane", { ...base }, { HERDR_ENV: "1", HERDR_BIN_PATH: "/x/herdr" }, "herdr-caller-pane-missing"],
|
|
537
590
|
["backend", { ...base, backend: "codex" }, HERDR_ENV, "herdr-backend-unsupported"],
|
|
538
591
|
["tmux seat", { ...base, placement: { tmuxSession: "org" } }, HERDR_ENV, "herdr-placement-tmux-rejected"],
|
|
539
592
|
["cwd", { ...base, cwd: "relative/dir" }, HERDR_ENV, "cwd-not-absolute"],
|
|
593
|
+
// #157 A-L1x: an agent-start call with no cwd from a known Codex caller whose record has none —
|
|
594
|
+
// refused before any herdr call, never sent to this process's (the app-server's) directory.
|
|
595
|
+
["codex caller, no record cwd", { ...base, callerIsCodex: true }, HERDR_ENV, "codex-caller-cwd-unavailable"],
|
|
540
596
|
];
|
|
541
597
|
// The five INPUT reasons are owned by HFC-INPUT-PARITY below, which asserts both their
|
|
542
598
|
// words and that they too reach no CLI. Splitting them keeps each mutant attributable: a
|
|
@@ -548,7 +604,7 @@ async function main(): Promise<void> {
|
|
|
548
604
|
if (result.ok || result.reason !== expected || calls.length !== 0) everyRefusalPreMutation = false;
|
|
549
605
|
}
|
|
550
606
|
ok(
|
|
551
|
-
"[QK:HFC-PREMUTATION-ONLY] the rail-shaped refusals — context, caller pane, backend, tmux seat, cwd — are decided with the herdr CLI never invoked, so a refused call leaves no tab behind",
|
|
607
|
+
"[QK:HFC-PREMUTATION-ONLY] the rail-shaped refusals — context, caller pane, backend, tmux seat, cwd, a Codex caller with no record cwd — are decided with the herdr CLI never invoked, so a refused call leaves no tab behind",
|
|
552
608
|
everyRefusalPreMutation,
|
|
553
609
|
);
|
|
554
610
|
|
|
@@ -930,16 +986,96 @@ async function main(): Promise<void> {
|
|
|
930
986
|
}
|
|
931
987
|
})(),
|
|
932
988
|
);
|
|
989
|
+
// Placed BEFORE the empty-cwd cell on purpose: `ok` throws at the first failure, and a tab create
|
|
990
|
+
// that drops the caller default must be named by THIS claim, not by the older one below.
|
|
933
991
|
ok(
|
|
934
|
-
"[QK:HFC-CWD-
|
|
992
|
+
"[QK:HFC-CWD-CALLER-DEFAULT] an agent-start call with no cwd starts the sibling where the CALLER is, sent explicitly as `--cwd` (#157 A-L1x) — this process's own directory for an ordinary caller, a known Codex caller's RECORD directory with its process directory (the app-server's) never read, and a requested directory exactly as given — never herdr's own default, which is not a fact about the caller",
|
|
935
993
|
await (async () => {
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
]);
|
|
941
|
-
const
|
|
942
|
-
|
|
994
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-herdr-cwd."));
|
|
995
|
+
const processDir = path.join(root, "process");
|
|
996
|
+
const recordDir = path.join(root, "record");
|
|
997
|
+
const requestedDir = path.join(root, "requested");
|
|
998
|
+
for (const dir of [processDir, recordDir, requestedDir]) fs.mkdirSync(dir);
|
|
999
|
+
const neverRead = (): string => {
|
|
1000
|
+
throw new Error("a Codex caller's process directory was read");
|
|
1001
|
+
};
|
|
1002
|
+
const rows: [Parameters<typeof herdrFreshCall>[0], () => string, string, string, string][] = [
|
|
1003
|
+
[
|
|
1004
|
+
{ ...base, backend: "claude-code" },
|
|
1005
|
+
() => processDir,
|
|
1006
|
+
processDir,
|
|
1007
|
+
"caller-process",
|
|
1008
|
+
"this agent's own process directory",
|
|
1009
|
+
],
|
|
1010
|
+
[
|
|
1011
|
+
{ ...base, backend: "claude-code", callerIsCodex: true, callerCwd: recordDir },
|
|
1012
|
+
neverRead,
|
|
1013
|
+
recordDir,
|
|
1014
|
+
"codex-caller-record",
|
|
1015
|
+
"this Codex caller's own record directory",
|
|
1016
|
+
],
|
|
1017
|
+
[
|
|
1018
|
+
{ ...base, backend: "claude-code", cwd: requestedDir },
|
|
1019
|
+
() => processDir,
|
|
1020
|
+
requestedDir,
|
|
1021
|
+
"requested",
|
|
1022
|
+
"the requested start directory",
|
|
1023
|
+
],
|
|
1024
|
+
];
|
|
1025
|
+
try {
|
|
1026
|
+
for (const [params, callerProcessCwd, dir, source, sourceText] of rows) {
|
|
1027
|
+
const { run, calls } = scriptedRun([
|
|
1028
|
+
{ status: 0, stdout: CALLER_PANE_OK },
|
|
1029
|
+
{ status: 0, stdout: TAB_OK },
|
|
1030
|
+
startReplyEchoingArgv(START_OK),
|
|
1031
|
+
]);
|
|
1032
|
+
const result = await herdrFreshCall(params, run, HERDR_ENV, NONCE, undefined, { callerProcessCwd });
|
|
1033
|
+
if (!result.ok) return false;
|
|
1034
|
+
const at = calls[1].indexOf("--cwd");
|
|
1035
|
+
if (at === -1 || calls[1][at + 1] !== dir) return false;
|
|
1036
|
+
if (result.receipt.cwd !== dir || result.receipt.cwdSource !== source) return false;
|
|
1037
|
+
const text = renderHerdrFreshCall(result).text;
|
|
1038
|
+
if (!text.includes(` cwd: ${dir} (${sourceText}, sent explicitly — not an observation)\n`))
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
return true;
|
|
1042
|
+
} finally {
|
|
1043
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1044
|
+
}
|
|
1045
|
+
})(),
|
|
1046
|
+
);
|
|
1047
|
+
ok(
|
|
1048
|
+
'[QK:HFC-CWD-EMPTY-IS-OMITTED] an empty cwd means OMITTED, exactly as the public verb has always meant it — reading it as a path made a documented no-op into an invalid-directory refusal; since #157 A-L1x omitted means the caller\'s own directory, sent explicitly, so `""` and no cwd produce the same tab create',
|
|
1049
|
+
await (async () => {
|
|
1050
|
+
const processDir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-herdr-empty."));
|
|
1051
|
+
try {
|
|
1052
|
+
const variants: Parameters<typeof herdrFreshCall>[0][] = [
|
|
1053
|
+
{ ...base, backend: "claude-code", cwd: "" },
|
|
1054
|
+
{ ...base, backend: "claude-code" },
|
|
1055
|
+
];
|
|
1056
|
+
const tabCreates: string[][] = [];
|
|
1057
|
+
for (const params of variants) {
|
|
1058
|
+
const { run, calls } = scriptedRun([
|
|
1059
|
+
{ status: 0, stdout: CALLER_PANE_OK },
|
|
1060
|
+
{ status: 0, stdout: TAB_OK },
|
|
1061
|
+
startReplyEchoingArgv(START_OK),
|
|
1062
|
+
]);
|
|
1063
|
+
const result = await herdrFreshCall(params, run, HERDR_ENV, NONCE, undefined, {
|
|
1064
|
+
callerProcessCwd: () => processDir,
|
|
1065
|
+
});
|
|
1066
|
+
if (!result.ok || result.receipt.cwd !== processDir || result.receipt.cwdSource !== "caller-process")
|
|
1067
|
+
return false;
|
|
1068
|
+
tabCreates.push(calls[1]);
|
|
1069
|
+
}
|
|
1070
|
+
const at = tabCreates[0].indexOf("--cwd");
|
|
1071
|
+
return (
|
|
1072
|
+
JSON.stringify(tabCreates[0]) === JSON.stringify(tabCreates[1]) &&
|
|
1073
|
+
at !== -1 &&
|
|
1074
|
+
tabCreates[0][at + 1] === processDir
|
|
1075
|
+
);
|
|
1076
|
+
} finally {
|
|
1077
|
+
fs.rmSync(processDir, { recursive: true, force: true });
|
|
1078
|
+
}
|
|
943
1079
|
})(),
|
|
944
1080
|
);
|
|
945
1081
|
ok(
|
|
@@ -1012,9 +1148,9 @@ async function main(): Promise<void> {
|
|
|
1012
1148
|
const reads = calls.filter((call) => call[0] === "agent" && call[1] === "get");
|
|
1013
1149
|
return (
|
|
1014
1150
|
result.ok &&
|
|
1015
|
-
result.receipt.witness
|
|
1016
|
-
result.receipt.witness
|
|
1017
|
-
result.receipt.witness
|
|
1151
|
+
result.receipt.witness?.state === "reported" &&
|
|
1152
|
+
result.receipt.witness?.reads === 2 &&
|
|
1153
|
+
result.receipt.witness?.settleMs === 2 * HERDR_AGENT_SESSION_POLL_MS &&
|
|
1018
1154
|
reads.length === 2 &&
|
|
1019
1155
|
// EXACT NAME, both times. A listing would make us pick "the one that looks like
|
|
1020
1156
|
// ours" out of every agent on the server — the guess this rail refuses.
|
|
@@ -1040,9 +1176,9 @@ async function main(): Promise<void> {
|
|
|
1040
1176
|
const expectedReads = HERDR_AGENT_SESSION_SETTLE_MS / HERDR_AGENT_SESSION_POLL_MS;
|
|
1041
1177
|
return (
|
|
1042
1178
|
result.ok &&
|
|
1043
|
-
result.receipt.witness
|
|
1044
|
-
result.receipt.witness
|
|
1045
|
-
result.receipt.witness
|
|
1179
|
+
result.receipt.witness?.state === "unavailable" &&
|
|
1180
|
+
result.receipt.witness?.settleMs === HERDR_AGENT_SESSION_SETTLE_MS &&
|
|
1181
|
+
result.receipt.witness?.reads === expectedReads &&
|
|
1046
1182
|
// The whole point: nothing was closed, and the window is bounded rather than open.
|
|
1047
1183
|
!calls.some((call) => call[0] === "pane" && call[1] === "close") &&
|
|
1048
1184
|
clock.elapsed() === HERDR_AGENT_SESSION_SETTLE_MS &&
|
|
@@ -1067,8 +1203,8 @@ async function main(): Promise<void> {
|
|
|
1067
1203
|
if (!result.ok) return false;
|
|
1068
1204
|
const text = renderHerdrFreshCall(result).text;
|
|
1069
1205
|
return (
|
|
1070
|
-
result.receipt.witness
|
|
1071
|
-
result.receipt.witness
|
|
1206
|
+
result.receipt.witness?.state === "unobserved" &&
|
|
1207
|
+
result.receipt.witness?.settleMs === HERDR_AGENT_SESSION_SETTLE_MS &&
|
|
1072
1208
|
text.includes("unobserved after") &&
|
|
1073
1209
|
text.includes("says nothing about whether the sibling is running") &&
|
|
1074
1210
|
// THE DEFECT THIS CELL OWNS: a receipt that never got a readable answer used to
|
|
@@ -1092,7 +1228,7 @@ async function main(): Promise<void> {
|
|
|
1092
1228
|
const result = await herdrFreshCall({ ...base, backend: "claude-code" }, run, HERDR_ENV, NONCE, clock);
|
|
1093
1229
|
return (
|
|
1094
1230
|
result.ok &&
|
|
1095
|
-
result.receipt.witness
|
|
1231
|
+
result.receipt.witness?.state === "unobserved" &&
|
|
1096
1232
|
!calls.some((call) => call[0] === "pane" && call[1] === "close")
|
|
1097
1233
|
);
|
|
1098
1234
|
})(),
|
|
@@ -99,7 +99,11 @@ interface Profile {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const activation = (await import(pathToFileURL(path.join(REPO, "scripts", "herdr-activation.mjs")).href)) as {
|
|
102
|
-
writeLedger: (
|
|
102
|
+
writeLedger: (
|
|
103
|
+
layout: { stateRoot: string; ledgerPath: string },
|
|
104
|
+
version: number,
|
|
105
|
+
entry: Record<string, unknown>,
|
|
106
|
+
) => void;
|
|
103
107
|
ledgerBody: (args: Record<string, unknown>) => Record<string, unknown>;
|
|
104
108
|
resolveActivationLayout: (env: NodeJS.ProcessEnv) => { stateRoot: string; ledgerPath: string };
|
|
105
109
|
resolveComponentRoots: (env: NodeJS.ProcessEnv) => Record<string, { path: string; source: string }>;
|
|
@@ -192,6 +196,8 @@ function fixtureAcquire(opts: {
|
|
|
192
196
|
log?: string;
|
|
193
197
|
fail?: boolean;
|
|
194
198
|
name?: string;
|
|
199
|
+
/** Extra package files, relative to the package root — e.g. the candidate's own ledger reader. */
|
|
200
|
+
files?: Record<string, string>;
|
|
195
201
|
}) {
|
|
196
202
|
const name = opts.name ?? PACKAGE;
|
|
197
203
|
return ({ identity, prefix }: AcquireArgs) => {
|
|
@@ -199,6 +205,10 @@ function fixtureAcquire(opts: {
|
|
|
199
205
|
const root = path.join(prefix, "node_modules", name);
|
|
200
206
|
fs.mkdirSync(path.join(root, path.dirname(COMPILED_ENTRY)), { recursive: true });
|
|
201
207
|
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name, version })}\n`);
|
|
208
|
+
for (const [rel, body] of Object.entries(opts.files ?? {})) {
|
|
209
|
+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
210
|
+
fs.writeFileSync(path.join(root, rel), body);
|
|
211
|
+
}
|
|
202
212
|
fs.writeFileSync(path.join(root, COMPILED_ENTRY), "// compiled entry fixture\n");
|
|
203
213
|
const bin = path.join(prefix, "node_modules", ".bin");
|
|
204
214
|
fs.mkdirSync(bin, { recursive: true });
|
|
@@ -564,6 +574,7 @@ function drive(
|
|
|
564
574
|
).artifactIdentity;
|
|
565
575
|
activation.writeLedger(
|
|
566
576
|
activation.resolveActivationLayout(env),
|
|
577
|
+
2,
|
|
567
578
|
activation.ledgerBody({
|
|
568
579
|
phase: "active",
|
|
569
580
|
runtimeRoot: activeDirOf(env),
|
|
@@ -1177,4 +1188,165 @@ function countingSentinel(): { acquire: () => never; calls: () => number } {
|
|
|
1177
1188
|
);
|
|
1178
1189
|
}
|
|
1179
1190
|
|
|
1191
|
+
// ── 12. a floor host admits only a candidate whose OWN reader reads its ledger (#153) ─
|
|
1192
|
+
{
|
|
1193
|
+
const PI = listing({ pi: "current (v8) (/home/u/.pi)" });
|
|
1194
|
+
const READER = "scripts/herdr-activation.mjs";
|
|
1195
|
+
// The current checkout's reader and the one module it imports: a candidate that knows the floor.
|
|
1196
|
+
const CHECKOUT_READER = {
|
|
1197
|
+
[READER]: fs.readFileSync(path.join(REPO, READER), "utf8"),
|
|
1198
|
+
"scripts/herdr-runtime.mjs": fs.readFileSync(path.join(REPO, "scripts", "herdr-runtime.mjs"), "utf8"),
|
|
1199
|
+
};
|
|
1200
|
+
// Minimal synthetic readers. RAW answers the file's own value, so each variant differs from an
|
|
1201
|
+
// exact answer in ONE respect only.
|
|
1202
|
+
const RAW = 'import fs from "node:fs";\nconst raw = (l) => JSON.parse(fs.readFileSync(l.ledgerPath, "utf8"));\n';
|
|
1203
|
+
const reader = (body: string) => ({ [READER]: body });
|
|
1204
|
+
const THROWS = reader(
|
|
1205
|
+
'export function readCertifiedLedger() {\n\tconst e = new Error("activation-ledger-uncertified: schemaVersion 3");\n\te.code = "activation-ledger-uncertified";\n\tthrow e;\n}\n',
|
|
1206
|
+
);
|
|
1207
|
+
/** A settled pi host on FIXTURE_NPM_LOCK, its ledger written at `version` — or no ledger at all. */
|
|
1208
|
+
const host = (tag: string, version: number | null) => {
|
|
1209
|
+
const env = world(tag);
|
|
1210
|
+
const log = path.join(env.HOME as string, "argv.log");
|
|
1211
|
+
drive(env, PI, { acquire: fixtureAcquire({ activate: "recorder", log }), resolveCommit: () => COMMIT_A });
|
|
1212
|
+
if (version !== null) {
|
|
1213
|
+
const artifactIdentity = (
|
|
1214
|
+
JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as { artifactIdentity: Record<string, unknown> }
|
|
1215
|
+
).artifactIdentity;
|
|
1216
|
+
activation.writeLedger(
|
|
1217
|
+
activation.resolveActivationLayout(env),
|
|
1218
|
+
version,
|
|
1219
|
+
activation.ledgerBody({
|
|
1220
|
+
phase: "active",
|
|
1221
|
+
runtimeRoot: activeDirOf(env),
|
|
1222
|
+
artifactIdentity,
|
|
1223
|
+
roots: activation.resolveComponentRoots(env),
|
|
1224
|
+
states: { pi: "active" },
|
|
1225
|
+
}),
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
return { env, log };
|
|
1229
|
+
};
|
|
1230
|
+
const ledgerText = (env: NodeJS.ProcessEnv): string => {
|
|
1231
|
+
const p = activation.resolveActivationLayout(env).ledgerPath;
|
|
1232
|
+
return fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "<absent>";
|
|
1233
|
+
};
|
|
1234
|
+
const journalOf = (env: NodeJS.ProcessEnv) =>
|
|
1235
|
+
JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as {
|
|
1236
|
+
phase: string;
|
|
1237
|
+
artifactIdentity: Record<string, unknown>;
|
|
1238
|
+
previousRuntime: Record<string, unknown> | null;
|
|
1239
|
+
};
|
|
1240
|
+
/** Rebuild onto ANOTHER integrity — a real candidate to stage — and record every non-herdr spawn. */
|
|
1241
|
+
const rebuild = (h: { env: NodeJS.ProcessEnv; log: string }, files: Record<string, string>) => {
|
|
1242
|
+
const before = {
|
|
1243
|
+
runtime: treeDigest(activeDirOf(h.env)),
|
|
1244
|
+
ledger: ledgerText(h.env),
|
|
1245
|
+
activations: fs.readFileSync(h.log, "utf8"),
|
|
1246
|
+
ready: journalOf(h.env).artifactIdentity,
|
|
1247
|
+
};
|
|
1248
|
+
const calls: string[][] = [];
|
|
1249
|
+
const spawn = (bin: string, argv: string[], opts: unknown): SpawnResult => {
|
|
1250
|
+
if (bin === HERDR_STUB) return { status: 0, stdout: PI, stderr: "" };
|
|
1251
|
+
calls.push([bin, ...argv]);
|
|
1252
|
+
return spawnSync(bin, argv, opts as Record<string, never>) as unknown as SpawnResult;
|
|
1253
|
+
};
|
|
1254
|
+
const run = drive(h.env, PI, {
|
|
1255
|
+
spawn,
|
|
1256
|
+
acquire: fixtureAcquire({ activate: "recorder", log: h.log, files }),
|
|
1257
|
+
readRuntimeLock: () => ({ ...FIXTURE_NPM_LOCK, integrity: FIXTURE_OTHER_INTEGRITY }),
|
|
1258
|
+
resolveCommit: () => COMMIT_B,
|
|
1259
|
+
});
|
|
1260
|
+
const probes = calls.filter((c) => c.includes("--input-type=module"));
|
|
1261
|
+
const runtimeDir = path.dirname(activeDirOf(h.env));
|
|
1262
|
+
const journal = journalOf(h.env);
|
|
1263
|
+
// A refusal of the candidate: the runtime the wiring names, the ledger and the activation log
|
|
1264
|
+
// are byte-identical, nothing is staged or backed up, and the journal is the `installing` retry
|
|
1265
|
+
// authority still carrying the runtime it replaced — not a zero-write rollback.
|
|
1266
|
+
const retained =
|
|
1267
|
+
run.refusal === "herdr-build-candidate-floor-refused" &&
|
|
1268
|
+
treeDigest(activeDirOf(h.env)) === before.runtime &&
|
|
1269
|
+
ledgerText(h.env) === before.ledger &&
|
|
1270
|
+
fs.readFileSync(h.log, "utf8") === before.activations &&
|
|
1271
|
+
!fs.existsSync(path.join(runtimeDir, "staging")) &&
|
|
1272
|
+
!fs.existsSync(path.join(runtimeDir, "previous")) &&
|
|
1273
|
+
journal.phase === "installing" &&
|
|
1274
|
+
JSON.stringify(journal.previousRuntime) === JSON.stringify(before.ready);
|
|
1275
|
+
return {
|
|
1276
|
+
run,
|
|
1277
|
+
probes,
|
|
1278
|
+
retained,
|
|
1279
|
+
stagingReader: probes[0]?.includes(path.join(runtimeDir, "staging", "node_modules", PACKAGE, READER)),
|
|
1280
|
+
};
|
|
1281
|
+
};
|
|
1282
|
+
const show = (r: ReturnType<typeof rebuild>) =>
|
|
1283
|
+
`${r.run.code ?? r.run.refusal}/probes=${r.probes.length}/retained=${r.retained}`;
|
|
1284
|
+
|
|
1285
|
+
const supplied = rebuild(host("floor-supplied", 3), CHECKOUT_READER);
|
|
1286
|
+
ok(
|
|
1287
|
+
"[QK:HPB-FLOOR-PROBE-SUPPLIED] on a 3-ledger host the official build ITSELF supplies the admission: the " +
|
|
1288
|
+
"candidate's own reader is run once, from staging, against this host's ledger, and a candidate whose reader " +
|
|
1289
|
+
"answers exactly the certified ledger is installed and activated. This observes the reader; it does not " +
|
|
1290
|
+
`certify the candidate's whole lifecycle (${show(supplied)} staging-reader=${supplied.stagingReader})`,
|
|
1291
|
+
supplied.run.code === 0 && supplied.probes.length === 1 && supplied.stagingReader === true,
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
const legacy2 = rebuild(host("floor-legacy-2", 2), THROWS);
|
|
1295
|
+
const legacyNone = rebuild(host("floor-legacy-none", null), THROWS);
|
|
1296
|
+
ok(
|
|
1297
|
+
"[QK:HPB-FLOOR-PROBE-LEGACY-UNPROBED] a 2-ledger host and a host with no ledger take the existing path: no " +
|
|
1298
|
+
"probe runs, so even a candidate whose reader would refuse everything is installed — the floor is what " +
|
|
1299
|
+
`turns the observation on, nothing else (two=${show(legacy2)} none=${show(legacyNone)})`,
|
|
1300
|
+
legacy2.run.code === 0 &&
|
|
1301
|
+
legacy2.probes.length === 0 &&
|
|
1302
|
+
legacyNone.run.code === 0 &&
|
|
1303
|
+
legacyNone.probes.length === 0,
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
const exit7 = rebuild(
|
|
1307
|
+
host("floor-exit7", 3),
|
|
1308
|
+
reader(`${RAW}process.exitCode = 7;\nexport const readCertifiedLedger = raw;\n`),
|
|
1309
|
+
);
|
|
1310
|
+
const throws = rebuild(host("floor-throws", 3), THROWS);
|
|
1311
|
+
const absent = rebuild(host("floor-absent", 3), {});
|
|
1312
|
+
ok(
|
|
1313
|
+
"[QK:HPB-FLOOR-PROBE-EXIT-STATUS] the candidate's reader must EXIT 0: one that prints the exact ledger and then " +
|
|
1314
|
+
"exits 7 is refused for its exit alone, and an older reader that refuses the floor, or a candidate with no " +
|
|
1315
|
+
"reader at all, is refused too — each with the active runtime, ledger and activation log byte-identical " +
|
|
1316
|
+
`(exit7=${show(exit7)}/named=${exit7.run.detail.includes("exited 7")} throws=${show(throws)} absent=${show(absent)})`,
|
|
1317
|
+
exit7.retained && exit7.run.detail.includes("exited 7") && throws.retained && absent.retained,
|
|
1318
|
+
);
|
|
1319
|
+
|
|
1320
|
+
const noisy = rebuild(
|
|
1321
|
+
host("floor-stderr", 3),
|
|
1322
|
+
reader(`${RAW}process.stderr.write("noise\\n");\nexport const readCertifiedLedger = raw;\n`),
|
|
1323
|
+
);
|
|
1324
|
+
ok(
|
|
1325
|
+
"[QK:HPB-FLOOR-PROBE-STDERR-SILENT] a reader that answers the exact ledger and exits 0 but writes to stderr is " +
|
|
1326
|
+
"refused: an unexpected word from the candidate is not consent " +
|
|
1327
|
+
`(${show(noisy)} named=${noisy.run.detail.includes("stderr")})`,
|
|
1328
|
+
noisy.retained && noisy.run.detail.includes("stderr"),
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
const nulled = rebuild(host("floor-null", 3), reader("export function readCertifiedLedger() {\n\treturn null;\n}\n"));
|
|
1332
|
+
const lowered = rebuild(
|
|
1333
|
+
host("floor-lowered", 3),
|
|
1334
|
+
reader(`${RAW}export const readCertifiedLedger = (l) => ({ ...raw(l), schemaVersion: 2 });\n`),
|
|
1335
|
+
);
|
|
1336
|
+
const otherPhase = rebuild(
|
|
1337
|
+
host("floor-other-value", 3),
|
|
1338
|
+
reader(`${RAW}export const readCertifiedLedger = (l) => ({ ...raw(l), phase: "activating" });\n`),
|
|
1339
|
+
);
|
|
1340
|
+
const malformed = rebuild(
|
|
1341
|
+
host("floor-malformed", 3),
|
|
1342
|
+
reader(`${RAW}process.stdout.write("noise\\n");\nexport const readCertifiedLedger = raw;\n`),
|
|
1343
|
+
);
|
|
1344
|
+
ok(
|
|
1345
|
+
"[QK:HPB-FLOOR-PROBE-VALUE-EXACT] the reader's answer must be exactly the ledger this build certified, as one " +
|
|
1346
|
+
"JSON value: null, the same ledger lowered to 2, a different phase, and output that is not one JSON value " +
|
|
1347
|
+
`are each refused (null=${show(nulled)} lowered=${show(lowered)} other=${show(otherPhase)} malformed=${show(malformed)})`,
|
|
1348
|
+
nulled.retained && lowered.retained && otherPhase.retained && malformed.retained,
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1180
1352
|
console.log(`\ncheck-herdr-plugin-build: ${passed} assertions passed`);
|
|
@@ -409,7 +409,7 @@ const PEERS = JSON.stringify({
|
|
|
409
409
|
const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
410
410
|
// The checkout's activation module may answer WHERE, and only where. The BODY is certified by
|
|
411
411
|
// the installed runtime's own copy, because the pane and the writer are two artifacts that can
|
|
412
|
-
// ship two
|
|
412
|
+
// ship two sets of admitted ledger versions and a checkout-side parser would report a newer
|
|
413
413
|
// runtime's ledger as uncertified on a perfectly healthy host. Location is not schema-versioned;
|
|
414
414
|
// certification is. So: the checkout module is reached for `resolveActivationLayout` and for
|
|
415
415
|
// nothing else.
|
|
@@ -831,7 +831,33 @@ const PEERS = JSON.stringify({
|
|
|
831
831
|
console.log(" ok no aggregate verdict token over the four blocks");
|
|
832
832
|
passed++;
|
|
833
833
|
|
|
834
|
-
//
|
|
834
|
+
// The two launch domains stay two (#157 A-L2): pi and claude-code are started by herdr as agents,
|
|
835
|
+
// pi-durable only gets a tab through one layout request. Literal on purpose, and placed BEFORE the
|
|
836
|
+
// shared-source claim: a pane that moved pi-durable into the agent-start group would still render
|
|
837
|
+
// every backend and every example, so only this claim can name that defect.
|
|
838
|
+
{
|
|
839
|
+
const rails = r.stdout.slice(r.stdout.indexOf("[3] RAILS"), r.stdout.indexOf("[4] CITIZENS"));
|
|
840
|
+
const agentStartLabel = rails.indexOf(" through tab create + agent start --kind:");
|
|
841
|
+
const layoutLabel = rails.indexOf(" through ONE layout.apply request (no herdr agent kind):");
|
|
842
|
+
const at = (backend: string) => rails.indexOf(` ${backend}: `);
|
|
843
|
+
const durableLine = rails.slice(at("pi-durable")).split("\n")[0];
|
|
844
|
+
assert.ok(
|
|
845
|
+
rails.includes("3 fresh-call backend(s): 2 through herdr agent start, 1 through one layout request") &&
|
|
846
|
+
agentStartLabel !== -1 &&
|
|
847
|
+
agentStartLabel < at("pi") &&
|
|
848
|
+
at("pi") < at("claude-code") &&
|
|
849
|
+
at("claude-code") < layoutLabel &&
|
|
850
|
+
layoutLabel < at("pi-durable") &&
|
|
851
|
+
durableLine.includes("neither detected nor associated with a session") &&
|
|
852
|
+
!rails.includes("--kind pi-durable"),
|
|
853
|
+
`[QK:HPL-RAILS-DOMAINS-DISTINCT] the RAILS block shows the public fresh-call aggregate of three as two launch domains — pi and claude-code under herdr agent start, pi-durable alone under one layout request — and never presents pi-durable as a herdr agent kind or as detected/session-associated (rails block: ${JSON.stringify(rails)})`,
|
|
854
|
+
);
|
|
855
|
+
console.log(" ok rails show agent start (pi, claude-code) and one layout request (pi-durable) as two domains");
|
|
856
|
+
passed++;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// RAILS come from the shared declaration leaf, not from prose in this pane: every backend of the
|
|
860
|
+
// public fresh-call aggregate, each with its one model SYNTAX example.
|
|
835
861
|
assert.ok(
|
|
836
862
|
HERDR_RAILS.HERDR_FRESH_CALL_BACKENDS.every(
|
|
837
863
|
(b) => r.stdout.includes(` ${b}: `) && r.stdout.includes(HERDR_RAILS.MODEL_SYNTAX_EXAMPLE[b]),
|