@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
|
@@ -160,7 +160,7 @@ interface Mod {
|
|
|
160
160
|
claudeUserConfig: Root;
|
|
161
161
|
};
|
|
162
162
|
readCertifiedLedger: (layout: Layout) => Ledger | null;
|
|
163
|
-
writeLedger: (layout: Layout, entry: Record<string, unknown>) => void;
|
|
163
|
+
writeLedger: (layout: Layout, version: number, entry: Record<string, unknown>) => void;
|
|
164
164
|
certifyRootsAgainstLedger: (ledger: Ledger | null, roots: unknown, runtimeRoot: string) => void;
|
|
165
165
|
planActivation: (args: { ledger: Ledger | null; requested: string[] }) => {
|
|
166
166
|
reconcile: string[];
|
|
@@ -590,7 +590,7 @@ function ledgerWith(
|
|
|
590
590
|
{
|
|
591
591
|
const env = world("ledger");
|
|
592
592
|
const layout = resolveActivationLayout(env);
|
|
593
|
-
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
593
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi"]));
|
|
594
594
|
const good = readCertifiedLedger(layout) as Ledger;
|
|
595
595
|
const write = (body: unknown): string | null => {
|
|
596
596
|
fs.writeFileSync(layout.ledgerPath, `${JSON.stringify(body)}\n`);
|
|
@@ -628,7 +628,7 @@ function ledgerWith(
|
|
|
628
628
|
{
|
|
629
629
|
const env = world("state-machine");
|
|
630
630
|
const layout = resolveActivationLayout(env);
|
|
631
|
-
writeLedger(layout, ledgerWith(env, "deactivating", { pi: "active", "claude-code": "removed" }));
|
|
631
|
+
writeLedger(layout, 2, ledgerWith(env, "deactivating", { pi: "active", "claude-code": "removed" }));
|
|
632
632
|
const mid = readCertifiedLedger(layout) as Ledger;
|
|
633
633
|
const write = (body: unknown): string | null => {
|
|
634
634
|
fs.writeFileSync(layout.ledgerPath, `${JSON.stringify(body)}\n`);
|
|
@@ -678,7 +678,7 @@ function ledgerWith(
|
|
|
678
678
|
{
|
|
679
679
|
const env = world("add-only");
|
|
680
680
|
const layout = resolveActivationLayout(env);
|
|
681
|
-
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
681
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi"]));
|
|
682
682
|
const ledger = readCertifiedLedger(layout);
|
|
683
683
|
const grow = planActivation({ ledger, requested: ["pi", "claude-code"] });
|
|
684
684
|
const shrink = planActivation({ ledger: readCertifiedLedger(layout), requested: [] });
|
|
@@ -702,7 +702,7 @@ function ledgerWith(
|
|
|
702
702
|
{
|
|
703
703
|
const env = world("roots");
|
|
704
704
|
const layout = resolveActivationLayout(env);
|
|
705
|
-
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
705
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi"]));
|
|
706
706
|
const ledger = readCertifiedLedger(layout);
|
|
707
707
|
const moved = { ...env, PI_CODING_AGENT_DIR: path.join(env.HOME as string, "somewhere-else") };
|
|
708
708
|
const drifted = refusal(() => certifyRootsAgainstLedger(ledger, resolveComponentRoots(moved), runtimeRootOf(env)));
|
|
@@ -720,7 +720,7 @@ function ledgerWith(
|
|
|
720
720
|
{
|
|
721
721
|
const env = world("order");
|
|
722
722
|
const layout = resolveActivationLayout(env);
|
|
723
|
-
writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
|
|
723
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi", "claude-code"]));
|
|
724
724
|
const steps = planDeactivation(readCertifiedLedger(layout));
|
|
725
725
|
const absent = refusal(() => planDeactivation(null));
|
|
726
726
|
ok(
|
|
@@ -894,7 +894,7 @@ function ledgerWith(
|
|
|
894
894
|
const agentSettings = path.join(env.PI_CODING_AGENT_DIR as string, "settings.json");
|
|
895
895
|
fs.writeFileSync(agentSettings, '{"theme":"dark"}\n');
|
|
896
896
|
sh(env, ["install-user-scope", "--plugin-runtime", runtimeRootOf(env)]);
|
|
897
|
-
writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
|
|
897
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi", "claude-code"]));
|
|
898
898
|
// claude-code is in the ledger but was never installed, so its preflight must refuse.
|
|
899
899
|
const settingsBefore = fs.readFileSync(agentSettings, "utf8");
|
|
900
900
|
const stateBefore = fs
|
|
@@ -960,7 +960,7 @@ function ledgerWith(
|
|
|
960
960
|
// Pi's has not. A retry must NOT re-run Claude — its state file is gone, so its preflight would
|
|
961
961
|
// refuse forever — and must pick up at Pi.
|
|
962
962
|
spawnSync("bash", [path.join(REPO, "run.sh"), "uninstall-meta-bridge"], { encoding: "utf8", env: runEnv });
|
|
963
|
-
writeLedger(layout, ledgerWith(env, "deactivating", { pi: "active", "claude-code": "removed" }));
|
|
963
|
+
writeLedger(layout, 2, ledgerWith(env, "deactivating", { pi: "active", "claude-code": "removed" }));
|
|
964
964
|
const resumed = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: runEnv });
|
|
965
965
|
const afterResume = JSON.parse(fs.readFileSync(agentSettings, "utf8")) as Record<string, unknown>;
|
|
966
966
|
|
|
@@ -968,7 +968,7 @@ function ledgerWith(
|
|
|
968
968
|
const env2 = world("retry-ledger");
|
|
969
969
|
const layout2 = resolveActivationLayout(env2);
|
|
970
970
|
fs.mkdirSync(path.join(env2.XDG_DATA_HOME as string, "entwurf", "herdr-plugin"), { recursive: true });
|
|
971
|
-
writeLedger(layout2, ledgerWith(env2, "deactivating", { pi: "removed", "claude-code": "removed" }));
|
|
971
|
+
writeLedger(layout2, 2, ledgerWith(env2, "deactivating", { pi: "removed", "claude-code": "removed" }));
|
|
972
972
|
const finished = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: env2 });
|
|
973
973
|
|
|
974
974
|
ok(
|
|
@@ -1009,7 +1009,7 @@ function ledgerWith(
|
|
|
1009
1009
|
previousRuntime: null,
|
|
1010
1010
|
})}\n`,
|
|
1011
1011
|
);
|
|
1012
|
-
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
1012
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi"]));
|
|
1013
1013
|
|
|
1014
1014
|
const run = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env });
|
|
1015
1015
|
const after = readJson(agentSettings) as Record<string, unknown>;
|
|
@@ -1042,7 +1042,12 @@ function ledgerWith(
|
|
|
1042
1042
|
|
|
1043
1043
|
// ── 17. nothing is imported after the runtime is deleted ───────────────────────
|
|
1044
1044
|
{
|
|
1045
|
-
const sources = [
|
|
1045
|
+
const sources = [
|
|
1046
|
+
DEACTIVATE,
|
|
1047
|
+
ACTIVATION,
|
|
1048
|
+
path.join(REPO, "scripts", "herdr-runtime.mjs"),
|
|
1049
|
+
path.join(REPO, "scripts", "herdr-operator-link.mjs"),
|
|
1050
|
+
];
|
|
1046
1051
|
// Comments are stripped first: these files DOCUMENT the hazard, and a scan that cannot tell a
|
|
1047
1052
|
// warning about `import()` from a call to it would be satisfied by deleting the explanation.
|
|
1048
1053
|
const strip = (body: string): string => body.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
@@ -1075,13 +1080,13 @@ function ledgerWith(
|
|
|
1075
1080
|
fs.writeFileSync(path.join(env.PI_CODING_AGENT_DIR as string, "settings.json"), '{"theme":"dark"}\n');
|
|
1076
1081
|
|
|
1077
1082
|
// forward over a teardown-in-progress: refused, and not one byte written.
|
|
1078
|
-
writeLedger(layout, ledgerWith(env, "deactivating", { pi: "active" }));
|
|
1083
|
+
writeLedger(layout, 2, ledgerWith(env, "deactivating", { pi: "active" }));
|
|
1079
1084
|
const midTeardown = fs.readFileSync(layout.ledgerPath, "utf8");
|
|
1080
1085
|
const piBefore = fs.readFileSync(path.join(env.PI_CODING_AGENT_DIR as string, "settings.json"), "utf8");
|
|
1081
1086
|
const overTeardown = spawnSync("node", [ACTIVATE, "pi"], { encoding: "utf8", env: runEnv });
|
|
1082
1087
|
|
|
1083
1088
|
// inverse over an activation-in-progress: refused, told to repair the activation first.
|
|
1084
|
-
writeLedger(layout, ledgerWith(env, "activating", { pi: "active", "claude-code": "pending" }));
|
|
1089
|
+
writeLedger(layout, 2, ledgerWith(env, "activating", { pi: "active", "claude-code": "pending" }));
|
|
1085
1090
|
const overActivating = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: runEnv });
|
|
1086
1091
|
|
|
1087
1092
|
// forward retry of the SAME set over `activating`: allowed, that is what the phase is for.
|
|
@@ -1111,7 +1116,7 @@ function ledgerWith(
|
|
|
1111
1116
|
const env = world("claude-roots");
|
|
1112
1117
|
const roots = resolveComponentRoots(env);
|
|
1113
1118
|
const layout = resolveActivationLayout(env);
|
|
1114
|
-
writeLedger(layout, ledgerFor(env, ["claude-code"]));
|
|
1119
|
+
writeLedger(layout, 2, ledgerFor(env, ["claude-code"]));
|
|
1115
1120
|
const ledger = readCertifiedLedger(layout) as Ledger;
|
|
1116
1121
|
// Same XDG, same CLAUDE_CONFIG_DIR, HOME moved: the settings half still resolves under the
|
|
1117
1122
|
// override, but `$HOME/.claude.json` — where the user-scope MCP entry lives — has moved with it.
|
|
@@ -1218,7 +1223,7 @@ function ledgerWith(
|
|
|
1218
1223
|
{
|
|
1219
1224
|
const env = world("ledger-identity");
|
|
1220
1225
|
const layout = resolveActivationLayout(env);
|
|
1221
|
-
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
1226
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi"]));
|
|
1222
1227
|
const good = readCertifiedLedger(layout) as Ledger;
|
|
1223
1228
|
const write = (body: unknown): string | null => {
|
|
1224
1229
|
fs.writeFileSync(layout.ledgerPath, `${JSON.stringify(body)}\n`);
|
|
@@ -1253,7 +1258,7 @@ function ledgerWith(
|
|
|
1253
1258
|
const env = world("rebind-legality");
|
|
1254
1259
|
const layout = resolveActivationLayout(env);
|
|
1255
1260
|
const ledgerA = () => {
|
|
1256
|
-
writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
|
|
1261
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi", "claude-code"]));
|
|
1257
1262
|
return readCertifiedLedger(layout) as Ledger;
|
|
1258
1263
|
};
|
|
1259
1264
|
const sourceDrift = refusal(() => certifyArtifactForForward(ledgerA(), IDENTITY_NPM, ["pi", "claude-code"]));
|
|
@@ -1261,11 +1266,11 @@ function ledgerWith(
|
|
|
1261
1266
|
const legal = certifyArtifactForForward(ledgerA(), IDENTITY_B, ["pi", "claude-code", "pi"]);
|
|
1262
1267
|
const fresh = certifyArtifactForForward(null, IDENTITY_B, ["pi"]);
|
|
1263
1268
|
const dropped = refusal(() => certifyArtifactForForward(ledgerA(), IDENTITY_B, ["pi"]));
|
|
1264
|
-
writeLedger(layout, ledgerWith(env, "activating", { pi: "pending" }));
|
|
1269
|
+
writeLedger(layout, 2, ledgerWith(env, "activating", { pi: "pending" }));
|
|
1265
1270
|
const midActivation = refusal(() =>
|
|
1266
1271
|
certifyArtifactForForward(readCertifiedLedger(layout) as Ledger, IDENTITY_B, ["pi"]),
|
|
1267
1272
|
);
|
|
1268
|
-
writeLedger(layout, ledgerWith(env, "deactivating", { pi: "removed" }));
|
|
1273
|
+
writeLedger(layout, 2, ledgerWith(env, "deactivating", { pi: "removed" }));
|
|
1269
1274
|
const midTeardown = refusal(() =>
|
|
1270
1275
|
certifyArtifactForForward(readCertifiedLedger(layout) as Ledger, IDENTITY_B, ["pi"]),
|
|
1271
1276
|
);
|
|
@@ -1505,7 +1510,7 @@ function ledgerWith(
|
|
|
1505
1510
|
};
|
|
1506
1511
|
// D2 — no ledger: a doctor running from the runtime package is simply not in plugin mode.
|
|
1507
1512
|
const absent = mode({ repo: runtimeRepo });
|
|
1508
|
-
writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
|
|
1513
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi", "claude-code"]));
|
|
1509
1514
|
// D1 — runtime package + an active ledger that activated claude-code at THIS stable root.
|
|
1510
1515
|
const derived = mode({ repo: runtimeRepo });
|
|
1511
1516
|
ok(
|
|
@@ -1536,11 +1541,11 @@ function ledgerWith(
|
|
|
1536
1541
|
// D4 — the admission clauses on a reading path, each on a CERTIFIED ledger so only the clause can
|
|
1537
1542
|
// refuse: a phase other than active, a ledger that never activated claude-code, and a runtimeRoot
|
|
1538
1543
|
// that is not this host's stable root. Each is refused by name, never read as 'not plugin mode'.
|
|
1539
|
-
writeLedger(layout, ledgerWith(env, "activating", { pi: "active", "claude-code": "pending" }));
|
|
1544
|
+
writeLedger(layout, 2, ledgerWith(env, "activating", { pi: "active", "claude-code": "pending" }));
|
|
1540
1545
|
const notActive = mode({ repo: runtimeRepo });
|
|
1541
|
-
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
1546
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi"]));
|
|
1542
1547
|
const piOnly = mode({ repo: runtimeRepo });
|
|
1543
|
-
writeLedger(layout, {
|
|
1548
|
+
writeLedger(layout, 2, {
|
|
1544
1549
|
...ledgerFor(env, ["pi", "claude-code"]),
|
|
1545
1550
|
runtimeRoot: path.join(env.HOME as string, "another-runtime", "active"),
|
|
1546
1551
|
});
|
|
@@ -1554,7 +1559,7 @@ function ledgerWith(
|
|
|
1554
1559
|
foreignRoot === "refused:doctor-plugin-runtime-unadmitted",
|
|
1555
1560
|
);
|
|
1556
1561
|
// D6 — the override must agree with the ledger's root; agreeing, it is admitted from a checkout.
|
|
1557
|
-
writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
|
|
1562
|
+
writeLedger(layout, 2, ledgerFor(env, ["pi", "claude-code"]));
|
|
1558
1563
|
const elsewhere = mode({ repo: REPO, override: path.join(env.HOME as string, "elsewhere") });
|
|
1559
1564
|
const agreeing = mode({ repo: REPO, override: active });
|
|
1560
1565
|
ok(
|
|
@@ -1564,4 +1569,277 @@ function ledgerWith(
|
|
|
1564
1569
|
);
|
|
1565
1570
|
}
|
|
1566
1571
|
|
|
1572
|
+
// ── 30. the ledger floor: 2 and 3 are read, and a write keeps the version it found (#153) ─
|
|
1573
|
+
{
|
|
1574
|
+
const versionOf = (layout: Layout): string => {
|
|
1575
|
+
try {
|
|
1576
|
+
const ledger = readCertifiedLedger(layout) as Ledger | null;
|
|
1577
|
+
return ledger === null ? "absent" : String(ledger.schemaVersion);
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
return `refused:${(err as { code?: string }).code ?? String(err)}`;
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const env = world("ledger-version");
|
|
1583
|
+
const layout = resolveActivationLayout(env);
|
|
1584
|
+
const body = ledgerFor(env, ["pi"]);
|
|
1585
|
+
const raw = (version: unknown): string => {
|
|
1586
|
+
fs.mkdirSync(layout.stateRoot, { recursive: true });
|
|
1587
|
+
fs.writeFileSync(layout.ledgerPath, `${JSON.stringify({ schemaVersion: version, ...body }, null, 2)}\n`);
|
|
1588
|
+
return versionOf(layout);
|
|
1589
|
+
};
|
|
1590
|
+
const cells: Record<string, string> = {
|
|
1591
|
+
v2: raw(2),
|
|
1592
|
+
v3: raw(3),
|
|
1593
|
+
v1: raw(1),
|
|
1594
|
+
v4: raw(4),
|
|
1595
|
+
"string-3": raw("3"),
|
|
1596
|
+
null: raw(null),
|
|
1597
|
+
};
|
|
1598
|
+
ok(
|
|
1599
|
+
"[QK:HAC-LEDGER-VERSION-ADMITTED] the reader admits schema 2 AND schema 3 under the same exact key set and " +
|
|
1600
|
+
"hands back the version it read, and nothing else: 3 is the floor an older reader refuses before its first " +
|
|
1601
|
+
"write, so a reader that admits only 2 is exactly the artifact the floor exists to stop " +
|
|
1602
|
+
`(${Object.entries(cells)
|
|
1603
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
1604
|
+
.join(" ")})`,
|
|
1605
|
+
cells.v2 === "2" &&
|
|
1606
|
+
cells.v3 === "3" &&
|
|
1607
|
+
cells.v1 === "refused:activation-ledger-uncertified" &&
|
|
1608
|
+
cells.v4 === "refused:activation-ledger-uncertified" &&
|
|
1609
|
+
cells["string-3"] === "refused:activation-ledger-uncertified" &&
|
|
1610
|
+
cells.null === "refused:activation-ledger-uncertified",
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
const fresh = world("ledger-writer");
|
|
1614
|
+
const freshLayout = resolveActivationLayout(fresh);
|
|
1615
|
+
const freshBody = ledgerFor(fresh, ["pi"]);
|
|
1616
|
+
const invalid = {
|
|
1617
|
+
v4: refusal(() => writeLedger(freshLayout, 4, freshBody)),
|
|
1618
|
+
v1: refusal(() => writeLedger(freshLayout, 1, freshBody)),
|
|
1619
|
+
undefined: refusal(() => writeLedger(freshLayout, undefined as unknown as number, freshBody)),
|
|
1620
|
+
};
|
|
1621
|
+
const absentAfterInvalid = !fs.existsSync(freshLayout.ledgerPath);
|
|
1622
|
+
ok(
|
|
1623
|
+
"[QK:HAC-LEDGER-WRITER-VERSION-CHECKED] the writer takes the version as an explicit argument and refuses one " +
|
|
1624
|
+
"outside the admitted set before the first byte — a ledger the reader would refuse must never be written " +
|
|
1625
|
+
`(${Object.entries(invalid)
|
|
1626
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
1627
|
+
.join(" ")} absent=${absentAfterInvalid})`,
|
|
1628
|
+
Object.values(invalid).every((v) => v === "activation-ledger-version-invalid") && absentAfterInvalid,
|
|
1629
|
+
);
|
|
1630
|
+
|
|
1631
|
+
writeLedger(freshLayout, 3, freshBody);
|
|
1632
|
+
const bytes3 = fs.readFileSync(freshLayout.ledgerPath, "utf8");
|
|
1633
|
+
const embedded = {
|
|
1634
|
+
lower: refusal(() => writeLedger(freshLayout, 3, { ...freshBody, schemaVersion: 2 })),
|
|
1635
|
+
raise: refusal(() => writeLedger(freshLayout, 2, { ...freshBody, schemaVersion: 3 })),
|
|
1636
|
+
};
|
|
1637
|
+
const unchanged = fs.readFileSync(freshLayout.ledgerPath, "utf8") === bytes3;
|
|
1638
|
+
ok(
|
|
1639
|
+
"[QK:HAC-LEDGER-WRITER-NO-EMBEDDED-VERSION] an entry carrying its own `schemaVersion` is refused rather than " +
|
|
1640
|
+
"merged, so no spread order lets a body silently lower or raise the floor the caller named, and the ledger " +
|
|
1641
|
+
`already there is byte-identical (lower=${embedded.lower} raise=${embedded.raise} unchanged=${unchanged})`,
|
|
1642
|
+
embedded.lower === "activation-ledger-version-invalid" &&
|
|
1643
|
+
embedded.raise === "activation-ledger-version-invalid" &&
|
|
1644
|
+
unchanged &&
|
|
1645
|
+
versionOf(freshLayout) === "3",
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
// activate: a host with no ledger is born at 2; a host already at 3 stays at 3 through a forward run.
|
|
1649
|
+
const act = world("ledger-version-activate");
|
|
1650
|
+
const actLayout = resolveActivationLayout(act);
|
|
1651
|
+
const actEnv = {
|
|
1652
|
+
...act,
|
|
1653
|
+
PATH: `${fakeClaude(act)}${path.delimiter}${act.PATH}`,
|
|
1654
|
+
FAKE_CLAUDE_LOG: path.join(act.HOME as string, "claude.log"),
|
|
1655
|
+
};
|
|
1656
|
+
fs.writeFileSync(path.join(act.PI_CODING_AGENT_DIR as string, "settings.json"), '{"theme":"dark"}\n');
|
|
1657
|
+
seedRuntime(act);
|
|
1658
|
+
const born = spawnSync("node", [ACTIVATE, "pi"], { encoding: "utf8", env: actEnv });
|
|
1659
|
+
const bornVersion = versionOf(actLayout);
|
|
1660
|
+
writeLedger(actLayout, 3, ledgerFor(act, ["pi"]));
|
|
1661
|
+
const kept = spawnSync("node", [ACTIVATE, "pi", "claude-code"], { encoding: "utf8", env: actEnv });
|
|
1662
|
+
const keptVersion = versionOf(actLayout);
|
|
1663
|
+
const keptLedger = keptVersion === "3" ? (readCertifiedLedger(actLayout) as Ledger) : null;
|
|
1664
|
+
ok(
|
|
1665
|
+
"[QK:HAC-LEDGER-VERSION-KEPT-BY-ACTIVATE] activation neither raises nor lowers the floor: a fresh host's " +
|
|
1666
|
+
"ledger is born at 2, and a forward run over a 3-ledger checkpoints and settles at 3 — a blanket rewrite to " +
|
|
1667
|
+
"the fresh version would hand the host back to every older reader " +
|
|
1668
|
+
`(born=${born.status}/${bornVersion} kept=${kept.status}/${keptVersion}/${keptLedger?.phase}/${JSON.stringify(keptLedger?.activatedBackends)})`,
|
|
1669
|
+
born.status === 0 &&
|
|
1670
|
+
bornVersion === "2" &&
|
|
1671
|
+
kept.status === 0 &&
|
|
1672
|
+
keptVersion === "3" &&
|
|
1673
|
+
keptLedger?.phase === "active" &&
|
|
1674
|
+
keptLedger.activatedBackends.join(",") === "pi,claude-code",
|
|
1675
|
+
);
|
|
1676
|
+
|
|
1677
|
+
// deactivate: its checkpoint is written before the first step, so a step refused after it leaves that
|
|
1678
|
+
// checkpoint on disk. Residue under the runtime root with no journal behind it is refused by the
|
|
1679
|
+
// runtime inverse, which runs right after the checkpoint here because both components are already
|
|
1680
|
+
// recorded removed. The seeded ledger is compact JSON, so a rewrite is visible in its bytes.
|
|
1681
|
+
const de = world("ledger-version-deactivate");
|
|
1682
|
+
const deLayout = resolveActivationLayout(de);
|
|
1683
|
+
fs.mkdirSync(deLayout.stateRoot, { recursive: true });
|
|
1684
|
+
const compact = `${JSON.stringify({ schemaVersion: 3, ...ledgerWith(de, "deactivating", { pi: "removed", "claude-code": "removed" }) })}\n`;
|
|
1685
|
+
fs.writeFileSync(deLayout.ledgerPath, compact);
|
|
1686
|
+
fs.mkdirSync(runtimeRootOf(de), { recursive: true });
|
|
1687
|
+
fs.writeFileSync(path.join(runtimeRootOf(de), "residue"), "not ours\n");
|
|
1688
|
+
const stopped = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: de });
|
|
1689
|
+
const rewritten = fs.existsSync(deLayout.ledgerPath) && fs.readFileSync(deLayout.ledgerPath, "utf8") !== compact;
|
|
1690
|
+
const deVersion = versionOf(deLayout);
|
|
1691
|
+
ok(
|
|
1692
|
+
"[QK:HAC-LEDGER-VERSION-KEPT-BY-DEACTIVATE] the teardown checkpoint carries the version it read: a 3-ledger " +
|
|
1693
|
+
"rewritten mid-teardown is still 3, so a teardown stopped halfway is resumed only by a reader that knows the " +
|
|
1694
|
+
`floor (exit=${stopped.status} refusal=${(stopped.stderr || "").includes("runtime-inverse-foreign-refused")} rewritten=${rewritten} version=${deVersion})`,
|
|
1695
|
+
stopped.status !== 0 &&
|
|
1696
|
+
(stopped.stderr || "").includes("runtime-inverse-foreign-refused") &&
|
|
1697
|
+
rewritten &&
|
|
1698
|
+
deVersion === "3",
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// ── 31. deactivate takes the operator link back FIRST, and judges it before the first write (#153) ─
|
|
1703
|
+
{
|
|
1704
|
+
/** A receipt-owned operator link in a physical bin dir, written raw — independent of the writer. */
|
|
1705
|
+
const linked = (env: NodeJS.ProcessEnv, link: "ours" | "foreign") => {
|
|
1706
|
+
const B = path.join(env.HOME as string, "bin-B");
|
|
1707
|
+
fs.mkdirSync(B, { recursive: true });
|
|
1708
|
+
const target = path.join(runtimeRootOf(env), "node_modules", ".bin", "entwurf");
|
|
1709
|
+
const linkPath = path.join(B, "entwurf");
|
|
1710
|
+
fs.symlinkSync(link === "ours" ? target : path.join(env.HOME as string, "someone-else"), linkPath);
|
|
1711
|
+
const layout = resolveActivationLayout(env);
|
|
1712
|
+
fs.mkdirSync(layout.stateRoot, { recursive: true });
|
|
1713
|
+
const receiptPath = path.join(layout.stateRoot, "operator-link.json");
|
|
1714
|
+
fs.writeFileSync(
|
|
1715
|
+
receiptPath,
|
|
1716
|
+
`${JSON.stringify({ schemaVersion: 1, phase: "linked", binDir: B, linkPath, target, runtimeRoot: runtimeRootOf(env) }, null, 2)}\n`,
|
|
1717
|
+
);
|
|
1718
|
+
return { linkPath, receiptPath };
|
|
1719
|
+
};
|
|
1720
|
+
const linkState = (p: string) => {
|
|
1721
|
+
const kind = fs.lstatSync(p, { throwIfNoEntry: false });
|
|
1722
|
+
return kind === undefined ? "<absent>" : kind.isSymbolicLink() ? `-> ${fs.readlinkSync(p)}` : "<other>";
|
|
1723
|
+
};
|
|
1724
|
+
/** A 3-ledger mid-teardown with both components already removed, written compact. */
|
|
1725
|
+
const floorLedger = (env: NodeJS.ProcessEnv): string => {
|
|
1726
|
+
const layout = resolveActivationLayout(env);
|
|
1727
|
+
fs.mkdirSync(layout.stateRoot, { recursive: true });
|
|
1728
|
+
const text = `${JSON.stringify({ schemaVersion: 3, ...ledgerWith(env, "deactivating", { pi: "removed", "claude-code": "removed" }) })}\n`;
|
|
1729
|
+
fs.writeFileSync(layout.ledgerPath, text);
|
|
1730
|
+
return text;
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
// (a) ORDER: the runtime step is refused (residue with no journal), so the run stops there — by
|
|
1734
|
+
// then the link and its receipt must already be gone, and the ledger kept as the retry authority.
|
|
1735
|
+
const first = world("operator-link-first");
|
|
1736
|
+
floorLedger(first);
|
|
1737
|
+
fs.mkdirSync(runtimeRootOf(first), { recursive: true });
|
|
1738
|
+
fs.writeFileSync(path.join(runtimeRootOf(first), "residue"), "not ours\n");
|
|
1739
|
+
const firstLink = linked(first, "ours");
|
|
1740
|
+
const stopped = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: first });
|
|
1741
|
+
const kept = refusal(() => readCertifiedLedger(resolveActivationLayout(first))) ?? "certified";
|
|
1742
|
+
|
|
1743
|
+
// (a') ...and BEFORE the components: a real pi + Claude activation whose preflights are all green, raised
|
|
1744
|
+
// to 3, whose FIRST component inverse (Claude's) is stopped at its own platform fence by a sandbox
|
|
1745
|
+
// `uname` shim. No preflight reads uname, so the stop lands after the checkpoint and before any
|
|
1746
|
+
// component is removed; by then the link and its receipt must be gone, the ledger deactivating at 3.
|
|
1747
|
+
const comp = world("operator-link-before-components");
|
|
1748
|
+
const compEnv = {
|
|
1749
|
+
...comp,
|
|
1750
|
+
PATH: `${fakeClaude(comp)}${path.delimiter}${comp.PATH}`,
|
|
1751
|
+
FAKE_CLAUDE_LOG: path.join(comp.HOME as string, "claude.log"),
|
|
1752
|
+
};
|
|
1753
|
+
seedRuntime(comp);
|
|
1754
|
+
fs.writeFileSync(path.join(comp.PI_CODING_AGENT_DIR as string, "settings.json"), '{"theme":"dark"}\n');
|
|
1755
|
+
const compActivated = spawnSync("node", [ACTIVATE, "pi", "claude-code"], { encoding: "utf8", env: compEnv });
|
|
1756
|
+
const compLayout = resolveActivationLayout(comp);
|
|
1757
|
+
const born = readCertifiedLedger(compLayout);
|
|
1758
|
+
if (born !== null) {
|
|
1759
|
+
writeLedger(compLayout, 3, Object.fromEntries(Object.entries(born).filter(([k]) => k !== "schemaVersion")));
|
|
1760
|
+
}
|
|
1761
|
+
const compLink = linked(comp, "ours");
|
|
1762
|
+
const unameShim = path.join(comp.HOME as string, "uname-shim");
|
|
1763
|
+
fs.mkdirSync(unameShim, { recursive: true });
|
|
1764
|
+
fs.writeFileSync(path.join(unameShim, "uname"), "#!/bin/sh\necho Plan9\n", { mode: 0o755 });
|
|
1765
|
+
const compRun = spawnSync("node", [DEACTIVATE], {
|
|
1766
|
+
encoding: "utf8",
|
|
1767
|
+
env: { ...compEnv, PATH: `${unameShim}${path.delimiter}${compEnv.PATH}` },
|
|
1768
|
+
});
|
|
1769
|
+
let compLedger: Ledger | null = null;
|
|
1770
|
+
try {
|
|
1771
|
+
compLedger = readCertifiedLedger(compLayout);
|
|
1772
|
+
} catch {
|
|
1773
|
+
// Bounded probe: an uncertifiable ledger is reported below as null.
|
|
1774
|
+
compLedger = null;
|
|
1775
|
+
}
|
|
1776
|
+
const compStates = (compLedger?.components ?? []).map((c) => `${c.backend}:${c.state}`).join(",");
|
|
1777
|
+
ok(
|
|
1778
|
+
"[QK:HAC-DEACTIVATE-LINK-FIRST] the operator link is removed BEFORE every component and the runtime — it names " +
|
|
1779
|
+
"the runtime, so a teardown that stops later must not leave it pointing at what it is about to delete " +
|
|
1780
|
+
`(exit=${stopped.status} runtime-refused=${(stopped.stderr || "").includes("runtime-inverse-foreign-refused")} link=${linkState(firstLink.linkPath)} receipt=${fs.existsSync(firstLink.receiptPath)} ledger=${kept}; ` +
|
|
1781
|
+
`first-component: activated=${compActivated.status} exit=${compRun.status} claude-refused=${(compRun.stderr || "").includes("claude-code inverse failed")} link=${linkState(compLink.linkPath)} receipt=${fs.existsSync(compLink.receiptPath)} ledger=${compLedger?.schemaVersion}/${compLedger?.phase}/${compStates})`,
|
|
1782
|
+
stopped.status !== 0 &&
|
|
1783
|
+
(stopped.stderr || "").includes("runtime-inverse-foreign-refused") &&
|
|
1784
|
+
linkState(firstLink.linkPath) === "<absent>" &&
|
|
1785
|
+
!fs.existsSync(firstLink.receiptPath) &&
|
|
1786
|
+
kept === "certified" &&
|
|
1787
|
+
compActivated.status === 0 &&
|
|
1788
|
+
compRun.status !== 0 &&
|
|
1789
|
+
(compRun.stderr || "").includes("deactivate-component-failed") &&
|
|
1790
|
+
(compRun.stderr || "").includes(
|
|
1791
|
+
"claude-code inverse failed — runtime and ledger preserved as retry authority (0 component(s) already removed and recorded)",
|
|
1792
|
+
) &&
|
|
1793
|
+
linkState(compLink.linkPath) === "<absent>" &&
|
|
1794
|
+
!fs.existsSync(compLink.receiptPath) &&
|
|
1795
|
+
compLedger?.schemaVersion === 3 &&
|
|
1796
|
+
compLedger.phase === "deactivating" &&
|
|
1797
|
+
compStates === "pi:active,claude-code:active",
|
|
1798
|
+
);
|
|
1799
|
+
|
|
1800
|
+
// (b) a link that is no longer the receipt's refuses the WHOLE teardown before the checkpoint: the
|
|
1801
|
+
// compact ledger would be rewritten by it, so byte-identity proves the refusal came first.
|
|
1802
|
+
const foreign = world("operator-link-foreign");
|
|
1803
|
+
const foreignLedger = floorLedger(foreign);
|
|
1804
|
+
const foreignLink = linked(foreign, "foreign");
|
|
1805
|
+
const receiptBefore = fs.readFileSync(foreignLink.receiptPath, "utf8");
|
|
1806
|
+
const linkBefore = linkState(foreignLink.linkPath);
|
|
1807
|
+
const refusedRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: foreign });
|
|
1808
|
+
ok(
|
|
1809
|
+
"[QK:HAC-DEACTIVATE-FOREIGN-LINK-REFUSES] a link that is not the receipt's stops the whole teardown before its " +
|
|
1810
|
+
"first write — the ledger, the receipt and that link are byte-identical — instead of being found later by " +
|
|
1811
|
+
`the removal after the ledger already moved (exit=${refusedRun.status} named=${(refusedRun.stderr || "").includes("operator-link-foreign")} ledger-unchanged=${fs.readFileSync(resolveActivationLayout(foreign).ledgerPath, "utf8") === foreignLedger})`,
|
|
1812
|
+
refusedRun.status !== 0 &&
|
|
1813
|
+
(refusedRun.stderr || "").includes("operator-link-foreign") &&
|
|
1814
|
+
fs.readFileSync(resolveActivationLayout(foreign).ledgerPath, "utf8") === foreignLedger &&
|
|
1815
|
+
fs.readFileSync(foreignLink.receiptPath, "utf8") === receiptBefore &&
|
|
1816
|
+
linkState(foreignLink.linkPath) === linkBefore,
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
// (c) no ledger: the receipt address is a named leftover; its link is named only from a certified
|
|
1820
|
+
// receipt, and a damaged receipt names nothing beyond its own address. Nothing is removed.
|
|
1821
|
+
const orphan = world("operator-link-orphan");
|
|
1822
|
+
const orphanLink = linked(orphan, "ours");
|
|
1823
|
+
const orphanRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: orphan });
|
|
1824
|
+
const damaged = world("operator-link-damaged");
|
|
1825
|
+
const damagedLink = linked(damaged, "ours");
|
|
1826
|
+
fs.writeFileSync(damagedLink.receiptPath, "{broken");
|
|
1827
|
+
const damagedRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: damaged });
|
|
1828
|
+
ok(
|
|
1829
|
+
"[QK:HAC-NO-LEDGER-OPERATOR-LEFTOVERS] with no ledger an operator-link receipt is a named leftover with a " +
|
|
1830
|
+
"non-zero exit and nothing removed; its link is named only when the receipt is certified, never searched " +
|
|
1831
|
+
`for (orphan=${orphanRun.status}/receipt-named=${(orphanRun.stderr || "").includes(orphanLink.receiptPath)}/link-named=${(orphanRun.stderr || "").includes(`operator-link (${orphanLink.linkPath})`)} damaged=${damagedRun.status}/link-named=${(damagedRun.stderr || "").includes(`operator-link (${damagedLink.linkPath})`)})`,
|
|
1832
|
+
orphanRun.status !== 0 &&
|
|
1833
|
+
(orphanRun.stderr || "").includes("deactivate-unattributed-leftovers") &&
|
|
1834
|
+
(orphanRun.stderr || "").includes(orphanLink.receiptPath) &&
|
|
1835
|
+
(orphanRun.stderr || "").includes(`operator-link (${orphanLink.linkPath})`) &&
|
|
1836
|
+
linkState(orphanLink.linkPath) !== "<absent>" &&
|
|
1837
|
+
fs.existsSync(orphanLink.receiptPath) &&
|
|
1838
|
+
damagedRun.status !== 0 &&
|
|
1839
|
+
(damagedRun.stderr || "").includes(damagedLink.receiptPath) &&
|
|
1840
|
+
!(damagedRun.stderr || "").includes(`operator-link (${damagedLink.linkPath})`) &&
|
|
1841
|
+
fs.readFileSync(damagedLink.receiptPath, "utf8") === "{broken",
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1567
1845
|
console.log(`\ncheck-herdr-activation: ${passed} assertions passed`);
|