@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,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-herdr-ci — put the MEASURED herdr binary on PATH for one CI job, and nothing else.
|
|
3
|
+
#
|
|
4
|
+
# This is not an installer for people. `entwurf setup` does not call it, the package does not
|
|
5
|
+
# ship it as a command, and it never writes outside the runner's temp dir: Hard Rule 17 says
|
|
6
|
+
# Entwurf supplies Entwurf's bytes, never a harness or a placement owner's. A CI job that wants
|
|
7
|
+
# the herdr rail proven fetches the exact asset this repository has measured, checks its digest
|
|
8
|
+
# BEFORE the file is ever made executable, and exposes only that one directory to later steps.
|
|
9
|
+
#
|
|
10
|
+
# What is deliberately NOT here, each for a reason:
|
|
11
|
+
# - no `curl … | sh`. A pipe to a shell has no version and no digest to check; whatever the
|
|
12
|
+
# upstream script does today it can do something else tomorrow, and the whole point of this
|
|
13
|
+
# file is that CI runs a byte we have named.
|
|
14
|
+
# - no package manager (brew/mise/nix). Each adds a layer whose contents we would then be
|
|
15
|
+
# asserting about without measuring.
|
|
16
|
+
# - no `latest`. A mutable URL turns a green run into a claim about a binary nobody chose.
|
|
17
|
+
# - no `gh attestation verify`. `[측정 2026-09-14, gh 2.97.0]` it cannot close herdr's
|
|
18
|
+
# predicate (`in-toto release/v0.2`) — default is 404, explicit is "no attestations found",
|
|
19
|
+
# while the REST API returns the attestation. A check we cannot run is not a check.
|
|
20
|
+
# - no global install, no ~/.local/bin, no sudo. The binary lives and dies with the job.
|
|
21
|
+
#
|
|
22
|
+
# Every version and digest comes from scripts/fixtures/herdr-supply.json. This file contains
|
|
23
|
+
# none of its own, on purpose: two copies of a digest is how a pin drifts.
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
27
|
+
manifest="${repo_dir}/scripts/fixtures/herdr-supply.json"
|
|
28
|
+
|
|
29
|
+
fail() {
|
|
30
|
+
echo "[install-herdr-ci] FAIL — $1" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[ -f "$manifest" ] || fail "the supply manifest is missing: ${manifest}"
|
|
35
|
+
|
|
36
|
+
# The runner's temp dir in CI; a private mktemp when someone runs this by hand. Never a
|
|
37
|
+
# directory that outlives the job.
|
|
38
|
+
dest_dir="${RUNNER_TEMP:-}"
|
|
39
|
+
if [ -z "$dest_dir" ]; then
|
|
40
|
+
dest_dir="$(mktemp -d)"
|
|
41
|
+
fi
|
|
42
|
+
mkdir -p "$dest_dir"
|
|
43
|
+
|
|
44
|
+
read_manifest() {
|
|
45
|
+
node -e '
|
|
46
|
+
const fs = require("node:fs");
|
|
47
|
+
const m = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
48
|
+
const arch = process.argv[2];
|
|
49
|
+
const asset = m.assets[arch];
|
|
50
|
+
if (!asset) {
|
|
51
|
+
// Fail closed. A machine architecture we have never measured does not get a
|
|
52
|
+
// "closest" binary — there is no such thing.
|
|
53
|
+
process.stderr.write(`unsupported architecture ${arch}; measured: ${Object.keys(m.assets).join(", ")}\n`);
|
|
54
|
+
process.exit(3);
|
|
55
|
+
}
|
|
56
|
+
process.stdout.write([m.version, m.releaseUrlPrefix, m.tag, asset.name, asset.sha256].join("\n"));
|
|
57
|
+
' "$manifest" "$1"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
arch="$(uname -m)"
|
|
61
|
+
if ! fields="$(read_manifest "$arch")"; then
|
|
62
|
+
fail "no measured herdr asset for $(uname -m) — the supply manifest owns that set"
|
|
63
|
+
fi
|
|
64
|
+
version="$(echo "$fields" | sed -n 1p)"
|
|
65
|
+
url_prefix="$(echo "$fields" | sed -n 2p)"
|
|
66
|
+
tag="$(echo "$fields" | sed -n 3p)"
|
|
67
|
+
asset_name="$(echo "$fields" | sed -n 4p)"
|
|
68
|
+
expected_sha="$(echo "$fields" | sed -n 5p)"
|
|
69
|
+
|
|
70
|
+
url="${url_prefix}/${tag}/${asset_name}"
|
|
71
|
+
target="${dest_dir}/herdr"
|
|
72
|
+
|
|
73
|
+
echo "[install-herdr-ci] fetching ${asset_name} (${tag}) for ${arch}"
|
|
74
|
+
curl -fsSL --retry 3 --retry-delay 2 -o "$target" "$url" || fail "download failed: ${url}"
|
|
75
|
+
|
|
76
|
+
# ORDER IS THE SAFETY ARGUMENT: the digest is checked while the file is still inert data.
|
|
77
|
+
# chmod comes after, so a byte we did not choose never becomes a thing this job can run.
|
|
78
|
+
actual_sha="$(sha256sum "$target" | cut -d' ' -f1)"
|
|
79
|
+
if [ "$actual_sha" != "$expected_sha" ]; then
|
|
80
|
+
rm -f "$target"
|
|
81
|
+
fail "digest mismatch for ${asset_name}: expected ${expected_sha}, got ${actual_sha}"
|
|
82
|
+
fi
|
|
83
|
+
chmod +x "$target"
|
|
84
|
+
|
|
85
|
+
reported="$("$target" --version 2>/dev/null || true)"
|
|
86
|
+
case "$reported" in
|
|
87
|
+
*"herdr ${version}"*) ;;
|
|
88
|
+
*) fail "the verified binary reports '${reported}', not 'herdr ${version}' — the manifest and the artifact disagree" ;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
# Only this directory reaches later steps. Nothing is copied into a shared bin dir.
|
|
92
|
+
if [ -n "${GITHUB_PATH:-}" ]; then
|
|
93
|
+
echo "$dest_dir" >>"$GITHUB_PATH"
|
|
94
|
+
fi
|
|
95
|
+
echo "[install-herdr-ci] ok — ${reported} at ${target} (sha256 ${expected_sha})"
|
|
96
|
+
echo "$dest_dir"
|
|
@@ -69,7 +69,22 @@ function listWorkSurface(pathspec: string[]): string[] {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const legacyFiles = listWorkSurface(["scripts/"]);
|
|
72
|
-
|
|
72
|
+
// The framework axis is every vitest-managed lane, and since #119 V2 that is TWO locations:
|
|
73
|
+
// the migration lane under test/, and tests written beside the behaviour they certify. The
|
|
74
|
+
// pathspecs are `:(glob)` so `**` means "at any depth" and nothing but a test file enters —
|
|
75
|
+
// pi-extensions/ and plugins/ are product trees, and pulling them in whole would put 20k lines
|
|
76
|
+
// of product into a VERIFICATION denominator.
|
|
77
|
+
//
|
|
78
|
+
// This is what keeps the combined total honest through the V3 migration: a gate moving from
|
|
79
|
+
// scripts/ to a file beside its subject moves lines BETWEEN axes, and only a drop in the
|
|
80
|
+
// combined number is subtraction. Before this, such a gate left the denominator entirely and
|
|
81
|
+
// every migrated line read as a deletion.
|
|
82
|
+
const frameworkFiles = listWorkSurface([
|
|
83
|
+
"test/",
|
|
84
|
+
"vitest.config.ts",
|
|
85
|
+
":(glob)pi-extensions/**/*.test.ts",
|
|
86
|
+
":(glob)plugins/herdr/**/*.test.mjs",
|
|
87
|
+
]);
|
|
73
88
|
|
|
74
89
|
function countLines(rel: string): number {
|
|
75
90
|
const body = fs.readFileSync(path.join(REPO_DIR, rel), "utf8");
|
|
@@ -83,6 +98,20 @@ const H_TEXT = /readFileSync[^\n]*(?:pi-extensions|mcp\/|\.ts["'`]|SOURCE|SRC)|\
|
|
|
83
98
|
const H_PROC = /\b(?:spawn|spawnSync|execFile|execFileSync|execSync|fork)\s*\(|subprocess\.(?:run|Popen|check_)/;
|
|
84
99
|
const H_NET = /\b(?:http|net)\.createServer|\.listen\(/;
|
|
85
100
|
const H_FS = /from\s+["']node:fs["']|require\(["']node:fs["']\)/;
|
|
101
|
+
// H_IMPORTS asks for an ASCENDING path into a product tree, because every gate under scripts/
|
|
102
|
+
// has to climb to reach one. A test written beside its subject never climbs: it imports
|
|
103
|
+
// `./subject.ts`. The distinction is the file's own location, not its spelling — a lane living
|
|
104
|
+
// inside pi-extensions/ or plugins/ that imports a relative sibling is importing product by
|
|
105
|
+
// construction. Applied ONLY to those lanes: scripts/check-*.ts also import `./lib/*`, and that
|
|
106
|
+
// is the verification surface importing itself, which is a different fact the mutant inventory
|
|
107
|
+
// already counts as infra-subject.
|
|
108
|
+
const H_IMPORTS_SIBLING = /(?:from\s+["']|import\(\s*["'])\.{1,2}\//;
|
|
109
|
+
// Wider than the framework-axis glob (`plugins/herdr/**`) on purpose, and the asymmetry only
|
|
110
|
+
// runs one way: the glob decides what ENTERS the denominator, this decides how a file already
|
|
111
|
+
// in it is read. A test under some other plugins/ package would have to be admitted by the
|
|
112
|
+
// glob first, so the looser shape here cannot inflate a count — it only keeps the classifier
|
|
113
|
+
// from throwing on the day that glob widens.
|
|
114
|
+
const BESIDE_BEHAVIOUR = /^(pi-extensions|plugins)\/.*\.test\.(ts|mjs)$/;
|
|
86
115
|
// H_LIVE asks whether the file's OWN CODE reads the LIVE switch — not whether the
|
|
87
116
|
// three letters appear. The naive `\bLIVE=1\b` form classified
|
|
88
117
|
// scripts/check-release-gate-outcomes.ts as real-live because that gate QUOTES the
|
|
@@ -145,6 +174,45 @@ const CLASS_OVERRIDES: Record<string, { cls: SemanticClass; reason: string }> =
|
|
|
145
174
|
cls: "source-topology",
|
|
146
175
|
reason: "asserts the scrubbed parent-transcript fixture's recorded shape — an artifact contract, no product run",
|
|
147
176
|
},
|
|
177
|
+
// #119 V1 measured the classifier THROWING on these four at HEAD 5062147 — the
|
|
178
|
+
// assertion below did its job and named real drift, in three distinct shapes. None
|
|
179
|
+
// of them is reachable by widening H_IMPORTS: there is no literal `../plugins/`
|
|
180
|
+
// import anywhere under scripts/ or test/ (measured, 0 matches), so a `plugins`
|
|
181
|
+
// alternative in that predicate would match nothing and only claim coverage it
|
|
182
|
+
// does not have.
|
|
183
|
+
//
|
|
184
|
+
// Shape 1 — the herdr gates reach their subject through a RUNTIME-ASSEMBLED
|
|
185
|
+
// dynamic import, `await import(pathToFileURL(LEAF).href)`. H_IMPORTS matches a
|
|
186
|
+
// literal specifier by design (a computed one cannot be read as text without
|
|
187
|
+
// executing the gate), so these two are override territory, not predicate
|
|
188
|
+
// territory.
|
|
189
|
+
"scripts/check-herdr-plugin-profile.ts": {
|
|
190
|
+
cls: "behavioral-contract",
|
|
191
|
+
reason:
|
|
192
|
+
"dynamically imports plugins/herdr/lib/integration-profile.mjs and source-reads that same leaf — executes product code, no process",
|
|
193
|
+
},
|
|
194
|
+
"scripts/check-herdr-runtime-bootstrap.ts": {
|
|
195
|
+
cls: "behavioral-contract",
|
|
196
|
+
reason:
|
|
197
|
+
"dynamically imports the herdr runtime leaf and its plugin re-export and drives them against mkdtemp HOME/XDG fixtures — executes product code, spawns nothing",
|
|
198
|
+
},
|
|
199
|
+
// Shape 2 — the reads go through a local `read(rel)` helper, so H_TEXT's
|
|
200
|
+
// line-level form (readFileSync on the same line as the path literal) cannot see
|
|
201
|
+
// them. No product is executed: this is a manifest/installer/CI structure gate.
|
|
202
|
+
"scripts/check-herdr-supply.ts": {
|
|
203
|
+
cls: "source-topology",
|
|
204
|
+
reason:
|
|
205
|
+
"reads the supply manifest, the CI installer and the workflow as text through a read() helper; runs no product",
|
|
206
|
+
},
|
|
207
|
+
// Shape 3 — a vitest lane whose SUBJECT is the verification surface itself
|
|
208
|
+
// (scripts/lib), not pi-extensions/ or mcp/. The mutant inventory below already
|
|
209
|
+
// counts that as infra-subject; the class axis needs the same honesty rather than
|
|
210
|
+
// a widened "product import" predicate that would blur the two.
|
|
211
|
+
"test/tmux-coordinate-row.test.ts": {
|
|
212
|
+
cls: "pure-unit",
|
|
213
|
+
reason:
|
|
214
|
+
"executes the scripts/lib/tmux-coordinate-row leaf — an INFRA-subject lane; no fs, no text read, no process",
|
|
215
|
+
},
|
|
148
216
|
};
|
|
149
217
|
|
|
150
218
|
/**
|
|
@@ -187,9 +255,13 @@ function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
|
187
255
|
const body = isShell ? fs.readFileSync(path.join(REPO_DIR, rel), "utf8") : effectiveBody(rel);
|
|
188
256
|
const base = path.basename(rel);
|
|
189
257
|
|
|
258
|
+
// One notion of "imports product", used by BOTH axes below so the style table and the class
|
|
259
|
+
// table can never disagree about the same file.
|
|
260
|
+
const importsProduct = H_IMPORTS.test(body) || (BESIDE_BEHAVIOUR.test(rel) && H_IMPORTS_SIBLING.test(body));
|
|
261
|
+
|
|
190
262
|
const style: Style = (() => {
|
|
191
263
|
if (isShell) return "shell";
|
|
192
|
-
const axes = [
|
|
264
|
+
const axes = [importsProduct, H_TEXT.test(body), H_PROC.test(body)];
|
|
193
265
|
const n = axes.filter(Boolean).length;
|
|
194
266
|
if (n >= 2) return "mixed";
|
|
195
267
|
if (axes[0]) return "imports-product";
|
|
@@ -207,11 +279,11 @@ function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
|
207
279
|
? "package-install"
|
|
208
280
|
: isShell || H_PROC.test(body) || H_NET.test(body)
|
|
209
281
|
? "hermetic-integration"
|
|
210
|
-
: H_TEXT.test(body) && !
|
|
282
|
+
: H_TEXT.test(body) && !importsProduct
|
|
211
283
|
? "source-topology"
|
|
212
|
-
:
|
|
284
|
+
: importsProduct && (H_TEXT.test(body) || H_FS.test(body))
|
|
213
285
|
? "behavioral-contract"
|
|
214
|
-
:
|
|
286
|
+
: importsProduct
|
|
215
287
|
? "pure-unit"
|
|
216
288
|
: null;
|
|
217
289
|
if (cls === null) {
|
|
@@ -224,7 +296,7 @@ function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
|
224
296
|
|
|
225
297
|
// ── buckets ──────────────────────────────────────────────────────────────────
|
|
226
298
|
const legacyGates = legacyFiles.filter((f) => /^scripts\/(check-|smoke-)/.test(f));
|
|
227
|
-
const frameworkGates = frameworkFiles.filter((f) =>
|
|
299
|
+
const frameworkGates = frameworkFiles.filter((f) => /\.test\.(ts|mjs)$/.test(f));
|
|
228
300
|
const lib = legacyFiles.filter((f) => f.startsWith("scripts/lib/"));
|
|
229
301
|
const mutantManifests = legacyFiles.filter((f) => f.startsWith("scripts/mutants/") && f.endsWith(".json"));
|
|
230
302
|
const fixtures = legacyFiles.filter((f) => f.startsWith("scripts/fixtures/"));
|
|
@@ -254,14 +326,16 @@ rule predicates (re-derive any number from these):
|
|
|
254
326
|
H_PROC ${H_PROC}
|
|
255
327
|
H_NET ${H_NET}
|
|
256
328
|
H_FS ${H_FS}
|
|
329
|
+
H_IMPORTS_SIBLING ${H_IMPORTS_SIBLING} (only for ${BESIDE_BEHAVIOUR})
|
|
257
330
|
H_LIVE ${H_LIVE}
|
|
258
331
|
H_LIVE is applied to the CODE-ONLY projection (comments and inert literals blanked;
|
|
259
332
|
shell double-quoted expansions kept), so a gate that merely QUOTES "LIVE=1" is not
|
|
260
333
|
real-live. Two-tier gates are listed under the class breakdown.
|
|
261
334
|
semantic classes, first match wins: override → real-live (name -live | H_LIVE on code) →
|
|
262
335
|
package-install (name pack|install) → hermetic-integration (.sh | H_PROC | H_NET) →
|
|
263
|
-
source-topology (H_TEXT ∧ ¬
|
|
264
|
-
pure-unit (
|
|
336
|
+
source-topology (H_TEXT ∧ ¬importsProduct) → behavioral-contract (importsProduct ∧ (H_TEXT ∨ H_FS)) →
|
|
337
|
+
pure-unit (importsProduct) → ERROR (unclassified is asserted zero)
|
|
338
|
+
importsProduct = H_IMPORTS, plus H_IMPORTS_SIBLING for a BESIDE_BEHAVIOUR lane
|
|
265
339
|
test/*.test.ts is classified together with the ./helpers/* bodies it imports.
|
|
266
340
|
`);
|
|
267
341
|
|
|
@@ -275,9 +349,11 @@ console.log(` scripts/lib/: ${lib.length} files, ${total(lib
|
|
|
275
349
|
console.log(` scripts/mutants/: ${mutantManifests.length} manifests`);
|
|
276
350
|
console.log(` scripts/fixtures/: ${fixtures.length} files`);
|
|
277
351
|
console.log(` other: ${legacyOther.length} files, ${total(legacyOther)} lines`);
|
|
278
|
-
console.log(`framework axis (test/ + vitest.config.ts): ${frameworkFiles.length} files, ${frameworkTotal} lines`);
|
|
279
352
|
console.log(
|
|
280
|
-
`
|
|
353
|
+
`framework axis (vitest-managed, both locations): ${frameworkFiles.length} files, ${frameworkTotal} lines`,
|
|
354
|
+
);
|
|
355
|
+
console.log(
|
|
356
|
+
` lanes (test/** + beside behaviour): ${frameworkGates.length} files, ${sum(rows.filter((r) => r.axis === "framework"))} lines`,
|
|
281
357
|
);
|
|
282
358
|
console.log(` helpers/config: ${frameworkSupport.length} files, ${total(frameworkSupport)} lines`);
|
|
283
359
|
console.log(
|
|
@@ -55,11 +55,11 @@ export function buildCodexInstruction(input: CodexInstructionInput): string {
|
|
|
55
55
|
'NO cwd parameter and NO placement parameter, and task "After your automatic callback succeeds, answer ACK and ' +
|
|
56
56
|
'stop. Do not open another sibling."\n' +
|
|
57
57
|
"2. Stop and wait for the Pi callback delivered into this Codex thread. Accept only a callback whose body " +
|
|
58
|
-
"repeats the exact nonce from your LAUNCH receipt and whose sender envelope
|
|
58
|
+
"repeats the exact nonce from your LAUNCH receipt and whose sender envelope carries a garden id on its `session:` line.\n" +
|
|
59
59
|
`3. Only after that match, call public mcp__entwurf_bridge__entwurf_v2 once with target ${input.callerGid}, ` +
|
|
60
60
|
"intent fire-and-forget, wants_reply false, and message:\n" +
|
|
61
61
|
`${input.finalToken}\nPI_LAUNCH_NONCE=<exact launch nonce>\nPI_CALLBACK_NONCE=<same exact nonce>\n` +
|
|
62
|
-
"PI_CALLBACK_FROM=<
|
|
62
|
+
"PI_CALLBACK_FROM=<the garden id on the callback envelope's `session:` line — NOT the model name on its `from:` line>\nPI_SESSION_ID=<exact $session id from the LAUNCH receipt>\n" +
|
|
63
63
|
"PI_WINDOW_ID=<exact @window id from the LAUNCH receipt>\nDo not claim completion in prose."
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree-digest — one deterministic digest of a whole directory tree, for gates that claim a tree is
|
|
3
|
+
* UNCHANGED.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Two #116 M3-b3 oracles compared a sorted list of relative paths (one of them with
|
|
6
|
+
* sizes) and called the result "byte-identical". A same-length content edit passes both — which is
|
|
7
|
+
* precisely the shape a refusal-must-not-mutate claim needs to catch, because a transaction that
|
|
8
|
+
* rewrites a file in place rarely changes its length. So the digest here reads the actual bytes.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS IN IT: every entry's relative path, its node TYPE, and for a regular file its permission
|
|
11
|
+
* bits, its size and its whole content; for a symlink its target; for a directory its own entry
|
|
12
|
+
* count, so an emptied directory and a removed one are different facts. That makes the honest claim
|
|
13
|
+
* "content + mode + topology", which is what the callers now say.
|
|
14
|
+
*
|
|
15
|
+
* FRAMING IS LENGTH-PREFIXED. A digest built by concatenating names cannot tell `a/b` + `c` from
|
|
16
|
+
* `a` + `b/c`; every field is therefore written as `label:<byteLength>:<bytes>\0`, so no boundary
|
|
17
|
+
* between two fields can be spelled by the contents of one.
|
|
18
|
+
*
|
|
19
|
+
* MEMORY IS BOUNDED. A runtime tree here is hundreds of megabytes, so files are hashed in 64 KiB
|
|
20
|
+
* chunks through one reused buffer rather than read whole. No `sha256sum`, no shell: a gate that
|
|
21
|
+
* shells out to hash acquires a dependency the hosts it runs on do not owe it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash, type Hash } from "node:crypto";
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
|
|
28
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
29
|
+
|
|
30
|
+
function field(hash: Hash, label: string, value: string | number): void {
|
|
31
|
+
const bytes = Buffer.from(String(value), "utf8");
|
|
32
|
+
hash.update(`${label}:${bytes.length}:`);
|
|
33
|
+
hash.update(bytes);
|
|
34
|
+
hash.update("\0");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hashFileContent(hash: Hash, file: string): void {
|
|
38
|
+
const fd = fs.openSync(file, "r");
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.allocUnsafe(CHUNK_BYTES);
|
|
41
|
+
let read = fs.readSync(fd, buf, 0, CHUNK_BYTES, null);
|
|
42
|
+
while (read > 0) {
|
|
43
|
+
hash.update(buf.subarray(0, read));
|
|
44
|
+
read = fs.readSync(fd, buf, 0, CHUNK_BYTES, null);
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
fs.closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
hash.update("\0");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The digest of `root`, or the literal `"absent"` when nothing is there.
|
|
54
|
+
*
|
|
55
|
+
* @returns `sha256-<hex>` over content + mode + topology, or `absent`.
|
|
56
|
+
*/
|
|
57
|
+
export function treeDigest(root: string): string {
|
|
58
|
+
const top = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
59
|
+
if (top === undefined) return "absent";
|
|
60
|
+
const hash = createHash("sha256");
|
|
61
|
+
if (!top.isDirectory()) {
|
|
62
|
+
field(hash, top.isSymbolicLink() ? "root-symlink" : "root-nondir", path.basename(root));
|
|
63
|
+
if (top.isSymbolicLink()) field(hash, "target", fs.readlinkSync(root));
|
|
64
|
+
else if (top.isFile()) hashFileContent(hash, root);
|
|
65
|
+
return `sha256-${hash.digest("hex")}`;
|
|
66
|
+
}
|
|
67
|
+
const walk = (dir: string): void => {
|
|
68
|
+
const entries = fs
|
|
69
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
70
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
71
|
+
field(hash, "dir", path.relative(root, dir) || ".");
|
|
72
|
+
field(hash, "entries", entries.length);
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const abs = path.join(dir, entry.name);
|
|
75
|
+
const rel = path.relative(root, abs);
|
|
76
|
+
if (entry.isSymbolicLink()) {
|
|
77
|
+
field(hash, "symlink", rel);
|
|
78
|
+
field(hash, "target", fs.readlinkSync(abs));
|
|
79
|
+
} else if (entry.isDirectory()) {
|
|
80
|
+
field(hash, "subdir", rel);
|
|
81
|
+
} else if (entry.isFile()) {
|
|
82
|
+
const stat = fs.statSync(abs);
|
|
83
|
+
field(hash, "file", rel);
|
|
84
|
+
field(hash, "mode", (stat.mode & 0o7777).toString(8));
|
|
85
|
+
field(hash, "size", stat.size);
|
|
86
|
+
hashFileContent(hash, abs);
|
|
87
|
+
} else {
|
|
88
|
+
field(hash, "other", rel);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) walk(path.join(dir, entry.name));
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
walk(root);
|
|
96
|
+
return `sha256-${hash.digest("hex")}`;
|
|
97
|
+
}
|
|
@@ -170,7 +170,13 @@ echo "[meta-bridge-install] platform=$(uname -s) node=$NODE_VER ($NODE_BIN) clau
|
|
|
170
170
|
# settings.json / ~/.claude.json. Re-runs preserve the first snapshot, so
|
|
171
171
|
# uninstall restores the true pre-entwurf state rather than the last install
|
|
172
172
|
# run's already-managed values.
|
|
173
|
-
|
|
173
|
+
# #116 M3-b2 — decide before writing. An aggregate activation has to be able to say "nothing will
|
|
174
|
+
# fail once I start" before it starts, and that promise is only real if this half can be checked
|
|
175
|
+
# without mutating.
|
|
176
|
+
python3 "$REPO/scripts/meta-bridge-state.py" preflight-install --repo "$REPO" --asm "$ASM" \
|
|
177
|
+
${ENTWURF_PLUGIN_RUNTIME:+--plugin-runtime "$ENTWURF_PLUGIN_RUNTIME"}
|
|
178
|
+
python3 "$REPO/scripts/meta-bridge-state.py" prepare --repo "$REPO" --asm "$ASM" \
|
|
179
|
+
${ENTWURF_PLUGIN_RUNTIME:+--plugin-runtime "$ENTWURF_PLUGIN_RUNTIME"}
|
|
174
180
|
|
|
175
181
|
# --- 1. assemble a self-contained, node-baked plugin ------------------------
|
|
176
182
|
rm -rf "$ASM"
|
|
@@ -251,6 +257,15 @@ claude mcp remove pi-tools-bridge -s user >/dev/null 2>&1 || true
|
|
|
251
257
|
# in node_modules/@junghanacs/entwurf) wires the STABLE `entwurf-bridge` bin shim; baking
|
|
252
258
|
# the pnpm store path here would go stale on any peer/version bump. A dev clone pins to
|
|
253
259
|
# this clone's start.sh. Both branches carry the same env desired_mcp() writes.
|
|
260
|
+
# #116 M3-b2 — the explicit plugin mode comes FIRST, and it is derived, not supplied. A Herdr
|
|
261
|
+
# plugin activates where nothing entwurf is on PATH, so the bare shim cannot resolve; the absolute
|
|
262
|
+
# bridge under the certified stable runtime can. `desired_mcp()` above is still the SSOT — this
|
|
263
|
+
# branch mirrors it, as the comment on the block already demanded.
|
|
264
|
+
if [ -n "${ENTWURF_PLUGIN_RUNTIME:-}" ]; then
|
|
265
|
+
claude mcp add -s user entwurf-bridge \
|
|
266
|
+
-e ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code \
|
|
267
|
+
-- "$ENTWURF_PLUGIN_RUNTIME/node_modules/.bin/entwurf-bridge" >/dev/null
|
|
268
|
+
else
|
|
254
269
|
case "$REPO" in
|
|
255
270
|
*/node_modules/@junghanacs/entwurf)
|
|
256
271
|
claude mcp add -s user entwurf-bridge \
|
|
@@ -261,6 +276,7 @@ case "$REPO" in
|
|
|
261
276
|
-e ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code \
|
|
262
277
|
-- bash "$REPO/mcp/entwurf-bridge/start.sh" >/dev/null ;;
|
|
263
278
|
esac
|
|
279
|
+
fi
|
|
264
280
|
# Capture, THEN match — and require BOTH the exit code and the content.
|
|
265
281
|
# `<cli> | grep -q` under `set -o pipefail` is a race, not a test: grep exits at the
|
|
266
282
|
# first match and closes the pipe, the still-writing CLI dies of SIGPIPE (141), and
|
|
@@ -283,7 +299,8 @@ echo "[meta-bridge-install] installed entwurf-bridge MCP (scope: user = global r
|
|
|
283
299
|
# Re-assert the repo-owned keyset through our stateful manager. The Claude CLI
|
|
284
300
|
# calls above are allowed to maintain their cache/registry files, but the
|
|
285
301
|
# operator-facing JSON keys are owned here so uninstall can be honest.
|
|
286
|
-
python3 "$REPO/scripts/meta-bridge-state.py" apply --repo "$REPO" --asm "$ASM"
|
|
302
|
+
python3 "$REPO/scripts/meta-bridge-state.py" apply --repo "$REPO" --asm "$ASM" \
|
|
303
|
+
${ENTWURF_PLUGIN_RUNTIME:+--plugin-runtime "$ENTWURF_PLUGIN_RUNTIME"}
|
|
287
304
|
|
|
288
305
|
# --- evidence ---------------------------------------------------------------
|
|
289
306
|
echo "--- claude plugin list ---"
|