@junghanacs/entwurf 0.35.0 → 0.35.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 (59) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/VERIFY.md +5 -4
  3. package/docs/acp-backend-rail.md +28 -2
  4. package/docs/setup-clean-host.md +1 -1
  5. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +46 -2
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +67 -0
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +9 -20
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +11 -0
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +27 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +13 -4
  13. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +119 -11
  14. package/package.json +2 -2
  15. package/pi-extensions/lib/acp/backend-adapter.ts +52 -2
  16. package/pi-extensions/lib/acp/model-ids.js +77 -0
  17. package/pi-extensions/lib/acp/models.ts +15 -22
  18. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +12 -0
  19. package/pi-extensions/lib/fresh-call-dispatch.ts +25 -1
  20. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  21. package/pi-extensions/lib/mux-fresh-call.ts +3 -0
  22. package/pi-extensions/lib/resume-launch-identity.ts +12 -4
  23. package/pi-extensions/meta-bridge-pi-durable.ts +153 -13
  24. package/run.sh +81 -20
  25. package/scripts/check-acp-backend-preflight.ts +10 -2
  26. package/scripts/check-acp-cortex.ts +7 -6
  27. package/scripts/check-acp-prompt-lifecycle.ts +2 -2
  28. package/scripts/check-acp-provider-surface.ts +1 -1
  29. package/scripts/check-acp-session-reuse.ts +5 -5
  30. package/scripts/check-acp-stop-reason.ts +2 -2
  31. package/scripts/check-acp-stream-hooks.ts +2 -2
  32. package/scripts/check-acp-usage-accounting.ts +2 -2
  33. package/scripts/check-fresh-call-dispatch.ts +47 -4
  34. package/scripts/check-gate-qualification.ts +6 -4
  35. package/scripts/check-probe-ordering.ts +13 -6
  36. package/scripts/check-resume-launch-identity.ts +38 -0
  37. package/scripts/mutants/acp-cortex.json +17 -8
  38. package/scripts/mutants/acp-set-model-readback.json +28 -0
  39. package/scripts/mutants/fresh-call-dispatch.json +11 -0
  40. package/scripts/mutants/herdr-dev-checkout.json +70 -0
  41. package/scripts/mutants/pi-durable.json +117 -0
  42. package/scripts/mutants/resume-launch-identity.json +15 -0
  43. package/scripts/mutants/v2-visible-resume.json +11 -0
  44. package/scripts/resolve-acp-bridge.ts +1 -1
  45. package/scripts/smoke-acp-bundled-mcp-live.ts +1 -1
  46. package/scripts/smoke-acp-carrier-augment-live.ts +1 -1
  47. package/scripts/smoke-acp-long-turn-live.ts +1 -1
  48. package/scripts/smoke-acp-mcp-live.ts +1 -1
  49. package/scripts/smoke-acp-memory-containment-live.ts +1 -1
  50. package/scripts/smoke-acp-ordering-probe-live.ts +1 -1
  51. package/scripts/smoke-acp-overlay-live.ts +1 -1
  52. package/scripts/smoke-acp-provider-live.ts +1 -1
  53. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  54. package/scripts/smoke-acp-session-reuse-live.ts +1 -1
  55. package/scripts/smoke-acp-skill-live.ts +1 -1
  56. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  57. package/scripts/smoke-acp-v2-send-live.ts +1 -1
  58. package/scripts/smoke-entwurf-chain-live.ts +1 -1
  59. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
@@ -58,6 +58,24 @@
58
58
  * vanished signal, a failed ring, a rejected admission or close retires the doorbell — watch
59
59
  * closed, our own marker removed, the reason kept by name — and nothing retries it.
60
60
  *
