@junghanacs/entwurf 0.12.10 → 0.13.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 (86) hide show
  1. package/AGENTS.md +6 -3
  2. package/BASELINE.md +56 -163
  3. package/CHANGELOG.md +35 -0
  4. package/CONTRIBUTING.md +4 -2
  5. package/DELIVERY.md +117 -261
  6. package/README.md +74 -399
  7. package/VERIFY.md +59 -85
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +231 -0
  10. package/docs/external-mcp-host.md +132 -0
  11. package/docs/fresh-cut-policy.md +99 -0
  12. package/docs/setup-clean-host.md +123 -328
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +155 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -5
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
  21. package/mcp/entwurf-bridge/src/index.ts +21 -48
  22. package/package.json +14 -9
  23. package/pi/settings.reference.json +1 -1
  24. package/pi-extensions/acp-provider.ts +20 -10
  25. package/pi-extensions/entwurf-control.ts +20 -49
  26. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  27. package/pi-extensions/lib/acp/augment.ts +99 -2
  28. package/pi-extensions/lib/acp/backend-adapter.ts +190 -14
  29. package/pi-extensions/lib/acp/backend.ts +310 -37
  30. package/pi-extensions/lib/acp/config.ts +19 -5
  31. package/pi-extensions/lib/acp/engraving.ts +46 -2
  32. package/pi-extensions/lib/acp/event-mapper.ts +26 -16
  33. package/pi-extensions/lib/acp/models.ts +69 -7
  34. package/pi-extensions/lib/acp/overlay.ts +234 -5
  35. package/pi-extensions/lib/acp/tool-surface.ts +12 -4
  36. package/prompts/engraving.md +13 -5
  37. package/run.sh +386 -119
  38. package/scripts/check-acp-carrier-augment.ts +246 -10
  39. package/scripts/check-acp-cortex.ts +668 -0
  40. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  41. package/scripts/check-acp-provider-surface.ts +50 -6
  42. package/scripts/check-acp-sdk-surface.ts +60 -21
  43. package/scripts/check-acp-session-reuse.ts +64 -1
  44. package/scripts/check-acp-stop-reason.ts +342 -0
  45. package/scripts/check-entwurf-v2-surface.ts +41 -0
  46. package/scripts/check-gate-qualification.ts +7 -2
  47. package/scripts/check-probe-cli-shim.ts +879 -0
  48. package/scripts/check-probe-ordering.ts +2482 -0
  49. package/scripts/check-release-gate-outcomes.ts +332 -0
  50. package/scripts/check-shell-quote.ts +4 -4
  51. package/scripts/fixtures/probe-cli-shim +20 -0
  52. package/scripts/fixtures/probe-mcp-server.ts +168 -12
  53. package/scripts/lib/live-skip.ts +33 -0
  54. package/scripts/lib/probe-acp-turn.ts +228 -0
  55. package/scripts/lib/probe-cli-shim.ts +464 -0
  56. package/scripts/lib/probe-cli-target.ts +165 -0
  57. package/scripts/lib/probe-event-log.ts +383 -0
  58. package/scripts/lib/probe-verdict.ts +1213 -0
  59. package/scripts/lib/step-outcome.sh +88 -0
  60. package/scripts/mutants/acp-augment.json +106 -0
  61. package/scripts/mutants/acp-cortex.json +196 -0
  62. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  63. package/scripts/mutants/acp-stop-reason.json +80 -0
  64. package/scripts/mutants/probe-ordering.json +1054 -0
  65. package/scripts/mutants/release-gate.json +105 -0
  66. package/scripts/mutants/v2-surface.json +26 -15
  67. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  68. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  69. package/scripts/smoke-acp-cortex-live.ts +386 -0
  70. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  71. package/scripts/smoke-acp-mcp-live.ts +2 -2
  72. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  73. package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
  74. package/scripts/smoke-acp-overlay-live.ts +2 -2
  75. package/scripts/smoke-acp-provider-live.ts +2 -2
  76. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  77. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  78. package/scripts/smoke-acp-skill-live.ts +2 -2
  79. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  80. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  81. package/scripts/smoke-agy-native-push-live.ts +4 -3
  82. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  83. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  84. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  85. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
  86. package/scripts/smoke-meta-install-state.sh +4 -0
