@junghanacs/entwurf 0.13.0 → 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 (72) hide show
  1. package/AGENTS.md +5 -3
  2. package/BASELINE.md +57 -203
  3. package/CHANGELOG.md +19 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +117 -261
  6. package/README.md +68 -408
  7. package/VERIFY.md +58 -87
  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 +11 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  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 +4 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +3 -3
  20. package/mcp/entwurf-bridge/src/index.ts +21 -48
  21. package/package.json +12 -9
  22. package/pi-extensions/entwurf-control.ts +20 -49
  23. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  24. package/pi-extensions/lib/acp/augment.ts +42 -3
  25. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  26. package/pi-extensions/lib/acp/backend.ts +305 -36
  27. package/pi-extensions/lib/acp/config.ts +1 -1
  28. package/pi-extensions/lib/acp/engraving.ts +43 -1
  29. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  30. package/pi-extensions/lib/acp/models.ts +4 -4
  31. package/pi-extensions/lib/acp/overlay.ts +3 -3
  32. package/prompts/engraving.md +13 -5
  33. package/run.sh +244 -108
  34. package/scripts/check-acp-carrier-augment.ts +246 -10
  35. package/scripts/check-acp-cortex.ts +5 -5
  36. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  37. package/scripts/check-acp-sdk-surface.ts +60 -21
  38. package/scripts/check-acp-stop-reason.ts +342 -0
  39. package/scripts/check-entwurf-v2-surface.ts +41 -0
  40. package/scripts/check-gate-qualification.ts +6 -3
  41. package/scripts/check-probe-ordering.ts +39 -7
  42. package/scripts/check-release-gate-outcomes.ts +332 -0
  43. package/scripts/lib/live-skip.ts +33 -0
  44. package/scripts/lib/probe-acp-turn.ts +33 -12
  45. package/scripts/lib/step-outcome.sh +88 -0
  46. package/scripts/mutants/acp-augment.json +106 -0
  47. package/scripts/mutants/acp-cortex.json +2 -2
  48. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  49. package/scripts/mutants/acp-stop-reason.json +80 -0
  50. package/scripts/mutants/probe-ordering.json +27 -5
  51. package/scripts/mutants/release-gate.json +105 -0
  52. package/scripts/mutants/v2-surface.json +26 -15
  53. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  54. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  55. package/scripts/smoke-acp-cortex-live.ts +8 -14
  56. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  57. package/scripts/smoke-acp-mcp-live.ts +2 -2
  58. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  59. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  60. package/scripts/smoke-acp-overlay-live.ts +2 -2
  61. package/scripts/smoke-acp-provider-live.ts +2 -2
  62. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  63. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  64. package/scripts/smoke-acp-skill-live.ts +2 -2
  65. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  66. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  67. package/scripts/smoke-agy-native-push-live.ts +4 -3
  68. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  69. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  70. package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
  71. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
  72. package/scripts/smoke-meta-install-state.sh +4 -0
@@ -35,6 +35,16 @@
35
35
  // incompatible and the next turn opens a fresh ACP session with the new carrier.
36
36
  // That per-turn rebuild is the accepted cost of the A/B opt-in surface, never the
37
37
  // shipped default (which stays cached precisely so a resident never rebuilds).
38
+ //
39
+ // A-JOIN (measured LIVE 2026-07-31, 0.64.0 adapter, fresh Claude ACP): the model's
40
+ // system prompt arrived as
41
+ // `You are a Claude agent, built on Anthropic's Claude Agent SDK.# Engraving Here`
42
+ // A string-form `_meta.systemPrompt` replaces the `claude_code` preset, but the
43
+ // SDK still PREFIXES its own fixed identity sentence and joins the two with
44
+ // NOTHING — so the operator's heading was swallowed into the tail of the SDK's
45
+ // sentence. The boundary therefore belongs to the CARRIER, and it cannot be
46
+ // delegated to engraving.md: the render is trimmed (below), so a leading blank
47
+ // line in the markdown is eaten before it ever reaches the wire.
38
48
 
39
49
  import { readFileSync } from "node:fs";
40
50
  import { dirname, join, resolve } from "node:path";
