@junghanacs/entwurf 0.31.0 → 0.33.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 +3 -2
- package/CHANGELOG.md +128 -0
- package/README.md +62 -117
- package/VERIFY.md +40 -21
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +112 -95
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +8 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +153 -205
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +6 -4
- package/mcp/entwurf-bridge/src/index.ts +1 -1
- package/package.json +14 -17
- package/pi/pi-durable/bootstrap.mjs +142 -19
- 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/upstream-pin.json +103 -11
- package/pi-extensions/lib/fresh-call-composition.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +8 -10
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +3 -3
- package/pi-extensions/lib/pi-durable-runtime.ts +197 -263
- package/pi-extensions/meta-bridge-pi-durable.ts +13 -7
- package/run.sh +519 -248
- package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
- package/scripts/check-fresh-call-dispatch.ts +1 -1
- package/scripts/check-gate-qualification.ts +86 -34
- package/scripts/check-herdr-activation.ts +5 -4
- package/scripts/check-pi-durable-contact.ts +26 -93
- package/scripts/check-pi-durable-receive.ts +22 -59
- package/scripts/check-pi-durable-send.ts +235 -75
- package/scripts/check-release-gate-outcomes.ts +21 -17
- package/scripts/emit-pi-durable-carrier.ts +355 -0
- package/scripts/lib/mutation-qualify.ts +21 -45
- package/scripts/lib/pi-durable-operator-guard.ts +89 -0
- package/scripts/lib/qualification-receipt.ts +7 -11
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/gate-qualification.json +13 -0
- package/scripts/mutants/mux-fresh-call.json +13 -13
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +442 -109
- package/scripts/mutants/pi-floor.json +24 -2
- package/scripts/mutants/release-gate.json +5 -3
- package/scripts/pi-durable-contact-host.mjs +3 -3
- package/scripts/pi-durable-receive-host.mjs +3 -3
- package/scripts/pi-durable-send-host.mjs +76 -5
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +3 -3
- package/scripts/smoke-setup-verdict.sh +15 -15
- package/scripts/smoke-user-scope-citizen.sh +1 -1
|
@@ -1,80 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-durable-runtime —
|
|
3
|
-
* (#129 W,
|
|
2
|
+
* pi-durable-runtime — whether THIS package's pi-durable carrier can run, and the resolver it runs
|
|
3
|
+
* under (#129 W, #130).
|
|
4
4
|
*
|
|
5
|
-
* The experimental durable app is
|
|
6
|
-
* `dist/experimental
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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).
|
|
10
11
|
*
|
|
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.
|
|
12
24
|
*
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
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).
|
|
15
27
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* untracked, the experimental source resolver is there, and the git-ignored model data matches the
|
|
19
|
-
* pin's recorded digests. Anything else is present-but-wrong, a named refusal — never a guess and
|
|
20
|
-
* never a repair. The index term is not optional: `git diff` compares the worktree to the INDEX, so a
|
|
21
|
-
* staged edit to any other tracked file — or a staged new file, which is not "untracked" either —
|
|
22
|
-
* would otherwise leave the diff equal to the patch.
|
|
23
|
-
*
|
|
24
|
-
* The diff bytes are compared, so the configuration knobs known to move them are pinned on the
|
|
25
|
-
* command line to the form the packaged patch is in: abbreviation length, prefixes, colour,
|
|
26
|
-
* external/textconv drivers, hunk algorithm and context, relative paths, and blank-context spacing
|
|
27
|
-
* (`diff.suppressBlankEmpty`; the packaged patch has blank context lines). That is a bounded list,
|
|
28
|
-
* not every knob git has: e.g. `diff.orderFile` (multi-file order) and line-ending conversion
|
|
29
|
-
* (`core.autocrlf`/`core.eol`) are not neutralized, and a byte change they cause reads as pin drift.
|
|
30
|
-
* Configuration is not disabled wholesale — `safe.directory` lives in global/system config.
|
|
31
|
-
*
|
|
32
|
-
* The overlay data (pin + patch) ships IN this package and is found relative to this module, at the
|
|
33
|
-
* two depths the module is shipped at: the source checkout (`<root>/pi-extensions/lib/`) and the
|
|
34
|
-
* compiled bridge closure (`<root>/mcp/entwurf-bridge/dist/pi-extensions/lib/`) — the same two-depth
|
|
35
|
-
* arithmetic `metaCapabilitiesFilePath` answers. The compiled durable units are named from that same
|
|
36
|
-
* root, so a launcher, a preflight and the bootstrap all read ONE package closure.
|
|
37
|
-
*
|
|
38
|
-
* Read-only: `git diff` and `git ls-files` run against a private COPY of the runtime's index
|
|
39
|
-
* (`GIT_INDEX_FILE`), with GIT_OPTIONAL_LOCKS=0. The copy is not ceremony: `git diff` was measured
|
|
40
|
-
* to refresh and rewrite a stat-dirty index even with optional locks off, so asking the operator's
|
|
41
|
-
* own index would mutate their checkout. No network, no install, no credential, settings or session read.
|
|
42
|
-
* Each `git` is a direct child with a wall-clock bound; one that does not answer in time is unverifiable.
|
|
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.
|
|
43
30
|
*/
|
|
44
31
|
|
|
45
|
-
import { spawnSync } from "node:child_process";
|
|
46
32
|
import { createHash } from "node:crypto";
|
|
47
33
|
import * as fs from "node:fs";
|
|
48
|
-
import
|
|
34
|
+
import { findPackageJSON } from "node:module";
|
|
49
35
|
import * as path from "node:path";
|
|
36
|
+
import { pathToFileURL } from "node:url";
|
|
50
37
|
|
|
51
|
-
/**
|
|
52
|
-
export const PI_DURABLE_RUNTIME_SEGMENTS = ["entwurf", "pi-durable", "runtime"] as const;
|
|
53
|
-
|
|
54
|
-
/** The upstream source resolver the durable app runs under, inside the runtime checkout. */
|
|
55
|
-
export const PI_DURABLE_RESOLVER_RELATIVE = path.join(
|
|
56
|
-
"packages",
|
|
57
|
-
"coding-agent",
|
|
58
|
-
"src",
|
|
59
|
-
"experimental",
|
|
60
|
-
"source-resolver.ts",
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
/** Every way the runtime can be refused. Stable contract: these cross the launcher, setup and the
|
|
38
|
+
/** Every way the carrier can be refused. Stable contract: these cross the launcher, setup and the
|
|
64
39
|
* fresh-call surfaces as named refusals. */
|
|
65
40
|
export type PiDurableRuntimeRejectReason =
|
|
66
41
|
| "pi-durable-package-incomplete"
|
|
67
|
-
| "pi-durable-
|
|
68
|
-
| "pi-durable-
|
|
69
|
-
| "pi-durable-
|
|
70
|
-
| "pi-durable-runtime-pin-drift"
|
|
71
|
-
| "pi-durable-runtime-modeldata-drift";
|
|
42
|
+
| "pi-durable-carrier-drift"
|
|
43
|
+
| "pi-durable-sdk-absent"
|
|
44
|
+
| "pi-durable-sdk-mismatch";
|
|
72
45
|
|
|
73
|
-
/** The package closure this module belongs to: the
|
|
46
|
+
/** The package closure this module belongs to: the pin, the carrier and the compiled durable units. */
|
|
74
47
|
export interface PiDurablePackageLayout {
|
|
75
48
|
readonly root: string;
|
|
76
49
|
readonly overlayDir: string;
|
|
77
|
-
/** The
|
|
50
|
+
/** The emitted upstream app files and their license. */
|
|
51
|
+
readonly carrierDir: string;
|
|
52
|
+
/** The `--import` resolver the launcher runs the bootstrap under. */
|
|
53
|
+
readonly resolver: string;
|
|
54
|
+
/** The relocation grammar the resolver imports. */
|
|
55
|
+
readonly relocation: string;
|
|
56
|
+
/** The packaged bootstrap the launcher executes under the resolver. */
|
|
78
57
|
readonly bootstrap: string;
|
|
79
58
|
/** The compiled durable contact (birth, doorbell, visible identity decoration). */
|
|
80
59
|
readonly adapter: string;
|
|
@@ -86,11 +65,15 @@ export interface PiDurablePackageLayout {
|
|
|
86
65
|
|
|
87
66
|
/** The layout rooted at `root`, as plain paths. */
|
|
88
67
|
export function piDurableLayoutAt(root: string): PiDurablePackageLayout {
|
|
68
|
+
const durable = path.join(root, "pi", "pi-durable");
|
|
89
69
|
const dist = path.join(root, "mcp", "entwurf-bridge", "dist");
|
|
90
70
|
return {
|
|
91
71
|
root,
|
|
92
|
-
overlayDir: path.join(
|
|
93
|
-
|
|
72
|
+
overlayDir: path.join(durable, "overlay"),
|
|
73
|
+
carrierDir: path.join(durable, "carrier"),
|
|
74
|
+
resolver: path.join(durable, "carrier-resolver.mjs"),
|
|
75
|
+
relocation: path.join(durable, "carrier-relocation.mjs"),
|
|
76
|
+
bootstrap: path.join(durable, "bootstrap.mjs"),
|
|
94
77
|
adapter: path.join(dist, "pi-extensions", "meta-bridge-pi-durable.js"),
|
|
95
78
|
bridgeEntry: path.join(dist, "mcp", "entwurf-bridge", "src", "index.js"),
|
|
96
79
|
deliverability: path.join(dist, "pi-extensions", "lib", "entwurf-deliverability.js"),
|
|
@@ -99,7 +82,8 @@ export function piDurableLayoutAt(root: string): PiDurablePackageLayout {
|
|
|
99
82
|
|
|
100
83
|
/**
|
|
101
84
|
* The package closure, found from where THIS module lives: the source depth first, then the compiled
|
|
102
|
-
* depth. `null` when neither carries the
|
|
85
|
+
* depth (`<root>/mcp/entwurf-bridge/dist/pi-extensions/lib/`). `null` when neither carries the pin —
|
|
86
|
+
* a packaging defect, refused by name.
|
|
103
87
|
*/
|
|
104
88
|
export function piDurablePackageLayout(from: string = import.meta.dirname): PiDurablePackageLayout | null {
|
|
105
89
|
for (const up of [
|
|
@@ -113,235 +97,189 @@ export function piDurablePackageLayout(from: string = import.meta.dirname): PiDu
|
|
|
113
97
|
return null;
|
|
114
98
|
}
|
|
115
99
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (typeof home === "string" && home.length > 0 && path.isAbsolute(home)) {
|
|
125
|
-
return path.join(home, ".local", "share", ...PI_DURABLE_RUNTIME_SEGMENTS);
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
100
|
+
/** What the verifier reads from the packaged pin. */
|
|
101
|
+
export interface PiDurableCarrierPin {
|
|
102
|
+
readonly files: readonly { readonly name: string; readonly sha256: string }[];
|
|
103
|
+
readonly license: { readonly sha256: string };
|
|
104
|
+
/** `@earendil-works/*` members: name → exact version. */
|
|
105
|
+
readonly members: Readonly<Record<string, string>>;
|
|
106
|
+
/** Third-party packages a carrier file imports itself: name → exact version. */
|
|
107
|
+
readonly direct: Readonly<Record<string, string>>;
|
|
128
108
|
}
|
|
129
109
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
readonly patches: readonly string[];
|
|
133
|
-
readonly modelData: {
|
|
134
|
-
readonly path: string;
|
|
135
|
-
readonly files: number;
|
|
136
|
-
readonly aggregateSha256: string;
|
|
137
|
-
readonly manifestSha256: string;
|
|
138
|
-
};
|
|
139
|
-
}
|
|
110
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
111
|
+
const EXACT = /^\d+\.\d+\.\d+$/;
|
|
140
112
|
|
|
141
|
-
/** The packaged pin and
|
|
142
|
-
export function
|
|
143
|
-
const pin = JSON.parse(fs.readFileSync(path.join(overlayDir, "upstream-pin.json"), "utf8"))
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const md = pin.modelData;
|
|
113
|
+
/** The packaged pin's carrier and SDK declarations. A malformed pin throws (crash, don't warn). */
|
|
114
|
+
export function readPiDurableCarrierPin(overlayDir: string): PiDurableCarrierPin {
|
|
115
|
+
const pin = JSON.parse(fs.readFileSync(path.join(overlayDir, "upstream-pin.json"), "utf8"));
|
|
116
|
+
const files = pin?.carrier?.files;
|
|
117
|
+
const license = pin?.carrier?.license;
|
|
118
|
+
const members = pin?.sdk?.members;
|
|
119
|
+
const direct = pin?.sdk?.direct;
|
|
120
|
+
const versions = (set: unknown) =>
|
|
121
|
+
typeof set === "object" && set !== null && Object.values(set).every((v) => typeof v === "string" && EXACT.test(v));
|
|
151
122
|
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
typeof
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
123
|
+
!Array.isArray(files) ||
|
|
124
|
+
files.length === 0 ||
|
|
125
|
+
!files.every((f) => typeof f?.name === "string" && /^[a-z0-9-]+\.js$/.test(f.name) && SHA256.test(f?.sha256)) ||
|
|
126
|
+
!SHA256.test(license?.sha256) ||
|
|
127
|
+
!versions(members) ||
|
|
128
|
+
Object.keys(members).length === 0 ||
|
|
129
|
+
!versions(direct)
|
|
158
130
|
) {
|
|
159
|
-
throw new Error("pi-durable pin:
|
|
131
|
+
throw new Error("pi-durable pin: carrier files/license digests and exact sdk members/direct versions are required");
|
|
160
132
|
}
|
|
161
|
-
return {
|
|
133
|
+
return { files, license, members, direct };
|
|
162
134
|
}
|
|
163
135
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"--no-relative",
|
|
178
|
-
"--diff-algorithm=myers",
|
|
179
|
-
"--indent-heuristic",
|
|
180
|
-
"-U3",
|
|
181
|
-
"--inter-hunk-context=0",
|
|
182
|
-
] as const;
|
|
183
|
-
|
|
184
|
-
/** Wall-clock bound for each local `git` the verifier runs (it also runs inside the bridge process). */
|
|
185
|
-
export const PI_DURABLE_GIT_TIMEOUT_MS = 30_000;
|
|
136
|
+
/** One physical dependency edge: `to` as resolved from `from`'s location. */
|
|
137
|
+
export interface PiDurableSdkEdge {
|
|
138
|
+
/** `carrier` or the member the edge leaves. */
|
|
139
|
+
readonly from: string;
|
|
140
|
+
readonly to: string;
|
|
141
|
+
readonly want: string;
|
|
142
|
+
/** The resolved package's version, or `null` when it does not resolve. */
|
|
143
|
+
readonly got: string | null;
|
|
144
|
+
/** The name the resolved package.json declares, or `null` — must be `to` itself. */
|
|
145
|
+
readonly name: string | null;
|
|
146
|
+
/** The resolved package directory, or `null`. */
|
|
147
|
+
readonly at: string | null;
|
|
148
|
+
}
|
|
186
149
|
|
|
187
|
-
/** What
|
|
150
|
+
/** What the installed package holds, read without changing it. */
|
|
188
151
|
export interface PiDurableRuntimeFacts {
|
|
189
|
-
|
|
190
|
-
readonly
|
|
191
|
-
/**
|
|
192
|
-
readonly
|
|
193
|
-
readonly
|
|
194
|
-
/** The index equals HEAD: nothing is staged. */
|
|
195
|
-
readonly indexMatchesHead: boolean;
|
|
196
|
-
readonly diffEqualsPatch: boolean;
|
|
197
|
-
readonly untracked: string;
|
|
198
|
-
readonly resolverPresent: boolean;
|
|
199
|
-
readonly modelDataFiles: number;
|
|
200
|
-
readonly modelDataAggregate: string;
|
|
201
|
-
readonly manifestSha256: string;
|
|
152
|
+
/** A carrier/resolver/relocation file that is not there. */
|
|
153
|
+
readonly missing: readonly string[];
|
|
154
|
+
/** A carrier file (or LICENSE) whose bytes are not the pinned ones. */
|
|
155
|
+
readonly drifted: readonly string[];
|
|
156
|
+
readonly edges: readonly PiDurableSdkEdge[];
|
|
202
157
|
}
|
|
203
158
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} catch {
|
|
210
|
-
// Bounded environment probe (Hard Rule 15's stated exception): absence is the answer.
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
})();
|
|
214
|
-
const base = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
215
|
-
const git = (args: string[], env: NodeJS.ProcessEnv = base) =>
|
|
216
|
-
spawnSync("git", ["-C", runtimeDir, ...args], {
|
|
217
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
218
|
-
env,
|
|
219
|
-
timeout: PI_DURABLE_GIT_TIMEOUT_MS,
|
|
220
|
-
});
|
|
221
|
-
const headRun = present ? git(["rev-parse", "HEAD"]) : undefined;
|
|
222
|
-
// A private copy of the index, so the refresh `git diff` performs lands in a file this process owns.
|
|
223
|
-
let scratch: string | undefined;
|
|
224
|
-
let cachedRun: ReturnType<typeof git> | undefined;
|
|
225
|
-
let diffRun: ReturnType<typeof git> | undefined;
|
|
226
|
-
let untrackedRun: ReturnType<typeof git> | undefined;
|
|
159
|
+
const sha256 = (data: Buffer): string => createHash("sha256").update(data).digest("hex");
|
|
160
|
+
|
|
161
|
+
/** `name` resolved from `fromFile` by Node's own package resolution: its version and directory. */
|
|
162
|
+
function resolveEdge(from: string, to: string, want: string, fromFile: string): PiDurableSdkEdge {
|
|
163
|
+
let manifest: string | undefined;
|
|
227
164
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const copy = path.join(scratch, "index");
|
|
233
|
-
fs.copyFileSync(index, copy);
|
|
234
|
-
const onCopy = { ...base, GIT_INDEX_FILE: copy };
|
|
235
|
-
// HEAD → index: exit 0 = nothing staged, 1 = something staged; anything else is no answer.
|
|
236
|
-
cachedRun = git(["diff", "--cached", "--quiet", "--no-ext-diff", "--no-relative"], onCopy);
|
|
237
|
-
diffRun = git(
|
|
238
|
-
["-c", "core.abbrev=8", "-c", "diff.suppressBlankEmpty=false", "diff", ...PI_DURABLE_DIFF_SHAPE],
|
|
239
|
-
onCopy,
|
|
240
|
-
);
|
|
241
|
-
untrackedRun = git(["ls-files", "--others", "--exclude-standard"], onCopy);
|
|
242
|
-
}
|
|
243
|
-
} finally {
|
|
244
|
-
if (scratch !== undefined) fs.rmSync(scratch, { recursive: true, force: true });
|
|
165
|
+
manifest = findPackageJSON(to, pathToFileURL(fromFile));
|
|
166
|
+
} catch {
|
|
167
|
+
// Bounded environment probe: an unresolvable package is the `got: null` answer.
|
|
168
|
+
manifest = undefined;
|
|
245
169
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
(cachedRun?.status === 0 || cachedRun?.status === 1) &&
|
|
249
|
-
diffRun?.status === 0 &&
|
|
250
|
-
untrackedRun?.status === 0;
|
|
251
|
-
const dataDir = path.join(runtimeDir, pin.modelData.path);
|
|
252
|
-
const dataFiles = (() => {
|
|
253
|
-
try {
|
|
254
|
-
return fs
|
|
255
|
-
.readdirSync(dataDir)
|
|
256
|
-
.filter((n) => n.endsWith(".json") && !n.startsWith("."))
|
|
257
|
-
.sort();
|
|
258
|
-
} catch {
|
|
259
|
-
return [];
|
|
260
|
-
}
|
|
261
|
-
})();
|
|
262
|
-
const manifest = path.join(dataDir, ".manifest.json");
|
|
170
|
+
if (manifest === undefined) return { from, to, want, got: null, name: null, at: null };
|
|
171
|
+
const json = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
263
172
|
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
untracked: untrackedRun?.stdout?.toString().trim() ?? "",
|
|
271
|
-
resolverPresent: present && fs.existsSync(path.join(runtimeDir, PI_DURABLE_RESOLVER_RELATIVE)),
|
|
272
|
-
modelDataFiles: dataFiles.length,
|
|
273
|
-
modelDataAggregate: sha256(
|
|
274
|
-
dataFiles.map((n) => `${sha256(fs.readFileSync(path.join(dataDir, n)))} ${n}\n`).join(""),
|
|
275
|
-
),
|
|
276
|
-
manifestSha256: fs.existsSync(manifest) ? sha256(fs.readFileSync(manifest)) : "absent",
|
|
173
|
+
from,
|
|
174
|
+
to,
|
|
175
|
+
want,
|
|
176
|
+
got: typeof json.version === "string" ? json.version : null,
|
|
177
|
+
name: typeof json.name === "string" ? json.name : null,
|
|
178
|
+
at: fs.realpathSync(path.dirname(manifest)),
|
|
277
179
|
};
|
|
278
180
|
}
|
|
279
181
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return "pi-durable-runtime-pin-drift";
|
|
182
|
+
export function inspectPiDurableRuntime(layout: PiDurablePackageLayout): PiDurableRuntimeFacts {
|
|
183
|
+
const pin = readPiDurableCarrierPin(layout.overlayDir);
|
|
184
|
+
const missing: string[] = [];
|
|
185
|
+
const drifted: string[] = [];
|
|
186
|
+
const pinned = [...pin.files, { name: "LICENSE", sha256: pin.license.sha256 }];
|
|
187
|
+
for (const file of pinned) {
|
|
188
|
+
const at = path.join(layout.carrierDir, file.name);
|
|
189
|
+
if (!fs.existsSync(at)) missing.push(path.relative(layout.root, at));
|
|
190
|
+
else if (sha256(fs.readFileSync(at)) !== file.sha256) drifted.push(path.relative(layout.root, at));
|
|
290
191
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
facts.modelDataAggregate !== pin.modelData.aggregateSha256 ||
|
|
294
|
-
facts.manifestSha256 !== pin.modelData.manifestSha256
|
|
295
|
-
) {
|
|
296
|
-
return "pi-durable-runtime-modeldata-drift";
|
|
192
|
+
for (const at of [layout.resolver, layout.relocation]) {
|
|
193
|
+
if (!fs.existsSync(at)) missing.push(path.relative(layout.root, at));
|
|
297
194
|
}
|
|
195
|
+
const edges: PiDurableSdkEdge[] = [];
|
|
196
|
+
// From the carrier's own location: the bare imports its files make resolve from there.
|
|
197
|
+
const base = path.join(layout.carrierDir, pin.files[0].name);
|
|
198
|
+
// Every member COPY the carrier can reach is walked, at any depth: a nested copy's own member
|
|
199
|
+
// dependencies are edges too. One physical directory is walked once (shared and duplicate copies
|
|
200
|
+
// are ordinary; a member cycle terminates), keyed by its realpath.
|
|
201
|
+
const pending: { name: string; at: string }[] = [];
|
|
202
|
+
for (const [name, want] of Object.entries({ ...pin.members, ...pin.direct })) {
|
|
203
|
+
const edge = resolveEdge("carrier", name, want, base);
|
|
204
|
+
edges.push(edge);
|
|
205
|
+
if (name in pin.members && edge.at !== null) pending.push({ name, at: edge.at });
|
|
206
|
+
}
|
|
207
|
+
const walked = new Set<string>();
|
|
208
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
209
|
+
if (walked.has(next.at)) continue;
|
|
210
|
+
walked.add(next.at);
|
|
211
|
+
const manifest = path.join(next.at, "package.json");
|
|
212
|
+
const deps = JSON.parse(fs.readFileSync(manifest, "utf8")).dependencies ?? {};
|
|
213
|
+
for (const dep of Object.keys(deps)
|
|
214
|
+
.filter((d) => d in pin.members)
|
|
215
|
+
.sort()) {
|
|
216
|
+
const edge = resolveEdge(next.name, dep, pin.members[dep], manifest);
|
|
217
|
+
edges.push(edge);
|
|
218
|
+
if (edge.at !== null) pending.push({ name: dep, at: edge.at });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { missing, drifted, edges };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const edgeText = (e: PiDurableSdkEdge): string =>
|
|
225
|
+
`${e.from} → ${e.to} wants ${e.want}, ${
|
|
226
|
+
e.got === null
|
|
227
|
+
? "resolves nothing"
|
|
228
|
+
: `binds ${e.name === e.to ? "" : `${e.name ?? "(unnamed)"}@`}${e.got} at ${e.at}`
|
|
229
|
+
}`;
|
|
230
|
+
|
|
231
|
+
/** The verdict over those facts, in repair order: `null` is "the carrier can run". */
|
|
232
|
+
export function piDurableRuntimeVerdict(
|
|
233
|
+
facts: PiDurableRuntimeFacts,
|
|
234
|
+
): { reason: PiDurableRuntimeRejectReason; detail: string } | null {
|
|
235
|
+
if (facts.missing.length > 0)
|
|
236
|
+
return { reason: "pi-durable-package-incomplete", detail: `missing ${facts.missing.join(", ")}` };
|
|
237
|
+
if (facts.drifted.length > 0)
|
|
238
|
+
return { reason: "pi-durable-carrier-drift", detail: `changed ${facts.drifted.join(", ")}` };
|
|
239
|
+
const absent = facts.edges.find((e) => e.got === null);
|
|
240
|
+
if (absent !== undefined) return { reason: "pi-durable-sdk-absent", detail: edgeText(absent) };
|
|
241
|
+
const mismatch = facts.edges.find((e) => e.got !== e.want || e.name !== e.to);
|
|
242
|
+
if (mismatch !== undefined) return { reason: "pi-durable-sdk-mismatch", detail: edgeText(mismatch) };
|
|
298
243
|
return null;
|
|
299
244
|
}
|
|
300
245
|
|
|
301
246
|
export type PiDurableRuntimeCheck =
|
|
302
|
-
| { ok: true;
|
|
303
|
-
| { ok: false; reason: PiDurableRuntimeRejectReason;
|
|
247
|
+
| { ok: true; resolver: string; edges: readonly PiDurableSdkEdge[] }
|
|
248
|
+
| { ok: false; reason: PiDurableRuntimeRejectReason; detail: string };
|
|
304
249
|
|
|
305
|
-
/** Locate and verify
|
|
306
|
-
export function checkPiDurableRuntime(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
const reason = piDurableRuntimeVerdict(inspectPiDurableRuntime(runtimeDir, layout.overlayDir), pin);
|
|
315
|
-
if (reason !== null) return { ok: false, reason, runtimeDir };
|
|
316
|
-
return { ok: true, runtimeDir, resolver: path.join(runtimeDir, PI_DURABLE_RESOLVER_RELATIVE) };
|
|
250
|
+
/** Locate and verify. The launcher, setup, the fresh preflight and the durable gates all ask this. */
|
|
251
|
+
export function checkPiDurableRuntime(layout: PiDurablePackageLayout | null): PiDurableRuntimeCheck {
|
|
252
|
+
if (layout === null) {
|
|
253
|
+
return { ok: false, reason: "pi-durable-package-incomplete", detail: "no pi-durable pin next to this code" };
|
|
254
|
+
}
|
|
255
|
+
const facts = inspectPiDurableRuntime(layout);
|
|
256
|
+
const verdict = piDurableRuntimeVerdict(facts);
|
|
257
|
+
if (verdict !== null) return { ok: false, ...verdict };
|
|
258
|
+
return { ok: true, resolver: layout.resolver, edges: facts.edges };
|
|
317
259
|
}
|
|
318
260
|
|
|
319
261
|
export const PI_DURABLE_RUNTIME_HINT: Record<PiDurableRuntimeRejectReason, string> = {
|
|
320
262
|
"pi-durable-package-incomplete":
|
|
321
|
-
"this Entwurf package
|
|
322
|
-
"pi-durable-
|
|
323
|
-
"
|
|
324
|
-
"pi-durable-
|
|
325
|
-
"
|
|
326
|
-
"pi-durable-
|
|
327
|
-
"
|
|
328
|
-
"pi-durable-runtime-pin-drift":
|
|
329
|
-
"the runtime is not the pin: HEAD, the overlay patch bytes, an untracked file or the source resolver differs from the packaged pi/pi-durable/overlay/upstream-pin.json — re-provision it at the pinned commit with exactly the packaged patch",
|
|
330
|
-
"pi-durable-runtime-modeldata-drift":
|
|
331
|
-
"the runtime's git-ignored model data is not the pinned bytes — restore the model data the pin records",
|
|
263
|
+
"this Entwurf package is missing part of its pi-durable carrier — reinstall Entwurf; nothing outside the package supplies it",
|
|
264
|
+
"pi-durable-carrier-drift":
|
|
265
|
+
"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",
|
|
266
|
+
"pi-durable-sdk-absent":
|
|
267
|
+
"a declared pi SDK package does not resolve from the carrier — reinstall Entwurf so its dependencies are installed",
|
|
268
|
+
"pi-durable-sdk-mismatch":
|
|
269
|
+
"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",
|
|
332
270
|
};
|
|
333
271
|
|
|
334
|
-
/** Exit codes of the CLI below, read by `run.sh`: 0 verified,
|
|
335
|
-
|
|
272
|
+
/** Exit codes of the CLI below, read by `run.sh`: 0 verified, 3 refused. There is no "absent"
|
|
273
|
+
* answer any more: the carrier ships in the package, so a missing piece is an incomplete install. */
|
|
274
|
+
export const PI_DURABLE_RUNTIME_EXIT = { verified: 0, refused: 3 } as const;
|
|
336
275
|
|
|
337
276
|
/**
|
|
338
|
-
* `resolve`: print the verified
|
|
339
|
-
* `<reason>: <hint>` (stderr) and exit
|
|
340
|
-
*
|
|
277
|
+
* `resolve`: print the verified carrier's resolver path (stdout) and exit 0, or print
|
|
278
|
+
* `<reason>: <detail> — <hint>` (stderr) and exit 3. The launcher execs node under that resolver;
|
|
279
|
+
* setup maps the two codes to PASS / FAIL.
|
|
341
280
|
*/
|
|
342
281
|
export function runPiDurableRuntimeCli(
|
|
343
282
|
argv: readonly string[],
|
|
344
|
-
env: NodeJS.ProcessEnv,
|
|
345
283
|
layout: PiDurablePackageLayout | null,
|
|
346
284
|
out: { stdout: (line: string) => void; stderr: (line: string) => void },
|
|
347
285
|
): number {
|
|
@@ -349,17 +287,13 @@ export function runPiDurableRuntimeCli(
|
|
|
349
287
|
out.stderr("usage: pi-durable-runtime resolve");
|
|
350
288
|
return 2;
|
|
351
289
|
}
|
|
352
|
-
const checked = checkPiDurableRuntime(
|
|
290
|
+
const checked = checkPiDurableRuntime(layout);
|
|
353
291
|
if (checked.ok) {
|
|
354
292
|
out.stdout(checked.resolver);
|
|
355
293
|
return PI_DURABLE_RUNTIME_EXIT.verified;
|
|
356
294
|
}
|
|
357
|
-
out.stderr(
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
return checked.reason === "pi-durable-runtime-absent"
|
|
361
|
-
? PI_DURABLE_RUNTIME_EXIT.absent
|
|
362
|
-
: PI_DURABLE_RUNTIME_EXIT.refused;
|
|
295
|
+
out.stderr(`${checked.reason}: ${checked.detail} — ${PI_DURABLE_RUNTIME_HINT[checked.reason]}`);
|
|
296
|
+
return PI_DURABLE_RUNTIME_EXIT.refused;
|
|
363
297
|
}
|
|
364
298
|
|
|
365
299
|
// RUN AS THE CLI ONLY WHEN THIS FILE IS THE PROCESS ENTRY. Decided by an argv comparison, as in
|
|
@@ -377,7 +311,7 @@ const invokedDirectly = (() => {
|
|
|
377
311
|
}
|
|
378
312
|
})();
|
|
379
313
|
if (invokedDirectly) {
|
|
380
|
-
process.exitCode = runPiDurableRuntimeCli(process.argv.slice(2),
|
|
314
|
+
process.exitCode = runPiDurableRuntimeCli(process.argv.slice(2), piDurablePackageLayout(), {
|
|
381
315
|
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
382
316
|
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
383
317
|
});
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* meta-bridge-pi-durable — the durable app's native contact, identity leaf (#129 L-identity).
|
|
3
3
|
*
|
|
4
4
|
* WHAT THIS IS. The long-running durable app (`@earendil-works/pi-coding-agent`
|
|
5
|
-
* `experimental/durable`,
|
|
6
|
-
*
|
|
5
|
+
* `experimental/durable`, which upstream omits from its npm distribution — Entwurf's package
|
|
6
|
+
* supplies it as a pinned carrier, #130, not as an operator prerequisite) owns its runtime, TUI,
|
|
7
|
+
* models/auth, storage, prompt and recovery. This module is the whole Entwurf side of its contact:
|
|
7
8
|
*
|
|
8
9
|
* createPiDurableContact({ bridgeEntry })
|
|
9
10
|
* .extension tools-only durable Extension, handed to the overlay's
|
|
@@ -13,8 +14,9 @@
|
|
|
13
14
|
* release the tools
|
|
14
15
|
*
|
|
15
16
|
* The bootstrap that composes the two lives with the overlay (`pi/pi-durable/bootstrap.mjs`), not
|
|
16
|
-
* here: this module never imports the durable runtime
|
|
17
|
-
* vendor checkout
|
|
17
|
+
* here: this module never imports or initializes the durable app runtime and carries no path into
|
|
18
|
+
* a vendor checkout. That is this module's import boundary, not the package's dependency list: the
|
|
19
|
+
* package does declare the exact `@earendil-works/pi-durable` (per the pin's SDK set) for its carrier (#130).
|
|
18
20
|
*
|
|
19
21
|
* WHY BIRTH IS AFTER OPEN, BEHIND A GATE. `[read @cd32f77]` The native session id is chosen
|
|
20
22
|
* inside `openDurable` (`runtime.ts:125`, `sessions.ts:18-55`), and `openDurable` calls
|
|
@@ -272,9 +274,13 @@ export interface PiDurableRoots {
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
// ---------------------------------------------------------------------------
|
|
275
|
-
// Durable surface, typed NARROWLY and locally
|
|
276
|
-
// `harness/types.ts
|
|
277
|
-
//
|
|
277
|
+
// Durable surface, typed NARROWLY and locally — read at pi-durable 1.0.2 @cd32f77 (#129,
|
|
278
|
+
// `harness/types.ts`; historical coordinates, not re-anchored here). The current supply is the
|
|
279
|
+
// carrier `pi/pi-durable/overlay/upstream-pin.json` names (1.1.0 at `abe508e`). `[read 2026-10-08]`
|
|
280
|
+
// v1.0.4 → v1.1.0 `harness/types.ts` adds `models`, `contextRetentionMs` and a `context()` options
|
|
281
|
+
// bag only; none of the shapes typed below moved.
|
|
282
|
+
// `defineExtension`/`defineTool` are identity functions (`harness/define.ts:6-17`), so a plain
|
|
283
|
+
// object is the extension.
|
|
278
284
|
// ---------------------------------------------------------------------------
|
|
279
285
|
|
|
280
286
|
/** `DurableView.session` — `runtime.ts:53`. */
|