@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.
@@ -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 spawnSync(command, args, { encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
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
- const preflights = [];
118
- if (selected.includes("claude-code")) {
119
- preflights.push([
120
- "claude-code",
121
- // The mode rides the ARGV, because that is what the state owner reads. The env var is the
122
- // installer script's channel; passing only that preflights the DEFAULT mode and then
123
- // installs the plugin one — a green check of work nobody is going to do.
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
- process.stdout.write("[herdr-plugin-deactivate] no certified activation ledger — nothing to undo\n");
60
- return 0;
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 CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$ASM" 2>&1 >/dev/null)"; then
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
- EXPECTED_STATUSLINE="$(python3 "$REPO/scripts/meta-bridge-state.py" desired-statusline --repo "$REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command", ""))')"
569
- 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
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
- bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
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="$( { python3 "$REPO/scripts/meta-bridge-state.py" desired-mcp --repo "$REPO" \
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 [ -z "$DESIRED_MCP" ]; then
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
  }