@junghanacs/entwurf 0.16.0 → 0.17.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 (48) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +337 -0
  3. package/README.md +8 -11
  4. package/VERIFY.md +8 -1
  5. package/demo/README.md +1 -1
  6. package/docs/acp-backend-rail.md +25 -14
  7. package/docs/setup-clean-host.md +24 -10
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +34 -10
  10. package/package.json +10 -10
  11. package/pi-extensions/lib/acp/acp-client.ts +57 -4
  12. package/pi-extensions/lib/acp/backend-adapter.ts +78 -9
  13. package/pi-extensions/lib/acp/backend.ts +578 -18
  14. package/pi-extensions/lib/acp/claude-acp-launch.js +100 -0
  15. package/pi-extensions/lib/acp/event-mapper.ts +43 -6
  16. package/run.sh +129 -32
  17. package/scripts/check-acp-launch-namespace.ts +127 -0
  18. package/scripts/check-acp-prompt-lifecycle.ts +145 -2
  19. package/scripts/check-acp-stop-reason.ts +8 -2
  20. package/scripts/check-acp-usage-accounting.ts +1074 -0
  21. package/scripts/check-copilot-birth-hook.ts +28 -1
  22. package/scripts/check-gate-qualification.ts +4 -2
  23. package/scripts/check-omp-fresh-preflight.ts +27 -0
  24. package/scripts/check-setup-qualification.sh +40 -2
  25. package/scripts/copilot-bridge-oracle.sh +14 -6
  26. package/scripts/fake-copilot-vendor.sh +4 -2
  27. package/scripts/lib/pi-record-discovery.ts +47 -0
  28. package/scripts/mutants/acp-launch-namespace.json +34 -0
  29. package/scripts/mutants/acp-prompt-lifecycle.json +67 -2
  30. package/scripts/mutants/acp-stream-hooks.json +4 -2
  31. package/scripts/mutants/acp-usage-accounting.json +181 -0
  32. package/scripts/mutants/copilot-birth.json +3 -5
  33. package/scripts/mutants/pack-install.json +2 -2
  34. package/scripts/mutants/setup-verdict.json +35 -0
  35. package/scripts/omp-config-xdev.py +310 -0
  36. package/scripts/omp-config-xdev.sh +76 -0
  37. package/scripts/omp-tool-surface.py +61 -10
  38. package/scripts/raw-acp-child-exit-measure/README.md +285 -0
  39. package/scripts/raw-acp-child-exit-measure/acp-turn-population.py +89 -0
  40. package/scripts/raw-acp-child-exit-measure/reaper-correlation.py +47 -0
  41. package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
  42. package/scripts/smoke-acp-cortex-live.ts +2 -2
  43. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  44. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  45. package/scripts/smoke-acp-v2-send-live.ts +2 -2
  46. package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
  47. package/scripts/smoke-mux-lifecycle-live.ts +46 -2
  48. package/scripts/smoke-setup-verdict.sh +48 -3
