@junghanacs/entwurf 0.51.0 → 0.53.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 (88) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +116 -397
  3. package/CHANGELOG.md +191 -2
  4. package/CONTRIBUTING.md +4 -4
  5. package/DELIVERY.md +2 -2
  6. package/ENTWURF-CLI.json +1 -1
  7. package/ENTWURF.json +10 -4
  8. package/README.md +11 -8
  9. package/VERIFY.md +333 -143
  10. package/demo/README.md +1 -1
  11. package/demo/demo-baseline.sh +1 -1
  12. package/demo/demo.sh +1 -1
  13. package/docs/acp-backend-rail.md +248 -34
  14. package/docs/setup-clean-host.md +75 -80
  15. package/mcp/entwurf-bridge/dist/ENTWURF.json +10 -4
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +4 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
  28. package/package.json +14 -14
  29. package/pi/pi-durable/carrier/harness-setup.js +1 -1
  30. package/pi/pi-durable/carrier/subagent.js +1 -1
  31. package/pi/pi-durable/overlay/upstream-pin.json +15 -15
  32. package/pi/settings.reference.json +0 -1
  33. package/pi-extensions/acp-provider.ts +12 -6
  34. package/pi-extensions/lib/acp/acp-client.ts +6 -7
  35. package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
  36. package/pi-extensions/lib/acp/backend.ts +261 -32
  37. package/pi-extensions/lib/acp/config.ts +4 -9
  38. package/pi-extensions/lib/acp/context.ts +17 -0
  39. package/pi-extensions/lib/acp/event-mapper.ts +236 -17
  40. package/pi-extensions/lib/acp/model-ids.js +9 -12
  41. package/pi-extensions/lib/acp/models.ts +46 -57
  42. package/pi-extensions/lib/acp/tool-surface.ts +34 -6
  43. package/pi-extensions/lib/entwurf-control-rpc.ts +5 -1
  44. package/pi-extensions/lib/fresh-call-composition.ts +37 -3
  45. package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
  46. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  47. package/pi-extensions/lib/mux-fresh-call.ts +3 -1
  48. package/pi-extensions/meta-bridge-pi-durable.ts +1 -1
  49. package/run.sh +62 -120
  50. package/scripts/check-acp-carrier-augment.ts +3 -21
  51. package/scripts/check-acp-prompt-lifecycle.ts +24 -3
  52. package/scripts/check-acp-provider-surface.ts +17 -26
  53. package/scripts/check-acp-session-reuse.ts +23 -4
  54. package/scripts/check-acp-stop-reason.ts +22 -3
  55. package/scripts/check-acp-stream-hooks.ts +24 -3
  56. package/scripts/check-acp-usage-accounting.ts +53 -24
  57. package/scripts/check-copilot-receive-arm.ts +19 -2
  58. package/scripts/check-entwurf-v2-surface.ts +7 -21
  59. package/scripts/check-fresh-call-dispatch.ts +9 -3
  60. package/scripts/check-gate-qualification.ts +6 -6
  61. package/scripts/check-herdr-fresh-call.ts +1 -0
  62. package/scripts/check-install-surface.ts +6 -0
  63. package/scripts/check-pi-durable-send.ts +4 -1
  64. package/scripts/check-probe-ordering.ts +34 -24
  65. package/scripts/check-release-gate-outcomes.ts +9 -23
  66. package/scripts/lib/codex-fresh-source-receipts.ts +12 -6
  67. package/scripts/lib/probe-cli-target.ts +3 -2
  68. package/scripts/mutants/acp-augment.json +0 -26
  69. package/scripts/mutants/acp-cortex.json +10 -183
  70. package/scripts/mutants/acp-foreground-only.json +11 -0
  71. package/scripts/mutants/acp-launch-namespace.json +3 -1
  72. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  73. package/scripts/mutants/acp-set-model-readback.json +488 -0
  74. package/scripts/mutants/agent-guidance.json +2 -2
  75. package/scripts/mutants/codex-native.json +2 -2
  76. package/scripts/mutants/control-send-receipt.json +47 -7
  77. package/scripts/mutants/mux-fresh-call.json +12 -1
  78. package/scripts/mutants/pack-install.json +4 -4
  79. package/scripts/mutants/pi-floor.json +8 -10
  80. package/scripts/mutants/release-gate.json +3 -3
  81. package/scripts/mutants/v2-surface.json +4 -6
  82. package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
  83. package/scripts/smoke-acp-cortex-live.ts +4 -408
  84. package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
  85. package/scripts/smoke-acp-session-reuse-live.ts +11 -2
  86. package/scripts/smoke-acp-v2-send-live.ts +1 -1
  87. package/scripts/smoke-herdr-raw-install-live.sh +4 -4
  88. package/scripts/check-acp-cortex.ts +0 -710
@@ -16,15 +16,10 @@
16
16
  //
17
17
  // The COMMON keys parsed here are Claude-SHAPED by origin (tools / permissionAllow /
18
18
  // disallowedTools / settingSources / skillPlugins are claude-agent-acp's surface).
