@junghanacs/entwurf 0.34.0 → 0.34.2
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/CHANGELOG.md +207 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +4 -7
- package/VERIFY.md +1 -1
- package/docs/setup-clean-host.md +18 -0
- package/package.json +2 -2
- package/run.sh +5 -3
- package/scripts/check-gate-qualification.ts +2 -2
- package/scripts/check-herdr-activation.ts +210 -10
- package/scripts/check-herdr-plugin-build.ts +235 -6
- package/scripts/check-meta-doctor-oracle.sh +86 -1
- package/scripts/herdr-activation.mjs +66 -1
- package/scripts/herdr-doctor-mode.mjs +26 -0
- package/scripts/herdr-plugin-activate.mjs +112 -76
- package/scripts/herdr-plugin-deactivate.mjs +41 -2
- package/scripts/meta-bridge-doctor.sh +79 -20
- package/scripts/mutants/herdr-activation.json +106 -0
- package/scripts/mutants/herdr-plugin-build.json +66 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +3 -3
|
@@ -27,6 +27,7 @@ import os from "node:os";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { pathToFileURL } from "node:url";
|
|
29
29
|
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
30
|
+
import { treeDigest } from "./lib/tree-digest.ts";
|
|
30
31
|
|
|
31
32
|
let passed = 0;
|
|
32
33
|
function ok(label: string, cond: boolean): void {
|
|
@@ -182,6 +183,7 @@ interface Mod {
|
|
|
182
183
|
resolveEntwurfDataRoot: (env: NodeJS.ProcessEnv) => string;
|
|
183
184
|
LEDGER_PHASES: string[];
|
|
184
185
|
COMPONENT_STATES: string[];
|
|
186
|
+
resolveDoctorPluginRuntime: (env: NodeJS.ProcessEnv, opts: { repo: string; override?: string }) => string | null;
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
const mod = (await import(pathToFileURL(ACTIVATION).href)) as Mod;
|
|
@@ -203,6 +205,7 @@ const {
|
|
|
203
205
|
resolveEntwurfDataRoot,
|
|
204
206
|
LEDGER_PHASES,
|
|
205
207
|
COMPONENT_STATES,
|
|
208
|
+
resolveDoctorPluginRuntime,
|
|
206
209
|
} = mod;
|
|
207
210
|
|
|
208
211
|
/** The pi pin package.json declares (an exact production dependency since #130) — the SSOT
|
|
@@ -737,21 +740,12 @@ function ledgerWith(
|
|
|
737
740
|
const dataRoot = resolveEntwurfDataRoot(bare);
|
|
738
741
|
const states = piStatePaths(bare);
|
|
739
742
|
const piExpected = path.join(home, ".local", "share", "entwurf");
|
|
740
|
-
// The python writers derive the SAME stable root from the SAME environment.
|
|
741
|
-
const pyRoot = spawnSync(
|
|
742
|
-
"python3",
|
|
743
|
-
[
|
|
744
|
-
"-c",
|
|
745
|
-
"import sys; sys.path.insert(0, 'scripts'); from importlib import import_module; m = import_module('register-pi-provider'.replace('-', '_')) if False else None",
|
|
746
|
-
],
|
|
747
|
-
{ encoding: "utf8", env: bare },
|
|
748
|
-
);
|
|
749
743
|
ok(
|
|
750
744
|
"[QK:HAC-XDG-RESOLUTION-SHARED] every root comes from one resolution — `XDG_DATA_HOME`, else " +
|
|
751
745
|
"`$HOME/.local/share` — so a host with XDG unset is the same host to the teardown as it is to the writers. " +
|
|
752
746
|
'The first cut built its Pi state paths from `env.XDG_DATA_HOME || ""`, which on such a host pointed at ' +
|
|
753
747
|
"`/entwurf/...`: a preflight that looked at a file nobody writes and passed for the wrong reason " +
|
|
754
|
-
`(dataRoot=${dataRoot === piExpected} packageState=${path.relative(home, states.packageState)}
|
|
748
|
+
`(dataRoot=${dataRoot === piExpected} packageState=${path.relative(home, states.packageState)})`,
|
|
755
749
|
dataRoot === piExpected &&
|
|
756
750
|
states.packageState === path.join(piExpected, "pi-package", "install-state.json") &&
|
|
757
751
|
states.providerState === path.join(piExpected, "pi-provider", "install-state.json"),
|
|
@@ -1364,4 +1358,210 @@ function ledgerWith(
|
|
|
1364
1358
|
);
|
|
1365
1359
|
}
|
|
1366
1360
|
|
|
1361
|
+
// ── 26. both Pi owner records are asked where their writers keep them (#135 D1) ─
|
|
1362
|
+
{
|
|
1363
|
+
const env = world("pi-state-paths");
|
|
1364
|
+
const defaultData = path.join(env.HOME as string, ".local", "share");
|
|
1365
|
+
// An EMPTY XDG_DATA_HOME: run.sh and the runtime owner both fall back to $HOME/.local/share.
|
|
1366
|
+
const runEnv = { ...env, XDG_DATA_HOME: "" };
|
|
1367
|
+
seedRuntime({ ...env, XDG_DATA_HOME: defaultData });
|
|
1368
|
+
const agentSettings = path.join(env.PI_CODING_AGENT_DIR as string, "settings.json");
|
|
1369
|
+
fs.writeFileSync(agentSettings, '{"theme":"dark"}\n');
|
|
1370
|
+
const foreign = path.join(env.HOME as string, "dev", "entwurf");
|
|
1371
|
+
fs.mkdirSync(foreign, { recursive: true });
|
|
1372
|
+
const seeded = py(env, "register-pi-provider.py", [
|
|
1373
|
+
"install",
|
|
1374
|
+
agentSettings,
|
|
1375
|
+
foreign,
|
|
1376
|
+
"--scope",
|
|
1377
|
+
"user",
|
|
1378
|
+
"--state",
|
|
1379
|
+
path.join(defaultData, "entwurf", "pi-provider", "install-state.json"),
|
|
1380
|
+
]);
|
|
1381
|
+
assert.equal(seeded.status, 0, `fixture: seeding the provider owner failed: ${seeded.stderr}`);
|
|
1382
|
+
const layout = resolveActivationLayout(runEnv);
|
|
1383
|
+
const run = spawnSync("node", [ACTIVATE, "pi"], { encoding: "utf8", env: runEnv, cwd: env.HOME as string });
|
|
1384
|
+
ok(
|
|
1385
|
+
"[QK:HAC-PI-STATE-PATHS-SHARED] both Pi ownership preflights read the record where its writer keeps it: with " +
|
|
1386
|
+
"XDG_DATA_HOME set but EMPTY, run.sh writes to $HOME/.local/share, so the provider preflight must look there " +
|
|
1387
|
+
"too — the first cut built a RELATIVE path for the provider half alone, saw no owner, and let the ledger land " +
|
|
1388
|
+
`on a host the provider writer then refused (exit=${run.status} ledger=${fs.existsSync(layout.ledgerPath)} refusal=${JSON.stringify((run.stderr || "").trim().split("\n").pop())})`,
|
|
1389
|
+
run.status !== 0 &&
|
|
1390
|
+
(run.stderr || "").includes("activation-preflight-refused") &&
|
|
1391
|
+
(run.stderr || "").includes("pi-provider") &&
|
|
1392
|
+
!fs.existsSync(layout.ledgerPath),
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// ── 27. no ledger is not "nothing here" (#135) ─────────────────────────────────
|
|
1397
|
+
{
|
|
1398
|
+
const plugin = (env: NodeJS.ProcessEnv) => path.join(env.XDG_DATA_HOME as string, "entwurf", "herdr-plugin");
|
|
1399
|
+
const rows: [string, (env: NodeJS.ProcessEnv) => void][] = [
|
|
1400
|
+
[
|
|
1401
|
+
"installing-journal",
|
|
1402
|
+
(env) => {
|
|
1403
|
+
fs.mkdirSync(path.join(plugin(env), "runtime"), { recursive: true });
|
|
1404
|
+
fs.writeFileSync(
|
|
1405
|
+
path.join(plugin(env), "journal.json"),
|
|
1406
|
+
`${JSON.stringify({
|
|
1407
|
+
schemaVersion: 2,
|
|
1408
|
+
phase: "installing",
|
|
1409
|
+
runtimeRoot: runtimeRootOf(env),
|
|
1410
|
+
artifactIdentity: { kind: "npm", name: PACKAGE, version: "0.21.0", expectedIntegrity: "sha512-fixture" },
|
|
1411
|
+
previousRuntime: null,
|
|
1412
|
+
})}\n`,
|
|
1413
|
+
);
|
|
1414
|
+
},
|
|
1415
|
+
],
|
|
1416
|
+
["ready-runtime", (env) => void seedRuntime(env)],
|
|
1417
|
+
[
|
|
1418
|
+
"cache-only",
|
|
1419
|
+
(env) =>
|
|
1420
|
+
fs.mkdirSync(path.join(env.XDG_CACHE_HOME as string, "entwurf", "herdr-plugin", "npm"), { recursive: true }),
|
|
1421
|
+
],
|
|
1422
|
+
["journal-less-runtime", (env) => fs.mkdirSync(path.join(plugin(env), "runtime", "active"), { recursive: true })],
|
|
1423
|
+
];
|
|
1424
|
+
const results = rows.map(([tag, make]) => {
|
|
1425
|
+
const env = world(`no-ledger-${tag}`);
|
|
1426
|
+
make(env);
|
|
1427
|
+
const before = treeDigest(env.HOME as string);
|
|
1428
|
+
const run = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env });
|
|
1429
|
+
return {
|
|
1430
|
+
tag,
|
|
1431
|
+
status: run.status,
|
|
1432
|
+
named: (run.stderr || "").includes("deactivate-unattributed-leftovers"),
|
|
1433
|
+
unchanged: before === treeDigest(env.HOME as string),
|
|
1434
|
+
};
|
|
1435
|
+
});
|
|
1436
|
+
const clean = world("no-ledger-clean");
|
|
1437
|
+
const cleanRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: clean });
|
|
1438
|
+
ok(
|
|
1439
|
+
"[QK:HAC-NO-LEDGER-LEFTOVERS-REPORTED] without a ledger the teardown no longer says 'nothing to undo' over what " +
|
|
1440
|
+
"an aborted install left: an installing journal, a ready runtime, our npm cache, or a runtime with no journal " +
|
|
1441
|
+
"are each NAMED and the verb exits non-zero with every byte untouched — nothing proves what they belong to, so " +
|
|
1442
|
+
"nothing is removed. A truly clean host still exits 0 with the old line " +
|
|
1443
|
+
`(rows=${JSON.stringify(results)} clean=${cleanRun.status}/${JSON.stringify((cleanRun.stdout || "").trim())})`,
|
|
1444
|
+
results.every((r) => r.status !== 0 && r.named && r.unchanged) &&
|
|
1445
|
+
cleanRun.status === 0 &&
|
|
1446
|
+
(cleanRun.stdout || "").includes("no certified activation ledger — nothing to undo"),
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// ── 28. only a regular file — or nothing — may stand at the ledger address (#135) ─
|
|
1451
|
+
{
|
|
1452
|
+
const env = world("ledger-kinds");
|
|
1453
|
+
seedRuntime(env);
|
|
1454
|
+
const layout = resolveActivationLayout(env);
|
|
1455
|
+
fs.mkdirSync(layout.stateRoot, { recursive: true });
|
|
1456
|
+
// a DANGLING symlink: readFileSync used to answer ENOENT, i.e. "no ledger".
|
|
1457
|
+
fs.symlinkSync(path.join(env.HOME as string, "nowhere.json"), layout.ledgerPath);
|
|
1458
|
+
const dangling = refusal(() => readCertifiedLedger(layout));
|
|
1459
|
+
const danglingRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env });
|
|
1460
|
+
fs.unlinkSync(layout.ledgerPath);
|
|
1461
|
+
// a symlink to a perfectly valid ledger is still not OUR ledger file.
|
|
1462
|
+
const elsewhere = path.join(env.HOME as string, "ledger-elsewhere.json");
|
|
1463
|
+
fs.writeFileSync(elsewhere, `${JSON.stringify({ schemaVersion: 2, ...ledgerFor(env, ["pi"]) }, null, 2)}\n`);
|
|
1464
|
+
fs.symlinkSync(elsewhere, layout.ledgerPath);
|
|
1465
|
+
const linked = refusal(() => readCertifiedLedger(layout));
|
|
1466
|
+
fs.unlinkSync(layout.ledgerPath);
|
|
1467
|
+
fs.mkdirSync(layout.ledgerPath);
|
|
1468
|
+
const directory = refusal(() => readCertifiedLedger(layout));
|
|
1469
|
+
// a state root that is a FILE: lstat answers ENOTDIR, which must stay a named refusal, not a raw errno.
|
|
1470
|
+
const fileRoot = path.join(env.HOME as string, "state-root-is-a-file");
|
|
1471
|
+
fs.writeFileSync(fileRoot, "not a directory\n");
|
|
1472
|
+
const notDir = refusal(() =>
|
|
1473
|
+
readCertifiedLedger({ stateRoot: fileRoot, ledgerPath: path.join(fileRoot, "activation.json") }),
|
|
1474
|
+
);
|
|
1475
|
+
ok(
|
|
1476
|
+
"[QK:HAC-LEDGER-PATH-KIND] the ledger address is read by lstat first: a dangling symlink, a symlink to a valid " +
|
|
1477
|
+
"ledger elsewhere, a directory, and a state root that is a file are each refused by name — none of them is " +
|
|
1478
|
+
"'no ledger', which is what a dangling link used to read as, letting every verb act as if nothing had been " +
|
|
1479
|
+
`activated (dangling=${dangling}/teardown-exit=${danglingRun.status} linked=${linked} directory=${directory} not-dir=${notDir})`,
|
|
1480
|
+
dangling === "activation-ledger-uncertified" &&
|
|
1481
|
+
danglingRun.status !== 0 &&
|
|
1482
|
+
(danglingRun.stderr || "").includes("activation-ledger-uncertified") &&
|
|
1483
|
+
linked === "activation-ledger-uncertified" &&
|
|
1484
|
+
directory === "activation-ledger-uncertified" &&
|
|
1485
|
+
notDir === "activation-ledger-uncertified",
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// ── 29. the doctor's expectation mode comes from the activation ledger (#136) ────
|
|
1490
|
+
// `doctor-meta-bridge` must compute the plugin activation's ABSOLUTE expectations exactly when the
|
|
1491
|
+
// ledger authorised that wiring, and never adopt it from a checkout. The oracle for the returned
|
|
1492
|
+
// root is `runtimeRootOf`, computed here independently of the resolver.
|
|
1493
|
+
{
|
|
1494
|
+
const env = world("doctor-mode");
|
|
1495
|
+
seedRuntime(env);
|
|
1496
|
+
const active = runtimeRootOf(env);
|
|
1497
|
+
const runtimeRepo = path.join(active, "node_modules", "@junghanacs", "entwurf");
|
|
1498
|
+
const layout = resolveActivationLayout(env);
|
|
1499
|
+
const mode = (opts: { repo: string; override?: string }): string | null => {
|
|
1500
|
+
try {
|
|
1501
|
+
return resolveDoctorPluginRuntime(env, opts);
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
return `refused:${(err as { code?: string }).code ?? String(err)}`;
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
// D2 — no ledger: a doctor running from the runtime package is simply not in plugin mode.
|
|
1507
|
+
const absent = mode({ repo: runtimeRepo });
|
|
1508
|
+
writeLedger(layout, ledgerFor(env, ["pi", "claude-code"]));
|
|
1509
|
+
// D1 — runtime package + an active ledger that activated claude-code at THIS stable root.
|
|
1510
|
+
const derived = mode({ repo: runtimeRepo });
|
|
1511
|
+
ok(
|
|
1512
|
+
"[QK:DOCTOR-MODE-DERIVED] a doctor running from the plugin runtime's package derives plugin-runtime mode from " +
|
|
1513
|
+
`an active ledger that activated claude-code at this host's stable root, and from nothing else (no-ledger=${absent} derived=${derived})`,
|
|
1514
|
+
absent === null && derived === active,
|
|
1515
|
+
);
|
|
1516
|
+
// D3 — the checkout never derives, and never even opens the ledger: an uncertified body is
|
|
1517
|
+
// planted first, so reading it would throw instead of returning null.
|
|
1518
|
+
fs.writeFileSync(layout.ledgerPath, "not json\n");
|
|
1519
|
+
const checkout = mode({ repo: REPO });
|
|
1520
|
+
ok(
|
|
1521
|
+
"[QK:DOCTOR-MODE-CHECKOUT-NEVER-DERIVES] a doctor whose repo is not the runtime package returns no plugin mode " +
|
|
1522
|
+
`without opening the ledger, so a checkout never adopts plugin wiring (checkout=${checkout})`,
|
|
1523
|
+
checkout === null,
|
|
1524
|
+
);
|
|
1525
|
+
// D5 — on a reading path an uncertified ledger dies with the reader's own refusal, never null.
|
|
1526
|
+
fs.unlinkSync(layout.ledgerPath);
|
|
1527
|
+
fs.symlinkSync(path.join(env.HOME as string, "nowhere.json"), layout.ledgerPath);
|
|
1528
|
+
const viaRuntime = mode({ repo: runtimeRepo });
|
|
1529
|
+
const viaOverride = mode({ repo: REPO, override: active });
|
|
1530
|
+
fs.unlinkSync(layout.ledgerPath);
|
|
1531
|
+
ok(
|
|
1532
|
+
"[QK:DOCTOR-MODE-UNCERTIFIED-DIES] an uncertified ledger on either reading path (runtime-repo derivation, explicit " +
|
|
1533
|
+
`override) refuses with the reader's own code, never 'not plugin mode' (runtime=${viaRuntime} override=${viaOverride})`,
|
|
1534
|
+
viaRuntime === "refused:activation-ledger-uncertified" && viaOverride === "refused:activation-ledger-uncertified",
|
|
1535
|
+
);
|
|
1536
|
+
// D4 — the admission clauses on a reading path, each on a CERTIFIED ledger so only the clause can
|
|
1537
|
+
// refuse: a phase other than active, a ledger that never activated claude-code, and a runtimeRoot
|
|
1538
|
+
// 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" }));
|
|
1540
|
+
const notActive = mode({ repo: runtimeRepo });
|
|
1541
|
+
writeLedger(layout, ledgerFor(env, ["pi"]));
|
|
1542
|
+
const piOnly = mode({ repo: runtimeRepo });
|
|
1543
|
+
writeLedger(layout, {
|
|
1544
|
+
...ledgerFor(env, ["pi", "claude-code"]),
|
|
1545
|
+
runtimeRoot: path.join(env.HOME as string, "another-runtime", "active"),
|
|
1546
|
+
});
|
|
1547
|
+
const foreignRoot = mode({ repo: runtimeRepo });
|
|
1548
|
+
ok(
|
|
1549
|
+
"[QK:DOCTOR-MODE-ADMISSION-CLAUSES] on the runtime repo a certified ledger still admits plugin mode only when " +
|
|
1550
|
+
"phase is active, claude-code was activated and its runtimeRoot is this host's stable root — each other case " +
|
|
1551
|
+
`is refused by name (activating=${notActive} pi-only=${piOnly} foreign-root=${foreignRoot})`,
|
|
1552
|
+
notActive === "refused:doctor-plugin-runtime-unadmitted" &&
|
|
1553
|
+
piOnly === "refused:doctor-plugin-runtime-unadmitted" &&
|
|
1554
|
+
foreignRoot === "refused:doctor-plugin-runtime-unadmitted",
|
|
1555
|
+
);
|
|
1556
|
+
// 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"]));
|
|
1558
|
+
const elsewhere = mode({ repo: REPO, override: path.join(env.HOME as string, "elsewhere") });
|
|
1559
|
+
const agreeing = mode({ repo: REPO, override: active });
|
|
1560
|
+
ok(
|
|
1561
|
+
"[QK:DOCTOR-MODE-OVERRIDE-AGREES] an explicit --plugin-runtime is admitted only when it names the ledger's own " +
|
|
1562
|
+
`stable root (elsewhere=${elsewhere} agreeing=${agreeing})`,
|
|
1563
|
+
elsewhere === "refused:doctor-plugin-runtime-unadmitted" && agreeing === active,
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1367
1567
|
console.log(`\ncheck-herdr-activation: ${passed} assertions passed`);
|
|
@@ -187,12 +187,18 @@ function tree(root: string): string[] {
|
|
|
187
187
|
* installed package's activation verb is — absent, incapable (exits 0 on an empty request), or a
|
|
188
188
|
* faithful recorder that refuses an empty request by name and logs the argv it was given.
|
|
189
189
|
*/
|
|
190
|
-
function fixtureAcquire(opts: {
|
|
190
|
+
function fixtureAcquire(opts: {
|
|
191
|
+
activate: "absent" | "incapable" | "recorder";
|
|
192
|
+
log?: string;
|
|
193
|
+
fail?: boolean;
|
|
194
|
+
name?: string;
|
|
195
|
+
}) {
|
|
196
|
+
const name = opts.name ?? PACKAGE;
|
|
191
197
|
return ({ identity, prefix }: AcquireArgs) => {
|
|
192
198
|
const version = identity.version ?? FIXTURE_VERSION;
|
|
193
|
-
const root = path.join(prefix, "node_modules",
|
|
199
|
+
const root = path.join(prefix, "node_modules", name);
|
|
194
200
|
fs.mkdirSync(path.join(root, path.dirname(COMPILED_ENTRY)), { recursive: true });
|
|
195
|
-
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name
|
|
201
|
+
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name, version })}\n`);
|
|
196
202
|
fs.writeFileSync(path.join(root, COMPILED_ENTRY), "// compiled entry fixture\n");
|
|
197
203
|
const bin = path.join(prefix, "node_modules", ".bin");
|
|
198
204
|
fs.mkdirSync(bin, { recursive: true });
|
|
@@ -222,7 +228,7 @@ function fixtureAcquire(opts: { activate: "absent" | "incapable" | "recorder"; l
|
|
|
222
228
|
observedDigest: `sha256-${createHash("sha256")
|
|
223
229
|
.update(identity.commit ?? "x")
|
|
224
230
|
.digest("hex")}`,
|
|
225
|
-
packageName:
|
|
231
|
+
packageName: name,
|
|
226
232
|
packageVersion: version,
|
|
227
233
|
};
|
|
228
234
|
};
|
|
@@ -238,7 +244,7 @@ function drive(
|
|
|
238
244
|
env: NodeJS.ProcessEnv,
|
|
239
245
|
text: string | { error: string } | { status: number; stderr: string },
|
|
240
246
|
deps: Record<string, unknown> = {},
|
|
241
|
-
): { code: number | null; refusal: string | null; out: string; progress: string[] } {
|
|
247
|
+
): { code: number | null; refusal: string | null; detail: string; out: string; progress: string[] } {
|
|
242
248
|
let out = "";
|
|
243
249
|
// The operator's terminal is a SEAM here, never the real `/dev/tty`: a gate that narrated into
|
|
244
250
|
// whatever terminal happened to be running it would be writing outside its own sandbox.
|
|
@@ -269,11 +275,12 @@ function drive(
|
|
|
269
275
|
readRuntimeLock: () => FIXTURE_NPM_LOCK,
|
|
270
276
|
...deps,
|
|
271
277
|
});
|
|
272
|
-
return { code, refusal: null, out, progress: progressLines };
|
|
278
|
+
return { code, refusal: null, detail: "", out, progress: progressLines };
|
|
273
279
|
} catch (err) {
|
|
274
280
|
return {
|
|
275
281
|
code: null,
|
|
276
282
|
refusal: (err as { code?: string }).code ?? `not-a-HerdrBuildError:${String(err)}`,
|
|
283
|
+
detail: (err as { detail?: string }).detail ?? String(err),
|
|
277
284
|
out,
|
|
278
285
|
progress: progressLines,
|
|
279
286
|
};
|
|
@@ -727,6 +734,228 @@ function drive(
|
|
|
727
734
|
);
|
|
728
735
|
}
|
|
729
736
|
|
|
737
|
+
// ── 6c. the selected components' owners are asked before the first byte (#135) ─
|
|
738
|
+
/** Run one of the shipped Pi ownership writers against a sandbox, exactly as run.sh does. */
|
|
739
|
+
function piOwner(env: NodeJS.ProcessEnv, script: string, args: string[]): SpawnResult {
|
|
740
|
+
return spawnSync("python3", [path.join(REPO, "scripts", script), ...args], {
|
|
741
|
+
encoding: "utf8",
|
|
742
|
+
env,
|
|
743
|
+
}) as unknown as SpawnResult;
|
|
744
|
+
}
|
|
745
|
+
function piSettingsOf(env: NodeJS.ProcessEnv): string {
|
|
746
|
+
return path.join(env.HOME as string, ".pi", "agent", "settings.json");
|
|
747
|
+
}
|
|
748
|
+
function piStateOf(env: NodeJS.ProcessEnv, half: "package" | "provider"): string {
|
|
749
|
+
return path.join(env.XDG_DATA_HOME as string, "entwurf", `pi-${half}`, "install-state.json");
|
|
750
|
+
}
|
|
751
|
+
/** A LIVE foreign owner: a real directory standing for a developer checkout, registered for real. */
|
|
752
|
+
function foreignPiOwner(env: NodeJS.ProcessEnv, halves: ("package" | "provider")[]): string {
|
|
753
|
+
const checkout = path.join(env.HOME as string, "dev", "entwurf");
|
|
754
|
+
fs.mkdirSync(checkout, { recursive: true });
|
|
755
|
+
fs.mkdirSync(path.dirname(piSettingsOf(env)), { recursive: true });
|
|
756
|
+
fs.writeFileSync(piSettingsOf(env), '{"theme":"dark"}\n');
|
|
757
|
+
if (halves.includes("package")) {
|
|
758
|
+
const r = piOwner(env, "register-pi-package.py", [
|
|
759
|
+
piSettingsOf(env),
|
|
760
|
+
checkout,
|
|
761
|
+
"--scope",
|
|
762
|
+
"user",
|
|
763
|
+
"--state",
|
|
764
|
+
piStateOf(env, "package"),
|
|
765
|
+
]);
|
|
766
|
+
assert.equal(r.status, 0, `fixture: seeding the foreign package owner failed: ${r.stderr}`);
|
|
767
|
+
}
|
|
768
|
+
if (halves.includes("provider")) {
|
|
769
|
+
const r = piOwner(env, "register-pi-provider.py", [
|
|
770
|
+
"install",
|
|
771
|
+
piSettingsOf(env),
|
|
772
|
+
checkout,
|
|
773
|
+
"--scope",
|
|
774
|
+
"user",
|
|
775
|
+
"--state",
|
|
776
|
+
piStateOf(env, "provider"),
|
|
777
|
+
]);
|
|
778
|
+
assert.equal(r.status, 0, `fixture: seeding the foreign provider owner failed: ${r.stderr}`);
|
|
779
|
+
}
|
|
780
|
+
return checkout;
|
|
781
|
+
}
|
|
782
|
+
/** An acquisition that must never be reached: it counts, and it throws a sentinel instead of fetching. */
|
|
783
|
+
function countingSentinel(): { acquire: () => never; calls: () => number } {
|
|
784
|
+
let calls = 0;
|
|
785
|
+
return {
|
|
786
|
+
acquire: () => {
|
|
787
|
+
calls++;
|
|
788
|
+
throw new Error("HPB_OWNERSHIP_SENTINEL_NO_NETWORK");
|
|
789
|
+
},
|
|
790
|
+
calls: () => calls,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
{
|
|
794
|
+
const env = world("owner-package");
|
|
795
|
+
foreignPiOwner(env, ["package"]);
|
|
796
|
+
const before = treeDigest(env.HOME as string);
|
|
797
|
+
const sentinel = countingSentinel();
|
|
798
|
+
const r = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
799
|
+
acquire: sentinel.acquire,
|
|
800
|
+
resolveCommit: () => COMMIT_A,
|
|
801
|
+
});
|
|
802
|
+
const after = treeDigest(env.HOME as string);
|
|
803
|
+
ok(
|
|
804
|
+
"[QK:HPB-OWNERSHIP-BEFORE-ACQUISITION] a Pi user-scope registration owned by another LIVE root refuses the " +
|
|
805
|
+
"plugin install BEFORE anything is fetched or written: the SAME shipped package-owner preflight the installed " +
|
|
806
|
+
"activation runs is asked against the prospective plugin root, so the refusal names the component and the " +
|
|
807
|
+
"acquisition is never called — no installing journal, runtime, cache or ledger appears and every byte under " +
|
|
808
|
+
"the sandbox HOME is unchanged. The first cut asked only after bootstrap, so a known refusal still fetched and " +
|
|
809
|
+
`installed a runtime first (refusal=${r.refusal} acquired=${sentinel.calls()} home-unchanged=${before === after} ` +
|
|
810
|
+
`journal=${fs.existsSync(journalPathOf(env))} detail=${JSON.stringify(r.detail.slice(0, 160))})`,
|
|
811
|
+
r.refusal === "herdr-build-component-ownership-refused" &&
|
|
812
|
+
r.detail.includes("pi-package") &&
|
|
813
|
+
sentinel.calls() === 0 &&
|
|
814
|
+
before === after &&
|
|
815
|
+
!fs.existsSync(journalPathOf(env)),
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
{
|
|
819
|
+
// The provider key carries its OWN ownership record; a host whose package entry is free but whose
|
|
820
|
+
// provider key another root owns must refuse just as early.
|
|
821
|
+
const env = world("owner-provider");
|
|
822
|
+
foreignPiOwner(env, ["provider"]);
|
|
823
|
+
const before = treeDigest(env.HOME as string);
|
|
824
|
+
const sentinel = countingSentinel();
|
|
825
|
+
const r = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
826
|
+
acquire: sentinel.acquire,
|
|
827
|
+
resolveCommit: () => COMMIT_A,
|
|
828
|
+
});
|
|
829
|
+
ok(
|
|
830
|
+
"[QK:HPB-OWNERSHIP-PROVIDER-HALF] the provider half is asked on its own: a free package entry with a provider " +
|
|
831
|
+
"key another LIVE root owns still refuses before acquisition, names `pi-provider`, and leaves the sandbox " +
|
|
832
|
+
`HOME byte-identical (refusal=${r.refusal} acquired=${sentinel.calls()} home-unchanged=${before === treeDigest(env.HOME as string)} detail=${JSON.stringify(r.detail.slice(0, 120))})`,
|
|
833
|
+
r.refusal === "herdr-build-component-ownership-refused" &&
|
|
834
|
+
r.detail.includes("pi-provider") &&
|
|
835
|
+
!r.detail.includes("pi-package:") &&
|
|
836
|
+
sentinel.calls() === 0 &&
|
|
837
|
+
before === treeDigest(env.HOME as string),
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
{
|
|
841
|
+
// Claude's preflight is its PLAN check (documents and managed-key shapes), run against the
|
|
842
|
+
// prospective root in plugin mode. It is not a claim about which root owns Claude.
|
|
843
|
+
const env = world("owner-claude-plan");
|
|
844
|
+
fs.mkdirSync(path.join(env.HOME as string, ".claude"), { recursive: true });
|
|
845
|
+
fs.writeFileSync(path.join(env.HOME as string, ".claude", "settings.json"), "[]\n");
|
|
846
|
+
const before = treeDigest(env.HOME as string);
|
|
847
|
+
const sentinel = countingSentinel();
|
|
848
|
+
const r = drive(env, listing({ claude: "current (v2) (/home/u/.claude)" }), {
|
|
849
|
+
acquire: sentinel.acquire,
|
|
850
|
+
resolveCommit: () => COMMIT_A,
|
|
851
|
+
});
|
|
852
|
+
ok(
|
|
853
|
+
"[QK:HPB-OWNERSHIP-CLAUDE-PLAN] a selected Claude whose install plan its own state owner refuses (a settings " +
|
|
854
|
+
"file that is not a JSON object) refuses before acquisition, names `claude-code`, and writes nothing " +
|
|
855
|
+
`(refusal=${r.refusal} acquired=${sentinel.calls()} home-unchanged=${before === treeDigest(env.HOME as string)} detail=${JSON.stringify(r.detail.slice(0, 120))})`,
|
|
856
|
+
r.refusal === "herdr-build-component-ownership-refused" &&
|
|
857
|
+
r.detail.includes("claude-code") &&
|
|
858
|
+
sentinel.calls() === 0 &&
|
|
859
|
+
before === treeDigest(env.HOME as string),
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
{
|
|
863
|
+
// Only the SELECTED components are asked: a backend Herdr has not integrated cannot veto another's install.
|
|
864
|
+
const piOnly = world("owner-unselected-claude");
|
|
865
|
+
fs.mkdirSync(path.join(piOnly.HOME as string, ".claude"), { recursive: true });
|
|
866
|
+
fs.writeFileSync(path.join(piOnly.HOME as string, ".claude", "settings.json"), "[]\n");
|
|
867
|
+
const piLog = path.join(piOnly.HOME as string, "argv.log");
|
|
868
|
+
const piRun = drive(piOnly, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
869
|
+
acquire: fixtureAcquire({ activate: "recorder", log: piLog }),
|
|
870
|
+
resolveCommit: () => COMMIT_A,
|
|
871
|
+
});
|
|
872
|
+
const claudeOnly = world("owner-unselected-pi");
|
|
873
|
+
foreignPiOwner(claudeOnly, ["package", "provider"]);
|
|
874
|
+
const claudeLog = path.join(claudeOnly.HOME as string, "argv.log");
|
|
875
|
+
const claudeRun = drive(claudeOnly, listing({ claude: "current (v2) (/home/u/.claude)" }), {
|
|
876
|
+
acquire: fixtureAcquire({ activate: "recorder", log: claudeLog }),
|
|
877
|
+
resolveCommit: () => COMMIT_A,
|
|
878
|
+
});
|
|
879
|
+
const logged = (f: string) => (fs.existsSync(f) ? fs.readFileSync(f, "utf8").trim() : "");
|
|
880
|
+
ok(
|
|
881
|
+
"[QK:HPB-OWNERSHIP-SELECTED-ONLY] only the backends in `A` are asked: Pi alone installs over a Claude settings " +
|
|
882
|
+
"file its owner would refuse, and Claude alone installs over a Pi registration another root owns — an " +
|
|
883
|
+
"unselected backend's owner never vetoes the selected one " +
|
|
884
|
+
`(pi-only=${piRun.code}/${piRun.refusal}/${JSON.stringify(logged(piLog))} claude-only=${claudeRun.code}/${claudeRun.refusal}/${JSON.stringify(logged(claudeLog))})`,
|
|
885
|
+
piRun.code === 0 && logged(piLog) === "pi" && claudeRun.code === 0 && logged(claudeLog) === "claude-code",
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
{
|
|
889
|
+
// A SAME-owner reinstall under a symlinked XDG_DATA_HOME. The installed verb sees its own root in
|
|
890
|
+
// PHYSICAL form (Node realpaths the ESM entry) and the provider owner records that root without
|
|
891
|
+
// resolving it, so the early ask must use the physical form too, while the stable command stays the
|
|
892
|
+
// lexical runtime address.
|
|
893
|
+
const home = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hpb-owner-symlinked-")));
|
|
894
|
+
const realData = path.join(home, "real-data");
|
|
895
|
+
fs.mkdirSync(realData, { recursive: true });
|
|
896
|
+
fs.symlinkSync(realData, path.join(home, "data"));
|
|
897
|
+
const env: NodeJS.ProcessEnv = {
|
|
898
|
+
HOME: home,
|
|
899
|
+
XDG_DATA_HOME: path.join(home, "data"),
|
|
900
|
+
XDG_CACHE_HOME: path.join(home, "cache"),
|
|
901
|
+
XDG_STATE_HOME: path.join(home, "state"),
|
|
902
|
+
PATH: process.env.PATH,
|
|
903
|
+
};
|
|
904
|
+
const physicalRoot = path.join(realData, "entwurf", "herdr-plugin", "runtime", "active", "node_modules", PACKAGE);
|
|
905
|
+
fs.mkdirSync(path.dirname(piSettingsOf(env)), { recursive: true });
|
|
906
|
+
fs.writeFileSync(piSettingsOf(env), '{"theme":"dark"}\n');
|
|
907
|
+
const pkg = piOwner(env, "register-pi-package.py", [
|
|
908
|
+
piSettingsOf(env),
|
|
909
|
+
physicalRoot,
|
|
910
|
+
"--scope",
|
|
911
|
+
"user",
|
|
912
|
+
"--state",
|
|
913
|
+
piStateOf(env, "package"),
|
|
914
|
+
]);
|
|
915
|
+
const prov = piOwner(env, "register-pi-provider.py", [
|
|
916
|
+
"install",
|
|
917
|
+
piSettingsOf(env),
|
|
918
|
+
physicalRoot,
|
|
919
|
+
"--scope",
|
|
920
|
+
"user",
|
|
921
|
+
"--state",
|
|
922
|
+
piStateOf(env, "provider"),
|
|
923
|
+
"--plugin-runtime",
|
|
924
|
+
activeDirOf(env),
|
|
925
|
+
]);
|
|
926
|
+
assert.equal(pkg.status, 0, `fixture: same-owner package seed failed: ${pkg.stderr}`);
|
|
927
|
+
assert.equal(prov.status, 0, `fixture: same-owner provider seed failed: ${prov.stderr}`);
|
|
928
|
+
const log = path.join(home, "argv.log");
|
|
929
|
+
const r = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
930
|
+
acquire: fixtureAcquire({ activate: "recorder", log }),
|
|
931
|
+
resolveCommit: () => COMMIT_A,
|
|
932
|
+
});
|
|
933
|
+
ok(
|
|
934
|
+
"[QK:HPB-OWNERSHIP-PHYSICAL-ROOT] a same-owner reinstall under a symlinked XDG_DATA_HOME is admitted, not " +
|
|
935
|
+
"mistaken for a foreign root: the early owner preflight asks with the PHYSICAL prospective package root " +
|
|
936
|
+
"(the form the installed verb records as its own) and passes the LEXICAL stable runtime as the plugin-mode " +
|
|
937
|
+
"runtime flag (the only form the provider owner accepts), so the build reaches activation " +
|
|
938
|
+
`(code=${r.code} refusal=${r.refusal} detail=${JSON.stringify(r.detail.slice(0, 160))})`,
|
|
939
|
+
r.code === 0 && fs.existsSync(log) && fs.readFileSync(log, "utf8").trim() === "pi",
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
{
|
|
943
|
+
// The owners were asked about one root; the installed verb must run from that same root.
|
|
944
|
+
const env = world("owner-root-tie");
|
|
945
|
+
const log = path.join(env.HOME as string, "argv.log");
|
|
946
|
+
const r = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
947
|
+
acquire: fixtureAcquire({ activate: "recorder", log, name: "@elsewhere/entwurf" }),
|
|
948
|
+
readRuntimeLock: () => FIXTURE_CHECKOUT_LOCK,
|
|
949
|
+
resolveCommit: () => COMMIT_A,
|
|
950
|
+
});
|
|
951
|
+
ok(
|
|
952
|
+
"[QK:HPB-PLANNED-ROOT-TIE] the root the component owners were asked about is the root the installed verb runs " +
|
|
953
|
+
"from: an artifact that lands under another package name is refused by name before activation, so the early " +
|
|
954
|
+
`answer can never have been about a different root (refusal=${r.refusal} activated=${fs.existsSync(log)} detail=${JSON.stringify(r.detail.slice(0, 160))})`,
|
|
955
|
+
r.refusal === "herdr-build-artifact-drifted" && !fs.existsSync(log),
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
730
959
|
// ── 7. the runner claims nothing about Herdr's own commit ──────────────────────
|
|
731
960
|
{
|
|
732
961
|
const env = world("post-build-gap");
|
|
@@ -227,6 +227,8 @@ ok "fake live entwurf MCP bridge running as a child of the stand-in Claude pid (
|
|
|
227
227
|
# --- doctor driver -----------------------------------------------------------
|
|
228
228
|
DOC_OUT=""
|
|
229
229
|
DOC_RC=0
|
|
230
|
+
# #136: arguments for the plugin-runtime block at the end; empty for every clone cell.
|
|
231
|
+
DOC_ARGS=()
|
|
230
232
|
run_doctor() {
|
|
231
233
|
set +e
|
|
232
234
|
# Deliberate cross-harness poison. Before the doctor scrubbed these carriers, the
|
|
@@ -237,7 +239,7 @@ run_doctor() {
|
|
|
237
239
|
DOC_OUT="$(PI_SESSION_ID=ambient-pi-session-must-not-win \
|
|
238
240
|
PI_AGENT_ID=ambient-pi-agent/must-not-win \
|
|
239
241
|
ENTWURF_META_SENDER_MARKER="$TMP/ambient-sender-marker-must-not-win.json" \
|
|
240
|
-
bash "$REPO/scripts/meta-bridge-doctor.sh" 2>&1)"
|
|
242
|
+
bash "$REPO/scripts/meta-bridge-doctor.sh" ${DOC_ARGS[@]+"${DOC_ARGS[@]}"} 2>&1)"
|
|
241
243
|
DOC_RC=$?
|
|
242
244
|
set -e
|
|
243
245
|
}
|
|
@@ -713,5 +715,88 @@ else
|
|
|
713
715
|
bad "doctor stayed red after reverting every mutation (a restore is incomplete):"$'\n'"$(printf '%s\n' "$DOC_OUT" | grep -E '^ FAIL' | sed 's/^/ /')"
|
|
714
716
|
fi
|
|
715
717
|
|
|
718
|
+
# --- 6. #136: a Herdr plugin activation's ABSOLUTE wiring ---------------------
|
|
719
|
+
# The plugin activation runs the same installer with ENTWURF_PLUGIN_RUNTIME, so Claude's MCP and
|
|
720
|
+
# statusLine name absolute bins under the stable runtime. The doctor must learn that mode from the
|
|
721
|
+
# activation ledger and certify the wiring, not call it drift. FIXTURE HONESTY: the two runtime
|
|
722
|
+
# bins are wrappers onto this checkout's own launcher and statusline, and the ledger is written by
|
|
723
|
+
# the shipped writer with a fixture artifact identity; no npm runtime is installed here.
|
|
724
|
+
echo "[plugin-runtime] the installer's absolute wiring, certified through the activation ledger"
|
|
725
|
+
ACTIVE="$XDG_DATA_HOME/entwurf/herdr-plugin/runtime/active"
|
|
726
|
+
mkdir -p "$ACTIVE/node_modules/.bin" "$ACTIVE/node_modules/@junghanacs/entwurf"
|
|
727
|
+
printf '#!/usr/bin/env bash\nexec bash "%s/mcp/entwurf-bridge/start.sh" "$@"\n' "$REPO" > "$ACTIVE/node_modules/.bin/entwurf-bridge"
|
|
728
|
+
printf '#!/usr/bin/env bash\nexec "%s/scripts/meta-bridge-statusline.sh" "$@"\n' "$REPO" > "$ACTIVE/node_modules/.bin/entwurf-statusline"
|
|
729
|
+
chmod +x "$ACTIVE/node_modules/.bin/entwurf-bridge" "$ACTIVE/node_modules/.bin/entwurf-statusline"
|
|
730
|
+
if ENTWURF_PLUGIN_RUNTIME="$ACTIVE" bash "$REPO/scripts/meta-bridge-install.sh" >"$TMP/install-plugin.log" 2>&1; then
|
|
731
|
+
ok "real meta-bridge-install.sh wires the plugin-runtime mode (absolute bins under the sandbox stable root)"
|
|
732
|
+
else
|
|
733
|
+
bad "plugin-runtime install-meta-bridge failed in the sandbox:"$'\n'"$(sed 's/^/ install| /' "$TMP/install-plugin.log")"
|
|
734
|
+
fi
|
|
735
|
+
rm -rf "$PLANTED"
|
|
736
|
+
mkdir -p "$PLANTED"
|
|
737
|
+
cp -r "$ASM/$PLUGIN/." "$PLANTED/"
|
|
738
|
+
write_ledger() {
|
|
739
|
+
node --input-type=module - "$REPO/scripts/herdr-activation.mjs" "$ACTIVE" "$1" <<'JS'
|
|
740
|
+
import { pathToFileURL } from "node:url";
|
|
741
|
+
const [modPath, active, backends] = process.argv.slice(2);
|
|
742
|
+
const m = await import(pathToFileURL(modPath).href);
|
|
743
|
+
const states = Object.fromEntries(backends.split(",").map((b) => [b, "active"]));
|
|
744
|
+
m.writeLedger(m.resolveActivationLayout(process.env), m.ledgerBody({
|
|
745
|
+
phase: "active",
|
|
746
|
+
runtimeRoot: active,
|
|
747
|
+
artifactIdentity: { kind: "npm", name: "@junghanacs/entwurf", version: "0.34.1", expectedIntegrity: "sha512-fixture", observedDigest: `sha256-${"c".repeat(64)}` },
|
|
748
|
+
roots: m.resolveComponentRoots(process.env),
|
|
749
|
+
states,
|
|
750
|
+
}));
|
|
751
|
+
JS
|
|
752
|
+
}
|
|
753
|
+
write_ledger "pi,claude-code"
|
|
754
|
+
DOC_ARGS=(--plugin-runtime "$ACTIVE")
|
|
755
|
+
|
|
756
|
+
# P1 — the plugin wiring the installer wrote is certified: PASS, and the doctor names its mode.
|
|
757
|
+
# The [QK:] token lives on the `bad` line only (see M9b).
|
|
758
|
+
run_doctor
|
|
759
|
+
if [ "$DOC_RC" -eq 0 ] && printf '%s\n' "$DOC_OUT" | grep -q 'meta-bridge doctor: PASS' \
|
|
760
|
+
&& printf '%s\n' "$DOC_OUT" | grep -qF "mode: plugin-runtime $ACTIVE (activation ledger)" \
|
|
761
|
+
&& printf '%s\n' "$DOC_OUT" | grep -qF 'live mcpServers.entwurf-bridge equals desired_mcp()'; then
|
|
762
|
+
ok "plugin-runtime wiring reaches doctor PASS through the activation ledger, and the doctor names that mode"
|
|
763
|
+
else
|
|
764
|
+
bad "[QK:DOCTOR-PLUGIN-MODE-GREEN] the plugin activation's absolute wiring did NOT reach PASS (exit $DOC_RC):"$'\n'"$(printf '%s\n' "$DOC_OUT" | grep -E '^ (FAIL|WARN)|mode:' | sed 's/^/ /')"
|
|
765
|
+
fi
|
|
766
|
+
|
|
767
|
+
# P2 — genuine drift stays red in plugin mode: an absolute statusLine under the same root, but not ours.
|
|
768
|
+
cp "$CLAUDE_CONFIG_DIR/settings.json" "$TMP/settings-p2.bak"
|
|
769
|
+
python3 - "$CLAUDE_CONFIG_DIR/settings.json" "$ACTIVE/node_modules/.bin/evil" <<'PY'
|
|
770
|
+
import json, sys
|
|
771
|
+
p = sys.argv[1]
|
|
772
|
+
d = json.load(open(p, encoding="utf-8"))
|
|
773
|
+
d["statusLine"]["command"] = sys.argv[2]
|
|
774
|
+
json.dump(d, open(p, "w", encoding="utf-8"), indent=2)
|
|
775
|
+
PY
|
|
776
|
+
expect_red "plugin-mode statusLine repointed under the same runtime root" "statusLine.command drifted"
|
|
777
|
+
cp "$TMP/settings-p2.bak" "$CLAUDE_CONFIG_DIR/settings.json"
|
|
778
|
+
|
|
779
|
+
# P3 — a ledger that did not activate claude-code admits no plugin mode. The refusal is named,
|
|
780
|
+
# and it is NOT an early exit: the store and live-owner sections still run (Hard Rule 13).
|
|
781
|
+
write_ledger "pi"
|
|
782
|
+
expect_red "override against a ledger that never activated claude-code" "doctor-plugin-runtime-unadmitted"
|
|
783
|
+
# Each of the three expectation sites says UNKNOWN for that reason (never a bare-bin fallback),
|
|
784
|
+
# and the runtime axes still run: the live command still DELIVERS, the store and owner join report.
|
|
785
|
+
for claim in \
|
|
786
|
+
'managed settings/MCP keyset is UNKNOWN: the expectation mode was not established' \
|
|
787
|
+
'statusLine expectation is UNKNOWN: the expectation mode was not established' \
|
|
788
|
+
'live-vs-desired MCP ownership is UNKNOWN: the expectation mode was not established' \
|
|
789
|
+
'live bridge command DELIVERS' \
|
|
790
|
+
'[meta-record store]' \
|
|
791
|
+
'[live Claude MCP owner join]'
|
|
792
|
+
do
|
|
793
|
+
if printf '%s\n' "$DOC_OUT" | sed -n '/expectation mode refused: doctor-plugin-runtime-unadmitted/,$p' | grep -qF "$claim"; then
|
|
794
|
+
ok "after the mode refusal the doctor still reports: $claim"
|
|
795
|
+
else
|
|
796
|
+
bad "after the mode refusal the doctor never reported: $claim"
|
|
797
|
+
fi
|
|
798
|
+
done
|
|
799
|
+
write_ledger "pi,claude-code"
|
|
800
|
+
|
|
716
801
|
echo
|
|
717
802
|
if [ "$fail" -eq 0 ]; then echo "check-meta-doctor-oracle: PASS"; else echo "check-meta-doctor-oracle: FAIL (see above)"; exit 1; fi
|