@junghanacs/entwurf 0.12.0 → 0.12.2

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 (49) hide show
  1. package/CHANGELOG.md +40 -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 +30 -9
  37. package/pi/meta-bridge/.claude-plugin/marketplace.json +0 -1
  38. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +101 -0
  39. package/pi-extensions/lib/entwurf-v2-contract.ts +10 -78
  40. package/pi-extensions/lib/entwurf-v2-decider.ts +6 -2
  41. package/pi-extensions/lib/entwurf-v2-production.ts +26 -4
  42. package/run.sh +150 -15
  43. package/scripts/check-entwurf-bridge-pi-free.ts +146 -0
  44. package/scripts/check-entwurf-v2-contract.ts +6 -4
  45. package/scripts/check-meta-manifest-schema.py +145 -0
  46. package/scripts/meta-bridge-install.sh +17 -3
  47. package/scripts/meta-bridge-state.py +37 -10
  48. package/scripts/smoke-acp-bundled-mcp-live.ts +13 -2
  49. package/scripts/smoke-acp-carrier-augment-live.ts +35 -19
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "meta-bridge-local",
3
3
  "owner": { "name": "junghan0611" },
4
- "description": "Local marketplace for the entwurf garden-native meta-bridge. Shipped in-repo (repo-stable, NOT /tmp — an ephemeral source breaks `claude plugin marketplace update`). The installer (./run.sh install-meta-bridge) assembles a self-contained copy under .assembled/ (entry + lib + node-path-baked hooks) and runs `marketplace add` + `install --scope user`.",
5
4
  "plugins": [
6
5
  {
7
6
  "name": "entwurf-meta-receive",
@@ -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
@@ -83,6 +93,7 @@ Usage:
83
93
  ./run.sh smoke-meta-install-state # 1.0.0 meta-bridge Phase 2: stateful install/uninstall + store-doctor regression gate. Offline/deterministic (deps: bash+node+python3)
84
94
  ./run.sh smoke-meta-prune # 1.0.0 meta-bridge Phase 4: listing-only store janitor regression gate — classify keep/orphan/stale/ambiguous, delete nothing. Offline/deterministic (deps: bash+node)
85
95
  ./run.sh smoke-meta-keyset-guard # 0.10.0 meta-bridge: keyset-owner guard regression — check-keyset-overlap + managed-keys SSOT (disjoint passes, collisions fail). Offline/hermetic (deps: bash+python3)
96
+ ./run.sh check-meta-manifest-schema # 0.12.2 meta-bridge: CLI-version-INDEPENDENT static guard — plugin manifests pinned to the minimal keyset that validates on the lowest supported Claude (closed-schema regression that broke 0.12.1 install on floor) + desired_mcp installed-vs-clone dual-mode. Offline (deps: python3)
86
97
  ./run.sh smoke-claude-native-resume-live # LIVE-only: Claude Code native fresh→--resume continuity + meta-record uniqueness; proves meta-bridge records identity without touching the backend resume path
87
98
 
88
99
  ./run.sh install-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
@@ -785,6 +796,17 @@ check_entwurf_bridge_boot() {
785
796
  (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-boot.ts)
786
797
  }
787
798
 
799
+ check_entwurf_bridge_pi_free() {
800
+ # 0.12.1 A-gate (static half): the entwurf-bridge MCP server must boot WITHOUT any
801
+ # pi package. entwurf is a harness-neutral npm package; pi is one optional adapter
802
+ # lane, not a boot dependency. Walks the EAGER static value-import closure of
803
+ # mcp/entwurf-bridge/src/index.ts and fails if any reachable module statically
804
+ # value-imports @earendil-works/pi-*. Type-only imports and dynamic `await import()`
805
+ # (the intended lazy preflight boundary) are excluded — the runtime boot smoke is the
806
+ # final authority that peers/self/list/mailbox-deliver come up pi-free.
807
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-pi-free.ts)
808
+ }
809
+
788
810
  check_entwurf_v2_production() {
789
811
  # Deterministic gate for 0.11 Stage 0 step 5d-2b: makeProductionEntwurfV2Deps — the ctx-free
790
812
  # PRODUCTION assembly of runEntwurfV2's deps. Proves the wiring over fake leaf-IO spies (no
@@ -1650,7 +1672,11 @@ check_pack() {
1650
1672
  section "pack invariants (dry-run)"
1651
1673
 
1652
1674
  local json
1653
- json=$(cd "$REPO_DIR" && npm pack --dry-run --json 2>/dev/null) || {
1675
+ # --silent so the `prepack` build (pnpm --silent run build-bridge → tsc) and
1676
+ # npm's own lifecycle banner stay off stdout; otherwise they pollute the --json
1677
+ # payload this parses. prepack runs on dry-run too, which is how dist lands in
1678
+ # this gate's file list.
1679
+ json=$(cd "$REPO_DIR" && npm pack --dry-run --json --silent 2>/dev/null) || {
1654
1680
  fail "[check-pack] npm pack --dry-run failed"
1655
1681
  return 1
1656
1682
  }
@@ -1701,6 +1727,10 @@ check_pack() {
1701
1727
  "pi-extensions/entwurf-control.ts"
1702
1728
  "pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
1703
1729
  "mcp/entwurf-bridge/src/index.ts"
1730
+ # 0.12.1 C — the prepack-built node_modules-safe boot artifact. start.sh runs
1731
+ # this dist JS when present (the .ts source can't strip-types under
1732
+ # node_modules). prepack runs on `npm pack --dry-run`, so it is in this gate.
1733
+ "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
1704
1734
  "scripts/postinstall-chmod.cjs"
1705
1735
  "pi/entwurf-capabilities.json"
1706
1736
  "pi/entwurf-targets.json"
@@ -1796,6 +1826,15 @@ check_pack_install() {
1796
1826
  tgz_path="${REPO_DIR}/${tgz_name}"
1797
1827
  rm -f "$tgz_path"
1798
1828
 
1829
+ # 0.12.1 C — stale-dist guard. `tsc` emit does NOT prune orphaned files from
1830
+ # outDir, and `files: ["mcp/"]` would carry any leftover dist file into the
1831
+ # tarball. build-bridge therefore `rm -rf`s dist before emit. Prove it: plant a
1832
+ # sentinel in dist, then assert the pack's prepack (build-bridge) wiped it so it
1833
+ # never reaches the tarball. Without the clean step this sentinel ships.
1834
+ local stale_probe="${REPO_DIR}/mcp/entwurf-bridge/dist/__stale_probe__.js"
1835
+ mkdir -p "$(dirname "$stale_probe")"
1836
+ printf 'module.exports = "stale";\n' > "$stale_probe"
1837
+
1799
1838
  echo "[check-pack-install] npm pack -> ${tgz_name}"
1800
1839
  (cd "$REPO_DIR" && npm pack --dry-run=false 2>&1 | tail -1) || {
1801
1840
  fail "[check-pack-install] npm pack failed"
@@ -1814,6 +1853,15 @@ check_pack_install() {
1814
1853
  local tar_files pass=1 f pat
1815
1854
  tar_files=$(tar -tf "$tgz_path" | sed 's|^package/||' | grep -v '/$' || true)
1816
1855
 
1856
+ # 0.12.1 C — the planted stale sentinel must NOT have survived into the tarball.
1857
+ # If it did, build-bridge's `rm -rf dist` clean step regressed and stale/orphan
1858
+ # emit can ship. (See the plant just before npm pack above.)
1859
+ if grep -qxF "mcp/entwurf-bridge/dist/__stale_probe__.js" <<<"$tar_files"; then
1860
+ rm -f "$tgz_path"
1861
+ fail "[check-pack-install] stale dist file shipped — build-bridge did not clean dist before emit (orphan-emit publish risk)"
1862
+ return 1
1863
+ fi
1864
+
1817
1865
  # Required tarball contents. The old 0.11.0 ACP root files (index.ts,
1818
1866
  # acp-bridge.ts, event-mapper.ts, engraving.ts, pi-context-augment.ts,
1819
1867
  # pi-extensions/entwurf.ts) were removed on v2-only and are GONE — keeping them
@@ -1830,6 +1878,8 @@ check_pack_install() {
1830
1878
  "pi-extensions/entwurf-control.ts"
1831
1879
  "pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
1832
1880
  "mcp/entwurf-bridge/src/index.ts"
1881
+ # 0.12.1 C — prepack-built node_modules-safe boot artifact (see check-pack).
1882
+ "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
1833
1883
  "scripts/postinstall-chmod.cjs"
1834
1884
  "pi/entwurf-capabilities.json"
1835
1885
  "pi/entwurf-targets.json"
@@ -1959,26 +2009,25 @@ check_pack_install() {
1959
2009
  done
1960
2010
  echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-4-6 + claude-opus-4-8 anchor)"
1961
2011
 
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
2012
+ # npm-managed neutral install regression — the README's PRIMARY install path is
2013
+ # now `npm install @junghanacs/entwurf` (NOT `pi install npm:...`). This layout
2014
+ # lands the package under node_modules, hoists runtime deps to the sibling
2015
+ # node_modules, has no package-local node_modules, and the pi peer trio must be
2016
+ # absent because pi is an optional adapter lane. The old cwd-relative
2017
+ # preflight_dep_integrity rejected every hoisted-dep npm install; 0.12.0 then
2018
+ # additionally died because start.sh tried strip-types under node_modules.
2019
+ # Prove `entwurf`/`entwurf-bridge` bins exist, `run.sh install` writes settings
2020
+ # from the hoisted layout, and the installed bridge boots from dist. HOME is
1972
2021
  # 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.
2022
+ # ~/.pi/agent and never touches the operator's real targets link.
1974
2023
  if ! command -v npm >/dev/null 2>&1; then
1975
2024
  fail "[check-pack-install] npm not on PATH — cannot run npm-managed install regression"
1976
2025
  return 1
1977
2026
  fi
1978
2027
  local npmroot="$npm_tmp/npmroot" npmhome="$npm_tmp/npmhome" npmproj="$npm_tmp/npmproj" npm_log npm_pkg wire_log
1979
2028
  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:"
2029
+ npm_log=$(cd "$npmroot" && npm install "$tgz_path" --no-audit --no-fund 2>&1) || {
2030
+ fail "[check-pack-install] npm-managed neutral install failed:"
1982
2031
  echo "$npm_log" | tail -10 | sed 's/^/ /' >&2
1983
2032
  return 1
1984
2033
  }
@@ -1987,6 +2036,11 @@ check_pack_install() {
1987
2036
  fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
1988
2037
  return 1
1989
2038
  fi
2039
+ if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ]; then
2040
+ fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge)"
2041
+ ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
2042
+ return 1
2043
+ fi
1990
2044
  wire_log=$(HOME="$npmhome" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
1991
2045
  fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
1992
2046
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
@@ -2004,6 +2058,75 @@ check_pack_install() {
2004
2058
  fi
2005
2059
  echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
2006
2060
 
2061
+ # 0.12.1 C — installed bridge BOOT regression. This is the test whose absence
2062
+ # let the 0.12.0 install bug ship: the README's bridge launcher was
2063
+ # `node --experimental-strip-types src/index.ts`, which Node REFUSES under
2064
+ # node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). Every gate above
2065
+ # wired settings or probed shape but never BOOTED the bridge from its installed
2066
+ # node_modules home, so the dead-on-arrival MCP server passed publish. Here we
2067
+ # boot the installed start.sh and assert it answers MCP tools/list with the v2
2068
+ # surface. Two proofs in one: (1) the prepack dist JS boots under node_modules
2069
+ # with plain node — a strip-types fallback would crash with the exact error
2070
+ # above, so a parseable tools/list IS proof the dist path was taken; (2) the
2071
+ # boot is pi-free — the @earendil-works peer trio is optional and must NOT be
2072
+ # installed by the neutral npm path, so the eager closure stands up with pi absent.
2073
+ local installed_start="$npmroot/node_modules/.bin/entwurf-bridge"
2074
+ local installed_dist="$npm_pkg/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js"
2075
+ if [ ! -f "$installed_dist" ]; then
2076
+ fail "[check-pack-install] installed bridge missing prebuilt dist (prepack did not emit into the tarball?): $installed_dist"
2077
+ return 1
2078
+ fi
2079
+ if [ -d "$npmroot/node_modules/@earendil-works" ]; then
2080
+ 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)"
2081
+ return 1
2082
+ fi
2083
+ local boot_out
2084
+ if ! boot_out=$(START_SH="$installed_start" node --input-type=module <<'JS'
2085
+ import { spawn } from 'node:child_process';
2086
+ const start = process.env.START_SH;
2087
+ // Sanitize the child env so the pi-free proof cannot be masked by a leaked
2088
+ // module-resolution path: if NODE_PATH (or a stray pi env) pointed at a tree
2089
+ // holding @earendil-works, a statically pi-importing eager graph could resolve
2090
+ // and boot anyway, turning this gate falsely green. Strip it so "boots with
2091
+ // @earendil absent" stays an honest adversarial proof.
2092
+ const env = { ...process.env };
2093
+ delete env.NODE_PATH;
2094
+ const child = spawn(start, { stdio: ['pipe', 'pipe', 'pipe'], env });
2095
+ let stdout = '', stderr = '', done = false;
2096
+ const timer = setTimeout(() => {
2097
+ child.kill('SIGKILL');
2098
+ console.error('installed bridge boot timeout');
2099
+ if (stderr.trim()) console.error(stderr.trim());
2100
+ process.exit(1);
2101
+ }, 5000);
2102
+ function finish(trimmed) {
2103
+ if (done) return;
2104
+ done = true;
2105
+ clearTimeout(timer);
2106
+ let msg;
2107
+ try { msg = JSON.parse(trimmed); }
2108
+ catch { console.error('unparseable tools/list:', trimmed.slice(0, 300)); if (stderr.trim()) console.error(stderr.trim()); process.exit(1); }
2109
+ const names = (msg?.result?.tools ?? []).map((t) => t?.name).sort();
2110
+ for (const need of ['entwurf_v2', 'entwurf_peers', 'entwurf_self', 'entwurf_inbox_read']) {
2111
+ if (!names.includes(need)) { console.error('missing MCP tool from installed boot:', need, '— got', names.join(',')); process.exit(1); }
2112
+ }
2113
+ console.log(names.join(','));
2114
+ child.kill('SIGTERM');
2115
+ process.exit(0);
2116
+ }
2117
+ child.stdout.on('data', (d) => { stdout += d.toString(); const t = stdout.trim(); if (t) finish(t); });
2118
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
2119
+ child.on('error', (e) => { clearTimeout(timer); console.error('installed bridge spawn error:', String(e)); process.exit(1); });
2120
+ 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); });
2121
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n');
2122
+ JS
2123
+ ); then
2124
+ 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):"
2125
+ echo "$boot_out" | tail -15 | sed 's/^/ /' >&2
2126
+ return 1
2127
+ fi
2128
+ echo "[check-pack-install] installed bridge boot pass (dist boots under node_modules, pi-free: $boot_out)"
2129
+
2007
2130
  ok "[check-pack-install] publish install smoke pass"
2008
2131
  return 0
2009
2132
  }
@@ -2560,6 +2683,9 @@ case "$cmd" in
2560
2683
  check-entwurf-bridge-boot)
2561
2684
  check_entwurf_bridge_boot
2562
2685
  ;;
2686
+ check-entwurf-bridge-pi-free)
2687
+ check_entwurf_bridge_pi_free
2688
+ ;;
2563
2689
  check-entwurf-v2-spawn)
2564
2690
  check_entwurf_v2_spawn
2565
2691
  ;;
@@ -2678,6 +2804,15 @@ case "$cmd" in
2678
2804
  # Offline/hermetic (deps: bash+python3).
2679
2805
  (cd "$REPO_DIR" && bash scripts/smoke-meta-keyset-guard.sh)
2680
2806
  ;;
2807
+ check-meta-manifest-schema)
2808
+ # 0.12.2 meta-bridge: deterministic, CLI-version-INDEPENDENT guard. `claude plugin
2809
+ # validate` is a CLOSED schema whose allowed keyset differs by version, so a
2810
+ # decorative key (0.12.1's root `description`) passed on the dev box but broke
2811
+ # install on the floor Claude. This pins the committed manifests to the minimal
2812
+ # validated keyset and asserts desired_mcp()'s installed-vs-clone dual-mode.
2813
+ # Offline/hermetic (deps: python3).
2814
+ (cd "$REPO_DIR" && python3 scripts/check-meta-manifest-schema.py)
2815
+ ;;
2681
2816
  smoke-meta-install-state)
2682
2817
  # 1.0.0 meta-bridge Phase 2 regression gate: state file captures pre-install
2683
2818
  # values, install/uninstall touches only the managed keyset, uninstall refuses