61
+ * WITHDRAW (#140). Each notice is a native `followUp`, placed one per run; a read drains EVERY
62
+ * unread body. So after a read the notices still queued for that mail would each cost a turn and an
63
+ * empty read. The contact keeps the native `Submission` handle of each notice it admitted, with the
64
+ * `.msg` names that notice announced, only while that mail is unread. After every
65
+ * `entwurf_inbox_read` attempt — whatever its garden id or outcome — it lists its OWN mailbox once
66
+ * and withdraws, through the native `Submission.abort` and the read's own invocation Context, each
67
+ * notice none of whose names is still `.msg.delivered`. That per-notice file predicate is the only
68
+ * authority: no garden-id guard, no requestId lookup, no queue of our own, no rewritten text. A placed
69
+ * or settled notice answers so and is let go; a withdraw that fails is let go too, named in one
70
+ * stderr line and on the receiver report, never retried and never retiring the doorbell; a mailbox
71
+ * that cannot be listed proves nothing absent, so every notice is kept and that is named the same
72
+ * way. The read's own result or error is returned unchanged — no diagnostic joins it. Ring passes
73
+ * and reads share one gate: a read waits for an admission in flight (else its drain could beat the
74
+ * handle it must withdraw), and an arrival during a read is rung once the read lets go. The count
75
+ * in a notice is its admission-time snapshot. Not covered: notices admitted before a restart (no
76
+ * handle), mail drained by another process (until this contact's next read), and operator abort,
77
+ * which natively withdraws queued notices while their mail stays unread.
78
+ *
61
79
  * VISIBLE IDENTITY (a static projection). `withGardenIdentity` hands the native TUI a view whose
62
80
  * every conversation label carries this citizen's garden id, so the footer the TUI keeps on screen
63
81
  * names the citizen; the bootstrap composes it only after birth succeeded. That is evidence of what
@@ -79,7 +97,8 @@
79
97
  *
80
98
  * NOT HERE: crash-safe receive. `entwurf_inbox_read` archives a body before the durable tool
81
99
  * result commits, so a crash between the two loses that body to the model; recovery settles the
82
- * call as interrupted and no doorbell rings again. No resume.
100
+ * call as interrupted and no doorbell rings again. A drain that fails part-way has archived the
101
+ * bodies before it (the bridge returns its error), and those too never reach the model. No resume.
83
102
  */
84
103
 
85
104
  import * as fs from "node:fs";
@@ -530,9 +549,14 @@ export interface DurableDoorbellDraft {
530
549
  readonly requestId: string;
531
550
  }
532
551
 
533
- /** The part of the native `Submission` handle a doorbell records; admission is not completion. */
552
+ /** What the native `Submission.abort` answers: only a still-queued input is withdrawn. */
553
+ export type DurableWithdrawal = "aborted" | "already_placed" | "settled";
554
+
555
+ /** The part of the native `Submission` handle a doorbell keeps; admission is not completion. */
534
556
  export interface DurableAdmittedSubmission {
535
557
  readonly id: unknown;
558
+ /** Native `Submission.abort(context)`, handed the read's own invocation Context unchanged. */
559
+ abort(context: DurableCallContext | undefined): Promise<DurableWithdrawal>;
536
560
  }
537
561
 
538
562
  /** The overlay's `OpenDurableResult.submitToRoot`. */
@@ -562,21 +586,40 @@ export interface PiDurableDoorbellAdmission {
562
586
  unread: number;
563
587
  }
564
588
 
589
+ /**
590
+ * The LATEST notice withdrawal that failed. `inspection-failed`: the own mailbox could not be listed,
591
+ * so no notice was judged and every one is kept. `abort-failed`: the native withdraw of the notice
592
+ * named by `requestId` rejected; that notice is let go, not retried.
593
+ */
594
+ export interface PiDurableWithdrawalFailure {
595
+ reason: "inspection-failed" | "abort-failed";
596
+ requestId?: string;
597
+ detail: string;
598
+ }
599
+
565
600
  /**
566
601
  * The receiver's diagnostic. `admissions` is the LATEST successful admission only — an empty array
567
602
  * before the first ring, then exactly one entry, replaced by each later success; a retirement keeps
568
603
  * the last one. It is a diagnostic, not a history: it never grows with the host's lifetime, and its
569
604
  * length is no count of how often the root was asked (the root's own submissions are that record).
605
+ * `withdrawalFailure`, present only once a withdrawal failed, is likewise the latest one only.
570
606
  */
571
607
  export type PiDurableReceiverReport =
572
608
  | { status: "off" }
573
- | { status: "armed"; gardenId: string; markerPath: string; admissions: PiDurableDoorbellAdmission[] }
609
+ | {
610
+ status: "armed";
611
+ gardenId: string;
612
+ markerPath: string;
613
+ admissions: PiDurableDoorbellAdmission[];
614
+ withdrawalFailure?: PiDurableWithdrawalFailure;
615
+ }
574
616
  | {
575
617
  status: "retired";
576
618
  gardenId: string;
577
619
  reason: PiDurableReceiverRetirement;
578
620
  detail: string;
579
621
  admissions: PiDurableDoorbellAdmission[];
622
+ withdrawalFailure?: PiDurableWithdrawalFailure;
580
623
  };
581
624
 
582
625
  export interface PiDurableAttachment {
@@ -641,6 +684,13 @@ interface ArmedDoorbell {
641
684
  submitToRoot: DurableSubmitToRoot;
642
685
  }
643
686
 
687
+ /** One admitted notice: its native handle and the `.msg` names it announced. */
688
+ interface HeldNotice {
689
+ readonly requestId: string;
690
+ readonly fresh: readonly string[];
691
+ readonly submission: DurableAdmittedSubmission;
692
+ }
693
+
644
694
  const TOOL_DESCRIPTIONS: Record<PiDurableToolName, string> = {
645
695
  entwurf_self:
646
696
  "Return this durable host's garden identity envelope (sessionId = garden id, agentId, cwd, replyable, rail) " +
@@ -730,7 +780,8 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
730
780
  throw new Error(`${name} arguments must be an object`);
731
781
  }
732
782
  const client = await waitReady(context?.abortSignal);
733
- const result = await client.callTool(name, (args ?? {}) as Record<string, unknown>, context?.abortSignal);
783
+ const call = () => client.callTool(name, (args ?? {}) as Record<string, unknown>, context?.abortSignal);
784
+ const result = name === "entwurf_inbox_read" ? await readInbox(call, context) : await call();
734
785
  if (result.isError) throw new Error(result.text || `${name} failed`);
735
786
  return { content: [{ type: "text", text: result.text }] };
736
787
  },
@@ -745,8 +796,34 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
745
796
  let doorbell: ArmedDoorbell | null = null;
746
797
  let report: PiDurableReceiverReport = { status: "off" };
747
798
  const admissions: PiDurableDoorbellAdmission[] = [];
748
- let ringing = false;
799
+ /** The armed mailbox, kept past a retirement so notices admitted before it can still be judged. */
800
+ let inbox: string | null = null;
801
+ /** Every admitted notice still owed a withdraw judgement, oldest first. */
802
+ const notices: HeldNotice[] = [];
803
+ let withdrawalFailure: PiDurableWithdrawalFailure | undefined;
804
+ // One gate for ring passes and inbox reads; `pending` is a signal that landed while it was held.
805
+ let gated = false;
749
806
  let pending = false;
807
+ const gateWaiters: (() => void)[] = [];
808
+
809
+ const enterGate = (): Promise<void> => {
810
+ if (!gated) {
811
+ gated = true;
812
+ return Promise.resolve();
813
+ }
814
+ return new Promise<void>((resolve) => gateWaiters.push(resolve));
815
+ };
816
+
817
+ /** Hand the gate to the next waiting read, else open it — and ring once for a signal that landed meanwhile. */
818
+ const leaveGate = (): void => {
819
+ const next = gateWaiters.shift();
820
+ if (next !== undefined) {
821
+ next();
822
+ return;
823
+ }
824
+ gated = false;
825
+ if (pending) void ring("signal");
826
+ };
750
827
 
751
828
  /**
752
829
  * Give the doorbell back: close the watch and remove the marker, in one step, because a
@@ -791,19 +868,19 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
791
868
  /**
792
869
  * One contract with the other self-fetch doorbells: a fresh `*.msg` is the trigger, it is
793
870
  * stamped `.msg.delivered` before the announce, and the announced count is every
794
- * `.msg.delivered`. Events that land while an admission is in flight coalesce into one more
795
- * pass. A failed pass retires the doorbell; it is not retried. Only a fresh `.msg` rings —
796
- * the startup pass included — so a `.msg.delivered` left undrained by an earlier host is not
797
- * announced again on its own; it shows only in the count of the next fresh arrival.
871
+ * `.msg.delivered`. Events that land while an admission or an inbox read holds the gate coalesce
872
+ * into one more pass. A failed pass retires the doorbell; it is not retried. Only a fresh `.msg`
873
+ * rings — the startup pass included — so a `.msg.delivered` left undrained by an earlier host is
874
+ * not announced again on its own; it shows only in the count of the next fresh arrival.
798
875
  */
799
876
  const ring = async (why: PiDurableDoorbellAdmission["why"]): Promise<void> => {
800
877
  const armed = doorbell;
801
878
  if (armed === null) return;
802
- if (ringing) {
879
+ if (gated) {
803
880
  pending = true;
804
881
  return;
805
882
  }
806
- ringing = true;
883
+ gated = true;
807
884
  try {
808
885
  do {
809
886
  pending = false;
@@ -836,11 +913,71 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
836
913
  }
837
914
  // Latest only: the diagnostic names the LAST success and never grows with the host's lifetime.
838
915
  admissions.splice(0, admissions.length, { why, requestId, submissionId: submission.id, fresh, unread });
916
+ notices.push({ requestId, fresh, submission });
839
917
  } while (pending && doorbell === armed);
840
918
  } catch (error) {
841
919
  retire("doorbell-failed", describeError(error));
842
920
  } finally {
843
- ringing = false;
921
+ leaveGate();
922
+ }
923
+ };
924
+
925
+ /**
926
+ * One named stderr line per failed withdrawal — stderr ALWAYS, as `entwurf-control` does, since
927
+ * nothing in the native app reads the receiver report — and the report keeps the latest one.
928
+ */
929
+ const noteWithdrawalFailure = (failure: PiDurableWithdrawalFailure): void => {
930
+ withdrawalFailure = failure;
931
+ const subject = failure.requestId === undefined ? "" : ` ${failure.requestId}`;
932
+ const detail = failure.detail.replace(/\s+/g, " ");
933
+ process.stderr.write(`[entwurf pi-durable] notice withdrawal ${failure.reason}${subject}: ${detail}\n`);
934
+ };
935
+
936
+ /**
937
+ * Withdraw every held notice none of whose announced `.msg` names is still `.msg.delivered` in the
938
+ * armed mailbox, listed once. A mailbox that cannot be listed proves nothing absent: every notice
939
+ * is kept and that is named. A judged notice is let go whatever the native withdraw answers — or
940
+ * when it rejects, which is named and never retried — so no handle outlives its mail by more
941
+ * than one read. Never throws: the read's own answer is what the caller gets.
942
+ */
943
+ const withdrawDrained = async (context: DurableCallContext | undefined): Promise<void> => {
944
+ if (inbox === null || notices.length === 0) return;
945
+ let unread: Set<string>;
946
+ try {
947
+ unread = new Set(fs.readdirSync(inbox).filter((name) => name.endsWith(".msg.delivered")));
948
+ } catch (error) {
949
+ noteWithdrawalFailure({ reason: "inspection-failed", detail: `${inbox}: ${describeError(error)}` });
950
+ return;
951
+ }
952
+ for (const notice of [...notices]) {
953
+ if (notice.fresh.some((name) => unread.has(`${name}.delivered`))) continue;
954
+ notices.splice(notices.indexOf(notice), 1);
955
+ try {
956
+ await notice.submission.abort(context);
957
+ } catch (error) {
958
+ noteWithdrawalFailure({ reason: "abort-failed", requestId: notice.requestId, detail: describeError(error) });
959
+ }
960
+ }
961
+ };
962
+
963
+ /**
964
+ * An inbox read under the doorbell's gate, then the withdrawal its drain made due — after a
965
+ * failed read too, whose partial drain may already have archived mail. The bridge's own answer,
966
+ * result or rejection, is returned unchanged.
967
+ */
968
+ const readInbox = async (
969
+ call: () => Promise<BridgeCallResult>,
970
+ context: DurableCallContext | undefined,
971
+ ): Promise<BridgeCallResult> => {
972
+ await enterGate();
973
+ try {
974
+ return await call();
975
+ } finally {
976
+ try {
977
+ await withdrawDrained(context);
978
+ } finally {
979
+ leaveGate();
980
+ }
844
981
  }
845
982
  };
846
983
 
@@ -893,6 +1030,7 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
893
1030
  receiversDir: roots.receiversDir,
894
1031
  });
