@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
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import fs from "node:fs";
|
|
30
30
|
import path from "node:path";
|
|
31
|
-
import { artifactStage, certifyArtifactIdentity, sameArtifactRequest } from "./herdr-runtime.mjs";
|
|
31
|
+
import { artifactStage, certifyArtifactIdentity, resolveRuntimeLayout, sameArtifactRequest } from "./herdr-runtime.mjs";
|
|
32
32
|
|
|
33
33
|
/** Ledger format. Bump only with a reader that understands both. */
|
|
34
34
|
export const ACTIVATION_SCHEMA_VERSION = 2;
|
|
@@ -155,6 +155,22 @@ export function writeLedger(layout, entry) {
|
|
|
155
155
|
* host it was never describing.
|
|
156
156
|
*/
|
|
157
157
|
export function readCertifiedLedger(layout) {
|
|
158
|
+
// lstat FIRST (#135). `readFileSync` follows links, so a dangling symlink at the ledger address
|
|
159
|
+
// read as ENOENT — "no ledger" — and every verb then acted as if nothing had ever been activated.
|
|
160
|
+
// Only a regular file, or nothing at all, may stand there.
|
|
161
|
+
let kind;
|
|
162
|
+
try {
|
|
163
|
+
kind = fs.lstatSync(layout.ledgerPath, { throwIfNoEntry: false });
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new ActivationError("activation-ledger-uncertified", `${layout.ledgerPath}: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
if (kind === undefined) return null;
|
|
168
|
+
if (!kind.isFile()) {
|
|
169
|
+
throw new ActivationError(
|
|
170
|
+
"activation-ledger-uncertified",
|
|
171
|
+
`${layout.ledgerPath} is ${kind.isSymbolicLink() ? "a symlink" : kind.isDirectory() ? "a directory" : "not a regular file"}; only a regular file or nothing may stand at the ledger address`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
158
174
|
let raw;
|
|
159
175
|
try {
|
|
160
176
|
raw = fs.readFileSync(layout.ledgerPath, "utf8");
|
|
@@ -486,6 +502,55 @@ export function piStatePaths(env) {
|
|
|
486
502
|
});
|
|
487
503
|
}
|
|
488
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Which expectations `doctor-meta-bridge` must compute (#136): the stable active root when this
|
|
507
|
+
* host's Claude wiring was written by a plugin activation, otherwise null (clone / installed).
|
|
508
|
+
*
|
|
509
|
+
* The plugin activation writes Claude's MCP and statusLine as ABSOLUTE commands under the stable
|
|
510
|
+
* runtime. A doctor that does not know that expects bare bins and calls correct wiring drifted. The
|
|
511
|
+
* mode is read from the record that authorised the wiring, the certified activation ledger, never
|
|
512
|
+
* from settings, environment or a path guess:
|
|
513
|
+
* - phase `active`, `claude-code` activated, and the ledger's runtimeRoot is THIS host's stable root;
|
|
514
|
+
* - without an override, only a doctor running FROM that runtime's package derives the mode. A
|
|
515
|
+
* checkout's doctor returns null without opening the ledger, so it never adopts plugin wiring;
|
|
516
|
+
* - an explicit override must agree with the ledger, or it is refused.
|
|
517
|
+
* An absent ledger is "not plugin mode"; an uncertified one throws the reader's own refusal.
|
|
518
|
+
*/
|
|
519
|
+
export function resolveDoctorPluginRuntime(env, { repo, override } = {}) {
|
|
520
|
+
const activeDir = resolveRuntimeLayout(env).activeDir;
|
|
521
|
+
if (override === undefined) {
|
|
522
|
+
const runtimePackage = path.join(activeDir, "node_modules", "@junghanacs", "entwurf");
|
|
523
|
+
let same = false;
|
|
524
|
+
try {
|
|
525
|
+
same = fs.realpathSync(repo) === fs.realpathSync(runtimePackage);
|
|
526
|
+
} catch {
|
|
527
|
+
// Bounded probe: no runtime package at the stable address means this repo is not it.
|
|
528
|
+
same = false;
|
|
529
|
+
}
|
|
530
|
+
if (!same) return null;
|
|
531
|
+
}
|
|
532
|
+
const ledger = readCertifiedLedger(resolveActivationLayout(env));
|
|
533
|
+
if (ledger === null) {
|
|
534
|
+
if (override === undefined) return null;
|
|
535
|
+
throw new ActivationError(
|
|
536
|
+
"doctor-plugin-runtime-unadmitted",
|
|
537
|
+
"--plugin-runtime was given but no activation ledger exists",
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const refuse = (clause) => {
|
|
541
|
+
throw new ActivationError("doctor-plugin-runtime-unadmitted", clause);
|
|
542
|
+
};
|
|
543
|
+
if (ledger.phase !== "active") refuse(`activation ledger phase is ${ledger.phase}, not active`);
|
|
544
|
+
if (!ledger.activatedBackends.includes("claude-code")) refuse("the activation ledger did not activate claude-code");
|
|
545
|
+
if (ledger.runtimeRoot !== activeDir) {
|
|
546
|
+
refuse(`the ledger's runtimeRoot ${ledger.runtimeRoot} is not this host's stable runtime root ${activeDir}`);
|
|
547
|
+
}
|
|
548
|
+
if (override !== undefined && override !== activeDir) {
|
|
549
|
+
refuse(`--plugin-runtime ${override} does not name the ledger's runtime root ${activeDir}`);
|
|
550
|
+
}
|
|
551
|
+
return activeDir;
|
|
552
|
+
}
|
|
553
|
+
|
|
489
554
|
/** A component row, in the one shape the certifier accepts. */
|
|
490
555
|
export function componentRow(backend, state) {
|
|
491
556
|
return Object.freeze({ backend, state });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* herdr-doctor-mode — the one call `meta-bridge-doctor.sh` makes to learn its expectation mode (#136).
|
|
4
|
+
*
|
|
5
|
+
* node herdr-doctor-mode.mjs <repo> [<plugin-runtime override>]
|
|
6
|
+
*
|
|
7
|
+
* Prints the stable active root (plugin-runtime mode) or nothing (clone / installed), exit 0.
|
|
8
|
+
* A refusal prints `code: detail` to stderr and exits 3. Any other exit is a crash the doctor
|
|
9
|
+
* reports as UNKNOWN. All judgement lives in `resolveDoctorPluginRuntime`; nothing is decided here.
|
|
10
|
+
*/
|
|
11
|
+
import { resolveDoctorPluginRuntime } from "./herdr-activation.mjs";
|
|
12
|
+
|
|
13
|
+
const [repo, override] = process.argv.slice(2);
|
|
14
|
+
if (!repo) {
|
|
15
|
+
process.stderr.write("usage: herdr-doctor-mode.mjs <repo> [<plugin-runtime>]\n");
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
let root;
|
|
19
|
+
try {
|
|
20
|
+
root = resolveDoctorPluginRuntime(process.env, { repo, override });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err?.name !== "ActivationError") throw err;
|
|
23
|
+
process.stderr.write(`${err.message}\n`);
|
|
24
|
+
process.exit(3);
|
|
25
|
+
}
|
|
26
|
+
if (root !== null) process.stdout.write(`${root}\n`);
|
|
@@ -33,11 +33,11 @@ import {
|
|
|
33
33
|
certifyRootsAgainstLedger,
|
|
34
34
|
componentStates,
|
|
35
35
|
ledgerBody,
|
|
36
|
+
piStatePaths,
|
|
36
37
|
planActivation,
|
|
37
38
|
readCertifiedLedger,
|
|
38
39
|
resolveActivationLayout,
|
|
39
40
|
resolveComponentRoots,
|
|
40
|
-
resolveEntwurfDataRoot,
|
|
41
41
|
writeLedger,
|
|
42
42
|
} from "./herdr-activation.mjs";
|
|
43
43
|
import {
|
|
@@ -49,8 +49,109 @@ import {
|
|
|
49
49
|
|
|
50
50
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
51
51
|
|
|
52
|
-
function run(command, args, env) {
|
|
53
|
-
return
|
|
52
|
+
function run(command, args, env, spawn = spawnSync) {
|
|
53
|
+
return spawn(command, args, { encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ask every selected component's OWN ownership authority, read-only, whether a write for
|
|
58
|
+
* `packageRoot` would be admitted. Returns the refused `[name, result]` pairs; writes nothing.
|
|
59
|
+
*
|
|
60
|
+
* TWO CALL POSITIONS, ONE LIST (#135). The installed verb below asks it about itself, after the
|
|
61
|
+
* runtime exists. The Herdr build asks it BEFORE any byte of runtime work, with the checkout's copy
|
|
62
|
+
* of the same owners and the PROSPECTIVE installed root — so an owner the late check would refuse
|
|
63
|
+
* is refused while nothing has been fetched. `scriptRoot` is whose owner scripts run; `packageRoot`
|
|
64
|
+
* is the root they judge. The prospective root must be in the form this verb will later see as its
|
|
65
|
+
* own PACKAGE_ROOT (physical: Node realpaths the ESM entry), because the provider owner records the
|
|
66
|
+
* root without resolving it; `runtimeRoot` stays the lexical stable address the commands are
|
|
67
|
+
* derived from.
|
|
68
|
+
*
|
|
69
|
+
* Claude's preflight is its document/plan-shape check. It does not judge which root owns Claude.
|
|
70
|
+
*/
|
|
71
|
+
export function preflightSelectedComponents(
|
|
72
|
+
env,
|
|
73
|
+
{ scriptRoot, packageRoot, runtimeRoot, selected, spawn = spawnSync },
|
|
74
|
+
) {
|
|
75
|
+
const preflights = [];
|
|
76
|
+
if (selected.includes("claude-code")) {
|
|
77
|
+
preflights.push([
|
|
78
|
+
"claude-code",
|
|
79
|
+
// The mode rides the ARGV, because that is what the state owner reads. The env var is the
|
|
80
|
+
// installer script's channel; passing only that preflights the DEFAULT mode and then
|
|
81
|
+
// installs the plugin one — a green check of work nobody is going to do.
|
|
82
|
+
run(
|
|
83
|
+
"python3",
|
|
84
|
+
[
|
|
85
|
+
path.join(scriptRoot, "scripts", "meta-bridge-state.py"),
|
|
86
|
+
"preflight-install",
|
|
87
|
+
"--repo",
|
|
88
|
+
packageRoot,
|
|
89
|
+
"--plugin-runtime",
|
|
90
|
+
runtimeRoot,
|
|
91
|
+
],
|
|
92
|
+
{ ...env, ENTWURF_PLUGIN_RUNTIME: runtimeRoot },
|
|
93
|
+
spawn,
|
|
94
|
+
),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
if (selected.includes("pi")) {
|
|
98
|
+
const agentSettings = path.join(resolveComponentRoots(env).piAgentDir.path, "settings.json");
|
|
99
|
+
// ONE resolution for both owner records, the one their writers use (`run.sh` `${XDG_DATA_HOME:-…}`).
|
|
100
|
+
const piState = piStatePaths(env);
|
|
101
|
+
// BOTH halves. The user-scope citizen is a package registration and a provider key with
|
|
102
|
+
// SEPARATE ownership records, and a foreign or corrupt package state refuses independently —
|
|
103
|
+
// preflighting only the provider would let the first ledger write land on a host the package
|
|
104
|
+
// writer was always going to refuse.
|
|
105
|
+
preflights.push([
|
|
106
|
+
"pi-package",
|
|
107
|
+
run(
|
|
108
|
+
"python3",
|
|
109
|
+
[
|
|
110
|
+
path.join(scriptRoot, "scripts", "register-pi-package.py"),
|
|
111
|
+
agentSettings,
|
|
112
|
+
packageRoot,
|
|
113
|
+
"--scope",
|
|
114
|
+
"user",
|
|
115
|
+
"--state",
|
|
116
|
+
piState.packageState,
|
|
117
|
+
"--preflight",
|
|
118
|
+
],
|
|
119
|
+
env,
|
|
120
|
+
spawn,
|
|
121
|
+
),
|
|
122
|
+
]);
|
|
123
|
+
preflights.push([
|
|
124
|
+
"pi-provider",
|
|
125
|
+
run(
|
|
126
|
+
"python3",
|
|
127
|
+
[
|
|
128
|
+
path.join(scriptRoot, "scripts", "register-pi-provider.py"),
|
|
129
|
+
"install",
|
|
130
|
+
agentSettings,
|
|
131
|
+
packageRoot,
|
|
132
|
+
"--scope",
|
|
133
|
+
"user",
|
|
134
|
+
"--state",
|
|
135
|
+
piState.providerState,
|
|
136
|
+
"--plugin-runtime",
|
|
137
|
+
runtimeRoot,
|
|
138
|
+
"--preflight",
|
|
139
|
+
],
|
|
140
|
+
env,
|
|
141
|
+
spawn,
|
|
142
|
+
),
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
return preflights.filter(([, r]) => r.status !== 0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The owner's own last line for each refused component, for the operator to act on. */
|
|
149
|
+
export function describeRefusedComponents(refused) {
|
|
150
|
+
return refused
|
|
151
|
+
.map(
|
|
152
|
+
([name, r]) => `${name}: ${(r.stderr || r.stdout || (r.error ? r.error.message : "")).trim().split("\n").pop()}`,
|
|
153
|
+
)
|
|
154
|
+
.join("; ");
|
|
54
155
|
}
|
|
55
156
|
|
|
56
157
|
/** The only input: a subset of P, parsed from argv and refused if it is anything else. */
|
|
@@ -113,79 +214,14 @@ export function activate(env, requested) {
|
|
|
113
214
|
const plan = planActivation({ ledger, requested });
|
|
114
215
|
const selected = [...plan.reconcile, ...plan.add];
|
|
115
216
|
|
|
116
|
-
// 3. preflight EVERY selected component before the first byte
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
run(
|
|
125
|
-
"python3",
|
|
126
|
-
[
|
|
127
|
-
path.join(PACKAGE_ROOT, "scripts", "meta-bridge-state.py"),
|
|
128
|
-
"preflight-install",
|
|
129
|
-
"--repo",
|
|
130
|
-
PACKAGE_ROOT,
|
|
131
|
-
"--plugin-runtime",
|
|
132
|
-
runtime.activeDir,
|
|
133
|
-
],
|
|
134
|
-
{ ...env, ENTWURF_PLUGIN_RUNTIME: runtime.activeDir },
|
|
135
|
-
),
|
|
136
|
-
]);
|
|
137
|
-
}
|
|
138
|
-
if (selected.includes("pi")) {
|
|
139
|
-
const agentSettings = path.join(roots.piAgentDir.path, "settings.json");
|
|
140
|
-
const dataRoot = resolveEntwurfDataRoot(env);
|
|
141
|
-
// BOTH halves. The user-scope citizen is a package registration and a provider key with
|
|
142
|
-
// SEPARATE ownership records, and a foreign or corrupt package state refuses independently —
|
|
143
|
-
// preflighting only the provider would let the first ledger write land on a host the package
|
|
144
|
-
// writer was always going to refuse.
|
|
145
|
-
preflights.push([
|
|
146
|
-
"pi-package",
|
|
147
|
-
run(
|
|
148
|
-
"python3",
|
|
149
|
-
[
|
|
150
|
-
path.join(PACKAGE_ROOT, "scripts", "register-pi-package.py"),
|
|
151
|
-
agentSettings,
|
|
152
|
-
PACKAGE_ROOT,
|
|
153
|
-
"--scope",
|
|
154
|
-
"user",
|
|
155
|
-
"--state",
|
|
156
|
-
path.join(dataRoot, "pi-package", "install-state.json"),
|
|
157
|
-
"--preflight",
|
|
158
|
-
],
|
|
159
|
-
env,
|
|
160
|
-
),
|
|
161
|
-
]);
|
|
162
|
-
preflights.push([
|
|
163
|
-
"pi-provider",
|
|
164
|
-
run(
|
|
165
|
-
"python3",
|
|
166
|
-
[
|
|
167
|
-
path.join(PACKAGE_ROOT, "scripts", "register-pi-provider.py"),
|
|
168
|
-
"install",
|
|
169
|
-
agentSettings,
|
|
170
|
-
PACKAGE_ROOT,
|
|
171
|
-
"--scope",
|
|
172
|
-
"user",
|
|
173
|
-
"--state",
|
|
174
|
-
path.join(
|
|
175
|
-
env.XDG_DATA_HOME ?? path.join(env.HOME, ".local", "share"),
|
|
176
|
-
"entwurf",
|
|
177
|
-
"pi-provider",
|
|
178
|
-
"install-state.json",
|
|
179
|
-
),
|
|
180
|
-
"--plugin-runtime",
|
|
181
|
-
runtime.activeDir,
|
|
182
|
-
"--preflight",
|
|
183
|
-
],
|
|
184
|
-
env,
|
|
185
|
-
),
|
|
186
|
-
]);
|
|
187
|
-
}
|
|
188
|
-
const refused = preflights.filter(([, r]) => r.status !== 0);
|
|
217
|
+
// 3. preflight EVERY selected component before the first byte — the same list the Herdr build
|
|
218
|
+
// already asked about the prospective root; this is the installed recheck.
|
|
219
|
+
const refused = preflightSelectedComponents(env, {
|
|
220
|
+
scriptRoot: PACKAGE_ROOT,
|
|
221
|
+
packageRoot: PACKAGE_ROOT,
|
|
222
|
+
runtimeRoot: runtime.activeDir,
|
|
223
|
+
selected,
|
|
224
|
+
});
|
|
189
225
|
if (refused.length > 0) {
|
|
190
226
|
for (const [name, r] of refused) {
|
|
191
227
|
process.stderr.write(` ${name}: ${(r.stderr || r.stdout || "").trim().split("\n").pop()}\n`);
|
|
@@ -25,6 +25,7 @@ import fs from "node:fs";
|
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
27
|
import {
|
|
28
|
+
ActivationError,
|
|
28
29
|
certifyPhaseForInverse,
|
|
29
30
|
certifyRootsAgainstLedger,
|
|
30
31
|
componentStates,
|
|
@@ -49,6 +50,31 @@ function fail(code, detail) {
|
|
|
49
50
|
process.exit(1);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* What stands at this host's fixed plugin addresses — journal, runtime root, our npm cache — by
|
|
55
|
+
* lstat, without reading or following anything. Only consulted when there is no ledger.
|
|
56
|
+
*/
|
|
57
|
+
function leftoversAtPluginAddresses(runtime) {
|
|
58
|
+
const found = [];
|
|
59
|
+
for (const [label, target] of [
|
|
60
|
+
["journal", runtime.journalPath],
|
|
61
|
+
["runtime", runtime.runtimeRoot],
|
|
62
|
+
["cache", runtime.cacheDir],
|
|
63
|
+
]) {
|
|
64
|
+
const stat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
65
|
+
if (stat === undefined) continue;
|
|
66
|
+
const kind = stat.isSymbolicLink()
|
|
67
|
+
? "symlink"
|
|
68
|
+
: stat.isDirectory()
|
|
69
|
+
? "directory"
|
|
70
|
+
: stat.isFile()
|
|
71
|
+
? "file"
|
|
72
|
+
: "other";
|
|
73
|
+
found.push(`${label}=${kind} (${target})`);
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
export function deactivate(env) {
|
|
53
79
|
const activation = resolveActivationLayout(env);
|
|
54
80
|
const runtime = resolveRuntimeLayout(env);
|
|
@@ -56,8 +82,21 @@ export function deactivate(env) {
|
|
|
56
82
|
|
|
57
83
|
const ledger = readCertifiedLedger(activation);
|
|
58
84
|
if (ledger === null) {
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
// NO LEDGER IS NOT "NOTHING HERE" (#135). An install that stopped before its activation was
|
|
86
|
+
// recorded leaves a journal, a runtime or a cache behind, and saying "nothing to undo" over them
|
|
87
|
+
// was a false success. Without a ledger nothing proves what they belong to or that no harness
|
|
88
|
+
// wiring names them, so this verb removes nothing: it names what it found and exits non-zero.
|
|
89
|
+
const leftovers = leftoversAtPluginAddresses(runtime);
|
|
90
|
+
if (leftovers.length === 0) {
|
|
91
|
+
process.stdout.write("[herdr-plugin-deactivate] no certified activation ledger — nothing to undo\n");
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
throw new ActivationError(
|
|
95
|
+
"deactivate-unattributed-leftovers",
|
|
96
|
+
`no certified activation ledger, yet ${leftovers.join(", ")} — NOTHING was removed. Without an activation ` +
|
|
97
|
+
"ledger nothing proves what these belong to or that no harness wiring still names them; inspect them and " +
|
|
98
|
+
"remove them by hand only if they are yours",
|
|
99
|
+
);
|
|
61
100
|
}
|
|
62
101
|
certifyRootsAgainstLedger(ledger, roots, runtime.activeDir);
|
|
63
102
|
certifyPhaseForInverse(ledger);
|
|
@@ -17,6 +17,18 @@ MKT_NAME="meta-bridge-local"
|
|
|
17
17
|
PLUGIN="entwurf-meta-receive"
|
|
18
18
|
CLAUDE_CFG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
19
19
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
20
|
+
# #136: the one argument. A Herdr plugin activation wires Claude with ABSOLUTE commands under the
|
|
21
|
+
# stable runtime; the mode is normally derived from the activation ledger (see [expectation mode]),
|
|
22
|
+
# and this override exists so a checkout's doctor can name that root. It must agree with the ledger.
|
|
23
|
+
PLUGIN_RUNTIME_OVERRIDE=""
|
|
24
|
+
while [ $# -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--plugin-runtime)
|
|
27
|
+
[ -n "${2:-}" ] || { echo "meta-bridge-doctor: --plugin-runtime requires a value" >&2; exit 2; }
|
|
28
|
+
PLUGIN_RUNTIME_OVERRIDE="$2"; shift 2 ;;
|
|
29
|
+
*) echo "usage: meta-bridge-doctor.sh [--plugin-runtime <stable active runtime root>]" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
20
32
|
# The live artifact lives under the XDG data dir (dev clone and installed package
|
|
21
33
|
# alike — never the checkout). Prefer the path RECORDED in install-state so a doctor
|
|
22
34
|
# run under a different XDG_DATA_HOME than install still hashes the REAL assembled
|
|
@@ -110,6 +122,44 @@ if command -v node >/dev/null; then
|
|
|
110
122
|
if [ "${MJ:-0}" -ge 24 ]; then ok "node $NV (>= 24, supported axis)"; else bad "node $NV too old (need >= 24; there is no Node 22 lane)"; fi
|
|
111
123
|
else bad "node not on PATH"; fi
|
|
112
124
|
|
|
125
|
+
echo "[expectation mode]"
|
|
126
|
+
# #136: which expectations the three desired-state calls below compute. The judgement is the
|
|
127
|
+
# activation owner's (`resolveDoctorPluginRuntime`, herdr-activation.mjs) reached through one narrow
|
|
128
|
+
# call, so no second reader of the ledger exists. A refusal is NOT a fallback to bare expectations:
|
|
129
|
+
# each call site then reports UNKNOWN, and every other section still runs (Hard Rule 13).
|
|
130
|
+
PLUGIN_ARGS=()
|
|
131
|
+
MODE_ERR=""
|
|
132
|
+
if ! command -v node >/dev/null; then
|
|
133
|
+
MODE_ERR="node not on PATH, so the activation ledger could not be read"
|
|
134
|
+
bad "expectation mode is UNKNOWN: $MODE_ERR"
|
|
135
|
+
else
|
|
136
|
+
MODE_STDERR="$(mktemp -t entwurf-doctor-mode.XXXXXX)"
|
|
137
|
+
set +e
|
|
138
|
+
if [ -n "$PLUGIN_RUNTIME_OVERRIDE" ]; then
|
|
139
|
+
MODE_ROOT="$(node "$REPO/scripts/herdr-doctor-mode.mjs" "$REPO" "$PLUGIN_RUNTIME_OVERRIDE" 2>"$MODE_STDERR")"
|
|
140
|
+
else
|
|
141
|
+
MODE_ROOT="$(node "$REPO/scripts/herdr-doctor-mode.mjs" "$REPO" 2>"$MODE_STDERR")"
|
|
142
|
+
fi
|
|
143
|
+
MODE_RC=$?
|
|
144
|
+
set -e
|
|
145
|
+
MODE_DETAIL="$(tr '\n' ' ' < "$MODE_STDERR" | cut -c1-400)"
|
|
146
|
+
rm -f "$MODE_STDERR"
|
|
147
|
+
if [ "$MODE_RC" -eq 3 ]; then
|
|
148
|
+
MODE_ERR="$MODE_DETAIL"
|
|
149
|
+
bad "expectation mode refused: $MODE_ERR"
|
|
150
|
+
elif [ "$MODE_RC" -ne 0 ]; then
|
|
151
|
+
MODE_ERR="herdr-doctor-mode.mjs exited $MODE_RC: $MODE_DETAIL"
|
|
152
|
+
bad "expectation mode is UNKNOWN: $MODE_ERR"
|
|
153
|
+
elif [ -n "$MODE_ROOT" ]; then
|
|
154
|
+
PLUGIN_ARGS=(--plugin-runtime "$MODE_ROOT")
|
|
155
|
+
ok "mode: plugin-runtime $MODE_ROOT (activation ledger)"
|
|
156
|
+
elif [ "$LIB_EXT" = "js" ]; then
|
|
157
|
+
ok "mode: installed (no activation ledger, or this package is not the plugin runtime)"
|
|
158
|
+
else
|
|
159
|
+
ok "mode: clone (this checkout is not the plugin runtime)"
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
|
|
113
163
|
echo "[managed config state]"
|
|
114
164
|
if command -v python3 >/dev/null; then
|
|
115
165
|
# state.py check is the effect-based keyset-survival guard: it asserts every
|
|
@@ -123,7 +173,9 @@ if command -v python3 >/dev/null; then
|
|
|
123
173
|
# which key drifted — silent instead of fail-loud. As an `if` condition the
|
|
124
174
|
# substitution's nonzero status is consumed (set -e suspended), so we always
|
|
125
175
|
# reach the detail. stderr (drift detail) is captured via `2>&1 >/dev/null`.
|
|
126
|
-
if
|
|
176
|
+
if [ -n "$MODE_ERR" ]; then
|
|
177
|
+
bad "managed settings/MCP keyset is UNKNOWN: the expectation mode was not established ($MODE_ERR)"
|
|
178
|
+
elif CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$ASM" ${PLUGIN_ARGS[@]+"${PLUGIN_ARGS[@]}"} 2>&1 >/dev/null)"; then
|
|
127
179
|
ok "state file present and managed settings/MCP keyset survives intact"
|
|
128
180
|
else
|
|
129
181
|
bad "managed settings/MCP keyset missing or overwritten — another consumer may have clobbered a pi-owned key. Re-run ./run.sh install-meta-bridge. Drift detail:"
|
|
@@ -565,25 +617,29 @@ except Exception:
|
|
|
565
617
|
print('')
|
|
566
618
|
PY
|
|
567
619
|
)"
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if [[ "$EXPECTED_STATUSLINE" == */* ]]; then
|
|
571
|
-
if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
|
|
572
|
-
elif command -v "$EXPECTED_STATUSLINE" >/dev/null 2>&1; then
|
|
573
|
-
ok "statusline bin resolves on PATH: $(command -v "$EXPECTED_STATUSLINE")"
|
|
620
|
+
if [ -n "$MODE_ERR" ]; then
|
|
621
|
+
bad "statusLine expectation is UNKNOWN: the expectation mode was not established ($MODE_ERR)"
|
|
574
622
|
else
|
|
575
|
-
|
|
623
|
+
EXPECTED_STATUSLINE="$(python3 "$REPO/scripts/meta-bridge-state.py" desired-statusline --repo "$REPO" ${PLUGIN_ARGS[@]+"${PLUGIN_ARGS[@]}"} | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command", ""))')"
|
|
624
|
+
if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command matches expected (dev: checkout path; installed: entwurf-statusline bin shim): $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
|
|
625
|
+
if [[ "$EXPECTED_STATUSLINE" == */* ]]; then
|
|
626
|
+
if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
|
|
627
|
+
elif command -v "$EXPECTED_STATUSLINE" >/dev/null 2>&1; then
|
|
628
|
+
ok "statusline bin resolves on PATH: $(command -v "$EXPECTED_STATUSLINE")"
|
|
629
|
+
else
|
|
630
|
+
bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
|
|
631
|
+
fi
|
|
632
|
+
SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
|
|
633
|
+
# The identity row may carry the #98 B unread badge between `cc` and the model letter:
|
|
634
|
+
# ` ✉N` when mail is waiting, nothing when the mailbox is empty, ` ✉?` when the count
|
|
635
|
+
# could not be taken. All three are healthy renders, so the shape test admits any of them.
|
|
636
|
+
if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -qE ' cc( ✉([0-9]+|\?))? \| s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
|
|
637
|
+
# The synthetic feeds a session_id with no meta-record, so the garden id does not
|
|
638
|
+
# resolve and there is no mailbox to count. `✉?` is the required answer: a statusline
|
|
639
|
+
# that drew nothing there would be claiming "no mail" about a mailbox it never opened —
|
|
640
|
+
# the exact silence #98 was opened to remove.
|
|
641
|
+
if printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '✉?'; then ok "statusline badge distinguishes 'could not count' (✉?) from zero on the no-record synthetic"; else bad "statusline badge omitted ✉? for the no-record synthetic (a false zero would be indistinguishable from an empty mailbox)"; fi
|
|
576
642
|
fi
|
|
577
|
-
SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
|
|
578
|
-
# The identity row may carry the #98 B unread badge between `cc` and the model letter:
|
|
579
|
-
# ` ✉N` when mail is waiting, nothing when the mailbox is empty, ` ✉?` when the count
|
|
580
|
-
# could not be taken. All three are healthy renders, so the shape test admits any of them.
|
|
581
|
-
if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -qE ' cc( ✉([0-9]+|\?))? \| s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
|
|
582
|
-
# The synthetic feeds a session_id with no meta-record, so the garden id does not
|
|
583
|
-
# resolve and there is no mailbox to count. `✉?` is the required answer: a statusline
|
|
584
|
-
# that drew nothing there would be claiming "no mail" about a mailbox it never opened —
|
|
585
|
-
# the exact silence #98 was opened to remove.
|
|
586
|
-
if printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '✉?'; then ok "statusline badge distinguishes 'could not count' (✉?) from zero on the no-record synthetic"; else bad "statusline badge omitted ✉? for the no-record synthetic (a false zero would be indistinguishable from an empty mailbox)"; fi
|
|
587
643
|
else
|
|
588
644
|
bad "cannot validate statusline without python3"
|
|
589
645
|
fi
|
|
@@ -669,9 +725,12 @@ PY
|
|
|
669
725
|
[ -n "$LIVE_MCP_DESC" ] || LIVE_MCP_DESC="(unprintable live command)"
|
|
670
726
|
# Ownership axis. `|| true` on the pipeline only: a state.py failure must read as
|
|
671
727
|
# UNKNOWN-and-loud below, never abort the doctor before the delivery drive.
|
|
672
|
-
DESIRED_MCP="
|
|
728
|
+
DESIRED_MCP=""
|
|
729
|
+
[ -n "$MODE_ERR" ] || DESIRED_MCP="$( { python3 "$REPO/scripts/meta-bridge-state.py" desired-mcp --repo "$REPO" ${PLUGIN_ARGS[@]+"${PLUGIN_ARGS[@]}"} \
|
|
673
730
|
| python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin), sort_keys=True))'; } 2>/dev/null || true)"
|
|
674
|
-
if [ -
|
|
731
|
+
if [ -n "$MODE_ERR" ]; then
|
|
732
|
+
bad "live-vs-desired MCP ownership is UNKNOWN: the expectation mode was not established ($MODE_ERR)"
|
|
733
|
+
elif [ -z "$DESIRED_MCP" ]; then
|
|
675
734
|
bad "could not compute the desired MCP entry (meta-bridge-state.py desired-mcp failed) — live-vs-desired ownership is UNKNOWN"
|
|
676
735
|
elif [ "$LIVE_MCP" = "$DESIRED_MCP" ]; then
|
|
677
736
|
ok "live mcpServers.entwurf-bridge equals desired_mcp() for this mode (installed: bin shim; clone: this checkout's start.sh)"
|
|
@@ -316,6 +316,112 @@
|
|
|
316
316
|
"timeoutSeconds": 420,
|
|
317
317
|
"signature": "[QK:HAC-REBIND-ATOMIC-CHECKPOINT]",
|
|
318
318
|
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"claim": "HAC-PI-STATE-PATHS-SHARED",
|
|
322
|
+
"title": "resolving the provider owner record with `??` turns an empty XDG_DATA_HOME into a relative path no writer uses, so the preflight sees no owner and the ledger lands on a host the provider writer then refuses",
|
|
323
|
+
"subject": "scripts/herdr-plugin-activate.mjs",
|
|
324
|
+
"find": ["\t\tconst piState = piStatePaths(env);"],
|
|
325
|
+
"replace": [
|
|
326
|
+
"\t\tconst piState = { packageState: piStatePaths(env).packageState, providerState: path.join(env.XDG_DATA_HOME ?? path.join(env.HOME, \".local\", \"share\"), \"entwurf\", \"pi-provider\", \"install-state.json\") };"
|
|
327
|
+
],
|
|
328
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
329
|
+
"timeoutSeconds": 300,
|
|
330
|
+
"signature": "[QK:HAC-PI-STATE-PATHS-SHARED]",
|
|
331
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"claim": "HAC-NO-LEDGER-LEFTOVERS-REPORTED",
|
|
335
|
+
"title": "answering 'nothing to undo' whenever the ledger is absent is a false success over the journal, runtime or cache an aborted install left behind",
|
|
336
|
+
"subject": "scripts/herdr-plugin-deactivate.mjs",
|
|
337
|
+
"find": ["\t\tif (leftovers.length === 0) {"],
|
|
338
|
+
"replace": ["\t\tif (true) {"],
|
|
339
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
340
|
+
"timeoutSeconds": 300,
|
|
341
|
+
"signature": "[QK:HAC-NO-LEDGER-LEFTOVERS-REPORTED]",
|
|
342
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"claim": "HAC-LEDGER-PATH-KIND",
|
|
346
|
+
"title": "reading the ledger without lstat lets a dangling symlink answer ENOENT, i.e. 'no ledger', and every verb then acts as if nothing had ever been activated",
|
|
347
|
+
"subject": "scripts/herdr-activation.mjs",
|
|
348
|
+
"find": ["\tif (!kind.isFile()) {"],
|
|
349
|
+
"replace": ["\tif (false) {"],
|
|
350
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
351
|
+
"timeoutSeconds": 300,
|
|
352
|
+
"signature": "[QK:HAC-LEDGER-PATH-KIND]",
|
|
353
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"claim": "DOCTOR-MODE-DERIVED",
|
|
357
|
+
"title": "a resolver that never answers plugin mode leaves the doctor computing bare-bin expectations on a plugin-activated host, so the activation's correct absolute wiring is reported as drift and the supported route can never be certified (#136)",
|
|
358
|
+
"subject": "scripts/herdr-activation.mjs",
|
|
359
|
+
"find": ["\treturn activeDir;\n}"],
|
|
360
|
+
"replace": ["\treturn null;\n}"],
|
|
361
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
362
|
+
"timeoutSeconds": 300,
|
|
363
|
+
"signature": "[QK:DOCTOR-MODE-DERIVED]",
|
|
364
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"claim": "DOCTOR-MODE-CHECKOUT-NEVER-DERIVES",
|
|
368
|
+
"title": "dropping the runtime-package precondition lets a checkout's doctor read the plugin ledger and adopt its absolute wiring as its own expectations, so the checkout certifies a runtime it does not own (#136)",
|
|
369
|
+
"subject": "scripts/herdr-activation.mjs",
|
|
370
|
+
"find": ["\t\tif (!same) return null;"],
|
|
371
|
+
"replace": ["\t\tvoid same;"],
|
|
372
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
373
|
+
"timeoutSeconds": 300,
|
|
374
|
+
"signature": "[QK:DOCTOR-MODE-CHECKOUT-NEVER-DERIVES]",
|
|
375
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"claim": "DOCTOR-MODE-UNCERTIFIED-DIES",
|
|
379
|
+
"title": "swallowing the reader's refusal turns an uncertified ledger into 'not plugin mode', so a doctor silently falls back to bare expectations over a record nobody can vouch for (#136)",
|
|
380
|
+
"subject": "scripts/herdr-activation.mjs",
|
|
381
|
+
"find": [
|
|
382
|
+
"\tconst ledger = readCertifiedLedger(resolveActivationLayout(env));\n\tif (ledger === null) {\n\t\tif (override === undefined) return null;"
|
|
383
|
+
],
|
|
384
|
+
"replace": [
|
|
385
|
+
"\tlet ledger = null;\n\ttry {\n\t\tledger = readCertifiedLedger(resolveActivationLayout(env));\n\t} catch {\n\t\tledger = null;\n\t}\n\tif (ledger === null) {\n\t\tif (override === undefined || override !== undefined) return null;"
|
|
386
|
+
],
|
|
387
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
388
|
+
"timeoutSeconds": 300,
|
|
389
|
+
"signature": "[QK:DOCTOR-MODE-UNCERTIFIED-DIES]",
|
|
390
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"claim": "DOCTOR-MODE-OVERRIDE-AGREES",
|
|
394
|
+
"title": "an override that need not agree with the ledger lets any caller-named root become the doctor's expectation, so the doctor would certify absolute commands pointing anywhere (#136)",
|
|
395
|
+
"subject": "scripts/herdr-activation.mjs",
|
|
396
|
+
"find": ["\tif (override !== undefined && override !== activeDir) {"],
|
|
397
|
+
"replace": ["\tif (false) {"],
|
|
398
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
399
|
+
"timeoutSeconds": 300,
|
|
400
|
+
"signature": "[QK:DOCTOR-MODE-OVERRIDE-AGREES]",
|
|
401
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"claim": "DOCTOR-PLUGIN-MODE-GREEN",
|
|
405
|
+
"title": "a doctor that resolves plugin mode but never passes it to its desired-state calls still expects bare bins, so the plugin activation's absolute wiring stays falsely red (#136). One mutant for the shared wire on purpose: omitting the mode at any single one of the three call sites also turns P1 red, but no per-site mutant proves that",
|
|
406
|
+
"subject": "scripts/meta-bridge-doctor.sh",
|
|
407
|
+
"find": [" PLUGIN_ARGS=(--plugin-runtime \"$MODE_ROOT\")"],
|
|
408
|
+
"replace": [" : \"$MODE_ROOT\""],
|
|
409
|
+
"gate": ["bash", "run.sh", "check-meta-doctor-oracle"],
|
|
410
|
+
"timeoutSeconds": 600,
|
|
411
|
+
"signature": "[QK:DOCTOR-PLUGIN-MODE-GREEN]",
|
|
412
|
+
"signatureSource": "scripts/check-meta-doctor-oracle.sh",
|
|
413
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
"claim": "DOCTOR-MODE-ADMISSION-CLAUSES",
|
|
417
|
+
"title": "dropping the phase clause lets a ledger caught mid-activation or mid-teardown admit plugin mode, so the doctor certifies absolute wiring the activation has not finished writing or is already removing (#136)",
|
|
418
|
+
"subject": "scripts/herdr-activation.mjs",
|
|
419
|
+
"find": ["\tif (ledger.phase !== \"active\") refuse(`activation ledger phase is ${ledger.phase}, not active`);"],
|
|
420
|
+
"replace": ["\tvoid refuse;"],
|
|
421
|
+
"gate": ["bash", "run.sh", "check-herdr-activation"],
|
|
422
|
+
"timeoutSeconds": 300,
|
|
423
|
+
"signature": "[QK:DOCTOR-MODE-ADMISSION-CLAUSES]",
|
|
424
|
+
"signatureSource": "scripts/check-herdr-activation.ts"
|
|
319
425
|
}
|
|
320
426
|
]
|
|
321
427
|
}
|