@junghanacs/entwurf 0.12.2 → 0.12.3

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 (37) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +3 -5
  4. package/VERIFY.md +4 -4
  5. package/demo/README.md +5 -5
  6. package/demo/demo-baseline.sh +2 -2
  7. package/demo/demo.sh +3 -3
  8. package/docs/setup-clean-host.md +5 -5
  9. package/package.json +9 -9
  10. package/pi/entwurf-targets.json +3 -6
  11. package/pi-extensions/lib/acp/acp-client.ts +2 -2
  12. package/pi-extensions/lib/acp/models.ts +7 -9
  13. package/run.sh +21 -21
  14. package/scripts/check-acp-backend-preflight.ts +1 -1
  15. package/scripts/check-acp-config.ts +3 -3
  16. package/scripts/check-acp-event-mapper.ts +1 -1
  17. package/scripts/check-acp-provider-surface.ts +1 -1
  18. package/scripts/check-acp-sdk-surface.ts +22 -22
  19. package/scripts/check-acp-session-reuse.ts +3 -3
  20. package/scripts/check-acp-session-store.ts +6 -6
  21. package/scripts/check-acp-tool-surface.ts +2 -2
  22. package/scripts/check-entwurf-session-identity.ts +9 -9
  23. package/scripts/check-model-lock.ts +2 -2
  24. package/scripts/check-package-source-routing.ts +4 -4
  25. package/scripts/meta-bridge-doctor.sh +1 -1
  26. package/scripts/meta-bridge-state.py +2 -0
  27. package/scripts/resolve-acp-bridge.ts +1 -1
  28. package/scripts/smoke-acp-bundled-mcp-live.ts +1 -1
  29. package/scripts/smoke-acp-carrier-augment-live.ts +1 -1
  30. package/scripts/smoke-acp-mcp-live.ts +1 -1
  31. package/scripts/smoke-acp-memory-containment-live.ts +8 -9
  32. package/scripts/smoke-acp-overlay-live.ts +8 -9
  33. package/scripts/smoke-acp-provider-live.ts +1 -1
  34. package/scripts/smoke-acp-raw-turn-live.ts +12 -11
  35. package/scripts/smoke-acp-session-reuse-live.ts +24 -9
  36. package/scripts/smoke-acp-skill-live.ts +1 -1
  37. package/scripts/smoke-meta-install-state.sh +3 -3
@@ -1,16 +1,16 @@
1
1
  // Deterministic gate for the S2a ACP SDK dependency surface.
2
2
  //
3
- // Pins the ACP runtime deps to the 0.11.0 behavior-oracle versions and locks
3
+ // Pins the ACP runtime deps to the current behavior-oracle versions and locks
4
4
  // the peer-resolution that makes the Claude ACP adapter satisfiable:
5
5
  //
6
- // @agentclientprotocol/sdk 0.29.0 wire SDK (acp-bridge import source)
7
- // @agentclientprotocol/claude-agent-acp 0.50.0 Claude adapter (spawn binary)
6
+ // @agentclientprotocol/sdk 1.1.0 wire SDK (acp-bridge import source)
7
+ // @agentclientprotocol/claude-agent-acp 0.54.1 Claude adapter (spawn binary)
8
8
  // @anthropic-ai/sdk 0.100.1 peer-resolution pin ONLY (see below)
9
9
  //
10
10
  // The anthropic SDK is NOT an API client / auth surface here. It is a direct
11
- // dep solely to satisfy @anthropic-ai/claude-agent-sdk@0.3.186's peer floor
11
+ // dep solely to satisfy @anthropic-ai/claude-agent-sdk@0.3.197's peer floor
12
12
  // (>=0.93.0); drop it and the tree resolves a stale 0.91.1 so the peer goes
13
- // unmet — a failure that would only surface at the first raw turn. 0.11.0's
13
+ // unmet — a failure that would only surface at the first raw turn. The
14
14
  // lockfile proves the same shape. Source-level import / API-client
15
15
  // instantiation / credential use stays forbidden — asserted in layer (4).
16
16
  // (GPT hard constraint 2, revised 2026-06-18: direct dep allowed ONLY as an
@@ -42,15 +42,15 @@ const read = (p: string): string => readFileSync(resolve(repoRoot, p), "utf8");
42
42
  const pkg = JSON.parse(read("package.json")) as { dependencies?: Record<string, string> };
43
43
  const deps = pkg.dependencies ?? {};
44
44
  const PINS: Record<string, string> = {
45
- "@agentclientprotocol/sdk": "0.29.0",
46
- "@agentclientprotocol/claude-agent-acp": "0.50.0",
45
+ "@agentclientprotocol/sdk": "1.1.0",
46
+ "@agentclientprotocol/claude-agent-acp": "0.54.1",
47
47
  [ANTHROPIC_SDK]: "0.100.1",
48
48
  };