895
1032
  doorbell = { gardenId, ownerPid, mailbox, signal, markerPath, watcher, submitToRoot };
1033
+ inbox = mailbox;
896
1034
  report = { status: "armed", gardenId, markerPath, admissions };
897
1035
  return markerPath;
898
1036
  } catch (error) {
@@ -1011,13 +1149,15 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
1011
1149
  settleFailed(describeError(error));
1012
1150
  },
1013
1151
  receiver() {
1014
- return report;
1152
+ return withdrawalFailure === undefined || report.status === "off" ? report : { ...report, withdrawalFailure };
1015
1153
  },
1016
1154
  async close() {
1017
1155
  const previous = state;
1018
1156
  settleFailed("the contact closed before attach completed");
1019
1157
  state = { kind: "closed" };
1020
1158
  const client = previous.kind === "ready" ? previous.client : undefined;
1159
+ // Queued notices stay native (a continued session places them); this contact judges no more.
1160
+ notices.length = 0;
1021
1161
  await closeAll(disarmOnClose, async () => {
1022
1162
  await client?.close();
1023
1163
  });
package/run.sh CHANGED
@@ -2,10 +2,10 @@
2
2
  #
3
3
  # Model id convention (see AGENTS.md Hard Rule #1):
4
4
  # - User-facing examples use the qualified form `entwurf/<backend-model>`
5
- # (e.g. `entwurf/claude-sonnet-5`); the prefix routes to this provider
5
+ # (e.g. `entwurf/claude-sonnet-5-5`); the prefix routes to this provider
6
6
  # so `--provider` is redundant and is dropped in docs.
7
7
  # - Smoke helpers that feed `ensureBridgeSession({modelId})` directly (cancel,
8
- # model-switch) pass BARE backend ids (`claude-sonnet-5`, `gpt-5.4`)
8
+ # model-switch) pass BARE backend ids (`claude-sonnet-5-5`, `gpt-5.4`)
9
9
  # because the bridge library contract is bare. Smoke helpers that invoke pi
10
10
  # via the CLI still pin `--provider entwurf` and can accept either
11
11
  # bare or qualified model, but we keep bare here to match the bridge-level
@@ -32,7 +32,7 @@ TARGET_PROJECT_DIR=${2:-$PROJECT_DIR_DEFAULT}
32
32
  # and install path against the same scope for traceability.
33
33
  PACKAGE_NAME="@junghanacs/entwurf"
34
34
  # Runtime provider id — DO NOT change. Embedded in model strings
35
- # (`entwurf/claude-sonnet-5`), settings keys (`entwurfProvider`),
35
+ # (`entwurf/claude-sonnet-5-5`), settings keys (`entwurfProvider`),
36
36
  # log prefixes (`[entwurf:bootstrap]`), and the `--provider entwurf`
37
37
  # CLI surface. Renaming this would break every consumer transcript and every
38
38
  # saved session anchor.
@@ -116,9 +116,55 @@ run_vitest() {
116
116
  (cd "$REPO_DIR" && "$bin" run "$@")
117
117
  }
118
118
 
119
+ # A source-only UI over the existing owned developer-return procedure. No second
120
+ # installer, npm acquisition, automatic mode switch or ownership writer lives here.
121
+ developer_menu() {
122
+ [ "$#" -eq 0 ] || { echo 'usage: ./run.sh menu' >&2; return 2; }
123
+ if [ ! -d "$REPO_DIR/.git" ] || [ ! -f "$REPO_DIR/plugins/herdr/use-dev-checkout.sh" ]; then
124
+ echo '[menu] developer-checkout-source-only — run ./run.sh menu from the prepared source checkout, not the npm installation.' >&2
125
+ return 1
126
+ fi
127
+ if [ ! -t 0 ]; then
128
+ echo '[menu] menu-tty-required — interactive stdin is required; nothing was changed.' >&2
129
+ return 2
130
+ fi
131
+ local choice confirm
132
+ printf '\nHerdr — npm installation check → developer board (local checkout)\n'
133
+ printf 'Checkout: %s\n' "$REPO_DIR"
134
+ printf ' 1) Readiness check — read-only (not a full certificate of installation prerequisites)\n'
135
+ printf ' 2) Switch to the developer board — Pi / Claude Code / Herdr local link\n'
136
+ printf ' 0) Quit — nothing is changed\n'
137
+ if ! IFS= read -r -p 'Choose [0/1/2]: ' choice; then
138
+ echo '[menu] menu-input-closed — nothing was changed.' >&2
139
+ return 2
140
+ fi
141
+ case "$choice" in
142
+ 0) return 0 ;;
143
+ 1) bash "$REPO_DIR/plugins/herdr/use-dev-checkout.sh" --check ;;
144
+ 2)
145
+ # Show the real owner's readiness first. Busy sessions or bad ownership
146
+ # stop here; confirmation grants no authority to bypass those guards.
147
+ bash "$REPO_DIR/plugins/herdr/use-dev-checkout.sh" --check || return $?
148
+ printf '\nClose the affected sessions yourself, and start no new session while switching.\n'
149
+ printf 'The currently owned installation is deactivated, then this checkout is linked. There is no automatic recovery or retry.\n'
150
+ if ! IFS= read -r -p 'Type DEV to switch (anything else cancels): ' confirm; then
151
+ echo '[menu] menu-input-closed — apply was not called.' >&2
152
+ return 2
153
+ fi
154
+ if [ "$confirm" != DEV ]; then
155
+ echo '[menu] menu-cancelled — installation wiring is unchanged.'
156
+ return 0
157
+ fi
158
+ bash "$REPO_DIR/plugins/herdr/use-dev-checkout.sh" --apply
159
+ ;;
160
+ *) echo '[menu] invalid-menu-choice — choose 0, 1 or 2; nothing was changed.' >&2; return 2 ;;
161
+ esac
162
+ }
163
+
119
164
  usage() {
120
165
  cat <<'EOF'
121
166
  Usage:
167
+ ./run.sh menu # source-only Herdr developer board: read-only readiness check or explicitly confirmed return to checkout wiring; never closes sessions or acquires npm artifacts
122
168
  ./run.sh setup [project-dir] # ONE presence-driven composition (#86): per-component PASS/SKIP/FAIL for pi/claude/agy/copilot/omp/codex + stable dev bins + v2 install smoke. Codex birth/MCP/status-line atoms are composed at user scope; setup stays non-green until the operator trusts the birth declaration once in visible Codex. Absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness, starts a daemon, touches credentials, or writes the vendor trust receipt
123
169
  ./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, 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) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex Pi -> visible Codex -> visible Pi receipt chain (smoke-codex-fresh-live; initial Pi and the Codex it opens in one session S, explicit operator-owned app-server PID in a DIFFERENT session — that separation is the lane B claim). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
