@junghanacs/entwurf 0.12.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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
package/run.sh
ADDED
|
@@ -0,0 +1,2832 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Model id convention (see AGENTS.md Hard Rule #1):
|
|
4
|
+
# - User-facing examples use the qualified form `entwurf/<backend-model>`
|
|
5
|
+
# (e.g. `entwurf/claude-sonnet-4-6`); the prefix routes to this provider
|
|
6
|
+
# so `--provider` is redundant and is dropped in docs.
|
|
7
|
+
# - Smoke helpers that feed `ensureBridgeSession({modelId})` directly (cancel,
|
|
8
|
+
# model-switch) pass BARE backend ids (`claude-sonnet-4-6`, `gpt-5.4`)
|
|
9
|
+
# because the bridge library contract is bare. Smoke helpers that invoke pi
|
|
10
|
+
# via the CLI still pin `--provider entwurf` and can accept either
|
|
11
|
+
# bare or qualified model, but we keep bare here to match the bridge-level
|
|
12
|
+
# dispatch tables.
|
|
13
|
+
#
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
REPO_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
17
|
+
PROJECT_DIR_DEFAULT=$(pwd)
|
|
18
|
+
TARGET_PROJECT_DIR=${2:-$PROJECT_DIR_DEFAULT}
|
|
19
|
+
# npm publish identity. Scoped 2026-05-18 — bare `entwurf` was not on npm
|
|
20
|
+
# and we adopted the same `@junghanacs` scope as the OpenClaw plugin sibling
|
|
21
|
+
# (`@junghanacs/openclaw-entwurf`) for source-of-origin parity. This
|
|
22
|
+
# variable documents intent; check-pack-install hardcodes the tarball name
|
|
23
|
+
# and install path against the same scope for traceability.
|
|
24
|
+
PACKAGE_NAME="@junghanacs/entwurf"
|
|
25
|
+
# Runtime provider id — DO NOT change. Embedded in model strings
|
|
26
|
+
# (`entwurf/claude-sonnet-4-6`), settings keys (`entwurfProvider`),
|
|
27
|
+
# log prefixes (`[entwurf:bootstrap]`), and the `--provider entwurf`
|
|
28
|
+
# CLI surface. Renaming this would break every consumer transcript and every
|
|
29
|
+
# saved session anchor.
|
|
30
|
+
PROVIDER_ID="entwurf"
|
|
31
|
+
|
|
32
|
+
usage() {
|
|
33
|
+
cat <<'EOF'
|
|
34
|
+
Usage:
|
|
35
|
+
./run.sh setup [project-dir] # ONE confident install: pnpm install + install + meta-bridge (if native harness) + v2 install smoke (LIVE substrate = release-gate)
|
|
36
|
+
./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
|
+
./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
|
|
38
|
+
./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
|
|
39
|
+
./run.sh check-model-lock # deterministic unit test for pi-extensions/model-lock.ts (4-quadrant + edge cases, no API)
|
|
40
|
+
./run.sh check-shell-quote # POSIX-safety gate for shellQuote (remote SSH arg quoting in entwurf paths) — source parity + behavior matrix, no SSH
|
|
41
|
+
./run.sh check-entwurf-session-identity # deterministic gate for locked garden session identity & name grammar (sessionId/buildSessionName/parse/collision), no API
|
|
42
|
+
./run.sh check-meta-session # deterministic gate (#30 step 2): meta-record mint/serialize/parse + scanByNativeId body-authority + idempotent decideUpsert, no API
|
|
43
|
+
./run.sh check-meta-record-v2 # deterministic golden gate (0.11 Stage 0 step 3A): synthetic v1 fixture → normalizeMetaIdentity v2 identity golden + dual-read version fences, no API
|
|
44
|
+
./run.sh check-mailbox-receipt-state # deterministic gate (0.11 Stage 0 step 3B): mailbox receipt state schema + store (stamp→persist→read-back) in a temp mailbox, strict keyset, no API
|
|
45
|
+
./run.sh check-entwurf-capabilities # deterministic gate (0.11 Stage 0 step 3C): backend capability registry (pi/entwurf-capabilities.json) — coverage==META_BACKENDS_V2 + agrees with live META_BACKEND_DESCRIPTORS + strict keyset, no API
|
|
46
|
+
./run.sh check-meta-dual-read # deterministic gate (0.11 Stage 0 step 3D-1): v2 write shape (serializeMetaIdentity) + dual-read dispatcher (parseMetaRecordAny/parseMetaIdentity) + write→read round-trip, pure, no API
|
|
47
|
+
./run.sh check-meta-mailbox-state-write # deterministic gate (0.11 Stage 0 step 3D-4 commit2): post-cut receipt is state-only — meta-record file byte-identical across enqueue/read, state carries lastEnqueuedAt/lastReadAt (field isolation), empty inbox no-op on record+state, drift surfaces; no API
|
|
48
|
+
./run.sh check-meta-receiver-marker # deterministic gate (SE-2 slice 2b): meta-receiver presence marker — write/read round-trip garden-id keyed + atomic 0600, dead-owner start-key guard reads null, armProvenance limited to arm-capable events (UserPromptSubmit can't mint presence), reader doesn't gate on record existence
|
|
49
|
+
./run.sh check-meta-migration # deterministic gate (0.11 Stage 0 step 3D-4 commit2): v1→v2 delivery-receipt migration (per-field state-wins, 3 timestamps, no-op when nothing to fill) + crash-order inside upsert (migrate before v2 rewrite; drift throws with record still v1), no API
|
|
50
|
+
./run.sh check-meta-dual-consumers # deterministic gate (0.11 Stage 0 step 3D-4): delivery-agnostic dual-read seam — readMetaIdentityByGardenId + scanIdentityByNativeId read v1 AND v2, cross-schema duplicate = ambiguity throw (G1); v1-only raw readers remain for v1-fixture gates, no API
|
|
51
|
+
./run.sh check-meta-capability-source # deterministic gate (0.11 Stage 0 step 3D-3): capability-source cut-over — mint/parse read wakeMode/deliveryLevel from the registry (metaCapabilityFor, registry-driven via injection), not META_BACKEND_DESCRIPTORS; behaviour-preserving (registry ≡ const), slot stays (3D-4), no API
|
|
52
|
+
./run.sh check-socket-probe # deterministic gate (0.11 Stage 0, F3): three-valued control-socket liveness (alive|dead|indeterminate) — GC reclaims dead only, indeterminate survives; pure classify + 2-socket integration, no API
|
|
53
|
+
./run.sh check-project-trust-handler # deterministic gate (0.11 Stage 0, Trust 2층): project_trust handler — decideProjectTrust matrix (escape=inherited-false+interactive+trust-here→{yes,remember:true}; non-interactive→undecided; never undefined) + adapter single-writer, fake prompt, no UI
|
|
54
|
+
./run.sh check-entwurf-v2-contract # deterministic gate (0.11 Stage 0 step 4-pre, 동결결정 10 + Fable R1-R5): FROZEN entwurf_v2 contract — R1 backend liveness domain (pi only; claude/codex/agy=unsupported, not folded), 6-cell intent×liveness table (single verdict, 2 allow/4 reject), N1 indeterminate-no-spawn, Q2 owned-live-no-autosend, R3 table↔receipt round-trip, R5 taxonomy, schema↔types drift; pure, no API
|
|
55
|
+
./run.sh check-entwurf-v2-lock # deterministic gate (0.11 Stage 0 step 5a, 버킷 B F2): per-gid dispatch LOCK primitive — openSync wx atomic acquire, second-acquire=target-locked conflict (holder JSON for human cleanup), nonce-owned release (successor survives late release), stale reclaim same-host+ESRCH-only (EPERM/remote/alive/unknown fail-closed), empty/corrupt=conflict not auto-deleted, F2-P1 malformed gid throws; real temp dir, deps injected
|
|
56
|
+
./run.sh check-entwurf-v2-decider # deterministic gate (0.11 Stage 0 step 5b): PURE dispatch decider decideDispatch — frozen 7-step order over injected fakes, lock acquire+release tracked so reject⇒no-plan-no-lock proven; pre-probe rejects observedLiveness=null, send/resume execute keep lock + mailbox no-lock (?7), resume plan no mode/provider/model, invalid gid throws (F2-P1); pure, no IO
|
|
57
|
+
./run.sh check-entwurf-v2-matrix # deterministic gate (0.11 Stage 0 step 5d-5 a): REACHABILITY + LOCK SSOT table — drives REAL decideDispatch over fakes, fixes every (target kind → transport → lock class) cell as one table (control-socket/meta-mailbox/spawn-bg + bad-target/conflict/locked/undeliverable/owned-live/dormant/indeterminate rejects), coverage pass fails on a dropped cell; thin coverage not a decider re-impl; pure, no IO
|
|
58
|
+
./run.sh check-entwurf-v2-release # deterministic gate (0.11 Stage 0 step 5c-1): PURE release-policy reducer (decideReleasePolicy + reduceRelease) — Fable-3 release-after-observation as a state machine; spawn-started is NOT a release event, release on first socket-alive ∨ child-exited (any code) or failed start, socket↔exit race idempotent (single release), lock-nullness invariant enforced; pure, no IO
|
|
59
|
+
./run.sh check-entwurf-v2-send # deterministic gate (0.11 Stage 0 step 5c-2a): control-socket SEND hand (executeControlSocketSend) wiring transport IO onto the 5c-1 reducer — ack→sent, in-band reject→rejected (no fallback), dead→same-lock one-shot re-resolve (control retry / mailbox enqueue), indeterminate→failed+rethrow with NO fallback (no double-delivery); release exactly once, releaseLock throw never masks the send error; IO-via-dep
|
|
60
|
+
./run.sh check-entwurf-v2-send-fallback # deterministic gate (0.11 Stage 0 step 5c-2b): same-lock re-resolve RESOLVER (resolveDeadControlSendFallback) — fire-and-forget re-resolve: alive→control retry, dead→reject (NEVER spawn-bg), indeterminate→reject, unsupported+deliverable→mailbox plan, undeliverable/bad-target/conflict→reject; resolver never releases, mis-wire fails loud, inspect/probe throws propagate; no IO (fakes)
|
|
61
|
+
./run.sh check-entwurf-v2-runner # deterministic gate (0.11 Stage 0 step 5d-1): execute-router (executeDispatch) routing an already-decided DispatchDecision to its 5c transport hand → one outcome-rich EntwurfV2RunResult. reject→rejected (no hand) / control/spawn/mailbox→matching hand with decision.lock verbatim / spawn lock-retained rides executed (fail-closed) / N3 rejectReason carried / N1 SendDeliveredReleaseFailedError→execution-failed{finalizedOutcome,releaseFailed,retrySafe:false}; fake hands, no IO
|
|
62
|
+
./run.sh check-entwurf-v2-mailbox # deterministic gate (0.11 Stage 0 step 5c-4, LAST 5c transport slice): ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + production sendViaMailbox adapter — sender→formatMetaMailboxBody with plan.wantsReply threaded (divergence from legacy hard false), sender absent→raw plan.message, enqueue opts EXACTLY {gardenId,body,sessionsDir,mailboxDir}, enqueue throw PROPAGATES (no success:false fold — mailbox has no in-band refuse); adapter NEVER touches lock (release is the hand's job); source guard: no release/routing seam
|
|
63
|
+
./run.sh check-entwurf-v2-spawn # deterministic gate (0.11 Stage 0 step 5c-3a): spawn-bg RESUME watcher hand (executeSpawnBgResume) wiring spawn + socket-observe IO onto the 5c-1 reducer — Fable-3: TIMEOUT IS NOT A RELEASE (bare observeTimeout→killChild, release 0; bounded killGrace then real socket-alive ∨ child-exited releases ×1); spawnChild throw→spawn-start-failed; no observation obtainable (grace elapses / post-spawn watch dep throws)→lock-retained fail-closed (released:false, evidence surfaced), NO direct-release hatch; IO-via-dep, controlled promises
|
|
64
|
+
./run.sh check-entwurf-resume-args # deterministic gate (0.11 Stage 0 step 5c-3b): resume-argv SSOT (buildResumePiArgs) shared by the legacy async worker and the v2 spawn-bg resident citizen — A1: legacy=--no-extensions + no --entwurf-control (one-shot pi -p exits), v2-control=--entwurf-control + no --no-extensions (keep-alive is the goal, resumed session stays addressable); BOTH keep --mode json -p + prompt-as-turn (-p NOT dropped in v2); explicitExtensionArgs preserved once (#29); v2 includes plan.launchArgs (--approve); null provider→no --provider; no cross-contamination
|
|
65
|
+
./run.sh check-entwurf-v2-spawn-production # deterministic gate (0.11 Stage 0 step 5c-3c): production SpawnBgResumeDeps factory (makeProductionSpawnBgResumeDeps) wiring the 5c-3a watcher's 6 IO seams — no real pi/socket/timer (that=opt-in smoke-entwurf-v2-spawn-live, OUT of pnpm check). socketWatchVerdict: address-conflict→forged (reject, never wait)/alive→alive/dead·indeterminate→wait; spawnChild builds v2-control argv (--entwurf-control, no --no-extensions, -p+prompt, --approve, cwd authority); awaitSocketAlive connectable→resolve / symlink→reject without connect / dead→wait→alive / abort-clears; awaitChildExit code + listener cleanup; awaitTimeout schedule + abort-clear; killChild=SIGTERM; proc-less child fails loud
|
|
66
|
+
./run.sh smoke-entwurf-v2-spawn-live # LIVE phase gate (0.11 Stage 0 step 5c-3c, D5) — OUT of pnpm check, needs LIVE=1. Exercises the production SpawnBgResumeDeps against REAL OS objects: S1 real unix socket → awaitSocketAlive resolves (real lstat+probe), symlink→forged, absent→abort settles; S2 real child → spawn-event resolve + SIGTERM kill + exit-code capture; S3 watcher integration → real timeout→kill→child-exited→release ×1. Does NOT spawn a real pi resume (that=5d matrix). Run before 5d: LIVE=1 ./run.sh smoke-entwurf-v2-spawn-live
|
|
67
|
+
./run.sh smoke-entwurf-v2-spawn-resume-live # 0.11.0 (A) ACCEPTANCE gate — OUT of pnpm check, needs LIVE=1. The FULL spawn-bg resident lifecycle: mint backend=pi identity → seed a REAL dormant pi session (one-shot into ~/.pi/agent/sessions) → runEntwurfV2(owned-outcome) routes dormant→spawn-bg resume → a REAL detached pi --entwurf-control child stands its socket up, resumes, DOES a model turn. Asserts executed/spawn-bg/socket-alive/released + lock released ×1 + no lock file + pid alive + socket connectable + resume USER & assistant OK nonces in the session JSONL. Model-in-loop IN. The gate v1 deprecation (0.12) is predicated on. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4). LIVE=1 ./run.sh smoke-entwurf-v2-spawn-resume-live
|
|
68
|
+
./run.sh smoke-entwurf-v2-matrix-live # LIVE sentinel (0.11 Stage 0 step 5d-5, D4-b) — OUT of pnpm check, needs LIVE=1. Drives REAL production runEntwurfV2 deps over REAL OS objects, 4 cells: C1 control-socket (real pi --entwurf-control resident → RPC send → lock acquire→release ×1), C1b socket-only (record-less live pi → control-socket sent / owned→bad-target, A1 narrow), C2 meta-mailbox deliverable (armed self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver → reject, no garbage). Model-in-loop OUT (transport/lock/enqueue gate, GPT Q2); negative/timeout stay deterministic. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
|
|
69
|
+
./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b pi 4-value, facts-only keyset; union: PeerFact+SocketOnlyFact by gardenId, dormant→dead, F3 indeterminate preserved, non-pi+socket fail-loud; pure, no IO
|
|
70
|
+
./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
|
|
71
|
+
./run.sh check-meta-listing # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4a): META-STORE axis listAllMetaIdentities — explicit-partial: parse failure / body-filename drift → explicit {filename,message} error (verbatim, no synthetic fields), valid records still listed (corrupt doesn't blind); mode strict throws / collect partial; entries/readRecord injected, no IO
|
|
72
|
+
./run.sh check-entwurf-fact-provider # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4b): ASSEMBLY listEntwurfFacts — listAllMetaIdentities→scanSocketProbes→pre-quarantine non-pi/socket conflicts→resolveFactList(clean)→{facts,diagnostics}; C-원칙: expected corruption (parse/collision)→diagnostics (listing survives), impossible invariant (dup/unprobed)→throw; collision quarantines BOTH PeerFact+socket; deps injected, no IO
|
|
73
|
+
./run.sh check-entwurf-peers-surface # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4c): MCP entwurf_peers RENDER renderEntwurfPeers — legacy `sessions` = projection of facts (alive only, no 2nd scan), socketPath via controlSocketPath (SSOT), count=projection length, three distinct arrays, NO verb-routing key (JSON deep scan) NOR word (text), diagnostics both surfaces, empty→(none), unsupported shown, enrich→(not enriched); WIRING guard: bridge calls provider+render, getLiveSessions gone; facts fabricated, no IO
|
|
74
|
+
./run.sh check-entwurf-self-address # deterministic gate (SE-1/SE-2 slice 1): self-addressability honesty predicate computeSelfAddressability — pi replyable ⟺ live socket; meta ⟺ recordBacked ∧ ownerAlive ∧ watchArmed (regression-proof record-present rows); SOURCE GUARD buildStrictPiSenderEnvelope drops hardcoded replyable:true + existsSync-probes socket, entwurf_self renders alive vs expected. meta watchArmed wired in slice 2 (same release block)
|
|
75
|
+
./run.sh check-entwurf-deliverability # deterministic gate (SE-1/SE-2 slice 2c): conversational-mailbox deliverability predicate — computeMetaReceiverActive (recordBacked ∧ ownerAlive ∧ watchArmed) + mailboxConversationalDeliverable (self-fetch AND active); direct-inject pi refused (SE-1), self-fetch dead/unarmed refused (SE-2); self-address shares the same atom
|
|
76
|
+
./run.sh check-entwurf-mailbox-guard # deterministic gate (SE-1/SE-2 slice 2d): guarded mailbox enqueue — PURE 0-call (undeliverable target leaves injected enqueue uncalled) + TMPDIR snapshot (refused send leaves mailbox byte-identical, accepted writes one .msg) + fact gathering from record/capability/receiver-marker
|
|
77
|
+
./run.sh check-package-source-routing # deterministic gate (#29): package-source -> install-root mapping + fail-fast routing (local/git/npm/missing/project/no-source × local+remote, self-root, resume), no backend
|
|
78
|
+
./run.sh smoke-session-id-name # live 3-turn substrate smoke (Phase 3a): Pi 0.78 --session-id/--name through the bridge — header id/cwd, session_info name, append-not-recreate, spawn-only name, wrong-cwd footgun evidence
|
|
79
|
+
./run.sh new-session-id # print one fresh garden-native session id for operator launchers (--session-id)
|
|
80
|
+
./run.sh smoke-resident-garden-guard # live resident --entwurf-control garden guard (negative 0-token; SMOKE_RGG_POSITIVE=1 for positive)
|
|
81
|
+
./run.sh smoke-meta-async-drift # 1.0.0 meta-bridge step 1: drift sentinel — version pins + Claude binary undocumented-behavior markers (LIVE=1 adds plugin watch-arm probe)
|
|
82
|
+
./run.sh smoke-meta-honesty # 1.0.0 meta-bridge: honesty regression gate (#30 blockers) — doorbell counts ALL msgs honestly + hook logs failures as ERROR (best-effort, no scream). Offline/deterministic (deps: bash+node+python3)
|
|
83
|
+
./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
|
+
./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
|
+
./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)
|
|
86
|
+
./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
|
+
|
|
88
|
+
./run.sh install-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
|
|
89
|
+
./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
|
|
90
|
+
./run.sh doctor-meta-bridge # 1.0.0 meta-bridge Phase 2: fail-loud doctor — toolchain + state + plugin/MCP + store scan + hook errors + SessionStart evidence + writer-version parity (source↔assembled↔installed: FAIL on a stale deployed meta-record writer)
|
|
91
|
+
./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
|
|
92
|
+
./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
|
|
93
|
+
./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
|
|
94
|
+
./run.sh check-dep-versions # local deterministic check that version pins (package.json/run.sh/README.md + pi devDeps/peer pins) agree
|
|
95
|
+
./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
|
|
96
|
+
./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with 0.80.x peers
|
|
97
|
+
./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
|
|
98
|
+
./run.sh install [project-dir] # install this local package into project .pi/settings.json
|
|
99
|
+
./run.sh setup:links [--force] # repair ~/.pi/agent/entwurf-targets.json link (use --force to replace a stale operator file or wrong symlink; a .bak is taken)
|
|
100
|
+
./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json
|
|
101
|
+
|
|
102
|
+
Notes:
|
|
103
|
+
- project-dir defaults to current directory
|
|
104
|
+
- Claude Code login should already exist (e.g. ~/.claude.json)
|
|
105
|
+
- setup's runtime verification is the v2 install smoke (entwurf-bridge); the v2 dispatch substrate is proven live by release-gate
|
|
106
|
+
- API key is optional; this bridge is intended to work with Claude Code auth
|
|
107
|
+
EOF
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
require_cmd() {
|
|
111
|
+
command -v "$1" >/dev/null 2>&1 || {
|
|
112
|
+
echo "Missing command: $1" >&2
|
|
113
|
+
exit 1
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log() { echo " $*"; }
|
|
118
|
+
ok() { echo " ✅ $*"; }
|
|
119
|
+
warn() { echo " ⚠ $*"; }
|
|
120
|
+
fail() { echo " ❌ $*"; }
|
|
121
|
+
section() { echo ""; echo "=== $* ==="; }
|
|
122
|
+
|
|
123
|
+
normalize_project_dir() {
|
|
124
|
+
python3 - "$1" <<'PY'
|
|
125
|
+
import os, sys
|
|
126
|
+
print(os.path.abspath(os.path.expanduser(sys.argv[1])))
|
|
127
|
+
PY
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sync_auth() {
|
|
131
|
+
local auth_path="$HOME/.pi/agent/auth.json"
|
|
132
|
+
python3 - "$auth_path" "$PROVIDER_ID" <<'PY'
|
|
133
|
+
import json, os, sys
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
|
|
136
|
+
auth_path = Path(sys.argv[1]).expanduser()
|
|
137
|
+
provider_id = sys.argv[2]
|
|
138
|
+
auth_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
if auth_path.exists():
|
|
141
|
+
data = json.loads(auth_path.read_text())
|
|
142
|
+
if not isinstance(data, dict):
|
|
143
|
+
raise SystemExit("auth.json is not an object")
|
|
144
|
+
else:
|
|
145
|
+
data = {}
|
|
146
|
+
|
|
147
|
+
anthropic = data.get("anthropic")
|
|
148
|
+
if not isinstance(anthropic, dict):
|
|
149
|
+
print("sync-auth: skipped (no anthropic OAuth credentials in ~/.pi/agent/auth.json)")
|
|
150
|
+
raise SystemExit(0)
|
|
151
|
+
|
|
152
|
+
before = json.dumps(data.get(provider_id), sort_keys=True)
|
|
153
|
+
after = json.dumps(anthropic, sort_keys=True)
|
|
154
|
+
if before == after:
|
|
155
|
+
print(f"sync-auth: already synced ({provider_id})")
|
|
156
|
+
raise SystemExit(0)
|
|
157
|
+
|
|
158
|
+
data[provider_id] = anthropic
|
|
159
|
+
backup = auth_path.with_suffix(auth_path.suffix + ".bak")
|
|
160
|
+
if auth_path.exists():
|
|
161
|
+
backup.write_text(auth_path.read_text())
|
|
162
|
+
auth_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
163
|
+
print(f"sync-auth: wrote {provider_id} alias to {auth_path}")
|
|
164
|
+
if backup.exists():
|
|
165
|
+
print(f"sync-auth: backup -> {backup}")
|
|
166
|
+
PY
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Repo dependency integrity is a HARD requirement for install — NOT gated on
|
|
170
|
+
# backend (Claude/ACP) auth. A cloned-but-not-installed or a moved/renamed repo
|
|
171
|
+
# has missing or path-stale node_modules; `install` would otherwise happily wire
|
|
172
|
+
# settings.json and the failure only surfaces minutes later as a dead MCP bridge
|
|
173
|
+
# / pi-extension resolve error (the 2026-06-23 relocation failure: entwurf-bridge
|
|
174
|
+
# ✘ Failed to connect + check-entwurf-v2-surface ERR_MODULE_NOT_FOUND). Catch it
|
|
175
|
+
# HERE, before any settings file is written (no silent-red), with a package
|
|
176
|
+
# manifest access that follows the pnpm symlink to each dep — NOT a real module
|
|
177
|
+
# import (per-package "exports" maps forbid that uniformly) and NOT a bare
|
|
178
|
+
# `test -d node_modules`, which a dir-move breaks at the symlink-store level
|
|
179
|
+
# while the top-level dir still looks present.
|
|
180
|
+
preflight_dep_integrity() {
|
|
181
|
+
command -v node >/dev/null 2>&1 || { fail "node not on PATH — cannot verify repo dependency integrity"; exit 1; }
|
|
182
|
+
# Each wired pi-extension / MCP bridge / ACP backend root-imports its bundled
|
|
183
|
+
# runtime deps at load/spawn time. Assert they actually resolve from the
|
|
184
|
+
# package's location BEFORE writing settings, so a broken install fails loud
|
|
185
|
+
# here instead of surfacing later as a dead MCP bridge (entwurf-bridge ✘) or an
|
|
186
|
+
# ERR_MODULE_NOT_FOUND at runtime.
|
|
187
|
+
#
|
|
188
|
+
# Resolution must follow Node's OWN algorithm, because the package lives in two
|
|
189
|
+
# layouts: a pnpm clone (deps in $REPO_DIR/node_modules) OR a pi-managed
|
|
190
|
+
# `pi install npm:@junghanacs/entwurf` (deps HOISTED to an ancestor node_modules,
|
|
191
|
+
# e.g. ~/.pi/agent/npm/node_modules, with NO package-local node_modules). A
|
|
192
|
+
# cwd-relative `node_modules/<dep>` probe only sees the clone layout and wrongly
|
|
193
|
+
# rejects every pi-managed npm install. So walk Node's real module-resolution
|
|
194
|
+
# paths (Module._nodeModulePaths) and accessSync each candidate package.json:
|
|
195
|
+
# exports-immune (no bare-root / ./package.json import) and hoist-aware, while
|
|
196
|
+
# still catching a pnpm dir-move that left the symlink store dangling
|
|
197
|
+
# (accessSync follows the link).
|
|
198
|
+
#
|
|
199
|
+
# Probe set = the BUNDLED runtime `dependencies` only. The `@earendil-works/pi-*`
|
|
200
|
+
# peer trio is intentionally EXCLUDED: pi-managed installs omit peers
|
|
201
|
+
# (--legacy-peer-deps) and the pi loader provides that runtime itself, so the
|
|
202
|
+
# trio is legitimately absent from node_modules on the npm path. pi runtime
|
|
203
|
+
# presence/version is covered by check-pi-runtime-version / check-pi-import-surface.
|
|
204
|
+
local probe=(
|
|
205
|
+
"@modelcontextprotocol/sdk" "@agentclientprotocol/sdk" "@agentclientprotocol/claude-agent-acp"
|
|
206
|
+
"@anthropic-ai/sdk" "zod"
|
|
207
|
+
)
|
|
208
|
+
local missing=() dep
|
|
209
|
+
for dep in "${probe[@]}"; do
|
|
210
|
+
if ! (cd "$REPO_DIR" && node -e '
|
|
211
|
+
const M = require("module"), fs = require("fs"), path = require("path");
|
|
212
|
+
const dep = process.argv[1];
|
|
213
|
+
for (const nm of M._nodeModulePaths(process.cwd())) {
|
|
214
|
+
try { fs.accessSync(path.join(nm, dep, "package.json")); process.exit(0); } catch {}
|
|
215
|
+
}
|
|
216
|
+
process.exit(1);
|
|
217
|
+
' "$dep") >/dev/null 2>&1; then
|
|
218
|
+
missing+=("$dep")
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
if [ ${#missing[@]} -gt 0 ]; then
|
|
222
|
+
fail "repo dependency integrity check failed — cannot resolve: ${missing[*]}"
|
|
223
|
+
echo " node_modules is missing or path-stale (common right after a clone," >&2
|
|
224
|
+
echo " or a repo move/rename — pnpm's symlink store points at the old path)." >&2
|
|
225
|
+
echo " Fix: (cd \"$REPO_DIR\" && pnpm install)" >&2
|
|
226
|
+
echo " Then re-run ./run.sh install . — settings.json was NOT written." >&2
|
|
227
|
+
exit 1
|
|
228
|
+
fi
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
install_local_package() {
|
|
232
|
+
local project_dir
|
|
233
|
+
project_dir=$(normalize_project_dir "$1")
|
|
234
|
+
preflight_dep_integrity
|
|
235
|
+
mkdir -p "$project_dir/.pi"
|
|
236
|
+
python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
|
|
237
|
+
import json, sys
|
|
238
|
+
from pathlib import Path
|
|
239
|
+
|
|
240
|
+
settings_path = Path(sys.argv[1])
|
|
241
|
+
repo_dir = str(Path(sys.argv[2]).resolve())
|
|
242
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
if settings_path.exists():
|
|
244
|
+
data = json.loads(settings_path.read_text())
|
|
245
|
+
if not isinstance(data, dict):
|
|
246
|
+
raise SystemExit("settings.json is not an object")
|
|
247
|
+
else:
|
|
248
|
+
data = {}
|
|
249
|
+
|
|
250
|
+
# --- packages[] registration --------------------------------------------------
|
|
251
|
+
packages = data.get("packages")
|
|
252
|
+
if not isinstance(packages, list):
|
|
253
|
+
packages = []
|
|
254
|
+
|
|
255
|
+
filtered = []
|
|
256
|
+
for item in packages:
|
|
257
|
+
source = item.get("source") if isinstance(item, dict) else item
|
|
258
|
+
if isinstance(source, str) and ("entwurf" in source) and source != repo_dir:
|
|
259
|
+
continue
|
|
260
|
+
filtered.append(item)
|
|
261
|
+
|
|
262
|
+
if repo_dir not in filtered:
|
|
263
|
+
filtered.append(repo_dir)
|
|
264
|
+
|
|
265
|
+
data["packages"] = filtered
|
|
266
|
+
|
|
267
|
+
# --- entwurfProvider.mcpServers bundled entries ---------------------------
|
|
268
|
+
# Ship the two in-repo MCP adapters pre-wired so `pi install` produces a
|
|
269
|
+
# working setup without the consumer hand-editing settings.json. User-authored
|
|
270
|
+
# overrides (different command/args) are preserved untouched.
|
|
271
|
+
provider = data.setdefault("entwurfProvider", {})
|
|
272
|
+
if not isinstance(provider, dict):
|
|
273
|
+
raise SystemExit("entwurfProvider is not an object")
|
|
274
|
+
servers = provider.setdefault("mcpServers", {})
|
|
275
|
+
if not isinstance(servers, dict):
|
|
276
|
+
raise SystemExit("entwurfProvider.mcpServers is not an object")
|
|
277
|
+
|
|
278
|
+
BUNDLED = ("entwurf-bridge",)
|
|
279
|
+
# 0.4.14: session-bridge MCP was retracted (issue #7 — unified entwurf surface).
|
|
280
|
+
# Install path now only wires entwurf-bridge. Existing operator settings that
|
|
281
|
+
# carry the old bundled session-bridge entry are pruned below during install;
|
|
282
|
+
# `./run.sh remove` also keeps session-bridge in its cleanup tuple so legacy
|
|
283
|
+
# entries are removed on uninstall too.
|
|
284
|
+
for name in BUNDLED:
|
|
285
|
+
desired_cmd = f"{repo_dir}/mcp/{name}/start.sh"
|
|
286
|
+
desired = {"command": desired_cmd, "args": []}
|
|
287
|
+
existing = servers.get(name)
|
|
288
|
+
if existing is None:
|
|
289
|
+
servers[name] = desired
|
|
290
|
+
print(f"install: added entwurfProvider.mcpServers.{name}")
|
|
291
|
+
elif isinstance(existing, dict) and existing.get("command") == desired_cmd:
|
|
292
|
+
# Already managed by us at the current repo path. Add the default args
|
|
293
|
+
# field when missing, but never overwrite user-customized args.
|
|
294
|
+
if "args" not in existing:
|
|
295
|
+
existing["args"] = []
|
|
296
|
+
print(f"install: normalized entwurfProvider.mcpServers.{name}.args -> []")
|
|
297
|
+
elif existing.get("args") != []:
|
|
298
|
+
print(f"install: preserved entwurfProvider.mcpServers.{name}.args (custom args)")
|
|
299
|
+
else:
|
|
300
|
+
cmd_repr = existing.get("command") if isinstance(existing, dict) else existing
|
|
301
|
+
print(f"install: preserved entwurfProvider.mcpServers.{name} (user override: {cmd_repr})")
|
|
302
|
+
|
|
303
|
+
# Legacy-bundled MCP names that prior versions of this installer wrote and that
|
|
304
|
+
# the current cutover supersedes. One-shot install-time prune (NOT a runtime
|
|
305
|
+
# alias): only remove entries whose command matches the bundled start.sh path,
|
|
306
|
+
# so user-customized commands are left alone.
|
|
307
|
+
# - session-bridge: retracted in 0.4.14 (issue #7, unified entwurf surface).
|
|
308
|
+
# - pi-tools-bridge: renamed to entwurf-bridge in 0.11 S2 cutover.
|
|
309
|
+
LEGACY_BUNDLED = {
|
|
310
|
+
"session-bridge": "retracted in 0.4.14, issue #7",
|
|
311
|
+
"pi-tools-bridge": "renamed to entwurf-bridge in 0.11 S2 cutover",
|
|
312
|
+
}
|
|
313
|
+
for name, reason in LEGACY_BUNDLED.items():
|
|
314
|
+
existing = servers.get(name)
|
|
315
|
+
if not isinstance(existing, dict):
|
|
316
|
+
continue
|
|
317
|
+
cmd = existing.get("command")
|
|
318
|
+
if not isinstance(cmd, str):
|
|
319
|
+
continue
|
|
320
|
+
if cmd == f"{repo_dir}/mcp/{name}/start.sh" or cmd.endswith(f"/entwurf/mcp/{name}/start.sh"):
|
|
321
|
+
del servers[name]
|
|
322
|
+
print(f"install: pruned legacy entwurfProvider.mcpServers.{name} ({reason})")
|
|
323
|
+
|
|
324
|
+
settings_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
325
|
+
print(f"install: updated {settings_path}")
|
|
326
|
+
print(f"install: package source -> {repo_dir}")
|
|
327
|
+
PY
|
|
328
|
+
ensure_agent_dir_symlinks
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Ensure agent-level resources that entwurf code reads from
|
|
332
|
+
# ~/.pi/agent/ are wired up at install time. Currently:
|
|
333
|
+
# - entwurf-targets.json — pi-extensions/lib/entwurf-core.ts reads
|
|
334
|
+
# ~/.pi/agent/entwurf-targets.json. The package ships the canonical
|
|
335
|
+
# version at $REPO_DIR/pi/entwurf-targets.json. Without this symlink
|
|
336
|
+
# any entwurf tool call throws EntwurfRegistryError (lazy load — no
|
|
337
|
+
# surface during plain `pi --model ...` runs but blocks delegation
|
|
338
|
+
# immediately when the operator first calls entwurf).
|
|
339
|
+
#
|
|
340
|
+
# Fail-fast policy (added v0.5.0): drift between canonical and the
|
|
341
|
+
# operator's link/file is treated as a bug to fix, not noise to preserve.
|
|
342
|
+
# The v0.4.x oracle install regression came from a stale operator-copied
|
|
343
|
+
# entwurf-targets.json that previous installs silently kept "as operator
|
|
344
|
+
# file"; the only signal was a sentinel failure several minutes later.
|
|
345
|
+
#
|
|
346
|
+
# Two explicit exits are honored:
|
|
347
|
+
# 1. `./run.sh setup:links --force` — back up + overwrite with canonical
|
|
348
|
+
# 2. `ENTWURF_TARGETS_PATH=/path/to/custom.json` — tells entwurf-core
|
|
349
|
+
# to read elsewhere, freeing this slot from any policy obligation
|
|
350
|
+
#
|
|
351
|
+
# Idempotent for the happy path. Lazy registry load means a corrected
|
|
352
|
+
# file/symlink is picked up on the next entwurf call without restarting
|
|
353
|
+
# pi or the MCP bridge process.
|
|
354
|
+
ensure_agent_dir_symlinks() {
|
|
355
|
+
local agent_dir="$HOME/.pi/agent"
|
|
356
|
+
mkdir -p "$agent_dir"
|
|
357
|
+
|
|
358
|
+
local target="$REPO_DIR/pi/entwurf-targets.json"
|
|
359
|
+
local link="$agent_dir/entwurf-targets.json"
|
|
360
|
+
local force="${1:-}"
|
|
361
|
+
|
|
362
|
+
if [ ! -f "$target" ]; then
|
|
363
|
+
fail "canonical registry missing at $target — repo install is broken"
|
|
364
|
+
exit 1
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [ -L "$link" ]; then
|
|
368
|
+
local current
|
|
369
|
+
current=$(readlink "$link")
|
|
370
|
+
if [ "$current" = "$target" ]; then
|
|
371
|
+
return 0 # already correct, silent
|
|
372
|
+
fi
|
|
373
|
+
if [ "$force" = "--force" ]; then
|
|
374
|
+
rm -f "$link"
|
|
375
|
+
ln -s "$target" "$link"
|
|
376
|
+
echo "install: relinked $link -> $target (was -> $current)"
|
|
377
|
+
return 0
|
|
378
|
+
fi
|
|
379
|
+
fail "stale entwurf-targets symlink at $link"
|
|
380
|
+
echo " points to: $current" >&2
|
|
381
|
+
echo " expected: $target" >&2
|
|
382
|
+
echo " Fix with one of:" >&2
|
|
383
|
+
echo " ./run.sh setup:links --force # relink to canonical" >&2
|
|
384
|
+
echo " export ENTWURF_TARGETS_PATH=$current # honor your override explicitly" >&2
|
|
385
|
+
exit 1
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
if [ -e "$link" ]; then
|
|
389
|
+
if cmp -s "$link" "$target"; then
|
|
390
|
+
return 0 # operator copy is byte-identical to canonical, silent
|
|
391
|
+
fi
|
|
392
|
+
if [ "$force" = "--force" ]; then
|
|
393
|
+
local backup="${link}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
394
|
+
mv "$link" "$backup"
|
|
395
|
+
ln -s "$target" "$link"
|
|
396
|
+
echo "install: replaced stale $link with symlink -> $target (backup: $backup)"
|
|
397
|
+
return 0
|
|
398
|
+
fi
|
|
399
|
+
fail "stale entwurf-targets file at $link (drifts from canonical)"
|
|
400
|
+
echo " canonical: $target" >&2
|
|
401
|
+
echo " diff (link vs canonical):" >&2
|
|
402
|
+
diff -u "$link" "$target" | sed 's/^/ /' >&2 || true
|
|
403
|
+
echo " Fix with one of:" >&2
|
|
404
|
+
echo " ./run.sh setup:links --force # back up + replace with symlink to canonical" >&2
|
|
405
|
+
echo " export ENTWURF_TARGETS_PATH=$link # honor your file as an explicit override" >&2
|
|
406
|
+
exit 1
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
ln -s "$target" "$link"
|
|
410
|
+
echo "install: linked $link -> $target"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
remove_local_package() {
|
|
414
|
+
local project_dir
|
|
415
|
+
project_dir=$(normalize_project_dir "$1")
|
|
416
|
+
python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
|
|
417
|
+
import json, sys
|
|
418
|
+
from pathlib import Path
|
|
419
|
+
|
|
420
|
+
settings_path = Path(sys.argv[1])
|
|
421
|
+
repo_dir = str(Path(sys.argv[2]).resolve())
|
|
422
|
+
if not settings_path.exists():
|
|
423
|
+
print(f"remove: nothing to do ({settings_path} missing)")
|
|
424
|
+
raise SystemExit(0)
|
|
425
|
+
|
|
426
|
+
data = json.loads(settings_path.read_text())
|
|
427
|
+
if not isinstance(data, dict):
|
|
428
|
+
raise SystemExit("settings.json is not an object")
|
|
429
|
+
|
|
430
|
+
# --- packages[] cleanup -------------------------------------------------------
|
|
431
|
+
packages = data.get("packages")
|
|
432
|
+
pkg_removed = 0
|
|
433
|
+
if isinstance(packages, list):
|
|
434
|
+
filtered = []
|
|
435
|
+
for item in packages:
|
|
436
|
+
source = item.get("source") if isinstance(item, dict) else item
|
|
437
|
+
if isinstance(source, str) and ("entwurf" in source):
|
|
438
|
+
pkg_removed += 1
|
|
439
|
+
continue
|
|
440
|
+
filtered.append(item)
|
|
441
|
+
data["packages"] = filtered
|
|
442
|
+
|
|
443
|
+
# --- entwurfProvider.mcpServers cleanup ------------------------------------
|
|
444
|
+
# Only remove entries that look like they came from ./run.sh install: either
|
|
445
|
+
# the command matches the current $REPO_DIR anchor exactly, or it ends with
|
|
446
|
+
# the bundled "/entwurf/mcp/<name>/start.sh" pattern (covers a rebuilt
|
|
447
|
+
# checkout under a different directory). Anything else is treated as a user
|
|
448
|
+
# override and left in place.
|
|
449
|
+
BUNDLED = ("entwurf-bridge", "session-bridge", "pi-tools-bridge")
|
|
450
|
+
provider = data.get("entwurfProvider")
|
|
451
|
+
mcp_removed = 0
|
|
452
|
+
if isinstance(provider, dict):
|
|
453
|
+
servers = provider.get("mcpServers")
|
|
454
|
+
if isinstance(servers, dict):
|
|
455
|
+
for name in BUNDLED:
|
|
456
|
+
existing = servers.get(name)
|
|
457
|
+
if not isinstance(existing, dict):
|
|
458
|
+
continue
|
|
459
|
+
cmd = existing.get("command")
|
|
460
|
+
if not isinstance(cmd, str):
|
|
461
|
+
continue
|
|
462
|
+
exact = cmd == f"{repo_dir}/mcp/{name}/start.sh"
|
|
463
|
+
pattern = cmd.endswith(f"/entwurf/mcp/{name}/start.sh")
|
|
464
|
+
if exact or pattern:
|
|
465
|
+
del servers[name]
|
|
466
|
+
mcp_removed += 1
|
|
467
|
+
print(f"remove: removed entwurfProvider.mcpServers.{name}")
|
|
468
|
+
else:
|
|
469
|
+
print(f"remove: preserved entwurfProvider.mcpServers.{name} (user override: {cmd})")
|
|
470
|
+
if not servers:
|
|
471
|
+
provider.pop("mcpServers", None)
|
|
472
|
+
if not provider:
|
|
473
|
+
data.pop("entwurfProvider", None)
|
|
474
|
+
|
|
475
|
+
settings_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
476
|
+
print(f"remove: removed {pkg_removed} packages[] entries, {mcp_removed} mcpServers entries from {settings_path}")
|
|
477
|
+
PY
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
check_model_lock() {
|
|
481
|
+
# Deterministic policy unit test for pi-extensions/model-lock.ts.
|
|
482
|
+
# No pi process, no network, no API cost. Mocks ExtensionAPI/Context and
|
|
483
|
+
# drives the model_select handler through every quadrant + edge case
|
|
484
|
+
# (see scripts/check-model-lock.ts header for the full matrix).
|
|
485
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-model-lock.ts)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
check_shell_quote() {
|
|
489
|
+
# POSIX-safety gate for the shellQuote helper used in remote SSH command
|
|
490
|
+
# builders (entwurf.ts + entwurf-core.ts). Verifies source-string parity
|
|
491
|
+
# across the two duplication sites AND behavioral correctness on the
|
|
492
|
+
# payload classes that caused the 2026-05-18 remote entwurf incident
|
|
493
|
+
# (backtick / $(...) / $VAR / korean tokens). No process spawn, no SSH.
|
|
494
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-shell-quote.ts)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
check_entwurf_session_identity() {
|
|
498
|
+
# Deterministic gate for the locked garden session identity & name grammar
|
|
499
|
+
# (NEXT.md "Locked — session identity & name grammar"): sessionId validator,
|
|
500
|
+
# buildSessionName/parseSessionName round-trip incl. `.`-bearing registry
|
|
501
|
+
# models, titleSlug canonicalization, registry exact-tuple membership, name=
|
|
502
|
+
# info-only invariants, and header-scan collision pre-check. Isolates registry
|
|
503
|
+
# + sessions base to a temp dir. No backend, no API, no spawn.
|
|
504
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-session-identity.ts)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
check_meta_session() {
|
|
508
|
+
# Deterministic gate for the 1.0.0 meta-bridge record authority (#30 step 2):
|
|
509
|
+
# mint/serialize/parse round-trip + crash-on-malformed, scanByNativeId lookup
|
|
510
|
+
# authority BY RECORD BODY (not filename, proven with a decoy filename in a
|
|
511
|
+
# real temp dir), idempotent existence-keyed decideUpsert + identity-drift
|
|
512
|
+
# refusal, and the pre-drilled read-receipt mutators. Pure functions; no
|
|
513
|
+
# backend, no hook, no API.
|
|
514
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-session.ts)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
check_meta_record_v2() {
|
|
518
|
+
# Deterministic golden gate for 0.11 Stage 0 step 3A: the v1→v2 identity
|
|
519
|
+
# normalize seam. A synthetic, sanitized v1 fixture normalizes to a
|
|
520
|
+
# hand-written v2 identity literal (golden), plus dual-read version fences
|
|
521
|
+
# and v2 field-contract crashes. Reader/normalizer only — no v2 writer yet.
|
|
522
|
+
# Kept separate from check-meta-session so 3D's v1-gate rewrites leave this
|
|
523
|
+
# back-compat golden untouched. Pure functions; no backend, no hook, no API.
|
|
524
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-record-v2.ts)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
check_mailbox_receipt_state() {
|
|
528
|
+
# Deterministic gate for 0.11 Stage 0 step 3B: the mailbox receipt state
|
|
529
|
+
# schema + store — the new home for the read-receipt before v2 drops
|
|
530
|
+
# record.delivery (NEXT.md 고정순서 4). Pure schema round-trip + strict
|
|
531
|
+
# keyset, then the fs store (stamp → persist → read-back) in a temp mailbox
|
|
532
|
+
# dir. Schema/store only — no live enqueue/read dual-write (that is 3D). No
|
|
533
|
+
# backend, no hook, no API.
|
|
534
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-mailbox-receipt-state.ts)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
check_entwurf_capabilities() {
|
|
538
|
+
# Deterministic gate for 0.11 Stage 0 step 3C: the backend capability source
|
|
539
|
+
# (pi/entwurf-capabilities.json) — the new home for wakeMode/deliveryLevel/
|
|
540
|
+
# nativeIdLabel before v2 drops them from the record (frozen decision 1).
|
|
541
|
+
# Asserts coverage == META_BACKENDS_V2 (pi included), agreement with the live
|
|
542
|
+
# META_BACKEND_DESCRIPTORS for the three existing backends (drift guard), and
|
|
543
|
+
# strict keyset/coverage/field crashes. Parser/gate only — no live routing,
|
|
544
|
+
# no record/descriptor consumer change (that is 3D). No backend, no API.
|
|
545
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-capabilities.ts)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
check_meta_dual_read() {
|
|
549
|
+
# Deterministic gate for 0.11 Stage 0 step 3D-1: the v2 write shape
|
|
550
|
+
# (serializeMetaIdentity) + the dual-read dispatcher (parseMetaRecordAny /
|
|
551
|
+
# parseMetaIdentity). Canonical serialize + round-trip + version dispatch +
|
|
552
|
+
# unknown-version crash. Pure functions only — no fs upsert wiring, no
|
|
553
|
+
# readMetaInbox/enqueueMetaMessage change, no record.delivery removal (3D-2/3/4).
|
|
554
|
+
# No backend, no hook, no API.
|
|
555
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-read.ts)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
check_meta_mailbox_state_write() {
|
|
559
|
+
# Deterministic gate for 0.11 Stage 0 step 3D-4 commit2 (the cut). Renamed from
|
|
560
|
+
# check-meta-mailbox-dualwrite: after the cut the receipt is no longer dual-written
|
|
561
|
+
# (record.delivery is gone from the v2 record) — it lives SOLELY in the mailbox
|
|
562
|
+
# state store. Asserts the meta-record FILE is byte-identical before/after enqueue
|
|
563
|
+
# AND read (enqueue/read no longer touch the record — invariant ⑤), the state
|
|
564
|
+
# carries lastEnqueuedAt/lastReadAt with field isolation, lastDeliveredAt is never
|
|
565
|
+
# invented, an empty inbox is a no-op on BOTH record and state (⑥), and a state
|
|
566
|
+
# drift surfaces fail-loud. v2 citizen seeded via upsertMetaSession. No API.
|
|
567
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-mailbox-state-write.ts)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
check_meta_receiver_marker() {
|
|
571
|
+
# Deterministic gate for the meta-receiver presence marker (SE-2 slice 2b). The
|
|
572
|
+
# active-receiver signal a self-fetch backend (Claude Code) needs: a meta-record
|
|
573
|
+
# proves a session once existed; this marker proves a live watch owner is still
|
|
574
|
+
# there to be woken, so a terminated session's lingering record does not read as a
|
|
575
|
+
# ghost active receiver (mailbox garbage). Asserts: write→read round-trip keyed by
|
|
576
|
+
# GARDEN id, atomic 0600; dead-owner/pid-reuse start-key guard reads null (distinct
|
|
577
|
+
# from "no marker"); armProvenance constrained to the arm-capable events so
|
|
578
|
+
# UserPromptSubmit cannot mint presence; reader does NOT gate on record existence
|
|
579
|
+
# (recordBacked is the deliverability predicate's fact). Real tmpdir, no API.
|
|
580
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-receiver-marker.ts)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
check_meta_migration() {
|
|
584
|
+
# Deterministic gate for 0.11 Stage 0 step 3D-4 commit2: the v1→v2 delivery-receipt
|
|
585
|
+
# migration (migrateV1DeliveryReceipts) + its crash-order inside upsert. Per-field
|
|
586
|
+
# STATE WINS, 3 timestamps only; v1-all-null / state-already-wins are no-ops (no
|
|
587
|
+
# state.json). Crash-order: a v1 record's receipts migrate to state BEFORE the v2
|
|
588
|
+
# rewrite (proven via upsert attach), and a drift'd state makes migrate throw with
|
|
589
|
+
# the record STILL v1 (recoverable: next attach re-migrates). Temp dir, no API.
|
|
590
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-migration.ts)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
check_meta_dual_consumers() {
|
|
594
|
+
# Deterministic gate for 0.11 Stage 0 step 3D-4: the delivery-agnostic dual-read
|
|
595
|
+
# seam. readMetaIdentityByGardenId + scanIdentityByNativeId read v1 AND v2 records and
|
|
596
|
+
# return normalized identity, so the live consumers (enqueue/read, MCP marker, prune,
|
|
597
|
+
# store-doctor, the v2 upsert's existence scan) survive the v2 cut. Proves cross-schema
|
|
598
|
+
# match + THE G1 invariant (a nativeSessionId duplicated across a v1 AND v2 file is
|
|
599
|
+
# authority ambiguity → throw, so the v2 upsert never duplicate-mints) + v1 normalize +
|
|
600
|
+
# body/filename drift fail-fast. The v1-only raw readers remain for v1-fixture gates.
|
|
601
|
+
# Temp dir, no API.
|
|
602
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-consumers.ts)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
check_meta_capability_source() {
|
|
606
|
+
# Deterministic gate for 0.11 Stage 0 step 3D-3: the capability-source cut-over.
|
|
607
|
+
# mint/parse now read backend honesty metadata (wakeMode/deliveryLevel) from the
|
|
608
|
+
# capability registry (3C) via metaCapabilityFor, NOT META_BACKEND_DESCRIPTORS.
|
|
609
|
+
# Proves the seam is registry-DRIVEN (a doctored registry injection is followed),
|
|
610
|
+
# mint sources delivery metadata through it, the parse drift guard is now
|
|
611
|
+
# registry-sourced, and the cut-over preserves behaviour (registry ≡ const for the
|
|
612
|
+
# 3 backends). The record.delivery.wakeMode SLOT stays (removal is 3D-4); only the
|
|
613
|
+
# SOURCE moves. check-entwurf-capabilities still owns the registry ≡ const drift
|
|
614
|
+
# guard. Pure — no fs writes, no backend, no hook, no API.
|
|
615
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-capability-source.ts)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
check_socket_probe() {
|
|
619
|
+
# Deterministic gate for 0.11 Stage 0 (F3 fix): three-valued control-socket
|
|
620
|
+
# liveness. classifyConnectError is a pure boundary (ECONNREFUSED/ENOENT →
|
|
621
|
+
# dead; timeout/EACCES/unknown → indeterminate). GC reclaims dead only;
|
|
622
|
+
# indeterminate (a load-stalled live socket) survives the sweep — the F3
|
|
623
|
+
# invariant. Listing lists alive only. Pure classify + GC/listing policy +
|
|
624
|
+
# a two-socket integration (live listener → alive survives; nonexistent →
|
|
625
|
+
# dead GC-eligible). No wire timeout fixture, no backend, no API.
|
|
626
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-probe.ts)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
check_project_trust_handler() {
|
|
630
|
+
# Deterministic gate for 0.11 Stage 0 (Trust 2층 active-prompt escape): the
|
|
631
|
+
# project_trust handler. Pure decideProjectTrust over real preflight outcomes —
|
|
632
|
+
# approve/trusted-no-arg→{yes,remember:false}; direct distrust→{no}; inherited
|
|
633
|
+
# distrust + interactive + "trust-here"→{yes,remember:true} (THE escape, beats
|
|
634
|
+
# the ancestor false); non-interactive (pi -p / rpc)→{undecided}, never prompts;
|
|
635
|
+
# never undefined. Plus the thin adapter: fake ctx.ui.select, single-writer
|
|
636
|
+
# (handler never calls store.set — pi persists on remember:true), F5a evidence
|
|
637
|
+
# in the prompt title. No real UI, no backend, no API.
|
|
638
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-project-trust-handler.ts)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
check_entwurf_v2_contract() {
|
|
642
|
+
# Deterministic gate for 0.11 Stage 0 step 4-pre: the FROZEN entwurf_v2
|
|
643
|
+
# contract (동결결정 10 + 버킷 B F1/F4/F6 + Fable R1-R5). The intent×liveness
|
|
644
|
+
# decision table is a constant; the "table cell ↔ dispatch receipt" round-trip
|
|
645
|
+
# is asserted exhaustively — THE executable proof F6 demands ("산문 금지").
|
|
646
|
+
# R1 backend liveness domain (pi only; claude-code/codex/antigravity =
|
|
647
|
+
# unsupported, never folded into dead/indeterminate). 6-cell table, single
|
|
648
|
+
# verdict per cell (Q2), 2 allow / 4 reject. N1 indeterminate never spawns;
|
|
649
|
+
# Q2 owned-outcome+live never auto-sends. R5 taxonomy covers table reasons +
|
|
650
|
+
# pre-claims bad-target/untrusted-fail-fast/target-locked (bucket B F2). Plus
|
|
651
|
+
# a schema↔types drift guard on the TypeBox input/receipt. Pure, no API.
|
|
652
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-contract.ts)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
check_entwurf_v2_lock() {
|
|
656
|
+
# Deterministic gate for 0.11 Stage 0 step 5a (버킷 B F2): the per-gid dispatch
|
|
657
|
+
# LOCK primitive — the only guard against a double-spawn of the same dormant
|
|
658
|
+
# target (pi self-guards CREATE but not RESUME, 검증원장 F2). acquire =
|
|
659
|
+
# openSync(lockPath,"wx") atomic; a second acquire without release =
|
|
660
|
+
# target-locked conflict carrying the holder JSON (F2-P2 human cleanup). release
|
|
661
|
+
# = unlink ONLY when the on-disk nonce is still ours (a successor's re-acquire
|
|
662
|
+
# survives a late release). Stale reclaim ONLY for same host + ESRCH; EPERM
|
|
663
|
+
# (other user's live pid) / different host / alive pid / unknown error all
|
|
664
|
+
# fail-closed to conflict. Empty/corrupt lockfile = conflict, never
|
|
665
|
+
# auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
|
|
666
|
+
# temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
|
|
667
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-lock.ts)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
check_entwurf_v2_decider() {
|
|
671
|
+
# Deterministic gate for 0.11 Stage 0 step 5b: the PURE dispatch decider
|
|
672
|
+
# decideDispatch. Drives the frozen 7-step order over INJECTED fakes (target
|
|
673
|
+
# lookup / lock / socket inspect+probe / preflight / capability), tracking lock
|
|
674
|
+
# acquire+release so "reject ⇒ no plan AND no lock retained" is PROVEN. Covers:
|
|
675
|
+
# bad-target/target-locked/target-address-conflict carry observedLiveness=null
|
|
676
|
+
# (pre-probe), every other reject + untrusted-fail-fast carry a measured value;
|
|
677
|
+
# control-socket send + spawn-bg resume execute KEEP the lock, meta-mailbox send
|
|
678
|
+
# takes NO lock (?7); resume plan has no mode/wantsReply/provider/model but has
|
|
679
|
+
# expectedSocketPath/observeTimeoutMs/releaseWhen; an invalid gid throws before
|
|
680
|
+
# any lookup (F2-P1). Pure, no IO, no API.
|
|
681
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-decider.ts)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
check_entwurf_v2_matrix() {
|
|
685
|
+
# Deterministic gate for 0.11 Stage 0 step 5d-5 (a): the REACHABILITY + LOCK SSOT
|
|
686
|
+
# TABLE. Drives the REAL decideDispatch over minimal injected fakes and fixes, as
|
|
687
|
+
# one readable table, every (target kind → transport → lock class) cell the 5d-5
|
|
688
|
+
# claim covers: bad-target/address-conflict/target-locked rejects, unsupported
|
|
689
|
+
# meta-mailbox (deliverable) vs mailbox-undeliverable (inactive) vs owned reject,
|
|
690
|
+
# in-domain control-socket (live) / spawn-bg (dormant owned) / released rejects
|
|
691
|
+
# (owned-live, ff-dormant, indeterminate, under-lock conflict). A coverage pass
|
|
692
|
+
# FAILS if any transport / lock class / pre-probe reject is missing — a dropped
|
|
693
|
+
# decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
|
|
694
|
+
# parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
|
|
695
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-matrix.ts)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
check_entwurf_v2_release() {
|
|
699
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-1: the PURE release-policy reducer
|
|
700
|
+
# (decideReleasePolicy + reduceRelease) for the 5c transport hand. Proves the
|
|
701
|
+
# Fable-3 "release-after-observation" timing as a pure state machine BEFORE any
|
|
702
|
+
# spawn/send IO: meta-mailbox=never release (no lock), control-socket=release once
|
|
703
|
+
# on send-final, spawn-bg=spawn-started is NOT a release event (load-bearing) →
|
|
704
|
+
# release on the FIRST observed transition (socket-alive ∨ child-exited any code)
|
|
705
|
+
# or a failed start; socket↔exit race idempotent (single release either order);
|
|
706
|
+
# decideReleasePolicy enforces the lock-nullness invariant (?7). Pure, no IO.
|
|
707
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-release.ts)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
check_entwurf_v2_send() {
|
|
711
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-2a: the control-socket SEND hand
|
|
712
|
+
# (executeControlSocketSend) that WIRES real transport IO onto the 5c-1 release
|
|
713
|
+
# reducer. Proves the send->outcome->release ordering over injected fakes (no socket):
|
|
714
|
+
# ack->sent / in-band reject->rejected (no fallback) / dead->same-lock one-shot
|
|
715
|
+
# re-resolve (control retry or mailbox enqueue)->fallback-sent|rejected|failed /
|
|
716
|
+
# indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
|
|
717
|
+
# double-delivery on an alive-but-stalled socket). Release fires exactly once per
|
|
718
|
+
# send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
|
|
719
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send.ts)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
check_entwurf_v2_send_fallback() {
|
|
723
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-2b: the same-lock re-resolve RESOLVER
|
|
724
|
+
# (resolveDeadControlSendFallback) the 5c-2a hand calls on a dead connect. Proves the
|
|
725
|
+
# fire-and-forget re-resolve routing over injected fakes (no filesystem): alive->
|
|
726
|
+
# control-socket retry (inspected socketPath) / dead(absent)->reject (dormant-fire-
|
|
727
|
+
# forget-unsupported, NEVER spawn-bg) / indeterminate->reject / unsupported+deliverable
|
|
728
|
+
# ->meta-mailbox plan (mini-table, no inspect/probe) / unsupported+undeliverable->reject
|
|
729
|
+
# / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
|
|
730
|
+
# before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
|
|
731
|
+
# has NO release seam; every execute plan keeps the held gid and is never spawn-bg.
|
|
732
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send-fallback.ts)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
check_entwurf_v2_mailbox() {
|
|
736
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-4 (the LAST 5c transport slice): the
|
|
737
|
+
# ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + its production
|
|
738
|
+
# sendViaMailbox adapter (makeProductionSendViaMailbox). Proves the wiring over an
|
|
739
|
+
# injected fake enqueue (no filesystem): sender present -> formatMetaMailboxBody with
|
|
740
|
+
# plan.wantsReply threaded (yes/no in body, the deliberate divergence from legacy's
|
|
741
|
+
# hard-coded false) / sender absent -> raw plan.message / enqueue opts EXACTLY
|
|
742
|
+
# {gardenId: plan.targetGardenId, body, sessionsDir, mailboxDir} (no re-derivation) /
|
|
743
|
+
# enqueue throw PROPAGATES (never folded into success:false — a mailbox has no in-band
|
|
744
|
+
# refuse) / success -> {success:true}. Production adapter resolves {success:true},
|
|
745
|
+
# consults senderProvider once, and NEVER touches the lock (a poison LockClaim whose
|
|
746
|
+
# every access throws still resolves). Source guard: the lib code has NO release seam
|
|
747
|
+
# and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
|
|
748
|
+
# re-route is structurally impossible.
|
|
749
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-mailbox.ts)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
check_entwurf_v2_runner() {
|
|
753
|
+
# Deterministic gate for 0.11 Stage 0 step 5d-1: the execute-router (executeDispatch) that
|
|
754
|
+
# routes an already-decided DispatchDecision to its 5c transport hand and maps the outcome
|
|
755
|
+
# to one outcome-rich EntwurfV2RunResult. Proves over injected fake hands (no socket/spawn/
|
|
756
|
+
# timer): reject -> rejected (receipt+diagnostic carried, NO hand called) / control-socket ->
|
|
757
|
+
# sendControl(plan, lock) / spawn-bg -> resumeSpawnBg (socket-alive AND lock-retained both
|
|
758
|
+
# ride `executed`, fail-closed is not a failure) / meta-mailbox -> sendMailbox(plan, NULL
|
|
759
|
+
# lock, ?7). Carry-overs: N3 control `rejected` carries rejectReason verbatim; N1
|
|
760
|
+
# SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
|
|
761
|
+
# retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
|
|
762
|
+
# finalizedOutcome. Exactly one hand runs per execute.
|
|
763
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-runner.ts)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
check_entwurf_v2_surface() {
|
|
767
|
+
# Deterministic gate for 0.11 Stage 0 step 5d-3a: the ctx-free surface adapter
|
|
768
|
+
# (entwurf-v2-surface.ts) + the entwurf-control.ts wiring contract. Proves the pure parts:
|
|
769
|
+
# toDispatchInput (wants_reply→wantsReply, absent mode/wants_reply undefined) / renderEntwurfV2Result
|
|
770
|
+
# per result kind ({text,isError} surfacing reject diagnostic, control N3 rejectReason, spawn
|
|
771
|
+
# lock-retained, N1 delivered-but-dirty) / surface ctx-free source guard / entwurf-control
|
|
772
|
+
# registers entwurf_v2 + reaches the fence via a NON-LITERAL dynamic import (no static fence
|
|
773
|
+
# import → TS5097 stays closed) + decorates sender origin:pi-session/replyable:true.
|
|
774
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-surface.ts)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
check_entwurf_bridge_boot() {
|
|
778
|
+
# Deterministic gate for 0.11 step 5d-5-pre (G1a/G1b): boots the entwurf-bridge MCP server
|
|
779
|
+
# as it ships (start.sh → node --experimental-strip-types, no build) and asserts what the
|
|
780
|
+
# source-shape gate check-entwurf-v2-surface cannot — that the whole v2 fence graph LOADS at
|
|
781
|
+
# boot under strip-types (G1a: a parseable tools/list proves it) and that entwurf_v2 is
|
|
782
|
+
# registered on the runtime surface with its schema (G1b). tools/list only → no tools/call,
|
|
783
|
+
# no lock/fs side effect, no auth → safe in pnpm check. Broad protocol/negative suite stays
|
|
784
|
+
# in check-bridge/test.sh (D1=A안).
|
|
785
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-boot.ts)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
check_entwurf_v2_production() {
|
|
789
|
+
# Deterministic gate for 0.11 Stage 0 step 5d-2b: makeProductionEntwurfV2Deps — the ctx-free
|
|
790
|
+
# PRODUCTION assembly of runEntwurfV2's deps. Proves the wiring over fake leaf-IO spies (no
|
|
791
|
+
# real socket/lock/spawn/meta-record): decide wraps decideDispatch and acquires under the
|
|
792
|
+
# wired lockDir / control sendOverSocket builds the RpcSendCommand + maps + releases under
|
|
793
|
+
# lockDir / QB3 the spawn watcher releases via the SHARED lockDir release (not the spawn
|
|
794
|
+
# factory default) / the mailbox hand enqueues onto the wired dirs / a dead control send
|
|
795
|
+
# re-resolves to the SAME sendViaMailbox instance on the SAME dirs (Q3+Q5 no drift).
|
|
796
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-production.ts)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
check_entwurf_control_rpc() {
|
|
800
|
+
# Gate for 0.11 Stage 0 step 5d-2 (RPC-helper extraction micro-slice): the --entwurf-control
|
|
801
|
+
# socket protocol (wire types + the newline-JSON client sendRpcCommand) moved to the ctx-free
|
|
802
|
+
# SSOT lib/entwurf-control-rpc.ts behaviour-preservingly. Proves: lib is ctx-free (no
|
|
803
|
+
# ExtensionContext/ExtensionAPI/@earendil-works/pi-ai) / entwurf-control.ts imports
|
|
804
|
+
# sendRpcCommand from the lib and no longer defines its own / real short unix-socket round-trip
|
|
805
|
+
# (write command -> matched {type:response,command,success:true} -> resolve) / close-before-
|
|
806
|
+
# response rejects 'connection closed before response'. net.Server only, no model/pi process.
|
|
807
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-control-rpc.ts)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
check_entwurf_v2_spawn() {
|
|
811
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-3a: the spawn-bg RESUME watcher hand
|
|
812
|
+
# (executeSpawnBgResume) wiring spawn + socket-observe IO onto the 5c-1 reducer. Proves
|
|
813
|
+
# Fable-3 over injected deferred promises (no real child/socket/timer): TIMEOUT IS NOT A
|
|
814
|
+
# RELEASE — a bare observeTimeout escalates to killChild (release 0), then a BOUNDED
|
|
815
|
+
# killGrace waits for a real socket-alive / child-exited to release. socket-alive ∨
|
|
816
|
+
# child-exited(any code incl. null) -> release exactly once; the loser settling later is a
|
|
817
|
+
# no-op. spawnChild throw -> spawn-start-failed (release, nothing to watch). No observation
|
|
818
|
+
# obtainable (grace elapses, or a post-spawn watch dep throws and the exit can't be
|
|
819
|
+
# observed) -> lock-retained fail-closed (released:false, pid/socket/lockPath surfaced) —
|
|
820
|
+
# there is NO direct-release hatch; deps.releaseLock is reached ONLY via reduceRelease.
|
|
821
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn.ts)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
check_entwurf_resume_args() {
|
|
825
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-3b: the resume-argv SSOT
|
|
826
|
+
# (buildResumePiArgs) that the legacy async worker AND the v2 spawn-bg resident citizen
|
|
827
|
+
# share so their launch shapes never drift. Pins the load-bearing A1 difference: legacy =
|
|
828
|
+
# `--no-extensions` + NO `--entwurf-control` (one-shot `pi -p` can exit); v2-control =
|
|
829
|
+
# `--entwurf-control` + NO `--no-extensions` (the keep-alive legacy avoided is the goal —
|
|
830
|
+
# resumed session stands its socket up and stays addressable). BOTH keep `--mode json -p`
|
|
831
|
+
# and the prompt-as-turn positional (`-p` NOT dropped in v2); explicitExtensionArgs
|
|
832
|
+
# preserved exactly once (provider-resolution footgun #29); v2 includes plan.launchArgs
|
|
833
|
+
# (--approve) before the prompt; null provider emits no --provider; no cross-contamination.
|
|
834
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-resume-args.ts)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
check_entwurf_v2_spawn_production() {
|
|
838
|
+
# Deterministic gate for 0.11 Stage 0 step 5c-3c: the production SpawnBgResumeDeps factory
|
|
839
|
+
# (makeProductionSpawnBgResumeDeps) wiring the 5c-3a watcher's six IO seams onto the real
|
|
840
|
+
# world — proven WITHOUT a real pi spawn/socket/timer (that is the opt-in
|
|
841
|
+
# smoke-entwurf-v2-spawn-live, kept OUT of pnpm check). socketWatchVerdict (R2 policy):
|
|
842
|
+
# address-conflict→forged (reject, never wait), alive→alive, dead/indeterminate→wait.
|
|
843
|
+
# spawnChild builds the v2-control argv (--entwurf-control, no --no-extensions, -p+prompt,
|
|
844
|
+
# --approve, ext/provider/model, header cwd). awaitSocketAlive: connectable resolves, forged
|
|
845
|
+
# (symlink) rejects without connecting, dead→wait→alive, abort clears the sleep. awaitChildExit
|
|
846
|
+
# resolves the code + removes the listener on abort. awaitTimeout schedules + abort-clears.
|
|
847
|
+
# killChild=SIGTERM; releaseLock delegates; a proc-less child fails loud (mis-wire).
|
|
848
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn-production.ts)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
smoke_entwurf_v2_spawn_live() {
|
|
853
|
+
# LIVE phase gate for 0.11 Stage 0 step 5c-3c (D5) — kept OUT of `pnpm check`. Exercises the
|
|
854
|
+
# production SpawnBgResumeDeps against REAL OS objects (a real unix socket, real child
|
|
855
|
+
# processes, real timers, real abort teardown) to catch what the deterministic gate's fakes
|
|
856
|
+
# cannot: actual spawn/exit/error event semantics, real lstat+connect liveness, and the
|
|
857
|
+
# 5c-3a watcher's timeout→kill→child-exited→release integration on a live process. It does
|
|
858
|
+
# NOT spawn a real `pi --entwurf-control` resume (that is the 5d surface matrix). Run once
|
|
859
|
+
# before 5d and record the result: LIVE=1 ./run.sh smoke-entwurf-v2-spawn-live
|
|
860
|
+
if [ "${LIVE:-}" != "1" ]; then
|
|
861
|
+
echo "[smoke-entwurf-v2-spawn-live] skipped — set LIVE=1 to run (spawns real children + opens a real unix socket)."
|
|
862
|
+
return 0
|
|
863
|
+
fi
|
|
864
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-live.ts)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
smoke_acp_socket_citizen_live() {
|
|
868
|
+
# S1 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
|
|
869
|
+
# Spawns a REAL `pi --entwurf-control` resident on an ACP model
|
|
870
|
+
# (entwurf/claude-opus-4-8) and proves it is a first-class socket-citizen:
|
|
871
|
+
# the control socket stands up, get_info answers with the ACP model (model-lock
|
|
872
|
+
# did NOT revert — QM1), idle/cwd are reported, and the fail-loud streamSimple
|
|
873
|
+
# stub never fires (turn-free launch — QM2). No prompt is sent: S1 proves
|
|
874
|
+
# citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
|
|
875
|
+
# Model override: ENTWURF_S1_MODEL (default claude-opus-4-8).
|
|
876
|
+
# LIVE=1 ./run.sh smoke-acp-socket-citizen-live
|
|
877
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-socket-citizen-live.ts)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
smoke_acp_raw_turn_live() {
|
|
881
|
+
# S2a-2 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
|
|
882
|
+
# Drives ONE real ACP turn through the pinned Claude adapter: spawns
|
|
883
|
+
# claude-agent-acp from its resolved package bin, speaks ACP over stdio NDJSON
|
|
884
|
+
# (ndJsonStream + the connectAcpClient adapter), runs initialize -> newSession ->
|
|
885
|
+
# (sonnet) setSessionConfigOption(model) -> prompt("say OK"), and asserts a live "OK" reply
|
|
886
|
+
# plus captured raw NDJSON bytes. NO provider/overlay/streamSimple/_meta — the
|
|
887
|
+
# raw backend pipe only. Launch source must be the package bin (PATH fallback
|
|
888
|
+
# fails acceptance unless ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1, debug).
|
|
889
|
+
# Model override: ENTWURF_ACP_RAW_TURN_MODEL (default claude-sonnet-4-6).
|
|
890
|
+
# LIVE=1 ./run.sh smoke-acp-raw-turn-live
|
|
891
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-raw-turn-live.ts)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
smoke_acp_overlay_live() {
|
|
895
|
+
# S2b acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
|
|
896
|
+
# One layer above the S2a raw turn: materializes the Claude config overlay
|
|
897
|
+
# (realDir = operator ~/.claude for live creds; overlay settings.json is ours,
|
|
898
|
+
# hooks:{}), spawns claude-agent-acp with CLAUDE_CONFIG_DIR=<overlay> (verified
|
|
899
|
+
# in the child's /proc/<pid>/environ), opens a session with a tool-narrowed
|
|
900
|
+
# _meta.claudeCode.options (tools + disallowedTools) and NO _meta.systemPrompt
|
|
901
|
+
# (billing carrier stays absent), then drives one live "OK" turn. NO
|
|
902
|
+
# provider/streamSimple (backend-stub stays fail-loud — that is S2c); no
|
|
903
|
+
# event-mapping/session-reuse/engraving (S2d). Does NOT diff the live
|
|
904
|
+
# meta-store for mailbox absence (flaky — concurrent sessions); the honest
|
|
905
|
+
# claim is overlay-supplies-hooks:{}. Launch must be the package bin (PATH
|
|
906
|
+
# fallback fails acceptance unless ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1).
|
|
907
|
+
# Model override: ENTWURF_ACP_OVERLAY_MODEL (default claude-sonnet-4-6).
|
|
908
|
+
# LIVE=1 ./run.sh smoke-acp-overlay-live
|
|
909
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-overlay-live.ts)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
smoke_acp_memory_containment_live() {
|
|
913
|
+
# Gate D — ACP Claude memory containment, end-to-end. OUT of pnpm check, LIVE=1.
|
|
914
|
+
# THE regression guard that was missing: drives the SHIPPED config (overlay +
|
|
915
|
+
# PRESENT engraving carrier = the v1 preset-replacement lever) with a turn that
|
|
916
|
+
# EXPLICITLY asks the model to persist a nonce to its memory, then asserts NO
|
|
917
|
+
# file appears under <overlay>/projects/**/memory/**. Permission is GRANTED (not
|
|
918
|
+
# cancelled) and writeTextFile delegation is PERFORMED, so the only thing that
|
|
919
|
+
# can stop a memory write is the lever — not us. Fails loud if engraving.md is
|
|
920
|
+
# empty (carrier OFF = no containment). Launch must be the package bin (PATH
|
|
921
|
+
# fallback fails acceptance unless ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK=1).
|
|
922
|
+
# Model override: ENTWURF_ACP_MEMORY_MODEL (default claude-sonnet-4-6).
|
|
923
|
+
# LIVE=1 ./run.sh smoke-acp-memory-containment-live
|
|
924
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-memory-containment-live.ts)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
smoke_acp_provider_live() {
|
|
928
|
+
# S2c acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
|
|
929
|
+
# Drives the REAL pi PROVIDER path end to end: a real `pi` loads this
|
|
930
|
+
# checkout's extension (--no-extensions -e REPO_ROOT), selects
|
|
931
|
+
# entwurf/<model>, and pi's runner calls our streamSimple (backend.ts),
|
|
932
|
+
# which spawns claude-agent-acp under the overlay, runs one turn, and maps the
|
|
933
|
+
# result back through the S2c event mapper. Asserts a unique nonce in the
|
|
934
|
+
# assistant reply (live model proof) + the removed S0 stub error never appears
|
|
935
|
+
# (provider path actually opened) + pi exits 0. Tool-free prompt; the
|
|
936
|
+
# event-mapper gate owns the tool→notice contract.
|
|
937
|
+
# Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-4-6).
|
|
938
|
+
# LIVE=1 ./run.sh smoke-acp-provider-live
|
|
939
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-provider-live.ts)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
smoke_acp_session_reuse_live() {
|
|
943
|
+
# S2d-1b-2b acceptance smoke (in-memory session reuse) — OUT of pnpm check,
|
|
944
|
+
# needs LIVE=1. Forces process-scoped (pushes --entwurf-control into argv) and
|
|
945
|
+
# drives TWO real ACP turns over ONE reused claude-agent-acp child via the real
|
|
946
|
+
# streamShellAcp: turn 1 introduces a codeword (full transcript), turn 2 sends
|
|
947
|
+
# ONLY the latest user delta and must recall the codeword — proving the child
|
|
948
|
+
# was reused and the live ACP session kept turn-1 history (a respawn-per-turn
|
|
949
|
+
# backend would forget it). The one-shot exit0 half is owned by
|
|
950
|
+
# smoke-acp-provider-live.
|
|
951
|
+
# Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-4-6).
|
|
952
|
+
# LIVE=1 ./run.sh smoke-acp-session-reuse-live
|
|
953
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-session-reuse-live.ts)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
smoke_acp_carrier_augment_live() {
|
|
957
|
+
# S2e-1 acceptance smoke (billing carrier + first-user augment) — OUT of pnpm
|
|
958
|
+
# check, needs LIVE=1. Writes a unique secret into the scratch cwd's AGENTS.md
|
|
959
|
+
# (never the prompt) and drives one real provider turn: the reply must carry the
|
|
960
|
+
# secret (the augment rode the wire to the model) and the EMPTY default carrier
|
|
961
|
+
# must bill clean (exit 0, no HTTP-400 canary — 핀1 live). Optional tiny carrier
|
|
962
|
+
# check via SMOKE_ACP_CARRIER_PRESENT=1 (non-blocking).
|
|
963
|
+
# Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-4-6).
|
|
964
|
+
# LIVE=1 ./run.sh smoke-acp-carrier-augment-live
|
|
965
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-carrier-augment-live.ts)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
smoke_acp_mcp_live() {
|
|
969
|
+
# S2g LIVE 1 — operator MCP passthrough acceptance. OUT of pnpm check, needs
|
|
970
|
+
# LIVE=1. Registers a TINY isolated probe MCP server (scripts/fixtures/
|
|
971
|
+
# probe-mcp-server.ts, one tool probe_nonce) in a scratch .pi/settings.json and
|
|
972
|
+
# drives one real provider turn: the model must CALL the tool and echo the nonce
|
|
973
|
+
# that lives only inside the MCP server env. Proves the operator's
|
|
974
|
+
# entwurfProvider.mcpServers reaches the live ACP session (the GLG-baseline
|
|
975
|
+
# fix). Isolated probe (not entwurf-bridge) so a failure does not blur into
|
|
976
|
+
# identity/env wiring. Model override: ENTWURF_ACP_PROVIDER_MODEL.
|
|
977
|
+
# LIVE=1 ./run.sh smoke-acp-mcp-live
|
|
978
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-mcp-live.ts)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
smoke_acp_skill_live() {
|
|
982
|
+
# S2g LIVE 2 — operator skillPlugins passthrough acceptance. OUT of pnpm check,
|
|
983
|
+
# needs LIVE=1. Builds a temp skill plugin (.claude-plugin/plugin.json +
|
|
984
|
+
# skills/<name>/SKILL.md carrying a unique nonce instruction), points
|
|
985
|
+
# entwurfProvider.skillPlugins at it, and drives one real provider turn: the
|
|
986
|
+
# model must surface/use the skill and echo the nonce. Proves skillPlugins +
|
|
987
|
+
# the Skill/Skill(*) auto-add reach the live session (the other half of the GLG
|
|
988
|
+
# baseline). Model override: ENTWURF_ACP_PROVIDER_MODEL.
|
|
989
|
+
# LIVE=1 ./run.sh smoke-acp-skill-live
|
|
990
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-skill-live.ts)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
smoke_acp_bundled_mcp_live() {
|
|
994
|
+
# S2g LIVE 3 (axis 3) — the BUNDLED entwurf-bridge reaches the live ACP session
|
|
995
|
+
# via the 0.11.0 resident/RPC circuit. OUT of pnpm check, needs LIVE=1. Launches a
|
|
996
|
+
# real `pi --entwurf-control --mode rpc` resident on an ACP model and drives ONE
|
|
997
|
+
# model turn over the stdin RPC asking it to call mcp__entwurf-bridge__entwurf_self;
|
|
998
|
+
# captures the identity envelope (the resident's own fresh gid — never told to the
|
|
999
|
+
# model, only in the bridge env — + agentId + socketState alive) and agent_end
|
|
1000
|
+
# DIRECTLY from the stdout RPC event stream (gnew-rpc-drive shape). Complements
|
|
1001
|
+
# smoke-acp-mcp-live (tiny isolated probe): this proves the REAL bundled bridge with
|
|
1002
|
+
# envelope injection. NOT `pi -p` one-shot (that bundled-MCP teardown hang is
|
|
1003
|
+
# diagnostic backlog, not the 0.11.0 release circuit). Model override:
|
|
1004
|
+
# ENTWURF_ACP_PROVIDER_MODEL.
|
|
1005
|
+
# LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
|
|
1006
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-bundled-mcp-live.ts)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
smoke_acp_rgg_live() {
|
|
1010
|
+
# S2e-2 — ACP-provider resident garden guard (RGG). Thin wrapper (GPT c32a6c8):
|
|
1011
|
+
# runs the SHARED resident-garden-guard runner against the entwurf provider
|
|
1012
|
+
# target with the DETERMINISTIC half only (SMOKE_RGG_POSITIVE=0). What this lane
|
|
1013
|
+
# treats as release-blocking is that garden-native resident discipline (uuid
|
|
1014
|
+
# refuse / new·clone cancel / legacy-resume pre-cancel / gnew clean birth) holds
|
|
1015
|
+
# under the ACP provider too — the guard logic is provider-agnostic. The positive
|
|
1016
|
+
# GNEW T3 (model autonomously calling entwurf_self) is N/A here BY ACP BOUNDARY:
|
|
1017
|
+
# the ACP child is spawned with mcpServers:[] so it has no entwurf_self call
|
|
1018
|
+
# surface (plugin stays lightweight, no ambient MCP — S2b/S2d boundary). To
|
|
1019
|
+
# observe that boundary directly, run the shared runner with SMOKE_RGG_POSITIVE=1
|
|
1020
|
+
# and ENTWURF_LIVE_TARGET set (T3 will report N/A, not a real failure).
|
|
1021
|
+
# Target override: ENTWURF_RGG_TARGET (default entwurf/claude-sonnet-4-6).
|
|
1022
|
+
# ./run.sh smoke-acp-rgg-live
|
|
1023
|
+
local target="${ENTWURF_RGG_TARGET:-entwurf/claude-sonnet-4-6}"
|
|
1024
|
+
(cd "$REPO_DIR" && ENTWURF_LIVE_TARGET="$target" SMOKE_RGG_POSITIVE=0 bash scripts/smoke-resident-garden-guard.sh)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
smoke_entwurf_v2_matrix_live() {
|
|
1028
|
+
# LIVE sentinel for 0.11 Stage 0 step 5d-5 (D4-b) — kept OUT of `pnpm check`. The deterministic
|
|
1029
|
+
# sibling (check-entwurf-v2-matrix) fixes every (target kind → transport → lock) cell over fakes
|
|
1030
|
+
# with ZERO IO; this drives the REAL production runEntwurfV2 deps against REAL OS objects on the
|
|
1031
|
+
# substrate happy path across 3 cells: C1 control-socket (a real `pi --entwurf-control` resident
|
|
1032
|
+
# → control-socket RPC send → lock acquire→release ×1), C2 meta-mailbox deliverable (armed
|
|
1033
|
+
# self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver →
|
|
1034
|
+
# reject, no garbage). Model-in-loop is OUT (GPT Q2): "does the sender model call entwurf_v2"
|
|
1035
|
+
# is a separate behavior test — this is a transport/lock/enqueue gate. Negative/timeout/contention
|
|
1036
|
+
# stay deterministic. Honest skip when LIVE!=1 so the release-gate is runnable unattended.
|
|
1037
|
+
# Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4).
|
|
1038
|
+
# LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
|
|
1039
|
+
if [ "${LIVE:-}" != "1" ]; then
|
|
1040
|
+
echo "[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket)."
|
|
1041
|
+
return 0
|
|
1042
|
+
fi
|
|
1043
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-matrix-live.ts)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
smoke_entwurf_v2_spawn_resume_live() {
|
|
1047
|
+
# The 0.11.0 (A) acceptance gate — kept OUT of `pnpm check`. Unlike matrix-live (a
|
|
1048
|
+
# transport/lock sentinel, model-in-loop OUT) and spawn-live (OS-substrate watcher, no real
|
|
1049
|
+
# pi), this drives the FULL production loop: mint a backend=pi meta identity → seed a REAL
|
|
1050
|
+
# dormant pi session (one-shot `pi --mode json -p --no-extensions` into the REAL
|
|
1051
|
+
# ~/.pi/agent/sessions) → runEntwurfV2(intent=owned-outcome) routes the dormant in-domain pi
|
|
1052
|
+
# citizen to spawn-bg resume → a REAL detached `pi --entwurf-control` child stands its socket
|
|
1053
|
+
# up, resumes, and DOES a model turn. Asserts: executed/spawn-bg/socket-alive/released, lock
|
|
1054
|
+
# released exactly once + no lock file, resident pid alive + socket connectable, and the
|
|
1055
|
+
# resume USER + assistant OK nonces appended to the session JSONL (real work, not just
|
|
1056
|
+
# "process up"). This is the evidence v1 deprecation (0.12) is predicated on. Model-in-loop is
|
|
1057
|
+
# IN. Honest skip when LIVE!=1 (skip = CI safety, NOT an acceptance PASS).
|
|
1058
|
+
# Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4);
|
|
1059
|
+
# ENTWURF_SPAWN_RESUME_ASSISTANT_TIMEOUT_MS (default 180000).
|
|
1060
|
+
# LIVE=1 ./run.sh smoke-entwurf-v2-spawn-resume-live
|
|
1061
|
+
if [ "${LIVE:-}" != "1" ]; then
|
|
1062
|
+
echo "[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi resume child + opens a real socket)."
|
|
1063
|
+
return 0
|
|
1064
|
+
fi
|
|
1065
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-resume-live.ts)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
check_entwurf_facts() {
|
|
1069
|
+
# Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 1): the PURE
|
|
1070
|
+
# fact core. Locks the PeerFact shape + R1/R3b liveness invariant before any IO
|
|
1071
|
+
# wiring (gate-first). R1: out-of-domain backend (claude-code/codex/antigravity)
|
|
1072
|
+
# → unsupported for EVERY socket input (never coerced to the socket value or
|
|
1073
|
+
# dead). R3b: in-domain pi → alive/dead/indeterminate, null → indeterminate
|
|
1074
|
+
# (no proof ≠ dead). facts-only keyset: identity facts + liveness, NO
|
|
1075
|
+
# verb-routing (resumable/sendable/transport/dispatch/action) and NO
|
|
1076
|
+
# transcriptPath (동결결정 10). Pure, no IO, no API.
|
|
1077
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-facts.ts)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
check_socket_discovery() {
|
|
1081
|
+
# Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 3): the
|
|
1082
|
+
# SOCKET-axis wiring scanSocketProbes. Probes the union of (dir sockets) ∪
|
|
1083
|
+
# (every in-domain pi citizen's canonical path) so a dormant citizen with no
|
|
1084
|
+
# socket file reads dead (ENOENT) → resumable, never an unprobed gap (slice 2
|
|
1085
|
+
# throws on that). Three-valued throughout — a stalled socket stays
|
|
1086
|
+
# indeterminate (F3), never folded to dead by an alive-only listing. Dir
|
|
1087
|
+
# hygiene (non-.sock / malformed names ignored), dedup, missing-dir, sort, and
|
|
1088
|
+
# an end-to-end scanSocketProbes→resolveFactList. readdir/probe injected, no IO.
|
|
1089
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-discovery.ts)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
check_meta_listing() {
|
|
1093
|
+
# Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4a): the
|
|
1094
|
+
# meta-store axis listAllMetaIdentities. Explicit-partial: a parse failure or
|
|
1095
|
+
# body/filename drift does NOT blind the listing (valid records still surface)
|
|
1096
|
+
# and does NOT throw (0.10 "corrupt blocks registration forever" lesson) — it
|
|
1097
|
+
# becomes an explicit error carrying ONLY {filename, message}, verbatim (a
|
|
1098
|
+
# salvaged gid string as a fact = synthetic backdoor). mode strict throws on
|
|
1099
|
+
# any error, collect returns partial. entries/readRecord injected, no IO.
|
|
1100
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-listing.ts)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
check_entwurf_fact_provider() {
|
|
1104
|
+
# Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4b): the
|
|
1105
|
+
# ASSEMBLY layer listEntwurfFacts. listAllMetaIdentities → scanSocketProbes →
|
|
1106
|
+
# pre-quarantine non-pi/socket conflicts → resolveFactList(clean) →
|
|
1107
|
+
# {facts, diagnostics}. Throw-vs-diagnostics policy (GPT힣 C-원칙): expected
|
|
1108
|
+
# corruption (parse failure / gardenId↔socket collision) → diagnostics, listing
|
|
1109
|
+
# survives; impossible wiring invariant (resolveFactList duplicate/unprobed) →
|
|
1110
|
+
# throw, never swallowed. A collision quarantines BOTH the PeerFact and the
|
|
1111
|
+
# socket (gid is the universal address). meta + socket deps injected, no IO.
|
|
1112
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-fact-provider.ts)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
check_entwurf_peers_surface() {
|
|
1116
|
+
# Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4c): the MCP
|
|
1117
|
+
# entwurf_peers RENDER/PAYLOAD layer renderEntwurfPeers. Legacy `sessions` is a
|
|
1118
|
+
# PROJECTION of facts (alive pi citizens + alive socket-only), NOT a second scan
|
|
1119
|
+
# (a re-run getLiveSessions would bypass the provider quarantine); socketPath via
|
|
1120
|
+
# controlSocketPath (SSOT, no correlation-authority drift); count = projection
|
|
1121
|
+
# length not peers.length; three distinct arrays (peers/socketOnly/diagnostics);
|
|
1122
|
+
# NO verb-routing field in JSON (deep key scan) NOR word in text (title leak);
|
|
1123
|
+
# diagnostics in both surfaces; empty → "(none)"; unsupported shown; enrich null
|
|
1124
|
+
# → "(not enriched)". WIRING guard: bridge calls listEntwurfFacts+renderEntwurfPeers,
|
|
1125
|
+
# getLiveSessions gone. Facts fabricated, no IO (only static source read).
|
|
1126
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-peers-surface.ts)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
check_entwurf_self_address() {
|
|
1131
|
+
# Deterministic gate for the self-addressability honesty predicate (SE-1/SE-2
|
|
1132
|
+
# slice 1). Guards the bug where the MCP bridge / pi-native claim replyable:true
|
|
1133
|
+
# from env presence alone: a socketless pi session, or a meta citizen whose owner
|
|
1134
|
+
# exited / whose idle-watch was never armed, all advertised replyable while
|
|
1135
|
+
# delivery silently failed (SE-1). Asserts: PURE truth table (pi replyable ⟺
|
|
1136
|
+
# socketAlive; meta ⟺ recordBacked ∧ ownerAlive ∧ watchArmed; external never),
|
|
1137
|
+
# incl. the two regression-proof rows (record-present + owner-dead / watch-unarmed)
|
|
1138
|
+
# that stay meaningful after slice 3 mints records; SOURCE GUARD that
|
|
1139
|
+
# buildStrictPiSenderEnvelope drops the hardcoded `replyable: true` and existsSync-
|
|
1140
|
+
# probes the socket, and entwurf_self renders alive vs expected (no path lie).
|
|
1141
|
+
# Slice boundary: meta watchArmed is wired from the slice-2 presence marker; do NOT
|
|
1142
|
+
# claim slice 1 green standalone (1+2 close in the same release block).
|
|
1143
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-self-address.ts)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
check_entwurf_deliverability() {
|
|
1147
|
+
# Deterministic gate for the conversational-mailbox deliverability predicate
|
|
1148
|
+
# (SE-1/SE-2 slice 2c). The predicate the enqueue sites must consult (slice 2d)
|
|
1149
|
+
# before writing a .msg. Asserts: computeMetaReceiverActive (active iff recordBacked
|
|
1150
|
+
# AND ownerAlive AND watchArmed, fail-closed, per-cause reasons); mailboxConversational-
|
|
1151
|
+
# Deliverable (deliverable iff wakeMode self-fetch AND active) — KEY rows: direct-inject
|
|
1152
|
+
# (pi) refused even when active (SE-1, no mailbox drain), self-fetch + dead-owner/unarmed
|
|
1153
|
+
# refused (SE-2, would rot); WIRING that the self-addressability predicate shares the
|
|
1154
|
+
# SAME active-receiver atom (one source of truth). Pure, no IO.
|
|
1155
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-deliverability.ts)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
check_entwurf_mailbox_guard() {
|
|
1159
|
+
# Deterministic gate for the guarded mailbox enqueue (SE-1/SE-2 slice 2d) — the IO
|
|
1160
|
+
# orchestration conversational-reply sites use instead of enqueueMetaMessage directly.
|
|
1161
|
+
# Asserts (GPT Q5, both axes): PURE 0-call — an undeliverable target (dead receiver /
|
|
1162
|
+
# direct-inject pi / absent record) leaves the injected enqueue UNCALLED, a deliverable
|
|
1163
|
+
# one calls it exactly once; TMPDIR SNAPSHOT with the real enqueueMetaMessage — a refused
|
|
1164
|
+
# send leaves the mailbox tree byte-identical (file list + content hash, not just mtime),
|
|
1165
|
+
# an accepted send writes exactly one .msg; plus fact gathering from record/capability/marker.
|
|
1166
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-mailbox-guard.ts)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
check_package_source_routing() {
|
|
1172
|
+
# Deterministic gate for #29 (package-installed Entwurf ACP routing). Pins
|
|
1173
|
+
# resolveExplicitExtensionSpec()'s package-source -> install-root mapping and
|
|
1174
|
+
# the fail-fast routing contract through the two public routing surfaces
|
|
1175
|
+
# (getRegistryRouting spawn path, getEntwurfExplicitExtensions resume path).
|
|
1176
|
+
# Covers the install matrix: local path / git user / npm user (+version) /
|
|
1177
|
+
# install-missing / project-scope-unseen / no-source, across local + remote,
|
|
1178
|
+
# plus self-root fallback and the resume unresolvedAcpIntent signal. Isolated
|
|
1179
|
+
# via a temp PI_CODING_AGENT_DIR — the real ~/.pi/agent is never touched. No
|
|
1180
|
+
# backend, no spawn, no API cost.
|
|
1181
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-package-source-routing.ts)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
smoke_session_id_name() {
|
|
1186
|
+
# LIVE 3-turn substrate smoke (Phase 3a) for Pi 0.78 --session-id/--name,
|
|
1187
|
+
# exercised through the bridge but NOT through the Entwurf tool surface, so it
|
|
1188
|
+
# lands independently of the taskId->sessionId migration. Spawns real cheap
|
|
1189
|
+
# sonnet turns (auth + tokens) and asserts: header id/cwd, session_info name as
|
|
1190
|
+
# info layer, append-not-recreate, spawn-only name, and the wrong-cwd footgun
|
|
1191
|
+
# as documented evidence. Isolated via a temp PI_CODING_AGENT_DIR.
|
|
1192
|
+
section "smoke: --session-id / --name substrate (direct pi, no Entwurf API)"
|
|
1193
|
+
if ! command -v pi >/dev/null 2>&1; then
|
|
1194
|
+
fail "[smoke-session-id-name] pi binary not on PATH — cannot run live substrate proof"
|
|
1195
|
+
return 1
|
|
1196
|
+
fi
|
|
1197
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-session-id-name.ts) || {
|
|
1198
|
+
fail "[smoke-session-id-name] live substrate smoke failed"
|
|
1199
|
+
return 1
|
|
1200
|
+
}
|
|
1201
|
+
ok "[smoke-session-id-name] --session-id/--name substrate proven (append + spawn-only name + wrong-cwd footgun)"
|
|
1202
|
+
return 0
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
check_dep_versions() {
|
|
1208
|
+
# Catches version-pin drift across package.json, run.sh, and README.md.
|
|
1209
|
+
# Concretely the kind of skew that produced commit 21de0f9's "0.11.1
|
|
1210
|
+
# leftover" review comment: package.json bumped to 0.12.0 while README
|
|
1211
|
+
# and run.sh's setup gate still claimed 0.11.1. Static check, no
|
|
1212
|
+
# subprocess — fast enough to run inside `pnpm check` and pre-commit.
|
|
1213
|
+
(cd "$REPO_DIR" && node --input-type=module <<'EOF'
|
|
1214
|
+
import { strict as assert } from 'node:assert';
|
|
1215
|
+
import { readFileSync } from 'node:fs';
|
|
1216
|
+
|
|
1217
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
1218
|
+
const geminiBundled = pkg.dependencies['@google/gemini-cli'] ?? pkg.optionalDependencies?.['@google/gemini-cli'];
|
|
1219
|
+
assert.equal(geminiBundled, undefined, 'package.json must not bundle @google/gemini-cli — gemini is an external PATH runtime');
|
|
1220
|
+
|
|
1221
|
+
// `^` + `m` flag anchors to the start-of-line shell assignment so we don't
|
|
1222
|
+
// accidentally pick up the regex literal inside this very check function's
|
|
1223
|
+
// heredoc (which is indented, so won't match `^...`).
|
|
1224
|
+
const runSh = readFileSync('run.sh', 'utf8');
|
|
1225
|
+
|
|
1226
|
+
// pi peer/dev alignment (#26 / 0.8.0 dep-alignment gate). The three
|
|
1227
|
+
// @earendil-works/pi-* devDeps must pin one identical version, and that
|
|
1228
|
+
// version must match the check-pack-install peer-install pins below
|
|
1229
|
+
// (`pnpm add @earendil-works/pi-ai@X ...`). Without this, a pi bump could
|
|
1230
|
+
// drift package.json devDeps away from the fresh-temp install smoke and
|
|
1231
|
+
// the "dependency alignment gate" would not actually verify pi.
|
|
1232
|
+
const piAi = pkg.devDependencies?.['@earendil-works/pi-ai'];
|
|
1233
|
+
const piCoding = pkg.devDependencies?.['@earendil-works/pi-coding-agent'];
|
|
1234
|
+
const piTui = pkg.devDependencies?.['@earendil-works/pi-tui'];
|
|
1235
|
+
assert.ok(piAi, 'package.json devDependencies must pin @earendil-works/pi-ai');
|
|
1236
|
+
assert.equal(piCoding, piAi,
|
|
1237
|
+
`@earendil-works/pi-coding-agent (${piCoding}) must match @earendil-works/pi-ai (${piAi})`);
|
|
1238
|
+
assert.equal(piTui, piAi,
|
|
1239
|
+
`@earendil-works/pi-tui (${piTui}) must match @earendil-works/pi-ai (${piAi})`);
|
|
1240
|
+
|
|
1241
|
+
// check-pack-install peer-install pins (quoted `@earendil-works/pi-*@<ver>`
|
|
1242
|
+
// args; the `\d`-anchored version avoids matching this regex literal itself).
|
|
1243
|
+
const peerAi = runSh.match(/"@earendil-works\/pi-ai@(\d[\d.]*)"/)?.[1];
|
|
1244
|
+
const peerCoding = runSh.match(/"@earendil-works\/pi-coding-agent@(\d[\d.]*)"/)?.[1];
|
|
1245
|
+
const peerTui = runSh.match(/"@earendil-works\/pi-tui@(\d[\d.]*)"/)?.[1];
|
|
1246
|
+
assert.equal(peerAi, piAi,
|
|
1247
|
+
`run.sh check-pack-install pi-ai peer pin (${peerAi}) must match package.json devDep (${piAi})`);
|
|
1248
|
+
assert.equal(peerCoding, piAi,
|
|
1249
|
+
`run.sh check-pack-install pi-coding-agent peer pin (${peerCoding}) must match (${piAi})`);
|
|
1250
|
+
assert.equal(peerTui, piAi,
|
|
1251
|
+
`run.sh check-pack-install pi-tui peer pin (${peerTui}) must match (${piAi})`);
|
|
1252
|
+
|
|
1253
|
+
// peerDependencies must be a CLOSED range (0.11 Stage 0, drift-proofing): the
|
|
1254
|
+
// floor tracks the devDep pin so a consumer can't install against a pi lacking
|
|
1255
|
+
// the 0.80 public trust exports the bridge imports, AND an upper bound at the
|
|
1256
|
+
// next minor stops a fresh install from silently pulling a future pi (0.81+)
|
|
1257
|
+
// whose internal export surface has drifted from the one we typecheck against.
|
|
1258
|
+
// pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
|
|
1259
|
+
// factory churn is exactly this), so an open `>=` floor is exactly how the next
|
|
1260
|
+
// installer re-acquires the drift. Expected
|
|
1261
|
+
// shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.80.2 <0.81`).
|
|
1262
|
+
const [piMaj, piMin] = piAi.split('.').map(Number);
|
|
1263
|
+
assert.equal(piMaj, 0,
|
|
1264
|
+
`pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
|
|
1265
|
+
const expectedPeer = `>=${piAi} <0.${piMin + 1}`;
|
|
1266
|
+
const peerDepAi = pkg.peerDependencies?.['@earendil-works/pi-ai'];
|
|
1267
|
+
const peerDepCoding = pkg.peerDependencies?.['@earendil-works/pi-coding-agent'];
|
|
1268
|
+
const peerDepTui = pkg.peerDependencies?.['@earendil-works/pi-tui'];
|
|
1269
|
+
assert.equal(peerDepAi, expectedPeer,
|
|
1270
|
+
`package.json peerDependencies @earendil-works/pi-ai (${peerDepAi}) must be "${expectedPeer}" (devDep floor + next-minor ceiling)`);
|
|
1271
|
+
assert.equal(peerDepCoding, expectedPeer,
|
|
1272
|
+
`package.json peerDependencies @earendil-works/pi-coding-agent (${peerDepCoding}) must be "${expectedPeer}"`);
|
|
1273
|
+
assert.equal(peerDepTui, expectedPeer,
|
|
1274
|
+
`package.json peerDependencies @earendil-works/pi-tui (${peerDepTui}) must be "${expectedPeer}"`);
|
|
1275
|
+
|
|
1276
|
+
console.log('[check-dep-versions] 11 assertions ok');
|
|
1277
|
+
EOF
|
|
1278
|
+
)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
check_pi_import_surface() {
|
|
1282
|
+
# 0.11 Stage 0 (동결결정 9): the bridge may reference @earendil-works/pi-*
|
|
1283
|
+
# ONLY by the package root. ANY subpath (`/dist`, `/core`, `/src`, `/foo`, …)
|
|
1284
|
+
# reaches pi's private surface and silently breaks on pi internal reshuffles.
|
|
1285
|
+
# The check is intentionally SPECIFIER-shaped, not import-keyword-shaped: it
|
|
1286
|
+
# matches a quoted/backtick module specifier `@earendil-works/pi-*/…`, so one
|
|
1287
|
+
# pattern catches static `from`, dynamic `import()`, `require()`,
|
|
1288
|
+
# `export … from`, side-effect `import "…"`, and whitespace variants alike.
|
|
1289
|
+
# Root import `@earendil-works/pi-coding-agent` (no trailing slash) is allowed.
|
|
1290
|
+
# Scans EVERY tracked .ts/.js/.mjs/.cjs source (git ls-files), not a hardcoded
|
|
1291
|
+
# file list — a new root file (acp-bridge.ts, event-mapper.ts, engraving.ts,
|
|
1292
|
+
# pi-context-augment.ts, protocol.js, …) can never silently escape the gate.
|
|
1293
|
+
#
|
|
1294
|
+
# pi 0.80 EXCEPTION — exactly ONE allowlisted subpath:
|
|
1295
|
+
# @earendil-works/pi-ai/compat
|
|
1296
|
+
# 0.80 moved the standalone root `getModels()` to the deprecated `/compat`
|
|
1297
|
+
# entrypoint. This repo's pi-extensions/** are loaded by pi's EXTENSION loader
|
|
1298
|
+
# (pi-coding-agent `core/extensions/loader.ts`), whose jiti alias map resolves
|
|
1299
|
+
# ONLY three pi-ai specifiers for extensions — the bare root, `/compat`, and
|
|
1300
|
+
# `/oauth` — all to `ai/dist/compat.js`. A `providers/*` subpath is NOT in that
|
|
1301
|
+
# map: jiti prefix-matches the bare `@earendil-works/pi-ai` alias and appends
|
|
1302
|
+
# the remainder, producing the unresolvable `…/dist/compat.js/providers/
|
|
1303
|
+
# anthropic` (verified live: extension load crash — invisible to static
|
|
1304
|
+
# typecheck, which resolves against node_modules `exports`). So `/compat` is the
|
|
1305
|
+
# sanctioned extension entrypoint for the old global model-catalog API
|
|
1306
|
+
# (lib/acp/models.ts: `getModels`), and the ONLY allowlisted exception. The
|
|
1307
|
+
# allow-pattern is closing-quote-anchored (`@earendil-works/pi-ai/compat["'\`]`)
|
|
1308
|
+
# so it permits ONLY that exact specifier: `/compat-foo`, `/oauth`, every
|
|
1309
|
+
# `/providers/*`, and any deeper path stay FORBIDDEN (we use only `/compat`).
|
|
1310
|
+
# Do NOT widen this to a `providers/*` subpath — it typechecks but CANNOT
|
|
1311
|
+
# resolve under the extension loader.
|
|
1312
|
+
local hits
|
|
1313
|
+
hits=$(cd "$REPO_DIR" && git ls-files '*.ts' '*.js' '*.mjs' '*.cjs' \
|
|
1314
|
+
| grep -vE '^(node_modules|dist)/' \
|
|
1315
|
+
| xargs -r grep -HnE "[\"'\`]@earendil-works/pi-(ai|coding-agent|tui)/" 2>/dev/null \
|
|
1316
|
+
| grep -vE "[\"'\`]@earendil-works/pi-ai/compat[\"'\`]" 2>/dev/null || true)
|
|
1317
|
+
if [ -n "$hits" ]; then
|
|
1318
|
+
echo "[check-pi-import-surface] FAIL: pi private subpath reference(s) — import @earendil-works/pi-* by the package ROOT only:"
|
|
1319
|
+
echo "$hits"
|
|
1320
|
+
exit 1
|
|
1321
|
+
fi
|
|
1322
|
+
ok "[check-pi-import-surface] pi references are root-only (no private subpath; all tracked ts/js scanned)"
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
check_env_namespace() {
|
|
1326
|
+
# 0.11 S3 cutover lock: after the env-namespace rename, NO tracked source may
|
|
1327
|
+
# carry the old pi-centric env/const prefixes (PI_SHELL_ACP*, PI_META*,
|
|
1328
|
+
# PI_TOOLS_BRIDGE*, PI_ENTWURF*). This deterministic guard keeps the cutover
|
|
1329
|
+
# from silently regressing — a single old prefix slipping back in fails loud.
|
|
1330
|
+
# KEEP pi-adapter env (PI_SESSION_ID, PI_AGENT_ID, PI_CODING_AGENT_DIR,
|
|
1331
|
+
# PI_SETTINGS_PATH, PI_EMACS_AGENT_SOCKET) is NOT in the forbidden set, so it
|
|
1332
|
+
# passes untouched. The forbidden pattern uses a [_] char-class for the
|
|
1333
|
+
# trailing underscore so THIS gate's own definition never self-matches; for
|
|
1334
|
+
# the same reason every prose mention above uses a `*`, not a trailing `_`.
|
|
1335
|
+
# Docs/CHANGELOG/NEXT keep historical mentions and are excluded.
|
|
1336
|
+
local hits
|
|
1337
|
+
hits=$(cd "$REPO_DIR" && git ls-files \
|
|
1338
|
+
| grep -vE '\.(md|org)$|(^|/)NEXT|(^|/)CHANGELOG|^docs/' \
|
|
1339
|
+
| xargs -r grep -HnE 'PI_SHELL_ACP[_]|PI_META[_]|PI_TOOLS_BRIDGE[_]|PI_ENTWURF[_]' 2>/dev/null || true)
|
|
1340
|
+
if [ -n "$hits" ]; then
|
|
1341
|
+
echo "[check-env-namespace] FAIL: old pi env/const prefix survived the S3 cutover — rename to ENTWURF_*/ENTWURF_ACP_*/ENTWURF_META_*/ENTWURF_BRIDGE_*:"
|
|
1342
|
+
echo "$hits"
|
|
1343
|
+
exit 1
|
|
1344
|
+
fi
|
|
1345
|
+
ok "[check-env-namespace] env namespace is entwurf-only (no old pi env/const prefix in tracked source)"
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
check_pi_runtime_version() {
|
|
1349
|
+
# 0.11 Stage 0 (동결결정 9, runtime half): tsc catches a missing 0.80 export
|
|
1350
|
+
# at dev time, but an installed environment can still resolve an older pi at
|
|
1351
|
+
# runtime where the named trust exports / 0.80 provider-factory surface do not
|
|
1352
|
+
# exist. Verify VERSION >= floor via a DYNAMIC import of the package root only —
|
|
1353
|
+
# never statically import a floor-only symbol here, or this guard would crash
|
|
1354
|
+
# before it can fail loud.
|
|
1355
|
+
(cd "$REPO_DIR" && node --input-type=module <<'EOF'
|
|
1356
|
+
const FLOOR = '0.80.2';
|
|
1357
|
+
const cmp = (a, b) => {
|
|
1358
|
+
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
|
|
1359
|
+
for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
|
|
1360
|
+
return 0;
|
|
1361
|
+
};
|
|
1362
|
+
let VERSION;
|
|
1363
|
+
try {
|
|
1364
|
+
({ VERSION } = await import('@earendil-works/pi-coding-agent'));
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
console.error(`[check-pi-runtime-version] FAIL: cannot import @earendil-works/pi-coding-agent root — ${e?.message ?? e}`);
|
|
1367
|
+
process.exit(1);
|
|
1368
|
+
}
|
|
1369
|
+
if (typeof VERSION !== 'string') {
|
|
1370
|
+
console.error('[check-pi-runtime-version] FAIL: pi root export VERSION is not a string');
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
if (cmp(VERSION, FLOOR) < 0) {
|
|
1374
|
+
console.error(`[check-pi-runtime-version] FAIL: pi VERSION ${VERSION} < ${FLOOR} — the bridge is built and tested against the 0.80.2 public/runtime surface (trust exports hasTrustRequiringProjectResources + ProjectTrustStore nearest-ancestor get, the 0.80 model-catalog API getModels reached via the deprecated /compat entrypoint — 0.80 moved the standalone root getModels there, and the extension loader resolves only /compat, NOT the providers/* factory subpath — provider registration surface, compaction semantics) that older pi lacks or behaves differently on. Bump @earendil-works/pi-*.`);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
console.log(`[check-pi-runtime-version] ok — pi VERSION ${VERSION} >= ${FLOOR}`);
|
|
1378
|
+
EOF
|
|
1379
|
+
)
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
check_install_preflight() {
|
|
1383
|
+
# 0.12 relocation guard (2026-06-23): `install` MUST fail loud on a repo whose
|
|
1384
|
+
# node_modules is missing (fresh clone, no pnpm install) or path-stale (a dir
|
|
1385
|
+
# move/rename broke the pnpm symlink store) — and it must fail BEFORE writing
|
|
1386
|
+
# settings.json, so the breakage is not a silent-red that only surfaces minutes
|
|
1387
|
+
# later as a dead MCP bridge (entwurf-bridge ✘ Failed to connect) + a
|
|
1388
|
+
# check-entwurf-v2-surface ERR_MODULE_NOT_FOUND. The preflight follows each
|
|
1389
|
+
# dep's symlink to its real package.json (immune to per-package "exports" maps
|
|
1390
|
+
# that forbid a bare-root or ./package.json import), which a bare
|
|
1391
|
+
# `test -d node_modules` cannot do for a dir-move. REPO_DIR is the dir holding
|
|
1392
|
+
# run.sh (line 16), so the negative cases copy run.sh into a temp dir to point
|
|
1393
|
+
# REPO_DIR at a deliberately-broken tree.
|
|
1394
|
+
local rc out proj fake dep
|
|
1395
|
+
|
|
1396
|
+
# positive — the live repo passes the REAL preflight. Calling it directly keeps
|
|
1397
|
+
# preflight_dep_integrity the single source of truth for the probe set (no
|
|
1398
|
+
# second hardcoded list to drift). Subshell so its exit-on-fail can't kill us.
|
|
1399
|
+
if ! ( preflight_dep_integrity ) >/dev/null 2>&1; then
|
|
1400
|
+
fail "[check-install-preflight] live repo fails preflight_dep_integrity — run 'pnpm install' (gate cannot validate against a broken repo)"
|
|
1401
|
+
exit 1
|
|
1402
|
+
fi
|
|
1403
|
+
ok "[check-install-preflight] live repo passes preflight (all runtime hard deps resolve)"
|
|
1404
|
+
|
|
1405
|
+
# negative 1 — missing node_modules (fresh clone)
|
|
1406
|
+
fake=$(mktemp -d); proj=$(mktemp -d)
|
|
1407
|
+
cp "$REPO_DIR/run.sh" "$fake/run.sh"
|
|
1408
|
+
rc=0; out=$("$fake/run.sh" install "$proj" 2>&1) || rc=$?
|
|
1409
|
+
if [ "$rc" -eq 0 ]; then
|
|
1410
|
+
fail "[check-install-preflight] missing node_modules did NOT fail install"; rm -rf "$fake" "$proj"; exit 1
|
|
1411
|
+
fi
|
|
1412
|
+
if [ -f "$proj/.pi/settings.json" ]; then
|
|
1413
|
+
fail "[check-install-preflight] install wrote settings.json with missing deps (SILENT-RED)"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
|
|
1414
|
+
fi
|
|
1415
|
+
if ! printf '%s' "$out" | grep -q "repo dependency integrity check failed"; then
|
|
1416
|
+
fail "[check-install-preflight] missing node_modules failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
|
|
1417
|
+
fi
|
|
1418
|
+
rm -rf "$fake" "$proj"
|
|
1419
|
+
ok "[check-install-preflight] missing node_modules → fails before writing settings"
|
|
1420
|
+
|
|
1421
|
+
# negative 2 — representative dangling symlink (dir move): node_modules/ EXISTS
|
|
1422
|
+
# (a bare `test -d` would pass) with the other runtime deps symlinked live, but a
|
|
1423
|
+
# representative dep (@anthropic-ai/sdk) dangles. Asserts the dir-move blind spot
|
|
1424
|
+
# is closed AND that the failure names the broken dep. Uses a BUNDLED runtime dep
|
|
1425
|
+
# because the @earendil-works/pi-* peer trio is loader-provided and no longer part
|
|
1426
|
+
# of the install preflight probe set.
|
|
1427
|
+
fake=$(mktemp -d); proj=$(mktemp -d)
|
|
1428
|
+
cp "$REPO_DIR/run.sh" "$fake/run.sh"
|
|
1429
|
+
mkdir -p "$fake/node_modules/@modelcontextprotocol" "$fake/node_modules/@agentclientprotocol" "$fake/node_modules/@anthropic-ai"
|
|
1430
|
+
ln -s "$REPO_DIR/node_modules/@modelcontextprotocol/sdk" "$fake/node_modules/@modelcontextprotocol/sdk"
|
|
1431
|
+
ln -s "$REPO_DIR/node_modules/@agentclientprotocol/sdk" "$fake/node_modules/@agentclientprotocol/sdk"
|
|
1432
|
+
ln -s "$REPO_DIR/node_modules/@agentclientprotocol/claude-agent-acp" "$fake/node_modules/@agentclientprotocol/claude-agent-acp"
|
|
1433
|
+
ln -s "$REPO_DIR/node_modules/zod" "$fake/node_modules/zod"
|
|
1434
|
+
ln -s /nonexistent/pnpm-store/anthropic-sdk "$fake/node_modules/@anthropic-ai/sdk"
|
|
1435
|
+
rc=0; out=$("$fake/run.sh" install "$proj" 2>&1) || rc=$?
|
|
1436
|
+
if [ "$rc" -eq 0 ]; then
|
|
1437
|
+
fail "[check-install-preflight] dangling dep symlink did NOT fail install (test -d blind spot)"; rm -rf "$fake" "$proj"; exit 1
|
|
1438
|
+
fi
|
|
1439
|
+
if [ -f "$proj/.pi/settings.json" ]; then
|
|
1440
|
+
fail "[check-install-preflight] install wrote settings.json with a dangling dep (SILENT-RED)"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
|
|
1441
|
+
fi
|
|
1442
|
+
if ! printf '%s' "$out" | grep -q "repo dependency integrity check failed"; then
|
|
1443
|
+
fail "[check-install-preflight] dangling symlink failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
|
|
1444
|
+
fi
|
|
1445
|
+
if ! printf '%s' "$out" | grep -q "@anthropic-ai/sdk"; then
|
|
1446
|
+
fail "[check-install-preflight] dangling case did not name the broken dep (@anthropic-ai/sdk):"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
|
|
1447
|
+
fi
|
|
1448
|
+
rm -rf "$fake" "$proj"
|
|
1449
|
+
ok "[check-install-preflight] representative dangling symlink (test -d blind spot) → fails before writing settings, names the dep"
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
check_pi_preflight() {
|
|
1453
|
+
# 0.11 Stage 0 (2): the controlled-launch trust decision. Proves frozen
|
|
1454
|
+
# decision 8 precedence (saved false > saved true > prefix > no-inputs >
|
|
1455
|
+
# fail-fast) and decision 7's separator-boundary prefix against pi's own
|
|
1456
|
+
# ProjectTrustStore in a temp agentDir. Deterministic, no network/backend.
|
|
1457
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-pi-preflight.ts)
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
check_auth_boundary() {
|
|
1463
|
+
# Auth-boundary guard (re-introduced for the ACP plugin on v2, retargeted off
|
|
1464
|
+
# the deleted 0.11.0 index.ts/acp-bridge.ts onto the new provider entry). This
|
|
1465
|
+
# is the code-level pair of AGENTS §Operating boundaries (trust invariants):
|
|
1466
|
+
# entwurf is a no-auth ACP plugin at the pi provider layer — it does NOT
|
|
1467
|
+
# provide, resell, or bypass any backend credentials.
|
|
1468
|
+
#
|
|
1469
|
+
# pi.registerProvider requires an apiKey when defining custom models, but the
|
|
1470
|
+
# plugin consumes none: backend auth belongs to the operator's own Claude CLI
|
|
1471
|
+
# child process. The registration MUST therefore use the lowercase+hyphen
|
|
1472
|
+
# no-auth sentinel, NOT a bare ALL-CAPS legacy-ENV reference (e.g.
|
|
1473
|
+
# "ANTHROPIC_API_KEY") which trips pi's legacy-env deprecation AND falsely
|
|
1474
|
+
# presents the plugin as API-key dependent.
|
|
1475
|
+
#
|
|
1476
|
+
# Scope: the new provider entry + its lib/acp/* modules. The regex matches only
|
|
1477
|
+
# an `apiKey:` field assigned a quoted ALL-CAPS env name, so explanatory
|
|
1478
|
+
# comments and the no-auth sentinel identifier pass.
|
|
1479
|
+
section "auth boundary (ACP plugin no-auth sentinel)"
|
|
1480
|
+
(cd "$REPO_DIR" && node --input-type=module <<'EOF'
|
|
1481
|
+
import { strict as assert } from 'node:assert';
|
|
1482
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
1483
|
+
const files = ['pi-extensions/acp-provider.ts'];
|
|
1484
|
+
for (const f of readdirSync('pi-extensions/lib/acp')) {
|
|
1485
|
+
if (f.endsWith('.ts')) files.push(`pi-extensions/lib/acp/${f}`);
|
|
1486
|
+
}
|
|
1487
|
+
const offenders = [];
|
|
1488
|
+
let sentinelSeen = false;
|
|
1489
|
+
for (const f of files) {
|
|
1490
|
+
const src = readFileSync(f, 'utf8');
|
|
1491
|
+
const re = /apiKey:\s*"([A-Z][A-Z0-9_]*)"/g;
|
|
1492
|
+
let m;
|
|
1493
|
+
while ((m = re.exec(src)) !== null) offenders.push(`${f}: apiKey: "${m[1]}"`);
|
|
1494
|
+
if (src.includes('entwurf-no-auth')) sentinelSeen = true;
|
|
1495
|
+
}
|
|
1496
|
+
assert.equal(offenders.length, 0,
|
|
1497
|
+
`ACP provider apiKey must be a no-auth sentinel, not a legacy-ENV reference. Offenders:\n ${offenders.join('\n ')}`);
|
|
1498
|
+
assert.ok(sentinelSeen,
|
|
1499
|
+
'no-auth sentinel literal "entwurf-no-auth" not found in the ACP provider surface — auth boundary unverified');
|
|
1500
|
+
console.log(`[check-auth-boundary] ok — no legacy-ENV apiKey literal across ${files.length} ACP provider file(s); no-auth sentinel present`);
|
|
1501
|
+
EOF
|
|
1502
|
+
)
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
check_acp_provider_surface() {
|
|
1506
|
+
# Deterministic gate for the S0 ACP provider loader/fence slice. Loads the REAL
|
|
1507
|
+
# provider lib modules and asserts the registration surface: one surface name,
|
|
1508
|
+
# curated Claude anchor present with full ProviderModelConfig rows, no-auth
|
|
1509
|
+
# sentinel shape, and a FAIL-LOUD streamSimple (calling it throws — no native
|
|
1510
|
+
# fallback, no empty-but-successful stream). Pure, no pi runtime, no API.
|
|
1511
|
+
section "ACP provider surface (S0 loader/fence)"
|
|
1512
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-provider-surface.ts)
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
check_acp_sdk_surface() {
|
|
1516
|
+
# Deterministic gate for the S2a ACP SDK dependency surface. Pins the three
|
|
1517
|
+
# ACP runtime deps to the 0.11.0 oracle versions (@agentclientprotocol/sdk
|
|
1518
|
+
# 0.22.1 + claude-agent-acp 0.39.0 + @anthropic-ai/sdk 0.100.1), locks the
|
|
1519
|
+
# peer-resolution that keeps claude-agent-sdk satisfiable (0.100.1, not the
|
|
1520
|
+
# stale 0.91.1), asserts the wire SDK still value-exports the symbols the raw
|
|
1521
|
+
# turn needs (silent-rename gate), and forbids any source-level anthropic SDK
|
|
1522
|
+
# import / API-client use (the anthropic dep is a peer-pin ONLY).
|
|
1523
|
+
section "ACP SDK surface (S2a dep pin + peer-resolution + no-client-use)"
|
|
1524
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-sdk-surface.ts)
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
check_acp_overlay() {
|
|
1528
|
+
# Deterministic gate for the S2b Claude config overlay materializer. Drives
|
|
1529
|
+
# ensureClaudeConfigOverlay against injected temp realDir/overlayDir (no
|
|
1530
|
+
# operator ~/.claude touched) and asserts: settings.json hooks:{} +
|
|
1531
|
+
# defaultMode default + autoMemory off; whitelisted entries symlinked;
|
|
1532
|
+
# projects/sessions overlay-private real dirs (NOT symlinks); operator
|
|
1533
|
+
# personal config (CLAUDE.md/settings.local.json/plugins/agents) never leaks;
|
|
1534
|
+
# stale symlinks cleaned; binary-owned files preserved; CLAUDE_CONFIG_DIR
|
|
1535
|
+
# launch-env planted; idempotent. Pure, no live model.
|
|
1536
|
+
section "ACP overlay (S2b claude-config-overlay)"
|
|
1537
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-overlay.ts)
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
check_acp_tool_surface() {
|
|
1541
|
+
# Deterministic gate for the S2b Claude tool surface + exclude-tools
|
|
1542
|
+
# truthfulness preflight. Matrix over assertExcludeToolsHonored (claude
|
|
1543
|
+
# narrows via tools / native always-exposes / extension-tool exclusion is
|
|
1544
|
+
# honest) + buildClaudeSessionMeta shape lock (tools/allow/disallowed/
|
|
1545
|
+
# extraArgs/plugins) + the S2b billing-carrier guard (no _meta.systemPrompt
|
|
1546
|
+
# unless a caller supplies one). Pure preflight — NOT a backend wire read.
|
|
1547
|
+
section "ACP tool surface (S2b exclude-tools preflight + session meta)"
|
|
1548
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-tool-surface.ts)
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
check_acp_event_mapper() {
|
|
1552
|
+
# Deterministic gate for the S2c ACP→pi event mapper + context conversion.
|
|
1553
|
+
# Feeds synthetic ACP session_notification updates through the mapper and
|
|
1554
|
+
# asserts the pi AssistantMessageEvent sequence, including the hard boundary:
|
|
1555
|
+
# tool_call / tool_call_update render as TEXT NOTICES, never structured
|
|
1556
|
+
# toolcall_* (the ACP child already executed the tool). Also locks the
|
|
1557
|
+
# context→ACP-prompt transcript passthrough (excludes systemPrompt/thinking,
|
|
1558
|
+
# single text block). Pure, no live backend.
|
|
1559
|
+
section "ACP event mapper (S2c notification→stream + context)"
|
|
1560
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-event-mapper.ts)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
check_acp_prompt_builder() {
|
|
1564
|
+
# Deterministic gate for the S2d bootstrapPath-scoped ACP prompt builder (핀4).
|
|
1565
|
+
# Proves prompt SCOPE follows bootstrapPath: new=full transcript (history
|
|
1566
|
+
# carrier), reuse/resume/load=latest user delta (first user after last
|
|
1567
|
+
# assistant, SessionStart hook skipped, image marker kept, prior history
|
|
1568
|
+
# excluded so a reuse session is not re-injected its own history). Pure, no
|
|
1569
|
+
# session store yet — locks the builder before S2d wires the reuse paths.
|
|
1570
|
+
section "ACP prompt builder (S2d bootstrapPath prompt scope)"
|
|
1571
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-prompt-builder.ts)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
check_acp_config() {
|
|
1575
|
+
# Deterministic gate for the S2g operator provider-config loader. Locks:
|
|
1576
|
+
# global+project merge (project overrides defined keys only; mcpServers merge
|
|
1577
|
+
# per-name with project win), defaults (strict-mcp-config on, [] sources,
|
|
1578
|
+
# baseline tools), fail-loud on invalid mcpServers/skillPlugins/
|
|
1579
|
+
# appendSystemPrompt:true/strictMcpConfig:false, nonempty skillPlugins auto-add
|
|
1580
|
+
# Skill+Skill(*), deterministic sorted mcp hash sensitive to command/env/url/
|
|
1581
|
+
# headers, and envelope enrich (PI_SESSION_ID/PI_AGENT_ID into entwurf-bridge
|
|
1582
|
+
# only, stale filtered, post-hash). Pure + temp-dir settings I/O, no child/spawn.
|
|
1583
|
+
section "ACP provider config (S2g operator mcpServers/skillPlugins/tools passthrough)"
|
|
1584
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-config.ts)
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
check_acp_session_store() {
|
|
1588
|
+
# Deterministic gate for the S2d-1b-1 session store / signature / bootstrap
|
|
1589
|
+
# decision. Locks: model-lock fail-loud throw in the pure decision, prefix-
|
|
1590
|
+
# compat (only a prefix history reuses; edited/compaction → new), carrier
|
|
1591
|
+
# drift → signature change → incompatible, and bootstrapPath ⟂ lifecyclePolicy
|
|
1592
|
+
# (turn-scoped/-p one-shot is ALWAYS new — no in-memory reuse, no persisted
|
|
1593
|
+
# resume/load in the first cut). Pure + temp-dir record I/O, no child/spawn.
|
|
1594
|
+
section "ACP session store (S2d-1b-1 signature/compat/bootstrap decision)"
|
|
1595
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-store.ts)
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
check_acp_backend_preflight() {
|
|
1599
|
+
# Deterministic gate for the S2c runtime tool-surface preflight. Calls
|
|
1600
|
+
# streamShellAcp with a context whose declared tools exclude a built-in the
|
|
1601
|
+
# Claude child still exposes (read) and asserts the turn fails fast into the
|
|
1602
|
+
# returned stream as an error event BEFORE any spawn — proving
|
|
1603
|
+
# assertExcludeToolsHonored is wired into the live provider path, not just the
|
|
1604
|
+
# pure gate. No backend launched (preflight throws first). Pure.
|
|
1605
|
+
section "ACP backend preflight (S2c runtime exclude-tools wiring)"
|
|
1606
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-backend-preflight.ts)
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
check_acp_session_reuse() {
|
|
1610
|
+
# Deterministic gate for S2d-1b-2b in-memory session reuse (backend.ts). Injects
|
|
1611
|
+
# a fake spawn/connection seam and CAPTURES each turn's prompt payload to prove
|
|
1612
|
+
# reuse is DELTA-ONLY: turn 2 carries the new nonce, never the turn-1 history,
|
|
1613
|
+
# with no second spawn/newSession. Also proves the mutable activePromptHandler
|
|
1614
|
+
# routes each turn's notices to its own stream, a persisted record is NOT
|
|
1615
|
+
# resumed in 1b-2b, a concurrent prompt fails loud (busy), the reused child is
|
|
1616
|
+
# never torn down between turns, and source-locks buildAcpPrompt wiring +
|
|
1617
|
+
# single-site applyAcpSessionUpdate via the router. No real child launched.
|
|
1618
|
+
section "ACP session reuse (S2d-1b-2b delta-only capture + mutable routing)"
|
|
1619
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-reuse.ts)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
check_acp_carrier_augment() {
|
|
1623
|
+
# Deterministic gate for S2d-1c billing carrier (engraving) + first-user augment.
|
|
1624
|
+
# Separate axis from the reuse gate (GPT c32a6c8): locks that the carrier is
|
|
1625
|
+
# SHORT/empty-by-default/pure and folds into bridgeConfigSignature (so a carrier
|
|
1626
|
+
# change invalidates reuse but a stable carrier never rebuilds), and that the
|
|
1627
|
+
# rich augment rides the `new` prompt on the WIRE only — never the pi Context,
|
|
1628
|
+
# so it never enters contextMessageSignatures — with entwurf cwd/AGENTS.md
|
|
1629
|
+
# de-dup. Pure + temp-dir fs, no spawn.
|
|
1630
|
+
section "ACP carrier + augment (S2d-1c engraving + first-user augment)"
|
|
1631
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-carrier-augment.ts)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
check_pack() {
|
|
1635
|
+
# Dry-run tarball invariant gate for the public npm surface.
|
|
1636
|
+
#
|
|
1637
|
+
# Runs `npm pack --dry-run --json`, then asserts:
|
|
1638
|
+
# - runtime-critical files and the public verification/docs
|
|
1639
|
+
# surface (run.sh, scripts/, curated docs/assets/*.gif,
|
|
1640
|
+
# demo/) are present;
|
|
1641
|
+
# - private/dev residue is absent (session dumps, debug logs,
|
|
1642
|
+
# dev configs, workspace metadata, the OpenClaw plugin
|
|
1643
|
+
# monorepo sibling that ships as its own npm package).
|
|
1644
|
+
#
|
|
1645
|
+
# Scope: this is the first of four checks in #13's publish gate.
|
|
1646
|
+
# The remaining three — actual `npm pack`, `tar -tf`, and local
|
|
1647
|
+
# install smoke from the packed tarball — are covered by
|
|
1648
|
+
# check_pack_install() below (commit 9e2a2ca, Phase 2.3 closeout).
|
|
1649
|
+
# Intent + policy live in NEXT.md Phase 2.3.
|
|
1650
|
+
section "pack invariants (dry-run)"
|
|
1651
|
+
|
|
1652
|
+
local json
|
|
1653
|
+
json=$(cd "$REPO_DIR" && npm pack --dry-run --json 2>/dev/null) || {
|
|
1654
|
+
fail "[check-pack] npm pack --dry-run failed"
|
|
1655
|
+
return 1
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
local file_list
|
|
1659
|
+
file_list=$(node -e '
|
|
1660
|
+
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
1661
|
+
if (!Array.isArray(data) || data.length !== 1) {
|
|
1662
|
+
console.error("[check-pack] expected single tarball entry, got " +
|
|
1663
|
+
(Array.isArray(data) ? data.length : "non-array"));
|
|
1664
|
+
process.exit(2);
|
|
1665
|
+
}
|
|
1666
|
+
for (const f of data[0].files) console.log(f.path);
|
|
1667
|
+
' <<<"$json") || {
|
|
1668
|
+
fail "[check-pack] failed to parse npm pack output"
|
|
1669
|
+
return 1
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
# .sh mode regression gate. The repo tracks 100755 in git, but if a
|
|
1673
|
+
# contributor's umask or a stray `git update-index --chmod=-x` drops
|
|
1674
|
+
# the bit the tarball will ship 0644 — and pi install hands the
|
|
1675
|
+
# tarball straight to `npm install`, so the bit needs to survive the
|
|
1676
|
+
# whole publish pipeline. Catch it here at dry-run time.
|
|
1677
|
+
local sh_mode_violations
|
|
1678
|
+
sh_mode_violations=$(node -e '
|
|
1679
|
+
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
1680
|
+
const bad = data[0].files
|
|
1681
|
+
.filter(f => f.path.endsWith(".sh"))
|
|
1682
|
+
.filter(f => (f.mode & 0o111) === 0);
|
|
1683
|
+
for (const f of bad) console.log(f.path + " mode=0" + (f.mode || 0).toString(8));
|
|
1684
|
+
' <<<"$json") || {
|
|
1685
|
+
fail "[check-pack] failed to inspect tarball modes"
|
|
1686
|
+
return 1
|
|
1687
|
+
}
|
|
1688
|
+
if [ -n "$sh_mode_violations" ]; then
|
|
1689
|
+
fail "[check-pack] .sh files missing executable bit in tarball:"
|
|
1690
|
+
echo "$sh_mode_violations" | sed 's/^/ /' >&2
|
|
1691
|
+
return 1
|
|
1692
|
+
fi
|
|
1693
|
+
|
|
1694
|
+
local required=(
|
|
1695
|
+
"package.json" "README.md" "LICENSE" "CHANGELOG.md"
|
|
1696
|
+
"protocol.js" "run.sh"
|
|
1697
|
+
"pi-extensions/acp-provider.ts"
|
|
1698
|
+
"pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/backend.ts"
|
|
1699
|
+
"pi-extensions/lib/acp/overlay.ts" "pi-extensions/lib/acp/tool-surface.ts"
|
|
1700
|
+
"pi-extensions/lib/acp/event-mapper.ts" "pi-extensions/lib/acp/context.ts"
|
|
1701
|
+
"pi-extensions/entwurf-control.ts"
|
|
1702
|
+
"pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
|
|
1703
|
+
"mcp/entwurf-bridge/src/index.ts"
|
|
1704
|
+
"scripts/postinstall-chmod.cjs"
|
|
1705
|
+
"pi/entwurf-capabilities.json"
|
|
1706
|
+
"pi/entwurf-targets.json"
|
|
1707
|
+
"pi/meta-bridge/.claude-plugin/marketplace.json"
|
|
1708
|
+
"pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json"
|
|
1709
|
+
"pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json"
|
|
1710
|
+
"pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh"
|
|
1711
|
+
"pi-extensions/meta-bridge-hook.ts"
|
|
1712
|
+
"pi-extensions/lib/meta-session.ts"
|
|
1713
|
+
"pi-extensions/lib/session-id.js"
|
|
1714
|
+
"scripts/meta-bridge-install.sh"
|
|
1715
|
+
"scripts/meta-bridge-state.py"
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
# Patterns that must NOT appear in the tarball. Anchored where the
|
|
1719
|
+
# match should be exact (e.g. ^bench\.sh$); loose where the residue
|
|
1720
|
+
# may appear under any path (e.g. \.log$).
|
|
1721
|
+
local forbidden_patterns=(
|
|
1722
|
+
'pi-session-.*\.html$'
|
|
1723
|
+
'\.log$'
|
|
1724
|
+
'\.cast$'
|
|
1725
|
+
'^bench\.sh$'
|
|
1726
|
+
'^biome\.json$'
|
|
1727
|
+
'^tsconfig\.json$'
|
|
1728
|
+
'^pnpm-(lock\.yaml|workspace\.yaml)$'
|
|
1729
|
+
'^NEXT\.md$'
|
|
1730
|
+
'^plugins/'
|
|
1731
|
+
'^node_modules/'
|
|
1732
|
+
'\.tmp-verify/'
|
|
1733
|
+
'\.agent-(reports|shell)/'
|
|
1734
|
+
'pi/meta-bridge/\.assembled/'
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
local pass=1 f pat hit
|
|
1738
|
+
|
|
1739
|
+
for f in "${required[@]}"; do
|
|
1740
|
+
if ! grep -qxF "$f" <<<"$file_list"; then
|
|
1741
|
+
fail "[check-pack] MISSING required: $f"
|
|
1742
|
+
pass=0
|
|
1743
|
+
fi
|
|
1744
|
+
done
|
|
1745
|
+
|
|
1746
|
+
for pat in "${forbidden_patterns[@]}"; do
|
|
1747
|
+
hit=$(grep -E "$pat" <<<"$file_list" || true)
|
|
1748
|
+
if [ -n "$hit" ]; then
|
|
1749
|
+
fail "[check-pack] FORBIDDEN matches pattern $pat:"
|
|
1750
|
+
echo "$hit" | sed 's/^/ /' >&2
|
|
1751
|
+
pass=0
|
|
1752
|
+
fi
|
|
1753
|
+
done
|
|
1754
|
+
|
|
1755
|
+
local total
|
|
1756
|
+
total=$(printf '%s\n' "$file_list" | wc -l | tr -d ' ')
|
|
1757
|
+
echo "[check-pack] $total files in tarball"
|
|
1758
|
+
|
|
1759
|
+
if [ "$pass" = "1" ]; then
|
|
1760
|
+
ok "[check-pack] invariants pass"
|
|
1761
|
+
return 0
|
|
1762
|
+
fi
|
|
1763
|
+
fail "[check-pack] invariants violated"
|
|
1764
|
+
return 1
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
check_pack_install() {
|
|
1768
|
+
# Heavy publish gate. Runs the remaining three checks in #13's
|
|
1769
|
+
# publish checklist that check_pack (dry-run only) does not cover:
|
|
1770
|
+
#
|
|
1771
|
+
# 2. actual `npm pack` — produces the real tarball
|
|
1772
|
+
# 3. `tar -tf` — cross-checks contents against dry-run invariants
|
|
1773
|
+
# 4. fresh-temp project local install smoke — pnpm add the tarball
|
|
1774
|
+
# with required peers, then import('@junghanacs/entwurf/package.json')
|
|
1775
|
+
# to confirm the installed shape resolves end-to-end.
|
|
1776
|
+
#
|
|
1777
|
+
# Excluded from the default `pnpm check` because the install smoke
|
|
1778
|
+
# spends 5-15s on dependency resolution. Wired into prepublishOnly
|
|
1779
|
+
# so `npm publish` cannot succeed if the actual install path is
|
|
1780
|
+
# broken even when dry-run invariants look fine.
|
|
1781
|
+
#
|
|
1782
|
+
# Force --dry-run=false because `npm publish --dry-run` exports
|
|
1783
|
+
# npm_config_dry_run=true into lifecycle scripts. Without the explicit
|
|
1784
|
+
# override, this nested actual-pack smoke prints the tarball name but
|
|
1785
|
+
# does not write the .tgz file, causing prepublishOnly to fail before
|
|
1786
|
+
# a real publish can be exercised.
|
|
1787
|
+
section "publish install smoke (actual pack + tar + fresh install)"
|
|
1788
|
+
|
|
1789
|
+
local version tgz_name tgz_path
|
|
1790
|
+
version=$(node -p "require('${REPO_DIR}/package.json').version")
|
|
1791
|
+
# Scoped npm packages produce a tarball named "<scope>-<name>-<version>.tgz"
|
|
1792
|
+
# where the `@` is stripped and `/` becomes `-`. For `@junghanacs/entwurf`
|
|
1793
|
+
# that lands as `junghanacs-entwurf-<version>.tgz`. Hardcoded against
|
|
1794
|
+
# the scope above so a name change cannot silently slide past this gate.
|
|
1795
|
+
tgz_name="junghanacs-entwurf-${version}.tgz"
|
|
1796
|
+
tgz_path="${REPO_DIR}/${tgz_name}"
|
|
1797
|
+
rm -f "$tgz_path"
|
|
1798
|
+
|
|
1799
|
+
echo "[check-pack-install] npm pack -> ${tgz_name}"
|
|
1800
|
+
(cd "$REPO_DIR" && npm pack --dry-run=false 2>&1 | tail -1) || {
|
|
1801
|
+
fail "[check-pack-install] npm pack failed"
|
|
1802
|
+
return 1
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if [ ! -f "$tgz_path" ]; then
|
|
1806
|
+
fail "[check-pack-install] tarball not produced: $tgz_path"
|
|
1807
|
+
return 1
|
|
1808
|
+
fi
|
|
1809
|
+
|
|
1810
|
+
# tar -tf invariants — cross-check against dry-run shape. Same
|
|
1811
|
+
# required/forbidden axes as check_pack; if they disagree, the
|
|
1812
|
+
# dry-run resolver and the actual tarball diverged (npm bug or
|
|
1813
|
+
# files allowlist drift) and publish must not proceed.
|
|
1814
|
+
local tar_files pass=1 f pat
|
|
1815
|
+
tar_files=$(tar -tf "$tgz_path" | sed 's|^package/||' | grep -v '/$' || true)
|
|
1816
|
+
|
|
1817
|
+
# Required tarball contents. The old 0.11.0 ACP root files (index.ts,
|
|
1818
|
+
# acp-bridge.ts, event-mapper.ts, engraving.ts, pi-context-augment.ts,
|
|
1819
|
+
# pi-extensions/entwurf.ts) were removed on v2-only and are GONE — keeping them
|
|
1820
|
+
# here made this heavy gate silently RED (publish-blocking) while `pnpm check`
|
|
1821
|
+
# (which runs only check-pack, not check-pack-install) stayed green. The ACP
|
|
1822
|
+
# plugin re-enters on v2 as the provider entry + lib/acp/* modules below.
|
|
1823
|
+
local tar_required=(
|
|
1824
|
+
"package.json" "README.md" "LICENSE" "CHANGELOG.md"
|
|
1825
|
+
"protocol.js" "run.sh"
|
|
1826
|
+
"pi-extensions/acp-provider.ts"
|
|
1827
|
+
"pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/backend.ts"
|
|
1828
|
+
"pi-extensions/lib/acp/overlay.ts" "pi-extensions/lib/acp/tool-surface.ts"
|
|
1829
|
+
"pi-extensions/lib/acp/event-mapper.ts" "pi-extensions/lib/acp/context.ts"
|
|
1830
|
+
"pi-extensions/entwurf-control.ts"
|
|
1831
|
+
"pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
|
|
1832
|
+
"mcp/entwurf-bridge/src/index.ts"
|
|
1833
|
+
"scripts/postinstall-chmod.cjs"
|
|
1834
|
+
"pi/entwurf-capabilities.json"
|
|
1835
|
+
"pi/entwurf-targets.json"
|
|
1836
|
+
"pi/meta-bridge/.claude-plugin/marketplace.json"
|
|
1837
|
+
"pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json"
|
|
1838
|
+
"pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json"
|
|
1839
|
+
"pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh"
|
|
1840
|
+
"pi-extensions/meta-bridge-hook.ts"
|
|
1841
|
+
"pi-extensions/lib/meta-session.ts"
|
|
1842
|
+
"pi-extensions/lib/session-id.js"
|
|
1843
|
+
"scripts/meta-bridge-install.sh"
|
|
1844
|
+
"scripts/meta-bridge-state.py"
|
|
1845
|
+
)
|
|
1846
|
+
for f in "${tar_required[@]}"; do
|
|
1847
|
+
if ! grep -qxF "$f" <<<"$tar_files"; then
|
|
1848
|
+
fail "[check-pack-install] tar missing required: $f"
|
|
1849
|
+
pass=0
|
|
1850
|
+
fi
|
|
1851
|
+
done
|
|
1852
|
+
|
|
1853
|
+
local tar_forbidden=(
|
|
1854
|
+
'pi-session-.*\.html$' '\.log$' '\.cast$'
|
|
1855
|
+
'^bench\.sh$' '^biome\.json$' '^tsconfig\.json$'
|
|
1856
|
+
'^pnpm-(lock\.yaml|workspace\.yaml)$' '^NEXT\.md$'
|
|
1857
|
+
'^plugins/' '^node_modules/'
|
|
1858
|
+
'\.tmp-verify/' '\.agent-(reports|shell)/'
|
|
1859
|
+
'pi/meta-bridge/\.assembled/'
|
|
1860
|
+
)
|
|
1861
|
+
for pat in "${tar_forbidden[@]}"; do
|
|
1862
|
+
local hit
|
|
1863
|
+
hit=$(grep -E "$pat" <<<"$tar_files" || true)
|
|
1864
|
+
if [ -n "$hit" ]; then
|
|
1865
|
+
fail "[check-pack-install] tar contains forbidden pattern $pat:"
|
|
1866
|
+
echo "$hit" | sed 's/^/ /' >&2
|
|
1867
|
+
pass=0
|
|
1868
|
+
fi
|
|
1869
|
+
done
|
|
1870
|
+
|
|
1871
|
+
if [ "$pass" != "1" ]; then
|
|
1872
|
+
rm -f "$tgz_path"
|
|
1873
|
+
fail "[check-pack-install] tar -tf invariants violated"
|
|
1874
|
+
return 1
|
|
1875
|
+
fi
|
|
1876
|
+
echo "[check-pack-install] tar -tf invariants pass ($(printf '%s\n' "$tar_files" | wc -l | tr -d ' ') files)"
|
|
1877
|
+
|
|
1878
|
+
# Fresh-temp install smoke. Uses pnpm because that is what this
|
|
1879
|
+
# repo packages with; --ignore-workspace stops it from re-attaching
|
|
1880
|
+
# to our pnpm-workspace.yaml; --ignore-scripts blocks the husky
|
|
1881
|
+
# prepare hook (and any future install scripts) from running inside
|
|
1882
|
+
# the consumer project. Peer deps are pinned to the 0.80.x release
|
|
1883
|
+
# baseline so the smoke matches the same shape an external pi user
|
|
1884
|
+
# would have after `pi install`.
|
|
1885
|
+
local tmp npm_tmp
|
|
1886
|
+
tmp=$(mktemp -d -t entwurf-install-smoke.XXXXXX)
|
|
1887
|
+
# Separate tree for the npm-managed regression below: npm install must NOT be
|
|
1888
|
+
# nested under $tmp, whose pnpm-add node_modules/package.json would make npm
|
|
1889
|
+
# climb the parent and choke ("Cannot read properties of null").
|
|
1890
|
+
npm_tmp=$(mktemp -d -t entwurf-npm-managed.XXXXXX)
|
|
1891
|
+
trap 'rm -rf "$tmp" "$npm_tmp" "$tgz_path"' RETURN
|
|
1892
|
+
|
|
1893
|
+
printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
|
|
1894
|
+
|
|
1895
|
+
echo "[check-pack-install] pnpm add into $tmp (with 0.80.x peers + typebox)"
|
|
1896
|
+
local install_log
|
|
1897
|
+
install_log=$(cd "$tmp" && pnpm add \
|
|
1898
|
+
"$tgz_path" \
|
|
1899
|
+
"@earendil-works/pi-ai@0.80.2" \
|
|
1900
|
+
"@earendil-works/pi-coding-agent@0.80.2" \
|
|
1901
|
+
"@earendil-works/pi-tui@0.80.2" \
|
|
1902
|
+
"typebox@latest" \
|
|
1903
|
+
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
1904
|
+
fail "[check-pack-install] pnpm add failed:"
|
|
1905
|
+
echo "$install_log" | tail -10 | sed 's/^/ /' >&2
|
|
1906
|
+
return 1
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
# Resolve the installed package.json and confirm pi.extensions
|
|
1910
|
+
# arrived intact. If pi.extensions is empty or missing, the
|
|
1911
|
+
# consumer pi runtime would fail to register any extension.
|
|
1912
|
+
local probe
|
|
1913
|
+
probe=$(cd "$tmp" && node --input-type=module -e "
|
|
1914
|
+
const m = await import('@junghanacs/entwurf/package.json', { with: { type: 'json' } });
|
|
1915
|
+
const pkg = m.default;
|
|
1916
|
+
const exts = Array.isArray(pkg.pi?.extensions) ? pkg.pi.extensions.length : 0;
|
|
1917
|
+
if (exts === 0) { console.error('pi.extensions missing or empty'); process.exit(1); }
|
|
1918
|
+
console.log(pkg.version + ' (' + exts + ' extensions)');
|
|
1919
|
+
" 2>&1) || {
|
|
1920
|
+
fail "[check-pack-install] installed package probe failed:"
|
|
1921
|
+
echo "$probe" | sed 's/^/ /' >&2
|
|
1922
|
+
return 1
|
|
1923
|
+
}
|
|
1924
|
+
echo "[check-pack-install] installed: $probe"
|
|
1925
|
+
|
|
1926
|
+
# Pi package loader smoke — actual `pi` reads the manifest and
|
|
1927
|
+
# registers the provider. rc=0 + the curated model list in the
|
|
1928
|
+
# output means pi accepted the package as a real extension, not
|
|
1929
|
+
# just a well-shaped npm tarball. `--list-models` does not spawn
|
|
1930
|
+
# the Claude/Codex/Gemini backends, so this stays credential-free
|
|
1931
|
+
# and safe to run in CI. Output goes to stderr; capture both
|
|
1932
|
+
# streams with 2>&1.
|
|
1933
|
+
if ! command -v pi >/dev/null 2>&1; then
|
|
1934
|
+
fail "[check-pack-install] pi binary not on PATH — cannot run loader smoke"
|
|
1935
|
+
return 1
|
|
1936
|
+
fi
|
|
1937
|
+
|
|
1938
|
+
local loader_out
|
|
1939
|
+
loader_out=$(cd "$tmp" && pi -e "$tmp/node_modules/@junghanacs/entwurf" --list-models entwurf 2>&1) || {
|
|
1940
|
+
fail "[check-pack-install] pi loader smoke failed (exit non-zero):"
|
|
1941
|
+
echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
|
|
1942
|
+
return 1
|
|
1943
|
+
}
|
|
1944
|
+
if ! grep -q "entwurf" <<<"$loader_out"; then
|
|
1945
|
+
fail "[check-pack-install] pi loader output missing entwurf model surface:"
|
|
1946
|
+
echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
|
|
1947
|
+
return 1
|
|
1948
|
+
fi
|
|
1949
|
+
# Verify the full curated Claude surface is visible: the Sonnet row AND the
|
|
1950
|
+
# opus anchor (CURATED_ANCHOR_MODEL_ID in lib/acp/models.ts — the model whose
|
|
1951
|
+
# absence is a hard registry regression). Checking only one would let half the
|
|
1952
|
+
# surface drop silently.
|
|
1953
|
+
for anchor in "claude-sonnet-4-6" "claude-opus-4-8"; do
|
|
1954
|
+
if ! grep -q "$anchor" <<<"$loader_out"; then
|
|
1955
|
+
fail "[check-pack-install] pi loader output missing curated Claude model $anchor:"
|
|
1956
|
+
echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
|
|
1957
|
+
return 1
|
|
1958
|
+
fi
|
|
1959
|
+
done
|
|
1960
|
+
echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-4-6 + claude-opus-4-8 anchor)"
|
|
1961
|
+
|
|
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
|
|
1972
|
+
# 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.
|
|
1974
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
1975
|
+
fail "[check-pack-install] npm not on PATH — cannot run npm-managed install regression"
|
|
1976
|
+
return 1
|
|
1977
|
+
fi
|
|
1978
|
+
local npmroot="$npm_tmp/npmroot" npmhome="$npm_tmp/npmhome" npmproj="$npm_tmp/npmproj" npm_log npm_pkg wire_log
|
|
1979
|
+
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:"
|
|
1982
|
+
echo "$npm_log" | tail -10 | sed 's/^/ /' >&2
|
|
1983
|
+
return 1
|
|
1984
|
+
}
|
|
1985
|
+
npm_pkg="$npmroot/node_modules/@junghanacs/entwurf"
|
|
1986
|
+
if [ ! -x "$npm_pkg/run.sh" ]; then
|
|
1987
|
+
fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
|
|
1988
|
+
return 1
|
|
1989
|
+
fi
|
|
1990
|
+
wire_log=$(HOME="$npmhome" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
|
|
1991
|
+
fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
|
|
1992
|
+
echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
|
|
1993
|
+
return 1
|
|
1994
|
+
}
|
|
1995
|
+
if [ ! -f "$npmproj/.pi/settings.json" ]; then
|
|
1996
|
+
fail "[check-pack-install] npm-managed run.sh install did not write settings.json:"
|
|
1997
|
+
echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
|
|
1998
|
+
return 1
|
|
1999
|
+
fi
|
|
2000
|
+
if ! grep -q "node_modules/@junghanacs/entwurf" <<<"$wire_log"; then
|
|
2001
|
+
fail "[check-pack-install] npm-managed install did not report the npm package source:"
|
|
2002
|
+
echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
|
|
2003
|
+
return 1
|
|
2004
|
+
fi
|
|
2005
|
+
echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
|
|
2006
|
+
|
|
2007
|
+
ok "[check-pack-install] publish install smoke pass"
|
|
2008
|
+
return 0
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
# --- v2 install/runtime verification gates ---
|
|
2013
|
+
#
|
|
2014
|
+
# These validators complement the local deterministic check_* gates by exercising
|
|
2015
|
+
# the runtime surfaces an installed v2 package depends on. On v2-only, setup must
|
|
2016
|
+
# NOT run the legacy ACP/v1 Axis 1 interview gates (session-messaging/sentinel):
|
|
2017
|
+
# those call removed surfaces and survive only as fail-loud reference subcommands
|
|
2018
|
+
# until rewritten onto entwurf_v2. The release-gate owns the heavier live v2
|
|
2019
|
+
# substrate proof (matrix-live + spawn-resume-live).
|
|
2020
|
+
|
|
2021
|
+
validate_entwurf_bridge() {
|
|
2022
|
+
local bridge_dir="$REPO_DIR/mcp/entwurf-bridge"
|
|
2023
|
+
local raw
|
|
2024
|
+
|
|
2025
|
+
if [ ! -x "$bridge_dir/start.sh" ]; then
|
|
2026
|
+
fail "entwurf-bridge: launcher missing at $bridge_dir/start.sh"
|
|
2027
|
+
return 1
|
|
2028
|
+
fi
|
|
2029
|
+
|
|
2030
|
+
log "entwurf-bridge: direct MCP smoke (strip-types launcher, no build step)"
|
|
2031
|
+
|
|
2032
|
+
if ! raw=$(cd "$bridge_dir" && node --input-type=module <<'JS'
|
|
2033
|
+
import { spawn } from 'node:child_process';
|
|
2034
|
+
|
|
2035
|
+
const child = spawn('./start.sh');
|
|
2036
|
+
let stdout = '';
|
|
2037
|
+
let stderr = '';
|
|
2038
|
+
let done = false;
|
|
2039
|
+
|
|
2040
|
+
function finishOk(trimmed) {
|
|
2041
|
+
if (done) return;
|
|
2042
|
+
done = true;
|
|
2043
|
+
clearTimeout(timer);
|
|
2044
|
+
if (stderr.trim()) console.error(stderr.trim());
|
|
2045
|
+
const msg = JSON.parse(trimmed);
|
|
2046
|
+
const tools = msg?.result?.tools;
|
|
2047
|
+
if (!Array.isArray(tools)) {
|
|
2048
|
+
console.error('tools/list response missing result.tools');
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
}
|
|
2051
|
+
const names = tools.map((t) => t?.name).sort();
|
|
2052
|
+
const expected = ['entwurf_peers', 'entwurf_self', 'entwurf_inbox_read', 'entwurf_v2'];
|
|
2053
|
+
for (const name of expected) {
|
|
2054
|
+
if (!names.includes(name)) {
|
|
2055
|
+
console.error(`missing MCP tool: ${name}`);
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
console.log(names.join(','));
|
|
2060
|
+
child.kill('SIGTERM');
|
|
2061
|
+
process.exit(0);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
child.stdout.on('data', (d) => {
|
|
2065
|
+
stdout += d.toString();
|
|
2066
|
+
const trimmed = stdout.trim();
|
|
2067
|
+
if (trimmed) finishOk(trimmed);
|
|
2068
|
+
});
|
|
2069
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
2070
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n');
|
|
2071
|
+
|
|
2072
|
+
const timer = setTimeout(() => {
|
|
2073
|
+
child.kill('SIGKILL');
|
|
2074
|
+
console.error('entwurf-bridge direct smoke timeout');
|
|
2075
|
+
process.exit(1);
|
|
2076
|
+
}, 3000);
|
|
2077
|
+
|
|
2078
|
+
child.on('error', (err) => {
|
|
2079
|
+
if (done) return;
|
|
2080
|
+
clearTimeout(timer);
|
|
2081
|
+
console.error(String(err));
|
|
2082
|
+
process.exit(1);
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
child.on('close', () => {
|
|
2086
|
+
if (done) return;
|
|
2087
|
+
clearTimeout(timer);
|
|
2088
|
+
const trimmed = stdout.trim();
|
|
2089
|
+
if (!trimmed) {
|
|
2090
|
+
if (stderr.trim()) console.error(stderr.trim());
|
|
2091
|
+
console.error('empty tools/list response');
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
finishOk(trimmed);
|
|
2095
|
+
});
|
|
2096
|
+
JS
|
|
2097
|
+
); then
|
|
2098
|
+
fail "entwurf-bridge: direct MCP smoke failed"
|
|
2099
|
+
return 1
|
|
2100
|
+
fi
|
|
2101
|
+
ok "entwurf-bridge direct MCP smoke ($raw)"
|
|
2102
|
+
|
|
2103
|
+
if ! (cd "$bridge_dir" && ./test.sh >/dev/null); then
|
|
2104
|
+
fail "entwurf-bridge: protocol/negative-path tests failed"
|
|
2105
|
+
return 1
|
|
2106
|
+
fi
|
|
2107
|
+
ok "entwurf-bridge test.sh"
|
|
2108
|
+
|
|
2109
|
+
# check-bridge deliberately stops at the objective MCP boundary. Live v2
|
|
2110
|
+
# substrate/orchestration is covered by the v2 live smokes, whose assertions
|
|
2111
|
+
# parse operational artifacts instead of asking a model to self-report which
|
|
2112
|
+
# tool schema it sees. This split keeps check-bridge credential-free and avoids
|
|
2113
|
+
# model self-recognition variance blocking setup.
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
check_bridge() {
|
|
2117
|
+
section "entwurf-bridge (direct MCP protocol)"
|
|
2118
|
+
validate_entwurf_bridge
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
# setup_all — full entwurf v2 install.
|
|
2123
|
+
#
|
|
2124
|
+
# Installs the v2 dispatch substrate + MCP entwurf-bridge into a target project
|
|
2125
|
+
# and verifies the installed bridge boundary. ACP/v1 backend interview gates are
|
|
2126
|
+
# deliberately not part of setup on v2-only; the heavier live v2 substrate proof
|
|
2127
|
+
# is release-gate's job.
|
|
2128
|
+
#
|
|
2129
|
+
# An external harness that consumes entwurf (e.g. agent-config as a
|
|
2130
|
+
# pi package + skills set) may still have its own install/setup for its
|
|
2131
|
+
# own concerns; those are outside the scope of this script.
|
|
2132
|
+
setup_all() {
|
|
2133
|
+
local project_dir
|
|
2134
|
+
project_dir=$(normalize_project_dir "$1")
|
|
2135
|
+
|
|
2136
|
+
require_cmd pnpm
|
|
2137
|
+
require_cmd python3
|
|
2138
|
+
require_cmd pi
|
|
2139
|
+
require_cmd node
|
|
2140
|
+
|
|
2141
|
+
# MCP bridge launchers run via `node --experimental-strip-types` (stable in
|
|
2142
|
+
# Node 23.6, experimental from 22.6). Anything older lacks the flag, and an
|
|
2143
|
+
# ACP session would hit a cryptic "unknown argument" rather than a clear
|
|
2144
|
+
# setup-time error. Fail early with an actionable message. package.json
|
|
2145
|
+
# engines.node mirrors this floor.
|
|
2146
|
+
if ! node -e 'const [M,m]=process.versions.node.split(".").map(Number); process.exit((M>22||(M===22&&m>=6))?0:1)'; then
|
|
2147
|
+
echo "[setup] entwurf requires Node >= 22.6.0 (got $(node -v))" >&2
|
|
2148
|
+
echo "[setup] MCP bridge launchers depend on --experimental-strip-types." >&2
|
|
2149
|
+
exit 1
|
|
2150
|
+
fi
|
|
2151
|
+
|
|
2152
|
+
echo "[setup] repo: $REPO_DIR"
|
|
2153
|
+
echo "[setup] project: $project_dir"
|
|
2154
|
+
echo "[setup] scope: entwurf v2 orchestration install (pi-native; ACP backends dropped)"
|
|
2155
|
+
echo "[setup] verification: v2 install smoke (entwurf-bridge; LIVE substrate = release-gate)"
|
|
2156
|
+
|
|
2157
|
+
(cd "$REPO_DIR" && pnpm install --frozen-lockfile)
|
|
2158
|
+
sync_auth
|
|
2159
|
+
install_local_package "$project_dir"
|
|
2160
|
+
|
|
2161
|
+
# Single confident install command (GLG 2026-06-23): setup ALSO wires the
|
|
2162
|
+
# native-harness meta-bridge, so a relocate/clone needs ONE command — not the
|
|
2163
|
+
# install + install-meta-bridge two-pronged split that froze the Claude
|
|
2164
|
+
# statusLine on the old path (the 2026-06-23 relocation gap). Detection-gated:
|
|
2165
|
+
# a pi-only host with no native harness skips it cleanly (§10 "있으면 설정,
|
|
2166
|
+
# 없으면 담아준다"); meta-bridge-install.sh is itself idempotent.
|
|
2167
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2168
|
+
section "meta-bridge install (native harness detected: Claude Code)"
|
|
2169
|
+
(cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh)
|
|
2170
|
+
else
|
|
2171
|
+
echo "[setup] no native harness (claude) on PATH — skipping meta-bridge wiring (pi-only host)"
|
|
2172
|
+
fi
|
|
2173
|
+
|
|
2174
|
+
# Deterministic preflight lives in `pnpm check`; live substrate acceptance lives
|
|
2175
|
+
# in `LIVE=1 ./run.sh release-gate <scratch>`. Setup is the install path, so it
|
|
2176
|
+
# verifies the installed MCP bridge boundary only and does NOT run the legacy
|
|
2177
|
+
# ACP/v1 session-messaging/sentinel gates.
|
|
2178
|
+
section "v2 install smoke: entwurf-bridge (direct MCP protocol)"
|
|
2179
|
+
validate_entwurf_bridge
|
|
2180
|
+
|
|
2181
|
+
echo ""
|
|
2182
|
+
echo "DONE: entwurf setup (pi package + meta-bridge + v2 install smoke) green."
|
|
2183
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2184
|
+
echo "Verify native-harness wiring with: ./run.sh doctor-meta-bridge"
|
|
2185
|
+
fi
|
|
2186
|
+
echo "Run 'LIVE=1 ./run.sh release-gate <scratch>' for live substrate acceptance."
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
# release-gate — the single command that, when GREEN, is sufficient to cut
|
|
2190
|
+
# release cuts. Runs the full static floor (`pnpm check`) followed by the
|
|
2191
|
+
# v2-native live gates, then emits one PASS/FAIL/SKIP summary. Everything is
|
|
2192
|
+
# invoked through run.sh subcommands — never a script in scripts/ directly.
|
|
2193
|
+
#
|
|
2194
|
+
# Design invariants (NEXT Step 1e + GPT-5.5 reviews):
|
|
2195
|
+
# - v2-native live floor: the MUST tier is the v2 dispatch substrate
|
|
2196
|
+
# (smoke-entwurf-v2-matrix-live + smoke-entwurf-v2-spawn-resume-live, opt-in
|
|
2197
|
+
# LIVE), the MCP bridge (check-bridge), and the garden-native substrate/guard
|
|
2198
|
+
# (smoke-session-id-name on a pi-native target via ENTWURF_LIVE_TARGET,
|
|
2199
|
+
# and smoke-resident-garden-guard).
|
|
2200
|
+
# - ACP plugin acceptance floor (S0~S2g): the 11 ACP LIVE smokes
|
|
2201
|
+
# (socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/
|
|
2202
|
+
# memory-containment/rgg + S2g mcp/skill config passthrough + S2g axis-3 bundled-mcp resident/RPC)
|
|
2203
|
+
# are MUST, not BEHAVIOR — they prove programmatic transport/provider/backend
|
|
2204
|
+
# invariants of the ACP plugin on the v2 core, so a failure is a release
|
|
2205
|
+
# defect, not an advisory model-in-loop signal. Each is LIVE-gated honest-SKIP.
|
|
2206
|
+
# - v1 entwurf verbs are gone (v2 core): the old xt-tool-surface / session-messaging
|
|
2207
|
+
# / sentinel floor gates do not exist on this tree. --allow-skip-gemini is
|
|
2208
|
+
# accepted-but-ignored (back-compat).
|
|
2209
|
+
# - Final release authorization is GLG's, not this script's: a green
|
|
2210
|
+
# run is necessary, and the operator closes the decision.
|
|
2211
|
+
release_gate() {
|
|
2212
|
+
local -a positional=()
|
|
2213
|
+
local a
|
|
2214
|
+
for a in "$@"; do
|
|
2215
|
+
case "$a" in
|
|
2216
|
+
--allow-skip-gemini) ;; # accepted-but-ignored: gemini removed from the claude-only floor (back-compat for existing scripts)
|
|
2217
|
+
*) positional+=("$a") ;;
|
|
2218
|
+
esac
|
|
2219
|
+
done
|
|
2220
|
+
local project_dir
|
|
2221
|
+
project_dir=$(normalize_project_dir "${positional[0]:-$PROJECT_DIR_DEFAULT}")
|
|
2222
|
+
|
|
2223
|
+
# Absolute path to this script — survives the `cd "$project_dir"` below. The
|
|
2224
|
+
# live gates derive their pi session dir from $PWD (tmux `-c "$PWD"`,
|
|
2225
|
+
# PROJECT_DIR_DEFAULT, and the bare `pi -p` invocations that don't `cd`
|
|
2226
|
+
# themselves). Some gates (e.g. check-bridge and the garden guard) take no
|
|
2227
|
+
# project arg, so if release-gate runs from the repo their sessions could land
|
|
2228
|
+
# in the repo's own session dir — polluting the very
|
|
2229
|
+
# tree we ship and breaking the "scratch full gate" evidence claim. Running
|
|
2230
|
+
# EVERY live gate with PWD=project_dir makes a single
|
|
2231
|
+
# `./run.sh release-gate <scratch>` invocation route all sessions to scratch
|
|
2232
|
+
# regardless of the operator's cwd. `-e "$REPO_DIR/..."` (extension load) and
|
|
2233
|
+
# every other path the gates touch are absolute, so the cd is safe.
|
|
2234
|
+
#
|
|
2235
|
+
# The two garden-native identity gates (smoke-session-id-name,
|
|
2236
|
+
# smoke-resident-garden-guard) also take no project arg but are exempt from
|
|
2237
|
+
# the repo-pollution concern by construction: the substrate smoke runs every
|
|
2238
|
+
# pi turn under its own os.tmpdir() agent dir + cwds (mkdtemp, cleaned up),
|
|
2239
|
+
# and the guard is wired here as the NEGATIVE path only — a 0-token fail-fast
|
|
2240
|
+
# that writes no session file at all.
|
|
2241
|
+
#
|
|
2242
|
+
# smoke-acp-bundled-mcp-live is a DELIBERATE exception to the PWD=project_dir
|
|
2243
|
+
# routing: it runs its resident with cwd=os.tmpdir() and relies on the
|
|
2244
|
+
# operator's INSTALLED bundled bridge (global ~/.pi/agent/settings.json
|
|
2245
|
+
# entwurfProvider.mcpServers.entwurf-bridge) — that IS the operator circuit
|
|
2246
|
+
# this axis restores, not a scratch-isolated probe (that is smoke-acp-mcp-live's
|
|
2247
|
+
# job). It writes only a tmpdir-cwd session (no repo pollution) and fails loud if
|
|
2248
|
+
# the operator has not wired the bundled bridge.
|
|
2249
|
+
local self="$REPO_DIR/run.sh"
|
|
2250
|
+
gate() { ( cd "$project_dir" && "$@" ); }
|
|
2251
|
+
|
|
2252
|
+
local pass=0 failc=0 skip=0
|
|
2253
|
+
local -a results=()
|
|
2254
|
+
|
|
2255
|
+
run_step() {
|
|
2256
|
+
local name="$1"; shift
|
|
2257
|
+
section "release-gate step: $name"
|
|
2258
|
+
if "$@"; then
|
|
2259
|
+
ok "$name: PASS"
|
|
2260
|
+
results+=("PASS $name"); pass=$((pass + 1))
|
|
2261
|
+
else
|
|
2262
|
+
fail "$name: FAIL"
|
|
2263
|
+
results+=("FAIL $name"); failc=$((failc + 1))
|
|
2264
|
+
fi
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
# LIVE-gated MUST step: a release-blocking gate that needs a real backend turn
|
|
2268
|
+
# (auth/model/credit). When LIVE!=1 it is an HONEST SKIP — counted as SKIP, NOT
|
|
2269
|
+
# PASS — so an unattended `./run.sh release-gate` stays runnable without faking
|
|
2270
|
+
# coverage. A release CUT therefore requires `LIVE=1 ./run.sh release-gate
|
|
2271
|
+
# <scratch>` to land MUST PASS with SKIP=0; a green run that still shows SKIP is
|
|
2272
|
+
# CI safety, never live acceptance. (Same rule the v2 substrate sentinels below
|
|
2273
|
+
# spell out inline; this helper applies it to the ACP plugin acceptance floor.)
|
|
2274
|
+
run_live_step() {
|
|
2275
|
+
local name="$1"; shift
|
|
2276
|
+
section "release-gate step: $name"
|
|
2277
|
+
if [ "${LIVE:-}" = "1" ]; then
|
|
2278
|
+
if "$@"; then
|
|
2279
|
+
ok "$name: PASS"
|
|
2280
|
+
results+=("PASS $name"); pass=$((pass + 1))
|
|
2281
|
+
else
|
|
2282
|
+
fail "$name: FAIL"
|
|
2283
|
+
results+=("FAIL $name"); failc=$((failc + 1))
|
|
2284
|
+
fi
|
|
2285
|
+
else
|
|
2286
|
+
warn "$name: LIVE!=1 — skipped (opt-in: needs auth/model — NOT a live acceptance run)"
|
|
2287
|
+
results+=("SKIP $name (LIVE!=1)"); skip=$((skip + 1))
|
|
2288
|
+
fi
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
# BEHAVIOR lane (0.11.0, GLG+GPT+Opus): a SEPARATE advisory counter for
|
|
2292
|
+
# model-in-loop gates that probe whether the *model* autonomously selects the
|
|
2293
|
+
# MCP entwurf surface (vs. bypassing via Bash/Terminal/pi-CLI). These gates are
|
|
2294
|
+
# flaky by the model's nature (Claude Sonnet's MCP-vs-Bash choice is
|
|
2295
|
+
# non-deterministic on 0.79.4), so a single flake must NOT block the cut. They
|
|
2296
|
+
# are NEVER folded into `failc`/`pass` — exit authority below is `failc` only.
|
|
2297
|
+
# Honesty rails: (1) "non-blocking" is NOT "pass" — a BEHAVIOR-FAIL is surfaced
|
|
2298
|
+
# loudly in the summary with its artifact path, never buried; (2) a Bash-bypass
|
|
2299
|
+
# of the entwurf surface stays a hard FAIL *inside* this lane — a bypass is
|
|
2300
|
+
# never relabelled a pass; (3) the entwurf_v2 surface itself is proven by the
|
|
2301
|
+
# deterministic/programmatic must-pass gates above (check-entwurf-v2-*,
|
|
2302
|
+
# check-bridge) — this lane is autonomous-tool-selection *behavior*, not
|
|
2303
|
+
# *function*. Residual bypass → 0.11.x usability lane.
|
|
2304
|
+
local behavior_pass=0 behavior_failc=0
|
|
2305
|
+
local -a behavior_results=()
|
|
2306
|
+
|
|
2307
|
+
run_behavior_step() {
|
|
2308
|
+
local name="$1"; shift
|
|
2309
|
+
section "release-gate BEHAVIOR step (advisory, non-blocking): $name"
|
|
2310
|
+
if "$@"; then
|
|
2311
|
+
ok "$name: BEHAVIOR-PASS"
|
|
2312
|
+
behavior_results+=("BEHAVIOR-PASS $name"); behavior_pass=$((behavior_pass + 1))
|
|
2313
|
+
else
|
|
2314
|
+
warn "$name: BEHAVIOR-FAIL (advisory — model-in-loop signal; S7=Bash-bypass stays hard-fail here; NOT a cut blocker)"
|
|
2315
|
+
behavior_results+=("BEHAVIOR-FAIL $name"); behavior_failc=$((behavior_failc + 1))
|
|
2316
|
+
fi
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
# 1. Static floor (deterministic; includes the two folded gates).
|
|
2320
|
+
section "release-gate step: static (pnpm check)"
|
|
2321
|
+
if (cd "$REPO_DIR" && pnpm check); then
|
|
2322
|
+
ok "static (pnpm check): PASS"
|
|
2323
|
+
results+=("PASS static (pnpm check)"); pass=$((pass + 1))
|
|
2324
|
+
else
|
|
2325
|
+
fail "static (pnpm check): FAIL"
|
|
2326
|
+
results+=("FAIL static (pnpm check)"); failc=$((failc + 1))
|
|
2327
|
+
fi
|
|
2328
|
+
|
|
2329
|
+
# 2. (gemini-availability step removed — claude-only floor; gemini CLI is
|
|
2330
|
+
# deprecated, so the gate no longer asserts a three-backend claim.)
|
|
2331
|
+
|
|
2332
|
+
# 3. Live per-invariant gates (each is a run.sh subcommand). Every one runs
|
|
2333
|
+
# with PWD=project_dir (via gate()) so cwd-derived pi session dirs land in
|
|
2334
|
+
# the scratch project, never the repo — see the note above.
|
|
2335
|
+
#
|
|
2336
|
+
# Foundational garden-native identity gates run first (0.9.0, #28): the
|
|
2337
|
+
# substrate proof (Pi --session-id/--name through the bridge) and the
|
|
2338
|
+
# resident --entwurf-control guard (non-garden id → 0-token fail-fast,
|
|
2339
|
+
# PLUS the positive path: a garden id resident actually boots, gets a
|
|
2340
|
+
# control-tagged name, and is never entwurf-resumable). If the identity
|
|
2341
|
+
# foundation is broken, every Entwurf live gate below is meaningless, so
|
|
2342
|
+
# fail fast here. Negative path is 0-token; positive path is ~1 cheap turn;
|
|
2343
|
+
# the substrate smoke is a few cheap turns.
|
|
2344
|
+
run_step "smoke-session-id-name (3a substrate)" gate bash "$self" smoke-session-id-name
|
|
2345
|
+
# RGG split (0.11.0): the deterministic half (negative/id-safety + /gnew
|
|
2346
|
+
# zero-token live path) is release-blocking and stays here as a must-pass with
|
|
2347
|
+
# SMOKE_RGG_POSITIVE=0. The model-in-loop half (post-/gnew backend entwurf_self
|
|
2348
|
+
# identity turn [T3] + positive garden --session-id model turn) is gated behind
|
|
2349
|
+
# SMOKE_RGG_POSITIVE=1 and runs in the BEHAVIOR lane below — advisory, because
|
|
2350
|
+
# it depends on the backend child autonomously calling entwurf_self.
|
|
2351
|
+
run_step "smoke-resident-garden-guard (3c guard: negative/id-safety + /gnew 0-token, deterministic)" gate env SMOKE_RGG_POSITIVE=0 bash "$self" smoke-resident-garden-guard
|
|
2352
|
+
run_step "check-bridge" gate bash "$self" check-bridge
|
|
2353
|
+
# D4-c: the v2 dispatch substrate sentinel (5d-5). A SINGLE run (NOT backend-looped — it proves
|
|
2354
|
+
# production runEntwurfV2 deps + real pi control-socket RPC + real mailbox enqueue + v2 lock, not
|
|
2355
|
+
# per-backend model behavior). Placed right after check-bridge: the MCP/protocol substrate must be
|
|
2356
|
+
# green first so a matrix-live failure reads as "v2 transport/lock/enqueue", not bridge basics.
|
|
2357
|
+
# Opt-in LIVE: it spawns a real `pi --entwurf-control` (needs auth/model), so LIVE!=1 is an HONEST
|
|
2358
|
+
# SKIP (not a PASS) — an unattended release-gate stays runnable without faking coverage. Independent
|
|
2359
|
+
# of --allow-skip-gemini (now a no-op back-compat flag on the claude-only floor; this gates substrate auth).
|
|
2360
|
+
section "release-gate step: smoke-entwurf-v2-matrix-live"
|
|
2361
|
+
if [ "${LIVE:-}" = "1" ]; then
|
|
2362
|
+
if gate env LIVE=1 bash "$self" smoke-entwurf-v2-matrix-live; then
|
|
2363
|
+
ok "smoke-entwurf-v2-matrix-live: PASS"
|
|
2364
|
+
results+=("PASS smoke-entwurf-v2-matrix-live"); pass=$((pass + 1))
|
|
2365
|
+
else
|
|
2366
|
+
fail "smoke-entwurf-v2-matrix-live: FAIL"
|
|
2367
|
+
results+=("FAIL smoke-entwurf-v2-matrix-live"); failc=$((failc + 1))
|
|
2368
|
+
fi
|
|
2369
|
+
else
|
|
2370
|
+
warn "smoke-entwurf-v2-matrix-live: LIVE!=1 — skipped (v2 substrate sentinel, opt-in: needs auth/model)"
|
|
2371
|
+
results+=("SKIP smoke-entwurf-v2-matrix-live (LIVE!=1)"); skip=$((skip + 1))
|
|
2372
|
+
fi
|
|
2373
|
+
# 0.11.0 (A) acceptance: the FULL spawn-bg resident lifecycle — a real `pi` resume child stands
|
|
2374
|
+
# its control socket up, does a model turn, lock released ×1. Placed right after matrix-live (the
|
|
2375
|
+
# transport sentinel): a spawn-resume failure then reads as "resume/resident lifecycle", not
|
|
2376
|
+
# transport basics. Same opt-in LIVE rule — LIVE!=1 is an HONEST SKIP (model-in-loop, needs
|
|
2377
|
+
# auth/model). NOTE: a 0.11.0 tag REQUIRES `LIVE=1 ./run.sh release-gate` (or this step direct)
|
|
2378
|
+
# to PASS — the SKIP is CI safety, never acceptance.
|
|
2379
|
+
section "release-gate step: smoke-entwurf-v2-spawn-resume-live"
|
|
2380
|
+
if [ "${LIVE:-}" = "1" ]; then
|
|
2381
|
+
if gate env LIVE=1 bash "$self" smoke-entwurf-v2-spawn-resume-live; then
|
|
2382
|
+
ok "smoke-entwurf-v2-spawn-resume-live: PASS"
|
|
2383
|
+
results+=("PASS smoke-entwurf-v2-spawn-resume-live"); pass=$((pass + 1))
|
|
2384
|
+
else
|
|
2385
|
+
fail "smoke-entwurf-v2-spawn-resume-live: FAIL"
|
|
2386
|
+
results+=("FAIL smoke-entwurf-v2-spawn-resume-live"); failc=$((failc + 1))
|
|
2387
|
+
fi
|
|
2388
|
+
else
|
|
2389
|
+
warn "smoke-entwurf-v2-spawn-resume-live: LIVE!=1 — skipped (0.11.0 A acceptance, opt-in: needs auth/model)"
|
|
2390
|
+
results+=("SKIP smoke-entwurf-v2-spawn-resume-live (LIVE!=1)"); skip=$((skip + 1))
|
|
2391
|
+
fi
|
|
2392
|
+
|
|
2393
|
+
# 3b. ACP plugin acceptance floor (S0~S2f live). These prove the ACP plugin's
|
|
2394
|
+
# programmatic transport/provider/backend invariants on the v2 core — NOT
|
|
2395
|
+
# model-in-loop autonomous tool-selection — so they belong in the MUST tier,
|
|
2396
|
+
# not BEHAVIOR. Their deterministic counterparts (check-acp-*, check-auth-
|
|
2397
|
+
# boundary, check-acp-overlay/tool-surface/event-mapper/prompt-builder/
|
|
2398
|
+
# session-store/session-reuse/carrier-augment) already run inside `pnpm
|
|
2399
|
+
# check` above; these are the LIVE acceptance halves. Ordered from the
|
|
2400
|
+
# cheapest, most foundational invariant outward: turn-free citizenship →
|
|
2401
|
+
# pinned ACP pipe/auth → overlay/tool meta → real pi provider path (+
|
|
2402
|
+
# progress visibility / L3 marker) → process-scoped reuse + semantic recall
|
|
2403
|
+
# → first-user augment delivery + empty-carrier billing-clean (핀1) →
|
|
2404
|
+
# ACP-target garden guard (deterministic half). Opt-in LIVE: LIVE!=1 is an
|
|
2405
|
+
# HONEST SKIP via run_live_step (see its note) — a CUT needs LIVE=1, SKIP=0.
|
|
2406
|
+
run_live_step "smoke-acp-socket-citizen-live (S1: turn-free socket citizenship)" gate env LIVE=1 bash "$self" smoke-acp-socket-citizen-live
|
|
2407
|
+
run_live_step "smoke-acp-raw-turn-live (S2a: pinned ACP pipe + local auth)" gate env LIVE=1 bash "$self" smoke-acp-raw-turn-live
|
|
2408
|
+
run_live_step "smoke-acp-overlay-live (S2b: config overlay + hooks:{} + tool meta)" gate env LIVE=1 bash "$self" smoke-acp-overlay-live
|
|
2409
|
+
run_live_step "smoke-acp-provider-live (S2c/S2f: real pi provider path + progress/L3)" gate env LIVE=1 bash "$self" smoke-acp-provider-live
|
|
2410
|
+
run_live_step "smoke-acp-session-reuse-live (S2d: process-scoped reuse + recall)" gate env LIVE=1 bash "$self" smoke-acp-session-reuse-live
|
|
2411
|
+
run_live_step "smoke-acp-carrier-augment-live (S2e-1: augment delivery + 핀1 billing)" gate env LIVE=1 bash "$self" smoke-acp-carrier-augment-live
|
|
2412
|
+
run_live_step "smoke-acp-memory-containment-live (Gate D: no overlay memory leak)" gate env LIVE=1 bash "$self" smoke-acp-memory-containment-live
|
|
2413
|
+
run_live_step "smoke-acp-rgg-live (S2e-2: ACP-target garden guard, deterministic half)" gate env LIVE=1 bash "$self" smoke-acp-rgg-live
|
|
2414
|
+
run_live_step "smoke-acp-mcp-live (S2g: operator mcpServers reach the live ACP session)" gate env LIVE=1 bash "$self" smoke-acp-mcp-live
|
|
2415
|
+
run_live_step "smoke-acp-skill-live (S2g: operator skillPlugins reach the live ACP session)" gate env LIVE=1 bash "$self" smoke-acp-skill-live
|
|
2416
|
+
run_live_step "smoke-acp-bundled-mcp-live (S2g axis 3: bundled entwurf-bridge via 0.11.0 resident/RPC circuit)" gate env LIVE=1 bash "$self" smoke-acp-bundled-mcp-live
|
|
2417
|
+
|
|
2418
|
+
# 4. BEHAVIOR lane (advisory, non-blocking). Model-in-loop gates that probe
|
|
2419
|
+
# whether the model AUTONOMOUSLY drives the MCP entwurf surface. These never
|
|
2420
|
+
# touch `failc`; the cut is decided by the MUST tier above.
|
|
2421
|
+
#
|
|
2422
|
+
# Only genuinely flaky model-in-loop signals live here. Programmatic ACP
|
|
2423
|
+
# plugin invariants are MUST (section 3b above), not BEHAVIOR — a failed
|
|
2424
|
+
# transport/provider/backend smoke is a release defect, not advisory. The old
|
|
2425
|
+
# v1 floor gates (session-messaging / xt-tool-surface / sentinel) do not exist
|
|
2426
|
+
# on the v2 core — the v1 entwurf verbs they exercised are gone.
|
|
2427
|
+
# SMOKE_RGG_POSITIVE=1 re-runs the FULL guard with its positives enabled (not a
|
|
2428
|
+
# positive-only mode) — the deterministic paths run again here too, but only the
|
|
2429
|
+
# two model-in-loop turns (post-/gnew entwurf_self identity [T3] + positive
|
|
2430
|
+
# garden model turn) are the reason this run is advisory; the deterministic half
|
|
2431
|
+
# is already release-blocking via the POSITIVE=0 must-pass step above.
|
|
2432
|
+
run_behavior_step "smoke-resident-garden-guard (positives enabled: post-/gnew entwurf_self identity turn [T3] + positive garden model turn)" gate env SMOKE_RGG_POSITIVE=1 bash "$self" smoke-resident-garden-guard
|
|
2433
|
+
|
|
2434
|
+
# 5. Summary — two tiers. MUST is release-blocking and owns the exit code; the
|
|
2435
|
+
# word "green" is reserved for the MUST tier. BEHAVIOR is advisory and is
|
|
2436
|
+
# surfaced (with per-step artifact paths above) but never blocks the cut.
|
|
2437
|
+
section "release-gate summary"
|
|
2438
|
+
echo " MUST (release-blocking):"
|
|
2439
|
+
printf ' %s\n' "${results[@]}"
|
|
2440
|
+
echo " MUST: PASS=$pass FAIL=$failc SKIP=$skip"
|
|
2441
|
+
echo ""
|
|
2442
|
+
echo " BEHAVIOR (advisory, non-blocking — model-in-loop autonomous MCP tool-selection):"
|
|
2443
|
+
if [ "${#behavior_results[@]}" -gt 0 ]; then
|
|
2444
|
+
printf ' %s\n' "${behavior_results[@]}"
|
|
2445
|
+
fi
|
|
2446
|
+
echo " BEHAVIOR: PASS=$behavior_pass FAIL=$behavior_failc"
|
|
2447
|
+
echo " (per-step artifact paths are printed in each step's output above)"
|
|
2448
|
+
if [ "$behavior_failc" -gt 0 ]; then
|
|
2449
|
+
echo ""
|
|
2450
|
+
warn "BEHAVIOR FAIL present ($behavior_failc) — advisory model-in-loop signal (e.g. S7 Bash-bypass / entwurf_self not autonomously called). Tracked, NOT a cut blocker; see the 0.11.x usability lane."
|
|
2451
|
+
fi
|
|
2452
|
+
# Exit authority = MUST `failc` ONLY. A BEHAVIOR fail never blocks the cut.
|
|
2453
|
+
if [ "$failc" -gt 0 ]; then
|
|
2454
|
+
echo ""
|
|
2455
|
+
fail "release-gate MUST NOT green — $failc release-blocking step(s) failed. Current release is NOT releasable."
|
|
2456
|
+
echo " A green MUST gate is necessary but not sufficient; GLG closes the call."
|
|
2457
|
+
return 1
|
|
2458
|
+
fi
|
|
2459
|
+
if [ "$behavior_failc" -gt 0 ]; then
|
|
2460
|
+
ok "release-gate MUST PASS (all release-blocking steps green); BEHAVIOR FAIL present (advisory). Necessary condition met — GLG authorizes the cut."
|
|
2461
|
+
else
|
|
2462
|
+
ok "release-gate MUST PASS + BEHAVIOR PASS — all green. Necessary condition met — GLG authorizes the cut."
|
|
2463
|
+
fi
|
|
2464
|
+
return 0
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
cmd=${1:-}
|
|
2468
|
+
case "$cmd" in
|
|
2469
|
+
setup)
|
|
2470
|
+
setup_all "$TARGET_PROJECT_DIR"
|
|
2471
|
+
;;
|
|
2472
|
+
release-gate)
|
|
2473
|
+
shift || true
|
|
2474
|
+
release_gate "$@"
|
|
2475
|
+
;;
|
|
2476
|
+
check-bridge)
|
|
2477
|
+
check_bridge
|
|
2478
|
+
;;
|
|
2479
|
+
check-model-lock)
|
|
2480
|
+
check_model_lock
|
|
2481
|
+
;;
|
|
2482
|
+
check-shell-quote)
|
|
2483
|
+
check_shell_quote
|
|
2484
|
+
;;
|
|
2485
|
+
check-entwurf-session-identity)
|
|
2486
|
+
check_entwurf_session_identity
|
|
2487
|
+
;;
|
|
2488
|
+
check-meta-session)
|
|
2489
|
+
check_meta_session
|
|
2490
|
+
;;
|
|
2491
|
+
check-meta-record-v2)
|
|
2492
|
+
check_meta_record_v2
|
|
2493
|
+
;;
|
|
2494
|
+
check-mailbox-receipt-state)
|
|
2495
|
+
check_mailbox_receipt_state
|
|
2496
|
+
;;
|
|
2497
|
+
check-entwurf-capabilities)
|
|
2498
|
+
check_entwurf_capabilities
|
|
2499
|
+
;;
|
|
2500
|
+
check-meta-dual-read)
|
|
2501
|
+
check_meta_dual_read
|
|
2502
|
+
;;
|
|
2503
|
+
check-meta-mailbox-state-write)
|
|
2504
|
+
check_meta_mailbox_state_write
|
|
2505
|
+
;;
|
|
2506
|
+
check-meta-receiver-marker)
|
|
2507
|
+
check_meta_receiver_marker
|
|
2508
|
+
;;
|
|
2509
|
+
check-meta-migration)
|
|
2510
|
+
check_meta_migration
|
|
2511
|
+
;;
|
|
2512
|
+
check-meta-dual-consumers)
|
|
2513
|
+
check_meta_dual_consumers
|
|
2514
|
+
;;
|
|
2515
|
+
check-meta-capability-source)
|
|
2516
|
+
check_meta_capability_source
|
|
2517
|
+
;;
|
|
2518
|
+
check-socket-probe)
|
|
2519
|
+
check_socket_probe
|
|
2520
|
+
;;
|
|
2521
|
+
check-project-trust-handler)
|
|
2522
|
+
check_project_trust_handler
|
|
2523
|
+
;;
|
|
2524
|
+
check-entwurf-v2-contract)
|
|
2525
|
+
check_entwurf_v2_contract
|
|
2526
|
+
;;
|
|
2527
|
+
check-entwurf-v2-lock)
|
|
2528
|
+
check_entwurf_v2_lock
|
|
2529
|
+
;;
|
|
2530
|
+
check-entwurf-v2-decider)
|
|
2531
|
+
check_entwurf_v2_decider
|
|
2532
|
+
;;
|
|
2533
|
+
check-entwurf-v2-matrix)
|
|
2534
|
+
check_entwurf_v2_matrix
|
|
2535
|
+
;;
|
|
2536
|
+
check-entwurf-v2-release)
|
|
2537
|
+
check_entwurf_v2_release
|
|
2538
|
+
;;
|
|
2539
|
+
check-entwurf-v2-send)
|
|
2540
|
+
check_entwurf_v2_send
|
|
2541
|
+
;;
|
|
2542
|
+
check-entwurf-v2-send-fallback)
|
|
2543
|
+
check_entwurf_v2_send_fallback
|
|
2544
|
+
;;
|
|
2545
|
+
check-entwurf-v2-mailbox)
|
|
2546
|
+
check_entwurf_v2_mailbox
|
|
2547
|
+
;;
|
|
2548
|
+
check-entwurf-v2-runner)
|
|
2549
|
+
check_entwurf_v2_runner
|
|
2550
|
+
;;
|
|
2551
|
+
check-entwurf-control-rpc)
|
|
2552
|
+
check_entwurf_control_rpc
|
|
2553
|
+
;;
|
|
2554
|
+
check-entwurf-v2-production)
|
|
2555
|
+
check_entwurf_v2_production
|
|
2556
|
+
;;
|
|
2557
|
+
check-entwurf-v2-surface)
|
|
2558
|
+
check_entwurf_v2_surface
|
|
2559
|
+
;;
|
|
2560
|
+
check-entwurf-bridge-boot)
|
|
2561
|
+
check_entwurf_bridge_boot
|
|
2562
|
+
;;
|
|
2563
|
+
check-entwurf-v2-spawn)
|
|
2564
|
+
check_entwurf_v2_spawn
|
|
2565
|
+
;;
|
|
2566
|
+
check-entwurf-resume-args)
|
|
2567
|
+
check_entwurf_resume_args
|
|
2568
|
+
;;
|
|
2569
|
+
check-entwurf-v2-spawn-production)
|
|
2570
|
+
check_entwurf_v2_spawn_production
|
|
2571
|
+
;;
|
|
2572
|
+
smoke-entwurf-v2-spawn-live)
|
|
2573
|
+
smoke_entwurf_v2_spawn_live
|
|
2574
|
+
;;
|
|
2575
|
+
smoke-entwurf-v2-spawn-resume-live)
|
|
2576
|
+
smoke_entwurf_v2_spawn_resume_live
|
|
2577
|
+
;;
|
|
2578
|
+
smoke-entwurf-v2-matrix-live)
|
|
2579
|
+
smoke_entwurf_v2_matrix_live
|
|
2580
|
+
;;
|
|
2581
|
+
smoke-acp-raw-turn-live)
|
|
2582
|
+
smoke_acp_raw_turn_live
|
|
2583
|
+
;;
|
|
2584
|
+
smoke-acp-overlay-live)
|
|
2585
|
+
smoke_acp_overlay_live
|
|
2586
|
+
;;
|
|
2587
|
+
smoke-acp-memory-containment-live)
|
|
2588
|
+
smoke_acp_memory_containment_live
|
|
2589
|
+
;;
|
|
2590
|
+
smoke-acp-provider-live)
|
|
2591
|
+
smoke_acp_provider_live
|
|
2592
|
+
;;
|
|
2593
|
+
smoke-acp-session-reuse-live)
|
|
2594
|
+
smoke_acp_session_reuse_live
|
|
2595
|
+
;;
|
|
2596
|
+
smoke-acp-mcp-live)
|
|
2597
|
+
smoke_acp_mcp_live
|
|
2598
|
+
;;
|
|
2599
|
+
smoke-acp-skill-live)
|
|
2600
|
+
smoke_acp_skill_live
|
|
2601
|
+
;;
|
|
2602
|
+
smoke-acp-bundled-mcp-live)
|
|
2603
|
+
smoke_acp_bundled_mcp_live
|
|
2604
|
+
;;
|
|
2605
|
+
smoke-acp-carrier-augment-live)
|
|
2606
|
+
smoke_acp_carrier_augment_live
|
|
2607
|
+
;;
|
|
2608
|
+
smoke-acp-rgg-live)
|
|
2609
|
+
smoke_acp_rgg_live
|
|
2610
|
+
;;
|
|
2611
|
+
smoke-acp-socket-citizen-live)
|
|
2612
|
+
smoke_acp_socket_citizen_live
|
|
2613
|
+
;;
|
|
2614
|
+
check-entwurf-facts)
|
|
2615
|
+
check_entwurf_facts
|
|
2616
|
+
;;
|
|
2617
|
+
check-socket-discovery)
|
|
2618
|
+
check_socket_discovery
|
|
2619
|
+
;;
|
|
2620
|
+
check-meta-listing)
|
|
2621
|
+
check_meta_listing
|
|
2622
|
+
;;
|
|
2623
|
+
check-entwurf-fact-provider)
|
|
2624
|
+
check_entwurf_fact_provider
|
|
2625
|
+
;;
|
|
2626
|
+
check-entwurf-peers-surface)
|
|
2627
|
+
check_entwurf_peers_surface
|
|
2628
|
+
;;
|
|
2629
|
+
check-entwurf-self-address)
|
|
2630
|
+
check_entwurf_self_address
|
|
2631
|
+
;;
|
|
2632
|
+
check-entwurf-deliverability)
|
|
2633
|
+
check_entwurf_deliverability
|
|
2634
|
+
;;
|
|
2635
|
+
check-entwurf-mailbox-guard)
|
|
2636
|
+
check_entwurf_mailbox_guard
|
|
2637
|
+
;;
|
|
2638
|
+
new-session-id)
|
|
2639
|
+
# Garden launcher helper: print one fresh garden sessionId (SSOT:
|
|
2640
|
+
# generateSessionId). Used by the operator alias to make every
|
|
2641
|
+
# --entwurf-control session a garden citizen. Stdout = the id only.
|
|
2642
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/new-session-id.ts)
|
|
2643
|
+
;;
|
|
2644
|
+
smoke-resident-garden-guard)
|
|
2645
|
+
# LIVE negative (0 tokens) + opt-in positive gate for the resident
|
|
2646
|
+
# --entwurf-control garden-native enforcement. NEGATIVE: raw uuid session
|
|
2647
|
+
# must blow up before any turn. POSITIVE (SMOKE_RGG_POSITIVE=1): garden id
|
|
2648
|
+
# passes + control-tagged name.
|
|
2649
|
+
(cd "$REPO_DIR" && bash scripts/smoke-resident-garden-guard.sh)
|
|
2650
|
+
;;
|
|
2651
|
+
smoke-meta-async-drift)
|
|
2652
|
+
# 1.0.0 meta-bridge step 1 (#30): drift sentinel + capability gate. DEFAULT is
|
|
2653
|
+
# deterministic/offline — version pins (Claude/codex/agy) + Claude-binary
|
|
2654
|
+
# undocumented-behavior marker cross-validation; SCREAMS on drift. LIVE=1 adds
|
|
2655
|
+
# the plugin SessionStart watch-arm probe (spawns one metered claude -p).
|
|
2656
|
+
(cd "$REPO_DIR" && bash scripts/smoke-meta-async-drift.sh)
|
|
2657
|
+
;;
|
|
2658
|
+
smoke-meta-honesty)
|
|
2659
|
+
# 1.0.0 meta-bridge HONESTY regression gate (#30 bbot release blockers): the
|
|
2660
|
+
# doorbell must count EVERY queued message honestly (blocker #1), and the
|
|
2661
|
+
# runtime hook must log a silent registration miss as ` ERROR ` for the doctor
|
|
2662
|
+
# to catch while staying best-effort (blocker #2). Offline + deterministic (no
|
|
2663
|
+
# claude binary; deps bash+node+python3), so unlike the drift sentinel it is
|
|
2664
|
+
# CI/pnpm-check safe.
|
|
2665
|
+
(cd "$REPO_DIR" && bash scripts/smoke-meta-honesty.sh)
|
|
2666
|
+
;;
|
|
2667
|
+
smoke-meta-prune)
|
|
2668
|
+
# 1.0.0 meta-bridge Phase 4 regression gate: synthetic store covering every
|
|
2669
|
+
# class (keep/orphan/stale/duplicate/corrupt/drift) proves meta-bridge-prune
|
|
2670
|
+
# classifies correctly, exits 0, and deletes NOTHING (listing-only invariant).
|
|
2671
|
+
# Offline/deterministic (deps: bash+node).
|
|
2672
|
+
(cd "$REPO_DIR" && bash scripts/smoke-meta-prune.sh)
|
|
2673
|
+
;;
|
|
2674
|
+
smoke-meta-keyset-guard)
|
|
2675
|
+
# 0.10.0 meta-bridge regression gate: the PREVENTIVE keyset guard
|
|
2676
|
+
# (check-keyset-overlap) + managed-keys SSOT. Synthetic fragments prove a
|
|
2677
|
+
# disjoint consumer passes and exact/array/parent-child collisions fail loud.
|
|
2678
|
+
# Offline/hermetic (deps: bash+python3).
|
|
2679
|
+
(cd "$REPO_DIR" && bash scripts/smoke-meta-keyset-guard.sh)
|
|
2680
|
+
;;
|
|
2681
|
+
smoke-meta-install-state)
|
|
2682
|
+
# 1.0.0 meta-bridge Phase 2 regression gate: state file captures pre-install
|
|
2683
|
+
# values, install/uninstall touches only the managed keyset, uninstall refuses
|
|
2684
|
+
# to guess without state, and the doctor store scan fails on corrupt/
|
|
2685
|
+
# duplicate/drift records. Offline + deterministic (deps bash+node+python3).
|
|
2686
|
+
(cd "$REPO_DIR" && bash scripts/smoke-meta-install-state.sh)
|
|
2687
|
+
;;
|
|
2688
|
+
smoke-claude-native-resume-live)
|
|
2689
|
+
# LIVE-only Detour A probe: two real Claude Code native turns (fresh +
|
|
2690
|
+
# --resume) in a scratch cwd. Verifies native resume works while the
|
|
2691
|
+
# meta-bridge only records backend=claude-code/nativeSessionId/transcriptPath
|
|
2692
|
+
# once. Not in pnpm check; does not use the ACP provider.
|
|
2693
|
+
(cd "$REPO_DIR" && bash scripts/smoke-claude-native-resume-live.sh)
|
|
2694
|
+
;;
|
|
2695
|
+
install-meta-bridge)
|
|
2696
|
+
# 1.0.0 meta-bridge step 5: operator-grade GLOBAL install of the garden-native
|
|
2697
|
+
# receive plugin. Assembles a self-contained, node-path-baked copy under
|
|
2698
|
+
# pi/meta-bridge/.assembled and runs marketplace add + install --scope user, so
|
|
2699
|
+
# every native Claude Code session auto-loads it (no manual --plugin-dir).
|
|
2700
|
+
# Idempotent; Linux/macOS only (Windows fail-fast).
|
|
2701
|
+
(cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh "$@")
|
|
2702
|
+
;;
|
|
2703
|
+
uninstall-meta-bridge)
|
|
2704
|
+
# 1.0.0 meta-bridge Phase 2: honest inverse of install-meta-bridge. Uses the
|
|
2705
|
+
# install-state file to restore original scalar/map values and remove only the
|
|
2706
|
+
# permission-array entries entwurf added; without state it refuses to guess.
|
|
2707
|
+
(cd "$REPO_DIR" && bash scripts/meta-bridge-uninstall.sh "$@")
|
|
2708
|
+
;;
|
|
2709
|
+
doctor-meta-bridge)
|
|
2710
|
+
# 1.0.0 meta-bridge Phase 2: the FAIL-LOUD surface. Proves toolchain (incl.
|
|
2711
|
+
# python3), stateful managed config, baked node path (NixOS store-churn guard),
|
|
2712
|
+
# global plugin install, USER MCP reach, meta-record store integrity, hook log
|
|
2713
|
+
# no-ERROR, and actual SessionStart creation evidence. A plugin present with
|
|
2714
|
+
# zero claude-code meta-records is a SILENT MISS -> non-zero exit.
|
|
2715
|
+
(cd "$REPO_DIR" && bash scripts/meta-bridge-doctor.sh "$@")
|
|
2716
|
+
;;
|
|
2717
|
+
meta-bridge-prune)
|
|
2718
|
+
# 1.0.0 meta-bridge Phase 4: LISTING-ONLY janitor for the meta-session store.
|
|
2719
|
+
# doctor reds on corrupt/duplicate/drift but intentionally does NOT fail on
|
|
2720
|
+
# transcript-gone records, so a green store can silently bloat with abandoned
|
|
2721
|
+
# records. This surface CLASSIFIES (orphan/stale/ambiguous/keep) and prints
|
|
2722
|
+
# the exact manual rm commands. It deletes NOTHING — no --apply in 1.0.0;
|
|
2723
|
+
# ambiguous (corrupt/duplicate/drift) stays manual-only (operator picks the
|
|
2724
|
+
# surviving authority). Default store = defaultMetaSessionsDir(); pass [dir]
|
|
2725
|
+
# + [--ttl-days N] to override.
|
|
2726
|
+
shift || true
|
|
2727
|
+
(cd "$REPO_DIR" && node --experimental-strip-types scripts/meta-bridge-prune.ts "$@")
|
|
2728
|
+
;;
|
|
2729
|
+
meta-bridge-managed-keys)
|
|
2730
|
+
# 0.10.0 meta-bridge: emit the SSOT of settings.json/~/.claude.json keys that
|
|
2731
|
+
# entwurf's install OWNS. Consumers (agent-config fragment, future
|
|
2732
|
+
# harnesses) read this to set only their OWN keys — the keyset-owner invariant.
|
|
2733
|
+
(cd "$REPO_DIR" && python3 scripts/meta-bridge-state.py managed-keys)
|
|
2734
|
+
;;
|
|
2735
|
+
check-keyset-overlap)
|
|
2736
|
+
# 0.10.0 meta-bridge: PREVENTIVE half of the keyset guard. Fails loud if a
|
|
2737
|
+
# consumer fragment sets a key entwurf owns (exact or ancestor/descendant).
|
|
2738
|
+
# Cross-repo + non-hermetic (fragment path is an arg) → NOT in pnpm check;
|
|
2739
|
+
# its own logic is regression-tested hermetically by smoke-meta-keyset-guard.
|
|
2740
|
+
shift || true
|
|
2741
|
+
(cd "$REPO_DIR" && python3 scripts/check-keyset-overlap.py "$@")
|
|
2742
|
+
;;
|
|
2743
|
+
check-package-source-routing)
|
|
2744
|
+
check_package_source_routing
|
|
2745
|
+
;;
|
|
2746
|
+
smoke-session-id-name)
|
|
2747
|
+
smoke_session_id_name
|
|
2748
|
+
;;
|
|
2749
|
+
check-dep-versions)
|
|
2750
|
+
check_dep_versions
|
|
2751
|
+
;;
|
|
2752
|
+
check-install-preflight)
|
|
2753
|
+
check_install_preflight
|
|
2754
|
+
;;
|
|
2755
|
+
check-pi-import-surface)
|
|
2756
|
+
check_pi_import_surface
|
|
2757
|
+
;;
|
|
2758
|
+
check-env-namespace)
|
|
2759
|
+
check_env_namespace
|
|
2760
|
+
;;
|
|
2761
|
+
check-pi-runtime-version)
|
|
2762
|
+
check_pi_runtime_version
|
|
2763
|
+
;;
|
|
2764
|
+
check-pi-preflight)
|
|
2765
|
+
check_pi_preflight
|
|
2766
|
+
;;
|
|
2767
|
+
check-auth-boundary)
|
|
2768
|
+
check_auth_boundary
|
|
2769
|
+
;;
|
|
2770
|
+
check-acp-provider-surface)
|
|
2771
|
+
check_acp_provider_surface
|
|
2772
|
+
;;
|
|
2773
|
+
check-acp-sdk-surface)
|
|
2774
|
+
check_acp_sdk_surface
|
|
2775
|
+
;;
|
|
2776
|
+
check-acp-overlay)
|
|
2777
|
+
check_acp_overlay
|
|
2778
|
+
;;
|
|
2779
|
+
check-acp-tool-surface)
|
|
2780
|
+
check_acp_tool_surface
|
|
2781
|
+
;;
|
|
2782
|
+
check-acp-event-mapper)
|
|
2783
|
+
check_acp_event_mapper
|
|
2784
|
+
;;
|
|
2785
|
+
check-acp-prompt-builder)
|
|
2786
|
+
check_acp_prompt_builder
|
|
2787
|
+
;;
|
|
2788
|
+
check-acp-config)
|
|
2789
|
+
check_acp_config
|
|
2790
|
+
;;
|
|
2791
|
+
check-acp-session-store)
|
|
2792
|
+
check_acp_session_store
|
|
2793
|
+
;;
|
|
2794
|
+
check-acp-backend-preflight)
|
|
2795
|
+
check_acp_backend_preflight
|
|
2796
|
+
;;
|
|
2797
|
+
check-acp-session-reuse)
|
|
2798
|
+
check_acp_session_reuse
|
|
2799
|
+
;;
|
|
2800
|
+
check-acp-carrier-augment)
|
|
2801
|
+
check_acp_carrier_augment
|
|
2802
|
+
;;
|
|
2803
|
+
check-pack)
|
|
2804
|
+
check_pack
|
|
2805
|
+
;;
|
|
2806
|
+
check-pack-install)
|
|
2807
|
+
check_pack_install
|
|
2808
|
+
;;
|
|
2809
|
+
sync-auth)
|
|
2810
|
+
sync_auth
|
|
2811
|
+
;;
|
|
2812
|
+
install)
|
|
2813
|
+
install_local_package "$TARGET_PROJECT_DIR"
|
|
2814
|
+
;;
|
|
2815
|
+
setup:links)
|
|
2816
|
+
# Repair / refresh ~/.pi/agent/entwurf-targets.json without re-running
|
|
2817
|
+
# the full setup flow. Pass --force to overwrite a stale operator file
|
|
2818
|
+
# or a wrong symlink (a backup is taken for regular files).
|
|
2819
|
+
ensure_agent_dir_symlinks "${2:-}"
|
|
2820
|
+
;;
|
|
2821
|
+
remove)
|
|
2822
|
+
remove_local_package "$TARGET_PROJECT_DIR"
|
|
2823
|
+
;;
|
|
2824
|
+
-h|--help|help|"")
|
|
2825
|
+
usage
|
|
2826
|
+
;;
|
|
2827
|
+
*)
|
|
2828
|
+
echo "Unknown command: $cmd" >&2
|
|
2829
|
+
usage
|
|
2830
|
+
exit 1
|
|
2831
|
+
;;
|
|
2832
|
+
esac
|