19
- // What varies per backend is whether they are PROJECTED ONTO THE BACKEND SESSION —
20
- // not whether they are read at all. Cortex runs native tools and reaches MCP through
21
- // its own overlay mcp.json projection, so none of those keys shape a cortex session;
22
- // they nonetheless stay live on the common path, where backend.ts reads `tools` for
23
- // the backend-invariant exclude-tools preflight and folds every one of them into
24
- // `bridgeConfigSignature` (so changing one still invalidates a reused cortex
25
- // session). Backend-OWNED settings never land here: they ride the opaque
26
- // `adapterSettings` seam (rail “Adapter contract”), which is what keeps a backend-named key like
27
- // `cortexConnection` off this common shape.
19
+ // The common path reads `tools` for the backend-invariant exclude-tools preflight
20
+ // and folds these keys into `bridgeConfigSignature`. Backend-owned settings ride
21
+ // the opaque `adapterSettings` seam. A dormant `cortexConnection` does not select
22
+ // or enable Cortex ACP, which is disabled.
28
23
  //
29
24
  // PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
30
25
  // - `normalizeMcpServers` is pure: a SORTED, validated server list + a sha256
@@ -57,6 +57,17 @@ function textFromAssistantContent(content) {
57
57
  .map((c) => c.text)
58
58
  .join(""));
59
59
  }
60
+ /**
61
+ * The harness fact pi already holds about an assistant turn that was cut
62
+ * (`stopReason: "aborted"` — an operator abort, or a vendor `cancelled` mapped to it; the line
63
+ * names no cause), stated so a NEW session's transcript does not read a
64
+ * cancelled turn as a clean refusal (#142: a fresh session concluded a command had
65
+ * not run when its first effect was on disk). It adds no claim about what did or
66
+ * did not happen — only that the turn was cut and that a cut is not a rollback.
67
+ * Only the full transcript carries it: a reused session already holds its own
68
+ * record of the interruption and receives just the latest user delta.
69
+ */
70
+ export const ABORTED_TURN_STATUS = "[turn status: aborted — actions this turn had started may already have taken effect; cancellation is not rollback]";
60
71
  /** Render one pi message as a transcript line, or undefined to skip it. */