124
170
  ./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
@@ -223,7 +269,7 @@ Usage:
223
269
  ./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)
224
270
  ./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
225
271
 
226
- ./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)
272
+ ./run.sh install-meta-bridge # INTERNAL wiring leaf: setup, the source-only developer menu and doctor recovery compose it; stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
227
273
  ./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
228
274
  ./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 CERTIFIED and stays nonzero, on ONE narrowed reason — start-key and ppid are portable there (`ps` fallback, same string the TS core mints), so what holds the lane closed is per-process environment DISCOVERY alone, pending physical-host evidence. Detection power is held by check-meta-doctor-oracle
229
275
  ./run.sh copilot [args...] # #82 RAIL 7: the MANAGED Copilot launch. exec()s the vendor CLI in THIS terminal (cwd/pid/exit preserved) with COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS set for that one invocation — entwurf owns no part of your shell and writes nothing to it, but it owns the process it is about to become. Without that flag Copilot skips the extension scan SILENTLY. Refuses to launch unless the receiver unit is actually installed. Injects `--model auto` when no --model was given and `--yolo` when no explicit permission/surface policy flag was given, both BEFORE any `--`; every argument you pass is forwarded byte-identical. RUNNING THIS IS YOUR CONSENT to that profile — use plain `copilot` for stock vendor behaviour. Not tmux, not fresh-call, mints no citizen (birth is still the first prompt)
