-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathrun.sh
More file actions
executable file
·4770 lines (4520 loc) · 286 KB
/
Copy pathrun.sh
File metadata and controls
executable file
·4770 lines (4520 loc) · 286 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
#
# Model id convention (see AGENTS.md Hard Rule #1):
# - User-facing examples use the qualified form `entwurf/<backend-model>`
# (e.g. `entwurf/claude-sonnet-5`); the prefix routes to this provider
# so `--provider` is redundant and is dropped in docs.
# - Smoke helpers that feed `ensureBridgeSession({modelId})` directly (cancel,
# model-switch) pass BARE backend ids (`claude-sonnet-5`, `gpt-5.4`)
# because the bridge library contract is bare. Smoke helpers that invoke pi
# via the CLI still pin `--provider entwurf` and can accept either
# bare or qualified model, but we keep bare here to match the bridge-level
# dispatch tables.
#
set -euo pipefail
SOURCE="${BASH_SOURCE[0]}"
while [ -L "$SOURCE" ]; do
DIR="$(cd -P -- "$(dirname -- "$SOURCE")" && pwd)"
TARGET="$(readlink "$SOURCE")"
case "$TARGET" in
/*) SOURCE="$TARGET" ;;
*) SOURCE="$DIR/$TARGET" ;;
esac
done
REPO_DIR=$(cd -P -- "$(dirname -- "$SOURCE")" && pwd)
PROJECT_DIR_DEFAULT=$(pwd)
TARGET_PROJECT_DIR=${2:-$PROJECT_DIR_DEFAULT}
# npm publish identity. Scoped 2026-05-18 — bare `entwurf` was not on npm
# and we adopted the same `@junghanacs` scope as the OpenClaw plugin sibling
# (`@junghanacs/openclaw-entwurf`) for source-of-origin parity. This
# variable documents intent; check-pack-install hardcodes the tarball name
# and install path against the same scope for traceability.
PACKAGE_NAME="@junghanacs/entwurf"
# Runtime provider id — DO NOT change. Embedded in model strings
# (`entwurf/claude-sonnet-5`), settings keys (`entwurfProvider`),
# log prefixes (`[entwurf:bootstrap]`), and the `--provider entwurf`
# CLI surface. Renaming this would break every consumer transcript and every
# saved session anchor.
PROVIDER_ID="entwurf"
# THE strip-types fence, in one place. Node REFUSES `--experimental-strip-types`
# for any .ts below node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so
# an installed package MUST run the prepack-emitted JS twin — the same boundary
# start.sh (0.12.1), the store-doctor (0.12.4), the plugin hook (0.12.5), and the
# agy imprint (0.12.7) each cross. Every .ts entrypoint routes through here so a
# NEW one cannot silently reintroduce the class: it was hand-written per surface
# before, and three operator commands (doctor-pi-provider / new-session-id /
# meta-bridge-prune) shipped dead under node_modules because of exactly that.
#
# A dev-only gate has no emitted twin by design (check-*/smoke-* are not shipped
# surfaces). Under an installed package it REFUSES rather than falling back to raw
# .ts — a fallback would just re-raise the fence error with a worse message.
# check-install-surface pins both halves statically.
run_ts() {
local rel="$1"; shift
case "$REPO_DIR" in
*/node_modules/*)
local dist="$REPO_DIR/mcp/entwurf-bridge/dist/${rel%.ts}.js"
if [ ! -f "$dist" ]; then
echo "entwurf: '$rel' is a dev-clone-only surface — the installed package ships no compiled twin." >&2
echo " (Node cannot strip types below node_modules; run this from a checkout.)" >&2
return 1
fi
(cd "$REPO_DIR" && node "$dist" "$@")
;;
*)
(cd "$REPO_DIR" && node --experimental-strip-types "$rel" "$@")
;;
esac
}
usage() {
cat <<'EOF'
Usage:
./run.sh setup [project-dir] # ONE confident install: pnpm install + install + meta-bridge (if native harness) + v2 install smoke (LIVE substrate = release-gate)
./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, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send). 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.
./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
./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
./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
./run.sh check-model-lock # deterministic unit test for pi-extensions/model-lock.ts (4-quadrant + edge cases, no API)
./run.sh check-shell-quote # POSIX-safety gate for shellQuote (remote SSH arg quoting in entwurf paths) — source parity + behavior matrix, no SSH
./run.sh check-entwurf-session-identity # deterministic gate for record-era session identity (garden-id grammar + readSessionIdentity: first-model_change authority, name-blind — #50 C3), no API
./run.sh check-meta-session # deterministic gate (#30 step 2, V3-only): fs store — idempotent decideUpsert/upsertMetaSession + mailbox enqueue/read + receipt state, no API
./run.sh check-meta-v3-record # deterministic gate: the ONE live record schema (v3) — canonical serialize/round-trip/mint, foreign-generation rejections name fresh-cut with the actual version value, strict keyset, no API
./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
./run.sh check-entwurf-capabilities # deterministic gate (0.11 Stage 0 step 3C): backend capability registry (pi/entwurf-capabilities.json) — coverage==META_CITIZEN_BACKENDS + agrees with live META_BACKEND_DESCRIPTORS + strict keyset, no API
./run.sh check-capability-bundle-reach # deterministic gate (IN pnpm check): re-ask EVERY shipped copy of meta-session (source + bridge bundle emit) whether metaCapabilitiesFilePath() reaches the registry — the artifact-depth check the source-path gates cannot make; needs a built dist, missing dist FAILS
./run.sh smoke-pi-attach # deterministic gate (#50 C2 checkpoint + C3 ACP tail): a pi session attaches as a V3 meta-record citizen (backend:"pi"), the gardenId is the RECORD's not pi's session id, the control socket is keyed on it, a re-open ATTACHES to the same address (never a second mint), the BUILT DIST ENTRY driven over MCP stdio lists the citizen + delivers entwurf_v2 to that socket with an RPC ack, and the ACP identity chain lands a send AS the host record (enrichMcpServersWithEnvelope env → bridge sender = host gardenId). mkdtemp-isolated; the live store is never read
./run.sh check-bridge-delivery # deterministic gate (IN pnpm check): demo scene 3 recovered — seed strict meta-sender + armed receiver citizens in an isolated temp world, scrub ambient pi/sender carriers, drive the BUILT DIST ENTRY over MCP stdio through a real tools/call entwurf_v2, assert the .msg landed under the seeded sender + doorbell poked. DELIVERS through the artifact, not from source. ENTWURF_DELIVERY_SUBJECT=<launcher> replays the same scene against another consumer artifact (check-pack-install passes the npm-installed bin). No model/network/cost; stale or missing dist FAILS
./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
./run.sh check-meta-receiver-marker # deterministic gate (SE-2): receiver marker round-trip/start-key/provenance, UserPromptSubmit cannot mint presence, reader does not gate on record existence — marker SEMANTICS only; launch topology moved to check-hook-launch-topology
./run.sh check-hook-launch-topology # #51 gate 1: shipped hooks.json is exec form through hook-launch.sh, launcher is loud on an empty argv (older Claude's silent args drop), exec preserves the pid so the hook's parent is Claude, and a space/$/backtick plugin path survives as one argv element
./run.sh check-meta-identity-consumers # deterministic gate: V3-only consumer seam — per-entry targeted read + addressable read snapshot uniqueness, non-regular rivals never read, drift/unparseable rivals unreachable, unreadable regular rivals fail loud; strict upsert refuses an unreadable store before any write, no API
./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); the record.delivery slot 3D-3 preserved was deleted by 3D-4, no API
./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
./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
./run.sh check-entwurf-v2-contract # deterministic gate (0.11 Stage 0 step 4-pre, 동결결정 10 + Fable R1-R5): FROZEN entwurf_v2 contract — R1 control-socket domain (currently pi; 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
./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
./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
./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
./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
./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
./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)
./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
./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
./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
./run.sh check-entwurf-resume-args # deterministic gate (0.11 Stage 0 step 5c-3b): resume-argv SSOT (buildResumePiArgs) for the v2 spawn-bg RESIDENT citizen — the `legacy` one-shot variant and its async worker were removed 2026-07-27, so the shipped posture is the only one the gate drives: v2-control=--entwurf-control + no --no-extensions (keep-alive is the goal, resumed session stays addressable); keeps --mode json -p + prompt-as-turn (-p NOT dropped); explicitExtensionArgs preserved exactly once (#29); plan.launchArgs (--approve) ride before the prompt; null provider→no --provider
./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
./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
./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
./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 record-less socket (#50 C4: live record-less pi → EVERY intent rejected pre-probe record-less-socket, no lock, rendered hint names record authority + fresh-cut), 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
./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), owned-outcome→native-push-no-resume-authority, bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
./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 socket-domain 4-value, facts-only keyset; union: PeerFact + RecordLessSocketFact by gardenId (#50 C4: record-less socket = diagnostic subject, gid+liveness only), dormant→dead, F3 indeterminate preserved, out-of-socket-domain+socket fail-loud; pure, no IO
./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
./run.sh check-meta-listing # deterministic gate: META-STORE facts axis — kind-carrying entries; non-regular records are never read, parse/drift become diagnostics, duplicate nativeSessionId quarantines every rival but not unrelated citizens; strict throws / collect partial; pure injected IO
./run.sh check-entwurf-fact-provider # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4b): ASSEMBLY listEntwurfFacts — listAllMetaIdentities→scanSocketProbes→pre-quarantine out-of-socket-domain/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
./run.sh check-entwurf-peers-surface # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4c): MCP entwurf_peers RENDER renderEntwurfPeers (#50 C4) — payload keyset exactly {peers, diagnostics}; FORBIDDEN keys sessions/socketOnly/controlDir/socketPath/count + no .sock in text (socket is transport, never identity); record-less socket = aggregated record-less-socket diagnostic (F8, liveness-keyed message, alive names fresh-cut); NO verb-routing key (JSON deep scan) NOR word (text), diagnostics both surfaces, empty→(none), unsupported shown; WIRING guard: both surfaces call provider+render, getLiveSessions + /entwurf-sessions gone; facts fabricated, no IO
./run.sh check-entwurf-self-address # deterministic gate (SE-1/SE-2 slice 1): self-addressability honesty predicate computeSelfAddressability — pi replyable ⟺ live socket; meta splits by RAIL: self-fetch ⟺ recordBacked ∧ ownerAlive ∧ watchArmed (regression-proof record-present rows), native-push ⟺ recordBacked ∧ probeAlive (separate axis — no mailbox fact may rescue or sink it), unsupplied rail fail-closed; SOURCE GUARD buildStrictPiSenderEnvelope drops hardcoded replyable:true + existsSync-probes socket, entwurf_self renders alive vs expected AND renders the meta rail per-rail (mailbox only inside the self-fetch branch; native-push denies an inbox and gates injection on the probe)
./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
./run.sh check-native-push-adapter # deterministic gate (봉인 3/8): native-push adapter leaf (antigravity) via a FAKE runner — FULL pid scan (not head -1), dead vs indeterminate, VOLATILE route re-discovery (no cache), send argv+ANTIGRAVITY_LS_ADDRESS env, non-zero exit throws, NO adapter-level retry (executor-owned), resolveNativePushAdapter fail-fast
./run.sh check-native-push-register # deterministic gate (봉인 5): registerNativeConversation (entwurf_register_native core) via fake adapter + isolated mkdtemp store — live probe→CREATE, re-register→ATTACH (same gid, cwd refreshed, no dup), not-live probe→REFUSE (throws, no record), receiver-marker abstinence (보정① source guard)
./run.sh check-agy-sender-identity # deterministic gate (#46 sender lane): WHO is calling the bridge — real agy hook as a child process writes an antigravity sender marker keyed by its PARENT pid (never on upsert failure), and resolveTrustedMetaSenderIdentity over isolated stores yields 0→null / 1→identity on EITHER backend / two distinct live identities on one owner pid→THROW (never guess, never downgrade to anonymous). This is what turns an agy send from external-mcp/unknown-host into a replyable garden citizen
./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
./run.sh new-session-id # print one fresh garden id from the generateSessionId SSOT (#50 C2: no launcher injection — the record layer is the consumer; pi mints its own session id)
./run.sh smoke-resident-garden-guard # live resident --entwurf-control garden guard (negative 0-token; SMOKE_RGG_POSITIVE=1 for positive)
./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)
./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)
./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)
./run.sh check-meta-doctor-oracle # 0.12.8 (#51): detection power of the release ORACLE — healthy fixture must reach `doctor: PASS`, then 21 planted defects (retired shell form, partial hand-patch, launcher bypass/repoint/provenance loss, malformed exec args, owner type drift, extra leaf/group, doorbell asyncRewake/path/timeout, no live bridge, stale receiver, ambiguous/missing cache, missing hook log/writer, failing CLI probes) must each turn it FAIL naming their own cause, plus a positive case pinning that a long-writing CLI is NOT a false negative. Offline/deterministic (deps: bash+node+python3)
./run.sh smoke-agy-install-state # agy MCP + exact permission ownership regression: isolated HOME+XDG, adopt/state/inverse, symlink refuse, setup degrade. Offline/deterministic
./run.sh check-agy-permission-matrix # AGY permission CONTRACT SPACE as a literal table (55 cells): parser-state × operation × settings × ownership × precedence with stated exclusion rules; expectations are hand-written literals, never read from the SUT. Offline/deterministic (deps: python3)
./run.sh check-gate-qualification # kill-proof qualification (the gate-of-gates): runner self-test (classifier truth table + synthetic negatives incl. wrong-reason/hang/control-red/impurity) + committed mutant manifests (scripts/mutants/*.json) run in an isolated snapshot repo under control→mutant→restore→control; the real checkout is never written. Evidence = claim IDs + killed mutant IDs, never assertion counts
./run.sh smoke-agy-statusline-state # agy ambient garden-id statusLine install/doctor/inverse regression. Offline/deterministic
./run.sh smoke-agy-hooks-state # agy PreInvocation birth/sender hook install/doctor/inverse + direct stdin→meta-record regression. Offline/deterministic
./run.sh smoke-user-scope-citizen # 0.12.6 install-boundary: pi packages[] registration SSOT (register-pi-package.py) — idempotent + preserves unrelated + normalizes stale + remove symmetry + fails loud. Offline/hermetic (deps: bash+python3)
./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)
./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)
./run.sh check-meta-manifest-schema # 0.12.2 meta-bridge: CLI-version-INDEPENDENT static guard — plugin manifests pinned to the minimal keyset that validates on the lowest supported Claude (closed-schema regression that broke 0.12.1 install on floor) + desired_mcp installed-vs-clone dual-mode. Offline (deps: python3)
./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
./run.sh install-meta-bridge # INTERNAL part of `setup` (native-harness plugin) + doctor recovery path — prefer `setup`; stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
./run.sh doctor-meta-bridge # THE RELEASE ORACLE (#51, Linux-certified repair axis). exit 0 = every required layer was MEASURED on this Linux host: toolchain + state + plugin/MCP + resolved-artifact launch-form classification (all 3 owner hooks + doorbell static contract) + synthetic owner join + store scan + hook errors + SessionStart evidence + REQUIRED live MCP↔marker join + writer-version parity. Missing live evidence is NOT CERTIFIED (open a Claude session and re-run), never a pass; Darwin is not yet verified/certified and stays nonzero for this cut (future validation may reopen it). Detection power is held by check-meta-doctor-oracle
./run.sh install-agy-bridge # 봉인 7: agy MCP install adapter — register ONE entwurf-bridge server in the agy mcp_config (adopt file / create / REFUSE symlink), stable bin command, install-state under $XDG_DATA_HOME/entwurf/agy-bridge/
./run.sh uninstall-agy-bridge # 봉인 7: honest inverse of install-agy-bridge from install-state (restore preimage / remove key; refuse if config became a symlink)
./run.sh doctor-agy-bridge # fail-loud doctor: MCP config + exact permission rule + state + live probe label
./run.sh install-agy-statusline # own the agy statusLine subtree with bare entwurf-agy-statusline; preserve unrelated settings
./run.sh uninstall-agy-statusline # honest inverse from statusline install-state
./run.sh doctor-agy-statusline # fail-loud statusLine config/bin/state doctor + honest live SKIP
./run.sh install-agy-hooks # #46 agy birth imprint hook — named PreInvocation hook running bare entwurf-agy-imprint, preserving other hooks
./run.sh uninstall-agy-hooks # honest inverse of install-agy-hooks from install-state
./run.sh doctor-agy-hooks # fail-loud doctor for agy hooks.json imprint wiring
./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])
./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only. EXIT CONTRACT (#54, `--help` prints it): 0 complete / 1 NOTHING MOVED (re-run, do not setup) / 2 usage / 3 cut transition incomplete (inspect) / 4 cut complete but residue cleanup failed (`setup` may run; re-run only before new citizen birth, otherwise remove residue manually)
./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)
./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)
./run.sh check-dep-versions # local deterministic check that the pi pin agrees across package.json (devDeps + peer range), run.sh (peer-install pins), and the baseline docs (AGENTS/README/ROADMAP/setup-clean-host/demo)
./run.sh check-node-floor-coherence # binds the Node floor (24+, single axis) across engines.node, run.sh setup preflight, meta-bridge install/doctor judgment logic, clean-host docs, the bridge launcher header, and the CI runner node-version — engines.node is the SSOT, everything else is derived; sweeps tracked contract text for an unregistered declaration
./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm check): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (0.82.x) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES)
./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
./run.sh install [project-dir] # INTERNAL part of `setup` (project .pi/settings.json wiring) + npm-consumer entry — prefer `setup`, don't call directly for dev
./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
./run.sh remove-user-scope # explicit GLOBAL inverse of install's user-scope citizen: drop entwurf from ~/.pi/agent/settings.json packages[] (affects ALL cwds — shared entry, not per-project)
Notes:
- project-dir defaults to current directory
- Claude Code login should already exist (e.g. ~/.claude.json)
- setup's runtime verification is the v2 install smoke (entwurf-bridge); the v2 dispatch substrate is proven live by release-gate
- API key is optional; this bridge is intended to work with Claude Code auth
EOF
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing command: $1" >&2
exit 1
}
}
log() { echo " $*"; }
ok() { echo " ✅ $*"; }
warn() { echo " ⚠ $*"; }
fail() { echo " ❌ $*"; }
section() { echo ""; echo "=== $* ==="; }
normalize_project_dir() {
python3 - "$1" <<'PY'
import os, sys
print(os.path.abspath(os.path.expanduser(sys.argv[1])))
PY
}
sync_auth() {
local auth_path="$HOME/.pi/agent/auth.json"
python3 - "$auth_path" "$PROVIDER_ID" <<'PY'
import json, os, sys
from pathlib import Path
auth_path = Path(sys.argv[1]).expanduser()
provider_id = sys.argv[2]
auth_path.parent.mkdir(parents=True, exist_ok=True)
if auth_path.exists():
data = json.loads(auth_path.read_text())
if not isinstance(data, dict):
raise SystemExit("auth.json is not an object")
else:
data = {}
anthropic = data.get("anthropic")
if not isinstance(anthropic, dict):
print("sync-auth: skipped (no anthropic OAuth credentials in ~/.pi/agent/auth.json)")
raise SystemExit(0)
before = json.dumps(data.get(provider_id), sort_keys=True)
after = json.dumps(anthropic, sort_keys=True)
if before == after:
print(f"sync-auth: already synced ({provider_id})")
raise SystemExit(0)
data[provider_id] = anthropic
backup = auth_path.with_suffix(auth_path.suffix + ".bak")
if auth_path.exists():
backup.write_text(auth_path.read_text())
auth_path.write_text(json.dumps(data, indent=2) + "\n")
print(f"sync-auth: wrote {provider_id} alias to {auth_path}")
if backup.exists():
print(f"sync-auth: backup -> {backup}")
PY
}
# Repo dependency integrity is a HARD requirement for install — NOT gated on
# backend (Claude/ACP) auth. A cloned-but-not-installed or a moved/renamed repo
# has missing or path-stale node_modules; `install` would otherwise happily wire
# settings.json and the failure only surfaces minutes later as a dead MCP bridge
# / pi-extension resolve error (the 2026-06-23 relocation failure: entwurf-bridge
# ✘ Failed to connect + check-entwurf-v2-surface ERR_MODULE_NOT_FOUND). Catch it
# HERE, before any settings file is written (no silent-red), with a package
# manifest access that follows the pnpm symlink to each dep — NOT a real module
# import (per-package "exports" maps forbid that uniformly) and NOT a bare
# `test -d node_modules`, which a dir-move breaks at the symlink-store level
# while the top-level dir still looks present.
# The store gate, in one place (fresh-cut generation policy). Every activation
# surface this repo owns wires up V3-ONLY readers (parse, birth, peers, self,
# v2, inbox, store-doctor). On a host whose meta-record store holds records the
# live schema cannot read — a previous generation, foreign bytes, corruption —
# activating them produces a host that installs clean, validates clean, and then
# rejects at RUNTIME. The active store carries NO cross-generation continuity
# (sessions flow; memory lives in the native transcript + andenken embedding
# axes, never in the bridge), so there is exactly ONE prescription and no
# diagnosis matrix: quiesce → `meta-bridge-fresh-cut` (archive the whole
# generation, open an empty one) → re-run.
#
# READ-ONLY by construction: it asks the store-doctor (fail-loud full scan) and
# reports. It never cuts — archiving a generation is the operator's conscious
# act, never an install side effect.
#
# The verdict is delegated rather than re-derived: store resolution (env +
# default) and the per-record causes live in the store-doctor, and a second
# implementation here would be a second truth that drifts. An absent store and
# a clean v3 store both exit 0, so a clean host needs no special case.
#
# WHAT THIS DOES NOT CLAIM: it is a preflight, not a lock. A session that writes
# a record between this check and the writes that follow is not prevented — the
# contract this proves is the ordering on a QUIESCED host. The rollout procedure
# for a host whose settings point straight at a checkout (quiesce sessions →
# pull → fresh-cut → setup) is the operational half, and it lives in the README.
preflight_v3_store() {
local surface="$1" verdict
# A tree carrying run.sh but not the doctor is a broken checkout/package, not a
# host with a bad store. Say so, rather than letting node's MODULE_NOT_FOUND
# stack stand in for a verdict this gate never reached.
#
# Guard the entry `run_ts` will ACTUALLY run — dev clone .ts, installed package
# the dist .js twin — the way meta-bridge-install.sh already guards its own
# DOCTOR_ENTRY. Checking the source .ts in both modes was a guard over the wrong
# file: an installed package ships scripts/ (so the .ts is present) while a
# broken prepack can leave the compiled twin missing, and there `run_ts` returns
# its OWN 1 — indistinguishable from the doctor's exit 1 "certification defects",
# so the rc branch below prescribed the destructive cut from an artifact failure
# (2026-07-25 closure round; install.sh was already immune).
local doctor_entry
case "$REPO_DIR" in
*/node_modules/*) doctor_entry="$REPO_DIR/mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js" ;;
*) doctor_entry="$REPO_DIR/scripts/meta-bridge-store-doctor.ts" ;;
esac
if [ ! -f "$doctor_entry" ]; then
fail "[$surface] cannot certify the meta-record store: the store-doctor entry this mode runs is missing ($doctor_entry)."
echo " This is a doctor/ARTIFACT failure, not a store verdict — no cut can help and none is prescribed." >&2
echo " Reinstall @junghanacs/entwurf (the package ships the compiled twin via prepack build-bridge), or run 'pnpm run build-bridge' in a dev clone, then re-run this step." >&2
exit 1
fi
# The doctor's EXIT CODE is the verdict (see its header): 1 = certification defects,
# 3 = the store could not be READ. Those take OPPOSITE prescriptions, and this function
# prints the last line the operator (or an agent) acts on — so it must branch on the
# code rather than end every refusal with "archive the generation".
local rc=0
verdict="$(run_ts scripts/meta-bridge-store-doctor.ts 2>&1)" || rc=$?
if [ "$rc" -eq 0 ]; then
echo "[$surface] meta-record store certifies clean v3 ($(printf '%s\n' "$verdict" | tail -1))"
return 0
fi
if [ "$rc" -eq 3 ]; then
printf '%s\n' "$verdict" | head -8 >&2
fail "[$surface] refused BEFORE any write: this host's meta-record store could not be READ (details above)."
echo " Nothing was written. This is an ACCESS problem, not a generation problem — a fresh-cut CANNOT fix it and is not what to run here." >&2
echo " Repair the store path's ownership/permissions, or point ENTWURF_META_SESSIONS_DIR at the intended store, then re-run this step." >&2
exit 1
fi
# Only exit 1 IS a defect list (the doctor's EXIT CONTRACT). Anything else —
# 2 (usage), a node crash (9/134/139), a killed process — means the DOCTOR never
# delivered a store verdict, and prescribing a destructive cut from a crash
# would send the operator at an archive command over a store nobody examined.
if [ "$rc" -ne 1 ]; then
printf '%s\n' "$verdict" | head -8 >&2
fail "[$surface] cannot certify the meta-record store: the store-doctor itself FAILED (exit $rc — neither a defect list nor an access verdict)."
echo " This is a doctor/runtime failure, not a store verdict. Nothing was written. Diagnose the output above (broken checkout/package, bad node runtime), then re-run this step." >&2
exit 1
fi
# Aggregate, never a wall: a large previous-generation store fails every
# record with the same cause, so show the first few + a count (F8 lesson).
printf '%s\n' "$verdict" | head -8 >&2
local nfail
nfail="$(printf '%s\n' "$verdict" | grep -c '^FAIL' || true)"
if [ -n "$nfail" ] && [ "$nfail" -gt 8 ] 2>/dev/null; then
echo " … and $((nfail - 8)) more record(s) with causes (full scan: the store-doctor)" >&2
fi
fail "[$surface] refused BEFORE any write: this host's meta-record store holds entry/entries the live generation cannot certify (details above)."
echo " Every surface this step activates is V3-only, and the store carries no cross-generation continuity — sessions flow; memory lives in the transcript and embedding axes." >&2
echo " Nothing was written. Quiesce this host's sessions, archive the generation with \`./run.sh meta-bridge-fresh-cut\` (from an installed package: \`entwurf meta-bridge-fresh-cut\`), then re-run this step." >&2
exit 1
}
preflight_dep_integrity() {
command -v node >/dev/null 2>&1 || { fail "node not on PATH — cannot verify repo dependency integrity"; exit 1; }
# Each wired pi-extension / MCP bridge / ACP backend root-imports its bundled
# runtime deps at load/spawn time. Assert they actually resolve from the
# package's location BEFORE writing settings, so a broken install fails loud
# here instead of surfacing later as a dead MCP bridge (entwurf-bridge ✘) or an
# ERR_MODULE_NOT_FOUND at runtime.
#
# Resolution must follow Node's OWN algorithm, because the package lives in two
# layouts: a pnpm clone (deps in $REPO_DIR/node_modules) OR a pi-managed
# `pi install npm:@junghanacs/entwurf` (deps HOISTED to an ancestor node_modules,
# e.g. ~/.pi/agent/npm/node_modules, with NO package-local node_modules). A
# cwd-relative `node_modules/<dep>` probe only sees the clone layout and wrongly
# rejects every pi-managed npm install. So walk Node's real module-resolution
# paths (Module._nodeModulePaths) and accessSync each candidate package.json:
# exports-immune (no bare-root / ./package.json import) and hoist-aware, while
# still catching a pnpm dir-move that left the symlink store dangling
# (accessSync follows the link).
#
# Probe set = the BUNDLED runtime `dependencies` only. The `@earendil-works/pi-*`
# peer trio is intentionally EXCLUDED, and the reason depends on the lane — the old
# blanket claim ("the pi loader provides that runtime itself") was true for only one
# of them and is corrected here:
# pi-managed install — pi omits peers (--legacy-peer-deps) and its own loader
# supplies the runtime, so the trio is legitimately absent.
# neutral npm/pnpm install — nothing supplies it. The optional peer is simply
# unresolved (entwurf-preflight.ts:51), and the owned-outcome lane that needs it
# is dead on that host. Excluding the trio from this probe is still right (a hard
# require would reject every consumer install), but it is a KNOWN GAP, not proof
# that the runtime is present by another route.
# pi runtime presence/version is covered by check-pi-runtime-version /
# check-pi-import-surface, neither of which runs on a consumer host.
local probe=(
"@modelcontextprotocol/sdk" "@agentclientprotocol/sdk" "@agentclientprotocol/claude-agent-acp"
"@anthropic-ai/sdk" "zod"
)
local missing=() dep
for dep in "${probe[@]}"; do
if ! (cd "$REPO_DIR" && node -e '
const M = require("module"), fs = require("fs"), path = require("path");
const dep = process.argv[1];
for (const nm of M._nodeModulePaths(process.cwd())) {
try { fs.accessSync(path.join(nm, dep, "package.json")); process.exit(0); } catch {}
}
process.exit(1);
' "$dep") >/dev/null 2>&1; then
missing+=("$dep")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
fail "repo dependency integrity check failed — cannot resolve: ${missing[*]}"
echo " node_modules is missing or path-stale (common right after a clone," >&2
echo " or a repo move/rename — pnpm's symlink store points at the old path)." >&2
echo " Fix: (cd \"$REPO_DIR\" && pnpm install)" >&2
echo " Then re-run ./run.sh install . — settings.json was NOT written." >&2
exit 1
fi
}
preflight_pi_settings_shapes() {
local project_settings="$1"
local user_settings="$2"
python3 - "$project_settings" "$user_settings" <<'PY'
import json, sys
from pathlib import Path
project = Path(sys.argv[1])
user = Path(sys.argv[2])
def load_object(path: Path, label: str):
if not path.exists():
return None
data = json.loads(path.read_text())
if not isinstance(data, dict):
raise SystemExit(f"{label} settings is not a JSON object: {path}")
return data
def check_packages(data, path: Path, label: str):
if data is None:
return
packages = data.get("packages")
if packages is not None and not isinstance(packages, list):
raise SystemExit(f"{label} settings packages is not a JSON array: {path}")
def check_project_provider(data, path: Path):
if data is None:
return
provider = data.get("entwurfProvider")
if provider is None:
return
if not isinstance(provider, dict):
raise SystemExit(f"project settings entwurfProvider is not an object: {path}")
servers = provider.get("mcpServers")
if servers is not None and not isinstance(servers, dict):
raise SystemExit(f"project settings entwurfProvider.mcpServers is not an object: {path}")
project_data = load_object(project, "project")
user_data = load_object(user, "user")
check_packages(project_data, project, "project")
check_packages(user_data, user, "user")
check_project_provider(project_data, project)
PY
}
install_local_package() {
local project_dir agent_dir
project_dir=$(normalize_project_dir "$1")
agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
preflight_dep_integrity
# Pre-cut store gate — repeated here on purpose. `setup` already gated, but
# `run.sh install` reaches this function directly, and this is the step that
# registers entwurf in project + USER scope packages[] (= the V3-only
# extensions load from any cwd). A gate only at the setup entrypoint would be
# bypassed by the documented direct call.
#
# It sits AFTER dependency integrity because both are read-only and a broken
# checkout has to fail as a broken checkout: this gate runs a script out of
# scripts/, so on a tree that has run.sh and nothing else it would otherwise
# preempt the dep verdict with a less true one. Still ahead of every write.
preflight_v3_store install
# Fail BEFORE any settings write if either target config already has a corrupt
# shape. The packages[] SSOT and provider writer run in two separate steps, so
# without this preflight a bad entwurfProvider could leave a half-installed
# packages[] entry behind (2026-07-03 install-boundary hardening).
preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$agent_dir/settings.json"
mkdir -p "$project_dir/.pi"
# packages[] registration via the shared SSOT — same is_entwurf_source
# predicate + idempotency as user-scope and remove (not a substring match).
python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR"
# entwurfProvider.mcpServers.entwurf-bridge (project scope — checkout-local, NO state; #46
# Task 2) via the shared register-pi-provider SSOT: normalize the command to the bare stable
# bin `entwurf-bridge` (ownership-classified: absent/managed-current/managed-legacy adopt, a
# true user-override is left untouched) + prune legacy bundles. project remove is the inverse.
python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$project_dir/.pi/settings.json" "$REPO_DIR" --scope project
register_user_scope_citizen
}
# Register entwurf as a pi USER-SCOPE citizen so its extensions
# (entwurf-control.ts → --entwurf-control / --emacs-agent-socket) load from ANY
# cwd, not only inside the entwurf checkout. project-scope `.pi/settings.json`
# only applies when pi runs inside the repo; the `pit`/`pia`/`pihome` global
# launchers and the npm consumer's "installs → just works" both need the entry
# in ~/.pi/agent/settings.json's packages[]. This is the wiring that dropped when
# `pi install` was removed from setup (2026-07-03: `--entwurf-control` unknown in
# a foreign cwd). Idempotent: absent → append, present → no-op; a stale entwurf
# entry at a different path is normalized to REPO_DIR. Every other package and key
# in the operator's user settings is preserved untouched.
register_user_scope_citizen() {
local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
# Shared idempotent implementation (also driven by smoke-user-scope-citizen).
python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR"
# #46 Task 2: own entwurfProvider.mcpServers.entwurf-bridge as the bare stable bin at USER scope
# (GLOBAL/durable →파급s to every cwd), so its inverse needs an install-state honest inverse
# under $XDG_DATA_HOME/entwurf/pi-provider/ (Task 0/1 discipline). project scope is checkout-
# local and covered by `run.sh remove` (no state) — deliberate, reasoned asymmetry.
local pp_state="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"
python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$pp_state"
}
# The honest inverse of register_user_scope_citizen: drop entwurf from the GLOBAL
# ~/.pi/agent/settings.json packages[]. Deliberately NOT folded into `run.sh remove`
# (project scope): the user-scope citizen is a single GLOBAL entry keyed on this
# checkout and SHARED by every project + every foreign cwd, so tearing it down as a
# side effect of one project's remove would break `--entwurf-control` everywhere else
# (the exact "install → just works from any cwd" invariant register_user_scope_citizen
# exists to hold). Same explicit-global-lifecycle shape as install/uninstall-meta-bridge.
# Uses the same is_entwurf_source SSOT + --remove, so it never over-deletes a look-alike
# (entwurf-notes, openclaw-entwurf) and preserves every other package/key. Idempotent:
# no entwurf entry → no-op. A settings-relative entry naming this repo is preserved and
# reported rather than deleted — install never writes that form (#53 B).
remove_user_scope_citizen() {
local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" --remove
# #46 Task 2: honest inverse of the user-scope entwurfProvider ownership — the install-state
# drives it (absent/managed-* → remove OUR key; a user-override we never owned is untouched).
local pp_state="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"
python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$pp_state"
}
# The ~/.pi/agent/entwurf-targets.json symlink machinery (ensure_agent_dir_symlinks
# + the `setup:links` command) is GONE (#50 C3): the target registry it linked has
# no reader anymore — v2 resumes record-backed citizens and never resolves a spawn
# model from a file. An operator's existing link/copy is inert; nothing reads it.
remove_local_package() {
local project_dir
project_dir=$(normalize_project_dir "$1")
# Same fail-before-write rule as install: remove has two writers too
# (packages[] SSOT, then entwurfProvider cleanup), so a malformed provider must
# not leave a half-removed packages[] entry behind.
preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$(mktemp -u)"
# packages[] cleanup via the shared SSOT — same is_entwurf_source predicate as
# install, so remove never over-deletes a look-alike repo (entwurf-notes, …)
# that install would never have registered. Same rule, one shape further (#53 B):
# register only ever WRITES the absolute path, so a settings-RELATIVE entry naming
# this repo (the committed `".."` in <repo>/.pi/settings.json) is source, not
# install state — remove leaves it and says so instead of editing tracked bytes.
python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR" --remove
# entwurfProvider.mcpServers.entwurf-bridge cleanup (project scope) via the shared SSOT: strip
# our-managed shapes (the bare stable bin AND the legacy repo start.sh path — a true user
# override is left in place) + prune legacy bundles. Mirrors install's ownership predicate so it
# never over-deletes a look-alike, and now also catches the bare bin the Task-2 install writes.
python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$project_dir/.pi/settings.json" "$REPO_DIR" --scope project
# `remove` is project-scope only. The GLOBAL user-scope citizen in
# ~/.pi/agent/settings.json (written by install's register_user_scope_citizen)
# is shared across every project + foreign cwd, so it is left intact here to
# avoid breaking `--entwurf-control` elsewhere. Point the operator at the
# explicit global inverse so the install↔remove asymmetry is never silent.
local agent_settings="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/settings.json"
if python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_settings" "$REPO_DIR" --remove --dry-run 2>/dev/null | grep -q 'would remove'; then
log "note: global user-scope citizen still registered in $agent_settings"
log " run './run.sh remove-user-scope' to remove it (affects ALL cwds)"
fi
}
check_model_lock() {
# Deterministic policy unit test for pi-extensions/model-lock.ts.
# No pi process, no network, no API cost. Mocks ExtensionAPI/Context and
# drives the model_select handler through every quadrant + edge case
# (see scripts/check-model-lock.ts header for the full matrix).
run_ts scripts/check-model-lock.ts
}
check_shell_quote() {
# POSIX-safety gate for the shellQuote helper used in remote SSH command
# builders (entwurf.ts + entwurf-core.ts). Verifies source-string parity
# across the two duplication sites AND behavioral correctness on the
# payload classes that caused the 2026-05-18 remote entwurf incident
# (backtick / $(...) / $VAR / korean tokens). No process spawn, no SSH.
run_ts scripts/check-shell-quote.ts
}
check_entwurf_session_identity() {
# Deterministic gate for the record-era session identity contract: garden-id
# grammar (validator/generator — the RECORD's gardenId shape), readSessionIdentity
# (first model_change authority, drift fail-fast, name-blind — #50 C3), and the
# 🪛 status label. Isolates the sessions base to a temp dir. No backend, no API,
# no spawn.
run_ts scripts/check-entwurf-session-identity.ts
}
check_meta_session() {
# Deterministic gate for the meta-bridge STORE authority (#30 step 2, V3-only
# since the #50 cut — the pure record contract moved to check-meta-v3-record,
# the scan/read seam to check-meta-identity-consumers): idempotent
# existence-keyed decideUpsert + identity-drift
# refusal, and the pre-drilled read-receipt mutators. Pure functions; no
# backend, no hook, no API.
run_ts scripts/check-meta-session.ts
}
check_meta_v3_record() {
# Deterministic gate for the ONE live record schema (v3). Canonical serialize
# + round-trip, v3 mint, the reader rejecting any foreign generation naming
# the fresh-cut command (with the actual version value), and the strict
# keyset (retired/unknown fields are stray, never coerced). Pure functions;
# no backend, no hook, no API.
run_ts scripts/check-meta-v3-record.ts
}
check_mailbox_receipt_state() {
# Deterministic gate for 0.11 Stage 0 step 3B: the mailbox receipt state
# schema + store — the SOLE home of the read-receipt since 3D-4 deleted
# record.delivery with the rest of delivery{} (#50 is V3-only, and v3 never
# had the field). Pure schema round-trip + strict keyset, then the fs store
# (stamp → persist → read-back) in a temp mailbox dir. Schema/store only —
# the live enqueue/read path landed in 3D-4 and is gated by
# check-meta-mailbox-state-write. No backend, no hook, no API.
run_ts scripts/check-mailbox-receipt-state.ts
}
check_entwurf_capabilities() {
# Deterministic gate for 0.11 Stage 0 step 3C: the backend capability source
# (pi/entwurf-capabilities.json) — the SOLE home of wakeMode/deliveryLevel/
# nativeIdLabel since 3D-4 dropped delivery{} from the record (frozen decision
# 1; v3 never carried it). Asserts coverage == META_CITIZEN_BACKENDS (pi included),
# agreement with META_BACKEND_DESCRIPTORS for the three existing backends —
# which since the 3D-3 cut-over survives ONLY as that drift-guard reference —
# and strict keyset/coverage/field crashes. Parser/gate only — the consumer
# seam it feeds is gated by check-meta-capability-source. No backend, no API.
run_ts scripts/check-entwurf-capabilities.ts
}
check_capability_bundle_reach() {
# The gate check-entwurf-capabilities structurally cannot be. That gate imports
# metaCapabilitiesFilePath() from ../pi-extensions/lib/ — the one location where
# its relative path arithmetic is correct — so "the registry resolves" is a
# tautology there. This one DISCOVERS every shipped copy of the module and
# re-asks each from where it actually lives, including the bridge bundle emit at
# mcp/entwurf-bridge/dist/pi-extensions/lib/ that answers the real entwurf_v2.
# Requires a built bridge; a missing dist FAILS (never skips).
run_ts scripts/check-capability-bundle-reach.ts
}
check_bridge_delivery() {
# demo/demo.sh scene 3, recovered as a deterministic gate. demo drove two real pi
# panes so it proved DELIVERY through the installed binary (at model cost); what
# replaced it split in half — matrix-live C2 delivers but from repo source
# in-process, while bridge-boot/pack-install/install-container run the artifact but
# only tools/list. entwurf_v2 through the shipped bundle was never executed once,
# and it shipped dead through 0.12.8-repair.0. This seeds an armed self-fetch
# citizen (C2 recipe) into an env-isolated temp world, then drives the BUILT DIST
# ENTRY as its own process over MCP stdio through a real tools/call entwurf_v2 and
# asserts the .msg physically landed + the doorbell was poked. Seeder is source,
# subject is the artifact, separate processes. No model, no network, no cost.
run_ts scripts/check-bridge-delivery.ts
}
smoke_pi_attach() {
# The #50 C2 checkpoint gate: pi attaches as a meta-record citizen and the BUILT
# ARTIFACT routes that garden id to its control socket. Two halves, deliberately
# different in kind: the record+address half drives `birthPiCitizen` (the exact seam
# entwurf-control's session_start calls) so the gate is deterministic and lives in
# `pnpm check`; the delivery half spawns the dist entry as its own process and speaks
# MCP stdio (check-bridge-delivery's driver, socket-rail fixture). P4 is the one with
# teeth — re-opening the same pi session must ATTACH to the same gardenId, never mint
# a second address under peers that already hold it. P8 (#50 C3 tail) gates goal 3:
# the REAL ACP env enrichment carries the host record's gardenId into the bridge
# child, and a send from that child lands AS the host record identity. That a REAL
# pi process runs the seam is the LIVE axis (smoke-resident-garden-guard), not this one.
run_ts scripts/smoke-pi-attach.ts
}
check_meta_mailbox_state_write() {
# Deterministic gate for 0.11 Stage 0 step 3D-4 commit2 (the cut). Renamed from
# check-meta-mailbox-dualwrite: after the cut the receipt is no longer dual-written
# (there is no record.delivery on the identity record) — it lives SOLELY in the mailbox
# state store. Asserts the meta-record FILE is byte-identical before/after enqueue
# AND read (enqueue/read no longer touch the record — invariant ⑤), the state
# carries lastEnqueuedAt/lastReadAt with field isolation, lastDeliveredAt is never
# invented, an empty inbox is a no-op on BOTH record and state (⑥), and a state
# drift surfaces fail-loud. v2 citizen seeded via upsertMetaSession. No API.
run_ts scripts/check-meta-mailbox-state-write.ts
}
check_meta_receiver_marker() {
# Deterministic gate for the meta-receiver presence marker (SE-2 slice 2b). The
# active-receiver signal a self-fetch backend (Claude Code) needs: a meta-record
# proves a session once existed; this marker proves a live watch owner is still
# there to be woken, so a terminated session's lingering record does not read as a
# ghost active receiver (mailbox garbage). Asserts: write→read round-trip keyed by
# GARDEN id, atomic 0600; dead-owner/pid-reuse start-key guard reads null (distinct
# from "no marker"); armProvenance constrained to the arm-capable events so
# UserPromptSubmit cannot mint presence; reader does NOT gate on record existence
# (recordBacked is the deliverability predicate's fact). Real tmpdir, no API.
run_ts scripts/check-meta-receiver-marker.ts
}
check_hook_launch_topology() {
# #51 gate 1, deliberately built only AFTER the B/B2 verdict. Binds the SHIPPED
# hooks.json to real child topology: every leaf is exec form through the shipped
# hook-launch.sh, the launcher refuses an empty argv (the only visible symptom of an
# older Claude silently dropping `args`), and `exec` preserves the pid so the hook's
# parent is the process that stood in for Claude. Includes a plugin path containing
# space/$/backtick/;& — under exec form that is one opaque argv element, which is
# exactly what the retired shell form could not promise. No API, no live session.
run_ts scripts/check-hook-launch-topology.ts
}
check_meta_identity_consumers() {
# Deterministic gate for the V3-only identity consumer seam (#50 hard cut).
# readMetaIdentityByGardenId reads v3 to identity (drift fail-fast, a
# foreign-generation record names fresh-cut), and certifyActiveStore holds the
# ONE active-store contract the doctor and every writer share: regular files,
# live schema, no body/filename drift, globally unique nativeSessionId (THE G1
# invariant). Each rule is proven on its own store, and a writer refuses even a
# defect that involves neither of its own ids. Temp dir, no API.
run_ts scripts/check-meta-identity-consumers.ts
}
check_meta_capability_source() {
# Deterministic gate for 0.11 Stage 0 step 3D-3: the capability-source cut-over.
# mint/parse now read backend honesty metadata (wakeMode/deliveryLevel) from the
# capability registry (3C) via metaCapabilityFor, NOT META_BACKEND_DESCRIPTORS.
# Proves the seam is registry-DRIVEN (a doctored registry injection is followed),
# mint sources delivery metadata through it, the parse drift guard is now
# registry-sourced, and the cut-over preserves behaviour (registry ≡ const for the
# 3 backends). At 3D-3 only the SOURCE moved and the record.delivery.wakeMode SLOT
# still existed; 3D-4 then deleted that slot with the rest of delivery{}, so today
# the registry is the sole home. check-entwurf-capabilities still owns the registry ≡ const drift
# guard. Pure — no fs writes, no backend, no hook, no API.
run_ts scripts/check-meta-capability-source.ts
}
check_socket_probe() {
# Deterministic gate for 0.11 Stage 0 (F3 fix): three-valued control-socket
# liveness. classifyConnectError is a pure boundary (ECONNREFUSED/ENOENT →
# dead; timeout/EACCES/unknown → indeterminate). GC reclaims dead only;
# indeterminate (a load-stalled live socket) survives the sweep — the F3
# invariant. Listing lists alive only. Pure classify + GC/listing policy +
# a two-socket integration (live listener → alive survives; nonexistent →
# dead GC-eligible). No wire timeout fixture, no backend, no API.
run_ts scripts/check-socket-probe.ts
}
check_project_trust_handler() {
# Deterministic gate for 0.11 Stage 0 (Trust 2층 active-prompt escape): the
# project_trust handler. Pure decideProjectTrust over real preflight outcomes —
# approve/trusted-no-arg→{yes,remember:false}; direct distrust→{no}; inherited
# distrust + interactive + "trust-here"→{yes,remember:true} (THE escape, beats
# the ancestor false); non-interactive (pi -p / rpc)→{undecided}, never prompts;
# never undefined. Plus the thin adapter: fake ctx.ui.select, single-writer
# (handler never calls store.set — pi persists on remember:true), F5a evidence
# in the prompt title. No real UI, no backend, no API.
run_ts scripts/check-project-trust-handler.ts
}
check_entwurf_v2_contract() {
# Deterministic gate for 0.11 Stage 0 step 4-pre: the FROZEN entwurf_v2
# contract (동결결정 10 + 버킷 B F1/F4/F6 + Fable R1-R5). The intent×liveness
# decision table is a constant; the "table cell ↔ dispatch receipt" round-trip
# is asserted exhaustively — THE executable proof F6 demands ("산문 금지").
# R1 control-socket capability domain (currently pi; claude-code/codex/antigravity =
# unsupported, never folded into dead/indeterminate). 6-cell table, single
# verdict per cell (Q2), 2 allow / 4 reject. N1 indeterminate never spawns;
# Q2 owned-outcome+live never auto-sends. R5 taxonomy covers table reasons +
# pre-claims bad-target/untrusted-fail-fast/target-locked (bucket B F2). Plus
# a schema↔types drift guard on the TypeBox input/receipt. Pure, no API.
run_ts scripts/check-entwurf-v2-contract.ts
}
check_entwurf_v2_lock() {
# Deterministic gate for 0.11 Stage 0 step 5a (버킷 B F2): the per-gid dispatch
# LOCK primitive — the only guard against a double-spawn of the same dormant
# target (pi self-guards CREATE but not RESUME, 검증원장 F2). acquire =
# openSync(lockPath,"wx") atomic; a second acquire without release =
# target-locked conflict carrying the holder JSON (F2-P2 human cleanup). release
# = unlink ONLY when the on-disk nonce is still ours (a successor's re-acquire
# survives a late release). Stale reclaim ONLY for same host + ESRCH; EPERM
# (other user's live pid) / different host / alive pid / unknown error all
# fail-closed to conflict. Empty/corrupt lockfile = conflict, never
# auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
# temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
run_ts scripts/check-entwurf-v2-lock.ts
}
check_entwurf_v2_decider() {
# Deterministic gate for 0.11 Stage 0 step 5b: the PURE dispatch decider
# decideDispatch. Drives the frozen 7-step order over INJECTED fakes (target
# lookup / lock / socket inspect+probe / preflight / capability), tracking lock
# acquire+release so "reject ⇒ no plan AND no lock retained" is PROVEN. Covers:
# bad-target/target-locked/target-address-conflict carry observedLiveness=null
# (pre-probe), every other reject + untrusted-fail-fast carry a measured value;
# control-socket send + spawn-bg resume execute KEEP the lock, meta-mailbox send
# takes NO lock (?7); resume plan has no mode/wantsReply/provider/model but has
# expectedSocketPath/observeTimeoutMs/releaseWhen; an invalid gid throws before
# any lookup (F2-P1). Pure, no IO, no API.
run_ts scripts/check-entwurf-v2-decider.ts
}
check_entwurf_v2_matrix() {
# Deterministic gate for 0.11 Stage 0 step 5d-5 (a): the REACHABILITY + LOCK SSOT
# TABLE. Drives the REAL decideDispatch over minimal injected fakes and fixes, as
# one readable table, every (target kind → transport → lock class) cell the 5d-5
# claim covers: bad-target/address-conflict/target-locked rejects, unsupported
# meta-mailbox (deliverable) vs mailbox-undeliverable (inactive) vs owned reject,
# in-domain control-socket (live) / spawn-bg (dormant owned) / released rejects
# (owned-live, ff-dormant, indeterminate, under-lock conflict). A coverage pass
# FAILS if any transport / lock class / pre-probe reject is missing — a dropped
# decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
# parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
run_ts scripts/check-entwurf-v2-matrix.ts
}
check_entwurf_v2_release() {
# Deterministic gate for 0.11 Stage 0 step 5c-1: the PURE release-policy reducer
# (decideReleasePolicy + reduceRelease) for the 5c transport hand. Proves the
# Fable-3 "release-after-observation" timing as a pure state machine BEFORE any
# spawn/send IO: meta-mailbox=never release (no lock), control-socket=release once
# on send-final, spawn-bg=spawn-started is NOT a release event (load-bearing) →
# release on the FIRST observed transition (socket-alive ∨ child-exited any code)
# or a failed start; socket↔exit race idempotent (single release either order);
# decideReleasePolicy enforces the lock-nullness invariant (?7). Pure, no IO.
run_ts scripts/check-entwurf-v2-release.ts
}
check_entwurf_v2_send() {
# Deterministic gate for 0.11 Stage 0 step 5c-2a: the control-socket SEND hand
# (executeControlSocketSend) that WIRES real transport IO onto the 5c-1 release
# reducer. Proves the send->outcome->release ordering over injected fakes (no socket):
# ack->sent / in-band reject->rejected (no fallback) / dead->same-lock one-shot
# re-resolve (control retry or mailbox enqueue)->fallback-sent|rejected|failed /
# indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
# double-delivery on an alive-but-stalled socket). Release fires exactly once per
# send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
run_ts scripts/check-entwurf-v2-send.ts
}
check_entwurf_v2_send_fallback() {
# Deterministic gate for 0.11 Stage 0 step 5c-2b: the same-lock re-resolve RESOLVER
# (resolveDeadControlSendFallback) the 5c-2a hand calls on a dead connect. Proves the
# fire-and-forget re-resolve routing over injected fakes (no filesystem): alive->
# control-socket retry (inspected socketPath) / dead(absent)->reject (dormant-fire-
# forget-unsupported, NEVER spawn-bg) / indeterminate->reject / unsupported+deliverable
# ->meta-mailbox plan (mini-table, no inspect/probe) / unsupported+undeliverable->reject
# / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
# before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
# has NO release seam; every execute plan keeps the held gid and is never spawn-bg.
run_ts scripts/check-entwurf-v2-send-fallback.ts
}
check_entwurf_v2_mailbox() {
# Deterministic gate for 0.11 Stage 0 step 5c-4 (the LAST 5c transport slice): the
# ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + its production
# sendViaMailbox adapter (makeProductionSendViaMailbox). Proves the wiring over an
# injected fake enqueue (no filesystem): sender present -> formatMetaMailboxBody with
# plan.wantsReply threaded (yes/no in body, the deliberate divergence from legacy's
# hard-coded false) / sender absent -> raw plan.message / enqueue opts EXACTLY
# {gardenId: plan.targetGardenId, body, sessionsDir, mailboxDir} (no re-derivation) /
# enqueue throw PROPAGATES (never folded into success:false — a mailbox has no in-band
# refuse) / success -> {success:true}. Production adapter resolves {success:true},
# consults senderProvider once, and NEVER touches the lock (a poison LockClaim whose
# every access throws still resolves). Source guard: the lib code has NO release seam
# and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
# re-route is structurally impossible.
run_ts scripts/check-entwurf-v2-mailbox.ts
}
check_entwurf_v2_native_push() {
# Deterministic gate for 봉인 3/4: the native-push SEND hand (deliverViaNativePush +
# makeNativePushSend), the executor half of the native-push rail — where the 1-shot retry
# lives (moved out of the adapter leaf). Proves over a fake adapter (no agy/socket): success
# first try -> {retried:false}, ONE send over the planted route, ZERO re-probe; fail ->
# re-probe alive -> re-send success -> {retried:true}, TWO sends, the 2nd over the RE-
# DISCOVERED route; re-send FAIL -> throws (no 3rd attempt); re-probe dead/indeterminate ->
# throws (not retried), NO second send. makeNativePushSend resolves the adapter from
# plan.backend and IGNORES the lock (lock-free rail).
run_ts scripts/check-entwurf-v2-native-push.ts
}
check_entwurf_v2_runner() {
# Deterministic gate for 0.11 Stage 0 step 5d-1: the execute-router (executeDispatch) that
# routes an already-decided DispatchDecision to its 5c transport hand and maps the outcome
# to one outcome-rich EntwurfV2RunResult. Proves over injected fake hands (no socket/spawn/
# timer): reject -> rejected (receipt+diagnostic carried, NO hand called) / control-socket ->
# sendControl(plan, lock) / spawn-bg -> resumeSpawnBg (socket-alive AND lock-retained both
# ride `executed`, fail-closed is not a failure) / meta-mailbox -> sendMailbox(plan, NULL
# lock, ?7). Carry-overs: N3 control `rejected` carries rejectReason verbatim; N1
# SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
# retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
# finalizedOutcome. Exactly one hand runs per execute.
run_ts scripts/check-entwurf-v2-runner.ts
}
check_entwurf_v2_surface() {
# Deterministic gate for 0.11 Stage 0 step 5d-3a: the ctx-free surface adapter
# (entwurf-v2-surface.ts) + the entwurf-control.ts wiring contract. Proves the pure parts:
# toDispatchInput (wants_reply→wantsReply, absent mode/wants_reply undefined) / renderEntwurfV2Result
# per result kind ({text,isError} surfacing reject diagnostic, control N3 rejectReason, spawn
# lock-retained, N1 delivered-but-dirty) / surface ctx-free source guard / entwurf-control
# registers entwurf_v2 + reaches the fence via a NON-LITERAL dynamic import (no static fence
# import → TS5097 stays closed) + decorates sender origin:pi-session/replyable:true.
run_ts scripts/check-entwurf-v2-surface.ts
}
check_entwurf_bridge_boot() {
# Deterministic gate for 0.11 step 5d-5-pre (G1a/G1b): boots the entwurf-bridge MCP server
# as it ships (start.sh → node --experimental-strip-types, no build) and asserts what the
# source-shape gate check-entwurf-v2-surface cannot — that the whole v2 fence graph LOADS at
# boot under strip-types (G1a: a parseable tools/list proves it) and that entwurf_v2 is
# registered on the runtime surface with its schema (G1b). tools/list only → no tools/call,
# no lock/fs side effect, no auth → safe in pnpm check. Broad protocol/negative suite stays
# in check-bridge/test.sh (D1=A안).
run_ts scripts/check-entwurf-bridge-boot.ts
}
check_entwurf_bridge_pi_free() {
# 0.12.1 A-gate (static half): the entwurf-bridge MCP server must boot WITHOUT any
# pi package. entwurf is a harness-neutral npm package; pi is one optional adapter
# lane, not a boot dependency. Walks the EAGER static value-import closure of
# mcp/entwurf-bridge/src/index.ts and fails if any reachable module statically
# value-imports @earendil-works/pi-*. Type-only imports and dynamic `await import()`
# (the intended lazy preflight boundary) are excluded — the runtime boot smoke is the
# final authority that peers/self/list/mailbox-deliver come up pi-free.
run_ts scripts/check-entwurf-bridge-pi-free.ts
}
check_entwurf_v2_production() {
# Deterministic gate for 0.11 Stage 0 step 5d-2b: makeProductionEntwurfV2Deps — the ctx-free
# PRODUCTION assembly of runEntwurfV2's deps. Proves the wiring over fake leaf-IO spies (no
# real socket/lock/spawn/meta-record): decide wraps decideDispatch and acquires under the
# wired lockDir / control sendOverSocket builds the RpcSendCommand + maps + releases under
# lockDir / QB3 the spawn watcher releases via the SHARED lockDir release (not the spawn
# factory default) / the mailbox hand enqueues onto the wired dirs / a dead control send
# re-resolves to the SAME sendViaMailbox instance on the SAME dirs (Q3+Q5 no drift).
run_ts scripts/check-entwurf-v2-production.ts
}
check_entwurf_control_rpc() {
# Gate for 0.11 Stage 0 step 5d-2 (RPC-helper extraction micro-slice): the --entwurf-control
# socket protocol (wire types + the newline-JSON client sendRpcCommand) moved to the ctx-free
# SSOT lib/entwurf-control-rpc.ts behaviour-preservingly. Proves: lib is ctx-free (no
# ExtensionContext/ExtensionAPI/@earendil-works/pi-ai) / entwurf-control.ts imports
# sendRpcCommand from the lib and no longer defines its own / real short unix-socket round-trip
# (write command -> matched {type:response,command,success:true} -> resolve) / close-before-
# response rejects 'connection closed before response'. net.Server only, no model/pi process.
run_ts scripts/check-entwurf-control-rpc.ts
}
check_entwurf_v2_spawn() {
# Deterministic gate for 0.11 Stage 0 step 5c-3a: the spawn-bg RESUME watcher hand
# (executeSpawnBgResume) wiring spawn + socket-observe IO onto the 5c-1 reducer. Proves
# Fable-3 over injected deferred promises (no real child/socket/timer): TIMEOUT IS NOT A
# RELEASE — a bare observeTimeout escalates to killChild (release 0), then a BOUNDED
# killGrace waits for a real socket-alive / child-exited to release. socket-alive ∨
# child-exited(any code incl. null) -> release exactly once; the loser settling later is a
# no-op. spawnChild throw -> spawn-start-failed (release, nothing to watch). No observation
# obtainable (grace elapses, or a post-spawn watch dep throws and the exit can't be
# observed) -> lock-retained fail-closed (released:false, pid/socket/lockPath surfaced) —
# there is NO direct-release hatch; deps.releaseLock is reached ONLY via reduceRelease.
run_ts scripts/check-entwurf-v2-spawn.ts
}
check_entwurf_resume_args() {
# Deterministic gate for 0.11 Stage 0 step 5c-3b: the resume-argv SSOT
# (buildResumePiArgs). v2-control is the only shipped shape (the legacy one-shot variant and
# its launcher were removed 2026-07-27). Pins the resident posture: `--entwurf-control`
# present + `--no-extensions` never (the keep-alive is the goal — a resumed session stands
# its socket up and stays addressable), the headless `--mode json -p` prefix and the
# prompt-as-turn positional (`-p` NOT dropped), explicitExtensionArgs preserved exactly once
# (provider-resolution footgun #29), plan.launchArgs (--approve) before the prompt, and a
# null provider emitting no --provider.
run_ts scripts/check-entwurf-resume-args.ts
}
check_entwurf_v2_spawn_production() {
# Deterministic gate for 0.11 Stage 0 step 5c-3c: the production SpawnBgResumeDeps factory
# (makeProductionSpawnBgResumeDeps) wiring the 5c-3a watcher's six IO seams onto the real
# world — proven WITHOUT a real pi spawn/socket/timer (that is the opt-in
# smoke-entwurf-v2-spawn-live, kept OUT of pnpm check). socketWatchVerdict (R2 policy):
# address-conflict→forged (reject, never wait), alive→alive, dead/indeterminate→wait.
# spawnChild builds the v2-control argv (--entwurf-control, no --no-extensions, -p+prompt,
# --approve, ext/provider/model, header cwd). awaitSocketAlive: connectable resolves, forged
# (symlink) rejects without connecting, dead→wait→alive, abort clears the sleep. awaitChildExit
# resolves the code + removes the listener on abort. awaitTimeout schedules + abort-clears.
# killChild=SIGTERM; releaseLock delegates; a proc-less child fails loud (mis-wire).
run_ts scripts/check-entwurf-v2-spawn-production.ts
}
smoke_entwurf_v2_spawn_live() {
# LIVE phase gate for 0.11 Stage 0 step 5c-3c (D5) — kept OUT of `pnpm check`. Exercises the
# production SpawnBgResumeDeps against REAL OS objects (a real unix socket, real child
# processes, real timers, real abort teardown) to catch what the deterministic gate's fakes
# cannot: actual spawn/exit/error event semantics, real lstat+connect liveness, and the
# 5c-3a watcher's timeout→kill→child-exited→release integration on a live process. It does
# NOT spawn a real `pi --entwurf-control` resume (that is the 5d surface matrix). Run once
# before 5d and record the result: LIVE=1 ./run.sh smoke-entwurf-v2-spawn-live
if [ "${LIVE:-}" != "1" ]; then
echo "[smoke-entwurf-v2-spawn-live] skipped — set LIVE=1 to run (spawns real children + opens a real unix socket)."
return 0
fi
run_ts scripts/smoke-entwurf-v2-spawn-live.ts
}
smoke_acp_socket_citizen_live() {
# S1 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
# Spawns a REAL `pi --entwurf-control` resident on an ACP model
# (entwurf/claude-opus-5) and proves it is a first-class socket-citizen:
# the control socket stands up, get_info answers with the ACP model (model-lock
# did NOT revert — QM1), idle/cwd are reported, and the fail-loud streamSimple
# stub never fires (turn-free launch — QM2). No prompt is sent: S1 proves
# citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
# Model override: ENTWURF_S1_MODEL (default claude-opus-5).