@junghanacs/entwurf 0.30.1 → 0.32.0
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 -2
- package/CHANGELOG.md +194 -0
- package/DELIVERY.md +1 -0
- package/README.md +64 -14
- package/VERIFY.md +36 -1
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +121 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +236 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +746 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +19 -19
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +404 -0
- package/pi/pi-durable/carrier/LICENSE +21 -0
- package/pi/pi-durable/carrier/harness-setup.js +123 -0
- package/pi/pi-durable/carrier/prompt.js +74 -0
- package/pi/pi-durable/carrier/runtime.js +402 -0
- package/pi/pi-durable/carrier/sessions.js +56 -0
- package/pi/pi-durable/carrier/subagent.js +55 -0
- package/pi/pi-durable/carrier/tui.js +655 -0
- package/pi/pi-durable/carrier-relocation.mjs +160 -0
- package/pi/pi-durable/carrier-resolver.mjs +38 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +108 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
- package/pi-extensions/lib/fresh-call-composition.ts +42 -7
- package/pi-extensions/lib/meta-sender-identity.ts +7 -1
- package/pi-extensions/lib/meta-session.ts +28 -2
- package/pi-extensions/lib/mux-fresh-call.ts +32 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +318 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1024 -0
- package/run.sh +624 -184
- package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +87 -34
- package/scripts/check-herdr-activation.ts +5 -4
- package/scripts/check-pi-durable-contact.ts +767 -0
- package/scripts/check-pi-durable-receive.ts +1079 -0
- package/scripts/check-pi-durable-send.ts +1821 -0
- package/scripts/emit-pi-durable-carrier.ts +355 -0
- package/scripts/lib/mutation-qualify.ts +21 -45
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
- package/scripts/lib/pi-durable-operator-guard.ts +89 -0
- package/scripts/lib/pi-durable-r1.ts +100 -0
- package/scripts/lib/pi-durable-reap.ts +27 -0
- package/scripts/lib/pi-durable-receive.ts +137 -0
- package/scripts/lib/qualification-receipt.ts +7 -11
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- package/scripts/mutants/gate-qualification.json +13 -0
- package/scripts/mutants/meta-hook-session-switch.json +4 -2
- package/scripts/mutants/mux-fresh-call.json +69 -3
- package/scripts/mutants/omp-birth.json +2 -2
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +723 -0
- package/scripts/mutants/pi-floor.json +24 -2
- package/scripts/pi-durable-contact-host.mjs +105 -0
- package/scripts/pi-durable-receive-host.mjs +127 -0
- package/scripts/pi-durable-send-host.mjs +284 -0
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +22 -3
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-durable-runtime — whether THIS package's pi-durable carrier can run, and the resolver it runs
|
|
3
|
+
* under (#129 W, #130).
|
|
4
|
+
*
|
|
5
|
+
* The experimental durable app is source-only upstream: published `@earendil-works/pi-coding-agent`
|
|
6
|
+
* ships no `dist/experimental`. Entwurf therefore ships the app itself as a CARRIER — the pinned
|
|
7
|
+
* upstream files, type-stripped and relocated (pi/pi-durable/carrier/, emitted only by the
|
|
8
|
+
* maintainer's scripts/emit-pi-durable-carrier.ts) — and declares the published SDK set it runs
|
|
9
|
+
* against as exact production dependencies. Nothing is provisioned beside the package: there is no
|
|
10
|
+
* runtime directory, no source checkout and no model data to copy (it rides the published pi-ai).
|
|
11
|
+
*
|
|
12
|
+
* The carrier counts as runnable only when, read from where this package is installed:
|
|
13
|
+
* - every pinned carrier file, the license, the resolver and its relocation grammar are present
|
|
14
|
+
* (else `pi-durable-package-incomplete`);
|
|
15
|
+
* - each carrier file and the license carry the pinned sha256 (else `pi-durable-carrier-drift`);
|
|
16
|
+
* - every declared SDK member and direct dependency resolves from the carrier, and every member
|
|
17
|
+
* copy the carrier can reach — at any depth, nested copies included — resolves each of its own
|
|
18
|
+
* member dependencies (else `pi-durable-sdk-absent`);
|
|
19
|
+
* - each of those PHYSICAL edges lands on a package that is the one it names, at exactly the
|
|
20
|
+
* declared version (else `pi-durable-sdk-mismatch`). The members declare each other with caret ranges, so semver
|
|
21
|
+
* satisfaction is not the contract — the bytes a carrier import actually binds are. Sharing one
|
|
22
|
+
* copy with a host install of the same version is an ordinary layout and passes; how many copies
|
|
23
|
+
* of a version exist is not asked.
|
|
24
|
+
*
|
|
25
|
+
* Every refusal names the first failing item, in repair order. This leaf is the native-set check
|
|
26
|
+
* only: the ordinary Pi HOST on PATH is a different subject (`pi_supported_range` in run.sh).
|
|
27
|
+
*
|
|
28
|
+
* Read-only: file reads and Node's own package resolution (`module.findPackageJSON`) from the
|
|
29
|
+
* carrier's location. No git, no network, no environment, no credential, settings or session read.
|
|
30
|
+
*/
|
|
31
|
+
import { createHash } from "node:crypto";
|
|
32
|
+
import * as fs from "node:fs";
|
|
33
|
+
import { findPackageJSON } from "node:module";
|
|
34
|
+
import * as path from "node:path";
|
|
35
|
+
import { pathToFileURL } from "node:url";
|
|
36
|
+
/** The layout rooted at `root`, as plain paths. */
|
|
37
|
+
export function piDurableLayoutAt(root) {
|
|
38
|
+
const durable = path.join(root, "pi", "pi-durable");
|
|
39
|
+
const dist = path.join(root, "mcp", "entwurf-bridge", "dist");
|
|
40
|
+
return {
|
|
41
|
+
root,
|
|
42
|
+
overlayDir: path.join(durable, "overlay"),
|
|
43
|
+
carrierDir: path.join(durable, "carrier"),
|
|
44
|
+
resolver: path.join(durable, "carrier-resolver.mjs"),
|
|
45
|
+
relocation: path.join(durable, "carrier-relocation.mjs"),
|
|
46
|
+
bootstrap: path.join(durable, "bootstrap.mjs"),
|
|
47
|
+
adapter: path.join(dist, "pi-extensions", "meta-bridge-pi-durable.js"),
|
|
48
|
+
bridgeEntry: path.join(dist, "mcp", "entwurf-bridge", "src", "index.js"),
|
|
49
|
+
deliverability: path.join(dist, "pi-extensions", "lib", "entwurf-deliverability.js"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The package closure, found from where THIS module lives: the source depth first, then the compiled
|
|
54
|
+
* depth (`<root>/mcp/entwurf-bridge/dist/pi-extensions/lib/`). `null` when neither carries the pin —
|
|
55
|
+
* a packaging defect, refused by name.
|
|
56
|
+
*/
|
|
57
|
+
export function piDurablePackageLayout(from = import.meta.dirname) {
|
|
58
|
+
for (const up of [
|
|
59
|
+
["..", ".."],
|
|
60
|
+
["..", "..", "..", "..", ".."],
|
|
61
|
+
]) {
|
|
62
|
+
const root = path.resolve(from, ...up);
|
|
63
|
+
const layout = piDurableLayoutAt(root);
|
|
64
|
+
if (fs.existsSync(path.join(layout.overlayDir, "upstream-pin.json")))
|
|
65
|
+
return layout;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
70
|
+
const EXACT = /^\d+\.\d+\.\d+$/;
|
|
71
|
+
/** The packaged pin's carrier and SDK declarations. A malformed pin throws (crash, don't warn). */
|
|
72
|
+
export function readPiDurableCarrierPin(overlayDir) {
|
|
73
|
+
const pin = JSON.parse(fs.readFileSync(path.join(overlayDir, "upstream-pin.json"), "utf8"));
|
|
74
|
+
const files = pin?.carrier?.files;
|
|
75
|
+
const license = pin?.carrier?.license;
|
|
76
|
+
const members = pin?.sdk?.members;
|
|
77
|
+
const direct = pin?.sdk?.direct;
|
|
78
|
+
const versions = (set) => typeof set === "object" && set !== null && Object.values(set).every((v) => typeof v === "string" && EXACT.test(v));
|
|
79
|
+
if (!Array.isArray(files) ||
|
|
80
|
+
files.length === 0 ||
|
|
81
|
+
!files.every((f) => typeof f?.name === "string" && /^[a-z0-9-]+\.js$/.test(f.name) && SHA256.test(f?.sha256)) ||
|
|
82
|
+
!SHA256.test(license?.sha256) ||
|
|
83
|
+
!versions(members) ||
|
|
84
|
+
Object.keys(members).length === 0 ||
|
|
85
|
+
!versions(direct)) {
|
|
86
|
+
throw new Error("pi-durable pin: carrier files/license digests and exact sdk members/direct versions are required");
|
|
87
|
+
}
|
|
88
|
+
return { files, license, members, direct };
|
|
89
|
+
}
|
|
90
|
+
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
91
|
+
/** `name` resolved from `fromFile` by Node's own package resolution: its version and directory. */
|
|
92
|
+
function resolveEdge(from, to, want, fromFile) {
|
|
93
|
+
let manifest;
|
|
94
|
+
try {
|
|
95
|
+
manifest = findPackageJSON(to, pathToFileURL(fromFile));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Bounded environment probe: an unresolvable package is the `got: null` answer.
|
|
99
|
+
manifest = undefined;
|
|
100
|
+
}
|
|
101
|
+
if (manifest === undefined)
|
|
102
|
+
return { from, to, want, got: null, name: null, at: null };
|
|
103
|
+
const json = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
104
|
+
return {
|
|
105
|
+
from,
|
|
106
|
+
to,
|
|
107
|
+
want,
|
|
108
|
+
got: typeof json.version === "string" ? json.version : null,
|
|
109
|
+
name: typeof json.name === "string" ? json.name : null,
|
|
110
|
+
at: fs.realpathSync(path.dirname(manifest)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function inspectPiDurableRuntime(layout) {
|
|
114
|
+
const pin = readPiDurableCarrierPin(layout.overlayDir);
|
|
115
|
+
const missing = [];
|
|
116
|
+
const drifted = [];
|
|
117
|
+
const pinned = [...pin.files, { name: "LICENSE", sha256: pin.license.sha256 }];
|
|
118
|
+
for (const file of pinned) {
|
|
119
|
+
const at = path.join(layout.carrierDir, file.name);
|
|
120
|
+
if (!fs.existsSync(at))
|
|
121
|
+
missing.push(path.relative(layout.root, at));
|
|
122
|
+
else if (sha256(fs.readFileSync(at)) !== file.sha256)
|
|
123
|
+
drifted.push(path.relative(layout.root, at));
|
|
124
|
+
}
|
|
125
|
+
for (const at of [layout.resolver, layout.relocation]) {
|
|
126
|
+
if (!fs.existsSync(at))
|
|
127
|
+
missing.push(path.relative(layout.root, at));
|
|
128
|
+
}
|
|
129
|
+
const edges = [];
|
|
130
|
+
// From the carrier's own location: the bare imports its files make resolve from there.
|
|
131
|
+
const base = path.join(layout.carrierDir, pin.files[0].name);
|
|
132
|
+
// Every member COPY the carrier can reach is walked, at any depth: a nested copy's own member
|
|
133
|
+
// dependencies are edges too. One physical directory is walked once (shared and duplicate copies
|
|
134
|
+
// are ordinary; a member cycle terminates), keyed by its realpath.
|
|
135
|
+
const pending = [];
|
|
136
|
+
for (const [name, want] of Object.entries({ ...pin.members, ...pin.direct })) {
|
|
137
|
+
const edge = resolveEdge("carrier", name, want, base);
|
|
138
|
+
edges.push(edge);
|
|
139
|
+
if (name in pin.members && edge.at !== null)
|
|
140
|
+
pending.push({ name, at: edge.at });
|
|
141
|
+
}
|
|
142
|
+
const walked = new Set();
|
|
143
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
144
|
+
if (walked.has(next.at))
|
|
145
|
+
continue;
|
|
146
|
+
walked.add(next.at);
|
|
147
|
+
const manifest = path.join(next.at, "package.json");
|
|
148
|
+
const deps = JSON.parse(fs.readFileSync(manifest, "utf8")).dependencies ?? {};
|
|
149
|
+
for (const dep of Object.keys(deps)
|
|
150
|
+
.filter((d) => d in pin.members)
|
|
151
|
+
.sort()) {
|
|
152
|
+
const edge = resolveEdge(next.name, dep, pin.members[dep], manifest);
|
|
153
|
+
edges.push(edge);
|
|
154
|
+
if (edge.at !== null)
|
|
155
|
+
pending.push({ name: dep, at: edge.at });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { missing, drifted, edges };
|
|
159
|
+
}
|
|
160
|
+
const edgeText = (e) => `${e.from} → ${e.to} wants ${e.want}, ${e.got === null
|
|
161
|
+
? "resolves nothing"
|
|
162
|
+
: `binds ${e.name === e.to ? "" : `${e.name ?? "(unnamed)"}@`}${e.got} at ${e.at}`}`;
|
|
163
|
+
/** The verdict over those facts, in repair order: `null` is "the carrier can run". */
|
|
164
|
+
export function piDurableRuntimeVerdict(facts) {
|
|
165
|
+
if (facts.missing.length > 0)
|
|
166
|
+
return { reason: "pi-durable-package-incomplete", detail: `missing ${facts.missing.join(", ")}` };
|
|
167
|
+
if (facts.drifted.length > 0)
|
|
168
|
+
return { reason: "pi-durable-carrier-drift", detail: `changed ${facts.drifted.join(", ")}` };
|
|
169
|
+
const absent = facts.edges.find((e) => e.got === null);
|
|
170
|
+
if (absent !== undefined)
|
|
171
|
+
return { reason: "pi-durable-sdk-absent", detail: edgeText(absent) };
|
|
172
|
+
const mismatch = facts.edges.find((e) => e.got !== e.want || e.name !== e.to);
|
|
173
|
+
if (mismatch !== undefined)
|
|
174
|
+
return { reason: "pi-durable-sdk-mismatch", detail: edgeText(mismatch) };
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/** Locate and verify. The launcher, setup, the fresh preflight and the durable gates all ask this. */
|
|
178
|
+
export function checkPiDurableRuntime(layout) {
|
|
179
|
+
if (layout === null) {
|
|
180
|
+
return { ok: false, reason: "pi-durable-package-incomplete", detail: "no pi-durable pin next to this code" };
|
|
181
|
+
}
|
|
182
|
+
const facts = inspectPiDurableRuntime(layout);
|
|
183
|
+
const verdict = piDurableRuntimeVerdict(facts);
|
|
184
|
+
if (verdict !== null)
|
|
185
|
+
return { ok: false, ...verdict };
|
|
186
|
+
return { ok: true, resolver: layout.resolver, edges: facts.edges };
|
|
187
|
+
}
|
|
188
|
+
export const PI_DURABLE_RUNTIME_HINT = {
|
|
189
|
+
"pi-durable-package-incomplete": "this Entwurf package is missing part of its pi-durable carrier — reinstall Entwurf; nothing outside the package supplies it",
|
|
190
|
+
"pi-durable-carrier-drift": "a pi-durable carrier file is not the bytes pi/pi-durable/overlay/upstream-pin.json records — reinstall Entwurf; the carrier is never edited in place",
|
|
191
|
+
"pi-durable-sdk-absent": "a declared pi SDK package does not resolve from the carrier — reinstall Entwurf so its dependencies are installed",
|
|
192
|
+
"pi-durable-sdk-mismatch": "a pi SDK package the carrier binds is not the declared version (another install's copy, an override or a partial upgrade) — reinstall Entwurf into a tree whose @earendil-works packages are the pinned version",
|
|
193
|
+
};
|
|
194
|
+
/** Exit codes of the CLI below, read by `run.sh`: 0 verified, 3 refused. There is no "absent"
|
|
195
|
+
* answer any more: the carrier ships in the package, so a missing piece is an incomplete install. */
|
|
196
|
+
export const PI_DURABLE_RUNTIME_EXIT = { verified: 0, refused: 3 };
|
|
197
|
+
/**
|
|
198
|
+
* `resolve`: print the verified carrier's resolver path (stdout) and exit 0, or print
|
|
199
|
+
* `<reason>: <detail> — <hint>` (stderr) and exit 3. The launcher execs node under that resolver;
|
|
200
|
+
* setup maps the two codes to PASS / FAIL.
|
|
201
|
+
*/
|
|
202
|
+
export function runPiDurableRuntimeCli(argv, layout, out) {
|
|
203
|
+
if (argv.length !== 1 || argv[0] !== "resolve") {
|
|
204
|
+
out.stderr("usage: pi-durable-runtime resolve");
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
const checked = checkPiDurableRuntime(layout);
|
|
208
|
+
if (checked.ok) {
|
|
209
|
+
out.stdout(checked.resolver);
|
|
210
|
+
return PI_DURABLE_RUNTIME_EXIT.verified;
|
|
211
|
+
}
|
|
212
|
+
out.stderr(`${checked.reason}: ${checked.detail} — ${PI_DURABLE_RUNTIME_HINT[checked.reason]}`);
|
|
213
|
+
return PI_DURABLE_RUNTIME_EXIT.refused;
|
|
214
|
+
}
|
|
215
|
+
// RUN AS THE CLI ONLY WHEN THIS FILE IS THE PROCESS ENTRY. Decided by an argv comparison, as in
|
|
216
|
+
// meta-bridge-hook-codex.ts, and not by the `main` flag on import.meta: that flag landed in Node 24.2.0
|
|
217
|
+
// and the package floor is major 24, so on 24.0/24.1 it is undefined — `resolve` would exit 0 having
|
|
218
|
+
// printed nothing, which setup reads as PASS and the launcher as a resolver.
|
|
219
|
+
const invokedDirectly = (() => {
|
|
220
|
+
const entry = process.argv[1];
|
|
221
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
222
|
+
return false;
|
|
223
|
+
try {
|
|
224
|
+
return fs.realpathSync(entry) === fs.realpathSync(import.meta.filename);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Bounded environment probe: an entry that cannot be resolved is not this file.
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
if (invokedDirectly) {
|
|
232
|
+
process.exitCode = runPiDurableRuntimeCli(process.argv.slice(2), piDurablePackageLayout(), {
|
|
233
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
234
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
235
|
+
});
|
|
236
|
+
}
|