61
72
  function renderMessage(message) {
62
73
  switch (message.role) {
@@ -66,6 +77,10 @@ function renderMessage(message) {
66
77
  }
67
78
  case "assistant": {
68
79
  const text = textFromAssistantContent(message.content).trim();
80
+ if (message.stopReason === "aborted") {
81
+ // Kept even when the cut turn left no text: the cut itself is the fact.
82
+ return text ? `Assistant: ${text}\n${ABORTED_TURN_STATUS}` : `Assistant: ${ABORTED_TURN_STATUS}`;
83
+ }
69
84
  return text ? `Assistant: ${text}` : undefined;
70
85
  }
71
86
  case "toolResult": {
@@ -12,12 +12,9 @@
12
12
  // would need a `.ts` specifier the root config cannot allow.
13
13
  //
14
14
  // WHY THE LAUNCH SEAMS ASK AT ALL. Pi's `--model` resolver falls back to a
15
- // case-insensitive substring match among the provider's rows and prefers the
16
- // lexically later id. Under one `entwurf` provider a retired `claude-sonnet-5`
17
- // is a substring of `cortex-claude-sonnet-5`, so a launch Entwurf builds would
18
- // silently open on the Snowflake Cortex rail instead of the Claude subscription.
19
- // An exact match against this list is the only admission; nothing is aliased,
20
- // substituted or rewritten.
15
+ // case-insensitive substring match among provider rows. The old multi-backend
16
+ // surface could silently match a retired Claude id to a Cortex row. Cortex is
17
+ // now disabled; exact membership remains the admission, with no substitution.
21
18
  /** The single ACP provider id under which every curated row is registered. */
22
19
  export const PROVIDER_ID = "entwurf";
23
20
  // The curated Claude ids. Adding one here is a commitment to verify it across
@@ -26,20 +23,21 @@ export const SUPPORTED_ANTHROPIC_MODEL_IDS = /** @type {const} */ ([
26
23
  "claude-sonnet-5-5",
27
24
  "claude-opus-5-5",
28
25
  "claude-fable-5-1",
26
+ "claude-fable-5-2",
29
27
  ]);
30
28
  /** The anchor model whose absence is a hard registry regression, not a soft skip. */
31
29
  export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5-5";
32
- // Cortex ids carry the reserved `cortex-` prefix; the rest of each id is the
33
- // Snowflake-native model name, which is why Cortex still says `claude-opus-5`.
30
+ // These former IDs are retained only as historical refusal inputs. The prefix
31
+ // is reserved so a disabled Cortex request never falls through to Claude.
34
32
  export const CORTEX_MODEL_PREFIX = "cortex-";
35
- export const SUPPORTED_CORTEX_MODEL_IDS = /** @type {const} */ ([
33
+ export const LEGACY_CORTEX_MODEL_IDS = /** @type {const} */ ([
36
34
  "cortex-auto",
37
35
  "cortex-claude-opus-5",
38
36
  "cortex-claude-sonnet-5",
39
37
  "cortex-openai-gpt-5.4",
40
38
  ]);
41
39
  /** @type {ReadonlySet<string>} */
42
- const CURATED_ACP_MODEL_IDS = new Set([...SUPPORTED_ANTHROPIC_MODEL_IDS, ...SUPPORTED_CORTEX_MODEL_IDS]);
40
+ const CURATED_ACP_MODEL_IDS = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
43
41
  /**
44
42
  * Exact membership only — no trim, no case folding, no prefix.
45
43
  * @param {string} model
@@ -51,8 +49,7 @@ export function isCuratedAcpModelId(model) {
51
49
  /** The named refusal both launch seams answer with. */
52
50
  export const ACP_MODEL_NOT_CURATED = /** @type {const} */ ("acp-model-not-curated");
53
51
  export const ACP_MODEL_NOT_CURATED_HINT = `an ${PROVIDER_ID} model must be one exact curated ACP id (${[...CURATED_ACP_MODEL_IDS].join(", ")}); ` +
54
- "Pi would otherwise pick another row by substring — a retired claude-sonnet-5 lands on cortex-claude-sonnet-5 — " +
55
- "so nothing was opened, and nothing is aliased or rewritten";
52
+ "Cortex ACP is disabled; Pi may otherwise pick another row by substring, so nothing was opened and nothing is aliased or rewritten";
56
53
  /** Carries `reason` so callers match a FIELD, never the wording. */
57
54
  export class AcpModelNotCuratedError extends Error {
58
55
  /** @param {string} model */
@@ -1,18 +1,12 @@
1
- // ACP plugin — curated model surface (S0 loader/fence slice; cortex rows added
2
- // in 0.13).
1
+ // ACP plugin — curated Claude model surface. Cortex ACP is disabled.
3
2
  //
4
3
  // entwurf is an ACP *plugin* on the v2 core, NOT a general-purpose
5
4
  // Anthropic provider. It deliberately does not expose the full pi-ai model
6
5
  // registry — the surface is curated to the ids the plugin commits to driving
7
6
  // through a real ACP backend.
8
7
  //
9
- // Two curated sets live here, one per adapter on the rail
10
- // (backend-adapter.ts): the unprefixed Claude ids (the reference backend) and
11
- // the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
12
- // 0.13.0 — docs/acp-backend-rail.md “Cortex Code audit”). Codex has native
13
- // delivery-probe evidence but no managed citizen lane; neither it nor Gemini is an ACP backend here.
14
- // A third governed backend EXTENDS this file the same way — it does not change
15
- // the pattern.
8
+ // Only the unprefixed Claude IDs are registered. Historical Cortex IDs are
9
+ // reserved refusal inputs; native citizens are independent of this provider.
16
10
  // pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
17
11
  // `@earendil-works/pi-ai/compat` entrypoint (the global-API churn). We import it
18
12
  // from `/compat` directly.
@@ -36,7 +30,7 @@ import { CURATED_ANCHOR_MODEL_ID, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./model-
36
30
  // The provider id and the curated ID lists live in the dependency-free
37
31
  // `model-ids.js` leaf, shared with the launch seams (#128); this file is the only
38
32
  // one that loads the upstream catalog to turn them into registry rows.
39
- export { CORTEX_MODEL_PREFIX, CURATED_ANCHOR_MODEL_ID, PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_CORTEX_MODEL_IDS, } from "./model-ids.js";
33
+ export { CORTEX_MODEL_PREFIX, CURATED_ANCHOR_MODEL_ID, LEGACY_CORTEX_MODEL_IDS, PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS, } from "./model-ids.js";
40
34
  // #26 auth-boundary sentinel. `pi.registerProvider` requires an apiKey when a
41
35
  // provider defines custom models, but the ACP plugin consumes NO key: backend
42
36
  // auth belongs to the operator's own Claude CLI child process (AGENTS
@@ -70,54 +64,44 @@ function requireRegistryModel(models, id) {
70
64
  function claudeContextWindow(model) {
71
65
  return Math.min(model.contextWindow, CLAUDE_CONTEXT_DEFAULT);
72
66
  }
73
- // ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
74
- //
75
- // entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
76
- // backend to land through it (docs/acp-backend-rail.md, “Shipped adapters”). pi-ai carries no
77
- // snowflake/cortex model source, so this surface is HAND-CURATED. Every id
78
- // carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
79
- // authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
80
- // native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
81
- // `claude-sonnet-5-5` the claude adapter owns). `routeModel` strips the prefix to
82
- // recover the native id, which is enforced per-turn via
83
- // session/set_config_option — never a launch `-m` pin (CP0-M).
84
- //
85
- // The 4-row set is the GLG-decided curation (2026-07-29) — a subset of the 11
86
- // native ids cortex v1.1.52 exposed live when authenticated. A curated id that
87
- // the running cortex no longer serves fails LOUD at set-model, before the
88
- // prompt (measured: `Unsupported model: …` names the live set). Adding an id
89
- // here is the same verify-both-axes commitment as the Claude set — do not
90
- // extend casually. The prefix and the four ids are declared in model-ids.js.
91
- // Hand-set conservative context window. Cortex reports the LIVE window via its
92
- // own ACP session config, not this curated metadata — this is a floor for the
93
- // registry surface, deliberately not inflated to the Claude 1M.
94
- const CORTEX_CONTEXT_DEFAULT = 200_000;
67
+ // Cortex ACP is disabled; its former IDs are refusal inputs, not model rows.
95
68
  /**
96
- * The curated Cortex model rows handed to the single `entwurf` provider via
97
- * `allCuratedModels()`. The two Claude rows ride their OWN registry base
98
- * (opus-5 / sonnet-5); `auto` (default family Claude) and the GPT row (no pi-ai
99
- * source) ride the sonnet-5 base as a metadata floor. id / name / contextWindow
100
- * are overridden; the `cortex-` prefix keeps them from colliding with the
101
- * Claude curated ids. Same row shape as `curatedClaudeModels()` (AcpModelRow).
69
+ * What an ACP row declares it can take as input: text only. The rail delivers pi's transcript as
70
+ * ACP text blocks; an image becomes the marker `[image omitted: <mime>]` (`context.ts`), and no
71
+ * other path carries image bytes to the backend. pi's catalog lists the Anthropic models as
72
+ * `["text", "image"]` — true of the model behind the API, not of this transport — so copying it
73
+ * would advertise a capability the rail does not deliver (#142 landing review). A backend gains
74
+ * `image` here only together with a real image path and its proof.
102
75
  */
76
+ const ACP_ROW_INPUT = ["text"];
77
+ /** The retired Cortex adapter contributes no supported model rows. */
103
78
  export function curatedCortexModels() {
104
- const sonnetBase = requireRegistryModel(ANTHROPIC_MODELS_ALL, "claude-sonnet-5");
105
- const opusBase = requireRegistryModel(ANTHROPIC_MODELS_ALL, "claude-opus-5");
106
- const row = (id, name, base) => ({
107
- id,
108
- name,
109
- reasoning: base.reasoning,
110
- input: base.input,
111
- cost: base.cost,
112
- contextWindow: CORTEX_CONTEXT_DEFAULT,
113
- maxTokens: base.maxTokens,
114
- });
115
- return [
116
- row("cortex-auto", "Cortex · Auto", sonnetBase),
117
- row("cortex-claude-opus-5", "Cortex · Claude Opus 5", opusBase),
118
- row("cortex-claude-sonnet-5", "Cortex · Claude Sonnet 5", sonnetBase),
119
- row("cortex-openai-gpt-5.4", "Cortex · OpenAI GPT-5.4", sonnetBase),
120
- ];
79
+ return [];
80
+ }
81
+ /**
82
+ * The effort values claude-agent-acp offers on its `effort` config option (category
83
+ * `thought_level`). `[측정 2026-11-13]` 0.106.0, authenticated, no prompt (#142): the option lists
84
+ * `default, low, medium, high, xhigh, max`; `high → low → max` each read back on one live session and
85
+ * `minimal` was refused `-32602`. `default` is the vendor's own choice, so no pi level maps to it.
86
+ */
87
+ export const CLAUDE_ACP_EFFORT_VALUES = ["low", "medium", "high", "xhigh", "max"];
88
+ /**
89
+ * The pi `thinkingLevelMap` an ACP Claude row registers (#142). pi's documented model seam: the
90
+ * value is what the provider sends for each pi level, `null` marks a level unsupported, and xhigh/max
91
+ * appear only when mapped (`getSupportedThinkingLevels`). Each supported pi level maps to the effort
92
+ * value of the same name. `off` and `minimal` have no effort value, so they are `null` and never
93
+ * offered; a level the registry already marks unsupported for this model stays unsupported, and
94
+ * xhigh/max stay hidden where the registry does not map them. Nothing here is a model catalog: the
95
+ * registry decides which levels the model has, this decides which of them ACP can carry.
96
+ */
97
+ export function claudeAcpThinkingLevelMap(registry) {
98
+ const map = { off: null, minimal: null };
99
+ for (const level of CLAUDE_ACP_EFFORT_VALUES) {
100
+ const declared = registry?.[level];
101
+ const unmapped = declared === undefined && (level === "xhigh" || level === "max");
102
+ map[level] = declared === null || unmapped ? null : level;
103
+ }
104
+ return map;
121
105
  }
122
106
  /**
123
107
  * The curated Claude model rows handed to `pi.registerProvider({ models })`.
@@ -130,7 +114,8 @@ export function curatedClaudeModels() {
130
114
  id: m.id,
131
115
  name: m.name,
132
116
  reasoning: m.reasoning,
133
- input: m.input,
117
+ thinkingLevelMap: claudeAcpThinkingLevelMap(m.thinkingLevelMap),
118
+ input: [...ACP_ROW_INPUT],
134
119
  cost: m.cost,
135
120
  contextWindow: claudeContextWindow(m),
136
121
  maxTokens: m.maxTokens,
@@ -12,13 +12,10 @@
12
12
  //
13
13
  // The module's TWO jobs have different backend scopes — do not collapse them:
14
14
  // - `buildClaudeSessionMeta` is claude-only by CONSTRUCTION, with exactly one
15
- // caller (`claudeAdapter.buildSessionMeta`). Cortex exposes no
16
- // `_meta.systemPrompt` and no tools/permission declaration surface (native
17
- // tools + its own overlay mcp.json projection), so its adapter returns
18
- // `undefined` and that function is never reached for a cortex turn.
15
+ // caller (`claudeAdapter.buildSessionMeta`).
19
16
  // - `assertExcludeToolsHonored` is BACKEND-INVARIANT: backend.ts runs it for
20
- // every adapter before any spawn, passing `backend: adapter.backend` and the
21
- // resolved `config.tools`. A cortex turn DOES enter this module through it.
17
+ // every admitted adapter before any spawn, passing `backend: adapter.backend`
18
+ // and the resolved `config.tools`. Cortex ACP is disabled.
22
19
  // That split is why the `backend` field exists: the honest-divergence logic stays
23
20
  // explicit and the matrix gate can exercise both the "claude narrows via tools"
24
21
  // and "native always exposes" branches.
@@ -104,6 +101,28 @@ export function assertExcludeToolsHonored(activeToolNames, resolved) {
104
101
  ". Extension tools (entwurf_v2, entwurf_peers) can be excluded freely — they are pi-side and never reach the backend.");
105
102
  }
106
103
  }
104
+ /**
105
+ * #163: is this Pi's own summarization request — the one tool-less turn the preflight above
106
+ * honours instead of refusing?
107
+ *
108
+ * Pi summarizes for `/compact`, the compaction threshold, overflow recovery and `/tree` branch
109
+ * summaries through one call (pi-coding-agent 1.5.2 `dist/core/compaction/compaction.js:476-489`,
110
+ * `completeSummarization`; branch summaries at `compaction/branch-summarization.js:233-237`). Its
111
+ * context declares no tools (`buildSummarizationContext`, `compaction.js:499-509`) and its options
112
+ * carry `cacheRetention: "none"` (`compaction.js:482`), which an ordinary agent-loop turn does not
113
+ * (pi-agent-core `dist/agent.js:306-330`).
114
+ *
115
+ * BOTH are required. An empty surface alone is also an operator who excluded every tool on an
116
+ * ordinary turn, which keeps today's refusal; `cacheRetention` alone is a request option, not a
117
+ * surface. Neither the summarization prompt's text nor a missing session id is read: Pi fills the
118
+ * id with a fresh `uuidv7()` (`compaction.js:481`), and matching its prompt would copy it.
119
+ * `cacheRetention` is a Pi convention, not a contract — the oracle beside this calls Pi's own
120
+ * `completeSummarization` so a Pi upgrade that drops it turns that test red rather than this
121
+ * path silently back into a refusal.
122
+ */
123
+ export function isPiSummaryRequest(activeToolNames, cacheRetention) {
124
+ return activeToolNames.length === 0 && cacheRetention === "none";
125
+ }
107
126
  /**
108
127
  * Build the `_meta` object handed to `newSession` for a Claude ACP session.
109
128
  *
@@ -144,6 +163,14 @@ export function buildClaudeSessionMeta(params, normalizedSystemPrompt) {
144
163
  // the carrier's "doesn't-know" — keeping both means a future preset/SDK
145
164
  // change cannot silently re-open memory through a channel the strip misses.
146
165
  autoMemoryEnabled: false,
166
+ // No turn of the adapter's own (#142). claude-agent-acp 0.101.0 compacts an idle session
167
+ // before its 1h prompt cache expires by sending `/compact` as a prompt nobody here issued;
168
+ // a retained session's callbacks drop updates that arrive with no active turn, so that
169
+ // model turn would spend quota and replace context invisibly to pi. 0.106.0 (#1555) reads
170
+ // this inline key when no managed tier sets one. Only the IDLE trigger is off:
171
+ // `autoCompactEnabled` stays untouched, so Claude Code's own compaction inside a prompt
172
+ // we sent keeps working.
173
+ idleCompaction: false,
147
174
  },
148
175
  };
149
176
  if (params.skillPlugins.length > 0) {
@@ -29,7 +29,10 @@ import { win32Native } from "./win32-native.js";
29
29
  * renders nothing (etiquette marker, not transport contract).
30
30
  */
31
31
  export function formatSenderInfoBlock(sender, wantsReply = false) {
32
- return `\n\n<sender_info>${JSON.stringify({
32
+ const replyGuidance = sender.replyable === true
33
+ ? "\n\n[entwurf: When a reply is requested, send the result via entwurf_v2 to the requested recipient (default: sender_info.sessionId); a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment.]"
34
+ : "";
35
+ return `${replyGuidance}\n\n<sender_info>${JSON.stringify({
33
36
  sessionId: sender.sessionId,
34
37
  agentId: sender.agentId,
35
38
  cwd: sender.cwd,
@@ -22,6 +22,7 @@
22
22
  * (#116 D1), and its two-step launch breaks the ordering argument outright.
23
23
  */
24
24
  import { randomBytes } from "node:crypto";
25
+ import { PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./acp/model-ids.js";
25
26
  /** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
26
27
  * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
27
28
  * by #95 after its system birth and app-server rails were measured, and `pi-durable` by #129
@@ -142,6 +143,35 @@ export const FRESH_CALL_TOOL_LOAD_HINT = {
142
143
  codex: [],
143
144
  "pi-durable": [],
144
145
  };
146
+ /**
147
+ * Which dialect table spells the bridge tools for THIS launch. The backend picks the runtime; the
148
+ * model can pick a different tool surface underneath it.
149
+ *
150
+ * `[측정 2026-11-13, D1]` a pi sibling opened on an ACP Claude model (`entwurf/claude-*`) does not
151
+ * see Pi's MCP names: its turns run inside the Claude Agent SDK, which mints the bridge tools as
152
+ * `mcp__entwurf-bridge__*` (the ACP Sonnet interview of 2026-11-13, n=1, called them by the schema
153
+ * it was given, not by the pi spelling its framing named). So the NAMES follow the claude-code
154
+ * table. The load hint stays the pi row (none): in that n=1 the bridge tools were directly callable
155
+ * without loading. Whether ToolSearch exists on this surface, or deferral can occur with more
156
+ * declared servers (`acp/tool-surface.ts` names the SDK's deferred tools), is unmeasured — not a
157
+ * claim that it cannot.
158
+ *
159
+ * Only an exact curated Anthropic id switches. A thinking suffix (`:high`) or fuzzy alias never
160
+ * reaches here through the public verb: the dispatch root refuses any `entwurf/` id that is not
161
+ * exactly curated (`isUncuratedEntwurfPiModel`, #128), and the test pins that alignment. Retired
162
+ * Cortex ids are refused at the dispatch root before framing. The ACP id list arrives from
163
+ * `acp/model-ids.js`, an import-free module.
164
+ */
165
+ export function freshCallToolDialect(backend, model) {
166
+ if (backend !== "pi")
167
+ return backend;
168
+ const trimmed = model.trim();
169
+ const slash = trimmed.indexOf("/");
170
+ if (slash < 0 || trimmed.slice(0, slash) !== PROVIDER_ID)
171
+ return backend;
172
+ const id = trimmed.slice(slash + 1);
173
+ return SUPPORTED_ANTHROPIC_MODEL_IDS.includes(id) ? "claude-code" : backend;
174
+ }
145
175
  /**
146
176
  * The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
147
177
  * name (`extensions/loader.ts:221-228`) and this composition adds the `--` itself.
@@ -379,9 +409,10 @@ export function composeBackendArgs(backend, composition, model, resolveCodexSock
379
409
  * corroborate the caller instead of an instruction not to look.
380
410
  */
381
411
  export function composeFreshCallFraming(params) {
382
- const birthTool = FRESH_CALL_CALLBACK_TOOL[params.backend];
383
- const deliveryTool = FRESH_CALL_DELIVERY_TOOL[params.backend];
384
- const peersTool = FRESH_CALL_PEERS_TOOL[params.backend];
412
+ const dialect = freshCallToolDialect(params.backend, params.model);
413
+ const birthTool = FRESH_CALL_CALLBACK_TOOL[dialect];
414
+ const deliveryTool = FRESH_CALL_DELIVERY_TOOL[dialect];
415
+ const peersTool = FRESH_CALL_PEERS_TOOL[dialect];
385
416
  // The hint is a per-backend TEMPLATE so the two tool dialects stay the single source of their
386
417
  // own spelling here too — a hint that hard-coded Claude's name would go stale the day a dialect
387
418
  // moves, and it would go stale silently, in the one sentence a stuck child depends on.
@@ -554,6 +554,7 @@ export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL
554
554
  return { ok: false, reason: badCwd };
555
555
  const framing = composeFreshCallFraming({
556
556
  backend,
557
+ model,
557
558
  callerGardenId,
558
559
  nonce,
559
560
  openingLine: HERDR_FRESH_CALL_OPENING_LINE,
@@ -1220,6 +1221,7 @@ async function herdrLayoutFreshCall(params, context, env, nonce, deps) {
1220
1221
  return { ok: false, reason: "herdr-socket-path-missing" };
1221
1222
  const prompt = composeFreshCallPrompt({
1222
1223
  backend: "pi-durable",
1224
+ model,
1223
1225
  task,
1224
1226
  callerGardenId,
1225
1227
  nonce,
@@ -1287,7 +1289,7 @@ export const HERDR_REJECT_HINT = {
1287
1289
  "model-invalid": "the model is not in the accepted grammar (no whitespace, no leading dash)",
1288
1290
  // #128: decided at the dispatch root (`fresh-call-dispatch.ts`), worded here because this rail's
1289
1291
  // import fence admits no ACP module. The curated list itself lives in `acp/model-ids.js`.
1290
- "acp-model-not-curated": "an entwurf model must be one exact curated ACP id — Pi would otherwise pick another row by substring (a retired claude-sonnet-5 lands on cortex-claude-sonnet-5), so nothing was opened and nothing was aliased",
1292
+ "acp-model-not-curated": "an entwurf model must be one exact curated ACP id; Cortex ACP is disabled. Pi may otherwise pick another row by substring, so nothing was opened or aliased",
1291
1293
  "task-empty": "a sibling opened with nothing to do is a window nobody asked for",
1292
1294
  "task-too-long": `the task is over the ${TASK_MAX_CHARS}-character interface bound this verb shares with entwurf_v2`,
1293
1295
  "herdr-context-missing": "HERDR_ENV/HERDR_BIN_PATH are absent, so this process is not inside herdr and this rail does not exist here",
@@ -64,7 +64,7 @@ export function formatMetaMailboxBody(sender, message, wantsReply) {
64
64
  // SENDING host, so it is neither abbreviated against this host's HOME nor shown as a local path.
65
65
  const remote = !replyable ? sender.remote : undefined;
66
66
  const sessionLine = replyable
67
- ? `${sender.sessionId} (${kind}replyable — reply via entwurf_v2 to this sessionId, intent=fire-and-forget)`
67
+ ? `${sender.sessionId} (${kind}replyable — when a reply is requested, send the result via entwurf_v2 (intent=fire-and-forget) to the requested recipient, defaulting to this sessionId; a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment)`
68
68
  : remote
69
69
  ? `${sender.sessionId} (remote via ${remote.via}; ${remote.replyRoute === "declared" ? "reply route declared, not probed" : "no reply route declared"})`
70
70
  : isMeta
@@ -331,7 +331,7 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
331
331
  const runtime = layout === null ? null : checkPiDurableRuntime(layout);
332
332
  const missing = piDurableFreshPreflight({
333
333
  model,
334
- firstInput: buildFreshCallPrompt({ backend: params.backend, task, callerGardenId, nonce }),
334
+ firstInput: buildFreshCallPrompt({ backend: params.backend, model, task, callerGardenId, nonce }),
335
335
  layout,
336
336
  runtime: runtime === null || runtime.ok ? null : runtime.reason,
337
337
  });
@@ -373,6 +373,7 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
373
373
  const composition = {
374
374
  prompt: buildFreshCallPrompt({
375
375
  backend: params.backend,
376
+ model,
376
377
  task,
377
378
  callerGardenId,
378
379
  nonce,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.51.0",
4
- "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request, visibly reopen supported sessions under the same id, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
3
+ "version": "0.53.1",
4
+ "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request, visibly reopen supported sessions under the same id, and deliver messages by garden id; native bridges and a Claude ACP adapter.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Junghan Kim",
@@ -102,19 +102,19 @@
102
102
  "claudeCodeFloor": ">=2.1.306"
103
103
  },
104
104
  "dependencies": {
105
- "@agentclientprotocol/claude-agent-acp": "0.90.0",
106
- "@agentclientprotocol/sdk": "1.8.0",
105
+ "@agentclientprotocol/claude-agent-acp": "0.107.0",
106
+ "@agentclientprotocol/sdk": "2.6.0",
107
107
  "@anthropic-ai/sdk": "0.100.1",
108
- "@earendil-works/chord": "1.5.0",
109
- "@earendil-works/pi-agent-core": "1.5.0",
110
- "@earendil-works/pi-ai": "1.5.0",
111
- "@earendil-works/pi-codemode": "1.5.0",
112
- "@earendil-works/pi-coding-agent": "1.5.0",
113
- "@earendil-works/pi-durable": "1.5.0",
114
- "@earendil-works/pi-mcp": "1.5.0",
115
- "@earendil-works/pi-schema": "1.5.0",
116
- "@earendil-works/pi-telemetry": "1.5.0",
117
- "@earendil-works/pi-tui": "1.5.0",
108
+ "@earendil-works/chord": "1.5.2",
109
+ "@earendil-works/pi-agent-core": "1.5.2",
110
+ "@earendil-works/pi-ai": "1.5.2",
111
+ "@earendil-works/pi-codemode": "1.5.2",
112
+ "@earendil-works/pi-coding-agent": "1.5.2",
113
+ "@earendil-works/pi-durable": "1.5.2",
114
+ "@earendil-works/pi-mcp": "1.5.2",
115
+ "@earendil-works/pi-schema": "1.5.2",
116
+ "@earendil-works/pi-telemetry": "1.5.2",
117
+ "@earendil-works/pi-tui": "1.5.2",
118
118
  "@modelcontextprotocol/sdk": "^1.12.1",
119
119
  "proper-lockfile": "4.1.2",
120
120
  "smol-toml": "^1.8.0",
@@ -1,4 +1,4 @@
1
-
1
+
2
2
 
3
3
  import {
4
4
  createRegistry,
@@ -1,4 +1,4 @@
1
-
1
+
2
2
 
3
3
  import {
4
4
  AssistantEntry,
@@ -82,8 +82,8 @@
82
82
  {
83
83
  "name": "harness-setup.js",
84
84
  "source": "pi/pi-durable/src/experimental/durable/harness-setup.ts",
85
- "blob": "e8561809321ce8833a13e3757c172f4e38faabcc",
86
- "sha256": "eb1158030a8ab95187a4fdd5e7b90816a7995e5f446d09a8184d4f5007e7ac5f"
85
+ "blob": "5a799bfbfbbedbec229761b26f639fbe2702b623",
86
+ "sha256": "70b50044ab14c1293a2ace53b375dc48ad940ea9eeacc131d498ccb1ce040617"
87
87
  },
88
88
  {
89
89
  "name": "prompt.js",
@@ -100,8 +100,8 @@
100
100
  {
101
101
  "name": "subagent.js",
102
102
  "source": "pi/pi-durable/src/experimental/durable/subagent.ts",
103
- "blob": "e842d8a6b8dc55825d0381d803b6d6496b205c19",
104
- "sha256": "c9931fabd60f5fbf75220e896a57e5c8b131b4936887bf6f9660d50d01709f3a"
103
+ "blob": "8c18e24a030f0b701167f15a33579195303f5ac5",
104
+ "sha256": "208618441e8e81813de2f44fa9ad5d5bdcdbb78a754679bb7a93d82480e7bece"
105
105
  },
106
106
  {
107
107
  "name": "session-name.js",
@@ -138,18 +138,18 @@
138
138
  ]
139
139
  },
140
140
  "sdk": {
141
- "comment": "The published library set the carrier runs against, derived from the 1.5.0 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare (1.5.0 adds pi-schema, declared by chord, pi-agent-core, pi-ai and pi-durable). direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.5.0 pins). Each is an exact production dependency in package.json. Since 1.3.0 the members' own edges are exact too (upstream #10865), and the installed physical edges are still what must equal these versions.",
141
+ "comment": "The published library set the carrier runs against, derived from the 1.5.2 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare (1.5.0 adds pi-schema, declared by chord, pi-agent-core, pi-ai and pi-durable). direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.5.2 pins). Each is an exact production dependency in package.json. Since 1.3.0 the members' own edges are exact too (upstream #10865), and the installed physical edges are still what must equal these versions.",
142
142
  "members": {
143
- "@earendil-works/chord": "1.5.0",
144
- "@earendil-works/pi-agent-core": "1.5.0",
145
- "@earendil-works/pi-ai": "1.5.0",
146
- "@earendil-works/pi-codemode": "1.5.0",
147
- "@earendil-works/pi-coding-agent": "1.5.0",
148
- "@earendil-works/pi-durable": "1.5.0",
149
- "@earendil-works/pi-mcp": "1.5.0",
150
- "@earendil-works/pi-schema": "1.5.0",
151
- "@earendil-works/pi-telemetry": "1.5.0",
152
- "@earendil-works/pi-tui": "1.5.0"
143
+ "@earendil-works/chord": "1.5.2",
144
+ "@earendil-works/pi-agent-core": "1.5.2",
145
+ "@earendil-works/pi-ai": "1.5.2",
146
+ "@earendil-works/pi-codemode": "1.5.2",
147
+ "@earendil-works/pi-coding-agent": "1.5.2",
148
+ "@earendil-works/pi-durable": "1.5.2",
149
+ "@earendil-works/pi-mcp": "1.5.2",
150
+ "@earendil-works/pi-schema": "1.5.2",
151
+ "@earendil-works/pi-telemetry": "1.5.2",
152
+ "@earendil-works/pi-tui": "1.5.2"
153
153
  },
154
154
  "direct": {
155
155
  "proper-lockfile": "4.1.2"
@@ -32,7 +32,6 @@
32
32
  "WebFetch",
33
33
  "WebSearch"
34
34
  ],
35
- "cortexConnection": "MY_SNOWFLAKE_CONNECTION",
36
35
  "mcpServers": {
37
36
  "entwurf-bridge": {
38
37
  "command": "/path/to/entwurf/mcp/entwurf-bridge/start.sh",
@@ -3,9 +3,8 @@
3
3
  // This is the pi-extension entry point that registers `entwurf` as a pi
4
4
  // session provider/model. It is intentionally THIN: it stands up the provider
5
5
  // surface (every registered adapter's curated rows + the no-auth sentinel) and
6
- // wires streamSimple to the real ACP backend (lib/acp/backend.ts — a
7
- // spawn-per-turn ACP child, `claude-agent-acp` or `cortex acp serve` depending
8
- // on which adapter the model id routes to). It does NOT build a
6
+ // wires streamSimple to the real ACP backend (lib/acp/backend.ts — a Claude
7
+ // ACP child whose lifetime follows pi's lifecycle). It does NOT build a
9
8
  // socket/peers/citizen protocol or touch the v2 core — socket-citizenship is
10
9
  // supplied by the host `--entwurf-control` pi session (AGENTS §ACP Plugin
11
10
  // Boundary).
@@ -13,8 +12,8 @@
13
12
  // The model set comes from `allCuratedModels()` — the union across the adapter
14
13
  // registry (backend-adapter.ts), NOT a per-backend list spelled here. Adding a
15
14
  // backend therefore never edits this file. check-acp-provider-surface pins that
16
- // the compiled entry really registers the EXACT union
17
- // ([QK:CORTEX-PROVIDER-EXACT-ROW-SURFACE]).
15
+ // the compiled entry registers the exact supported Claude-only set
16
+ // ([QK:ACP-PROVIDER-CLAUDE-ONLY]).
18
17
  //
19
18
  // Fence: this entry rides the emit-capable root tsconfig (it is not in the root
20
19
  // `exclude` list); its lib modules are imported with `.js` suffixes (the root
@@ -22,7 +21,7 @@
22
21
  // strip-types fence is introduced.
23
22
 
24
23
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
- import { streamShellAcp } from "./lib/acp/backend.js";
24
+ import { closeRetainedAcpSession, streamShellAcp } from "./lib/acp/backend.js";
26
25
  import { allCuratedModels } from "./lib/acp/backend-adapter.js";
27
26
  import { ENTWURF_ACP_NO_AUTH_SENTINEL, PROVIDER_ID } from "./lib/acp/models.js";
28
27
 
@@ -65,6 +64,13 @@ export default function (pi: ExtensionAPI) {
65
64
  streamSimple: streamShellAcp,
66
65
  });
67
66
 
67
+ // #163, GLG 2026-11-14: a successful compaction closes the Claude child of this Pi session at
68
+ // once. The next turn opens a new child from the compacted transcript (backend.ts
69
+ // `closeRetainedAcpSession`). A session that never used an ACP model has nothing to close.
70
+ pi.on("session_compact", (_event, ctx) => {
71
+ closeRetainedAcpSession(ctx.sessionManager.getSessionId());
72
+ });
73
+
68
74
  // Mark only AFTER a successful registration. If allCuratedModels() (which
69
75
  // fail-fasts on an unowned/duplicate curated id, and whose claude rows carry a
70
76
  // fail-loud anchor check) or registerProvider throws, the runtime is not left
@@ -17,13 +17,12 @@
17
17
  // smokes both drive ONE adapter — the SDK method-name mapping lives here only,
18
18
  // and the backend's orchestration + the gate fakes stay untouched.
19
19
 
20
- import {
21
- type Usage as AcpWireUsage,
22
- AGENT_METHODS,
23
- CLIENT_METHODS,
24
- client,
25
- type Stream,
26
- } from "@agentclientprotocol/sdk";
20
+ import { AGENT_METHODS, CLIENT_METHODS, client, type Stream } from "@agentclientprotocol/sdk";
21
+ // SDK 2.x moved the experimental `Usage` out of the stable root into the `./unstable`
22
+ // entry (`dist/v1/unstable/schema/types.gen.d.ts`, marked UNSTABLE/@experimental). A
23
+ // type-only import: nothing of it reaches the emitted JS, and the wire carries the
24
+ // same `usage` object either way.
25
+ import type { Usage as AcpWireUsage } from "@agentclientprotocol/sdk/unstable";
27
26
  import type { AcpTextBlock } from "./context.js";
28
27
 
29
28
  /**