@@ -291,7 +337,7 @@ Usage:
291
337
  ./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): 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
292
338
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke + the #116 M3-b1 herdr-plugin RUNTIME VERIFIER run against that actual installed tarball (the focused check-herdr-runtime-bootstrap drives a fixture; THIS is where the real package is proven) with the pinned pi SDK (pins derived from the package.json exact production dependency; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + 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) + the INSTALLED-PACKAGE branch of the Copilot and OMP birth installers actually RUN (compiled entry selected, no raw .ts, and a real birth edge mints a citizen — the half a required-artifact list can never stand in for) + the INSTALLED #129 durable bootstrap (pi/pi-durable/bootstrap.mjs) imported from node_modules: main/openDurableCitizen exported and PACKAGED_BRIDGE_ENTRY = the installed package's own dist entry — import only, no runtime/TUI/birth/bridge started + the #130 pi-durable CARRIER on actual npm consumers (Entwurf-only / aligned host / isolated global): the installed verifier, a consumer-contained resolution oracle with a same-sandbox neighbour control, every physical SDK edge at the pin, a mixed tree refused as pi-durable-sdk-mismatch though it still imports, and the public `entwurf pi-durable` reaching the app's own explicit-model refusal before any birth, also with one explicit `--native-module` (the synthetic fixture, initialized first; a reserved extension name refused before anything opens) (offline, sandbox HOME)
293
339
  ./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)
294
- ./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
340
+ ./run.sh install [project-dir] # INTERNAL wiring leaf: setup/npm-consumer entry and the source-only developer menu compose it; prefer those owning procedures
295
341
  ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
296
342
  ./run.sh install-user-scope [--plugin-runtime <root>] # #116 M3-b2 PLUGIN-ONLY forward seam: compose ONLY the global pi citizen (user-scope packages[] + provider), never a project `.pi/settings.json` — a Herdr plugin activation has no project and must not invent one. With --plugin-runtime the provider's managed command becomes the absolute bridge under that certified stable runtime (a clean host has nothing entwurf on PATH); without it every byte is the historical bare-bin install. Inverse: remove-user-scope
297
343
  ./run.sh herdr-plugin-activate <backend...> # #116 M3-b2 public forward activation. Takes a subset of {pi, claude-code} and NOTHING else — the stable runtime root is DERIVED from XDG here and handed down, and both harness writers accept a named root only when it IS that derived one. Certifies the runtime-ready journal AND the installed tree, certifies the existing ledger and its recorded harness roots, plans ADD-ONLY (a backend absent from this request is retained, never removed), preflights every selected component before the first byte, then writes each component `pending` BEFORE mutating and checkpoints `active` the moment its writer returns — so a run that dies halfway leaves a record of how far it got. Which backends is M3-b3's question: this verb is handed the answer, it does not read Herdr's integration status
@@ -1395,6 +1441,14 @@ check_acp_foreground_only() {
1395
1441
  run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
1396
1442
  }
1397
1443
 