@@ -16,7 +16,7 @@
16
16
  // This module owns the `AcpConnectionLike` seam so the backend and the live
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
- import { AGENT_METHODS, CLIENT_METHODS, client } from "@agentclientprotocol/sdk";
19
+ import { AGENT_METHODS, CLIENT_METHODS, client, } from "@agentclientprotocol/sdk";
20
20
  /**
21
21
  * Production factory — wrap the SDK 1.1 fluent `client()` into the
22
22
  * `AcpConnectionLike` seam the backend (and the live smokes) drive.
@@ -17,10 +17,9 @@
17
17
  //
18
18
  // Fence: imported by the root program with `.js` suffixes, same as the sibling
19
19
  // lib/acp modules — no new strip-types fence.
20
- import { readFileSync } from "node:fs";
21
20
  import { createRequire } from "node:module";
22
21
  import { homedir } from "node:os";
23
- import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
24
23
  import { enrichMcpServersWithEnvelope } from "./config.js";
25
24
  import { loadEngraving } from "./engraving.js";
26
25
  import { CORTEX_MODEL_PREFIX, curatedClaudeModels, curatedCortexModels, SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_CORTEX_MODEL_IDS, } from "./models.js";
@@ -39,19 +38,29 @@ function shellQuote(value) {
39
38
  // claude adapter — the first implementation (the rail's reference backend)
40
39
  // ---------------------------------------------------------------------------
41
40
  const SUPPORTED_CLAUDE_IDS = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
42
- /** Resolve the claude-agent-acp launch — package bin (resolve), env override for debug.
43
- * This is the single source for the claude launch spec; backend.ts holds no private copy. */
41
+ /**
42
+ * Resolve the claude launch an ENTWURF-OWNED launcher, or the env override for debug.
43
+ * This is the single source for the claude launch spec; backend.ts holds no private copy.
44
+ *
45
+ * The default no longer names the vendor bin directly. `claude-acp-launch.js`
46
+ * imports it in-process; that file's header carries the reason (#72: a janitor
47
+ * for another harness selects `claude-agent-acp` by argv substring and SIGTERMs
48
+ * it by age, and the vendor's own handler erases the signal into exit 0).
49
+ *
50
+ * `CLAUDE_AGENT_ACP_COMMAND` is an EXPLICIT operator override and is deliberately
51
+ * NOT routed through the launcher: an operator who names their own command owns
52
+ * the result, including the loss of the name split and the signal observation.
53
+ */
44
54
  function resolveClaudeLaunch() {
45
55
  const override = process.env.CLAUDE_AGENT_ACP_COMMAND?.trim();
46
56
  if (override)
47
57
  return { command: "bash", args: ["-lc", override] };
58
+ // Resolved here (not inside the launcher's own directory lookup) so a missing
59
+ // vendor package still fails at launch resolution, where it always failed.
48
60
  const require = createRequire(import.meta.url);
49
- const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
50
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
51
- const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
52
- if (!binPath)
53
- throw new Error("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
54
- return { command: process.execPath, args: [join(dirname(pkgJsonPath), binPath)] };
61
+ require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
62
+ const launcher = fileURLToPath(new URL("./claude-acp-launch.js", import.meta.url));
63
+ return { command: process.execPath, args: [launcher] };
55
64
  }
56
65
  export const claudeAdapter = {
57
66
  backend: "claude",
@@ -112,6 +121,13 @@ export const claudeAdapter = {
112
121
  // `_adapterSettings` here.
113
122
  return {};
114
123
  },
124
+ // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
125
+ // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
126
+ // and a context-occupancy scalar (`used`; both read at 0.73.0
127
+ // `dist/acp-agent.js:2918-2924`). Both are measured, so backend.ts seals them.
128
+ // The token partition is NOT declared here: ACP's only token carrier is a
129
+ // per-turn round-trip aggregate, which is not what pi's four fields mean.
130
+ sealsTurnAccounting: true,
115
131
  };
116
132
  // ---------------------------------------------------------------------------
117
133
  // Registry — modelId → adapter
@@ -250,6 +266,14 @@ export const cortexAdapter = {
250
266
  const settings = adapterSettings;
251
267
  return { cortexConnection: settings?.cortexConnection ?? null };
252
268
  },
269
+ // NO sealsTurnAccounting — a DELIBERATE, permanent absence, not an omission to be
270
+ // filled in later by symmetry with claude. Nobody has measured what cortex's
271
+ // ACP `usage` (and its usage_update cost, if it sends one) MEAN: whether the
272
+ // token counts are a turn delta or a session total, and against which price
273
+ // table. Until that measurement exists, the honest report is no report:
274
+ // backend.ts seals nothing for a backend without this flag, so cortex's
275
+ // emitted usage is byte-identical to what it was before #93. Declaring this
276
+ // with a guess would mint exactly the silent misaccounting #93 exists to end.
253
277
  };
254
278
  const ADAPTERS = [claudeAdapter, cortexAdapter];
255
279
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -82,23 +82,23 @@
82
82
  "claudeCodeFloor": ">=2.1.217"
83
83
  },
84
84
  "dependencies": {
85
- "@agentclientprotocol/claude-agent-acp": "0.70.0",
86
- "@agentclientprotocol/sdk": "1.3.0",
85
+ "@agentclientprotocol/claude-agent-acp": "0.73.0",
86
+ "@agentclientprotocol/sdk": "1.4.0",
87
87
  "@anthropic-ai/sdk": "0.100.1",
88
88
  "@modelcontextprotocol/sdk": "^1.12.1",
89
89
  "zod": "^3.25.0 || ^4.0.0"
90
90
  },
