@junghanacs/entwurf 0.23.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AGENTS.md +3 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +134 -0
  4. package/VERIFY.md +41 -0
  5. package/docs/acp-backend-rail.md +96 -17
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  10. package/package.json +8 -7
  11. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  12. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  13. package/pi-extensions/lib/acp/backend.ts +4 -4
  14. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  15. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  16. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  17. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  18. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  19. package/run.sh +192 -196
  20. package/scripts/check-acp-usage-accounting.ts +9 -9
  21. package/scripts/check-gate-qualification.ts +1 -0
  22. package/scripts/check-pi-launch.ts +358 -0
  23. package/scripts/check-release-gate-outcomes.ts +7 -0
  24. package/scripts/inventory-verification-surface.ts +86 -10
  25. package/scripts/mutants/herdr-plugin-build.json +1 -1
  26. package/scripts/mutants/pi-launch.json +113 -0
  27. package/scripts/mutants/resume-args.json +6 -6
  28. package/scripts/mutants/v2-surface.json +2 -2
  29. package/scripts/mutants/v2-visible-resume.json +17 -17
  30. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  31. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  32. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  33. package/scripts/tsconfig.json +5 -0
  34. package/scripts/check-entwurf-facts.ts +0 -391
  35. package/scripts/check-entwurf-resume-args.ts +0 -122
  36. package/scripts/check-entwurf-v2-decider.ts +0 -657
  37. package/scripts/check-entwurf-v2-lock.ts +0 -371
  38. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  39. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  40. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  41. package/scripts/check-entwurf-v2-release.ts +0 -151
  42. package/scripts/check-entwurf-v2-runner.ts +0 -448
  43. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  44. package/scripts/check-entwurf-v2-send.ts +0 -458
  45. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  46. package/scripts/check-socket-discovery.ts +0 -401
@@ -60,8 +60,8 @@ export type AcpPiStreamState = {
60
60
  *
61
61
  * Last write wins, never a sum: both are latest session-level observations, and
62
62
  * one turn can legitimately see several (claude emits one per `result` message,
63
- * including a sub-agent's own — read at claude-agent-acp 0.76.0
64
- * `dist/acp-agent.js:3471-3486`), each carrying that result's current values.
63
+ * including a sub-agent's own — read at claude-agent-acp 0.79.0
64
+ * `dist/acp-agent.js:3504-3519`), each carrying that result's current values.
65
65
  */
66
66
  observedSessionCostUsd?: number;
67
67
  observedContextOccupancyTokens?: number;