1444
+ check_acp_set_model_readback() {
1445
+ # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1446
+ # MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
1447
+ # adapter refuses a set-model answer that does not name the requested model, before the prompt.
1448
+ section "claude ACP set-model readback (mutant execution coordinate)"
1449
+ run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
1450
+ }
1451
+
1398
1452
  check_acp_vendor_rejection() {
1399
1453
  # Behaviour-adjacent tests are discovered by the glob lane. This narrow case is the
1400
1454
  # mutant execution coordinate: include the adapter admission and real turn pipeline,
@@ -1603,12 +1657,12 @@ check_mux_launch_tmux() {
1603
1657
  smoke_acp_socket_citizen_live() {
1604
1658
  # S1 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
1605
1659
  # Spawns a REAL `pi --entwurf-control` resident on an ACP model
1606
- # (entwurf/claude-opus-5) and proves it is a first-class socket-citizen:
1660
+ # (entwurf/claude-opus-5-5) and proves it is a first-class socket-citizen:
1607
1661
  # the control socket stands up, get_info answers with the ACP model (model-lock
1608
1662
  # did NOT revert — QM1), idle/cwd are reported, and the fail-loud streamSimple
1609
1663
  # stub never fires (turn-free launch — QM2). No prompt is sent: S1 proves
1610
1664
  # citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
1611
- # Model override: ENTWURF_S1_MODEL (default claude-opus-5).
1665
+ # Model override: ENTWURF_S1_MODEL (default claude-opus-5-5).
1612
1666
  # LIVE=1 ./run.sh smoke-acp-socket-citizen-live
1613
1667
  run_ts scripts/smoke-acp-socket-citizen-live.ts
1614
1668
  }
@@ -1622,7 +1676,7 @@ smoke_acp_raw_turn_live() {
1622
1676
  # plus captured raw NDJSON bytes. NO provider/overlay/streamSimple/_meta — the
1623
1677
  # raw backend pipe only. Launch source must be the package bin (PATH fallback
1624
1678
  # fails acceptance unless ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1, debug).
1625
- # Model override: ENTWURF_ACP_RAW_TURN_MODEL (default claude-sonnet-5).
1679
+ # Model override: ENTWURF_ACP_RAW_TURN_MODEL (default claude-sonnet-5-5).
1626
1680
  # LIVE=1 ./run.sh smoke-acp-raw-turn-live
1627
1681
  run_ts scripts/smoke-acp-raw-turn-live.ts
1628
1682
  }
@@ -1640,7 +1694,7 @@ smoke_acp_overlay_live() {
1640
1694
  # meta-store for mailbox absence (flaky — concurrent sessions); the honest
1641
1695
  # claim is overlay-supplies-hooks:{}. Launch must be the package bin (PATH
1642
1696
  # fallback fails acceptance unless ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1).
1643
- # Model override: ENTWURF_ACP_OVERLAY_MODEL (default claude-sonnet-5).
1697
+ # Model override: ENTWURF_ACP_OVERLAY_MODEL (default claude-sonnet-5-5).
1644
1698
  # LIVE=1 ./run.sh smoke-acp-overlay-live
1645
1699
  run_ts scripts/smoke-acp-overlay-live.ts
1646
1700
  }
@@ -1655,7 +1709,7 @@ smoke_acp_memory_containment_live() {
1655
1709
  # can stop a memory write is the lever — not us. Fails loud if engraving.md is
1656
1710
  # empty (carrier OFF = no containment). Launch must be the package bin (PATH
1657
1711
  # fallback fails acceptance unless ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK=1).
1658
- # Model override: ENTWURF_ACP_MEMORY_MODEL (default claude-sonnet-5).
1712
+ # Model override: ENTWURF_ACP_MEMORY_MODEL (default claude-sonnet-5-5).
1659
1713
  # LIVE=1 ./run.sh smoke-acp-memory-containment-live
1660
1714
  run_ts scripts/smoke-acp-memory-containment-live.ts
1661
1715
  }
@@ -1682,7 +1736,7 @@ smoke_acp_provider_live() {
1682
1736
  # assistant reply (live model proof) + the removed S0 stub error never appears
1683
1737
  # (provider path actually opened) + pi exits 0. Tool-free prompt; the
1684
1738
  # event-mapper gate owns the tool→notice contract.
1685
- # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1739
+ # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5-5).
1686
1740
  # LIVE=1 ./run.sh smoke-acp-provider-live
1687
1741
  run_ts scripts/smoke-acp-provider-live.ts
1688
1742
  }
@@ -1696,7 +1750,7 @@ smoke_acp_session_reuse_live() {
1696
1750
  # is the verdict; codeword recall is supporting only (a rebuilt full transcript
1697
1751
  # could recall it too). The one-shot exit0 half is owned by
1698
1752
  # smoke-acp-provider-live.
1699
- # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1753
+ # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5-5).
1700
1754
  # LIVE=1 ./run.sh smoke-acp-session-reuse-live
1701
1755
  run_ts scripts/smoke-acp-session-reuse-live.ts
1702
1756
  }
@@ -1708,7 +1762,7 @@ smoke_acp_carrier_augment_live() {
1708
1762
  # secret (the augment rode the wire to the model) and the EMPTY default carrier
1709
1763
  # must bill clean (exit 0, no HTTP-400 canary — 핀1 live). Optional tiny carrier
1710
1764
  # check via SMOKE_ACP_CARRIER_PRESENT=1 (non-blocking).
1711
- # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1765
+ # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5-5).
1712
1766
  # LIVE=1 ./run.sh smoke-acp-carrier-augment-live
1713
1767
  run_ts scripts/smoke-acp-carrier-augment-live.ts
1714
1768
  }
@@ -1794,9 +1848,9 @@ smoke_acp_rgg_live() {
1794
1848
  # birth, record-keyed socket, attach-on-reopen, no pi-session-id socket) holds
1795
1849
  # under the ACP provider too — the logic is provider-agnostic. The POSITIVE cell
1796
1850
  # (one real turn completing transcriptPath/model) is the only model-in-loop part.
1797
- # Target override: ENTWURF_RGG_TARGET (default entwurf/claude-sonnet-5).
1851
+ # Target override: ENTWURF_RGG_TARGET (default entwurf/claude-sonnet-5-5).
1798
1852
  # ./run.sh smoke-acp-rgg-live
1799
- local target="${ENTWURF_RGG_TARGET:-entwurf/claude-sonnet-5}"
1853
+ local target="${ENTWURF_RGG_TARGET:-entwurf/claude-sonnet-5-5}"
1800
1854
  (cd "$REPO_DIR" && ENTWURF_LIVE_TARGET="$target" SMOKE_RGG_POSITIVE=0 bash scripts/smoke-resident-garden-guard.sh)
1801
1855
  }
1802
1856
 
@@ -3646,7 +3700,7 @@ check_pack() {
3646
3700
  "package.json" "README.md" "LICENSE" "CHANGELOG.md"
3647
3701
  "protocol.js" "run.sh"
3648
3702
  "pi-extensions/acp-provider.ts"
3649
- "pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/backend.ts"
3703
+ "pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/model-ids.js" "pi-extensions/lib/acp/backend.ts"
3650
3704
  "pi-extensions/lib/acp/overlay.ts" "pi-extensions/lib/acp/tool-surface.ts"
3651
3705
  "pi-extensions/lib/acp/event-mapper.ts" "pi-extensions/lib/acp/context.ts"
3652
3706
  "pi-extensions/entwurf-control.ts"
@@ -4294,7 +4348,7 @@ _check_pack_install_impl() {
4294
4348
  "package.json" "README.md" "LICENSE" "CHANGELOG.md"
4295
4349
  "protocol.js" "run.sh"
4296
4350
  "pi-extensions/acp-provider.ts"
4297
- "pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/backend.ts"
4351
+ "pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/model-ids.js" "pi-extensions/lib/acp/backend.ts"
4298
4352
  "pi-extensions/lib/acp/overlay.ts" "pi-extensions/lib/acp/tool-surface.ts"
4299
4353
  "pi-extensions/lib/acp/event-mapper.ts" "pi-extensions/lib/acp/context.ts"
4300
4354
  "pi-extensions/entwurf-control.ts"
@@ -4720,8 +4774,8 @@ _check_pack_install_impl() {
4720
4774
  loader_ids=$(awk '$1 == "entwurf" { print $2 }' <<<"$loader_out" | LC_ALL=C sort)
4721
4775
  loader_expected=$(printf '%s\n' \
4722
4776
  claude-fable-5-1 \
4723
- claude-opus-5 \
4724
- claude-sonnet-5 \
4777
+ claude-opus-5-5 \
4778
+ claude-sonnet-5-5 \
4725
4779
  cortex-auto \
4726
4780
  cortex-claude-opus-5 \
4727
4781
  cortex-claude-sonnet-5 \
@@ -4830,7 +4884,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
4830
4884
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
4831
4885
  return 1
4832
4886
  }
4833
- if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-5" <<<"$foreign_out"; then
4887
+ if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-5-5" <<<"$foreign_out"; then
4834
4888
  fail "[check-pack-install] foreign-cwd --entwurf-control did not load the entwurf extension from user scope:"
4835
4889
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
4836
4890
  return 1
@@ -7069,6 +7123,10 @@ release_gate() {
7069
7123
 
7070
7124
  cmd=${1:-}
7071
7125
  case "$cmd" in
7126
+ menu)
7127
+ shift
7128
+ developer_menu "$@"
7129
+ ;;
7072
7130
  setup)
7073
7131
  setup_all "$TARGET_PROJECT_DIR"
7074
7132
  ;;
@@ -7243,6 +7301,9 @@ case "$cmd" in
7243
7301
  check-acp-foreground-only)
7244
7302
  check_acp_foreground_only
7245
7303
  ;;
7304
+ check-acp-set-model-readback)
7305
+ check_acp_set_model_readback
7306
+ ;;
7246
7307
  check-resume-launch-identity)
7247
7308
  check_resume_launch_identity
7248
7309
  ;;
@@ -16,13 +16,13 @@
16
16
 
17
17
  import { strict as assert } from "node:assert";
18
18
  import { execFileSync } from "node:child_process";
19
- import { readdirSync, readFileSync, rmSync } from "node:fs";
19
+ import { readdirSync, readFileSync, rmdirSync, rmSync } from "node:fs";
20
20
  import { join, resolve } from "node:path";
21
21
  import { pathToFileURL } from "node:url";
22
22
  import type { Api, AssistantMessageEvent, Model, TranscriptContext } from "@earendil-works/pi-ai";
23
23
  import { normalizeContext } from "@earendil-works/pi-ai";
24
24
 
25
- const model = { id: "claude-sonnet-5" } as unknown as Model<Api>;
25
+ const model = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
26
26
 
27
27
  // Declared tools exclude `read`, but the Claude child always exposes Read →
28
28
  // declared != actual → the preflight must reject before spawning.
@@ -157,6 +157,14 @@ try {
157
157
  );
158
158
  } finally {
159
159
  rmSync(TMP_EMIT, { recursive: true, force: true });
160
+ try {
161
+ // Same as check-acp-cortex: the emit created `.tmp-verify/` as TMP_EMIT's parent, and
162
+ // check-gate-qualification's snapshot manifest walks ignored paths, so a leftover empty
163
+ // parent reads as a dirty tree. Remove it when empty; a sibling's emit keeps it alive.
164
+ rmdirSync(".tmp-verify");
165
+ } catch {
166
+ // non-empty or already gone — fine either way
167
+ }
160
168
  }
