@junghanacs/entwurf 0.33.0 → 0.34.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 -4
- package/CHANGELOG.md +162 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +9 -6
- package/VERIFY.md +20 -7
- package/docs/setup-clean-host.md +43 -4
- package/package.json +2 -2
- package/pi/pi-durable/bootstrap.mjs +111 -11
- package/pi/pi-durable/carrier/runtime.js +11 -14
- package/pi/pi-durable/carrier/tui.js +76 -11
- package/pi/pi-durable/overlay/runtime-contacts.patch +199 -7
- package/pi/pi-durable/overlay/upstream-pin.json +3 -3
- package/run.sh +42 -10
- package/scripts/check-gate-qualification.ts +62 -5
- package/scripts/check-herdr-activation.ts +130 -10
- package/scripts/check-herdr-plugin-build.ts +235 -6
- package/scripts/check-release-gate-outcomes.ts +1 -1
- package/scripts/emit-pi-durable-carrier.ts +61 -21
- package/scripts/herdr-activation.mjs +16 -0
- package/scripts/herdr-plugin-activate.mjs +112 -76
- package/scripts/herdr-plugin-deactivate.mjs +41 -2
- package/scripts/mutants/gate-qualification.json +24 -0
- package/scripts/mutants/herdr-activation.json +35 -0
- package/scripts/mutants/herdr-plugin-build.json +66 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +3 -3
- package/scripts/mutants/pi-durable.json +178 -11
package/run.sh
CHANGED
|
@@ -134,7 +134,7 @@ Usage:
|
|
|
134
134
|
./run.sh check-omp-birth-hook # #87 gate: drives the real OMP assembler into a temp dir, imports the ASSEMBLED index.ts into a MOCK omp host and fires session_start/session_switch. tui mints one backend:"omp" record + a sender marker keyed to the host's OWN pid (the one-process join) + the garden id on the status line; print/rpc/json mint NOTHING (hasUI is true on the rpc rows, as in the vendor); switch attaches on the same native id and mints the replacement on a new one; the CERTIFIED receiver reader still finds no marker. Also the four-root joint binding: extension and omp-labeled bridge child resolve the same sessions/mailbox/senders/receivers bundle against test-built literals (never the production resolver as its own oracle), under a poisoned PI_CODING_AGENT_DIR and under four distinct ENTWURF_META_* overrides; the override grammar is absolute-or-~ and both halves plus the doctor refuse anything else by name; and a drifted provenance label selects no root policy at all. Hermetic; no omp, no model turn
|
|
135
135
|
./run.sh check-pi-durable-contact # #129 L-identity gate, CHECKOUT-ONLY (an installed package refuses it in run_ts). Opens the REAL durable app from THIS checkout's verified carrier (pi/pi-durable/carrier/ under its resolver, against the checkout's pinned pi SDK set — refused by name first) with the pi-durable contact, births the citizen and runs its tools adapter-direct through a privately emitted branch bridge (ENTWURF_PI_DURABLE_BRIDGE_ENTRY; its registry must match this checkout). Matrix roots {default, explicit} x {fresh, --continue}: one record keyed by the durable session dir, sender marker on the spawned host pid, exactly one bridge child whose /proc PPID is that host, poisoned identity carriers kept from it, its four root carriers bound to the roots written, the receiver ARMED (raw marker owned by the host pid + start key as pi-durable-host, joined to the sender marker; removed on close), self = the record and replyable, peers from an outside bridge, same garden id on reopen; the bundle's compiled owner join must admit pi-durable-host. Reaps only the host/bridges it owns on any failure. Harness-bypassed: no model turn, no ToolTask. Missing inputs → SKIP (97), never a pass
|
|
136
136
|
./run.sh check-pi-durable-send # #129 L-send FIRST CELL, CHECKOUT-ONLY. Same input as check-pi-durable-contact. One native durable turn: the app's own controller input → GenerationTask → ToolTask → the contact's entwurf_v2 → the private bridge → a seeded self-fetch target (C2 recipe, owned by a separate idle child). S = a 127.0.0.1 scripted OpenAI-compatible endpoint configured through the app's documented models.json (dummy key, PI_OFFLINE); H = the native ToolTask path; V (a vendor turn) is NOT run. Oracles: the wire (tools offered, v2 schema = the bridge's tools/list, exactly two requests, the tool result + receipt in the second), the raw mailbox (.msg 1, signal, sender = the host record), the native view (one call, its settled result, root current). Not exactly-once, not recovery, not a recipient turn. Missing inputs → SKIP (97)
|
|
137
|
-
./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the
|
|
137
|
+
./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the six pi-durable beside tests (adapter, carrier/SDK-set verifier, preflight, owner join, carrier relocation/resolver/emitter/task footer, native-module file and directory ingress) — the pi-durable qualification lane's gate argv. Same files check-tests-beside-behavior already runs.
|
|
138
138
|
./run.sh check-pi-durable-receive # #129 L-receive FIRST CELL, CHECKOUT-ONLY. Same input (the bundle must carry the pi-durable-host owner join). The host driver gives the app NO input; a seeded claude-code sender (marker owned by a separate idle child, handed to a gate-owned sending bridge through ENTWURF_META_SENDER_MARKER) enqueues one message, the host's doorbell admits its notice to the ROOT conversation, and the scripted model drains it with entwurf_inbox_read. S withholds response #1 until the ENQUEUE receipt, the .delivered bytes, the signal, the host-owned receiver marker and the notice are captured. Oracles: receiver() admission, native view, raw archive (.delivered.read + lastReadAt), wire (exactly two requests, drain result = view), and after a green reap a forensic read-only SQLite copy (submission row by request id = reported id, root, input, done, placed entry = the notice). V NOT run. Missing inputs → SKIP (97)
|
|
139
139
|
./run.sh check-copilot-receive-arm # #82 RAIL 5 gate: the REAL receiver installer + the REAL extension.mjs forked with a stubbed SDK. Arms only after birth, marker owned by the WATCHER pid, self-fetch dispatch answer, doorbell carries the garden id and NOT the body, id-drift/foreign-parent refusals. Hermetic; no Copilot, no model turn
|
|
140
140
|
./run.sh check-copilot-launch # #82 RAIL 7 gate: the MANAGED launch `entwurf copilot`, driven through its public address against a FAKE VENDOR on a sandbox PATH. Receiver precondition refusals, EXTENSIONS token + operator token preservation, injected defaults before the `--` terminator, byte-identical argv, the 11 explicit permission/surface policy overrides that suppress `--yolo`, exec (not fork) pid identity, exit passthrough, recursion refusal. Hermetic; no Copilot, no model turn
|
|
@@ -208,7 +208,7 @@ Usage:
|
|
|
208
208
|
./run.sh smoke-entwurf-chain-live # LIVE cross-harness delivery chain: native Claude Code -> pi GPT -> pi ACP Sonnet -> mailbox terminus, proving sender identity/replyable at every hop and a real read receipt at the end. Prerequisites (claude on PATH, pi credentials per backend) report protocol SKIP, never a pass
|
|
209
209
|
./run.sh check-release-gate-outcomes # release-gate STEP OUTCOME protocol (P1): one skip exit code shared by the shell + TS halves, classifier never rounds a skip up to a pass, `--cut` refuses a MUST SKIP while a bare diagnostic stays exit 0, no LIVE smoke keeps the old exit-0 skip shape, and both real skip surfaces are INVOKED and observed to propagate the code
|
|
210
210
|
./run.sh check-gate-manifests # the HEAD of that qualification, on its own: runner self-test + the committed manifests validated against the origin index + the declared lane inventory. Executes ZERO committed mutants (only the self-test's synthetic fixture mutants, in tmp fixture repos) and never snapshots this repo (~20s), which is why it lives in check:hermetic while the body stays on CI/release-gate — three of the five reds qualification ever produced in CI died right here, before a mutant gate ran (#99 B-3)
|
|
211
|
-
./run.sh check-gate-qualification # kill-proof qualification (the gate-of-gates): runner self-test (classifier truth table + synthetic negatives incl. wrong-reason/hang/control-red/impurity) + committed mutant manifests (scripts/mutants/*.json) run in an isolated snapshot repo under control→mutant→restore→control; the real checkout is never written. Evidence = claim IDs + killed mutant IDs, never assertion counts
|
|
211
|
+
./run.sh check-gate-qualification # kill-proof qualification (the gate-of-gates): runner self-test (classifier truth table + synthetic negatives incl. wrong-reason/hang/control-red/impurity) + committed mutant manifests (scripts/mutants/*.json) run in an isolated snapshot repo under control→mutant→restore→control; the real checkout is never written. Evidence = claim IDs + killed mutant IDs, never assertion counts. Bare = the whole body (the prepared SHA's CI call); `--group '<argv JSON>' --receipt <abs path outside the repo>` = one affected exact-argv group, a PARTIAL receipt (development's run, #133)
|
|
212
212
|
./run.sh check-probe-ordering # §11-7 ordering-probe deterministic gate: raw-client SAMENESS pinned to backend.ts (sequence/args/timeouts/permission policy — the probe may never measure a lookalike), phase attribution incl. set-model, probe-mode fixture wire markers (delay honored, probeRunId REQUIRED, smoke-acp-mcp-live legacy compat), event-log door integrity (reserved keys refused at write; unknown marker name, broken sort axis, or a payload the classifier cannot judge on is MALFORMED, never a quiet event), and the §11-7 paired-verdict truth table (P0/I0 outside the space, phase-qualified D, B promotion ladder, C, A two-delay rule). Offline/deterministic; one product claim (no production prompt cutoff) is replant-qualified via scripts/mutants/probe-ordering.json, while the other assertions are direct [CHECK:*] contracts
|
|
213
213
|
./run.sh check-probe-cli-shim # §11-7-c B-name-snapshot PRODUCER gate: the CLI shim driven as a REAL process against fake CLIs (no API, no cost). Proves what a defect would buy — FABRICATED evidence (a malformed init is never reported as an empty name set; the boot report carries the true target path+sha256 the classifier verifies against the roster), a DESTROYED turn (byte transparency across mid-UTF8/CRLF/oversized/unterminated framing, exit-code fidelity, signal re-raise, inbound signal forwarding, stderr passthrough, stdout backpressure), and LEAKED operator state (exact-allowlist env scrub, no argv/env/prompt body in the log). Offline/deterministic; 20 direct [CHECK:*] contracts, deliberately no longer replant-qualified after #70 subtraction
|
|
214
214
|
./run.sh smoke-agy-statusline-state # agy ambient garden-id statusLine install/doctor/inverse regression. Offline/deterministic
|
|
@@ -294,7 +294,7 @@ Usage:
|
|
|
294
294
|
./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
|
|
295
295
|
./run.sh install-user-scope [--plugin-runtime <root>] # #116 M3-b2 PLUGIN-ONLY forward seam: compose ONLY the global pi citizen (user-scope packages[] + provider), never a project `.pi/settings.json` — a Herdr plugin activation has no project and must not invent one. With --plugin-runtime the provider's managed command becomes the absolute bridge under that certified stable runtime (a clean host has nothing entwurf on PATH); without it every byte is the historical bare-bin install. Inverse: remove-user-scope
|
|
296
296
|
./run.sh herdr-plugin-activate <backend...> # #116 M3-b2 public forward activation. Takes a subset of {pi, claude-code} and NOTHING else — the stable runtime root is DERIVED from XDG here and handed down, and both harness writers accept a named root only when it IS that derived one. Certifies the runtime-ready journal AND the installed tree, certifies the existing ledger and its recorded harness roots, plans ADD-ONLY (a backend absent from this request is retained, never removed), preflights every selected component before the first byte, then writes each component `pending` BEFORE mutating and checkpoints `active` the moment its writer returns — so a run that dies halfway leaves a record of how far it got. Which backends is M3-b3's question: this verb is handed the answer, it does not read Herdr's integration status
|
|
297
|
-
./run.sh herdr-plugin-deactivate # #116 M3-b2 public teardown of a Herdr-plugin activation. Herdr's `plugin uninstall` deletes its checkout and calls NO cleanup hook, so the harness wiring and the stable runtime are retained, not cleaned — this is the shipped verb that takes them back. Certifies the activation ledger, the resolved Pi/Claude roots and BOTH component inverses read-only first; mutates only when every preflight is green; then Claude -> Pi user scope -> runtime -> ledger LAST. A component failure stops with the runtime and ledger intact as retry authority. Static imports only: the process deletes the runtime it is running from, and a lazy import after that fails
|
|
297
|
+
./run.sh herdr-plugin-deactivate # #116 M3-b2 public teardown of a Herdr-plugin activation. Herdr's `plugin uninstall` deletes its checkout and calls NO cleanup hook, so the harness wiring and the stable runtime are retained, not cleaned — this is the shipped verb that takes them back. Certifies the activation ledger, the resolved Pi/Claude roots and BOTH component inverses read-only first; mutates only when every preflight is green; then Claude -> Pi user scope -> runtime -> ledger LAST. A component failure stops with the runtime and ledger intact as retry authority. With NO ledger it removes nothing: a clean host exits 0, and anything found at the plugin's journal/runtime/cache addresses is named with a non-zero exit (#135). Static imports only: the process deletes the runtime it is running from, and a lazy import after that fails
|
|
298
298
|
./run.sh remove-user-scope # explicit GLOBAL inverse of install's user-scope citizen: drop entwurf from ~/.pi/agent/settings.json packages[] (affects ALL cwds — shared entry, not per-project). #86 C2: same-owner-only — a LIVE foreign owner refuses; a MISSING owner is removed only when package entry + package state + provider installerRoot all align (reported orphan cleanup)
|
|
299
299
|
./run.sh takeover-user-scope # #86 C2: operator-EXPLICIT ownership move of the shared user-scope registration to THIS root (old→new reported, packages[] entry + provider installerRoot together). The only writer over another owner — normal install/setup/remove refuse instead. No --force exists
|
|
300
300
|
./run.sh doctor-pi-package # #86 C2: package-side user-scope ownership verdict — unregistered / owned / owned-by-other(live) / legacy-no-state / mismatch / missing-owner (nonzero on defect verdicts). Provider runtime verdicts stay with doctor-pi-provider
|
|
@@ -1520,8 +1520,8 @@ check_pi_durable_beside() {
|
|
|
1520
1520
|
# from re-running every beside test. The six files are one contact: the adapter and its packaged
|
|
1521
1521
|
# bootstrap, the installed carrier/SDK-set verifier (which also reads the three checkout gates as
|
|
1522
1522
|
# SOURCE), the fresh preflight, the owner join that admits the durable host, the carrier's
|
|
1523
|
-
# relocation grammar, resolver and maintainer emitter (#130)
|
|
1524
|
-
# --native-module ingress (#130 P2).
|
|
1523
|
+
# relocation grammar, resolver and maintainer emitter (#130) with the carrier's task footer (#134),
|
|
1524
|
+
# and the bootstrap's explicit --native-module ingress (#130 P2) and its --native-module-dir (#134).
|
|
1525
1525
|
run_vitest pi-extensions/meta-bridge-pi-durable.test.ts pi-extensions/lib/pi-durable-runtime.test.ts pi-extensions/lib/pi-durable-fresh-preflight.test.ts pi-extensions/lib/entwurf-deliverability.owner-join.test.ts pi-extensions/lib/pi-durable-carrier.test.ts pi-extensions/lib/pi-durable-native-module.test.ts
|
|
1526
1526
|
}
|
|
1527
1527
|
|
|
@@ -4025,8 +4025,8 @@ ORACLE
|
|
|
4025
4025
|
# copied from this checkout into the sandbox — it never ships (test/ is outside the package files).
|
|
4026
4026
|
# Its initialization appends `init` to the receipt this cell names; the same public launch must then
|
|
4027
4027
|
# reach the same carrier model refusal, so the installed bootstrap loaded the module before the
|
|
4028
|
-
# runtime and the native registry installed its extension (carrier/runtime.js
|
|
4029
|
-
# refusal (runtime.js
|
|
4028
|
+
# runtime and the native registry installed its extension (carrier/runtime.js 148) before that
|
|
4029
|
+
# refusal (runtime.js 164). A module claiming an extension name the app installs itself is refused by
|
|
4030
4030
|
# name before anything opens: no durable session directory, no citizen, in a fresh sandbox home.
|
|
4031
4031
|
# Native SDK file opens here are ordinary (the module runs in the native process); none is judged.
|
|
4032
4032
|
local mod_dir="$box/pd-module" mod_home="$box/pd-module-home" mod_env=()
|
|
@@ -4048,6 +4048,32 @@ ORACLE
|
|
|
4048
4048
|
fi
|
|
4049
4049
|
echo "[check-pack-install] pi-durable native module: the installed bootstrap initialized one explicit module before the carrier's runtime (then the same model refusal), and refused a reserved extension name before anything opened"
|
|
4050
4050
|
|
|
4051
|
+
# (c3) #134: the paired --native-module-dir on the INSTALLED package. One named directory holding the
|
|
4052
|
+
# same synthetic module under a matching name beside a helper and a dotfile that would throw if
|
|
4053
|
+
# opened: the installed bootstrap must read only the top-level *.extension.mjs, initialize it once
|
|
4054
|
+
# (one `init` receipt) and reach the same carrier model refusal. A directory without a matching
|
|
4055
|
+
# module is refused by name before anything opens: no durable session directory, no citizen.
|
|
4056
|
+
local dir_mods="$box/pd-module-dir" dir_empty="$box/pd-module-dir-empty" dir_home="$box/pd-module-dir-home" dir_env=()
|
|
4057
|
+
mkdir -p "$dir_mods" "$dir_empty" "$dir_home/.pi/agent"
|
|
4058
|
+
cp "$REPO_DIR/test/fixtures/pi-durable-native-module/mock.mjs" "$dir_mods/10-mock.extension.mjs"
|
|
4059
|
+
printf 'throw new Error("pack-install: helper opened");\n' > "$dir_mods/helper.mjs"
|
|
4060
|
+
printf 'throw new Error("pack-install: dotfile opened");\n' > "$dir_mods/.hidden.extension.mjs"
|
|
4061
|
+
cp "$dir_mods/helper.mjs" "$dir_empty/index.mjs"
|
|
4062
|
+
rc=0; out=$(cd "$launch_cwd" && env "${launch_env[@]}" PI_OFFLINE=1 MOCK_NATIVE_RECEIPT="$dir_mods/receipt.log" "$l1s/node_modules/.bin/entwurf" pi-durable --provider entwurf-pack-probe --model no-such-model --width task-wide --native-module-dir "$dir_mods" 2>&1 </dev/null) || rc=$?
|
|
4063
|
+
if [ "$rc" -eq 0 ] || [ "$(cat "$dir_mods/receipt.log" 2>/dev/null)" != "init" ] || ! grep -q 'Could not resolve model: Unknown provider "entwurf-pack-probe"' <<<"$out" || ! grep -q "at openDurable (file://.*/pi/pi-durable/carrier/runtime.js" <<<"$out" || grep -q -e "native-module-" -e "pack-install: .* opened" <<<"$out"; then
|
|
4064
|
+
fail "[QK:PACK-INSTALL-DURABLE-NATIVE-MODULE-DIR] [check-pack-install] the installed 'entwurf pi-durable --native-module-dir' did not initialize exactly its one top-level *.extension.mjs and then reach the carrier openDurable's own model refusal (rc=$rc, receipt $(cat "$dir_mods/receipt.log" 2>/dev/null | tr '\n' ' ')):"
|
|
4065
|
+
echo "$out" | tail -15 | sed 's/^/ /' >&2
|
|
4066
|
+
return 1
|
|
4067
|
+
fi
|
|
4068
|
+
mapfile -t dir_env < <(pack_install_consumer_env "$dir_home")
|
|
4069
|
+
rc=0; out=$(cd "$launch_cwd" && env "${dir_env[@]}" PI_OFFLINE=1 "$l1s/node_modules/.bin/entwurf" pi-durable --provider entwurf-pack-probe --model no-such-model --width task-wide --native-module-dir "$dir_empty" 2>&1 </dev/null) || rc=$?
|
|
4070
|
+
if [ "$rc" -eq 0 ] || ! grep -q "native-module-dir-empty: " <<<"$out" || grep -q -e "Unknown provider" -e "pack-install: .* opened" <<<"$out" || [ -e "$dir_home/.pi/agent/experimental" ] || [ -e "$dir_home/.pi/agent/meta-sessions" ]; then
|
|
4071
|
+
fail "[QK:PACK-INSTALL-DURABLE-NATIVE-MODULE-DIR] [check-pack-install] the installed launch did not refuse a directory without a module by name before anything opened (rc=$rc):"
|
|
4072
|
+
echo "$out" | tail -15 | sed 's/^/ /' >&2
|
|
4073
|
+
return 1
|
|
4074
|
+
fi
|
|
4075
|
+
echo "[check-pack-install] pi-durable module directory: the installed bootstrap initialized only the named directory's one top-level *.extension.mjs before the carrier's runtime (then the same model refusal), and refused a directory without a module before anything opened"
|
|
4076
|
+
|
|
4051
4077
|
# (d) Host vs native are two subjects. The installed package's native SDK set is the pin (above);
|
|
4052
4078
|
# the ordinary Pi HOST on PATH is judged by its OWN real `pi --version` against the derived range.
|
|
4053
4079
|
# Real host binaries, no forged version: a sandbox install of the previous pi generation must make the
|
|
@@ -4367,8 +4393,8 @@ _check_pack_install_impl() {
|
|
|
4367
4393
|
|
|
4368
4394
|
# Fresh-temp install smoke. Uses pnpm because that is what this
|
|
4369
4395
|
# repo packages with; --ignore-workspace stops it from re-attaching
|
|
4370
|
-
# to our pnpm-workspace.yaml; --ignore-scripts blocks
|
|
4371
|
-
# prepare
|
|
4396
|
+
# to our pnpm-workspace.yaml; --ignore-scripts blocks
|
|
4397
|
+
# prepare (and any future install scripts) from running inside
|
|
4372
4398
|
# the consumer project. The pi SDK is pinned to the CURRENT pi release
|
|
4373
4399
|
# baseline (the package.json exact production dependency — the pins below
|
|
4374
4400
|
# are what check-dep-versions binds to it) so the smoke matches the same
|
|
@@ -7483,7 +7509,13 @@ case "$cmd" in
|
|
|
7483
7509
|
# (HEAD + work-surface content hash asserted identical before/after). The runner is negative-controlled
|
|
7484
7510
|
# first: zero-match/multi-match/survived/wrong-reason/hang(+pgroup grandchild kill)/
|
|
7485
7511
|
# control-red/impurity all must classify red before any real manifest counts.
|
|
7486
|
-
|
|
7512
|
+
# Arguments reach the runner's own parser unchanged (#133): `--group '<argv JSON>'
|
|
7513
|
+
# --receipt <abs path>` is the affected PARTIAL a development lane runs, and a malformed,
|
|
7514
|
+
# missing or repeated flag is refused there before any body or snapshot. Dropping them
|
|
7515
|
+
# turned a requested PARTIAL into the whole body. Bare, it stays the whole body — the
|
|
7516
|
+
# prepared SHA's CI call. Never retry bare after a refusal.
|
|
7517
|
+
shift || true
|
|
7518
|
+
run_ts scripts/check-gate-qualification.ts "$@"
|
|
7487
7519
|
;;
|
|
7488
7520
|
check-probe-ordering)
|
|
7489
7521
|
# §11-7 ordering-probe gate (docs/acp-backend-rail.md §11-7): the raw-client seam is
|
|
@@ -49,7 +49,7 @@ import { createHash } from "node:crypto";
|
|
|
49
49
|
import * as fs from "node:fs";
|
|
50
50
|
import * as os from "node:os";
|
|
51
51
|
import * as path from "node:path";
|
|
52
|
-
import { fileURLToPath } from "node:url";
|
|
52
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
53
53
|
import {
|
|
54
54
|
classifyMutantRun,
|
|
55
55
|
countOccurrences,
|
|
@@ -1178,6 +1178,63 @@ function buildDeclaredOutputFixture(): string {
|
|
|
1178
1178
|
parsedCompose.expectPath === "/e.txt",
|
|
1179
1179
|
);
|
|
1180
1180
|
|
|
1181
|
+
// The operator's door to this parser is `run.sh check-gate-qualification` (#133): it
|
|
1182
|
+
// once dropped every argument, so a requested PARTIAL started the whole body. The
|
|
1183
|
+
// cells run a byte copy of this checkout's run.sh whose runner is a fixture: the REAL
|
|
1184
|
+
// parser, then a print where the body would start — so neither a refusal nor a mutated
|
|
1185
|
+
// wrapper can launch a body or a snapshot from here. Mutation acceptance stays the
|
|
1186
|
+
// runner's own; this proves only the door's control flow.
|
|
1187
|
+
const door = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualification-door-")));
|
|
1188
|
+
fs.mkdirSync(path.join(door, "scripts"));
|
|
1189
|
+
fs.copyFileSync(path.join(REPO_DIR, "run.sh"), path.join(door, "run.sh"));
|
|
1190
|
+
const parserUrl = pathToFileURL(path.join(REPO_DIR, "scripts", "lib", "qualification-receipt.ts")).href;
|
|
1191
|
+
fs.writeFileSync(
|
|
1192
|
+
path.join(door, "scripts", "check-gate-qualification.ts"),
|
|
1193
|
+
[
|
|
1194
|
+
`import { parseQualificationArgs } from ${JSON.stringify(parserUrl)};`,
|
|
1195
|
+
"const argv = process.argv.slice(2);",
|
|
1196
|
+
"let parsed;",
|
|
1197
|
+
"try { parsed = parseQualificationArgs(argv); }",
|
|
1198
|
+
'catch (err) { process.stderr.write("refused: " + (err instanceof Error ? err.message : String(err)) + "\\n"); process.exit(2); }',
|
|
1199
|
+
'process.stdout.write(JSON.stringify({ argv, parsed }) + "\\n");',
|
|
1200
|
+
"",
|
|
1201
|
+
].join("\n"),
|
|
1202
|
+
);
|
|
1203
|
+
const viaDoor = (argv: string[]) => {
|
|
1204
|
+
const r = spawnSync("bash", [path.join(door, "run.sh"), "check-gate-qualification", ...argv], {
|
|
1205
|
+
cwd: door,
|
|
1206
|
+
encoding: "utf8",
|
|
1207
|
+
env: { PATH: process.env.PATH ?? "", HOME: door },
|
|
1208
|
+
timeout: 30_000,
|
|
1209
|
+
});
|
|
1210
|
+
const started = r.stdout.trim() === "" ? undefined : JSON.parse(r.stdout);
|
|
1211
|
+
return { status: r.status, stderr: r.stderr, started };
|
|
1212
|
+
};
|
|
1213
|
+
const requested = ["--group", '["bash","run.sh","check pi *"]', "--receipt", "/x/r.json"];
|
|
1214
|
+
const partial = viaDoor(requested);
|
|
1215
|
+
const bare = viaDoor([]);
|
|
1216
|
+
ok(
|
|
1217
|
+
"[QK:QUALIFY-DOOR-FORWARDS-ARGV] run.sh check-gate-qualification hands --group/--receipt to the parser unchanged (a space and a glob inside the argv JSON included) and the parser reads one PARTIAL group; called bare it still reads the whole body — the prepared SHA's CI call",
|
|
1218
|
+
partial.status === 0 &&
|
|
1219
|
+
JSON.stringify(partial.started?.argv) === JSON.stringify(requested) &&
|
|
1220
|
+
partial.started?.parsed.mode === "group" &&
|
|
1221
|
+
partial.started.parsed.receiptPath === "/x/r.json" &&
|
|
1222
|
+
bare.status === 0 &&
|
|
1223
|
+
JSON.stringify(bare.started?.argv) === "[]" &&
|
|
1224
|
+
bare.started?.parsed.mode === "full",
|
|
1225
|
+
);
|
|
1226
|
+
const doorRefusals: Array<[string[], string]> = [
|
|
1227
|
+
[["--group", '["bash","run.sh","check-x"]'], "--group needs --receipt"],
|
|
1228
|
+
[["--group", '["a"]', "--group", '["a"]', "--receipt", "/x/r.json"], "repeated flag"],
|
|
1229
|
+
[["--grop", '["a"]', "--receipt", "/x/r.json"], "unknown argument"],
|
|
1230
|
+
[["--group", "check-x", "--receipt", "/x/r.json"], "JSON array"],
|
|
1231
|
+
];
|
|
1232
|
+
const refused = doorRefusals.map(([argv, naming]) => ({ naming, run: viaDoor(argv) }));
|
|
1233
|
+
ok(
|
|
1234
|
+
"[QK:QUALIFY-DOOR-REFUSES-BEFORE-BODY] through run.sh, a missing --receipt, a repeated flag, a misspelled flag and a non-array --group are each refused by name and nothing starts — never a whole body in place of the PARTIAL that was asked for",
|
|
1235
|
+
refused.every(({ naming, run }) => run.status === 2 && run.stderr.includes(naming) && run.started === undefined),
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1181
1238
|
const tmpRoot = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-receipt-selftest-")));
|
|
1182
1239
|
const { dir: origin, externalTarget } = buildFixtureOrigin();
|
|
1183
1240
|
try {
|
|
@@ -3405,17 +3462,17 @@ let manifestCount: number;
|
|
|
3405
3462
|
"pi-bridge-sender": 4,
|
|
3406
3463
|
"pi-mcp-bridge": 2,
|
|
3407
3464
|
"pi-mcp-register": 3,
|
|
3408
|
-
"pi-durable":
|
|
3465
|
+
"pi-durable": 58,
|
|
3409
3466
|
"copilot-receive": 20,
|
|
3410
3467
|
"entwurf-peers": 1,
|
|
3411
3468
|
"fresh-call-dispatch": 11,
|
|
3412
3469
|
"fresh-cut": 3,
|
|
3413
|
-
"gate-qualification":
|
|
3470
|
+
"gate-qualification": 10,
|
|
3414
3471
|
"herdr-placement": 13,
|
|
3415
3472
|
"herdr-plugin": 25,
|
|
3416
3473
|
"herdr-plugin-profile": 14,
|
|
3417
|
-
"herdr-activation":
|
|
3418
|
-
"herdr-plugin-build":
|
|
3474
|
+
"herdr-activation": 28,
|
|
3475
|
+
"herdr-plugin-build": 20,
|
|
3419
3476
|
"herdr-runtime-bootstrap": 31,
|
|
3420
3477
|
"herdr-fresh-call": 38,
|
|
3421
3478
|
"herdr-supply": 11,
|
|
@@ -27,6 +27,7 @@ import os from "node:os";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { pathToFileURL } from "node:url";
|
|
29
29
|
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
30
|
+
import { treeDigest } from "./lib/tree-digest.ts";
|
|
30
31
|
|
|
31
32
|
let passed = 0;
|
|
32
33
|
function ok(label: string, cond: boolean): void {
|
|
@@ -737,21 +738,12 @@ function ledgerWith(
|
|
|
737
738
|
const dataRoot = resolveEntwurfDataRoot(bare);
|
|
738
739
|
const states = piStatePaths(bare);
|
|
739
740
|
const piExpected = path.join(home, ".local", "share", "entwurf");
|
|
740
|
-
// The python writers derive the SAME stable root from the SAME environment.
|
|
741
|
-
const pyRoot = spawnSync(
|
|
742
|
-
"python3",
|
|
743
|
-
[
|
|
744
|
-
"-c",
|
|
745
|
-
"import sys; sys.path.insert(0, 'scripts'); from importlib import import_module; m = import_module('register-pi-provider'.replace('-', '_')) if False else None",
|
|
746
|
-
],
|
|
747
|
-
{ encoding: "utf8", env: bare },
|
|
748
|
-
);
|
|
749
741
|
ok(
|
|
750
742
|
"[QK:HAC-XDG-RESOLUTION-SHARED] every root comes from one resolution — `XDG_DATA_HOME`, else " +
|
|
751
743
|
"`$HOME/.local/share` — so a host with XDG unset is the same host to the teardown as it is to the writers. " +
|
|
752
744
|
'The first cut built its Pi state paths from `env.XDG_DATA_HOME || ""`, which on such a host pointed at ' +
|
|
753
745
|
"`/entwurf/...`: a preflight that looked at a file nobody writes and passed for the wrong reason " +
|
|
754
|
-
`(dataRoot=${dataRoot === piExpected} packageState=${path.relative(home, states.packageState)}
|
|
746
|
+
`(dataRoot=${dataRoot === piExpected} packageState=${path.relative(home, states.packageState)})`,
|
|
755
747
|
dataRoot === piExpected &&
|
|
756
748
|
states.packageState === path.join(piExpected, "pi-package", "install-state.json") &&
|
|
757
749
|
states.providerState === path.join(piExpected, "pi-provider", "install-state.json"),
|
|
@@ -1364,4 +1356,132 @@ function ledgerWith(
|
|
|
1364
1356
|
);
|
|
1365
1357
|
}
|
|
1366
1358
|
|
|
1359
|
+
// ── 26. both Pi owner records are asked where their writers keep them (#135 D1) ─
|
|
1360
|
+
{
|
|
1361
|
+
const env = world("pi-state-paths");
|
|
1362
|
+
const defaultData = path.join(env.HOME as string, ".local", "share");
|
|
1363
|
+
// An EMPTY XDG_DATA_HOME: run.sh and the runtime owner both fall back to $HOME/.local/share.
|
|
1364
|
+
const runEnv = { ...env, XDG_DATA_HOME: "" };
|
|
1365
|
+
seedRuntime({ ...env, XDG_DATA_HOME: defaultData });
|
|
1366
|
+
const agentSettings = path.join(env.PI_CODING_AGENT_DIR as string, "settings.json");
|
|
1367
|
+
fs.writeFileSync(agentSettings, '{"theme":"dark"}\n');
|
|
1368
|
+
const foreign = path.join(env.HOME as string, "dev", "entwurf");
|
|
1369
|
+
fs.mkdirSync(foreign, { recursive: true });
|
|
1370
|
+
const seeded = py(env, "register-pi-provider.py", [
|
|
1371
|
+
"install",
|
|
1372
|
+
agentSettings,
|
|
1373
|
+
foreign,
|
|
1374
|
+
"--scope",
|
|
1375
|
+
"user",
|
|
1376
|
+
"--state",
|
|
1377
|
+
path.join(defaultData, "entwurf", "pi-provider", "install-state.json"),
|
|
1378
|
+
]);
|
|
1379
|
+
assert.equal(seeded.status, 0, `fixture: seeding the provider owner failed: ${seeded.stderr}`);
|
|
1380
|
+
const layout = resolveActivationLayout(runEnv);
|
|
1381
|
+
const run = spawnSync("node", [ACTIVATE, "pi"], { encoding: "utf8", env: runEnv, cwd: env.HOME as string });
|
|
1382
|
+
ok(
|
|
1383
|
+
"[QK:HAC-PI-STATE-PATHS-SHARED] both Pi ownership preflights read the record where its writer keeps it: with " +
|
|
1384
|
+
"XDG_DATA_HOME set but EMPTY, run.sh writes to $HOME/.local/share, so the provider preflight must look there " +
|
|
1385
|
+
"too — the first cut built a RELATIVE path for the provider half alone, saw no owner, and let the ledger land " +
|
|
1386
|
+
`on a host the provider writer then refused (exit=${run.status} ledger=${fs.existsSync(layout.ledgerPath)} refusal=${JSON.stringify((run.stderr || "").trim().split("\n").pop())})`,
|
|
1387
|
+
run.status !== 0 &&
|
|
1388
|
+
(run.stderr || "").includes("activation-preflight-refused") &&
|
|
1389
|
+
(run.stderr || "").includes("pi-provider") &&
|
|
1390
|
+
!fs.existsSync(layout.ledgerPath),
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// ── 27. no ledger is not "nothing here" (#135) ─────────────────────────────────
|
|
1395
|
+
{
|
|
1396
|
+
const plugin = (env: NodeJS.ProcessEnv) => path.join(env.XDG_DATA_HOME as string, "entwurf", "herdr-plugin");
|
|
1397
|
+
const rows: [string, (env: NodeJS.ProcessEnv) => void][] = [
|
|
1398
|
+
[
|
|
1399
|
+
"installing-journal",
|
|
1400
|
+
(env) => {
|
|
1401
|
+
fs.mkdirSync(path.join(plugin(env), "runtime"), { recursive: true });
|
|
1402
|
+
fs.writeFileSync(
|
|
1403
|
+
path.join(plugin(env), "journal.json"),
|
|
1404
|
+
`${JSON.stringify({
|
|
1405
|
+
schemaVersion: 2,
|
|
1406
|
+
phase: "installing",
|
|
1407
|
+
runtimeRoot: runtimeRootOf(env),
|
|
1408
|
+
artifactIdentity: { kind: "npm", name: PACKAGE, version: "0.21.0", expectedIntegrity: "sha512-fixture" },
|
|
1409
|
+
previousRuntime: null,
|
|
1410
|
+
})}\n`,
|
|
1411
|
+
);
|
|
1412
|
+
},
|
|
1413
|
+
],
|
|
1414
|
+
["ready-runtime", (env) => void seedRuntime(env)],
|
|
1415
|
+
[
|
|
1416
|
+
"cache-only",
|
|
1417
|
+
(env) =>
|
|
1418
|
+
fs.mkdirSync(path.join(env.XDG_CACHE_HOME as string, "entwurf", "herdr-plugin", "npm"), { recursive: true }),
|
|
1419
|
+
],
|
|
1420
|
+
["journal-less-runtime", (env) => fs.mkdirSync(path.join(plugin(env), "runtime", "active"), { recursive: true })],
|
|
1421
|
+
];
|
|
1422
|
+
const results = rows.map(([tag, make]) => {
|
|
1423
|
+
const env = world(`no-ledger-${tag}`);
|
|
1424
|
+
make(env);
|
|
1425
|
+
const before = treeDigest(env.HOME as string);
|
|
1426
|
+
const run = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env });
|
|
1427
|
+
return {
|
|
1428
|
+
tag,
|
|
1429
|
+
status: run.status,
|
|
1430
|
+
named: (run.stderr || "").includes("deactivate-unattributed-leftovers"),
|
|
1431
|
+
unchanged: before === treeDigest(env.HOME as string),
|
|
1432
|
+
};
|
|
1433
|
+
});
|
|
1434
|
+
const clean = world("no-ledger-clean");
|
|
1435
|
+
const cleanRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env: clean });
|
|
1436
|
+
ok(
|
|
1437
|
+
"[QK:HAC-NO-LEDGER-LEFTOVERS-REPORTED] without a ledger the teardown no longer says 'nothing to undo' over what " +
|
|
1438
|
+
"an aborted install left: an installing journal, a ready runtime, our npm cache, or a runtime with no journal " +
|
|
1439
|
+
"are each NAMED and the verb exits non-zero with every byte untouched — nothing proves what they belong to, so " +
|
|
1440
|
+
"nothing is removed. A truly clean host still exits 0 with the old line " +
|
|
1441
|
+
`(rows=${JSON.stringify(results)} clean=${cleanRun.status}/${JSON.stringify((cleanRun.stdout || "").trim())})`,
|
|
1442
|
+
results.every((r) => r.status !== 0 && r.named && r.unchanged) &&
|
|
1443
|
+
cleanRun.status === 0 &&
|
|
1444
|
+
(cleanRun.stdout || "").includes("no certified activation ledger — nothing to undo"),
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// ── 28. only a regular file — or nothing — may stand at the ledger address (#135) ─
|
|
1449
|
+
{
|
|
1450
|
+
const env = world("ledger-kinds");
|
|
1451
|
+
seedRuntime(env);
|
|
1452
|
+
const layout = resolveActivationLayout(env);
|
|
1453
|
+
fs.mkdirSync(layout.stateRoot, { recursive: true });
|
|
1454
|
+
// a DANGLING symlink: readFileSync used to answer ENOENT, i.e. "no ledger".
|
|
1455
|
+
fs.symlinkSync(path.join(env.HOME as string, "nowhere.json"), layout.ledgerPath);
|
|
1456
|
+
const dangling = refusal(() => readCertifiedLedger(layout));
|
|
1457
|
+
const danglingRun = spawnSync("node", [DEACTIVATE], { encoding: "utf8", env });
|
|
1458
|
+
fs.unlinkSync(layout.ledgerPath);
|
|
1459
|
+
// a symlink to a perfectly valid ledger is still not OUR ledger file.
|
|
1460
|
+
const elsewhere = path.join(env.HOME as string, "ledger-elsewhere.json");
|
|
1461
|
+
fs.writeFileSync(elsewhere, `${JSON.stringify({ schemaVersion: 2, ...ledgerFor(env, ["pi"]) }, null, 2)}\n`);
|
|
1462
|
+
fs.symlinkSync(elsewhere, layout.ledgerPath);
|
|
1463
|
+
const linked = refusal(() => readCertifiedLedger(layout));
|
|
1464
|
+
fs.unlinkSync(layout.ledgerPath);
|
|
1465
|
+
fs.mkdirSync(layout.ledgerPath);
|
|
1466
|
+
const directory = refusal(() => readCertifiedLedger(layout));
|
|
1467
|
+
// a state root that is a FILE: lstat answers ENOTDIR, which must stay a named refusal, not a raw errno.
|
|
1468
|
+
const fileRoot = path.join(env.HOME as string, "state-root-is-a-file");
|
|
1469
|
+
fs.writeFileSync(fileRoot, "not a directory\n");
|
|
1470
|
+
const notDir = refusal(() =>
|
|
1471
|
+
readCertifiedLedger({ stateRoot: fileRoot, ledgerPath: path.join(fileRoot, "activation.json") }),
|
|
1472
|
+
);
|
|
1473
|
+
ok(
|
|
1474
|
+
"[QK:HAC-LEDGER-PATH-KIND] the ledger address is read by lstat first: a dangling symlink, a symlink to a valid " +
|
|
1475
|
+
"ledger elsewhere, a directory, and a state root that is a file are each refused by name — none of them is " +
|
|
1476
|
+
"'no ledger', which is what a dangling link used to read as, letting every verb act as if nothing had been " +
|
|
1477
|
+
`activated (dangling=${dangling}/teardown-exit=${danglingRun.status} linked=${linked} directory=${directory} not-dir=${notDir})`,
|
|
1478
|
+
dangling === "activation-ledger-uncertified" &&
|
|
1479
|
+
danglingRun.status !== 0 &&
|
|
1480
|
+
(danglingRun.stderr || "").includes("activation-ledger-uncertified") &&
|
|
1481
|
+
linked === "activation-ledger-uncertified" &&
|
|
1482
|
+
directory === "activation-ledger-uncertified" &&
|
|
1483
|
+
notDir === "activation-ledger-uncertified",
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1367
1487
|
console.log(`\ncheck-herdr-activation: ${passed} assertions passed`);
|