@junghanacs/entwurf 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smoke-herdr-plugin-build-live — the #116 M3-b3 journey a real Herdr user performs, with a REAL
|
|
3
|
+
* `herdr plugin install` driving our REAL `[[build]]` and a REAL `npm pack` of the product git spec.
|
|
4
|
+
*
|
|
5
|
+
* OUT of `pnpm check`. Needs `LIVE=1`, a `herdr` binary, `git`, `npm` — and the NETWORK, because a
|
|
6
|
+
* git-spec pack builds the bridge through `prepare`, which installs devDependencies from the
|
|
7
|
+
* registry. That is the axis `check-herdr-plugin-build` cannot have and why it does not claim it.
|
|
8
|
+
*
|
|
9
|
+
* WHAT MAKES IT HERMETIC ANYWAY. Two substitutions, and only two. HOME/XDG are a mkdtemp sandbox,
|
|
10
|
+
* so Herdr's registry, its managed checkout, our runtime, our ledger and both harnesses' wiring all
|
|
11
|
+
* land inside it. And the FIXED product remote — `https://github.com/junghan0611/entwurf.git`, which
|
|
12
|
+
* neither this smoke nor any caller can change — is redirected through git's own `insteadOf`, in the
|
|
13
|
+
* sandbox `.gitconfig`, to a bare clone of a CANDIDATE SNAPSHOT built inside the sandbox (see the
|
|
14
|
+
* block below: a bare clone of the worktree would carry only the last commit, which is how this
|
|
15
|
+
* fixture first passed while testing the wrong bytes). So the argv under test is byte-for-byte the
|
|
16
|
+
* product argv; what differs is where git looks, which is the one substitution that keeps an
|
|
17
|
+
* unpushed candidate testable. This repo's HEAD, its porcelain status and the operator's own herdr
|
|
18
|
+
* registry are all read before and after and must be identical.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS REAL VENDOR EVIDENCE HERE. If `claude` is on PATH, the Claude activation runs the vendor
|
|
21
|
+
* CLI for real against the sandbox HOME — which is the named LIVE boundary the deterministic gates
|
|
22
|
+
* refuse to claim: whether Claude Code accepts an ABSOLUTE executable as an MCP command. A host
|
|
23
|
+
* without `claude` reports that cell as a named skip inside an otherwise green run, never a pass.
|
|
24
|
+
*
|
|
25
|
+
* THE FOUR CELLS, AND THEY ARE NOT ALL THE SAME KIND OF EVIDENCE:
|
|
26
|
+
*
|
|
27
|
+
* | cell | what drives it | what it is evidence of |
|
|
28
|
+
* |---|---|---|
|
|
29
|
+
* | 1. remote-commit AVAILABLE | real `herdr plugin install` | the Herdr journey: one install, runtime at the stable root keyed by the commit Herdr resolved, Pi wired at the absolute bridge, ledger `active` |
|
|
30
|
+
* | 2. REINSTALL widening | real `herdr plugin install` | the Herdr journey: Pi reconciled JSON-identical, Claude added as exactly ONE counted owner entry (real vendor CLI), OpenCode given zero Entwurf state |
|
|
31
|
+
* | 3. remote-commit UNAVAILABLE | the shipped runtime leaf, called directly with the FIXED product argv | acquisition-leaf evidence only — a remote that cannot serve a commit. Routing it through Herdr would add a second explanation for one red, so it does not, and it does not call itself a journey |
|
|
32
|
+
* | 4. POST-BUILD GAP | real `herdr plugin install` | the Herdr journey: a build that mutates the manifest makes HERDR's own commit fail after our runner already succeeded, so Herdr keeps the previous registration while the runtime and the rebound activation REMAIN, and the next normal install re-binds to HEAD. Nothing here calls that atomic |
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import assert from "node:assert/strict";
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import fs from "node:fs";
|
|
38
|
+
import os from "node:os";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
41
|
+
import { treeDigest } from "./lib/tree-digest.ts";
|
|
42
|
+
|
|
43
|
+
const LABEL = "smoke-herdr-plugin-build-live";
|
|
44
|
+
const REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
45
|
+
const PACKAGE = "@junghanacs/entwurf";
|
|
46
|
+
const PRODUCT_REMOTE = "https://github.com/junghan0611/entwurf.git";
|
|
47
|
+
const PLUGIN_SPEC = "junghan0611/entwurf/plugins/herdr";
|
|
48
|
+
const PLUGIN_ID = "junghan0611.entwurf";
|
|
49
|
+
|
|
50
|
+
let passed = 0;
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
function ok(label: string, cond: boolean): void {
|
|
53
|
+
assert.ok(cond, label);
|
|
54
|
+
console.log(` ok ${label}`);
|
|
55
|
+
passed++;
|
|
56
|
+
}
|
|
57
|
+
function skip(label: string, why: string): void {
|
|
58
|
+
console.log(` skip ${label} — ${why}`);
|
|
59
|
+
skipped++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function run(bin: string, argv: string[], env: NodeJS.ProcessEnv, cwd = REPO) {
|
|
63
|
+
return spawnSync(bin, argv, { encoding: "utf8", env, cwd, timeout: 900_000 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (process.env.LIVE !== "1") {
|
|
67
|
+
skipLive(
|
|
68
|
+
LABEL,
|
|
69
|
+
"LIVE=1 not set — this smoke runs a real `herdr plugin install` and a real npm pack over the network",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
for (const bin of ["herdr", "git", "npm"]) {
|
|
73
|
+
const probe = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
74
|
+
if (probe.error || probe.status !== 0) skipLive(LABEL, `\`${bin}\` is not usable on this host`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hpbl-"));
|
|
78
|
+
const HOME = path.join(ROOT, "home");
|
|
79
|
+
const MIRROR = path.join(ROOT, "entwurf.git");
|
|
80
|
+
for (const d of ["config", "data", "state", "cache", ".pi", ".claude", ".config/opencode"]) {
|
|
81
|
+
fs.mkdirSync(path.join(HOME, d), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
const env: NodeJS.ProcessEnv = {
|
|
84
|
+
PATH: process.env.PATH,
|
|
85
|
+
TERM: "dumb",
|
|
86
|
+
HOME,
|
|
87
|
+
XDG_CONFIG_HOME: path.join(HOME, "config"),
|
|
88
|
+
XDG_DATA_HOME: path.join(HOME, "data"),
|
|
89
|
+
XDG_STATE_HOME: path.join(HOME, "state"),
|
|
90
|
+
XDG_CACHE_HOME: path.join(HOME, "cache"),
|
|
91
|
+
PI_CODING_AGENT_DIR: path.join(HOME, ".pi", "agent"),
|
|
92
|
+
CLAUDE_CONFIG_DIR: path.join(HOME, ".claude"),
|
|
93
|
+
GIT_CONFIG_GLOBAL: path.join(HOME, ".gitconfig"),
|
|
94
|
+
};
|
|
95
|
+
fs.mkdirSync(env.PI_CODING_AGENT_DIR as string, { recursive: true });
|
|
96
|
+
fs.writeFileSync(path.join(env.PI_CODING_AGENT_DIR as string, "settings.json"), '{"theme":"dark"}\n');
|
|
97
|
+
|
|
98
|
+
// ── COMMITS RUN UNDER THE OPERATOR'S OWN GIT CONFIG, HOOKS INCLUDED ────────────────────────
|
|
99
|
+
//
|
|
100
|
+
// The sandbox `GIT_CONFIG_GLOBAL` above is what keeps herdr's clones and our `insteadOf` out of the
|
|
101
|
+
// operator's config — but handing it to a `git commit` HIDES their global
|
|
102
|
+
// `core.hooksPath`, which is the safety rail that scans staged diffs for identity terms and
|
|
103
|
+
// secrets. A fixture that commits without it has bypassed that rail by omission, which is worse
|
|
104
|
+
// than bypassing it on purpose: nothing says so. So the two commits below run under the process's
|
|
105
|
+
// NORMAL environment — no sandbox config, no `--no-verify`, no `AGENT_ALLOW_UNSAFE_COMMIT`, and no
|
|
106
|
+
// argv or env that sets, clears or redirects `core.hooksPath`. The only `-c` overrides are the
|
|
107
|
+
// repo-local author identity, because a temp clone has no reason to inherit one.
|
|
108
|
+
const COMMIT_ENV: NodeJS.ProcessEnv = { ...process.env };
|
|
109
|
+
const configuredHooksPath = (
|
|
110
|
+
spawnSync("git", ["config", "--global", "--get", "core.hooksPath"], { encoding: "utf8", env: COMMIT_ENV }).stdout ??
|
|
111
|
+
""
|
|
112
|
+
).trim();
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One ordinary commit, with its hook invocation OBSERVED rather than steered: `GIT_TRACE2_EVENT`
|
|
116
|
+
* writes git's own child-process events to a sandbox file, and the assertion reads that file for a
|
|
117
|
+
* `pre-commit` child under the configured hooks path. Trace is read-only — it cannot change which
|
|
118
|
+
* hook git chooses — so this proves the rail ran instead of asserting that we did not disable it.
|
|
119
|
+
*/
|
|
120
|
+
function commitInClone(dir: string, message: string, tag: string): { hookTrace: string; traceFile: string } {
|
|
121
|
+
const traceFile = path.join(ROOT, `git-trace-${tag}.json`);
|
|
122
|
+
const committed = spawnSync(
|
|
123
|
+
"git",
|
|
124
|
+
[
|
|
125
|
+
"-C",
|
|
126
|
+
dir,
|
|
127
|
+
"-c",
|
|
128
|
+
"user.email=candidate@sandbox.local",
|
|
129
|
+
"-c",
|
|
130
|
+
"user.name=candidate snapshot",
|
|
131
|
+
"commit",
|
|
132
|
+
"--quiet",
|
|
133
|
+
"-a",
|
|
134
|
+
"-m",
|
|
135
|
+
message,
|
|
136
|
+
],
|
|
137
|
+
{ encoding: "utf8", env: { ...COMMIT_ENV, GIT_TRACE2_EVENT: traceFile } },
|
|
138
|
+
);
|
|
139
|
+
assert.equal(committed.status, 0, `${tag} commit failed: ${committed.stderr || committed.stdout}`);
|
|
140
|
+
const trace = fs.existsSync(traceFile) ? fs.readFileSync(traceFile, "utf8") : "";
|
|
141
|
+
const hookLines = trace.split("\n").filter((line) => line.includes('"pre-commit"') && line.includes('"child_start"'));
|
|
142
|
+
if (configuredHooksPath === "") {
|
|
143
|
+
console.log(` info ${tag}: no global core.hooksPath on this host — portable behaviour, no hook receipt to read`);
|
|
144
|
+
return { hookTrace: "no-configured-hook-path", traceFile };
|
|
145
|
+
}
|
|
146
|
+
// EXACTLY ONE. `> 0` would also pass a commit that ran the rail twice, and two invocations mean
|
|
147
|
+
// one of them is reading a state the other already changed — the count is part of the claim, so
|
|
148
|
+
// it is asserted rather than reported.
|
|
149
|
+
const underConfigured = hookLines.filter((line) => line.includes(configuredHooksPath));
|
|
150
|
+
assert.equal(
|
|
151
|
+
underConfigured.length,
|
|
152
|
+
1,
|
|
153
|
+
`${tag}: expected EXACTLY ONE pre-commit child under the configured hook path ${configuredHooksPath}, observed ` +
|
|
154
|
+
`${underConfigured.length} (pre-commit child_start lines in ${traceFile}: ${hookLines.length})`,
|
|
155
|
+
);
|
|
156
|
+
return {
|
|
157
|
+
hookTrace: `exactly ${underConfigured.length} pre-commit child under ${configuredHooksPath}`,
|
|
158
|
+
traceFile,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A clone that a commit has just finished with: nothing staged, nothing modified, nothing untracked. */
|
|
163
|
+
function assertExactlyClean(dir: string, tag: string): void {
|
|
164
|
+
const porcelain = (
|
|
165
|
+
spawnSync("git", ["-C", dir, "status", "--porcelain"], { encoding: "utf8", env }).stdout ?? ""
|
|
166
|
+
).trim();
|
|
167
|
+
const diff = spawnSync("git", ["-C", dir, "diff", "--binary", "HEAD"], { encoding: "utf8", env }).stdout ?? "";
|
|
168
|
+
assert.equal(
|
|
169
|
+
porcelain,
|
|
170
|
+
"",
|
|
171
|
+
`${tag} is not clean after its commit — a hook or the commit itself left changes: ${porcelain}`,
|
|
172
|
+
);
|
|
173
|
+
assert.equal(
|
|
174
|
+
diff.length,
|
|
175
|
+
0,
|
|
176
|
+
`${tag} still differs from its own HEAD after committing (${diff.length} bytes of diff)`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── THE CANDIDATE HAS TO BE IN THE MIRROR, AND A BARE CLONE DOES NOT PUT IT THERE ──────────
|
|
181
|
+
//
|
|
182
|
+
// Measured on this smoke's first run: `git clone --bare <repo>` copies COMMITS, so a frozen
|
|
183
|
+
// uncommitted candidate — 22 modified files and 5 new ones sitting in the operator's index and
|
|
184
|
+
// worktree — never reached Herdr at all. `herdr plugin install` reported status 0 while installing
|
|
185
|
+
// the PREVIOUS commit, whose manifest has no `[[build]]`, and every candidate assertion below was
|
|
186
|
+
// absent rather than wrong. A fixture that quietly tests the wrong bytes is worse than a red.
|
|
187
|
+
//
|
|
188
|
+
// So: clone the operator repo into the sandbox, replay its `git diff --binary HEAD` (the exact
|
|
189
|
+
// index+worktree bytes) into that clone, and commit it there with an ORDINARY commit under the
|
|
190
|
+
// sandbox `GIT_CONFIG_GLOBAL` — no `--no-verify`, no hook path games. The bare MIRROR is then a
|
|
191
|
+
// clone of THAT snapshot. Post-commit (an empty diff) the same code runs with no snapshot commit at
|
|
192
|
+
// all, which is the shape this will have once the candidate lands.
|
|
193
|
+
//
|
|
194
|
+
// UNTRACKED FILES ARE A HARD FAILURE, not a silent omission: `git diff HEAD` cannot see them, so a
|
|
195
|
+
// candidate byte living only in an untracked file would be dropped exactly the way the whole
|
|
196
|
+
// candidate just was.
|
|
197
|
+
const operatorStatusBefore = (run("git", ["status", "--porcelain"], env).stdout ?? "").trim();
|
|
198
|
+
const operatorHeadBefore = (run("git", ["rev-parse", "HEAD"], env).stdout ?? "").trim();
|
|
199
|
+
const untracked = operatorStatusBefore.split("\n").filter((line) => line.startsWith("??"));
|
|
200
|
+
assert.equal(
|
|
201
|
+
untracked.length,
|
|
202
|
+
0,
|
|
203
|
+
`the candidate carries untracked files, which \`git diff HEAD\` cannot capture — stage them (git add -N) ` +
|
|
204
|
+
`so this fixture tests the candidate and not the last commit: ${untracked.join(", ")}`,
|
|
205
|
+
);
|
|
206
|
+
const SNAPSHOT = path.join(ROOT, "snapshot");
|
|
207
|
+
assert.equal(run("git", ["clone", "--quiet", REPO, SNAPSHOT], env).status, 0, "candidate clone failed");
|
|
208
|
+
const candidatePatch = run("git", ["diff", "--binary", "HEAD"], env).stdout ?? "";
|
|
209
|
+
let snapshotKind = "operator HEAD (no uncommitted candidate)";
|
|
210
|
+
let snapshotHook = "no snapshot commit was needed";
|
|
211
|
+
if (candidatePatch.trim().length > 0) {
|
|
212
|
+
const patchFile = path.join(ROOT, "candidate.patch");
|
|
213
|
+
fs.writeFileSync(patchFile, candidatePatch);
|
|
214
|
+
const applied = run("git", ["-C", SNAPSHOT, "apply", "--index", patchFile], env);
|
|
215
|
+
assert.equal(applied.status, 0, `the candidate patch did not apply to the snapshot clone: ${applied.stderr}`);
|
|
216
|
+
snapshotHook = commitInClone(SNAPSHOT, "candidate snapshot under test", "snapshot").hookTrace;
|
|
217
|
+
// LOAD-BEARING, before the mirror is made from it: if a hook rewrote a file, or the patch left
|
|
218
|
+
// anything behind, the mirror would carry bytes the candidate does not have.
|
|
219
|
+
assertExactlyClean(SNAPSHOT, "the candidate snapshot clone");
|
|
220
|
+
snapshotKind = "snapshot commit of the frozen candidate";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// The ONE substitution: the fixed product remote resolves to a bare clone of that snapshot.
|
|
224
|
+
const mirrorClone = run("git", ["clone", "--bare", "--quiet", SNAPSHOT, MIRROR], env);
|
|
225
|
+
assert.equal(mirrorClone.status, 0, `bare clone failed: ${mirrorClone.stderr}`);
|
|
226
|
+
fs.writeFileSync(
|
|
227
|
+
path.join(HOME, ".gitconfig"),
|
|
228
|
+
[`[url "file://${MIRROR}"]`, `\tinsteadOf = ${PRODUCT_REMOTE}`, "[safe]", "\tdirectory = *", ""].join("\n"),
|
|
229
|
+
);
|
|
230
|
+
// HEAD/BRANCH come from what the MIRROR actually serves, never from the operator repo: the whole
|
|
231
|
+
// failure above was the gap between those two.
|
|
232
|
+
const HEAD = (run("git", ["-C", SNAPSHOT, "rev-parse", "HEAD"], env).stdout ?? "").trim();
|
|
233
|
+
const BRANCH = (run("git", ["-C", SNAPSHOT, "rev-parse", "--abbrev-ref", "HEAD"], env).stdout ?? "").trim();
|
|
234
|
+
const mirrorCommit = (run("git", ["--git-dir", MIRROR, "rev-parse", BRANCH], env).stdout ?? "").trim();
|
|
235
|
+
assert.equal(mirrorCommit, HEAD, `the mirror serves ${mirrorCommit} for ${BRANCH}, not the candidate ${HEAD}`);
|
|
236
|
+
console.log(
|
|
237
|
+
` info candidate under test: ${HEAD.slice(0, 12)} on ${BRANCH} — ${snapshotKind}; safety hooks: ${snapshotHook}`,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const operatorBefore = spawnSync("herdr", ["plugin", "list"], { encoding: "utf8" }).stdout ?? "";
|
|
241
|
+
|
|
242
|
+
const activeDir = path.join(env.XDG_DATA_HOME as string, "entwurf", "herdr-plugin", "runtime", "active");
|
|
243
|
+
const journalPath = path.join(env.XDG_DATA_HOME as string, "entwurf", "herdr-plugin", "journal.json");
|
|
244
|
+
const ledgerPath = path.join(env.XDG_STATE_HOME as string, "entwurf", "herdr-plugin", "activation.json");
|
|
245
|
+
const piSettings = path.join(env.PI_CODING_AGENT_DIR as string, "settings.json");
|
|
246
|
+
const opencodeDir = path.join(HOME, ".config", "opencode");
|
|
247
|
+
fs.writeFileSync(path.join(opencodeDir, "config.json"), '{"theme":"tokyonight"}\n');
|
|
248
|
+
const opencodeBefore = fs.readFileSync(path.join(opencodeDir, "config.json"), "utf8");
|
|
249
|
+
|
|
250
|
+
function readJson(file: string): Record<string, unknown> {
|
|
251
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function integrationInstall(atom: string) {
|
|
255
|
+
return run("herdr", ["integration", "install", atom], env);
|
|
256
|
+
}
|
|
257
|
+
function pluginInstall(ref: string) {
|
|
258
|
+
return run("herdr", ["plugin", "install", PLUGIN_SPEC, "--ref", ref, "--yes"], env);
|
|
259
|
+
}
|
|
260
|
+
function managedCheckout(): string | null {
|
|
261
|
+
const base = path.join(env.XDG_CONFIG_HOME as string, "herdr", "plugins", "github");
|
|
262
|
+
if (!fs.existsSync(base)) return null;
|
|
263
|
+
const entry = fs.readdirSync(base).find((e) => e.startsWith(PLUGIN_ID));
|
|
264
|
+
return entry ? path.join(base, entry) : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// ── 1. the remote HAS the commit: one install, {pi} activated ──────────────
|
|
269
|
+
const piIntegration = integrationInstall("pi");
|
|
270
|
+
if (piIntegration.status !== 0) {
|
|
271
|
+
skipLive(
|
|
272
|
+
LABEL,
|
|
273
|
+
`herdr could not admit the pi integration in a sandbox HOME (${(piIntegration.stderr || piIntegration.stdout || "").trim().slice(0, 200)}) — without a selected atom there is no journey to run`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const first = pluginInstall(BRANCH);
|
|
277
|
+
const journal = fs.existsSync(journalPath) ? readJson(journalPath) : null;
|
|
278
|
+
const ledger = fs.existsSync(ledgerPath) ? readJson(ledgerPath) : null;
|
|
279
|
+
const identity = (journal?.artifactIdentity ?? {}) as Record<string, string>;
|
|
280
|
+
const checkout = managedCheckout();
|
|
281
|
+
// The managed checkout's git root is asked for rather than guessed: the manifest root is a
|
|
282
|
+
// SUBDIRECTORY of the clone, and a hardcoded `<dir>/checkout` would silently answer nothing.
|
|
283
|
+
const checkoutRoot =
|
|
284
|
+
checkout === null ? "" : (run("git", ["-C", checkout, "rev-parse", "--show-toplevel"], env).stdout ?? "").trim();
|
|
285
|
+
const checkoutHead =
|
|
286
|
+
checkoutRoot === "" ? "" : (run("git", ["-C", checkoutRoot, "rev-parse", "HEAD"], env).stdout ?? "").trim();
|
|
287
|
+
const shallow =
|
|
288
|
+
checkoutRoot === ""
|
|
289
|
+
? "no-checkout"
|
|
290
|
+
: (run("git", ["-C", checkoutRoot, "rev-parse", "--is-shallow-repository"], env).stdout ?? "").trim();
|
|
291
|
+
const piAfterFirst = readJson(piSettings);
|
|
292
|
+
const provider = (piAfterFirst.entwurfProvider ?? {}) as Record<string, unknown>;
|
|
293
|
+
const providerText = JSON.stringify(provider);
|
|
294
|
+
ok(
|
|
295
|
+
`[${LABEL}] REMOTE-COMMIT AVAILABLE: a real \`herdr plugin install\` drove this plugin's own \`[[build]]\`, which ` +
|
|
296
|
+
"packed the FIXED product git spec at the commit Herdr resolved, installed it at the stable runtime root, and " +
|
|
297
|
+
"activated exactly the integrated set — the whole journey a Herdr user gets for one command, with no Entwurf " +
|
|
298
|
+
"clone and no npm release anywhere in it. THREE commits have to be the same one here — what the mirror " +
|
|
299
|
+
"served, what Herdr checked out, and what the runtime records — because this fixture's first failure was " +
|
|
300
|
+
`exactly the gap between them (install=${first.status} mirror=${mirrorCommit.slice(0, 12)} checkout=${checkoutHead.slice(0, 12)} runtime=${identity.commit?.slice(0, 12)} kind=${identity.kind} package=${identity.packageName}@${identity.packageVersion} ledger=${JSON.stringify(ledger?.activatedBackends)} phase=${ledger?.phase} shallow=${shallow} provider-absolute=${providerText.includes(activeDir)})`,
|
|
301
|
+
first.status === 0 &&
|
|
302
|
+
identity.kind === "herdr-checkout" &&
|
|
303
|
+
identity.commit === HEAD &&
|
|
304
|
+
checkoutHead === HEAD &&
|
|
305
|
+
mirrorCommit === HEAD &&
|
|
306
|
+
identity.repository === "junghan0611/entwurf" &&
|
|
307
|
+
identity.packageName === PACKAGE &&
|
|
308
|
+
(ledger?.phase as string) === "active" &&
|
|
309
|
+
JSON.stringify(ledger?.activatedBackends) === JSON.stringify(["pi"]) &&
|
|
310
|
+
(ledger?.runtimeRoot as string) === activeDir &&
|
|
311
|
+
providerText.includes(activeDir),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// ── 2. the reinstall widens {pi} → {pi, claude-code}, OpenCode untouched ────
|
|
315
|
+
const claudeOnPath = spawnSync("claude", ["--version"], { encoding: "utf8" }).status === 0;
|
|
316
|
+
const claudeIntegration = integrationInstall("claude");
|
|
317
|
+
const opencodeIntegration = integrationInstall("opencode");
|
|
318
|
+
const piBeforeSecond = JSON.stringify(readJson(piSettings));
|
|
319
|
+
const second = pluginInstall(BRANCH);
|
|
320
|
+
const ledger2 = fs.existsSync(ledgerPath) ? readJson(ledgerPath) : null;
|
|
321
|
+
const piAfterSecond = JSON.stringify(readJson(piSettings));
|
|
322
|
+
const opencodeUntouched = fs.readFileSync(path.join(opencodeDir, "config.json"), "utf8") === opencodeBefore;
|
|
323
|
+
const opencodeEntwurfState = fs
|
|
324
|
+
.readdirSync(opencodeDir)
|
|
325
|
+
.filter((e) => e.toLowerCase().includes("entwurf"))
|
|
326
|
+
.concat(JSON.stringify(readJson(path.join(opencodeDir, "config.json"))).includes("entwurf") ? ["config"] : []);
|
|
327
|
+
if (!claudeOnPath) {
|
|
328
|
+
skip(
|
|
329
|
+
`[${LABEL}] REINSTALL WIDENING`,
|
|
330
|
+
"no `claude` on PATH, so the vendor half of this cell cannot run; the Pi/OpenCode halves are proven by " +
|
|
331
|
+
"check-herdr-activation and a partial pass here would read as vendor evidence",
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
// EXACTLY ONE OWNER ENTRY, counted. A substring search would pass on a config that carried the
|
|
335
|
+
// command twice, or carried it under a key nobody reads — neither is "wired exactly once".
|
|
336
|
+
const claudeConfig = path.join(HOME, ".claude.json");
|
|
337
|
+
const claudeCfg = fs.existsSync(claudeConfig) ? readJson(claudeConfig) : {};
|
|
338
|
+
const servers = (claudeCfg.mcpServers ?? {}) as Record<string, { command?: string }>;
|
|
339
|
+
const bridgeKeys = Object.keys(servers).filter((k) => k.includes("entwurf"));
|
|
340
|
+
const bridgeCommand = bridgeKeys.length === 1 ? (servers[bridgeKeys[0] as string]?.command ?? "") : "";
|
|
341
|
+
ok(
|
|
342
|
+
`[${LABEL}] REINSTALL WIDENING: with Claude Code and OpenCode now integrated, the same install command ` +
|
|
343
|
+
"reconciles Pi with JSON-IDENTICAL settings and adds Claude as EXACTLY ONE counted owner entry whose " +
|
|
344
|
+
"command is the absolute bridge under the stable runtime — accepted by the REAL vendor CLI, which is the " +
|
|
345
|
+
"LIVE boundary no deterministic gate may claim. The entry is counted, not searched for: a substring would " +
|
|
346
|
+
"pass on a config carrying the command twice, or under a key nobody reads. OpenCode, present the whole " +
|
|
347
|
+
`time, is given zero Entwurf state (claude-integration=${claudeIntegration.status} opencode-integration=${opencodeIntegration.status} install=${second.status} ledger=${JSON.stringify(ledger2?.activatedBackends)} pi-identical=${piBeforeSecond === piAfterSecond} entwurf-mcp-keys=${JSON.stringify(bridgeKeys)} command-under-runtime=${bridgeCommand.startsWith(activeDir)} opencode-untouched=${opencodeUntouched} opencode-entwurf=${JSON.stringify(opencodeEntwurfState)})`,
|
|
348
|
+
second.status === 0 &&
|
|
349
|
+
JSON.stringify(ledger2?.activatedBackends) === JSON.stringify(["pi", "claude-code"]) &&
|
|
350
|
+
(ledger2?.phase as string) === "active" &&
|
|
351
|
+
piBeforeSecond === piAfterSecond &&
|
|
352
|
+
bridgeKeys.length === 1 &&
|
|
353
|
+
path.isAbsolute(bridgeCommand) &&
|
|
354
|
+
bridgeCommand.startsWith(activeDir) &&
|
|
355
|
+
opencodeUntouched &&
|
|
356
|
+
opencodeEntwurfState.length === 0,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── 3. ACQUISITION LEAF: the remote does NOT have the commit ───────────────
|
|
361
|
+
//
|
|
362
|
+
// This cell is NOT the Herdr journey. It calls the shipped runtime leaf directly with the FIXED
|
|
363
|
+
// product argv, because the failure it measures is the acquisition's — a remote that cannot serve
|
|
364
|
+
// a commit — and routing it through `herdr plugin install` would add nothing but a second
|
|
365
|
+
// explanation for the same red. Cells 1, 2 and 4 are the real-Herdr journey; this one is
|
|
366
|
+
// acquisition-leaf LIVE evidence, and that distinction is the point of naming it here.
|
|
367
|
+
{
|
|
368
|
+
const runtimeBefore = treeDigest(activeDir);
|
|
369
|
+
const ledgerBefore = fs.readFileSync(ledgerPath, "utf8");
|
|
370
|
+
const piBefore = JSON.stringify(readJson(piSettings));
|
|
371
|
+
const journalBefore = readJson(journalPath) as {
|
|
372
|
+
artifactIdentity: Record<string, string>;
|
|
373
|
+
previousRuntime: Record<string, string> | null;
|
|
374
|
+
};
|
|
375
|
+
const absentCommit = "9".repeat(40);
|
|
376
|
+
const probe = run(
|
|
377
|
+
"node",
|
|
378
|
+
[
|
|
379
|
+
"--input-type=module",
|
|
380
|
+
"-e",
|
|
381
|
+
[
|
|
382
|
+
`const m = await import(${JSON.stringify(path.join(REPO, "scripts", "herdr-runtime.mjs"))});`,
|
|
383
|
+
"try {",
|
|
384
|
+
" m.bootstrapRuntime({ env: process.env, lock: { source: 'herdr-checkout', repository: 'junghan0611/entwurf' },",
|
|
385
|
+
` checkoutRoot: ${JSON.stringify(REPO)}, resolveCommit: () => ${JSON.stringify(absentCommit)} });`,
|
|
386
|
+
" console.log('NO-REFUSAL');",
|
|
387
|
+
"} catch (err) { console.log(err.code); }",
|
|
388
|
+
].join("\n"),
|
|
389
|
+
],
|
|
390
|
+
env,
|
|
391
|
+
);
|
|
392
|
+
const verdict = (probe.stdout || "").trim().split("\n").pop();
|
|
393
|
+
// The HONEST oracle. The journal is NOT byte-identical and must not be claimed as such: the
|
|
394
|
+
// transaction deliberately writes a certified `installing` entry BEFORE acquiring, because that
|
|
395
|
+
// entry is the retry authority and the only thing that makes the staging tree and the cache
|
|
396
|
+
// ours to reclaim. What must be unchanged is everything the failure had no business touching —
|
|
397
|
+
// the active runtime, the activation ledger, the harness wiring — while the journal is left in
|
|
398
|
+
// the recoverable state its own contract describes, and the normal source still installs.
|
|
399
|
+
//
|
|
400
|
+
// MEASURE THE FAILURE BEFORE THE RETRY. The first run of this cell asserted the post-failure
|
|
401
|
+
// facts AFTER running the retry install, so the retry's own (correct) work — a fresh runtime
|
|
402
|
+
// tree and a re-created cache — read as "the failure changed things". Order is the oracle here.
|
|
403
|
+
const afterFailure = {
|
|
404
|
+
runtime: treeDigest(activeDir),
|
|
405
|
+
ledger: fs.readFileSync(ledgerPath, "utf8"),
|
|
406
|
+
pi: JSON.stringify(readJson(piSettings)),
|
|
407
|
+
staging: fs.existsSync(path.join(path.dirname(activeDir), "staging")),
|
|
408
|
+
cache: fs.existsSync(path.join(env.XDG_CACHE_HOME as string, "entwurf", "herdr-plugin", "npm")),
|
|
409
|
+
journal: readJson(journalPath) as {
|
|
410
|
+
phase: string;
|
|
411
|
+
artifactIdentity: Record<string, string>;
|
|
412
|
+
previousRuntime: Record<string, string> | null;
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
const journalAfter = afterFailure.journal;
|
|
416
|
+
const retry = pluginInstall(BRANCH);
|
|
417
|
+
const journalRetried = readJson(journalPath) as { phase: string; artifactIdentity: Record<string, string> };
|
|
418
|
+
ok(
|
|
419
|
+
`[${LABEL}] REMOTE-COMMIT UNAVAILABLE (acquisition leaf, not the Herdr journey): the FIXED product argv at a ` +
|
|
420
|
+
"commit the remote does not carry is refused by name and substitutes NOTHING. The active runtime, the " +
|
|
421
|
+
"activation ledger and the Pi wiring are byte-identical; the journal is left as a CERTIFIED `installing` " +
|
|
422
|
+
"entry naming the requested commit with the previous runtime's identity preserved — that is the retry " +
|
|
423
|
+
"authority, not damage — the staging tree and cache are gone, and a normal install afterwards is green. " +
|
|
424
|
+
"This is the branch where a fallback to a registry version would be most tempting and would silently serve " +
|
|
425
|
+
`an artifact nobody asked for (verdict=${verdict} runtime-identical=${runtimeBefore === afterFailure.runtime} ledger-identical=${ledgerBefore === afterFailure.ledger} pi-identical=${piBefore === afterFailure.pi} journal=${journalAfter.phase}/${journalAfter.artifactIdentity.commit?.slice(0, 8)}/prev=${journalAfter.previousRuntime?.commit?.slice(0, 8)} staging=${afterFailure.staging} cache=${afterFailure.cache} retry=${retry.status}/${journalRetried.phase}/${journalRetried.artifactIdentity.commit?.slice(0, 8)})`,
|
|
426
|
+
verdict === "runtime-checkout-source-unavailable" &&
|
|
427
|
+
runtimeBefore === afterFailure.runtime &&
|
|
428
|
+
ledgerBefore === afterFailure.ledger &&
|
|
429
|
+
piBefore === afterFailure.pi &&
|
|
430
|
+
journalAfter.phase === "installing" &&
|
|
431
|
+
journalAfter.artifactIdentity.commit === absentCommit &&
|
|
432
|
+
journalAfter.previousRuntime?.commit === journalBefore.artifactIdentity.commit &&
|
|
433
|
+
!afterFailure.staging &&
|
|
434
|
+
!afterFailure.cache &&
|
|
435
|
+
retry.status === 0 &&
|
|
436
|
+
journalRetried.phase === "runtime-ready" &&
|
|
437
|
+
journalRetried.artifactIdentity.commit === HEAD,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── 4. Herdr's own commit fails AFTER our runner succeeded ─────────────────
|
|
442
|
+
{
|
|
443
|
+
const scratch = path.join(ROOT, "scratch");
|
|
444
|
+
assert.equal(run("git", ["clone", "--quiet", MIRROR, scratch], env).status, 0, "scratch clone");
|
|
445
|
+
assert.equal(run("git", ["-C", scratch, "checkout", "--quiet", "-b", "gapprobe", HEAD], env).status, 0, "branch");
|
|
446
|
+
// A build that MUTATES the manifest: herdr reloads it after the build and aborts the install
|
|
447
|
+
// (measured: `ensure_manifest_unchanged_after_build`). Our runner still ran to completion.
|
|
448
|
+
const runnerPath = path.join(scratch, "plugins", "herdr", "lib", "build.mjs");
|
|
449
|
+
// The mutation has to run BEFORE the runner's process exits, so it is INSERTED INTO the entry
|
|
450
|
+
// block rather than appended after it. Measured on the previous two runs: appended top-level
|
|
451
|
+
// code never executed at all (`process.exit(runBuild(...))` ends the process first), and the
|
|
452
|
+
// earlier `# comment` version of it would not have mattered anyway — herdr compares the PARSED
|
|
453
|
+
// manifest by full struct equality (`ensure_manifest_unchanged_after_build`, read at
|
|
454
|
+
// src/cli/plugin.rs), so only a semantic change makes its commit fail. Rewriting the declared
|
|
455
|
+
// plugin version is exactly that.
|
|
456
|
+
const runnerSource = fs.readFileSync(runnerPath, "utf8");
|
|
457
|
+
const entry = "\t\tprocess.exit(runBuild(process.env));";
|
|
458
|
+
assert.ok(runnerSource.includes(entry), "the runner's entry block is not the shape this probe patches");
|
|
459
|
+
fs.writeFileSync(
|
|
460
|
+
runnerPath,
|
|
461
|
+
runnerSource.replace(
|
|
462
|
+
entry,
|
|
463
|
+
[
|
|
464
|
+
"\t\tconst probeCode = runBuild(process.env);",
|
|
465
|
+
"\t\t// gap probe: herdr re-reads the manifest after the build and aborts its own commit.",
|
|
466
|
+
'\t\tconst probeManifest = path.join(PLUGIN_DIR, "herdr-plugin.toml");',
|
|
467
|
+
"\t\tfs.writeFileSync(",
|
|
468
|
+
"\t\t\tprobeManifest,",
|
|
469
|
+
'\t\t\tfs.readFileSync(probeManifest, "utf8").replace(\'version = "0.1.0"\', \'version = "0.1.1"\'),',
|
|
470
|
+
"\t\t);",
|
|
471
|
+
"\t\tprocess.exit(probeCode);",
|
|
472
|
+
].join("\n"),
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
const gapHook = commitInClone(scratch, "gap probe", "gapprobe").hookTrace;
|
|
476
|
+
assertExactlyClean(scratch, "the gap-probe scratch clone");
|
|
477
|
+
assert.equal(run("git", ["-C", scratch, "push", "--quiet", MIRROR, "gapprobe"], env).status, 0, "push probe");
|
|
478
|
+
const gapCommit = (run("git", ["-C", scratch, "rev-parse", "HEAD"], env).stdout ?? "").trim();
|
|
479
|
+
const registryBefore = run("herdr", ["plugin", "list"], env).stdout ?? "";
|
|
480
|
+
const ledgerBefore = readJson(ledgerPath) as {
|
|
481
|
+
artifactIdentity: Record<string, string>;
|
|
482
|
+
activatedBackends: string[];
|
|
483
|
+
components: { backend: string; state: string }[];
|
|
484
|
+
};
|
|
485
|
+
const gapRun = pluginInstall("gapprobe");
|
|
486
|
+
const registryAfter = run("herdr", ["plugin", "list"], env).stdout ?? "";
|
|
487
|
+
// The gap commit is a DIFFERENT artifact, so the ledger cannot be byte-identical here and
|
|
488
|
+
// claiming it would be false: our runner ran to completion, which means it legally rebound the
|
|
489
|
+
// activation to that commit. What Herdr then failed to do is its own registration.
|
|
490
|
+
const ledgerAfterGap = readJson(ledgerPath) as {
|
|
491
|
+
phase: string;
|
|
492
|
+
artifactIdentity: Record<string, string>;
|
|
493
|
+
activatedBackends: string[];
|
|
494
|
+
components: { backend: string; state: string }[];
|
|
495
|
+
};
|
|
496
|
+
const reconcile = pluginInstall(BRANCH);
|
|
497
|
+
const ledgerAfterReconcile = readJson(ledgerPath) as {
|
|
498
|
+
phase: string;
|
|
499
|
+
artifactIdentity: Record<string, string>;
|
|
500
|
+
activatedBackends: string[];
|
|
501
|
+
components: { backend: string; state: string }[];
|
|
502
|
+
};
|
|
503
|
+
const noDuplicates = (l: { components: { backend: string }[] }) =>
|
|
504
|
+
new Set(l.components.map((c) => c.backend)).size === l.components.length;
|
|
505
|
+
ok(
|
|
506
|
+
`[${LABEL}] POST-BUILD GAP: Herdr commits its checkout and registry only AFTER a build, so a build that makes ` +
|
|
507
|
+
"that commit fail leaves Herdr's PREVIOUS registration standing while what our runner already did REMAINS " +
|
|
508
|
+
"— the runtime is the gap commit and the ledger has legally rebound to it, active and retained, because " +
|
|
509
|
+
"Herdr has no cleanup hook to call. The next normal install re-binds to HEAD's identity and reconciles the " +
|
|
510
|
+
"same components rather than duplicating them. The gap is NAMED, not atomic, and this cell asserts the " +
|
|
511
|
+
`actual transition rather than an unchanged-bytes claim that could never be true (gap-commit-hooks=${gapHook} gap-install=${gapRun.status} registry-unchanged=${registryBefore === registryAfter} before=${ledgerBefore.artifactIdentity.commit?.slice(0, 8)} after-gap=${ledgerAfterGap.phase}/${ledgerAfterGap.artifactIdentity.commit?.slice(0, 8)}=${gapCommit.slice(0, 8)} reconcile=${reconcile.status}/${ledgerAfterReconcile.phase}/${ledgerAfterReconcile.artifactIdentity.commit?.slice(0, 8)}=${HEAD.slice(0, 8)} backends=${JSON.stringify(ledgerAfterReconcile.activatedBackends)} no-dupes=${noDuplicates(ledgerAfterGap)}/${noDuplicates(ledgerAfterReconcile)})`,
|
|
512
|
+
gapRun.status !== 0 &&
|
|
513
|
+
registryBefore === registryAfter &&
|
|
514
|
+
ledgerAfterGap.phase === "active" &&
|
|
515
|
+
ledgerAfterGap.artifactIdentity.commit === gapCommit &&
|
|
516
|
+
noDuplicates(ledgerAfterGap) &&
|
|
517
|
+
reconcile.status === 0 &&
|
|
518
|
+
ledgerAfterReconcile.phase === "active" &&
|
|
519
|
+
ledgerAfterReconcile.artifactIdentity.commit === HEAD &&
|
|
520
|
+
JSON.stringify(ledgerAfterReconcile.activatedBackends) === JSON.stringify(ledgerBefore.activatedBackends) &&
|
|
521
|
+
noDuplicates(ledgerAfterReconcile),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── the operator's own herdr surface is untouched ──────────────────────────
|
|
526
|
+
const operatorAfter = spawnSync("herdr", ["plugin", "list"], { encoding: "utf8" }).stdout ?? "";
|
|
527
|
+
const operatorStatusAfter = (run("git", ["status", "--porcelain"], env).stdout ?? "").trim();
|
|
528
|
+
const operatorHeadAfter = (run("git", ["rev-parse", "HEAD"], env).stdout ?? "").trim();
|
|
529
|
+
ok(
|
|
530
|
+
`[${LABEL}] the OPERATOR is untouched: their herdr plugin registry is byte-identical, and so are this repo's HEAD ` +
|
|
531
|
+
"and porcelain status — the candidate snapshot is built by READING this repo's diff and committing it inside " +
|
|
532
|
+
"the sandbox, because a fixture that needed the candidate committed for real would be indistinguishable from " +
|
|
533
|
+
`the product asking for it (registry=${operatorBefore === operatorAfter} head=${operatorHeadBefore === operatorHeadAfter} status=${operatorStatusBefore === operatorStatusAfter})`,
|
|
534
|
+
operatorBefore === operatorAfter &&
|
|
535
|
+
operatorHeadBefore === operatorHeadAfter &&
|
|
536
|
+
operatorStatusBefore === operatorStatusAfter,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
console.log(`\n${LABEL}: ${passed} assertions passed, ${skipped} skipped (sandbox kept at ${ROOT})`);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
console.error(`\n${LABEL}: FAILED — sandbox kept for inspection at ${ROOT}`);
|
|
542
|
+
throw err;
|
|
543
|
+
}
|