91
91
  "peerDependencies": {
92
- "@earendil-works/pi-ai": ">=0.84.3 <0.85",
93
- "@earendil-works/pi-coding-agent": ">=0.84.3 <0.85",
94
- "@earendil-works/pi-tui": ">=0.84.3 <0.85",
92
+ "@earendil-works/pi-ai": ">=0.84.4 <0.85",
93
+ "@earendil-works/pi-coding-agent": ">=0.84.4 <0.85",
94
+ "@earendil-works/pi-tui": ">=0.84.4 <0.85",
95
95
  "typebox": "*"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@biomejs/biome": "^2.4.13",
99
- "@earendil-works/pi-ai": "0.84.3",
100
- "@earendil-works/pi-coding-agent": "0.84.3",
101
- "@earendil-works/pi-tui": "0.84.3",
99
+ "@earendil-works/pi-ai": "0.84.4",
100
+ "@earendil-works/pi-coding-agent": "0.84.4",
101
+ "@earendil-works/pi-tui": "0.84.4",
102
102
  "@types/node": "^24.3.0",
103
103
  "husky": "^9.1.7",
104
104
  "rregex": "1.13.1",
@@ -165,7 +165,7 @@
165
165
  "check:toolchain": "pnpm lint && pnpm typecheck",
166
166
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
167
167
  "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-resume-args && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment",
168
- "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
168
+ "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
169
169
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
170
170
  },