161
169
 
162
170
  // ---------------------------------------------------------------------------
@@ -53,11 +53,11 @@ import {
53
53
  import { tmpdir } from "node:os";
54
54
  import { join, resolve } from "node:path";
55
55
  import { pathToFileURL } from "node:url";
56
+ import { getModels } from "@earendil-works/pi-ai/compat";
56
57
  import { buildPiContextAugment } from "../pi-extensions/lib/acp/augment.ts";
57
58
  import type { AcpMcpServer } from "../pi-extensions/lib/acp/config.ts";
58
59
  import {
59
60
  CORTEX_MODEL_PREFIX,
60
- curatedClaudeModels,
61
61
  curatedCortexModels,
62
62
  SUPPORTED_CORTEX_MODEL_IDS,
63
63
  } from "../pi-extensions/lib/acp/models.ts";
@@ -87,12 +87,13 @@ const cortexRowIds = cortexRows.map((m) => m.id);
87
87
  assert.deepEqual(cortexRowIds, [...SUPPORTED_CORTEX_MODEL_IDS], "curated cortex rows must match the supported id set");
88
88
  // The two Claude rows ride their OWN registry bases (a fabricated row would
89
89
  // drift from the pi-ai catalog); auto/gpt ride the sonnet base as a floor.
90
- const claudeRows = curatedClaudeModels();
90
+ // Cortex retains its own native 5 ids even when Claude's curated set moves to 5.5.
91
+ const claudeRows = getModels("anthropic");
91
92
  const sonnetBase = claudeRows.find((m) => m.id === "claude-sonnet-5");
92
93
  const opusBase = claudeRows.find((m) => m.id === "claude-opus-5");
93
94
  // Plain throw (not assert.ok): an asserts-function premise makes tsc report the
94
95
  // downstream `base` const as circularly-typed (TS7022).
95
- if (!sonnetBase || !opusBase) throw new Error("test premise: sonnet-5/opus-5 present in curated claude surface");
96
+ if (!sonnetBase || !opusBase) throw new Error("test premise: sonnet-5/opus-5 present in pi-ai registry");
96
97
  for (const row of cortexRows) {
97
98
  // Explicit annotation: node:assert's asserts-typed helpers otherwise make tsc
98
99
  // flag this flow-dependent const as circularly typed (TS7022).
@@ -480,7 +481,7 @@ try {
480
481
  // throwing IS the anti-collision proof; we also assert the rows are present.
481
482
  const registered = mod.allCuratedModels().map((m) => m.id);
482
483
  for (const want of [
483
- "claude-sonnet-5",
484
+ "claude-sonnet-5-5",
484
485
  "cortex-auto",
485
486
  "cortex-claude-opus-5",
486
487
  "cortex-claude-sonnet-5",
@@ -494,12 +495,12 @@ try {
494
495
  assert.equal(routedSonnet.adapter.backend, "cortex", "cortex-claude-sonnet-5 must route to the cortex adapter");
495
496
  assert.equal(routedSonnet.nativeModelId, "claude-sonnet-5", "prefix strip must recover the native model id");
496
497
  assert.equal(
497
- mod.resolveAcpBackendAdapter("claude-sonnet-5").adapter.backend,
498
+ mod.resolveAcpBackendAdapter("claude-sonnet-5-5").adapter.backend,
498
499
  "claude",
499
500
  "the unprefixed claude id must still route to claude (prefix keeps the namespaces apart)",
500
501
  );
501
502
  assert.equal(
502
- mod.cortexAdapter.routeModel("claude-sonnet-5"),
503
+ mod.cortexAdapter.routeModel("claude-sonnet-5-5"),
503
504
  undefined,
504
505
  "cortex must NOT claim the unprefixed claude id",
505
506
  );
@@ -40,7 +40,7 @@ import type { Api, AssistantMessageEvent, Message, Model, TranscriptContext } fr
40
40
  import { normalizeContext } from "@earendil-works/pi-ai";
41
41
  import { isRetryableAssistantError } from "@earendil-works/pi-ai/compat";
42
42
 
43
- const sonnet = { id: "claude-sonnet-5" } as unknown as Model<Api>;
43
+ const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
44
44
 
45
45
  type Stream = AsyncIterable<AssistantMessageEvent>;
46
46
 
@@ -151,7 +151,7 @@ function makeHarness(recordDir: string) {
151
151
  const makeConnection = (handlers: any) => ({
152
152
  initialize: async () => ({ agentCapabilities: {} }),
153
153
  newSession: async () => ({ sessionId: "ACP-1" }),
154
- setSessionConfigOption: async () => ({}),
154
+ setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: "model", currentValue: p.value }] }),
155
155
  prompt: ({ sessionId }: any) => {
156
156
  promptCount++;
157
157
  notifier = (update) => handlers.sessionUpdate({ update, sessionId });
@@ -123,7 +123,7 @@ try {
123
123
  assert.equal(cap.cfg.apiKey, ENTWURF_ACP_NO_AUTH_SENTINEL, "entry apiKey is not the no-auth sentinel");
124
124
  assert.equal(cap.cfg.api, "entwurf", "entry api field drifted");
125
125
  const capIds = (cap.cfg.models ?? []).map((m) => m.id);
126
- for (const want of ["claude-sonnet-5", CURATED_ANCHOR_MODEL_ID]) {
126
+ for (const want of ["claude-sonnet-5-5", CURATED_ANCHOR_MODEL_ID]) {
127
127
  assert.ok(capIds.includes(want), `entry model surface missing ${want} (got: ${capIds.join(", ") || "none"})`);
128
128
  }
129
129