@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.
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +628 -0
- package/DELIVERY.md +24 -1
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +20 -2
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +46 -5
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +83 -154
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +6 -1
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
- package/pi-extensions/lib/herdr-placement.ts +24 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/protocol.js +0 -8
- package/run.sh +306 -53
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +51 -9
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +30 -6
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +8 -2
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +15 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
- package/scripts/smoke-herdr-raw-install-live.sh +90 -5
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +108 -2
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -170,14 +170,13 @@ async function main(): Promise<void> {
|
|
|
170
170
|
|
|
171
171
|
// ── one nonce, into whichever rail ───────────────────────────────────────────────────
|
|
172
172
|
const nonceSpawn = recordingSpawn();
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
const dispatchNonce = "mux-fresh-call-deadbeefdeadbeefdeadbeef";
|
|
174
|
+
await dispatchFreshCall(REQUEST, HERDR_ENV, nonceSpawn.spawn, dispatchNonce);
|
|
175
|
+
const tabCall = nonceSpawn.calls.find((call) => call[1] === "tab" && call[2] === "create");
|
|
175
176
|
ok(
|
|
176
177
|
"[QK:FCD-ONE-NONCE] ONE nonce is minted per call and handed to the selected rail — two mints would mean the sibling calls back with a tag the caller never recorded",
|
|
177
178
|
dispatchCode.split("mintNonce(").length - 1 === 1 &&
|
|
178
|
-
(
|
|
179
|
-
startCall.some((token) => token.includes("fresh-call-dispatch-test-nonce")) ||
|
|
180
|
-
nonceSpawn.calls.length > 0),
|
|
179
|
+
(tabCall === undefined || tabCall.some((token) => token.includes(dispatchNonce)) || nonceSpawn.calls.length > 0),
|
|
181
180
|
);
|
|
182
181
|
|
|
183
182
|
// ── the composition root holds no synchronous child authority ────────────────────────
|
|
@@ -49,6 +49,7 @@ import * as path from "node:path";
|
|
|
49
49
|
import { fileURLToPath } from "node:url";
|
|
50
50
|
import {
|
|
51
51
|
classifyMutantRun,
|
|
52
|
+
countOccurrences,
|
|
52
53
|
createRepoSnapshot,
|
|
53
54
|
ManifestError,
|
|
54
55
|
type MutantManifest,
|
|
@@ -820,6 +821,7 @@ let manifestCount: number;
|
|
|
820
821
|
// manifest lost in a merge must name itself here — "0/0 killed" is not a pass.
|
|
821
822
|
// Extend this inventory and the manifests TOGETHER, never silently.
|
|
822
823
|
const EXPECTED_LANE_MUTANTS: Record<string, number> = {
|
|
824
|
+
"acp-backend-preflight": 1,
|
|
823
825
|
"acp-augment": 10,
|
|
824
826
|
"acp-cortex": 14,
|
|
825
827
|
"acp-launch-namespace": 2,
|
|
@@ -829,37 +831,39 @@ let manifestCount: number;
|
|
|
829
831
|
"acp-stream-hooks": 10,
|
|
830
832
|
"acp-usage-accounting": 12,
|
|
831
833
|
"agy-permission": 6,
|
|
832
|
-
"bridge-boot-resume":
|
|
833
|
-
"bridge-command-boot":
|
|
834
|
+
"bridge-boot-resume": 5,
|
|
835
|
+
"bridge-command-boot": 12,
|
|
834
836
|
"capability-cache": 3,
|
|
835
837
|
"codex-app-server-launch": 9,
|
|
836
838
|
"codex-caller-seat": 28,
|
|
839
|
+
"control-send-receipt": 19,
|
|
837
840
|
"codex-native": 82,
|
|
838
841
|
"compaction-send-guard": 7,
|
|
839
842
|
"control-socket-disconnect": 4,
|
|
840
843
|
"copilot-birth": 19,
|
|
841
844
|
"copilot-launch": 14,
|
|
842
845
|
"pi-launch": 9,
|
|
846
|
+
"pi-floor": 1,
|
|
843
847
|
"copilot-receive": 20,
|
|
844
848
|
"entwurf-peers": 1,
|
|
845
849
|
"fresh-call-dispatch": 12,
|
|
846
850
|
"fresh-cut": 3,
|
|
847
851
|
"gate-qualification": 2,
|
|
848
852
|
"herdr-placement": 13,
|
|
849
|
-
"herdr-plugin":
|
|
853
|
+
"herdr-plugin": 25,
|
|
850
854
|
"herdr-plugin-profile": 14,
|
|
851
|
-
"herdr-activation":
|
|
855
|
+
"herdr-activation": 25,
|
|
852
856
|
"herdr-plugin-build": 13,
|
|
853
857
|
"herdr-runtime-bootstrap": 31,
|
|
854
|
-
"herdr-fresh-call":
|
|
858
|
+
"herdr-fresh-call": 38,
|
|
855
859
|
"herdr-supply": 11,
|
|
856
860
|
"meta-facts": 4,
|
|
857
861
|
"peer-facts": 8,
|
|
858
862
|
"meta-hook-session-switch": 17,
|
|
859
863
|
"meta-identity": 4,
|
|
860
|
-
"meta-retire":
|
|
864
|
+
"meta-retire": 4,
|
|
861
865
|
"mux-boundary": 16,
|
|
862
|
-
"mux-fresh-call":
|
|
866
|
+
"mux-fresh-call": 59,
|
|
863
867
|
"mux-launcher-fence": 8,
|
|
864
868
|
"mux-parent-artifact": 3,
|
|
865
869
|
"pack-install": 2,
|
|
@@ -869,7 +873,8 @@ let manifestCount: number;
|
|
|
869
873
|
"omp-fresh": 24,
|
|
870
874
|
"omp-receive": 11,
|
|
871
875
|
"probe-ordering": 1,
|
|
872
|
-
"release-gate":
|
|
876
|
+
"release-gate": 22,
|
|
877
|
+
"retired-sent-surface": 2,
|
|
873
878
|
"resume-args": 6,
|
|
874
879
|
"resume-launch-identity": 6,
|
|
875
880
|
"self-address": 8,
|
|
@@ -902,6 +907,42 @@ let manifestCount: number;
|
|
|
902
907
|
(err instanceof Error ? err.message : String(err)),
|
|
903
908
|
);
|
|
904
909
|
}
|
|
910
|
+
// FIND MATCHES ITS SUBJECT, EXACTLY ONCE — the body's own precondition, hoisted into the head.
|
|
911
|
+
// `validateManifestSet` above proves the subject is TRACKED; it never opens it. So a change
|
|
912
|
+
// that edits a subject line a mutant quotes leaves the whole deterministic floor green and
|
|
913
|
+
// dies an hour later in CI or the release gate as MUTANT-STALE. That is exactly how the
|
|
914
|
+
// 0.23.2 pi bump landed: `pack_install_leaked_pi`'s pin regex moved 0.85.1 → 0.86.0 in
|
|
915
|
+
// run.sh, `scripts/mutants/pack-install.json` kept quoting the old line, and two claims came
|
|
916
|
+
// back NOT KILLED after ~50 minutes of mutant execution. The check costs one read per
|
|
917
|
+
// mutant and uses `countOccurrences` — the SAME function the body applies at
|
|
918
|
+
// mutation-qualify.ts:743 — so head and body cannot disagree about what "matches" means.
|
|
919
|
+
// This is a STALENESS check, not a kill-proof: it says the mutant still has a subject to
|
|
920
|
+
// corrupt, never that corrupting it is caught. That remains the body's verdict.
|
|
921
|
+
const stale: string[] = [];
|
|
922
|
+
for (const man of manifests) {
|
|
923
|
+
for (const m of man.mutants) {
|
|
924
|
+
const subjectAbs = path.join(REPO_DIR, m.subject);
|
|
925
|
+
let source: string;
|
|
926
|
+
try {
|
|
927
|
+
source = fs.readFileSync(subjectAbs, "utf8");
|
|
928
|
+
} catch (err) {
|
|
929
|
+
stale.push(`${man.lane}/${m.claim}: subject ${m.subject} unreadable (${(err as Error).message})`);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
const n = countOccurrences(source, m.find.join("\n"));
|
|
933
|
+
if (n !== 1) stale.push(`${man.lane}/${m.claim}: find matched ${n}× in ${m.subject} (expected exactly 1)`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
assert.deepEqual(
|
|
937
|
+
stale,
|
|
938
|
+
[],
|
|
939
|
+
"[QK:MUTANT-FIND-MATCHES-SUBJECT] every mutant's `find` must match its subject exactly once. A find that " +
|
|
940
|
+
"matches 0× is a mutant whose production line moved out from under it — the body can write nothing, the " +
|
|
941
|
+
"verdict is MUTANT-STALE, and the claim is silently unproven; a find that matches 2+× cannot say WHICH " +
|
|
942
|
+
"occurrence it corrupts. Either way the manifest and the source have to move together. Found:\n" +
|
|
943
|
+
stale.join("\n"),
|
|
944
|
+
);
|
|
945
|
+
|
|
905
946
|
manifestCount = manifests.length;
|
|
906
947
|
console.log(`[${SURFACE}] ${selected.length} mutants across ${manifestCount} lanes (no tiers — full set every run)`);
|
|
907
948
|
}
|
|
@@ -909,7 +950,8 @@ let manifestCount: number;
|
|
|
909
950
|
if (MANIFESTS_ONLY) {
|
|
910
951
|
console.log(
|
|
911
952
|
`[check-gate-manifests] ok — runner self-test green, ${selected.length} committed mutants across ` +
|
|
912
|
-
`${manifestCount} lanes validated against the origin index,
|
|
953
|
+
`${manifestCount} lanes validated against the origin index, every one of their \`find\` strings still ` +
|
|
954
|
+
"matching its subject exactly once, and the lane inventory matching its declared " +
|
|
913
955
|
"contract. ZERO mutants were executed and this repo was never snapshotted: the body " +
|
|
914
956
|
"(check-gate-qualification) owns that, unchanged.",
|
|
915
957
|
);
|
|
@@ -205,6 +205,21 @@ const {
|
|
|
205
205
|
COMPONENT_STATES,
|
|
206
206
|
} = mod;
|
|
207
207
|
|
|
208
|
+
/** The pi pin package.json declares — the SSOT `run.sh pi_supported_range` derives its range from. */
|
|
209
|
+
const PI_PIN = (
|
|
210
|
+
JSON.parse(fs.readFileSync(path.join(REPO, "package.json"), "utf8")) as {
|
|
211
|
+
devDependencies: Record<string, string>;
|
|
212
|
+
}
|
|
213
|
+
).devDependencies["@earendil-works/pi-coding-agent"];
|
|
214
|
+
|
|
215
|
+
/** A `pi` stand-in that reports `version` and nothing else — enough for a `--version` floor probe. */
|
|
216
|
+
function fakePi(dir: string, name: string, version: string): string {
|
|
217
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
+
const file = path.join(dir, name);
|
|
219
|
+
fs.writeFileSync(file, `#!/bin/sh\necho ${version}\n`, { mode: 0o755 });
|
|
220
|
+
return file;
|
|
221
|
+
}
|
|
222
|
+
|
|
208
223
|
function world(tag: string): NodeJS.ProcessEnv {
|
|
209
224
|
const home = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), `entwurf-hac-${tag}-`)));
|
|
210
225
|
for (const d of [".config", ".state", ".data", ".cache", "pi-agent", ".claude"]) {
|
|
@@ -220,6 +235,13 @@ function world(tag: string): NodeJS.ProcessEnv {
|
|
|
220
235
|
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
221
236
|
PI_CODING_AGENT_DIR: path.join(home, "pi-agent"),
|
|
222
237
|
CLAUDE_CONFIG_DIR: path.join(home, ".claude"),
|
|
238
|
+
// PI_BIN is a sandbox root like the four XDG vars above, not a convenience. Since #119
|
|
239
|
+
// `install-user-scope` probes `pi --version` against the same closed range setup enforces,
|
|
240
|
+
// so leaving this unset would let the OPERATOR'S installed pi decide whether these cells
|
|
241
|
+
// pass — the gate would go red on a developer machine sitting one minor behind the pin and
|
|
242
|
+
// green on another, for reasons none of these cells are about. It is pinned IN RANGE here;
|
|
243
|
+
// the one cell whose subject IS the floor overrides it per run.
|
|
244
|
+
PI_BIN: fakePi(path.join(home, "bin"), "pi", PI_PIN),
|
|
223
245
|
};
|
|
224
246
|
}
|
|
225
247
|
|
|
@@ -330,6 +352,57 @@ function ledgerWith(
|
|
|
330
352
|
);
|
|
331
353
|
}
|
|
332
354
|
|
|
355
|
+
// ── 1b. the plugin door enforces the SAME pi floor the setup door does ────────
|
|
356
|
+
// #119: `setup` refuses an out-of-range pi by name before it writes any Pi wiring, but
|
|
357
|
+
// `install-user-scope` — the door herdr plugin activation actually goes through
|
|
358
|
+
// (plugins/herdr/lib/build.mjs → scripts/herdr-plugin-activate.mjs → `run.sh
|
|
359
|
+
// install-user-scope --plugin-runtime …`) — had no version check at all. Two doors onto ONE
|
|
360
|
+
// registration telling different truths is the whole defect, so the oracle here is the
|
|
361
|
+
// out-of-range/in-range PAIR: the refusal alone would also be satisfied by a door that refuses
|
|
362
|
+
// everything. Absent pi is deliberately NOT asserted as a third state — this verb invents no
|
|
363
|
+
// presence verdict, setup owns that one.
|
|
364
|
+
{
|
|
365
|
+
const env = world("pi-floor");
|
|
366
|
+
const bin = path.join(env.HOME as string, "bin");
|
|
367
|
+
// The supported range is DERIVED from the package.json devDep pin (run.sh
|
|
368
|
+
// `pi_supported_range`), so the fixtures are derived from it too — a hardcoded "0.85.1" here
|
|
369
|
+
// would silently stop being out-of-range the day the pin moves past it.
|
|
370
|
+
const [maj, min] = PI_PIN.split(".").map(Number);
|
|
371
|
+
const belowFloor = `${maj}.${min - 1}.0`;
|
|
372
|
+
const settingsPath = path.join(env.PI_CODING_AGENT_DIR as string, "settings.json");
|
|
373
|
+
fs.writeFileSync(settingsPath, '{"theme":"dark"}\n');
|
|
374
|
+
const before = fs.readFileSync(settingsPath, "utf8");
|
|
375
|
+
|
|
376
|
+
const stale = sh({ ...env, PI_BIN: fakePi(bin, "stale-pi", belowFloor) }, [
|
|
377
|
+
"install-user-scope",
|
|
378
|
+
"--plugin-runtime",
|
|
379
|
+
runtimeRootOf(env),
|
|
380
|
+
]);
|
|
381
|
+
const afterRefusal = fs.readFileSync(settingsPath, "utf8");
|
|
382
|
+
const current = sh({ ...env, PI_BIN: fakePi(bin, "current-pi", PI_PIN) }, [
|
|
383
|
+
"install-user-scope",
|
|
384
|
+
"--plugin-runtime",
|
|
385
|
+
runtimeRootOf(env),
|
|
386
|
+
]);
|
|
387
|
+
const afterAccept = readJson(settingsPath) as { packages?: string[] };
|
|
388
|
+
|
|
389
|
+
ok(
|
|
390
|
+
"[QK:HAC-PI-FLOOR-BOTH-DOORS] the plugin activation door enforces the SAME closed pi range as the " +
|
|
391
|
+
"setup door: a below-floor pi is a named FAIL that writes zero settings bytes (not a SKIP, not a " +
|
|
392
|
+
"silent write), while an in-range pi still registers — one registration reached through two doors " +
|
|
393
|
+
"may not report two different verdicts, and the range both read is DERIVED from one package.json pin " +
|
|
394
|
+
`(pin=${PI_PIN} stale=${belowFloor}/exit=${stale.status} named=${(stale.stderr || "").includes("outside the supported range")} ` +
|
|
395
|
+
`untouched=${before === afterRefusal} current=exit=${current.status}/packages=${JSON.stringify(afterAccept.packages)})`,
|
|
396
|
+
stale.status !== 0 &&
|
|
397
|
+
(stale.stderr || "").includes("[install-user-scope] pi: FAIL") &&
|
|
398
|
+
(stale.stderr || "").includes("outside the supported range") &&
|
|
399
|
+
before === afterRefusal &&
|
|
400
|
+
current.status === 0 &&
|
|
401
|
+
Array.isArray(afterAccept.packages) &&
|
|
402
|
+
afterAccept.packages.length === 1,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
333
406
|
// ── 2. the plugin command is DERIVED from the runtime root, never supplied ─────
|
|
334
407
|
{
|
|
335
408
|
const env = world("derived");
|
|
@@ -270,7 +270,7 @@ const START_ERR = JSON.stringify({
|
|
|
270
270
|
});
|
|
271
271
|
|
|
272
272
|
const CALLER = "20260914T174741-2e9ba9";
|
|
273
|
-
const NONCE = "
|
|
273
|
+
const NONCE = "mux-fresh-call-0123456789abcdef01234567";
|
|
274
274
|
const HERDR_ENV = { HERDR_ENV: "1", HERDR_BIN_PATH: "/home/operator/.local/bin/herdr", HERDR_PANE_ID: "w7:p1" };
|
|
275
275
|
|
|
276
276
|
type ScriptedReply =
|
|
@@ -344,9 +344,19 @@ async function main(): Promise<void> {
|
|
|
344
344
|
const code = MODULE_SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
345
345
|
const specifiers = [...MODULE_SRC.matchAll(/^import\s[^;]*?from\s+"([^"]+)";$/gm)].map((m) => m[1]);
|
|
346
346
|
ok(
|
|
347
|
-
"[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins
|
|
347
|
+
"[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins, the neutral composition leaf, the callback-env pair, and the dependency-free rails DECLARATION leaf — one mux or entwurf import here would re-couple the two rails and make either one undeletable",
|
|
348
348
|
specifiers.length > 0 &&
|
|
349
|
-
specifiers.every(
|
|
349
|
+
specifiers.every(
|
|
350
|
+
(s) =>
|
|
351
|
+
s.startsWith("node:") ||
|
|
352
|
+
s === "./fresh-call-composition.ts" ||
|
|
353
|
+
s === "./callback-env.ts" ||
|
|
354
|
+
// #116 A7 — the ONE place the backend tuple and the per-backend model syntax
|
|
355
|
+
// example are written, shared with the status pane and the install narration.
|
|
356
|
+
// It imports nothing itself, so admitting it couples this rail to a constant
|
|
357
|
+
// rather than to another rail; the mux/entwurf ban below is untouched.
|
|
358
|
+
s === "../../scripts/herdr-rails.mjs",
|
|
359
|
+
) &&
|
|
350
360
|
!/from\s+"\.\/(mux-|entwurf-)/.test(code),
|
|
351
361
|
);
|
|
352
362
|
// tmux is NAMED in this module, and only in one place: the refusal of a tmux seat input.
|
|
@@ -453,15 +463,29 @@ async function main(): Promise<void> {
|
|
|
453
463
|
// ── argv grammar ─────────────────────────────────────────────────────────────────────
|
|
454
464
|
// Pure builders are checked before full launch paths so a framing mutant is attributed to
|
|
455
465
|
// its own contract rather than making an earlier integration success cell fail first.
|
|
456
|
-
const
|
|
466
|
+
const tabCallback = {
|
|
467
|
+
target: "20260101T010101-aaaaaa",
|
|
468
|
+
nonce: "mux-fresh-call-deadbeefdeadbeefdeadbeef",
|
|
469
|
+
};
|
|
470
|
+
const tabArgs = buildHerdrTabCreateArgs({ workspaceId: "w7", cwd: "/repo/dir", callback: tabCallback });
|
|
471
|
+
// The SPECIFIC claim runs before the umbrella one. `ok` throws, so the first red assertion is
|
|
472
|
+
// the only one a mutant run ever prints: with the exact-argv cell first, the callback-pair
|
|
473
|
+
// mutant died on the whole-argv claim below and never reached its own — a WRONG-REASON that
|
|
474
|
+
// qualification is exactly there to catch. Order is the fix; both cells keep their subject,
|
|
475
|
+
// and the argv cell keeps its own mutant, which drops `--no-focus` and this cell cannot see.
|
|
476
|
+
ok(
|
|
477
|
+
"[QK:HFC-CALLBACK-ENV] the launcher-computed callback pair rides as two more --env assignments beside the scrub",
|
|
478
|
+
tabArgs.includes("ENTWURF_CALLBACK_TARGET=20260101T010101-aaaaaa") &&
|
|
479
|
+
tabArgs.includes("ENTWURF_CALLBACK_NONCE=mux-fresh-call-deadbeefdeadbeefdeadbeef"),
|
|
480
|
+
);
|
|
457
481
|
ok(
|
|
458
482
|
"[QK:HFC-TAB-ARGV] one placement policy — a NEW TAB in the caller's own workspace, named explicitly so herdr cannot default it to the focused one, focus left alone, cwd carried literally, and BOTH identity carriers scrubbed by explicit repeated --env",
|
|
459
483
|
tabArgs.join(" ") ===
|
|
460
|
-
"tab create --workspace w7 --no-focus --cwd /repo/dir --env PI_SESSION_ID= --env PI_AGENT_ID=",
|
|
484
|
+
"tab create --workspace w7 --no-focus --cwd /repo/dir --env PI_SESSION_ID= --env PI_AGENT_ID= --env ENTWURF_CALLBACK_TARGET=20260101T010101-aaaaaa --env ENTWURF_CALLBACK_NONCE=mux-fresh-call-deadbeefdeadbeefdeadbeef",
|
|
461
485
|
);
|
|
462
486
|
ok(
|
|
463
487
|
"a cwd-free tab create omits --cwd entirely rather than sending an empty value, and the rail adds no layout axis of its own — no label, no ratio, no direction",
|
|
464
|
-
!buildHerdrTabCreateArgs({ workspaceId: "w7" }).includes("--cwd") &&
|
|
488
|
+
!buildHerdrTabCreateArgs({ workspaceId: "w7", callback: tabCallback }).includes("--cwd") &&
|
|
465
489
|
!tabArgs.includes("--label") &&
|
|
466
490
|
!tabArgs.includes("--ratio") &&
|
|
467
491
|
!tabArgs.includes("--direction"),
|
|
@@ -165,6 +165,57 @@ async function main(): Promise<void> {
|
|
|
165
165
|
piNativeSessionIdFromPath("/s/2026-09-14T05-17-03-979Z_not-a-uuid.jsonl") === null &&
|
|
166
166
|
piNativeSessionIdFromPath(`/s/${PI_NATIVE}.jsonl`) === null,
|
|
167
167
|
);
|
|
168
|
+
{
|
|
169
|
+
// THE VENDOR HALF OF THAT CLAIM, FROM THE INSTALLED VENDOR — not a literal we typed.
|
|
170
|
+
// The key this parser recovers lives inside a filename pi builds, so the rule is only as
|
|
171
|
+
// true as pi's naming, and a source read cannot settle whether that naming is a contract
|
|
172
|
+
// or a convention. `[측정 2026-09-22, @earendil-works/pi-coding-agent 0.87.0]` the installed
|
|
173
|
+
// `SessionManager` writes a REAL session file here, in a temp dir, with zero model, zero
|
|
174
|
+
// network and zero child process: `create()` then one `appendMessage` flushes
|
|
175
|
+
// `<stamp>_<id>.jsonl` whose own `{"type":"session"}` header carries the same id.
|
|
176
|
+
//
|
|
177
|
+
// The 0.86.0 receipt this replaces needed a provider call and was therefore taken by hand
|
|
178
|
+
// once; it stays in the bump ledger as a 0.86.0 fact. This one re-takes itself on every
|
|
179
|
+
// run, so the vendor floor cannot drift under us silently again.
|
|
180
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hp-vendor-"));
|
|
181
|
+
try {
|
|
182
|
+
const { SessionManager } = await import("@earendil-works/pi-coding-agent");
|
|
183
|
+
const manager = await SessionManager.create(process.cwd(), dir);
|
|
184
|
+
await manager.appendMessage({
|
|
185
|
+
role: "assistant",
|
|
186
|
+
content: [{ type: "text", text: "placement vendor-floor probe" }],
|
|
187
|
+
api: "test",
|
|
188
|
+
provider: "test",
|
|
189
|
+
model: "test",
|
|
190
|
+
// Zero everything: this message exists to make the manager flush a file, and a
|
|
191
|
+
// fixture that invented usage numbers would be inventing vendor accounting too.
|
|
192
|
+
usage: {
|
|
193
|
+
input: 0,
|
|
194
|
+
output: 0,
|
|
195
|
+
cacheRead: 0,
|
|
196
|
+
cacheWrite: 0,
|
|
197
|
+
totalTokens: 0,
|
|
198
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
199
|
+
},
|
|
200
|
+
stopReason: "stop",
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
const written = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
204
|
+
const header = JSON.parse(readFileSync(path.join(dir, written[0]), "utf8").split("\n")[0]);
|
|
205
|
+
const recovered = piNativeSessionIdFromPath(path.join(dir, written[0]));
|
|
206
|
+
ok(
|
|
207
|
+
"the vendor floor is re-measured, not remembered: the installed pi writes a real session file whose " +
|
|
208
|
+
"basename this parser reads back to the same id its own header declares",
|
|
209
|
+
written.length === 1 &&
|
|
210
|
+
header.type === "session" &&
|
|
211
|
+
typeof header.id === "string" &&
|
|
212
|
+
recovered === header.id &&
|
|
213
|
+
recovered === manager.getSessionId(),
|
|
214
|
+
);
|
|
215
|
+
} finally {
|
|
216
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
168
219
|
ok(
|
|
169
220
|
"[QK:HP-JOIN-NEVER-GUESSES] an unmeasured agent_session kind and an empty value both yield NO key — an " +
|
|
170
221
|
"unknown herdr shape is declined, never coerced into one of the two rules we actually measured",
|
|
@@ -33,6 +33,7 @@ import fs from "node:fs";
|
|
|
33
33
|
import os from "node:os";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { pathToFileURL } from "node:url";
|
|
36
|
+
import * as HERDR_RAILS from "./herdr-rails.mjs";
|
|
36
37
|
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
37
38
|
import { treeDigest } from "./lib/tree-digest.ts";
|
|
38
39
|
|
|
@@ -769,17 +770,23 @@ function drive(
|
|
|
769
770
|
// contains `claude-sonnet-5`, so a containment test cannot tell the two grammars apart —
|
|
770
771
|
// which is the whole claim.
|
|
771
772
|
const idIn = (lines: string[], backend: string): string | null => {
|
|
772
|
-
const line = lines.find((l) => l.startsWith(`note: ${backend}: `) && l.includes("model
|
|
773
|
+
const line = lines.find((l) => l.startsWith(`note: ${backend}: `) && l.includes("model string looks like"));
|
|
773
774
|
return line === undefined ? null : (/`([^`]+)`/.exec(line)?.[1] ?? null);
|
|
774
775
|
};
|
|
775
776
|
const piId = idIn(notesOf(full), "pi");
|
|
776
777
|
const claudeId = idIn(notesOf(full), "claude-code");
|
|
777
778
|
return (
|
|
778
|
-
|
|
779
|
+
// Compared against the SHARED leaf, not a literal copied into this gate: the install
|
|
780
|
+
// narration and the status pane's RAILS block now read one declaration
|
|
781
|
+
// (`scripts/herdr-rails.mjs`, #116 A7), and a gate holding its own third copy would
|
|
782
|
+
// be the very drift that leaf exists to stop. The GRAMMAR assertions stay literal —
|
|
783
|
+
// provider-qualified vs bare vendor id is the fact, and it must not become whatever
|
|
784
|
+
// the leaf happens to say.
|
|
785
|
+
piId === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE.pi &&
|
|
779
786
|
piId.includes("/") &&
|
|
780
|
-
claudeId === "claude-
|
|
787
|
+
claudeId === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE["claude-code"] &&
|
|
781
788
|
!claudeId.includes("/") &&
|
|
782
|
-
idIn(notesOf(piOnly), "pi") ===
|
|
789
|
+
idIn(notesOf(piOnly), "pi") === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE.pi &&
|
|
783
790
|
idIn(notesOf(piOnly), "claude-code") === null
|
|
784
791
|
);
|
|
785
792
|
})(),
|