@junghanacs/entwurf 0.12.0 → 0.12.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +88 -28
  3. package/docs/setup-clean-host.md +117 -219
  4. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +454 -0
  5. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +111 -0
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +1683 -0
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +121 -0
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +155 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +119 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +160 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +63 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +81 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +290 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +254 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +365 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +64 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +218 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +108 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-resume-marker.js +33 -0
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +116 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +125 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +184 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +237 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +216 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +164 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +66 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +1502 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +50 -0
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +259 -0
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-probe.js +81 -0
  32. package/mcp/entwurf-bridge/dist/protocol.js +29 -0
  33. package/mcp/entwurf-bridge/start.sh +49 -7
  34. package/mcp/entwurf-bridge/test.sh +12 -3
  35. package/mcp/entwurf-bridge/tsconfig.build.json +42 -0
  36. package/package.json +29 -9
  37. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +101 -0
  38. package/pi-extensions/lib/entwurf-v2-contract.ts +10 -78
  39. package/pi-extensions/lib/entwurf-v2-decider.ts +6 -2
  40. package/pi-extensions/lib/entwurf-v2-production.ts +26 -4
  41. package/run.sh +140 -15
  42. package/scripts/check-entwurf-bridge-pi-free.ts +146 -0
  43. package/scripts/check-entwurf-v2-contract.ts +6 -4
  44. package/scripts/smoke-acp-bundled-mcp-live.ts +13 -2
  45. package/scripts/smoke-acp-carrier-augment-live.ts +35 -19