@@ -43,6 +53,16 @@ import { fileURLToPath } from "node:url";
43
53
  const HERE = dirname(fileURLToPath(import.meta.url));
44
54
  const DEFAULT_ENGRAVING_PATH = join(HERE, "prompts", "engraving.md");
45
55
 
56
+ /**
57
+ * The carrier's LEADING boundary — the one the Claude Agent SDK does not supply
58
+ * (see A-JOIN above). A constant, never derived from the template, so the render
59
+ * stays a pure function of (template, backend, mcpServerNames) and the operator's
60
+ * file whitespace can never drift `bridgeConfigSignature`. One blank line is the
61
+ * whole lever: it puts the carrier's first line at the start of its own block
62
+ * instead of at the end of the SDK's fixed sentence.
63
+ */
64
+ export const CARRIER_LEAD_SEPARATOR = "\n\n";
65
+
46
66
  export interface EngravingParams {
47
67
  /** Always "claude" in practice — a system-prompt-carrier-less backend (cortex)
48
68
  * returns null from `loadCarrier` WITHOUT calling this loader, so claudeAdapter is
@@ -82,6 +102,22 @@ function interpolate(template: string, params: EngravingParams): string {
82
102
  return template.replace(/\{\{backend\}\}/g, params.backend).replace(/\{\{mcp_servers\}\}/g, mcpList);
83
103
  }
84
104
 
105
+ /**
106
+ * Render one template into a wire-ready carrier, or `""` when the template has no
107
+ * body (the caller turns that into the opt-out / fail-loud branches).
108
+ *
109
+ * The ORDER here is the A-join fix. Trim first: the operator template's own
110
+ * leading/trailing whitespace must not reach the wire (it would drift
111
+ * bridgeConfigSignature) and the emptiness test must see the BODY — a
112
+ * separator-only string is an opt-out, not a carrier. Then attach OUR boundary,
113
+ * which is what the SDK's fixed sentence has nothing of.
114
+ */
115
+ function renderCarrier(source: string, params: EngravingParams): string {
116
+ const body = interpolate(source, params).trim();
117
+ if (body.length === 0) return "";
118
+ return `${CARRIER_LEAD_SEPARATOR}${body}`;
119
+ }
120
+
85
121
  /**
86
122
  * The rendered engraving carrier, or null when an ENV-OVERRIDE engraving file
87
123
  * (`ENTWURF_ACP_ENGRAVING_PATH`) is empty, whitespace-only, missing, or
@@ -94,6 +130,12 @@ function interpolate(template: string, params: EngravingParams): string {
94
130
  * Callers MUST treat null as "no carrier configured" and omit `_meta.systemPrompt`
95
131
  * entirely (passing "" as the `appendSystemPrompt` signature input) so
96
132
  * subscription billing is never reclassified.
133
+ *
134
+ * A non-null carrier always LEADS with `CARRIER_LEAD_SEPARATOR`, and callers must
135
+ * pass it on BYTE-FOR-BYTE: the same string feeds `bridgeConfigSignature`
136
+ * (`appendSystemPrompt`) and the wire (`_meta.systemPrompt`), so normalizing it at
137
+ * either hop both re-opens the A-join and makes reuse key on a string that was
138
+ * never sent.
97
139
  */
98
140
  export function loadEngraving(params: EngravingParams): string | null {
99
141
  const filePath = resolveEngravingPath();
@@ -110,7 +152,7 @@ export function loadEngraving(params: EngravingParams): string | null {
110
152
  }
111
153
  return null;
112
154
  }
113
- const rendered = interpolate(source, params).trim();
155
+ const rendered = renderCarrier(source, params);
114
156
  if (rendered.length === 0) {
115
157
  if (isShippedDefault) {
116
158
  throw new Error(
@@ -39,7 +39,6 @@ export interface AcpStreamIdentity {
39
39
  type ObservedToolState = {
40
40
  title: string;
41
41
  status?: string;
42
- notifiedRunning?: boolean;
43
42
  };
44
43
 
45
44
  export type AcpPiStreamState = {
@@ -81,7 +80,12 @@ export function createAcpStreamState(
81
80
  provider: identity.provider,
82
81
  model: identity.model,
83
82
  usage: zeroUsage(),
84
- stopReason: "stop",
83
+ // Seed, not a verdict. pi 0.83 added "pending" for exactly this: a partial
84
+ // streaming message has not observed a terminal reason yet, and every pi
85
+ // provider seeds it here and treats a stream that ENDS still-pending as an
86
+ // error rather than a successful stop. Seeding "stop" instead would
87
+ // pre-claim success for the whole time the turn is in flight.
88
+ stopReason: "pending",
85
89
  timestamp: opts?.timestamp ?? Date.now(),
86
90
  };
87
91
  return {
@@ -221,22 +225,21 @@ function renderToolUpdate(state: AcpPiStreamState, update: Record<string, unknow
221
225
  const title = titleForTool(update, previous?.title);
222
226
  const status = typeof update?.status === "string" ? (update.status as string) : previous?.status;
223
227
  const updateContent = Array.isArray(update?.content) ? (update.content as unknown[]) : undefined;
224
- const meta = update?._meta as { terminal_output?: unknown } | undefined;
225
-
226
- let notifiedRunning = previous?.notifiedRunning;
227
228
 
228
229
  if (update.sessionUpdate === "tool_call") {
229
- observedTools.set(toolCallId, { title, status, notifiedRunning });
230
+ observedTools.set(toolCallId, { title, status });
230
231
  pushNotice(state, `\n[tool:start] ${sanitizeNoticeFragment(title, NOTICE_TITLE_MAX)}\n`);
231
232
  return;
232
233
  }
233
234
 
234
- // tool_call_update
235
- if (meta?.terminal_output && !notifiedRunning) {
236
- notifiedRunning = true;
237
- pushNotice(state, `\n[tool:running] ${sanitizeNoticeFragment(title, NOTICE_TITLE_MAX)}\n`);
238
- }
239
-
235
+ // tool_call_update. There is deliberately no mid-flight `[tool:running]`
236
+ // notice: it would have to be driven by the adapter's `_meta.terminal_output`,
237
+ // and that meta is gated upstream on `clientCapabilities._meta.terminal_output
238
+ // === true` while backend.ts initializes with `clientCapabilities: {}`. So the
239
+ // branch could never fire for any backend we ship — it claimed a transcript
240
+ // line the operator was never going to see. Declaring the terminal capability
241
+ // is a separate axis, not a one-line re-enable: the adapter would then send
242
+ // terminal widgets/metas this mapper cannot render honestly into a transcript.
240
243
  if (status && status !== previous?.status) {
241
244
  const summary = firstTextContent(update?.rawOutput) ?? firstTextContent(updateContent);
242
245
  const suffix = summary ? ` — ${sanitizeNoticeFragment(summary, NOTICE_SUMMARY_MAX)}` : "";
@@ -250,7 +253,7 @@ function renderToolUpdate(state: AcpPiStreamState, update: Record<string, unknow
250
253
  }
251
254
  }
252
255
 
253
- observedTools.set(toolCallId, { title, status, notifiedRunning });
256
+ observedTools.set(toolCallId, { title, status });
254
257
  }
255
258
 
256
259
  /** Push a permission-decision notice (informational text, not a tool call). */
@@ -9,8 +9,8 @@
9
9
  // Two curated sets live here, one per adapter on the rail
10
10
  // (backend-adapter.ts): the unprefixed Claude ids (the reference backend) and
11
11
  // the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
12
- // 0.13.0 — docs/acp-backend-rail.md §6/§11-8). Codex is already a native garden
13
- // citizen and Gemini/major tools use native, so neither is an ACP backend here.
12
+ // 0.13.0 — docs/acp-backend-rail.md “Cortex Code audit”). Codex has native
13
+ // delivery-probe evidence but no managed citizen lane; neither it nor Gemini is an ACP backend here.
14
14
  // A third governed backend EXTENDS this file the same way — it does not change
15
15
  // the pattern.
16
16
 
@@ -87,9 +87,9 @@ function claudeContextWindow(model: { id: string; contextWindow: number }): numb
87
87
  // ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
88
88
  //
89
89
  // entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
90
- // backend to land through it (docs/acp-backend-rail.md §6). pi-ai carries no
90
+ // backend to land through it (docs/acp-backend-rail.md, “Shipped adapters”). pi-ai carries no
91
91
  // snowflake/cortex model source, so this surface is HAND-CURATED. Every id
92
- // carries the reserved `cortex-` prefix (§9-1): the prefix is the SINGLE routing
92
+ // carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
93
93
  // authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
94
94
  // native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
95
95
  // `claude-sonnet-5` the claude adapter owns). `routeModel` strips the prefix to
@@ -15,7 +15,7 @@
15
15
  // materializer, and cortex did: `ensureCortexDualHomeOverlay` (+ projectCortexMcpJson /
16
16
  // sweepDeadCortexOverlays / cortexOverlayScopeId) lives further down this file with its
17
17
  // own D-number rationale — an isolated HOME rather than a redirect knob, because cortex
18
- // has no CLAUDE_CONFIG_DIR equivalent (§11-8 D2). The two share the module, never the
18
+ // has no CLAUDE_CONFIG_DIR equivalent (ACP rail Cortex audit D2). The two share the module, never the
19
19
  // strategy. Codex/Gemini overlays (CODEX_HOME / admin.toml) remain behavior-oracle
20
20
  // territory and are out of scope here.
21
21
  //
@@ -230,11 +230,11 @@ export function ensureClaudeConfigOverlay(
230
230
 
231
231
  // ============================================================================
232
232
  // Cortex dual-HOME overlay — the as-measured containment for `cortex acp serve`
233
- // (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md §4/§11-8).
233
+ // (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md “Cortex Code audit”).
234
234
  // ============================================================================
235
235
  //
236
236
  // Why this is NOT the claude-shaped `SNOWFLAKE_HOME`-only overlay PR #40 shipped
237
- // (each point is a measured defect, not a preference — §11-8 D-numbers):
237
+ // (each point is a measured defect, not a preference — Cortex audit D-numbers):
238
238
  //
239
239
  // - D2: cortex reads `CONFIG_DIRS = [".claude", ".cortex"]` at `homedir()` and
240
240
  // `~/.claude/skills` — a SNOWFLAKE_HOME redirect cannot move that axis. An
@@ -1,11 +1,15 @@
1
1
  <!-- ═══════════════════════════════════════════════════════════════════════
2
2
  SAMPLE — operator engraving carrier (entwurf)
3
3
 
4
- What this is: the markdown body below this comment is loaded VERBATIM
5
- into the backend's identity slot (Claude `_meta.systemPrompt`). It is
6
- the one place an operator stamps a short identity onto every entwurf
7
- ACP turn — replacing Claude's `claude_code` preset (which also strips
8
- the preset's auto-memory advertisement, the memory-containment lever).
4
+ What this is: the engraving file is loaded VERBATIM into the backend's
5
+ identity slot (Claude `_meta.systemPrompt`). It is the one place an
6
+ operator stamps a short identity onto every entwurf ACP turn —
7
+ replacing Claude's `claude_code` preset (which also strips the preset's
8
+ auto-memory advertisement, the memory-containment lever).
9
+
10
+ VERBATIM means the WHOLE file, this comment included — the loader does
11
+ not strip markdown comments. Copy this sample and delete everything you
12
+ do not want on the wire; do not point the runtime at it as-is.
9
13
 
10
14
  Where it loads from:
11
15
  • Runtime default = pi-extensions/lib/acp/prompts/engraving.md
@@ -22,6 +26,10 @@
22
26
  tool catalogs here — a large Claude carrier can route OAuth
23
27
  sessions to metered "extra usage" billing.
24
28
  • An empty or missing file = opt-out (no engraving). That is fine.
29
+ • Do NOT open the file with a blank line to separate yourself from
30
+ Claude's fixed SDK sentence. Your leading/trailing whitespace is
31
+ trimmed (it would otherwise drift the reuse signature) and the
32
+ loader opens the carrier with that blank line itself.
25
33
  ═══════════════════════════════════════════════════════════════════════ -->
26
34
 
27
35
  # Engraving Here