@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
@@ -27,11 +27,12 @@ import type { AcpConnectionLike } from "./acp-client.js";
27
27
  import { enrichMcpServersWithEnvelope, type ResolvedAcpConfig } from "./config.js";
28
28
  import { loadEngraving } from "./engraving.js";
29
29
  import {
30
+ CLAUDE_ACP_EFFORT_VALUES,
30
31
  CORTEX_MODEL_PREFIX,
31
32
  curatedClaudeModels,
32
33
  curatedCortexModels,
34
+ LEGACY_CORTEX_MODEL_IDS,
33
35
  SUPPORTED_ANTHROPIC_MODEL_IDS,
34
- SUPPORTED_CORTEX_MODEL_IDS,
35
36
  } from "./models.js";
36
37
  import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay, ensureCortexDualHomeOverlay } from "./overlay.js";
37
38
  import { buildClaudeSessionMeta } from "./tool-surface.js";
@@ -57,7 +58,10 @@ export interface AcpLaunchSpec {
57
58
  }
58
59
 
59
60
  /** A curated provider model row (the shape pi.registerProvider({ models }) takes). */
60
- export type AcpModelRow = ReturnType<typeof curatedClaudeModels>[number];
61
+ /** `thinkingLevelMap` is optional: Claude rows declare the effort levels its driver can set (#142);
62
+ * cortex rows declare none and keep pi's default thinking handling. */
63
+ type ClaudeModelRow = ReturnType<typeof curatedClaudeModels>[number];
64
+ export type AcpModelRow = Omit<ClaudeModelRow, "thinkingLevelMap"> & Partial<Pick<ClaudeModelRow, "thinkingLevelMap">>;
61
65
 
62
66
  /** routeModel result — the backend-native model id with the curation prefix stripped. */