49
49
  for (const [name, ver] of Object.entries(PINS)) {
50
50
  assert.equal(
51
51
  deps[name],
52
52
  ver,
53
- `package.json dependencies["${name}"] must be exact "${ver}" (got "${deps[name]}") — S2a pins the 0.11.0 oracle versions`,
53
+ `package.json dependencies["${name}"] must be exact "${ver}" (got "${deps[name]}") — S2a pins the current oracle versions`,
54
54
  );
55
55
  }
56
56
 
@@ -63,13 +63,13 @@ for (const [name, ver] of Object.entries(PINS)) {
63
63
  const lock = read("pnpm-lock.yaml");
64
64
  assert.match(
65
65
  lock,
66
- /@agentclientprotocol\/claude-agent-acp@0\.50\.0\(@anthropic-ai\/sdk@0\.100\.1/,
67
- "pnpm-lock: claude-agent-acp@0.50.0 must peer-resolve @anthropic-ai/sdk@0.100.1 (peer-pin), not the stale 0.91.1",
66
+ /@agentclientprotocol\/claude-agent-acp@0\.54\.1\(@anthropic-ai\/sdk@0\.100\.1/,
67
+ "pnpm-lock: claude-agent-acp@0.54.1 must peer-resolve @anthropic-ai/sdk@0.100.1 (peer-pin), not the stale 0.91.1",
68
68
  );
69
69
  assert.match(
70
70
  lock,
71
- /@anthropic-ai\/claude-agent-sdk@0\.3\.186\(@anthropic-ai\/sdk@0\.100\.1/,
72
- "pnpm-lock: claude-agent-sdk@0.3.186 must peer-resolve @anthropic-ai/sdk@0.100.1 — else its >=0.93.0 peer floor is unmet",
71
+ /@anthropic-ai\/claude-agent-sdk@0\.3\.197\(@anthropic-ai\/sdk@0\.100\.1/,
72
+ "pnpm-lock: claude-agent-sdk@0.3.197 must peer-resolve @anthropic-ai/sdk@0.100.1 — else its >=0.93.0 peer floor is unmet",
73
73
  );
74
74
 
75
75
  // ---------------------------------------------------------------------------
@@ -108,8 +108,8 @@ const casEntry = adapterRequire.resolve("@anthropic-ai/claude-agent-sdk");
108
108
  const casInfo = pkgInfoFromEntry(casEntry);
109
109
  assert.equal(
110
110
  casInfo.version,
111
- "0.3.186",
112
- `@anthropic-ai/claude-agent-sdk must runtime-resolve to 0.3.186 from the adapter context (got ${casInfo.version})`,
111
+ "0.3.197",
112
+ `@anthropic-ai/claude-agent-sdk must runtime-resolve to 0.3.197 from the adapter context (got ${casInfo.version})`,
113
113
  );
114
114
  const casRequire = createRequire(resolve(casInfo.dir, "package.json"));
115
115
  const sdkEntry = casRequire.resolve(ANTHROPIC_SDK);
@@ -121,19 +121,19 @@ assert.equal(
121
121
  );
122
122
 
123
123
  // ---------------------------------------------------------------------------
124
- // (2c) adapter-context wire-SDK + MCP-SDK runtime resolve — the 0.50 bump's real
125
- // fault surface. Lock text (layer 2) freezes the publish floor; these probe
124
+ // (2c) adapter-context wire-SDK + MCP-SDK runtime resolve — the ACP dep bump's
125
+ // real fault surface. Lock text (layer 2) freezes the publish floor; these probe
126
126
  // the LIVE module graph the adapter actually traverses: the adapter must SEE
127
- // the same wire SDK the backend imports at root (0.29.0), and claude-agent-sdk
127
+ // the same wire SDK the backend imports at root (1.1.0), and claude-agent-sdk
128
128
  // must SEE its declared MCP peer (1.29.x). Cheap edges, both newly relevant
129
- // after the 0.39→0.50 / 0.220.29 bump.
129
+ // after the 0.50→0.54 / 0.291.1 bump.
130
130
  // ---------------------------------------------------------------------------
131
131
  const wireEntry = adapterRequire.resolve("@agentclientprotocol/sdk");
132
132
  const wireInfo = pkgInfoFromEntry(wireEntry);
133
133
  assert.equal(
134
134
  wireInfo.version,
135
- "0.29.0",
136
- `@agentclientprotocol/sdk must runtime-resolve to 0.29.0 from the adapter context (got ${wireInfo.version}) — the adapter and the backend must share one wire SDK`,
135
+ "1.1.0",
136
+ `@agentclientprotocol/sdk must runtime-resolve to 1.1.0 from the adapter context (got ${wireInfo.version}) — the adapter and the backend must share one wire SDK`,
137
137
  );
138
138
  // @modelcontextprotocol/sdk gates its bare specifier behind "exports", so a
139
139
  // require.resolve of the package ROOT throws (no resolvable entry) — read the
@@ -143,12 +143,12 @@ assert.equal(
143
143
  const mcpPkg = JSON.parse(read("node_modules/@modelcontextprotocol/sdk/package.json")) as { version?: string };
144
144
  assert.ok(
145
145
  typeof mcpPkg.version === "string" && mcpPkg.version.startsWith("1.29."),
146
- `@modelcontextprotocol/sdk must be 1.29.x (got ${mcpPkg.version}) — claude-agent-sdk 0.3.186 declares a ^1.29.0 peer`,
146
+ `@modelcontextprotocol/sdk must be 1.29.x (got ${mcpPkg.version}) — claude-agent-sdk 0.3.197 declares a ^1.29.0 peer`,
147
147
  );
148
148
 
149
149
  // ---------------------------------------------------------------------------
150
150
  // (3) @agentclientprotocol/sdk value-export surface (silent-rename gate)
151
- // The 0.11.0 acp-bridge imports these from the wire SDK; a silent upstream
151
+ // The ACP bridge imports these from the wire SDK; a silent upstream
152
152
  // rename would not fail typecheck (type-only erasure) but would break the
153
153
  // raw turn. Assert the *value* exports exist at runtime.
154
154
  // ---------------------------------------------------------------------------
@@ -32,7 +32,7 @@ import { resolve } from "node:path";
32
32
  import { pathToFileURL } from "node:url";
33
33
  import type { Api, AssistantMessageEvent, Context, Message, Model } from "@earendil-works/pi-ai";
34
34
 
35
- const sonnet = { id: "claude-sonnet-4-6" } as unknown as Model<Api>;
35
+ const sonnet = { id: "claude-sonnet-5" } as unknown as Model<Api>;
36
36
  const opus = { id: "claude-opus-4-8" } as unknown as Model<Api>;
37
37
 
38
38
  type Stream = AsyncIterable<AssistantMessageEvent> & {
@@ -488,7 +488,7 @@ try {
488
488
  );
489
489
  const d1 = deltaText(t1);
490
490
  const iPrep = d1.indexOf("[acp: preparing claude session]");
491
- const iReady = d1.indexOf("[acp: session ready model=claude-sonnet-4-6]");
491
+ const iReady = d1.indexOf("[acp: session ready model=claude-sonnet-5]");
492
492
  const iSent = d1.indexOf("[acp: sending prompt]");
493
493
  assert.ok(
494
494
  iPrep >= 0 && iReady > iPrep && iSent > iReady,
@@ -517,7 +517,7 @@ try {
517
517
  messages: [
518
518
  { role: "user", content: "real user line", timestamp: 0 },
519
519
  mkAssistant([
520
- { type: "text", text: "\n[acp: session ready model=claude-sonnet-4-6]\n", textSignature: MARKER },
520
+ { type: "text", text: "\n[acp: session ready model=claude-sonnet-5]\n", textSignature: MARKER },
521
521
  { type: "text", text: "real assistant line" },
522
522
  ]),
523
523
  ],
@@ -43,8 +43,8 @@ import {
43
43
 
44
44
  const baseInput = (): BridgeConfigInput => ({
45
45
  backend: "claude",
46
- modelId: "claude-sonnet-4-6",
47
- nativeModelId: "claude-sonnet-4-6",
46
+ modelId: "claude-sonnet-5",
47
+ nativeModelId: "claude-sonnet-5",
48
48
  appendSystemPrompt: "",
49
49
  mcpServersHash: "deadbeef",
50
50
  settingSources: [],
@@ -61,7 +61,7 @@ const baseInput = (): BridgeConfigInput => ({
61
61
  {
62
62
  const sig0 = bridgeConfigSignature(baseInput());
63
63
  assert.ok(isSha256Hex(sig0), "config signature is a sha256 digest (no raw carrier text on disk)");
64
- assert.ok(!sig0.includes("claude-sonnet-4-6"), "config signature is a digest — never embeds the raw modelId/carrier");
64
+ assert.ok(!sig0.includes("claude-sonnet-5"), "config signature is a digest — never embeds the raw modelId/carrier");
65
65
  assert.equal(sig0, bridgeConfigSignature(baseInput()), "signature is deterministic");
66
66
  // Array copies do not affect equality (same content).
67
67
  assert.equal(bridgeConfigSignature({ ...baseInput(), settingSources: [] }), sig0, "empty settingSources stable");
@@ -178,7 +178,7 @@ const baseInput = (): BridgeConfigInput => ({
178
178
 
179
179
  const facts = (over: Partial<SessionCompatFacts> = {}): SessionCompatFacts => ({
180
180
  cwd: "/w",
181
- modelId: "claude-sonnet-4-6",
181
+ modelId: "claude-sonnet-5",
182
182
  bridgeConfigSignature: bridgeConfigSignature(baseInput()),
183
183
  contextMessageSignatures: ["user:text:a"],
184
184
  ...over,
@@ -201,7 +201,7 @@ const baseInput = (): BridgeConfigInput => ({
201
201
  const sig = bridgeConfigSignature(baseInput());
202
202
  const compatFacts: SessionCompatFacts = {
203
203
  cwd: "/w",
204
- modelId: "claude-sonnet-4-6",
204
+ modelId: "claude-sonnet-5",
205
205
  bridgeConfigSignature: sig,
206
206
  contextMessageSignatures: ["user:text:a"],
207
207
  };
@@ -305,7 +305,7 @@ const baseInput = (): BridgeConfigInput => ({
305
305
  {
306
306
  const facts: SessionCompatFacts = {
307
307
  cwd: "/w",
308
- modelId: "claude-sonnet-4-6",
308
+ modelId: "claude-sonnet-5",
309
309
  bridgeConfigSignature: bridgeConfigSignature(baseInput()),
310
310
  contextMessageSignatures: ["user:text:a"],
311
311
  };
@@ -90,7 +90,7 @@ assert.ok(nativeThrew, "non-claude backend exposing write natively must throw wh
90
90
  // ---------------------------------------------------------------------------
91
91
 
92
92
  const baseParams = {
93
- modelId: "claude-sonnet-4-6",
93
+ modelId: "claude-sonnet-5",
94
94
  tools: DEFAULT_CLAUDE_TOOLS,
95
95
  permissionAllow: DEFAULT_CLAUDE_PERMISSION_ALLOW,
96
96
  disallowedTools: DEFAULT_CLAUDE_DISALLOWED_TOOLS,
@@ -101,7 +101,7 @@ const baseParams = {
101
101
 
102
102
  const meta = buildClaudeSessionMeta(baseParams);
103
103
  const opts = (meta.claudeCode as { options: Record<string, unknown> }).options;
104
- assert.equal(opts.model, "claude-sonnet-4-6", "model must propagate");
104
+ assert.equal(opts.model, "claude-sonnet-5", "model must propagate");
105
105
  assert.deepEqual(opts.tools, [...DEFAULT_CLAUDE_TOOLS], "tools must propagate (copied)");
106
106
  assert.deepEqual(
107
107
  (opts.settings as { permissions: { allow: string[] } }).permissions.allow,
@@ -132,7 +132,7 @@ try {
132
132
  { p: "entwurf", m: "claude-opus-4-8", title: "Review substrate smoke", tags: ["entwurf", "review"] },
133
133
  { p: "openai-codex", m: "gpt-5.5", title: "async resume check", tags: ["entwurf", "smoke"] },
134
134
  { p: "entwurf", m: "gemini-3.1-pro-preview", title: "vision team", tags: ["entwurf"] },
135
- { p: "entwurf", m: "claude-sonnet-4-6", title: "manual session", tags: [] as string[] },
135
+ { p: "entwurf", m: "claude-sonnet-5", title: "manual session", tags: [] as string[] },
136
136
  ];
137
137
  for (const c of cases) {
138
138
  const sid = generateSessionId(new Date(2026, 5, 3, 19, 12, 45));
@@ -252,7 +252,7 @@ try {
252
252
  tags: ["entwurf"],
253
253
  });
254
254
  eq(parseSessionName(goodName)?.model, recorded, "name model mirrors recorded model");
255
- const driftName = "20260603T191245-deadbe==entwurf/claude-sonnet-4-6--x__entwurf";
255
+ const driftName = "20260603T191245-deadbe==entwurf/claude-sonnet-5--x__entwurf";
256
256
  ok(parseSessionName(driftName)?.model !== recorded, "model drift is detectable for fail-fast");
257
257
 
258
258
  // ---- T-collision: header scan is authority; spawn pre-check ----
@@ -440,12 +440,12 @@ try {
440
440
  bigFile,
441
441
  `${JSON.stringify({ type: "session", id: "20260603T210000-bbbbbb", cwd: "/b" })}\n` +
442
442
  msg({ content: bigBody, model: "m-big", provider: "p-big", usage: { cost: { total: 1 } } }) +
443
- msg({ content: "final", model: "claude-sonnet-4-6", provider: "entwurf", usage: { cost: { total: 0.5 } } }),
443
+ msg({ content: "final", model: "claude-sonnet-5", provider: "entwurf", usage: { cost: { total: 0.5 } } }),
444
444
  );
445
445
  const a2 = analyzeSessionFileLike(bigFile);
446
446
  eq(a2.turns, 2, "analyze(big): turns correct across chunk boundaries");
447
447
  eq(a2.lastAssistantText, "final", "analyze(big): trailing turn intact after multi-chunk line");
448
- eq(a2.lastModel, "claude-sonnet-4-6", "analyze(big): trailing model intact");
448
+ eq(a2.lastModel, "claude-sonnet-5", "analyze(big): trailing model intact");
449
449
  eq(Math.round(a2.cost * 100) / 100, 1.5, "analyze(big): cost summed across large line");
450
450
 
451
451
  // ---- T-identity: resume identity authority = FIRST model_change ----
@@ -513,21 +513,21 @@ try {
513
513
  const cleanName = buildSessionName({
514
514
  sessionId: idE,
515
515
  provider: "entwurf",
516
- model: "claude-sonnet-4-6",
516
+ model: "claude-sonnet-5",
517
517
  rawTitle: "clean resume",
518
518
  tags: ["entwurf", "async"],
519
519
  });
520
520
  fs.writeFileSync(
521
521
  fileE,
522
522
  sessionLine(idE, "/identity/e") +
523
- mc("entwurf", "claude-sonnet-4-6") +
523
+ mc("entwurf", "claude-sonnet-5") +
524
524
  infoLine(cleanName) +
525
- msg({ content: "ok", model: "claude-sonnet-4-6", provider: "entwurf" }),
525
+ msg({ content: "ok", model: "claude-sonnet-5", provider: "entwurf" }),
526
526
  );
527
527
  noThrow(() => readSessionIdentity(fileE), "identity: clean session does not throw");
528
528
  const recE = readSessionIdentity(fileE);
529
529
  eq(recE?.provider, "entwurf", "identity(clean): provider");
530
- eq(recE?.modelId, "claude-sonnet-4-6", "identity(clean): modelId mirrors name");
530
+ eq(recE?.modelId, "claude-sonnet-5", "identity(clean): modelId mirrors name");
531
531
 
532
532
  // 6. no model_change → null (caller refuses with its own no-recorded-model result).
533
533
  const idF = "20260603T220000-6666ff";
@@ -583,7 +583,7 @@ try {
583
583
  );
584
584
  eq(
585
585
  readSessionIdentity(fileE, { requireEntwurf: true })?.modelId,
586
- "claude-sonnet-4-6",
586
+ "claude-sonnet-5",
587
587
  "requireEntwurf: accepted Entwurf session returns first-model_change identity",
588
588
  );
589
589
 
@@ -210,7 +210,7 @@ async function makeHarness(opts: MakeHarnessOpts = {}): Promise<Harness> {
210
210
  // Test models.
211
211
  const NATIVE_A: ModelLike = { provider: "openai-codex", id: "gpt-5.4" };
212
212
  const NATIVE_B: ModelLike = { provider: "openai-codex", id: "gpt-5.5" };
213
- const PSA_SONNET: ModelLike = { provider: "entwurf", id: "claude-sonnet-4-6" };
213
+ const PSA_SONNET: ModelLike = { provider: "entwurf", id: "claude-sonnet-5" };
214
214
  const PSA_OPUS: ModelLike = { provider: "entwurf", id: "claude-opus-4-8" };
215
215
 
216
216
  let passed = 0;
@@ -250,7 +250,7 @@ await run("2. native → entwurf: revert + warning notify", async () => {
250
250
  assert.equal(h.notifyCalls.length, 1);
251
251
  assert.equal(h.notifyCalls[0].level, "warning");
252
252
  assert.match(h.notifyCalls[0].message, /locked to openai-codex\/gpt-5\.4/);
253
- assert.match(h.notifyCalls[0].message, /entwurf\/claude-sonnet-4-6/);
253
+ assert.match(h.notifyCalls[0].message, /entwurf\/claude-sonnet-5/);
254
254
  });
255
255
 
256
256
  await run("3. entwurf → native: revert + warning notify", async () => {
@@ -74,7 +74,7 @@ function rmInstall(...segs: string[]): void {
74
74
  fs.rmSync(path.join(tmpAgent, ...segs), { recursive: true, force: true });
75
75
  }
76
76
 
77
- const ACP: ResolvedTarget = { provider: "entwurf", model: "claude-sonnet-4-6", explicitOnly: false };
77
+ const ACP: ResolvedTarget = { provider: "entwurf", model: "claude-sonnet-5", explicitOnly: false };
78
78
  const eArg = (r: { args: string[] }): string | undefined => {
79
79
  const i = r.args.indexOf("-e");
80
80
  return i >= 0 ? r.args[i + 1] : undefined;
@@ -211,7 +211,7 @@ check("project-scope source (unseen in user settings) fails fast on remote", ()
211
211
  // (resume callers fail-fast; no -e injected).
212
212
  check("resume recorded entwurf + remote + no source → unresolvedAcpIntent", () => {
213
213
  setSource(null);
214
- const r = getEntwurfExplicitExtensions("claude-sonnet-4-6", true, "entwurf");
214
+ const r = getEntwurfExplicitExtensions("claude-sonnet-5", true, "entwurf");
215
215
  assert.equal(r.unresolvedAcpIntent, true);
216
216
  assert.equal(eArg(r), undefined);
217
217
  assert.ok(r.warnings.length > 0, "should warn about the unresolved bridge");
@@ -220,7 +220,7 @@ check("resume recorded entwurf + remote + no source → unresolvedAcpIntent", ()
220
220
  // 13. recorded provider=entwurf + LOCAL → self-root resolves, no fail-fast.
221
221
  check("resume recorded entwurf + local resolves via self-root", () => {
222
222
  setSource(null);
223
- const r = getEntwurfExplicitExtensions("claude-sonnet-4-6", false, "entwurf");
223
+ const r = getEntwurfExplicitExtensions("claude-sonnet-5", false, "entwurf");
224
224
  assert.ok(!r.unresolvedAcpIntent, "local self-root should resolve");
225
225
  assert.equal(eArg(r), REPO_ROOT);
226
226
  assert.equal(r.provider, "entwurf");
@@ -242,7 +242,7 @@ check("resume Codex-via-ACP opt-in + remote + no source → unresolvedAcpIntent"
242
242
  // fail-fast — because the legacy pi-claude-code-use bridge may exist.
243
243
  check("resume Claude-only heuristic + remote + no source stays warning-only", () => {
244
244
  setSource(null);
245
- const r = getEntwurfExplicitExtensions("claude-sonnet-4-6", true, undefined);
245
+ const r = getEntwurfExplicitExtensions("claude-sonnet-5", true, undefined);
246
246
  assert.ok(!r.unresolvedAcpIntent, "Claude heuristic must not fail-fast");
247
247
  assert.ok(r.warnings.length > 0, "should warn");
248
248
  assert.equal(eArg(r), undefined);
@@ -117,7 +117,7 @@ PY
117
117
  EXPECTED_STATUSLINE="$REPO/scripts/meta-bridge-statusline.sh"
118
118
  if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command is repo-owned: $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
119
119
  if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
120
- SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-4-6"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
120
+ SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
121
121
  if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q ' cc | s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
122
122
  else
123
123
  bad "cannot validate statusline without python3"
@@ -80,6 +80,8 @@ MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
80
80
  ("showTurnDuration", ["showTurnDuration"], False),
81
81
  ("terminalProgressBarEnabled", ["terminalProgressBarEnabled"], False),
82
82
  ("useAutoModeDuringPlan", ["useAutoModeDuringPlan"], False),
83
+ ("enableWorkflows", ["enableWorkflows"], False),
84
+ ("workflowKeywordTriggerEnabled", ["workflowKeywordTriggerEnabled"], False),
83
85
  ]
84
86
 
85
87
 
@@ -16,7 +16,7 @@ import type { ResolvedTarget } from "../pi-extensions/lib/entwurf-core.ts";
16
16
  import { getRegistryRouting } from "../pi-extensions/lib/entwurf-core.ts";
17
17
 
18
18
  const isRemote = process.argv[2] === "remote";
19
- const target: ResolvedTarget = { provider: "entwurf", model: "claude-sonnet-4-6", explicitOnly: false };
19
+ const target: ResolvedTarget = { provider: "entwurf", model: "claude-sonnet-5", explicitOnly: false };
20
20
 
21
21
  // Throws EntwurfRoutingError (non-zero exit) when the bridge cannot be resolved —
22
22
  // exactly the fail-fast the smoke wants to observe as a failure, not a pass.
@@ -46,7 +46,7 @@ import { generateSessionId } from "../pi-extensions/lib/entwurf-core.ts";
46
46
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
47
47
 
48
48
  const ACP_PROVIDER = "entwurf";
49
- const ACP_MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
49
+ const ACP_MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
50
50
 
51
51
  const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
52
52
  const SOCKET_SUFFIX = ".sock";
@@ -47,7 +47,7 @@ import { dirname, join, resolve } from "node:path";
47
47
  import { fileURLToPath } from "node:url";
48
48
 
49
49
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
50
- const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
50
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
51
51
  const PROVIDER = "entwurf";
52
52
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
53
53
 
@@ -29,7 +29,7 @@ import { fileURLToPath } from "node:url";
29
29
 
30
30
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
31
  const PROBE_SERVER = join(REPO_ROOT, "scripts", "fixtures", "probe-mcp-server.ts");
32
- const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
32
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
33
33
  const PROVIDER = "entwurf";
34
34
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
35
35
 
@@ -69,7 +69,7 @@ import {
69
69
  } from "../pi-extensions/lib/acp/tool-surface.ts";
70
70
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
71
71
 
72
- const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_MEMORY_MODEL ?? "claude-sonnet-4-6";
72
+ const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_MEMORY_MODEL ?? "claude-sonnet-5";
73
73
  const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK === "1";
74
74
  const RAW_TAIL_CAP = 64 * 1024;
75
75
 
@@ -78,15 +78,14 @@ function fail(msg: string): never {
78
78
  process.exit(1);
79
79
  }
80
80
 
81
- const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
82
-
83
81
  function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
84
- return Promise.race([
85
- p,
86
- sleep(ms).then((): never => {
87
- throw new Error(`${label} timed out after ${ms}ms`);
88
- }),
89
- ]);
82
+ let timer: ReturnType<typeof setTimeout> | undefined;
83
+ const timeout = new Promise<never>((_, reject) => {
84
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
85
+ });
86
+ return Promise.race([p, timeout]).finally(() => {
87
+ if (timer) clearTimeout(timer);
88
+ });
90
89
  }
91
90
 
92
91
  /** Recursively list files under `dir` whose path contains a `memory` segment. */
@@ -56,7 +56,7 @@ import {
56
56
  } from "../pi-extensions/lib/acp/tool-surface.ts";
57
57
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
58
58
 
59
- const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_OVERLAY_MODEL ?? "claude-sonnet-4-6";
59
+ const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_OVERLAY_MODEL ?? "claude-sonnet-5";
60
60
  const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK === "1";
61
61
  const RAW_TAIL_CAP = 64 * 1024;
62
62
 
@@ -65,15 +65,14 @@ function fail(msg: string): never {
65
65
  process.exit(1);
66
66
  }
67
67
 
68
- const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
69
-
70
68
  function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
71
- return Promise.race([
72
- p,
73
- sleep(ms).then((): never => {
74
- throw new Error(`${label} timed out after ${ms}ms`);
75
- }),
76
- ]);
69
+ let timer: ReturnType<typeof setTimeout> | undefined;
70
+ const timeout = new Promise<never>((_, reject) => {
71
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
72
+ });
73
+ return Promise.race([p, timeout]).finally(() => {
74
+ if (timer) clearTimeout(timer);
75
+ });
77
76
  }
78
77
 
79
78
  if (process.env.LIVE !== "1") {
@@ -28,7 +28,7 @@ import { dirname, join, resolve } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
29
 
30
30
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
- const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
31
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
32
32
  const PROVIDER = "entwurf";
33
33
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
34
34
 
@@ -3,7 +3,7 @@
3
3
  // LIVE=1 ./run.sh smoke-acp-raw-turn-live
4
4
  //
5
5
  // What this proves (and ONLY this): the pinned Claude ACP adapter
6
- // (@agentclientprotocol/claude-agent-acp@0.39.0) spawns, speaks the ACP wire
6
+ // (@agentclientprotocol/claude-agent-acp@0.54.1) spawns, speaks the ACP wire
7
7
  // protocol over stdio NDJSON, and returns one real model turn. It is the
8
8
  // bytes-flow proof that the S2a dep surface is not just installable but
9
9
  // actually drivable — before any provider/overlay/streamSimple code (S2b+).
@@ -35,7 +35,7 @@ import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
35
35
  import { connectAcpClient } from "../pi-extensions/lib/acp/acp-client.ts";
36
36
  import { terminateChild } from "./lib/acp-child-cleanup.ts";
37
37
 
38
- const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_RAW_TURN_MODEL ?? "claude-sonnet-4-6";
38
+ const REQUESTED_MODEL_ID = process.env.ENTWURF_ACP_RAW_TURN_MODEL ?? "claude-sonnet-5";
39
39
  const ALLOW_PATH_FALLBACK = process.env.ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK === "1";
40
40
  const RAW_TAIL_CAP = 64 * 1024; // cap captured raw NDJSON to 64KB tail on report.
41
41
 
@@ -44,17 +44,18 @@ function fail(msg: string): never {
44
44
  process.exit(1);
45
45
  }
46
46
 
47
- const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
48
-
49
47
  // Guard each RPC so a hung backend fails this smoke instead of the outer
50
- // process timeout — keeps the failure attributable to the right step.
48
+ // process timeout — keeps the failure attributable to the right step. Always
49
+ // clear the timeout; otherwise a successful live turn can print PASS and keep
50
+ // Node alive until the stale timer fires.
51
51
  function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
52
- return Promise.race([
53
- p,
54
- sleep(ms).then((): never => {
55
- throw new Error(`${label} timed out after ${ms}ms`);
56
- }),
57
- ]);
52
+ let timer: ReturnType<typeof setTimeout> | undefined;
53
+ const timeout = new Promise<never>((_, reject) => {
54
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
55
+ });
56
+ return Promise.race([p, timeout]).finally(() => {
57
+ if (timer) clearTimeout(timer);
58
+ });
58
59
  }
59
60
 
60
61
  if (process.env.LIVE !== "1") {
@@ -27,7 +27,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
27
27
  import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
28
28
 
29
29
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
30
- const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
30
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
31
31
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
32
32
 
33
33
  function fail(msg: string): never {
@@ -35,6 +35,16 @@ function fail(msg: string): never {
35
35
  process.exit(1);
36
36
  }
37
37
 
38
+ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
39
+ let timer: ReturnType<typeof setTimeout> | undefined;
40
+ const timeout = new Promise<never>((_, reject) => {
41
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
42
+ });
43
+ return Promise.race([p, timeout]).finally(() => {
44
+ if (timer) clearTimeout(timer);
45
+ });
46
+ }
47
+
38
48
  if (process.env.LIVE !== "1") {
39
49
  console.error("[smoke-acp-session-reuse-live] skipped — set LIVE=1 to run the real 2-turn reuse acceptance.");
40
50
  process.exit(0);
@@ -100,13 +110,15 @@ async function main(): Promise<void> {
100
110
  },
101
111
  ],
102
112
  };
103
- const r1 = await Promise.race([
113
+ const r1 = await withTimeout(
114
+ "turn 1",
104
115
  consume(backend.streamShellAcp(model, turn1, options) as Stream),
105
- new Promise<never>((_, rej) => setTimeout(() => rej(new Error("turn 1 timed out")), TURN_TIMEOUT_MS)),
106
- ]);
116
+ TURN_TIMEOUT_MS,
117
+ );
107
118
  if (r1.error) fail(`turn 1 errored: ${r1.error}`);
108
119
  if (!r1.done) fail("turn 1 did not complete");
109
120
  console.error(`[smoke-acp-session-reuse-live] turn 1 reply: ${JSON.stringify(r1.text.slice(-120))}`);
121
+ console.error("[smoke-acp-session-reuse-live] starting turn 2 reuse prompt");
110
122
 
111
123
  // --- turn 2 (reuse): ask for the codeword, DELTA ONLY ---
112
124
  const turn2: Context = {
@@ -136,10 +148,11 @@ async function main(): Promise<void> {
136
148
  },
137
149
  ],
138
150
  };
139
- const r2 = await Promise.race([
151
+ const r2 = await withTimeout(
152
+ "turn 2",
140
153
  consume(backend.streamShellAcp(model, turn2, options) as Stream),
141
- new Promise<never>((_, rej) => setTimeout(() => rej(new Error("turn 2 timed out")), TURN_TIMEOUT_MS)),
142
- ]);
154
+ TURN_TIMEOUT_MS,
155
+ );
143
156
  if (r2.error) fail(`turn 2 errored: ${r2.error}`);
144
157
  if (!r2.done) fail("turn 2 did not complete");
145
158
  console.error(`[smoke-acp-session-reuse-live] turn 2 reply: ${JSON.stringify(r2.text.slice(-120))}`);
@@ -160,8 +173,10 @@ async function main(): Promise<void> {
160
173
 
161
174
  main()
162
175
  .then(() => {
163
- // retained child is SIGKILLed by the backend's process 'exit' hook.
164
- process.exit(0);
176
+ // Let stdout/stderr and finally cleanup drain naturally; the backend's process
177
+ // 'exit' hook still SIGKILLs the retained child when Node exits. A direct
178
+ // process.exit(0) can truncate the success/PASS log after the live turn.
179
+ process.exitCode = 0;
165
180
  })
166
181
  .catch((err) => {
167
182
  fail(err instanceof Error ? err.message : String(err));
@@ -30,7 +30,7 @@ import { dirname, join, resolve } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
31
31
 
32
32
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
33
- const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
33
+ const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
34
34
  const PROVIDER = "entwurf";
35
35
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
36
36
 
@@ -102,7 +102,7 @@ assert settings['cleanupPeriodDays'] == 365
102
102
  assert settings['env']['DISABLE_AUTOCOMPACT'] == '1'
103
103
  assert settings['env']['KEEP_ME'] == 'yes'
104
104
  assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
105
- for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan']:
105
+ for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
106
106
  assert settings[key] is False, key
107
107
  assert settings['skipDangerousModePermissionPrompt'] is True
108
108
  for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
@@ -192,7 +192,7 @@ assert settings['statusLine']['command'] == '/old/user/statusline.sh'
192
192
  assert settings['promptSuggestionEnabled'] is True
193
193
  assert settings['skipDangerousModePermissionPrompt'] is False
194
194
  assert settings['showTurnDuration'] is True
195
- for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','terminalProgressBarEnabled','useAutoModeDuringPlan']:
195
+ for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
196
196
  assert key not in settings, key
197
197
  allow=settings['permissions']['allow']
198
198
  deny=settings['permissions']['deny']
@@ -319,7 +319,7 @@ STORE="$TMP/store"; export STORE; mkdir -p "$STORE"
319
319
  valid_record "20260606T000000-aaaaaa" "native-a" > "$STORE/20260606T000000-aaaaaa.meta.json"
320
320
  valid_record "20260606T000001-bbbbbb" "native-b" > "$STORE/20260606T000001-bbbbbb.meta.json"
321
321
 
322
- STATUS_INPUT_MATCH='{"session_id":"native-a","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-4-6"},"context_window":{"context_window_size":200000,"used_percentage":2,"current_usage":{"input_tokens":10}}}'
322
+ STATUS_INPUT_MATCH='{"session_id":"native-a","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":2,"current_usage":{"input_tokens":10}}}'
323
323
  STATUS_INPUT_MISS='{"session_id":"native-missing","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-opus-4-8"}}'
324
324
  STATUS_INPUT_READY='{"workspace":{"current_dir":"/tmp"},"model":{"id":"claude-haiku-4-5"}}'
325
325
  STATUS_OUT_MATCH="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"