@@ -1,9 +1,10 @@
1
1
  // ACP backend adapter rail — the PRODUCT seam by which a curated model id selects
2
- // which ACP backend (claude / future backend / …) drives a turn. See docs/acp-backend-rail.md §9.
2
+ // which ACP backend (claude / future backend / …) drives a turn. See the adapter contract in
3
+ // docs/acp-backend-rail.md.
3
4
  //
4
5
  // This seam is DISTINCT from `AcpTurnDeps` (backend.ts), which is the test/runtime
5
- // seam (fake spawn/connection/clock for the gates). The two are kept apart on
6
- // purpose (GPT-agreed §9-2): merging them would make a fake-deps fixture look like
6
+ // seam (fake spawn/connection/clock for the gates). Merging them would make a
7
+ // fake-deps fixture look like
7
8
  // a fake backend and force the adapter to carry clock/sessionDir/createConnection.
8
9
  // The wiring is `defaultDeps(adapter)` — the turn loop in backend.ts stays
9
10
  // backend-invariant; only these per-backend functions change with `adapter`.
@@ -19,15 +20,32 @@
19
20
 
20
21
  import { readFileSync } from "node:fs";
21
22
  import { createRequire } from "node:module";
23
+ import { homedir } from "node:os";
22
24
  import { dirname, join } from "node:path";
23
25
 
24
26
  import type { AcpConnectionLike } from "./acp-client.js";
25
- import type { ResolvedAcpConfig } from "./config.js";
27
+ import { enrichMcpServersWithEnvelope, type ResolvedAcpConfig } from "./config.js";
26
28
  import { loadEngraving } from "./engraving.js";
27
- import { curatedClaudeModels, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./models.js";
28
- import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay } from "./overlay.js";
29
+ import {
30
+ CORTEX_MODEL_PREFIX,
31
+ curatedClaudeModels,
32
+ curatedCortexModels,
33
+ SUPPORTED_ANTHROPIC_MODEL_IDS,
34
+ SUPPORTED_CORTEX_MODEL_IDS,
35
+ } from "./models.js";
36
+ import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay, ensureCortexDualHomeOverlay } from "./overlay.js";
29
37
  import { buildClaudeSessionMeta } from "./tool-surface.js";
30
38
 