63
67
  export interface AcpRoutedModel {
@@ -112,6 +116,23 @@ export interface AcpEnforceModelParams {
112
116
  nativeModelId: string;
113
117
  }
114
118
 
119
+ /** resolveThinking input — pi's requested level and the registered row's level map. `reasoning`
120
+ * is `undefined` both when the pi thinking level is `off` and when none was given: pi-agent-core
121
+ * sends `thinkingLevel === "off" ? undefined : level`, so the provider cannot tell them apart. */
122
+ export interface AcpResolveThinkingParams {
123
+ modelId: string;
124
+ reasoning: string | undefined;
125
+ thinkingLevelMap: Readonly<Record<string, string | null | undefined>> | undefined;
126
+ }
127
+
128
+ /** enforceThinking input — the live connection + acp session + the resolved backend value. */
129
+ export interface AcpEnforceThinkingParams {
130
+ connection: AcpConnectionLike;
131
+ acpSessionId: string;
132
+ modelId: string;
133
+ value: string;
134
+ }
135
+
115
136
  /** ensureOverlay result — env overrides backend.ts merges into the spawn env. */
116
137
  export interface AcpOverlayResult {
117
138
  envOverrides: Record<string, string>;
@@ -211,6 +232,15 @@ export interface AcpBackendAdapter {
211
232
  * launch-pinned backend: no-op here). backend.ts wraps the call in withTimeout. */
212
233
  enforceModel(params: AcpEnforceModelParams): Promise<void>;
213
234
 
235
+ /** OPTIONAL capability (#142): carry pi's thinking level to the backend. Both or neither.
236
+ * `resolveThinking` is PURE — it maps the requested level to the backend value through the
237
+ * registered row's `thinkingLevelMap` and throws a named refusal when there is none, before
238
+ * any process or session is touched. `enforceThinking` sets that value on the live session
239
+ * and refuses unless the answer reads it back. ABSENT (cortex) keeps the old behaviour: the
240
+ * level is not carried, and nothing claims it is. */
241
+ resolveThinking?(params: AcpResolveThinkingParams): string;
242
+ enforceThinking?(params: AcpEnforceThinkingParams): Promise<void>;
243
+
214
244
  /** Backend-specific fields folded into bridgeConfigSignature (reuse invalidation):
215
245
  * connection/profile/env-derived STABLE ids only — never raw env values / secrets.
216
246
  * Reads ONLY this backend's opaque `adapterSettings` (NOT the whole config), so the
@@ -345,7 +375,7 @@ export const CLAUDE_FOREGROUND_ONLY_ENV = { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
345
375
  * The set-model answer is read back before the prompt (#128). claude-agent-acp resolves the
346
376
  * requested id onto one of the rows it offers and answers with that row's `value` as the model
347
377
  * option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). `[측정 2026-10-12]` 0.85.1, no
348
- * prompt: `claude-fable-5-1` answers its own row, while `claude-sonnet-5-5` and `claude-opus-5-5`
378
+ * prompt: `claude-fable-5-1` answers its own row (and, at 0.106.0 #142, so does `claude-fable-5-2`), while `claude-sonnet-5-5` and `claude-opus-5-5`
349
379
  * answer the alias rows `sonnet` and `opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row
350
380
  * resolves to (`ModelInfo.resolvedModel`) is not on the ACP wire, so an alias answer is accepted
351
381
  * only as the exact measured row, value AND name, declared here for the requested id. No other
@@ -385,6 +415,61 @@ export function confirmClaudeSetModelAnswer(response: unknown, nativeModelId: st
385
415
  );
386
416
  }
387
417
 
418
+ const CLAUDE_EFFORT_VALUE_SET: ReadonlySet<string> = new Set(CLAUDE_ACP_EFFORT_VALUES);
419
+
420
+ /** The named refusal for a thinking level ACP Claude cannot carry (#142). The text is fixed and
421
+ * must read as NON-transient to pi's retry and overflow classifiers: replaying the turn cannot
422
+ * change the level that was asked for. */
423
+ export const ACP_THINKING_LEVEL_UNSUPPORTED = "acp-thinking-level-unsupported";
424
+
425
+ /**
426
+ * Map pi's requested level to the claude-agent-acp effort value, or refuse by name (#142).
427
+ * The value is the registered row's `thinkingLevelMap` entry for that level — the documented pi
428
+ * meaning of the map — and must be one of the measured effort values. `undefined` (pi off, or no
429
+ * level) is refused rather than left at the vendor `default`: that would let the turn run at a
430
+ * depth nobody chose while the operator believes a level was applied.
431
+ */
432
+ export function resolveClaudeEffort(
433
+ modelId: string,
434
+ reasoning: string | undefined,
435
+ thinkingLevelMap: Readonly<Record<string, string | null | undefined>> | undefined,
436
+ ): string {
437
+ const value = reasoning === undefined ? undefined : thinkingLevelMap?.[reasoning];
438
+ if (typeof value === "string" && CLAUDE_EFFORT_VALUE_SET.has(value)) return value;
439
+ const asked =
440
+ reasoning === undefined
441
+ ? "no thinking level (pi sends none for `off`, and none when no level is set; the two are indistinguishable here)"
442
+ : `thinking level ${JSON.stringify(reasoning)}`;
443
+ throw new Error(
444
+ `entwurf: ${ACP_THINKING_LEVEL_UNSUPPORTED} — ${modelId} over Claude ACP carries effort ` +
445
+ `${CLAUDE_ACP_EFFORT_VALUES.join("/")} only, and was asked for ${asked}. Choose one of those levels; ` +
446
+ `the turn was not sent, and repeating it at the same level cannot change that.`,
447
+ );
448
+ }
449
+
450
+ function claudeEffortRefusal(modelId: string, value: string, what: string, cause?: unknown): Error {
451
+ const error = new Error(
452
+ `entwurf: claude-agent-acp ${what} for effort ${JSON.stringify(value)} on ${modelId}. Refusing before the prompt ` +
453
+ `so the turn cannot run at an effort nobody chose; repeating the turn cannot change the driver's answer.`,
454
+ );
455
+ if (cause !== undefined) (error as Error & { cause?: unknown }).cause = cause;
456
+ return error;
457
+ }
458
+
459
+ /** Refuse unless the set-effort answer reads back the requested value. */
460
+ export function confirmClaudeEffortAnswer(response: unknown, modelId: string, value: string): void {
461
+ const configOptions = (response as { configOptions?: unknown } | null | undefined)?.configOptions;
462
+ const effort = Array.isArray(configOptions)
463
+ ? (configOptions as Array<Record<string, unknown> | null>).find((option) => option?.id === "effort")
464
+ : undefined;
465
+ if (effort?.currentValue === value) return;
466
+ throw claudeEffortRefusal(
467
+ modelId,
468
+ value,
469
+ effort == null ? "answered with no effort option" : `answered effort ${jsonText(effort.currentValue)}`,
470
+ );
471
+ }
472
+
388
473
  export const claudeAdapter: AcpBackendAdapter = {
389
474
  backend: "claude",
390
475
 
@@ -456,6 +541,24 @@ export const claudeAdapter: AcpBackendAdapter = {
456
541
  confirmClaudeSetModelAnswer(answer, nativeModelId);
457
542
  },
458
543
 
544
+ resolveThinking({ modelId, reasoning, thinkingLevelMap }) {
545
+ return resolveClaudeEffort(modelId, reasoning, thinkingLevelMap);
546
+ },
547
+
548
+ async enforceThinking({ connection, acpSessionId, modelId, value }) {
549
+ const setConfig = connection.setSessionConfigOption;
550
+ if (typeof setConfig !== "function") {
551
+ throw new Error(`setSessionConfigOption unsupported — cannot set effort for ${modelId}`);
552
+ }
553
+ let answer: unknown;
554
+ try {
555
+ answer = await setConfig.call(connection, { sessionId: acpSessionId, configId: "effort", value });
556
+ } catch (err) {
557
+ throw claudeEffortRefusal(modelId, value, "refused the set", err);
558
+ }
559
+ confirmClaudeEffortAnswer(answer, modelId, value);
560
+ },
561
+
459
562
  configSignatureFields(_adapterSettings) {
460
563
  // Claude folds no extra backend-specific fields beyond backend + nativeModelId
461
564
  // (which backend.ts adds). A future backend reads its own stable id off
@@ -480,17 +583,11 @@ export const claudeAdapter: AcpBackendAdapter = {
480
583
  // Registry — modelId → adapter
481
584
  // ---------------------------------------------------------------------------
482
585
 
483
- /** Registered adapters. Order carries NO routing authority — routeModel decides.
484
- * claude (unprefixed ids) + cortex (the `cortex-` prefix). A further backend
485
- * appends here with its own reserved prefix; the fail-fast below proves no two
486
- * adapters claim one id. */
487
- // ---------------------------------------------------------------------------
488
- // cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
489
- // rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
490
- // everything cortex-specific lives here + models.ts + overlay.ts + the gates.
491
- // ---------------------------------------------------------------------------
586
+ // The former Cortex implementation is retained privately for historical source
587
+ // continuity only. The exported adapter below denies every side-effecting entry,
588
+ // and the production registry does not contain this implementation.
492
589
 
493
- const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(SUPPORTED_CORTEX_MODEL_IDS);
590
+ const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(LEGACY_CORTEX_MODEL_IDS);
494
591
 
495
592
  /** Cortex's OWN settings: a Snowflake connection name, or null. Opaque to
496
593
  * config.ts / backend.ts — only cortexAdapter reads it (casting back). */
@@ -503,7 +600,7 @@ export interface CortexAdapterSettings {
503
600
  * ENTWURF_ACP_* convention (PR #40's legacy PI_SHELL_ACP* cortex-connection var renamed). */
504
601
  export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
505
602
 
506
- export const cortexAdapter: AcpBackendAdapter = {
603
+ const retiredCortexAdapter: AcpBackendAdapter = {
507
604
  backend: "cortex",
508
605
 
509
606
  // Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
@@ -649,7 +746,31 @@ export const cortexAdapter: AcpBackendAdapter = {
649
746
  // errorMessage. Borrowing claude's `incomplete_tool_call` reader would be a guess.
650
747
  };
651
748
 
652
- const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
749
+ const cortexDisabled = (): never => {
750
+ throw new Error("entwurf: cortex-acp-disabled: Cortex ACP is not supported");
751
+ };
752
+
753
+ /** Retained exported shape refuses the retired adapter before a launch or overlay can be made. */
754
+ export const cortexAdapter: AcpBackendAdapter = {
755
+ ...retiredCortexAdapter,
756
+ routeModel: () => undefined,
757
+ curatedModels: () => [],
758
+ resolveAdapterSettings: cortexDisabled,
759
+ resolveLaunch: cortexDisabled,
760
+ launchEnvDefaults: cortexDisabled,
761
+ ensureOverlay: cortexDisabled,
762
+ loadCarrier: cortexDisabled,
763
+ buildSessionMeta: cortexDisabled,
764
+ enforceModel: cortexDisabled,
765
+ configSignatureFields: cortexDisabled,
766
+ };
767
+
768
+ const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter];
769
+
770
+ /** A declared backend is only a guard, never a replacement routing authority. */
771
+ export function assertSupportedDeclaredBackend(backend: string | undefined): void {
772
+ if (backend?.toLowerCase() === "cortex") cortexDisabled();
773
+ }
653
774
 
654
775
  /**
655
776
  * Resolve the backend adapter that owns `modelId`.
@@ -662,6 +783,9 @@ const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
662
783
  * so callers get `{ adapter, nativeModelId }` and never re-parse the id string.
663
784
  */
664
785
  export function resolveAcpBackendAdapter(modelId: string): { adapter: AcpBackendAdapter; nativeModelId: string } {
786
+ if (modelId.toLowerCase().startsWith(CORTEX_MODEL_PREFIX)) {
787
+ throw new Error(`entwurf: cortex-acp-disabled: ${JSON.stringify(modelId)} is not supported`);
788
+ }
665
789
  const matches: Array<{ adapter: AcpBackendAdapter; nativeModelId: string }> = [];
666
790
  for (const adapter of ADAPTERS) {
667
791
  const routed = adapter.routeModel(modelId);