@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.
- package/AGENTS.md +5 -3
- package/BASELINE.md +57 -203
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +117 -261
- package/README.md +68 -408
- package/VERIFY.md +58 -87
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +231 -0
- package/docs/external-mcp-host.md +132 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +123 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +3 -3
- package/mcp/entwurf-bridge/src/index.ts +21 -48
- package/package.json +12 -9
- package/pi-extensions/entwurf-control.ts +20 -49
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +305 -36
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +244 -108
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-sdk-surface.ts +60 -21
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-entwurf-v2-surface.ts +41 -0
- package/scripts/check-gate-qualification.ts +6 -3
- package/scripts/check-probe-ordering.ts +39 -7
- package/scripts/check-release-gate-outcomes.ts +332 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/probe-ordering.json +27 -5
- package/scripts/mutants/release-gate.json +105 -0
- package/scripts/mutants/v2-surface.json +26 -15
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +4 -3
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
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
|
|
13
|
-
//
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 —
|
|
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
|
package/prompts/engraving.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
2
2
|
SAMPLE — operator engraving carrier (entwurf)
|
|
3
3
|
|
|
4
|
-
What this is: the
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|