@@ -346,9 +346,9 @@ export function applyAcpSessionUpdate(
346
346
  case "usage_update": {
347
347
  // `used` is OCCUPANCY-shaped — the backend's post-turn context size, not
348
348
  // the prompt response's turn aggregate. Claude sends `lastAssistantTotalUsage`
349
- // as `used` (read at claude-agent-acp 0.76.0
350
- // `dist/acp-agent.js:3871-3882`), after constructing that scalar from the
351
- // latest assistant snapshot (`:3853-3866`). pi reads `usage.totalTokens` as
349
+ // as `used` (read at claude-agent-acp 0.79.0
350
+ // `dist/acp-agent.js:3934-3945`), after constructing that scalar from the
351
+ // latest assistant snapshot (`:3916-3929`). pi reads `usage.totalTokens` as
352
352
  // exactly that occupancy (`calculateContextTokens(usage) = usage.totalTokens
353
353
  // || input + output + cacheRead + cacheWrite`, read at pi-coding-agent
354
354
  // `dist/core/compaction/compaction.js:86-88`). The assignment below is the
@@ -154,7 +154,8 @@ export function buildClaudeSessionMeta(
154
154
  // claude-agent-acp also accepts a STRING PATH, and at 0.76.0 the readFile +
155
155
  // JSON.parse for that form moved out of `resolvedProvider` up onto the
156
156
  // unconditional `session/new` path — a shape that WOULD reach us, and does not,
157
- // only because we never hand it a path. Keep it inline: a path would put file
157
+ // only because we never hand it a path. Still on that path at 0.79.0
158
+ // (`dist/acp-agent.js:6010-6012`). Keep it inline: a path would put file
158
159
  // IO, and its failure modes, inside every session creation.
159
160
  settings: {
160
161
  permissions: {
@@ -111,7 +111,7 @@ export type DispatchLiveness = (typeof DISPATCH_LIVENESSES)[number];
111
111
  // unreachable (the native-push rail measures it — entwurf-v2-decider.ts). Widening
112
112
  // THIS set is a deliberate future decision, gated by a real compatible
113
113
  // control-socket predicate — never by silently mapping sessions to dead/indeterminate
114
- // (R1 핵심). check-entwurf-facts pins this == ["pi"] and asserts the native-push
114
+ // (R1 핵심). pi-extensions/lib/entwurf-facts.test.ts pins this == ["pi"] and asserts the native-push
115
115
  // domain is disjoint from it.
116
116
  export const LIVENESS_DOMAIN_BACKENDS = ["pi"] as const;
117
117
  export type LivenessDomainBackend = (typeof LIVENESS_DOMAIN_BACKENDS)[number];
@@ -125,7 +125,7 @@ export function isLivenessSupported(backend: string): boolean {
125
125
  // live vendor app-server target probe), NOT a control socket. This domain is DISJOINT
126
126
  // from LIVENESS_DOMAIN_BACKENDS: an Antigravity or Codex session is `unsupported` on
127
127
  // the socket FACT axis (entwurf_peers) yet independently measured on the native-push
128
- // axis. The two are separate rails on purpose — check-entwurf-facts pins both sets
128
+ // axis. The two are separate rails on purpose — pi-extensions/lib/entwurf-facts.test.ts pins both sets
129
129
  // and asserts their intersection is ∅ (a backend can never be in both domains).
130
130
  export const NATIVE_PUSH_BACKENDS = ["antigravity", "codex"] as const;
131
131
  export type NativePushBackend = (typeof NATIVE_PUSH_BACKENDS)[number];
@@ -82,7 +82,7 @@ export const LOCK_SUFFIX = ".lock";
82
82
  export const LOCK_OWNER = "entwurf_v2" as const;
83
83
 
84
84
  /** The reject reason a lock conflict maps to. Kept as a literal here (the lock
85
- * primitive stays decoupled from the full contract); `check-entwurf-v2-lock`
85
+ * primitive stays decoupled from the full contract); `pi-extensions/lib/entwurf-v2-lock.test.ts`
86
86
  * cross-checks it against the contract's ENTWURF_V2_REJECT_REASONS so the two
87
87
  * cannot drift. */
88
88
  export const LOCK_CONFLICT_REASON = "target-locked" as const;
@@ -201,7 +201,7 @@ export async function executeDispatch(
201
201
  /**
202
202
  * 5d-2a: the deps `runEntwurfV2` joins. `decide` is the WHOLE decider as ONE injected
203
203
  * function — NOT `DispatchDeciderDeps`. The runner does not re-validate the 5b decider
204
- * logic (that is `check-entwurf-v2-decider`'s job); it proves only the decide→execute
204
+ * logic (that is `pi-extensions/lib/entwurf-v2-decider.test.ts`'s job); it proves only the decide→execute
205
205
  * COMPOSITION contract over a fake `decide`. Production wraps the real decider as
206
206
  * `decide: (input) => decideDispatch(input, productionDeciderDeps)` (assembled in 5d-2b),
207
207
  * so the runner stays gate-provable without any decider IO seam leaking in.
package/run.sh CHANGED
@@ -158,29 +158,17 @@ Usage:
158
158
  ./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
159
159
  ./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
160
160
  ./run.sh check-entwurf-v2-contract # FROZEN entwurf_v2 contract — control-socket liveness domain is currently pi; self-fetch/native-push citizens are out of that domain, not globally unsupported; pure, no IO
161
- ./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
162
- ./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
163
- ./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/native-push + bad-target/conflict/locked/undeliverable/dormant/indeterminate rejects), coverage pass fails on a dropped cell; thin coverage not a decider re-impl; pure, no IO
164
- ./run.sh check-entwurf-v2-release # deterministic gate (0.11 Stage 0 step 5c-1): PURE release-policy reducer (decideReleasePolicy + reduceRelease) — the spawn-observation policy and its four release events went with the transport in the visible-first cut, so the shipped machine is meta-mailbox=never release (no lock) + control-socket=release once on send-final; decideReleasePolicy still enforces the lock-nullness invariant; pure, no IO
165
- ./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
166
161
  ./run.sh check-compaction-send-guard # deterministic gate (#111): control-socket send during Pi compaction — event-armed refuse compacting, quiet unknown non-idle refuse busy (ctx.signal is not isStreaming); no pi.sendMessage, no delivered:true; idle/live-run steer/followUp preserved; pure, no IO
167
- ./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 (nothing is ever launched), 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)
168
- ./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/mailbox→matching hand with decision.lock verbatim / N3 rejectReason carried / N1 SendDeliveredReleaseFailedError→execution-failed{finalizedOutcome,releaseFailed,retrySafe:false}; fake hands, no IO
169
- ./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
170
- ./run.sh check-entwurf-resume-args # deterministic gate: resume-argv SSOT (buildResumePiArgs) for the S1 VISIBLE resume. The headless shape was measured wrong for a window before the consumer was written (`-p` is pi's own non-interactive mode), so the one shipped posture is `--entwurf-control` FIRST + ext args exactly once + `--session <abs file>` + optional `--provider` + `--model <m>` — and the gate pins the ABSENCES as hard as the presences: no --mode, no -p, no positional prompt (a resume runs no turn), no --no-extensions, never --session-id (which MINTS a session instead of resuming one)
171
162
  ./run.sh check-mux-resume-call # deterministic gate: S1 resume PLACEMENT composition (mux-resume-call.ts). No fake tmux. cwd rules are MEASURED tmux 3.6a behaviours, each a way a resume looks successful while being wrong: a nonexistent `-c` exits 0 and lands the child in $HOME (so it is refused HERE), `-c` is FORMAT-EXPANDED so `#{…}` silently rewrites the path and `#(…)` was observed running a command (so `#` is refused), and whitespace measured SAFE (so no escaping layer is owed). Also pins `-c` reaching tmux, runtime after `--`, carrier-free argv, zero identity in this module, and the surface seam that keeps the v2 composition from importing mux
172
163
  ./run.sh check-mux-parent-artifact # deterministic gate for the tracked scrubbed parent-transcript fixture — a version-pinned sample of the PARENT-SIDE shape (fresh_call toolResult + the later callback custom_message) so downstream never opens a private transcript. Pins event order, the toolCallId join on the RESULT (pi writes no separate toolCall row), the launch nonce reappearing verbatim in the callback body, the <sender_info> envelope field names, and the absence of operator paths / real garden ids / real uuids. NOT placement evidence
173
164
  ./run.sh check-mux-launcher-fence # deterministic gate for the shared operator-launcher fence (issue #67): scripts/lib/claude-launcher-fence.ts + its wiring into BOTH mux LIVE smokes. Replants the observed install-destruction shape (real HOME + fixture XDG_DATA_HOME → self-update retargets the real `claude` launcher into the fixture tree, teardown deletes it) wholly inside disposable mkdtemp roots — the real launcher is never inspected. Pins fail-closed preflight, retarget/content-change detection before cleanup, removal BLOCKED on fixture reference / unproven safety / surviving tracked panes, exact operator-parity XDG restore (absent = DELETED, not canonical defaults), the lifecycle cell-branch topology, and one shared helper consumed by both smokes
174
165
  ./run.sh check-typing-call-fence # deterministic gate for the boundary nobody has broken: this product composes ARGUMENTS and LETTERS, never keystrokes. Scans every tracked production source under pi-extensions/ and mcp/ — comments blanked, string literals kept — for tmux send-keys/paste-buffer/load-buffer and herdr's PTY verbs (agent.prompt, agent.send_keys, pane.send_text, pane.input.set). Prose is exempt on purpose: the sentence stating the rule names the very calls it forbids, and a scanner that read prose could be made green by deleting the law. Carries its own oracle — the detector is proven on planted code, proven blind to comments, and the scanned inventory is measured
175
- ./run.sh check-entwurf-v2-visible-resume # deterministic gate: S1 visible-resume COMPOSITION (entwurf-v2-visible-resume.ts) with every seam injected — the whole state machine incl. the timeout branch runs with no tmux/lock/socket/clock. Pins lock BEFORE liveness, identity under the lock and before any window (no-transcript citizen fails loud, opens nothing), live/indeterminate/address-conflict refused unlaunched, observation as a BOUNDED WAIT (measured: socket answers ~2–4s after launch, so one immediate probe would call a successful resume unobserved), exactly ONE launch on every path, timeout → lock released + window left open + nothing retried/killed, failed release throws, and the two receipts staying separate in type and text
176
166
  ./run.sh check-resume-launch-identity # deterministic gate for resume-launch-identity.ts, the record-authoritative launch-identity leaf preserved through the visible-first cut (spawn-bg and all its callers are gone; this leaf answers "which being is this, and which conversation is theirs"). Temp meta-store fixture: gardenId→record.transcriptPath happy path with header cwd/provider/model; C3 integrity (header id ≠ record.nativeSessionId → refused, never resumed); #52 ADDRESSABLE read (a gid that no longer holds its nativeSessionId alone is refused from EITHER side — the plain targeted read would resume one transcript twice under two locks); cause fidelity per impossible resume incl. the F7 pin (recorded-but-deleted transcript → MISSING, not "no recorded model"); header↔gate SSOT. No spawn/socket/timer
177
167
  ./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.6-luna). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
178
168
  ./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), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). COST FENCE: open that agy conversation on gemini-3.6-flash (free account) — never a Pro tier; entwurf never selects the agy model and no assertion reads it. LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
179
169
  ./run.sh smoke-codex-native-push-live # on-demand LIVE acceptance for Codex native-push — needs LIVE=1 + CODEX_LIVE_THREAD_ID loaded by a visible TUI attached to the default app-server; probes the real UDS, routes through production runEntwurfV2, queues one exact-token turn without retry, re-probes, and leaves the visible TUI to show the model result
180
170
  ./run.sh smoke-codex-fresh-live # #95 RELEASE MUST: record-backed receipt fixture -> PUBLIC initial Pi fresh/callback in a session S that is NOT the app-server's -> Pi PUBLIC omitted-placement Codex fresh/exact callback, also in S -> Pi-to-Codex addressed native-push v2 -> Codex PUBLIC omitted-placement AND omitted-cwd fresh Pi/exact callback IN S beside its own TUI (lane B claim: the app-server env names another session, so S can only come from the caller's pane title; lane C claim: no cwd was asked for, so the directory can only come from the Codex caller's own record — and the Codex thread's own rollout session_meta.cwd, its pane and its record must all be the requested scratch, not the app-server's repo) -> Codex final v2 evidence. Needs LIVE=1 + ENTWURF_CODEX_APP_SERVER_PID + ENTWURF_CODEX_FRESH_MODEL + ENTWURF_CODEX_FRESH_PI_MODEL, and its ONE stable launch directory answered 'Trust' once in a plain codex -C there: codex records its direct consent per exact directory, so a directory with no answer at all opens a consent screen instead of a first turn. Only THIS gate treats that as a precondition and fails up front as codex-launch-cwd-undecided rather than timing out on a callback — an ordinary entwurf_fresh_call only prints the note and opens the window, because a human can answer the screen. The smoke PRINTS that directory as 'launch-cwd <dir>' and repeats it in the failure as a ready-to-run repair command — do not retype it from memory, and do not write it as \$TMPDIR/... (os.tmpdir() falls back to /tmp when TMPDIR is unset, so that spelling names a different directory in the filesystem root). The PID names the operator-owned app-server, which must sit in a DIFFERENT session from S; the smoke checks/prints all four coordinates, never starts/stops/guesses the app-server/session, never reads screen text, interrupts only its exact still-running Codex turn, and cleans only receipt-named window ids
181
171
  ./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume refused target-not-pi, no window opened and no lock residue. LIVE=1 ./run.sh smoke-mux-lifecycle-live
182
- ./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
183
- ./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
184
172
  ./run.sh check-meta-facts # deterministic gate for the meta-facts projection (#65): drives the REAL CLI — full-record join, parse-before-uniqueness, no-winner duplicates, drift/symlink/invalid-UTF-8 defects in-band, deterministic bytes, exit contract 0/2/3, dispatch+emit reachability
185
173
  ./run.sh check-herdr-plugin # deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`): STATIC manifest shape for herdr 0.9.0 + the forbidden sections ([[startup]]/[[events]]/[[actions]]/[[link_handlers]]) asserted absent ([[build]] is check-herdr-plugin-build's subject since #116 M3-b3); BEHAVIOURAL drive of the REAL pane entry against a stub `entwurf` and stub HERDR_BIN_PATH that LOG every call — exactly one peer-facts + one agent list per open (counted, not claimed), skip-by-name when Entwurf is absent, four distinct named refusals instead of an empty table, ambiguous never first-wins, diagnostics shown, activity in its own column, and zero writes to the plugin state/config dirs. No herdr binary, no Entwurf install
186
174
  ./run.sh check-herdr-runtime-bootstrap # deterministic gate for the #116 M3-b1 runtime leaf (`plugins/herdr/lib/runtime-bootstrap.mjs`): the Entwurf-owned stable active root as a REAL directory (nothing is put on PATH — the scoped wiring above names ABSOLUTE commands under it), ownership decided BEFORE the first mkdir, disk facts from lstat (a dangling symlink is not 'absent' and a link into another tree is not a directory), exact name@version + compiled entry + all three required bins present AND executable + a real `check-bridge`, the plugin-owned artifact lock coherent with the checkout, the owned npm cache and --ignore-scripts, a CERTIFIED journal (non-object/array/scalar/blank-identity/phase-contradicting-digest all refused) as the only ownership proof, all EIGHT active/staging/previous combinations named, the last good runtime never lost (torn swap AND corrupt-active beside a good backup), prior provenance carried across an install that may not finish, a failed candidate leaving the running runtime byte-identical, idempotent same-spec reinstall, a journal believed only while the disk backs it, a preflight-then-mutate inverse taking runtime last, and the refusal to record Herdr's commit as ours. NETWORK ZERO, npm ZERO — it drives a FIXTURE package, so the ACTUAL package proof lives in check-pack-install
@@ -1095,6 +1083,14 @@ check_copilot_launch() {
1095
1083
  run_ts scripts/check-copilot-launch.ts
1096
1084
  }
1097
1085
 
1086
+ check_pi_launch() {
1087
+ # #118 홉 2. Same oracle as check-copilot-launch and for the same reason: the subject is
1088
+ # a process replacement, so a fake vendor named `pi` on a sandbox PATH reports the argv,
1089
+ # env and pid it was handed — never a reading of the launcher's source. Also carries the
1090
+ # --entwurf-control literal cross-check, because the shell cannot import the TS constant.
1091
+ run_ts scripts/check-pi-launch.ts
1092
+ }
1093
+
1098
1094
  check_codex_app_server_launch() {
1099
1095
  # #95: the managed app-server SPELLING, proved against a fake vendor. There is no second
1100
1096
  # spelling of the address to compare any more — the launcher asks `codex-socket-path` — so
@@ -1185,123 +1181,28 @@ check_entwurf_v2_contract() {
1185
1181
  run_ts scripts/check-entwurf-v2-contract.ts
1186
1182
  }
1187
1183
 
1188
- check_entwurf_v2_lock() {
1189
- # Deterministic gate for 0.11 Stage 0 step 5a (버킷 B F2): the per-gid dispatch
1190
- # LOCK primitive — what serializes concurrent in-domain dispatch at one
1191
- # target (pi self-guards CREATE but not RESUME, 검증원장 F2). acquire =
1192
- # openSync(lockPath,"wx") atomic; a second acquire without release =
1193
- # target-locked conflict carrying the holder JSON (F2-P2 human cleanup). release
1194
- # = unlink ONLY when the on-disk nonce is still ours (a successor's re-acquire
1195
- # survives a late release). Stale reclaim ONLY for same host + ESRCH; EPERM
1196
- # (other user's live pid) / different host / alive pid / unknown error all
1197
- # fail-closed to conflict. Empty/corrupt lockfile = conflict, never
1198
- # auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
1199
- # temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
1200
- run_ts scripts/check-entwurf-v2-lock.ts
1201
- }
1202
-
1203
- check_entwurf_v2_decider() {
1204
- # Deterministic gate for 0.11 Stage 0 step 5b: the PURE dispatch decider
1205
- # decideDispatch. Drives the frozen 7-step order over INJECTED fakes (target
1206
- # lookup / lock / socket inspect+probe / preflight / capability), tracking lock
1207
- # acquire+release so "reject ⇒ no plan AND no lock retained" is PROVEN. Covers:
1208
- # bad-target/target-locked/target-address-conflict carry observedLiveness=null
1209
- # (pre-probe), every other reject carries a measured value; a control-socket send
1210
- # executes KEEPING the lock, meta-mailbox send takes NO lock (?7); an invalid gid throws before
1211
- # any lookup (F2-P1). Pure, no IO, no API.
1212
- run_ts scripts/check-entwurf-v2-decider.ts
1213
- }
1214
-
1215
- check_entwurf_v2_matrix() {
1216
- # Deterministic gate for 0.11 Stage 0 step 5d-5 (a): the REACHABILITY + LOCK SSOT
1217
- # TABLE. Drives the REAL decideDispatch over minimal injected fakes and fixes, as
1218
- # one readable table, every (target kind → transport → lock class) cell the 5d-5
1219
- # claim covers: bad-target/address-conflict/target-locked rejects, unsupported
1220
- # meta-mailbox (deliverable) vs mailbox-undeliverable (inactive) reject,
1221
- # in-domain control-socket (live) / released rejects
1222
- # (ff-dormant, indeterminate, under-lock conflict). A coverage pass
1223
- # FAILS if any transport / lock class / pre-probe reject is missing — a dropped
1224
- # decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
1225
- # parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
1226
- run_ts scripts/check-entwurf-v2-matrix.ts
1227
- }
1228
-
1229
- check_entwurf_v2_release() {
1230
- # Deterministic gate for 0.11 Stage 0 step 5c-1: the PURE release-policy reducer
1231
- # (decideReleasePolicy + reduceRelease) for the 5c transport hand. Proves the
1232
- # Fable-3 "release-after-observation" timing as a pure state machine BEFORE any
1233
- # send IO: meta-mailbox=never release (no lock), control-socket=release once
1234
- # on send-final. The spawn-observation policy and its four release events went with
1235
- # the transport in the visible-first cut; decideReleasePolicy still enforces the
1236
- # lock-nullness invariant (?7). Pure, no IO.
1237
- run_ts scripts/check-entwurf-v2-release.ts
1238
- }
1239
-
1240
1184
  check_entwurf_v2_send() {
1241
- # Deterministic gate for 0.11 Stage 0 step 5c-2a: the control-socket SEND hand
1242
- # (executeControlSocketSend) that WIRES real transport IO onto the 5c-1 release
1243
- # reducer. Proves the send->outcome->release ordering over injected fakes (no socket):
1244
- # ack->sent / in-band reject->rejected (no fallback) / dead->same-lock one-shot
1245
- # re-resolve (control retry or mailbox enqueue)->fallback-sent|rejected|failed /
1246
- # indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
1247
- # double-delivery on an alive-but-stalled socket). Release fires exactly once per
1248
- # send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
1249
- run_ts scripts/check-entwurf-v2-send.ts
1250
- }
1251
-
1252
- check_entwurf_v2_send_fallback() {
1253
- # Deterministic gate for 0.11 Stage 0 step 5c-2b: the same-lock re-resolve RESOLVER
1254
- # (resolveDeadControlSendFallback) the 5c-2a hand calls on a dead connect. Proves the
1255
- # fire-and-forget re-resolve routing over injected fakes (no filesystem): alive->
1256
- # control-socket retry (inspected socketPath) / dead(absent)->reject (dormant-fire-
1257
- # forget-unsupported, and nothing is launched) / indeterminate->reject / unsupported+deliverable
1258
- # ->meta-mailbox plan (mini-table, no inspect/probe) / unsupported+undeliverable->reject
1259
- # / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
1260
- # before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
1261
- # has NO release seam; every execute plan keeps the held gid.
1262
- run_ts scripts/check-entwurf-v2-send-fallback.ts
1263
- }
1264
-
1265
- check_entwurf_v2_mailbox() {
1266
- # Deterministic gate for 0.11 Stage 0 step 5c-4 (the LAST 5c transport slice): the
1267
- # ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + its production
1268
- # sendViaMailbox adapter (makeProductionSendViaMailbox). Proves the wiring over an
1269
- # injected fake enqueue (no filesystem): sender present -> formatMetaMailboxBody with
1270
- # plan.wantsReply threaded (yes/no in body, the deliberate divergence from legacy's
1271
- # hard-coded false) / sender absent -> raw plan.message / enqueue opts EXACTLY
1272
- # {gardenId: plan.targetGardenId, body, sessionsDir, mailboxDir} (no re-derivation) /
1273
- # enqueue throw PROPAGATES (never folded into success:false — a mailbox has no in-band
1274
- # refuse) / success -> {success:true}. Production adapter resolves {success:true},
1275
- # consults senderProvider once, and NEVER touches the lock (a poison LockClaim whose
1276
- # every access throws still resolves). Source guard: the lib code has NO release seam
1277
- # and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
1278
- # re-route is structurally impossible.
1279
- run_ts scripts/check-entwurf-v2-mailbox.ts
1185
+ # MUTANT EXECUTION COORDINATE, not a discovery path the test is found by
1186
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
1187
+ # because the V2SEND-INBAND-REJECT-REASON mutant names it as gate argv, and attribution is
1188
+ # read from the JSON test titles `run_vitest` emits (run.sh:104-113). Narrow file filter: a
1189
+ # mutant pointed at the glob shim would re-run every beside-behaviour test once per mutant.
1190
+ # It stays inside check:contracts even though the discovery door also runs this file — see
1191
+ # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1192
+ section "control-socket send hand (mutant execution coordinate)"
1193
+ run_vitest pi-extensions/lib/entwurf-v2-send.test.ts
1280
1194
  }
1281
1195
 
1282
1196
  check_entwurf_v2_native_push() {
1283
- # Deterministic gate for 봉인 3/4: the native-push SEND hand (deliverViaNativePush +
1284
- # makeNativePushSend), the executor half of the native-push rail where the 1-shot retry
1285
- # lives (moved out of the adapter leaf). Proves over a fake adapter (no agy/socket): success
1286
- # first try -> {retried:false}, ONE send over the planted route, ZERO re-probe; fail ->
1287
- # re-probe alive -> re-send success -> {retried:true}, TWO sends, the 2nd over the RE-
1288
- # DISCOVERED route; re-send FAIL -> throws (no 3rd attempt); re-probe dead/indeterminate ->
1289
- # throws (not retried), NO second send. makeNativePushSend resolves the adapter from
1290
- # plan.backend and IGNORES the lock (lock-free rail).
1291
- run_ts scripts/check-entwurf-v2-native-push.ts
1292
- }
1293
-
1294
- check_entwurf_v2_runner() {
1295
- # Deterministic gate for 0.11 Stage 0 step 5d-1: the execute-router (executeDispatch) that
1296
- # routes an already-decided DispatchDecision to its 5c transport hand and maps the outcome
1297
- # to one outcome-rich EntwurfV2RunResult. Proves over injected fake hands (no socket/spawn/
1298
- # timer): reject -> rejected (receipt+diagnostic carried, NO hand called) / control-socket ->
1299
- # sendControl(plan, lock) / meta-mailbox -> sendMailbox(plan, NULL
1300
- # lock, ?7). Carry-overs: N3 control `rejected` carries rejectReason verbatim; N1
1301
- # SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
1302
- # retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
1303
- # finalizedOutcome. Exactly one hand runs per execute.
1304
- run_ts scripts/check-entwurf-v2-runner.ts
1197
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1198
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
1199
+ # because the NATIVE-PUSH-RETRY-BYTE-IDENTITY mutant names it as gate argv, and attribution
1200
+ # is read from the JSON test titles `run_vitest` emits (run.sh:104-113). Narrow file filter:
1201
+ # a mutant pointed at the glob shim would re-run every beside-behaviour test once per mutant.
1202
+ # It stays inside check:contracts even though the discovery door also runs this file — see
1203
+ # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1204
+ section "native-push send hand (mutant execution coordinate)"
1205
+ run_vitest pi-extensions/lib/entwurf-v2-native-push.test.ts
1305
1206
  }
1306
1207
 
1307
1208
  check_entwurf_v2_surface() {
@@ -1368,15 +1269,22 @@ check_compaction_send_guard() {
1368
1269
  }
1369
1270
 
1370
1271
  check_entwurf_resume_args() {
1371
- # Deterministic gate for the resume-argv SSOT (buildResumePiArgs). S1 replaced the headless
1372
- # shape with the VISIBLE one, measured against the runtime before the consumer was written:
1373
- # `-p` is pi's own non-interactive mode, so the old `--mode json -p … <prompt>` prefix would
1374
- # put a JSON stream in the operator's window instead of a session they can use. Pins the one
1375
- # shipped posture — `--entwurf-control` FIRST (the resumed session stands its socket up), no
1376
- # --mode/-p/positional prompt (a resume runs no turn), explicitExtensionArgs exactly once
1377
- # between the control flag and --session (#29 provider-resolution footgun), --session <abs
1378
- # file> never --session-id (which MINTS), and a null provider emitting no --provider.
1379
- run_ts scripts/check-entwurf-resume-args.ts
1272
+ # NOT a discovery path — the test is found by check-tests-beside-behavior like every other
1273
+ # beside-behaviour lane. This case exists as the MUTANT EXECUTION COORDINATE: the six
1274
+ # resume-args mutants name it as their gate argv, the qualification runner re-invokes that
1275
+ # argv once per mutant, and attribution is read from the JSON test titles `run_vitest` emits
1276
+ # under ENTWURF_MUTATION_VITEST_REPORT (run.sh:104-113). A gate argv that called vitest
1277
+ # directly would skip that reporter and every kill would come back unattributed.
1278
+ #
1279
+ # The file filter is NARROW on purpose. Pointing a mutant at the glob shim would make the
1280
+ # runner re-run every beside-behaviour test once per mutant.
1281
+ #
1282
+ # It stays inside check:contracts even though the discovery door also runs this file, and the
1283
+ # duplicate is deliberate: check-release-gate-outcomes requires every gate a mutant names to
1284
+ # run inside check:full, because a gate reachable ONLY through qualification makes the
1285
+ # 28-minute mutant body the only thing that can notice it going red on a clean tree.
1286
+ section "resume-argv SSOT (mutant execution coordinate)"
1287
+ run_vitest pi-extensions/lib/entwurf-resume-args.test.ts
1380
1288
  }
1381
1289
 
1382
1290
  check_mux_launcher_fence() {
@@ -1432,16 +1340,16 @@ check_mux_resume_call() {
1432
1340
  }
1433
1341
 
1434
1342
  check_entwurf_v2_visible_resume() {
1435
- # Deterministic gate for the S1 visible-resume composition (entwurf-v2-visible-resume.ts).
1436
- # Every seam is injected, so the whole state machine — including the timeout branch — runs
1437
- # without tmux, a lock file, a socket or a clock. Pins: lock BEFORE liveness, identity under
1438
- # the lock and before any window (a citizen with no transcript fails loud and opens nothing),
1439
- # live/indeterminate/address-conflict refused unlaunched, the observation as a BOUNDED WAIT
1440
- # (measured: the socket answers ~2–4s after launch, so one immediate probe would report a
1441
- # successful resume as unobserved), exactly one launch on every path, timeout releasing the
1442
- # lock and leaving the window open, a failed release throwing, and the two receipts staying
1443
- # separate in both the type and the rendered text.
1444
- run_ts scripts/check-entwurf-v2-visible-resume.ts
1343
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1344
+ # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
1345
+ # because SEVENTEEN mutants name it as gate argv, and attribution is read from the JSON test
1346
+ # titles `run_vitest` emits (run.sh:104-113), which is why each of those claims is its own
1347
+ # test title. Narrow file filter: pointing them at the glob shim would re-run every
1348
+ # beside-behaviour test seventeen times. It stays inside check:contracts even though the
1349
+ # discovery door also runs this file see check-release-gate-outcomes: a gate a mutant names
1350
+ # must be reachable outside qualification.
1351
+ section "visible same-id resume (mutant execution coordinate)"
1352
+ run_vitest pi-extensions/lib/entwurf-v2-visible-resume.test.ts
1445
1353
  }
1446
1354
 
1447
1355
  check_resume_launch_identity() {
@@ -1788,7 +1696,7 @@ smoke_acp_cortex_live() {
1788
1696
 
1789
1697
  smoke_entwurf_v2_matrix_live() {
1790
1698
  # LIVE sentinel for 0.11 Stage 0 step 5d-5 (D4-b) — kept OUT of `pnpm check`. The deterministic
1791
- # sibling (check-entwurf-v2-matrix) fixes every (target kind → transport → lock) cell over fakes
1699
+ # sibling (pi-extensions/lib/entwurf-v2-decider.matrix.test.ts) fixes every (target kind → transport → lock) cell over fakes
1792
1700
  # with ZERO IO; this drives the REAL production runEntwurfV2 deps against REAL OS objects on the
1793
1701
  # substrate happy path across 3 cells: C1 control-socket (a real `pi --entwurf-control` resident
1794
1702
  # → control-socket RPC send → lock acquire→release ×1), C2 meta-mailbox deliverable (armed
@@ -1806,18 +1714,6 @@ smoke_entwurf_v2_matrix_live() {
1806
1714
  run_ts scripts/smoke-entwurf-v2-matrix-live.ts
1807
1715
  }
1808
1716
 
1809
- check_entwurf_facts() {
1810
- # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 1): the PURE
1811
- # fact core. Locks the PeerFact shape + R1/R3b liveness invariant before any IO
1812
- # wiring (gate-first). R1: out-of-domain backend (claude-code/codex/antigravity)
1813
- # → unsupported for EVERY socket input (never coerced to the socket value or
1814
- # dead). R3b: in-domain pi → alive/dead/indeterminate, null → indeterminate
1815
- # (no proof ≠ dead). facts-only keyset: identity facts + liveness, NO
1816
- # verb-routing (resumable/sendable/transport/dispatch/action) and NO
1817
- # transcriptPath (동결결정 10). Pure, no IO, no API.
1818
- run_ts scripts/check-entwurf-facts.ts
1819
- }
1820
-
1821
1717
  check_control_socket_path() {
1822
1718
  # Deterministic gate: the control-socket path grammar `<dir>/<gid>.sock` has ONE
1823
1719
  # definition (pi-extensions/lib/control-socket-path.js) instead of one per
@@ -1828,18 +1724,6 @@ check_control_socket_path() {
1828
1724
  run_ts scripts/check-control-socket-path.ts
1829
1725
  }
1830
1726
 
1831
- check_socket_discovery() {
1832
- # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 3): the
1833
- # SOCKET-axis wiring scanSocketProbes. Probes the union of (dir sockets) ∪
1834
- # (every in-domain pi citizen's canonical path) so a dormant citizen with no
1835
- # socket file reads dead (ENOENT) → resumable, never an unprobed gap (slice 2
1836
- # throws on that). Three-valued throughout — a stalled socket stays
1837
- # indeterminate (F3), never folded to dead by an alive-only listing. Dir
1838
- # hygiene (non-.sock / malformed names ignored), dedup, missing-dir, sort, and
1839
- # an end-to-end scanSocketProbes→resolveFactList. readdir/probe injected, no IO.
1840
- run_ts scripts/check-socket-discovery.ts
1841
- }
1842
-
1843
1727
  check_meta_listing() {
1844
1728
  # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4a): the
1845
1729
  # meta-store axis listAllMetaIdentities. Explicit-partial: a parse failure or
@@ -3075,6 +2959,70 @@ check_acp_sdk_surface() {
3075
2959
  run_vitest test/acp-sdk-surface.contract.test.ts
3076
2960
  }
3077
2961
 
2962
+ check_tests_beside_behavior() {
2963
+ # The DISCOVERY door (issue #119 V2). A `.test.ts` beside a pi extension or a
2964
+ # `.test.mjs` beside the herdr plugin lib is run by public `pnpm check` without
2965
+ # anyone adding its filename anywhere. That is the whole contract: the other two
2966
+ # vitest shims list their files, and a door that also listed files would shut
2967
+ # again the first time someone forgot.
2968
+ #
2969
+ # The globs are EXPANDED here, not handed to vitest as positional filters. The first
2970
+ # cut passed `pi-extensions plugins/herdr` as positionals, and vitest reads a positional
2971
+ # as a path SUBSTRING — so `test/pi-extensions-probe.test.ts` was selected too, which is
2972
+ # the opposite of the two-locations contract this gate exists to hold (measured: that file
2973
+ # ran as "Test Files 1 passed"). Expanding first keeps the selected set exactly the two
2974
+ # globs while the shim still contains no filename: it computes them every run.
2975
+ #
2976
+ # Keeping these globs out of the other two shims stays deliberate — those are mutant gate
2977
+ # argv (mux-fresh-call carries 56), and widening their include would rerun unrelated tests
2978
+ # once per mutant.
2979
+ #
2980
+ # An empty expansion RETURNS rather than calling vitest with no arguments, because a
2981
+ # vitest run with no filter runs the whole include — that is, all of test/**. The honest
2982
+ # steady state is "the door is open and unused", and it must not be spelled as a run.
2983
+ section "tests beside behavior (discovery, not a filename list)"
2984
+ local cfg="$REPO_DIR/vitest.config.ts" glob
2985
+ for glob in 'pi-extensions/**/*.test.ts' 'plugins/herdr/**/*.test.mjs'; do
2986
+ if ! grep -qF "\"$glob\"" "$cfg"; then
2987
+ fail "[check-tests-beside-behavior] vitest.config.ts no longer includes $glob — the discovery door is shut, and an expansion that found nothing would still pass"
2988
+ return 1
2989
+ fi
2990
+ done
2991
+
2992
+ local -a selected=()
2993
+ local f
2994
+ while IFS= read -r f; do
2995
+ [ -n "$f" ] && selected+=("$f")
2996
+ done < <(cd "$REPO_DIR" && {
2997
+ find pi-extensions -name node_modules -prune -o -type f -name '*.test.ts' -print
2998
+ find plugins/herdr -name node_modules -prune -o -type f -name '*.test.mjs' -print
2999
+ } 2>/dev/null | LC_ALL=C sort)
3000
+
3001
+ # Independent of HOW the expansion was produced: every selected path must be under one of
3002
+ # the two declared locations, and none may come from test/. The second clause is the one
3003
+ # that names the defect above, so it is asserted rather than left implied by the first.
3004
+ for f in "${selected[@]}"; do
3005
+ case "$f" in
3006
+ test/*)
3007
+ fail "[check-tests-beside-behavior] selected $f — this gate covers the two behavior-adjacent locations, never test/**"
3008
+ return 1
3009
+ ;;
3010
+ pi-extensions/*.test.ts | plugins/herdr/*.test.mjs) ;;
3011
+ *)
3012
+ fail "[check-tests-beside-behavior] selected $f, which matches neither declared glob"
3013
+ return 1
3014
+ ;;
3015
+ esac
3016
+ done
3017
+
3018
+ if [ "${#selected[@]}" -eq 0 ]; then
3019
+ echo " the door is open and unused — 0 tests beside behavior at this HEAD"
3020
+ return 0
3021
+ fi
3022
+ echo " selected ${#selected[@]}: ${selected[*]}"
3023
+ run_vitest "${selected[@]}"
3024
+ }
3025
+
3078
3026
  check_acp_overlay() {
3079
3027
  # Deterministic gate for the S2b Claude config overlay materializer. Drives
3080
3028
  # ensureClaudeConfigOverlay against injected temp realDir/overlayDir (no
@@ -3478,6 +3426,13 @@ check_pack() {
3478
3426
  # tripwire that fails loud if that negation is ever dropped (결합 규칙).
3479
3427
  '__pycache__'
3480
3428
  '\.pyc$'
3429
+ # Tests written beside behavior (#119 V2) are a dev surface, and `pi-extensions/`
3430
+ # ships WHOLE through the files allowlist, so the `!pi-extensions/**/*.test.ts`
3431
+ # negation is the only thing keeping them out — same 결합 규칙 as __pycache__ above,
3432
+ # this pattern is its tripwire. Written loose rather than anchored under
3433
+ # pi-extensions/ so it also covers any future allowlist entry; `plugins/` needs no
3434
+ # companion negation because `^plugins/` above already refuses that whole tree.
3435
+ '\.test\.[cm]?[jt]s$'
3481
3436
  )
3482
3437
 
3483
3438
  local pass=1 f pat hit
@@ -6405,6 +6360,9 @@ case "$cmd" in
6405
6360
  check-copilot-launch)
6406
6361
  check_copilot_launch
6407
6362
  ;;
6363
+ check-pi-launch)
6364
+ check_pi_launch
6365
+ ;;
6408
6366
  check-copilot-receive-arm)
6409
6367
  check_copilot_receive_arm
6410
6368
  ;;
@@ -6420,33 +6378,12 @@ case "$cmd" in
6420
6378
  check-entwurf-v2-contract)
6421
6379
  check_entwurf_v2_contract
6422
6380
  ;;
6423
- check-entwurf-v2-lock)
6424
- check_entwurf_v2_lock
6425
- ;;
6426
- check-entwurf-v2-decider)
6427
- check_entwurf_v2_decider
6428
- ;;
6429
- check-entwurf-v2-matrix)
6430
- check_entwurf_v2_matrix
6431
- ;;
6432
- check-entwurf-v2-release)
6433
- check_entwurf_v2_release
6434
- ;;
6435
6381
  check-entwurf-v2-send)
6436
6382
  check_entwurf_v2_send
6437
6383
  ;;
6438
- check-entwurf-v2-send-fallback)
6439
- check_entwurf_v2_send_fallback
6440
- ;;
6441
- check-entwurf-v2-mailbox)
6442
- check_entwurf_v2_mailbox
6443
- ;;
6444
6384
  check-entwurf-v2-native-push)
6445
6385
  check_entwurf_v2_native_push
6446
6386
  ;;
6447
- check-entwurf-v2-runner)
6448
- check_entwurf_v2_runner
6449
- ;;
6450
6387
  check-entwurf-control-rpc)
6451
6388
  check_entwurf_control_rpc
6452
6389
  ;;
@@ -6567,15 +6504,9 @@ case "$cmd" in
6567
6504
  smoke-acp-socket-citizen-live)
6568
6505
  smoke_acp_socket_citizen_live
6569
6506
  ;;
6570
- check-entwurf-facts)
6571
- check_entwurf_facts
6572
- ;;
6573
6507
  check-control-socket-path)
6574
6508
  check_control_socket_path
6575
6509
  ;;
6576
- check-socket-discovery)
6577
- check_socket_discovery
6578
- ;;
6579
6510
  check-meta-listing)
6580
6511
  check_meta_listing
6581
6512
  ;;
@@ -6597,6 +6528,20 @@ case "$cmd" in
6597
6528
  smoke-herdr-plugin-build-live)
6598
6529
  smoke_herdr_plugin_build_live
6599
6530
  ;;
6531
+ smoke-herdr-raw-install-live)
6532
+ # #118 홉 1: the FIRST USER PATH. Every other herdr gate proves the plugin against a
6533
+ # source we control — a stub, or the product remote redirected by git insteadOf to a
6534
+ # local bare clone asserting identity.kind === "herdr-checkout". None has watched the
6535
+ # plugin acquire its runtime from npm on a machine that has never seen this repo, which
6536
+ # is the one axis VERIFY.md:61 leaves open while the production lock says npm. Clean
6537
+ # container, public remote, no mounts, no insteadOf. Its first question is whether
6538
+ # `plugin install --yes` completes with no herdr server; a "no" is #118 investigation
6539
+ # output, not a completion. on-demand LIVE, never an aggregate MUST.
6540
+ # shift is load-bearing: this dispatcher keeps the verb in "$@" and each branch that
6541
+ # forwards arguments drops it for itself. Without it `--ref` arrives behind the gate name.
6542
+ shift
6543
+ (cd "$REPO_DIR" && bash scripts/smoke-herdr-raw-install-live.sh "$@")
6544
+ ;;
6600
6545
  check-peer-facts)
6601
6546
  check_peer_facts
6602
6547
  ;;
@@ -7112,6 +7057,54 @@ case "$cmd" in
7112
7057
  shift || true
7113
7058
  exec bash "$REPO_DIR/scripts/copilot-launch.sh" "$@"
7114
7059
  ;;
7060
+ pi)
7061
+ # #118 홉 2: the managed pi launch. Same shape and same reasons as `copilot` above —
7062
+ # `exec`, no subshell and no cd, because this operates on the operator's session; and
7063
+ # `shift`, because this branch forwards straight into the VENDOR argv where a stray
7064
+ # "pi" would arrive as a prompt argument.
7065
+ #
7066
+ # NO separate launcher script, and no precondition check, because the precondition is
7067
+ # ZERO and that was measured rather than assumed: on a host where entwurf's pi extension
7068
+ # is not registered, `pi --entwurf-control` already fails as
7069
+ # `Error: Unknown option: --entwurf-control` with exit 1. A pre-check here would only
7070
+ # duplicate a refusal pi owns and would go stale the day pi renames it.
7071
+ #
7072
+ # ONE recursion fence, not copilot's two, and the difference is measured. The sentinel
7073
+ # closes the only real loop: a PATH executable named `pi` that shells back to
7074
+ # `entwurf pi` re-enters this branch, and finds the flag already exported. Copilot's
7075
+ # second fence — resolving the binary and refusing our own entrypoints — closes a case
7076
+ # that is not a loop here: a PATH `pi` symlinked to run.sh would be exec'd as
7077
+ # `run.sh --entwurf-control …`, which is an unknown verb this dispatcher already
7078
+ # refuses. `type -P` is what keeps the operator's own shell wrappers out of it
7079
+ # entirely: it returns a PATH file only, never a function or alias, so a
7080
+ # `pi() { entwurf pi "$@"; }` in a profile can never be re-entered through it.
7081
+ #
7082
+ # The flag is added, never deduplicated: `pit`/`pius`-style wrappers already pass
7083
+ # `--entwurf-control`, and passing it twice was measured to be byte-identical to
7084
+ # passing it once. An argv scan would be code earning nothing.
7085
+ #
7086
+ # Nothing else is injected. `--emacs-agent-socket` and friends are operator taste, and
7087
+ # a launcher that decides them stops being a spelling (Hard Rule 9).
7088
+ shift || true
7089
+ if [ -n "${ENTWURF_PI_LAUNCH_ACTIVE:-}" ]; then
7090
+ echo "entwurf: recursive managed launch detected (ENTWURF_PI_LAUNCH_ACTIVE is already set)." >&2
7091
+ echo " Something on PATH named 'pi' resolves back to this launcher. Fix PATH so 'pi'" >&2
7092
+ echo " is the pi coding agent, or run the vendor binary by its full path." >&2
7093
+ exit 1
7094
+ fi
7095
+ entwurf_pi_bin="$(type -P pi 2>/dev/null || true)"
7096
+ if [ -z "$entwurf_pi_bin" ]; then
7097
+ echo "entwurf: no 'pi' executable found on PATH." >&2
7098
+ echo " 'entwurf pi' launches the pi coding agent; it does not install one." >&2
7099
+ exit 1
7100
+ fi
7101
+ if [ ! -f "$entwurf_pi_bin" ] || [ ! -x "$entwurf_pi_bin" ]; then
7102
+ echo "entwurf: '$entwurf_pi_bin' is not an executable regular file — refusing to exec it." >&2
7103
+ exit 1
7104
+ fi
7105
+ export ENTWURF_PI_LAUNCH_ACTIVE=1
7106
+ exec "$entwurf_pi_bin" --entwurf-control "$@"
7107
+ ;;
7115
7108
  install-copilot-receive)
7116
7109
  # #82 RAIL 5: install the RECEIVER extension into the Copilot USER extensions dir.
7117
7110
  # Artifact ownership only — it sets no launch flag (`run.sh copilot` owns one
@@ -7349,6 +7342,9 @@ case "$cmd" in
7349
7342
  check-acp-sdk-surface)
7350
7343
  check_acp_sdk_surface
7351
7344
  ;;
7345
+ check-tests-beside-behavior)
7346
+ check_tests_beside_behavior
7347
+ ;;
7352
7348
  check-acp-overlay)
7353
7349
  check_acp_overlay
7354
7350
  ;;