171
171
  "pi": {
@@ -17,14 +17,68 @@
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 { AGENT_METHODS, CLIENT_METHODS, client, type Stream } from "@agentclientprotocol/sdk";
20
+ import {
21
+ type Usage as AcpWireUsage,
22
+ AGENT_METHODS,
23
+ CLIENT_METHODS,
24
+ client,
25
+ type Stream,
26
+ } from "@agentclientprotocol/sdk";
21
27
  import type { AcpTextBlock } from "./context.js";
22
28
 
29
+ /**
30
+ * What `session/prompt` answers with — entwurf's own narrow view of the SDK's
31
+ * `PromptResponse`.
32
+ *
33
+ * `stopReason` stays `string | undefined` DELIBERATELY, not the SDK's closed
34
+ * `StopReason` union: backend.ts's verdict mapping owes an honest answer for an
35
+ * unknown or absent reason (it seals those as errors), and typing the field as
36
+ * the closed union would make that branch look unreachable.
37
+ *
38
+ * `usage` reuses the SDK's own `Usage` type rather than a hand-copy, so a field
39
+ * rename upstream is a typecheck failure here instead of a silent zero. It is
40
+ * marked `@experimental` upstream and its per-field comments say "across all
41
+ * turns/session" while the outer one says "for this turn" — that contradiction
42
+ * is why NO common code interprets this shape — and #93 then measured that the
43
+ * counts are a per-turn ROUND-TRIP AGGREGATE, which is not what any of pi's four
44
+ * `Usage` fields mean. It is read in exactly ONE place, `sealTurnUsage`, which
45
+ * relays it verbatim onto `usage.acp` (an accounting key, never a per-request
46
+ * shape) and never onto those four. A field rename upstream fails the typecheck
47
+ * here rather than silently zeroing the operator's cache-effect badge.
48
+ */
49
+ /** One `_meta.quota.token_count` row (claude-agent-acp 0.73.0
50
+ * `dist/acp-agent.js:5750-5765`). `cachedInputTokens` is cache READS — the name
51
+ * differs from `usage.cachedReadTokens` because the shape is shared with
52
+ * codex-acp; `cachedWriteTokens` is Claude's extra sibling. */
53
+ export type AcpQuotaTokenCount = {
54
+ totalTokens?: number | null;
55
+ inputTokens?: number | null;
56
+ cachedInputTokens?: number | null;
57
+ cachedWriteTokens?: number | null;
58
+ outputTokens?: number | null;
59
+ };
60
+
61
+ export type AcpPromptResponse = {
62
+ stopReason?: string;
63
+ usage?: AcpWireUsage | null;
64
+ /** Vendor-private, version-pinned. `_meta` is a standard ACP extension slot
65
+ * whose values a client may not assume, and `quota` is NOT in
66
+ * claude-agent-acp's exported types — it is produced by the private
67
+ * `turnQuotaMeta()` (read at 0.73.0 `dist/acp-agent.js:5738-5748`). Read
68
+ * defensively, never structurally required, and re-measure on a pin move. */
69
+ _meta?: {
70
+ quota?: {
71
+ token_count?: AcpQuotaTokenCount | null;
72
+ model_usage?: ReadonlyArray<{ model?: string; token_count?: AcpQuotaTokenCount | null }> | null;
73
+ } | null;
74
+ } | null;
75
+ };
76
+
23
77
  /** The subset of the ACP agent connection the backend drives (real or fake). */
24
78
  export interface AcpConnectionLike {
25
79
  initialize(params: unknown): Promise<unknown>;
26
80
  newSession(params: unknown): Promise<{ sessionId?: string }>;
27
- prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<{ stopReason?: string }>;
81
+ prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<AcpPromptResponse>;
28
82
  setSessionConfigOption?(params: unknown): Promise<unknown>;
29
83
  /**
30
84
  * ACP `session/cancel` — the PROTOCOL way to end an in-flight prompt turn.
@@ -84,8 +138,7 @@ export function connectAcpClient(stream: Stream, handlers: AcpClientHandlers): A
84
138
  initialize: (params) => agent.request(AGENT_METHODS.initialize, params as never),
85
139
  newSession: (params) =>
86
140
  agent.request(AGENT_METHODS.session_new, params as never) as Promise<{ sessionId?: string }>,
87
- prompt: (params) =>
88
- agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<{ stopReason?: string }>,
141
+ prompt: (params) => agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<AcpPromptResponse>,
89
142
  setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params as never),
90
143
  cancel: (params) => {
91
144
  // Notification, not a request: nothing resolves it, and the connection
@@ -18,10 +18,9 @@
18
18
  // Fence: imported by the root program with `.js` suffixes, same as the sibling
19
19
  // lib/acp modules — no new strip-types fence.
20
20
 
21
- import { readFileSync } from "node:fs";
22
21
  import { createRequire } from "node:module";
23
22
  import { homedir } from "node:os";
24
- import { dirname, join } from "node:path";
23
+ import { fileURLToPath } from "node:url";
25
24
 
26
25
  import type { AcpConnectionLike } from "./acp-client.js";
27
26
  import { enrichMcpServersWithEnvelope, type ResolvedAcpConfig } from "./config.js";
@@ -132,6 +131,30 @@ export interface AcpAdapterSettingsParams {
132
131
  projectPath: string;
133
132
  }
134
133
 
134
+ /**
135
+ * NO turn-usage evidence type lives here any more, and its absence is the #93
136
+ * finding, not an omission.
137
+ *
138
+ * ACP reports a turn's tokens ONLY as the SUM OVER THAT TURN'S API ROUND TRIPS
139
+ * (measured 2026-09-02: one 21-round-trip turn summed to cacheRead 4,185,084
140
+ * while the context it occupied was 223,516 — the overlay ledger's 21 rows add
141
+ * up to the four numbers `PromptResponse.usage` reported). pi's four `Usage`
142
+ * fields are not that quantity: `isContextOverflow` reads `input + cacheRead`
143
+ * as ONE REQUEST's prompt size (read at pi-ai `dist/utils/overflow.js:132-145`)
144
+ * and `cache-stats.detectMiss` reads input, cache-read, and cache-write as
145
+ * one request's prompt shape (read at pi-coding-agent
146
+ * `dist/core/cache-stats.js:14-37`). Projecting the turn
147
+ * aggregate into them fired a false overflow that compacted a live 223k session
148
+ * on a 1M window, invented two phantom cache misses, and SILENCED the one real
149
+ * 195,177-token miss the operator needed to see.
150
+ *
151
+ * During stream handling the vendor keeps a per-message snapshot in
152
+ * `lastAssistantUsage`, but its `usage_update` publishes only the scalar `used`
153
+ * value (read at claude-agent-acp 0.73.0 `dist/acp-agent.js:3273-3297`). Until
154
+ * that partition is carried on the wire, NOTHING honest can go in pi's four
155
+ * fields, so entwurf writes none of them.
156
+ */
157
+
135
158
  // ---------------------------------------------------------------------------
136
159
  // The adapter interface
137
160
  // ---------------------------------------------------------------------------
@@ -194,6 +217,24 @@ export interface AcpBackendAdapter {
194
217
  * sorted-stable primitive map (JSON.stringify determinism — no nested objects /
195
218
  * non-deterministic order). `backend` + `nativeModelId` are added by backend.ts. */
196
219
  configSignatureFields(adapterSettings: unknown): Record<string, unknown>;
220
+
221
+ /** Declares that THIS backend's `usage_update` cost semantics have been
222
+ * MEASURED, so backend.ts may seal its context occupancy and its per-turn
223
+ * cost from the session baseline.
224
+ *
225
+ * ABSENT means no measured semantics — permanent and static, not an omission
226
+ * to fill in by symmetry (cortex). backend.ts then seals NOTHING: no
227
+ * occupancy, no cost baseline, no diff. That is what keeps this lane's
228
+ * accounting out of a backend whose wire meaning nobody has measured.
229
+ *
230
+ * This is deliberately NOT a method returning token evidence. It was one
231
+ * until #93 measured that ACP's only token carrier is a turn aggregate that
232
+ * pi's four `Usage` fields cannot honestly hold (see the note above
233
+ * `AcpBackendAdapter`). With no token axis left to extract, presence alone
234
+ * carries everything the seal needs, and a flag that gates only the two axes
235
+ * entwurf actually reads from a NOTIFICATION cannot desync from a response
236
+ * shape it never inspects. */
237
+ sealsTurnAccounting?: true;
197
238
  }
198
239
 
199
240
  // ---------------------------------------------------------------------------
@@ -202,17 +243,28 @@ export interface AcpBackendAdapter {
202
243
 
203
244
  const SUPPORTED_CLAUDE_IDS: ReadonlySet<string> = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
204
245
 
205
- /** Resolve the claude-agent-acp launch — package bin (resolve), env override for debug.
206
- * This is the single source for the claude launch spec; backend.ts holds no private copy. */
246
+ /**
247
+ * Resolve the claude launch an ENTWURF-OWNED launcher, or the env override for debug.
248
+ * This is the single source for the claude launch spec; backend.ts holds no private copy.
249
+ *
250
+ * The default no longer names the vendor bin directly. `claude-acp-launch.js`
251
+ * imports it in-process; that file's header carries the reason (#72: a janitor
252
+ * for another harness selects `claude-agent-acp` by argv substring and SIGTERMs
253
+ * it by age, and the vendor's own handler erases the signal into exit 0).
254
+ *
255
+ * `CLAUDE_AGENT_ACP_COMMAND` is an EXPLICIT operator override and is deliberately
256
+ * NOT routed through the launcher: an operator who names their own command owns
257
+ * the result, including the loss of the name split and the signal observation.
258
+ */
207
259
  function resolveClaudeLaunch(): AcpLaunchSpec {
208
260
  const override = process.env.CLAUDE_AGENT_ACP_COMMAND?.trim();
209
261
  if (override) return { command: "bash", args: ["-lc", override] };
262
+ // Resolved here (not inside the launcher's own directory lookup) so a missing
263
+ // vendor package still fails at launch resolution, where it always failed.
210
264
  const require = createRequire(import.meta.url);
211
- const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
212
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { bin?: string | Record<string, string> };
213
- const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
214
- if (!binPath) throw new Error("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
215
- return { command: process.execPath, args: [join(dirname(pkgJsonPath), binPath)] };
265
+ require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
266
+ const launcher = fileURLToPath(new URL("./claude-acp-launch.js", import.meta.url));
267
+ return { command: process.execPath, args: [launcher] };
216
268
  }
217
269
 
218
270
  export const claudeAdapter: AcpBackendAdapter = {
@@ -287,6 +339,14 @@ export const claudeAdapter: AcpBackendAdapter = {
287
339
  // `_adapterSettings` here.
288
340
  return {};
289
341
  },
342
+
343
+ // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
344
+ // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
345
+ // and a context-occupancy scalar (`used`; both read at 0.73.0
346
+ // `dist/acp-agent.js:2918-2924`). Both are measured, so backend.ts seals them.
347
+ // The token partition is NOT declared here: ACP's only token carrier is a
348
+ // per-turn round-trip aggregate, which is not what pi's four fields mean.
349
+ sealsTurnAccounting: true,
290
350
  };
291
351
 
292
352
  // ---------------------------------------------------------------------------
@@ -446,6 +506,15 @@ export const cortexAdapter: AcpBackendAdapter = {
446
506
  const settings = adapterSettings as CortexAdapterSettings | undefined;
447
507
  return { cortexConnection: settings?.cortexConnection ?? null };
448
508
  },
509
+
510
+ // NO sealsTurnAccounting — a DELIBERATE, permanent absence, not an omission to be
511
+ // filled in later by symmetry with claude. Nobody has measured what cortex's
512
+ // ACP `usage` (and its usage_update cost, if it sends one) MEAN: whether the
513
+ // token counts are a turn delta or a session total, and against which price
514
+ // table. Until that measurement exists, the honest report is no report:
515
+ // backend.ts seals nothing for a backend without this flag, so cortex's
516
+ // emitted usage is byte-identical to what it was before #93. Declaring this
517
+ // with a guess would mint exactly the silent misaccounting #93 exists to end.
449
518
  };
450
519
 
451
520
  const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];