39
+ // POSIX-safe single-quote wrapper for shell arg interpolation. Byte-for-byte
40
+ // identical to the reference in entwurf-core.ts; PARITY-PINNED by
41
+ // scripts/check-shell-quote.ts (SOURCE_SITES). Used only by the cortex override
42
+ // path below, where operator-configured connection/model tokens are appended to
43
+ // an operator `bash -lc` string — quoting keeps a connection name with shell
44
+ // metacharacters from being reinterpreted by the shell.
45
+ function shellQuote(value: string): string {
46
+ return `'${value.replace(/'/g, `'\\''`)}'`;
47
+ }
48
+
31
49
  // ---------------------------------------------------------------------------
32
50
  // Shared shapes
33
51
  // ---------------------------------------------------------------------------
@@ -64,13 +82,19 @@ export interface AcpCarrierParams {
64
82
 
65
83
  /** ensureOverlay input — cwd + (native) model id + the resolved config. A backend
66
84
  * whose overlay/env depends on its OWN settings reads them off `config.adapterSettings`
67
- * here; backend.ts never inspects config. Same shape as AcpLaunchParams (overlay and
68
- * launch are distinct phases, so they keep distinct names). */
85
+ * here; backend.ts never inspects config. Same shape as AcpLaunchParams plus the
86
+ * session key (overlay and launch are distinct phases, so they keep distinct names). */
69
87
  export interface AcpOverlayParams {
70
88
  cwd: string;
71
89
  modelId: string;
72
90
  nativeModelId: string;
73
91
  config: ResolvedAcpConfig;
92
+ /** The AUTHORITATIVE per-session key backend.ts already computed
93
+ * (resolveSessionKey: opts.sessionId → PI_SESSION_ID → cwd). A session-scoped
94
+ * overlay MUST scope on this value, never on an ambient re-derivation — the
95
+ * re-derived form drops `opts.sessionId`, so two sessions in one process/cwd
96
+ * would alias one overlay (GPT review 2026-07-29, cortex P0-1). */
97
+ sessionKey: string;
74
98
  }
75
99
 
76
100
  /** buildSessionMeta input — mirrors the newSession `_meta` inputs. */
@@ -194,7 +218,7 @@ function resolveClaudeLaunch(): AcpLaunchSpec {
194
218
  export const claudeAdapter: AcpBackendAdapter = {
195
219
  backend: "claude",
196
220
 
197
- // Claude owns its UNPREFIXED curated ids only (GPT-agreed §9-1). The native id
221
+ // Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
198
222
  // equals the curated id — claude carries no curation prefix to strip.
199
223
  routeModel(modelId) {
200
224
  return SUPPORTED_CLAUDE_IDS.has(modelId) ? { nativeModelId: modelId } : undefined;
@@ -270,14 +294,166 @@ export const claudeAdapter: AcpBackendAdapter = {
270
294
  // ---------------------------------------------------------------------------
271
295
 
272
296
  /** Registered adapters. Order carries NO routing authority — routeModel decides.
273
- * Step A: claude only. A second backend appends here with its reserved prefix
274
- * (e.g. `<backend>-*`), and the fail-fast below proves no two adapters claim one id. */
275
- const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter];
297
+ * claude (unprefixed ids) + cortex (the `cortex-` prefix). A further backend
298
+ * appends here with its own reserved prefix; the fail-fast below proves no two
299
+ * adapters claim one id. */
300
+ // ---------------------------------------------------------------------------
301
+ // cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
302
+ // rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
303
+ // everything cortex-specific lives here + models.ts + overlay.ts + the gates.
304
+ // ---------------------------------------------------------------------------
305
+
306
+ const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(SUPPORTED_CORTEX_MODEL_IDS);
307
+
308
+ /** Cortex's OWN settings: a Snowflake connection name, or null. Opaque to
309
+ * config.ts / backend.ts — only cortexAdapter reads it (casting back). */
310
+ export interface CortexAdapterSettings {
311
+ cortexConnection: string | null;
312
+ }
313
+
314
+ /** The env var an operator sets to pin a Snowflake connection per-shell without
315
+ * editing settings.json. Wins over `entwurfProvider.cortexConnection`. The
316
+ * ENTWURF_ACP_* convention (PR #40's legacy PI_SHELL_ACP* cortex-connection var renamed). */
317
+ export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
318
+
319
+ export const cortexAdapter: AcpBackendAdapter = {
320
+ backend: "cortex",
321
+
322
+ // Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
323
+ // native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
324
+ routeModel(modelId) {
325
+ if (!SUPPORTED_CORTEX_IDS.has(modelId)) return undefined;
326
+ return { nativeModelId: modelId.slice(CORTEX_MODEL_PREFIX.length) };
327
+ },
328
+
329
+ curatedModels() {
330
+ return curatedCortexModels();
331
+ },
332
+
333
+ // Cortex's ONLY own setting is the connection name. env override wins over
334
+ // settings (per-shell pin); empty/whitespace → null (Cortex falls back to its
335
+ // own default connection). A non-string settings value fails loud.
336
+ resolveAdapterSettings({ mergedBlock, projectBlock, globalPath, projectPath }): CortexAdapterSettings {
337
+ const raw = mergedBlock.cortexConnection;
338
+ if (raw !== undefined && typeof raw !== "string") {
339
+ const offending = projectBlock.cortexConnection !== undefined ? projectPath : globalPath;
340
+ throw new Error(`${offending}: invalid entwurfProvider settings: cortexConnection must be a string`);
341
+ }
342
+ const envConn = process.env[CORTEX_CONNECTION_ENV]?.trim();
343
+ const settingsConn = raw?.trim();
344
+ const cortexConnection = envConn || settingsConn || null;
345
+ return { cortexConnection };
346
+ },
347
+
348
+ // `cortex acp serve` resolved from PATH (the CLI itself IS the ACP server — no
349
+ // `*-acp` npm package, unlike claude). `-c <conn>` appended when a connection
350
+ // is pinned. NO `-m`: the model is enforced per-turn via
351
+ // session/set_config_option (enforceModel below, CP0-M measured GO) and a
352
+ // launch pin would be a SECOND model authority that drifts from it.
353
+ // CORTEX_ACP_COMMAND override runs via `bash -lc` with the selection flags
354
+ // appended so the bridge's choice wins (later yargs args override earlier ones).
355
+ resolveLaunch({ config }) {
356
+ const settings = config.adapterSettings as CortexAdapterSettings | undefined;
357
+ const connection = settings?.cortexConnection?.trim() || undefined;
358
+ const selectionArgs: string[] = [];
359
+ if (connection) selectionArgs.push("-c", connection);
360
+ const override = process.env.CORTEX_ACP_COMMAND?.trim();
361
+ if (override) {
362
+ const command = selectionArgs.length > 0 ? `${override} ${selectionArgs.map(shellQuote).join(" ")}` : override;
363
+ return { command: "bash", args: ["-lc", command] };
364
+ }
365
+ return { command: "cortex", args: ["acp", "serve", ...selectionArgs] };
366
+ },
367
+
368
+ // The overlay location is SESSION-SCOPED (never static), so the spawn env
369
+ // rides ensureOverlay(...).envOverrides; there is no static launch env. The
370
+ // v1.1.8-era CORTEX_DISABLE_AUTO_APPLY_PROFILES knob was retired with the
371
+ // dual-HOME redesign: profiles now live inside the overlay-owned isolated
372
+ // home (empty by construction), and the knob is unmeasured on v1.1.52.
373
+ launchEnvDefaults() {
374
+ return {};
375
+ },
376
+
377
+ // Dual-HOME containment (CP0 D2/D3/D9/D10 — see the overlay module header):
378
+ // refuse an ambient CORTEX_HOME outright, then materialize the session-scoped
379
+ // isolated HOME with auth symlinks, `autoUpdate:false`, and the mcp.json
380
+ // projection of the envelope-enriched explicit servers (cortex ignores the
381
+ // wire mcpServers param, so this file IS how tools reach a cortex session).
382
+ ensureOverlay({ modelId, config, sessionKey }) {
383
+ // D3 — presence refusal, empty string included: upstream's resolver treats
384
+ // a set-but-empty CORTEX_HOME differently from unset, and one ambient value
385
+ // would silently bypass SNOWFLAKE_HOME (the probe's CLAUDE_CODE_EXECUTABLE
386
+ // precondition is the same family). Refuse the ambiguity; never pick a side.
387
+ if ("CORTEX_HOME" in process.env) {
388
+ throw new Error(
389
+ "entwurf: CORTEX_HOME is present in the environment (empty string included) — it overrides " +
390
+ "SNOWFLAKE_HOME inside cortex and would bypass the dual-HOME overlay entirely (CP0 D3). " +
391
+ "Unset it to run a cortex ACP turn.",
392
+ );
393
+ }
394
+ // The scope authority is the AUTHORITATIVE params.sessionKey backend.ts
395
+ // computed — never an ambient re-derivation, which would drop opts.sessionId
396
+ // and alias two same-process/cwd sessions onto one overlay (P0-1). The
397
+ // envelope below still reads PI_SESSION_ID: that is the identity CARRIER for
398
+ // the bridge child (the same source the turn loop's wire enrichment uses),
399
+ // a different axis from overlay-dir scoping.
400
+ const piSessionId = process.env.PI_SESSION_ID?.trim() || undefined;
401
+ const enriched = enrichMcpServersWithEnvelope(config.mcpServers, { modelId, piSessionId });
402
+ const overlay = ensureCortexDualHomeOverlay({
403
+ scopeKey: sessionKey,
404
+ mcpServers: enriched,
405
+ realHome: homedir(),
406
+ });
407
+ return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
408
+ },
409
+
410
+ // System-prompt-carrier-less (ACP rail “Cortex Code audit”): Cortex ACP exposes no
411
+ // `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
412
+ // equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
413
+ // unexplored and deliberately not part of this contract.) loadCarrier returns
414
+ // null WITHOUT calling loadEngraving, so the cortex turn never touches the
415
+ // shipped-engraving / appendSystemPrompt signature; buildSessionMeta returns
416
+ // undefined so backend.ts omits the `_meta` key entirely. The operator
417
+ // engraving instead rides the first-user augment (augment.ts).
418
+ loadCarrier() {
419
+ return null;
420
+ },
421
+
422
+ buildSessionMeta() {
423
+ return undefined;
424
+ },
425
+
426
+ // Per-turn enforcement via session/set_config_option — the SAME wire call the
427
+ // claude adapter makes, measured live against cortex v1.1.52 (CP0-M): the
428
+ // option id is "model", accepted values are the NATIVE ids (`auto`,
429
+ // `claude-sonnet-5`, `openai-gpt-5.4`, …), and a value cortex no longer
430
+ // serves fails loud BEFORE the prompt (`Unsupported model: …`). PR #40's
431
+ // launch-time `-m` pin was retired for this: set-model is the single model
432
+ // authority (resolveLaunch never passes `-m`), and "auto" is set explicitly
433
+ // rather than treated as an unspoken default.
434
+ async enforceModel({ connection, acpSessionId, nativeModelId, modelId }) {
435
+ const setConfig = connection.setSessionConfigOption;
436
+ if (typeof setConfig !== "function") {
437
+ throw new Error(`setSessionConfigOption unsupported — cannot enforce model ${modelId}`);
438
+ }
439
+ await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
440
+ },
441
+
442
+ // A connection change must invalidate a reused session (rail: Adapter contract). Flat,
443
+ // sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
444
+ // + `nativeModelId` are added by backend.ts.
445
+ configSignatureFields(adapterSettings) {
446
+ const settings = adapterSettings as CortexAdapterSettings | undefined;
447
+ return { cortexConnection: settings?.cortexConnection ?? null };
448
+ },
449
+ };
450
+
451
+ const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
276
452
 
277
453
  /**
278
454
  * Resolve the backend adapter that owns `modelId`.
279
455
  *
280
- * GPT-agreed §9-1 fail-fast contract:
456
+ * Routing fail-fast contract (rail “Adapter contract”):
281
457
  * - 0 matches → throw (unknown model — no silent default).
282
458
  * - 2+ matches → throw (prefix collision — a startup-visible registry bug).
283
459
  *
@@ -301,7 +477,7 @@ export function resolveAcpBackendAdapter(modelId: string): { adapter: AcpBackend
301
477
  }
302
478
 
303
479
  /** Every curated model row across all registered adapters — for provider registration.
304
- * GPT-agreed §9-6 registration-time fail-fast: every curated id must route to EXACTLY
480
+ * Registration-time fail-fast (rail “Adapter contract”): every curated id must route to EXACTLY
305
481
  * one adapter and no id may be duplicated across adapters. Catching it here means a
306
482
  * prefix-collision / duplicate surfaces at provider registration, not mid-turn. */
307
483
  export function allCuratedModels(): AcpModelRow[] {