@@ -0,0 +1,101 @@
1
+ /**
2
+ * entwurf-v2-contract-schema — the pi-side TypeBox REPRESENTATION of the frozen
3
+ * `entwurf_v2` contract. This module exists ONLY to carry the pi-ai TypeBox
4
+ * schema builders (`StringEnum`, `Type`) out of the pi-free core
5
+ * (`entwurf-v2-contract.ts`); the constants/types/decision logic live there.
6
+ *
7
+ * ⚠️ pi LANE ONLY — the MCP bridge (`mcp/entwurf-bridge/src/index.ts`) MUST NOT
8
+ * import this module. It value-imports `@earendil-works/pi-ai`, so reaching it
9
+ * from the bridge boot closure would re-couple the harness-neutral meta-bridge to
10
+ * pi (the `check-entwurf-bridge-pi-free` gate fails if it does). The bridge needs
11
+ * the contract CONSTANTS + `resolveDispatch` (pi-free core), never these schemas.
12
+ * Consumers: the pi MCP-tool param surface and `check-entwurf-v2-contract`.
13
+ *
14
+ * StringEnum (typebox 1.x) inside Type.Object (typebox 0.34) — the same mix the
15
+ * existing entwurf tools use. The logic types in the core are hand-written unions,
16
+ * NOT `Static<>` inferences, so the 0.34/1.x widening caveat does not touch them;
17
+ * `check-entwurf-v2-contract` keeps these schemas and the core types in lockstep.
18
+ */
19
+
20
+ import { StringEnum, Type } from "@earendil-works/pi-ai";
21
+ import {
22
+ ENTWURF_INTENTS,
23
+ ENTWURF_V2_ACTIONS,
24
+ ENTWURF_V2_MODES,
25
+ ENTWURF_V2_OWNERSHIPS,
26
+ ENTWURF_V2_REJECT_REASONS,
27
+ ENTWURF_V2_TRANSPORTS,
28
+ FACT_LIVENESSES,
29
+ } from "./entwurf-v2-contract.ts";
30
+ import { SESSION_ID_RE } from "./session-id.js";
31
+
32
+ export const EntwurfV2InputSchema = Type.Object(
33
+ {
34
+ // R2/F6 executable: the garden-id shape is enforced by pattern, not prose —
35
+ // a malformed/typo gid fails the schema (→ bad-target) and can never reach a
36
+ // spawn. SSOT regex = SESSION_ID_RE (pi-extensions/lib/session-id.js).
37
+ target: Type.String({
38
+ pattern: SESSION_ID_RE.source,
39
+ description:
40
+ "garden-id of an EXISTING citizen (pattern-enforced). spawn-new is out of v2 scope (legacy entwurf keeps it); a malformed/typo gid is bad-target.",
41
+ }),
42
+ intent: StringEnum(ENTWURF_INTENTS, {
43
+ description:
44
+ "caller's declared outcome contract (F1): fire-and-forget = ack only, owned-outcome = caller owns completion.",
45
+ }),
46
+ mode: Type.Optional(
47
+ StringEnum(ENTWURF_V2_MODES, {
48
+ description:
49
+ "delivery mode (steer = interrupt current turn, follow_up = queue) — NOT the ownership axis (F1) nor liveness routing. MEANINGLESS on the meta-mailbox transport (F-mailbox): a mailbox ack is enqueue+doorbell, not a turn injection, so steer/follow_up does not apply when the verdict transport is meta-mailbox.",
50
+ }),
51
+ ),
52
+ wantsReply: Type.Optional(
53
+ Type.Boolean({
54
+ description: "conversation etiquette only — NOT ownership; never triggers an auto-send (Q2).",
55
+ }),
56
+ ),
57
+ // `additionalProperties: false` — a frozen contract input is exact; an unknown
58
+ // key is a caller error, not silently ignored.
59
+ },
60
+ { additionalProperties: false },
61
+ );
62
+
63
+ // Receipt = a DISCRIMINATED union on `ok` (R3/F6) — NOT one flat object with
64
+ // optionals. Each branch is EXACT (`additionalProperties: false`): without it,
65
+ // JSON Schema's default admits extra keys, so an illegal receipt like
66
+ // {ok:true, ..., reason:"bad-target"} would validate against the success branch.
67
+ // With it, success carries action/transport/ownership and rejects a stray reason;
68
+ // reject carries reason and rejects any allow facet — the branches are mutually
69
+ // exclusive at the schema level, not merely by declared-property convention.
70
+ export const EntwurfV2ReceiptSuccessSchema = Type.Object(
71
+ {
72
+ ok: Type.Literal(true),
73
+ action: StringEnum(ENTWURF_V2_ACTIONS),
74
+ transport: StringEnum(ENTWURF_V2_TRANSPORTS),
75
+ ownership: StringEnum(ENTWURF_V2_OWNERSHIPS),
76
+ observedLiveness: StringEnum(FACT_LIVENESSES, {
77
+ description: "the 4-value fact liveness the verdict was computed from (R1/R3).",
78
+ }),
79
+ },
80
+ { additionalProperties: false },
81
+ );
82
+
83
+ export const EntwurfV2ReceiptRejectSchema = Type.Object(
84
+ {
85
+ ok: Type.Literal(false),
86
+ reason: StringEnum(ENTWURF_V2_REJECT_REASONS),
87
+ // ?6: required-nullable, NOT optional — a reject branch ALWAYS carries the
88
+ // key, and it is `null` for the pre-probe rejects (PRE_PROBE_REJECT_REASONS)
89
+ // and a real FactLiveness otherwise. Optional would lose the "key always
90
+ // present, value may be null" shape and weaken the discriminated union; the
91
+ // reason-dependent null/non-null rule is enforced semantically (the gate's
92
+ // rejectObservedLivenessWellFormed fixture), not by this blanket union.
93
+ observedLiveness: Type.Union([StringEnum(FACT_LIVENESSES), Type.Null()], {
94
+ description:
95
+ "the 4-value fact liveness the reject was computed from (R1/R3); null for the pre-probe rejects (bad-target / target-locked / target-address-conflict) where no probe ran.",
96
+ }),
97
+ },
98
+ { additionalProperties: false },
99
+ );
100
+
101
+ export const EntwurfV2ReceiptSchema = Type.Union([EntwurfV2ReceiptSuccessSchema, EntwurfV2ReceiptRejectSchema]);
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * entwurf-v2-contract — the FROZEN contract surface for the unified `entwurf_v2`
3
- * verb (0.11 Stage 0 step 4-pre / 동결결정 10). PURE: TypeBox schemas + the
3
+ * verb (0.11 Stage 0 step 4-pre / 동결결정 10). PURE pi-FREE core: the
4
4
  * intent×liveness decision table + the reject taxonomy + a pure resolver.
5
5
  * NO runtime dispatch, NO spawn/send, NO I/O — step 5 wires this to transports.
6
+ * The pi-ai TypeBox REPRESENTATION of this contract lives in the separate
7
+ * `entwurf-v2-contract-schema.ts` (0.12.1 B-1) so this module — which the
8
+ * harness-neutral MCP bridge reaches at boot — carries no pi dependency.
6
9
  *
7
10
  * Why a frozen contract BEFORE the fact-provider (step 4): with the legacy
8
11
  * 3-verb surface (`entwurf`/`entwurf_resume`/`entwurf_send`) still live, building
@@ -41,8 +44,6 @@
41
44
  * not prose.
42
45
  */
43
46
 
44
- import { StringEnum, Type } from "@earendil-works/pi-ai";
45
- import { SESSION_ID_RE } from "./session-id.js";
46
47
  import type { SocketLiveness } from "./socket-probe.ts";
47
48
 
48
49
  // ── Caller-declared intent (F1) ────────────────────────────────────────────
@@ -350,78 +351,9 @@ export function resolveDispatch(
350
351
  };
351
352
  }
352
353
 
353
- // ── TypeBox schemas (for step 5 MCP tool params + the gate's structural assert) ──
354
- // StringEnum (typebox 1.x) inside Type.Object (typebox 0.34) same mix the
355
- // existing entwurf tools use (entwurf-control.ts:92-95). The logic types above
356
- // are hand-written unions, NOT `Static<>` inferences, so the 0.34/1.x widening
357
- // caveat does not touch them; the gate keeps schema ↔ types in lockstep.
358
- export const EntwurfV2InputSchema = Type.Object(
359
- {
360
- // R2/F6 executable: the garden-id shape is enforced by pattern, not prose —
361
- // a malformed/typo gid fails the schema (→ bad-target) and can never reach a
362
- // spawn. SSOT regex = SESSION_ID_RE (pi-extensions/lib/session-id.js).
363
- target: Type.String({
364
- pattern: SESSION_ID_RE.source,
365
- description:
366
- "garden-id of an EXISTING citizen (pattern-enforced). spawn-new is out of v2 scope (legacy entwurf keeps it); a malformed/typo gid is bad-target.",
367
- }),
368
- intent: StringEnum(ENTWURF_INTENTS, {
369
- description:
370
- "caller's declared outcome contract (F1): fire-and-forget = ack only, owned-outcome = caller owns completion.",
371
- }),
372
- mode: Type.Optional(
373
- StringEnum(ENTWURF_V2_MODES, {
374
- description:
375
- "delivery mode (steer = interrupt current turn, follow_up = queue) — NOT the ownership axis (F1) nor liveness routing. MEANINGLESS on the meta-mailbox transport (F-mailbox): a mailbox ack is enqueue+doorbell, not a turn injection, so steer/follow_up does not apply when the verdict transport is meta-mailbox.",
376
- }),
377
- ),
378
- wantsReply: Type.Optional(
379
- Type.Boolean({
380
- description: "conversation etiquette only — NOT ownership; never triggers an auto-send (Q2).",
381
- }),
382
- ),
383
- // `additionalProperties: false` — a frozen contract input is exact; an unknown
384
- // key is a caller error, not silently ignored.
385
- },
386
- { additionalProperties: false },
387
- );
388
-
389
- // Receipt = a DISCRIMINATED union on `ok` (R3/F6) — NOT one flat object with
390
- // optionals. Each branch is EXACT (`additionalProperties: false`): without it,
391
- // JSON Schema's default admits extra keys, so an illegal receipt like
392
- // {ok:true, ..., reason:"bad-target"} would validate against the success branch.
393
- // With it, success carries action/transport/ownership and rejects a stray reason;
394
- // reject carries reason and rejects any allow facet — the branches are mutually
395
- // exclusive at the schema level, not merely by declared-property convention.
396
- export const EntwurfV2ReceiptSuccessSchema = Type.Object(
397
- {
398
- ok: Type.Literal(true),
399
- action: StringEnum(ENTWURF_V2_ACTIONS),
400
- transport: StringEnum(ENTWURF_V2_TRANSPORTS),
401
- ownership: StringEnum(ENTWURF_V2_OWNERSHIPS),
402
- observedLiveness: StringEnum(FACT_LIVENESSES, {
403
- description: "the 4-value fact liveness the verdict was computed from (R1/R3).",
404
- }),
405
- },
406
- { additionalProperties: false },
407
- );
408
-
409
- export const EntwurfV2ReceiptRejectSchema = Type.Object(
410
- {
411
- ok: Type.Literal(false),
412
- reason: StringEnum(ENTWURF_V2_REJECT_REASONS),
413
- // ?6: required-nullable, NOT optional — a reject branch ALWAYS carries the
414
- // key, and it is `null` for the pre-probe rejects (PRE_PROBE_REJECT_REASONS)
415
- // and a real FactLiveness otherwise. Optional would lose the "key always
416
- // present, value may be null" shape and weaken the discriminated union; the
417
- // reason-dependent null/non-null rule is enforced semantically (the gate's
418
- // rejectObservedLivenessWellFormed fixture), not by this blanket union.
419
- observedLiveness: Type.Union([StringEnum(FACT_LIVENESSES), Type.Null()], {
420
- description:
421
- "the 4-value fact liveness the reject was computed from (R1/R3); null for the pre-probe rejects (bad-target / target-locked / target-address-conflict) where no probe ran.",
422
- }),
423
- },
424
- { additionalProperties: false },
425
- );
426
-
427
- export const EntwurfV2ReceiptSchema = Type.Union([EntwurfV2ReceiptSuccessSchema, EntwurfV2ReceiptRejectSchema]);
354
+ // ── TypeBox schemas ────────────────────────────────────────────────────────
355
+ // MOVED to `entwurf-v2-contract-schema.ts` (0.12.1 B-1): the pi-ai TypeBox
356
+ // builders (StringEnum/Type) are a pi-lane dependency, so they cannot live in
357
+ // this pi-free core the MCP bridge reaches this module at boot and must stay
358
+ // harness-neutral (check-entwurf-bridge-pi-free). The schemas import the
359
+ // constants/types above; pi-side consumers import the schemas from there.
@@ -195,7 +195,11 @@ export interface DispatchDeciderDeps {
195
195
  releaseLock: (claim: LockClaim) => unknown;
196
196
  inspectSocket: (gardenId: string) => Promise<TargetSocketInspection>;
197
197
  probeSocket: (socketPath: string) => Promise<SocketLiveness>;
198
- preflightForCwd: (cwd: string) => PreflightOutcome;
198
+ // MaybePromise (0.12.1 B-2): production lazy-imports the pi-coding-agent-backed
199
+ // preflight via `await import()` so the harness-neutral bridge boots pi-free;
200
+ // only the owned-outcome resume branch (below) awaits it. Sync test fakes that
201
+ // return a plain PreflightOutcome still satisfy this.
202
+ preflightForCwd: (cwd: string) => PreflightOutcome | Promise<PreflightOutcome>;
199
203
  /**
200
204
  * SE-2 slice 2d-3: the REQUIRED mailbox-deliverability seam (no default). The decider
201
205
  * does NOT judge deliverability itself — it asks this injected fn, which combines the
@@ -392,7 +396,7 @@ async function decideInDomain(
392
396
  // 1B: preflight runs ONLY here (the sole branch that launches a child into a
393
397
  // target cwd). deny → nonce-owned release → untrusted-fail-fast, with the
394
398
  // honest measured liveness (dormant = the `dead` we just probed).
395
- const outcome = deps.preflightForCwd(resume.cwd);
399
+ const outcome = await deps.preflightForCwd(resume.cwd);
396
400
  if (outcome.kind === "deny") {
397
401
  return rejectAfterRelease(makeRejectReceipt("untrusted-fail-fast", liveness));
398
402
  }
@@ -39,7 +39,12 @@ import {
39
39
  receiverMarkerMatchesIdentity,
40
40
  } from "./entwurf-deliverability.ts";
41
41
  import { isNonPiGardenIdSocketConflict } from "./entwurf-facts.ts";
42
- import { type PreflightInput, type PreflightOutcome, preflight as realPreflight } from "./entwurf-preflight.ts";
42
+ // 0.12.1 B-2: TYPE-ONLY import `entwurf-preflight.ts` value-imports
43
+ // `@earendil-works/pi-coding-agent` (ProjectTrustStore), so a static value-import
44
+ // here would re-couple the harness-neutral MCP bridge to pi at boot
45
+ // (check-entwurf-bridge-pi-free). The real preflight is reached ONLY via the lazy
46
+ // `await import()` in `lazyProductionPreflight`, on the owned-outcome resume branch.
47
+ import type { PreflightInput, PreflightOutcome } from "./entwurf-preflight.ts";
43
48
  import { isLivenessSupported } from "./entwurf-v2-contract.ts";
44
49
  import {
45
50
  type DispatchDeciderDeps,
@@ -109,7 +114,10 @@ export interface ProductionEntwurfV2Seams {
109
114
  releaseLock: (claim: LockClaim, deps: { dir?: string }) => unknown;
110
115
  inspectSocket: (gid: string, dir: string) => Promise<TargetSocketInspection>;
111
116
  probeSocket: (socketPath: string) => Promise<SocketLiveness>;
112
- preflight: (input: PreflightInput) => PreflightOutcome;
117
+ // MaybePromise (0.12.1 B-2): the production default is the lazy wrapper, which
118
+ // `await import()`s the pi-coding-agent-backed preflight only on a resume verdict.
119
+ // A deterministic gate may still inject a sync spy returning a plain PreflightOutcome.
120
+ preflight: (input: PreflightInput) => PreflightOutcome | Promise<PreflightOutcome>;
113
121
  classifyConnect: (code: string | undefined) => "dead" | "indeterminate";
114
122
  sendRpc: (socketPath: string, command: RpcCommand, options?: RpcClientOptions) => Promise<{ response: RpcResponse }>;
115
123
  enqueue: (opts: EnqueueMetaMessageOptions) => EnqueueMetaMessageResult;
@@ -138,6 +146,20 @@ export interface ProductionEntwurfV2Opts {
138
146
  seams?: Partial<ProductionEntwurfV2Seams>;
139
147
  }
140
148
 
149
+ /**
150
+ * 0.12.1 B-2: the production `preflight` seam default. preflight value-imports
151
+ * `@earendil-works/pi-coding-agent` (ProjectTrustStore), so importing it eagerly
152
+ * would pull pi into the harness-neutral MCP bridge's boot closure. This wrapper
153
+ * defers that to a lazy `await import()` reached ONLY on the owned-outcome resume
154
+ * branch (the decider awaits it). peers/self/list/mailbox-deliver therefore boot
155
+ * with no pi package present; a pi-less environment that DOES hit a spawn-bg resume
156
+ * surfaces an honest module-not-found at that point rather than failing boot.
157
+ */
158
+ async function lazyProductionPreflight(input: PreflightInput): Promise<PreflightOutcome> {
159
+ const { preflight } = await import("./entwurf-preflight.ts");
160
+ return preflight(input);
161
+ }
162
+
141
163
  /** Map a record-side socket inspection to the singleton (socketGids, symlinkedGids) the
142
164
  * `isNonPiGardenIdSocketConflict` predicate consumes. `indeterminate` fails LOUD (QB2): an
143
165
  * unprovable conflict must NOT be folded to "no conflict" — that would silently allow an
@@ -185,7 +207,7 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
185
207
  releaseLock: s.releaseLock ?? realReleaseLock,
186
208
  inspectSocket: s.inspectSocket ?? inspectTargetControlSocket,
187
209
  probeSocket: s.probeSocket ?? probeSocketLiveness,
188
- preflight: s.preflight ?? realPreflight,
210
+ preflight: s.preflight ?? lazyProductionPreflight,
189
211
  classifyConnect: s.classifyConnect ?? classifyConnectError,
190
212
  sendRpc: s.sendRpc ?? realSendRpc,
191
213
  enqueue: s.enqueue ?? enqueueMetaMessage,
@@ -265,7 +287,7 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
265
287
  releaseLock: release,
266
288
  inspectSocket,
267
289
  probeSocket,
268
- preflightForCwd: (cwd: string): PreflightOutcome =>
290
+ preflightForCwd: (cwd: string): PreflightOutcome | Promise<PreflightOutcome> =>
269
291
  io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
270
292
  mailboxDeliverabilityFor,
271
293
  mailboxDir,
package/run.sh CHANGED
@@ -13,7 +13,16 @@
13
13
  #
14
14
  set -euo pipefail
15
15
 
16
- REPO_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
16
+ SOURCE="${BASH_SOURCE[0]}"
17
+ while [ -L "$SOURCE" ]; do
18
+ DIR="$(cd -P -- "$(dirname -- "$SOURCE")" && pwd)"
19
+ TARGET="$(readlink "$SOURCE")"
20
+ case "$TARGET" in
21
+ /*) SOURCE="$TARGET" ;;
22
+ *) SOURCE="$DIR/$TARGET" ;;
23
+ esac
24
+ done
25
+ REPO_DIR=$(cd -P -- "$(dirname -- "$SOURCE")" && pwd)
17
26
  PROJECT_DIR_DEFAULT=$(pwd)
18
27
  TARGET_PROJECT_DIR=${2:-$PROJECT_DIR_DEFAULT}
19
28
  # npm publish identity. Scoped 2026-05-18 — bare `entwurf` was not on npm
@@ -36,6 +45,7 @@ Usage:
36
45
  ./run.sh release-gate [project-dir] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm check) + the v2-native live gates (v2 matrix/spawn-resume-live, check-bridge, retargeted smoke-session-id-name, RGG) + the ACP plugin acceptance floor (11 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). LIVE-gated MUST steps HONEST-SKIP when LIVE!=1 (a CUT needs LIVE=1, SKIP=0). --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
37
46
  ./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
38
47
  ./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b, IN pnpm check): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 registered/schema; tools/list only, no auth/side-effect
48
+ ./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
39
49
  ./run.sh check-model-lock # deterministic unit test for pi-extensions/model-lock.ts (4-quadrant + edge cases, no API)
40
50
  ./run.sh check-shell-quote # POSIX-safety gate for shellQuote (remote SSH arg quoting in entwurf paths) — source parity + behavior matrix, no SSH
41
51
  ./run.sh check-entwurf-session-identity # deterministic gate for locked garden session identity & name grammar (sessionId/buildSessionName/parse/collision), no API
@@ -785,6 +795,17 @@ check_entwurf_bridge_boot() {
785
795
  (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-boot.ts)
786
796
  }
787
797
 
798
+ check_entwurf_bridge_pi_free() {
799
+ # 0.12.1 A-gate (static half): the entwurf-bridge MCP server must boot WITHOUT any
800
+ # pi package. entwurf is a harness-neutral npm package; pi is one optional adapter
801
+ # lane, not a boot dependency. Walks the EAGER static value-import closure of
802
+ # mcp/entwurf-bridge/src/index.ts and fails if any reachable module statically
803
+ # value-imports @earendil-works/pi-*. Type-only imports and dynamic `await import()`
804
+ # (the intended lazy preflight boundary) are excluded — the runtime boot smoke is the
805
+ # final authority that peers/self/list/mailbox-deliver come up pi-free.
806
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-pi-free.ts)
807
+ }
808
+
788
809
  check_entwurf_v2_production() {
789
810
  # Deterministic gate for 0.11 Stage 0 step 5d-2b: makeProductionEntwurfV2Deps — the ctx-free
790
811
  # PRODUCTION assembly of runEntwurfV2's deps. Proves the wiring over fake leaf-IO spies (no
@@ -1650,7 +1671,11 @@ check_pack() {
1650
1671
  section "pack invariants (dry-run)"
1651
1672
 
1652
1673
  local json
1653
- json=$(cd "$REPO_DIR" && npm pack --dry-run --json 2>/dev/null) || {
1674
+ # --silent so the `prepack` build (pnpm --silent run build-bridge → tsc) and
1675
+ # npm's own lifecycle banner stay off stdout; otherwise they pollute the --json
1676
+ # payload this parses. prepack runs on dry-run too, which is how dist lands in
1677
+ # this gate's file list.
1678
+ json=$(cd "$REPO_DIR" && npm pack --dry-run --json --silent 2>/dev/null) || {
1654
1679
  fail "[check-pack] npm pack --dry-run failed"
1655
1680
  return 1
1656
1681
  }
@@ -1701,6 +1726,10 @@ check_pack() {
1701
1726
  "pi-extensions/entwurf-control.ts"
1702
1727
  "pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
1703
1728
  "mcp/entwurf-bridge/src/index.ts"
1729
+ # 0.12.1 C — the prepack-built node_modules-safe boot artifact. start.sh runs
1730
+ # this dist JS when present (the .ts source can't strip-types under
1731
+ # node_modules). prepack runs on `npm pack --dry-run`, so it is in this gate.
1732
+ "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
1704
1733
  "scripts/postinstall-chmod.cjs"
1705
1734
  "pi/entwurf-capabilities.json"
1706
1735
  "pi/entwurf-targets.json"
@@ -1796,6 +1825,15 @@ check_pack_install() {
1796
1825
  tgz_path="${REPO_DIR}/${tgz_name}"
1797
1826
  rm -f "$tgz_path"
1798
1827
 
1828
+ # 0.12.1 C — stale-dist guard. `tsc` emit does NOT prune orphaned files from
1829
+ # outDir, and `files: ["mcp/"]` would carry any leftover dist file into the
1830
+ # tarball. build-bridge therefore `rm -rf`s dist before emit. Prove it: plant a
1831
+ # sentinel in dist, then assert the pack's prepack (build-bridge) wiped it so it
1832
+ # never reaches the tarball. Without the clean step this sentinel ships.
1833
+ local stale_probe="${REPO_DIR}/mcp/entwurf-bridge/dist/__stale_probe__.js"
1834
+ mkdir -p "$(dirname "$stale_probe")"
1835
+ printf 'module.exports = "stale";\n' > "$stale_probe"
1836
+
1799
1837
  echo "[check-pack-install] npm pack -> ${tgz_name}"
1800
1838
  (cd "$REPO_DIR" && npm pack --dry-run=false 2>&1 | tail -1) || {
1801
1839
  fail "[check-pack-install] npm pack failed"
@@ -1814,6 +1852,15 @@ check_pack_install() {
1814
1852
  local tar_files pass=1 f pat
1815
1853
  tar_files=$(tar -tf "$tgz_path" | sed 's|^package/||' | grep -v '/$' || true)
1816
1854
 
1855
+ # 0.12.1 C — the planted stale sentinel must NOT have survived into the tarball.
1856
+ # If it did, build-bridge's `rm -rf dist` clean step regressed and stale/orphan
1857
+ # emit can ship. (See the plant just before npm pack above.)
1858
+ if grep -qxF "mcp/entwurf-bridge/dist/__stale_probe__.js" <<<"$tar_files"; then
1859
+ rm -f "$tgz_path"
1860
+ fail "[check-pack-install] stale dist file shipped — build-bridge did not clean dist before emit (orphan-emit publish risk)"
1861
+ return 1
1862
+ fi
1863
+
1817
1864
  # Required tarball contents. The old 0.11.0 ACP root files (index.ts,
1818
1865
  # acp-bridge.ts, event-mapper.ts, engraving.ts, pi-context-augment.ts,
1819
1866
  # pi-extensions/entwurf.ts) were removed on v2-only and are GONE — keeping them
@@ -1830,6 +1877,8 @@ check_pack_install() {
1830
1877
  "pi-extensions/entwurf-control.ts"
1831
1878
  "pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
1832
1879
  "mcp/entwurf-bridge/src/index.ts"
1880
+ # 0.12.1 C — prepack-built node_modules-safe boot artifact (see check-pack).
1881
+ "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
1833
1882
  "scripts/postinstall-chmod.cjs"
1834
1883
  "pi/entwurf-capabilities.json"
1835
1884
  "pi/entwurf-targets.json"
@@ -1959,26 +2008,25 @@ check_pack_install() {
1959
2008
  done
1960
2009
  echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-4-6 + claude-opus-4-8 anchor)"
1961
2010
 
1962
- # npm-managed `run.sh install` regression — the README's PRIMARY install path,
1963
- # `pi install npm:@junghanacs/entwurf` then `run.sh install .`. This layout
1964
- # differs from the pnpm-add smoke above in the exact way that broke the install:
1965
- # pi installs via npm with --legacy-peer-deps, so the package lands under a
1966
- # managed prefix (~/.pi/agent/npm/node_modules/@junghanacs/entwurf), runtime deps
1967
- # HOIST to the sibling node_modules, there is NO package-local node_modules, and
1968
- # the pi peer trio is absent (loader-provided). The old cwd-relative
1969
- # preflight_dep_integrity rejected every dep in this layout, so the documented
1970
- # primary path failed on first use while every dry-run/pnpm-add gate stayed green.
1971
- # Prove `run.sh install` actually writes settings from the hoisted layout. HOME is
2011
+ # npm-managed neutral install regression — the README's PRIMARY install path is
2012
+ # now `npm install @junghanacs/entwurf` (NOT `pi install npm:...`). This layout
2013
+ # lands the package under node_modules, hoists runtime deps to the sibling
2014
+ # node_modules, has no package-local node_modules, and the pi peer trio must be
2015
+ # absent because pi is an optional adapter lane. The old cwd-relative
2016
+ # preflight_dep_integrity rejected every hoisted-dep npm install; 0.12.0 then
2017
+ # additionally died because start.sh tried strip-types under node_modules.
2018
+ # Prove `entwurf`/`entwurf-bridge` bins exist, `run.sh install` writes settings
2019
+ # from the hoisted layout, and the installed bridge boots from dist. HOME is
1972
2020
  # redirected to a throwaway dir so ensure_agent_dir_symlinks operates on a temp
1973
- # ~/.pi/agent and never touches (or fails against) the operator's real targets link.
2021
+ # ~/.pi/agent and never touches the operator's real targets link.
1974
2022
  if ! command -v npm >/dev/null 2>&1; then
1975
2023
  fail "[check-pack-install] npm not on PATH — cannot run npm-managed install regression"
1976
2024
  return 1
1977
2025
  fi
1978
2026
  local npmroot="$npm_tmp/npmroot" npmhome="$npm_tmp/npmhome" npmproj="$npm_tmp/npmproj" npm_log npm_pkg wire_log
1979
2027
  mkdir -p "$npmroot" "$npmhome" "$npmproj"
1980
- npm_log=$(cd "$npmroot" && npm install "$tgz_path" --legacy-peer-deps --no-audit --no-fund 2>&1) || {
1981
- fail "[check-pack-install] npm-managed install failed:"
2028
+ npm_log=$(cd "$npmroot" && npm install "$tgz_path" --no-audit --no-fund 2>&1) || {
2029
+ fail "[check-pack-install] npm-managed neutral install failed:"
1982
2030
  echo "$npm_log" | tail -10 | sed 's/^/ /' >&2
1983
2031
  return 1
1984
2032
  }
@@ -1987,6 +2035,11 @@ check_pack_install() {
1987
2035
  fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
1988
2036
  return 1
1989
2037
  fi
2038
+ if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ]; then
2039
+ fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge)"
2040
+ ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
2041
+ return 1
2042
+ fi
1990
2043
  wire_log=$(HOME="$npmhome" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
1991
2044
  fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
1992
2045
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
@@ -2004,6 +2057,75 @@ check_pack_install() {
2004
2057
  fi
2005
2058
  echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
2006
2059
 
2060
+ # 0.12.1 C — installed bridge BOOT regression. This is the test whose absence
2061
+ # let the 0.12.0 install bug ship: the README's bridge launcher was
2062
+ # `node --experimental-strip-types src/index.ts`, which Node REFUSES under
2063
+ # node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). Every gate above
2064
+ # wired settings or probed shape but never BOOTED the bridge from its installed
2065
+ # node_modules home, so the dead-on-arrival MCP server passed publish. Here we
2066
+ # boot the installed start.sh and assert it answers MCP tools/list with the v2
2067
+ # surface. Two proofs in one: (1) the prepack dist JS boots under node_modules
2068
+ # with plain node — a strip-types fallback would crash with the exact error
2069
+ # above, so a parseable tools/list IS proof the dist path was taken; (2) the
2070
+ # boot is pi-free — the @earendil-works peer trio is optional and must NOT be
2071
+ # installed by the neutral npm path, so the eager closure stands up with pi absent.
2072
+ local installed_start="$npmroot/node_modules/.bin/entwurf-bridge"
2073
+ local installed_dist="$npm_pkg/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
2074
+ if [ ! -f "$installed_dist" ]; then
2075
+ fail "[check-pack-install] installed bridge missing prebuilt dist (prepack did not emit into the tarball?): $installed_dist"
2076
+ return 1
2077
+ fi
2078
+ if [ -d "$npmroot/node_modules/@earendil-works" ]; then
2079
+ fail "[check-pack-install] @earendil-works present in npm-managed node_modules — pi-free boot proof is void (neutral npm install should not install optional pi peers)"
2080
+ return 1
2081
+ fi
2082
+ local boot_out
2083
+ if ! boot_out=$(START_SH="$installed_start" node --input-type=module <<'JS'
2084
+ import { spawn } from 'node:child_process';
2085
+ const start = process.env.START_SH;
2086
+ // Sanitize the child env so the pi-free proof cannot be masked by a leaked
2087
+ // module-resolution path: if NODE_PATH (or a stray pi env) pointed at a tree
2088
+ // holding @earendil-works, a statically pi-importing eager graph could resolve
2089
+ // and boot anyway, turning this gate falsely green. Strip it so "boots with
2090
+ // @earendil absent" stays an honest adversarial proof.
2091
+ const env = { ...process.env };
2092
+ delete env.NODE_PATH;
2093
+ const child = spawn(start, { stdio: ['pipe', 'pipe', 'pipe'], env });
2094
+ let stdout = '', stderr = '', done = false;
2095
+ const timer = setTimeout(() => {
2096
+ child.kill('SIGKILL');
2097
+ console.error('installed bridge boot timeout');
2098
+ if (stderr.trim()) console.error(stderr.trim());
2099
+ process.exit(1);
2100
+ }, 5000);
2101
+ function finish(trimmed) {
2102
+ if (done) return;
2103
+ done = true;
2104
+ clearTimeout(timer);
2105
+ let msg;
2106
+ try { msg = JSON.parse(trimmed); }
2107
+ catch { console.error('unparseable tools/list:', trimmed.slice(0, 300)); if (stderr.trim()) console.error(stderr.trim()); process.exit(1); }
2108
+ const names = (msg?.result?.tools ?? []).map((t) => t?.name).sort();
2109
+ for (const need of ['entwurf_v2', 'entwurf_peers', 'entwurf_self', 'entwurf_inbox_read']) {
2110
+ if (!names.includes(need)) { console.error('missing MCP tool from installed boot:', need, '— got', names.join(',')); process.exit(1); }
2111
+ }
2112
+ console.log(names.join(','));
2113
+ child.kill('SIGTERM');
2114
+ process.exit(0);
2115
+ }
2116
+ child.stdout.on('data', (d) => { stdout += d.toString(); const t = stdout.trim(); if (t) finish(t); });
2117
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
2118
+ child.on('error', (e) => { clearTimeout(timer); console.error('installed bridge spawn error:', String(e)); process.exit(1); });
2119
+ child.on('close', () => { if (done) return; clearTimeout(timer); if (stderr.trim()) console.error(stderr.trim()); console.error('installed bridge closed with empty tools/list'); process.exit(1); });
2120
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n');
2121
+ JS
2122
+ ); then
2123
+ fail "[check-pack-install] installed bridge boot FAILED — the npm-installed MCP server does not answer tools/list (the 0.12.0 strip-types-under-node_modules regression):"
2124
+ echo "$boot_out" | tail -15 | sed 's/^/ /' >&2
2125
+ return 1
2126
+ fi
2127
+ echo "[check-pack-install] installed bridge boot pass (dist boots under node_modules, pi-free: $boot_out)"
2128
+
2007
2129
  ok "[check-pack-install] publish install smoke pass"
2008
2130
  return 0
2009
2131
  }
@@ -2560,6 +2682,9 @@ case "$cmd" in
2560
2682
  check-entwurf-bridge-boot)
2561
2683
  check_entwurf_bridge_boot
2562
2684
  ;;
2685
+ check-entwurf-bridge-pi-free)
2686
+ check_entwurf_bridge_pi_free
2687
+ ;;
2563
2688
  check-entwurf-v2-spawn)
2564
2689
  check_entwurf_v2_spawn
2565
2690
  ;;