@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
|
@@ -39,6 +39,18 @@ export function connectAcpClient(stream, handlers) {
|
|
|
39
39
|
newSession: (params) => agent.request(AGENT_METHODS.session_new, params),
|
|
40
40
|
prompt: (params) => agent.request(AGENT_METHODS.session_prompt, params),
|
|
41
41
|
setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params),
|
|
42
|
+
cancel: (params) => {
|
|
43
|
+
// Notification, not a request: nothing resolves it, and the connection
|
|
44
|
+
// may already be closing when the operator aborts. Swallow both the sync
|
|
45
|
+
// throw and the rejected send — the caller's abort path continues either
|
|
46
|
+
// way (it escalates to teardown after a bounded grace).
|
|
47
|
+
try {
|
|
48
|
+
void Promise.resolve(agent.notify(AGENT_METHODS.session_cancel, params)).catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// connection already closed — the abort path escalates on its own.
|
|
52
|
+
}
|
|
53
|
+
},
|
|
42
54
|
close: (error) => {
|
|
43
55
|
// Best-effort by contract (see AcpConnectionLike.close): a teardown-path
|
|
44
56
|
// close that threw would mask the turn's real error and skip the child
|
|
@@ -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
|
|
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).
|
|
6
|
-
//
|
|
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`.
|
|
@@ -54,7 +55,7 @@ function resolveClaudeLaunch() {
|
|
|
54
55
|
}
|
|
55
56
|
export const claudeAdapter = {
|
|
56
57
|
backend: "claude",
|
|
57
|
-
// Claude owns its UNPREFIXED curated ids only (
|
|
58
|
+
// Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
|
|
58
59
|
// equals the curated id — claude carries no curation prefix to strip.
|
|
59
60
|
routeModel(modelId) {
|
|
60
61
|
return SUPPORTED_CLAUDE_IDS.has(modelId) ? { nativeModelId: modelId } : undefined;
|
|
@@ -121,7 +122,7 @@ export const claudeAdapter = {
|
|
|
121
122
|
* adapters claim one id. */
|
|
122
123
|
// ---------------------------------------------------------------------------
|
|
123
124
|
// cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
|
|
124
|
-
// rail (docs/acp-backend-rail.md
|
|
125
|
+
// rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
|
|
125
126
|
// everything cortex-specific lives here + models.ts + overlay.ts + the gates.
|
|
126
127
|
// ---------------------------------------------------------------------------
|
|
127
128
|
const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
|
|
@@ -131,7 +132,7 @@ const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
|
|
|
131
132
|
export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
|
|
132
133
|
export const cortexAdapter = {
|
|
133
134
|
backend: "cortex",
|
|
134
|
-
// Cortex owns the reserved `cortex-` prefix (
|
|
135
|
+
// Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
|
|
135
136
|
// native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
|
|
136
137
|
routeModel(modelId) {
|
|
137
138
|
if (!SUPPORTED_CORTEX_IDS.has(modelId))
|
|
@@ -213,7 +214,7 @@ export const cortexAdapter = {
|
|
|
213
214
|
});
|
|
214
215
|
return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
|
|
215
216
|
},
|
|
216
|
-
// System-prompt-carrier-less (
|
|
217
|
+
// System-prompt-carrier-less (ACP rail “Cortex Code audit”): Cortex ACP exposes no
|
|
217
218
|
// `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
|
|
218
219
|
// equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
|
|
219
220
|
// unexplored and deliberately not part of this contract.) loadCarrier returns
|
|
@@ -242,7 +243,7 @@ export const cortexAdapter = {
|
|
|
242
243
|
}
|
|
243
244
|
await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
|
|
244
245
|
},
|
|
245
|
-
// A connection change must invalidate a reused session (
|
|
246
|
+
// A connection change must invalidate a reused session (rail: Adapter contract). Flat,
|
|
246
247
|
// sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
|
|
247
248
|
// + `nativeModelId` are added by backend.ts.
|
|
248
249
|
configSignatureFields(adapterSettings) {
|
|
@@ -254,7 +255,7 @@ const ADAPTERS = [claudeAdapter, cortexAdapter];
|
|
|
254
255
|
/**
|
|
255
256
|
* Resolve the backend adapter that owns `modelId`.
|
|
256
257
|
*
|
|
257
|
-
*
|
|
258
|
+
* Routing fail-fast contract (rail “Adapter contract”):
|
|
258
259
|
* - 0 matches → throw (unknown model — no silent default).
|
|
259
260
|
* - 2+ matches → throw (prefix collision — a startup-visible registry bug).
|
|
260
261
|
*
|
|
@@ -278,7 +279,7 @@ export function resolveAcpBackendAdapter(modelId) {
|
|
|
278
279
|
return matches[0];
|
|
279
280
|
}
|
|
280
281
|
/** Every curated model row across all registered adapters — for provider registration.
|
|
281
|
-
*
|
|
282
|
+
* Registration-time fail-fast (rail “Adapter contract”): every curated id must route to EXACTLY
|
|
282
283
|
* one adapter and no id may be duplicated across adapters. Catching it here means a
|
|
283
284
|
* prefix-collision / duplicate surfaces at provider registration, not mid-turn. */
|
|
284
285
|
export function allCuratedModels() {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// the backend-invariant exclude-tools preflight and folds every one of them into
|
|
24
24
|
// `bridgeConfigSignature` (so changing one still invalidates a reused cortex
|
|
25
25
|
// session). Backend-OWNED settings never land here: they ride the opaque
|
|
26
|
-
// `adapterSettings` seam (
|
|
26
|
+
// `adapterSettings` seam (rail “Adapter contract”), which is what keeps a backend-named key like
|
|
27
27
|
// `cortexConnection` off this common shape.
|
|
28
28
|
//
|
|
29
29
|
// PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
|
|
@@ -35,11 +35,30 @@
|
|
|
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
|
import { readFileSync } from "node:fs";
|
|
39
49
|
import { dirname, join, resolve } from "node:path";
|
|
40
50
|
import { fileURLToPath } from "node:url";
|
|
41
51
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
42
52
|
const DEFAULT_ENGRAVING_PATH = join(HERE, "prompts", "engraving.md");
|
|
53
|
+
/**
|
|
54
|
+
* The carrier's LEADING boundary — the one the Claude Agent SDK does not supply
|
|
55
|
+
* (see A-JOIN above). A constant, never derived from the template, so the render
|
|
56
|
+
* stays a pure function of (template, backend, mcpServerNames) and the operator's
|
|
57
|
+
* file whitespace can never drift `bridgeConfigSignature`. One blank line is the
|
|
58
|
+
* whole lever: it puts the carrier's first line at the start of its own block
|
|
59
|
+
* instead of at the end of the SDK's fixed sentence.
|
|
60
|
+
*/
|
|
61
|
+
export const CARRIER_LEAD_SEPARATOR = "\n\n";
|
|
43
62
|
let cached = null;
|
|
44
63
|
/** Point the loader at an alternate engraving file (A/B); bypasses the cache. */
|
|
45
64
|
function resolveEngravingPath() {
|
|
@@ -65,6 +84,22 @@ function interpolate(template, params) {
|
|
|
65
84
|
const mcpList = names.length > 0 ? names.join(", ") : "(none registered)";
|
|
66
85
|
return template.replace(/\{\{backend\}\}/g, params.backend).replace(/\{\{mcp_servers\}\}/g, mcpList);
|
|
67
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Render one template into a wire-ready carrier, or `""` when the template has no
|
|
89
|
+
* body (the caller turns that into the opt-out / fail-loud branches).
|
|
90
|
+
*
|
|
91
|
+
* The ORDER here is the A-join fix. Trim first: the operator template's own
|
|
92
|
+
* leading/trailing whitespace must not reach the wire (it would drift
|
|
93
|
+
* bridgeConfigSignature) and the emptiness test must see the BODY — a
|
|
94
|
+
* separator-only string is an opt-out, not a carrier. Then attach OUR boundary,
|
|
95
|
+
* which is what the SDK's fixed sentence has nothing of.
|
|
96
|
+
*/
|
|
97
|
+
function renderCarrier(source, params) {
|
|
98
|
+
const body = interpolate(source, params).trim();
|
|
99
|
+
if (body.length === 0)
|
|
100
|
+
return "";
|
|
101
|
+
return `${CARRIER_LEAD_SEPARATOR}${body}`;
|
|
102
|
+
}
|
|
68
103
|
/**
|
|
69
104
|
* The rendered engraving carrier, or null when an ENV-OVERRIDE engraving file
|
|
70
105
|
* (`ENTWURF_ACP_ENGRAVING_PATH`) is empty, whitespace-only, missing, or
|
|
@@ -77,6 +112,12 @@ function interpolate(template, params) {
|
|
|
77
112
|
* Callers MUST treat null as "no carrier configured" and omit `_meta.systemPrompt`
|
|
78
113
|
* entirely (passing "" as the `appendSystemPrompt` signature input) so
|
|
79
114
|
* subscription billing is never reclassified.
|
|
115
|
+
*
|
|
116
|
+
* A non-null carrier always LEADS with `CARRIER_LEAD_SEPARATOR`, and callers must
|
|
117
|
+
* pass it on BYTE-FOR-BYTE: the same string feeds `bridgeConfigSignature`
|
|
118
|
+
* (`appendSystemPrompt`) and the wire (`_meta.systemPrompt`), so normalizing it at
|
|
119
|
+
* either hop both re-opens the A-join and makes reuse key on a string that was
|
|
120
|
+
* never sent.
|
|
80
121
|
*/
|
|
81
122
|
export function loadEngraving(params) {
|
|
82
123
|
const filePath = resolveEngravingPath();
|
|
@@ -92,7 +133,7 @@ export function loadEngraving(params) {
|
|
|
92
133
|
}
|
|
93
134
|
return null;
|
|
94
135
|
}
|
|
95
|
-
const rendered =
|
|
136
|
+
const rendered = renderCarrier(source, params);
|
|
96
137
|
if (rendered.length === 0) {
|
|
97
138
|
if (isShippedDefault) {
|
|
98
139
|
throw new Error(`entwurf: shipped engraving carrier at ${filePath} is empty — it is the auto-memory ` +
|
|
@@ -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
|
// pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
|
|
@@ -77,9 +77,9 @@ function claudeContextWindow(model) {
|
|
|
77
77
|
// ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
|
|
78
78
|
//
|
|
79
79
|
// entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
|
|
80
|
-
// backend to land through it (docs/acp-backend-rail.md
|
|
80
|
+
// backend to land through it (docs/acp-backend-rail.md, “Shipped adapters”). pi-ai carries no
|
|
81
81
|
// snowflake/cortex model source, so this surface is HAND-CURATED. Every id
|
|
82
|
-
// carries the reserved `cortex-` prefix
|
|
82
|
+
// carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
|
|
83
83
|
// authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
|
|
84
84
|
// native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
|
|
85
85
|
// `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
|
//
|
|
@@ -201,11 +201,11 @@ export function ensureClaudeConfigOverlay(realDir = CLAUDE_REAL_CONFIG_DIR, over
|
|
|
201
201
|
}
|
|
202
202
|
// ============================================================================
|
|
203
203
|
// Cortex dual-HOME overlay — the as-measured containment for `cortex acp serve`
|
|
204
|
-
// (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md
|
|
204
|
+
// (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md “Cortex Code audit”).
|
|
205
205
|
// ============================================================================
|
|
206
206
|
//
|
|
207
207
|
// Why this is NOT the claude-shaped `SNOWFLAKE_HOME`-only overlay PR #40 shipped
|
|
208
|
-
// (each point is a measured defect, not a preference —
|
|
208
|
+
// (each point is a measured defect, not a preference — Cortex audit D-numbers):
|
|
209
209
|
//
|
|
210
210
|
// - D2: cortex reads `CONFIG_DIRS = [".claude", ".cortex"]` at `homedir()` and
|
|
211
211
|
// `~/.claude/skills` — a SNOWFLAKE_HOME redirect cannot move that axis. An
|
|
@@ -344,54 +344,27 @@ function abbreviateHomeMcp(cwd: string): string {
|
|
|
344
344
|
// sender replyability).
|
|
345
345
|
server.tool(
|
|
346
346
|
"entwurf_v2",
|
|
347
|
-
"CANONICAL DELIVERY SURFACE for garden ids
|
|
348
|
-
"
|
|
349
|
-
"
|
|
350
|
-
"
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"wants_reply:true if you need an answer (wants_reply is NOT owned-outcome). Replies to a citizen " +
|
|
369
|
-
"with NO socket liveness (liveness=unsupported) are ALSO fire-and-forget, and the decider picks " +
|
|
370
|
-
"that citizen's own rail: a self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, " +
|
|
371
|
-
"while a native-push backend (e.g. Antigravity) gets direct injection into its live conversation " +
|
|
372
|
-
"and has NO mailbox at all — do not assume mailbox semantics for every unsupported citizen. A " +
|
|
373
|
-
"native-push target IS measured by its own adapter probe, and that probe is THREE-valued, so " +
|
|
374
|
-
"the send is never silently queued: alive → injected; dead → rejected as " +
|
|
375
|
-
"native-push-target-dead; indeterminate → rejected as native-push-probe-indeterminate. " +
|
|
376
|
-
"Those two rejects stay distinct because 'we could not establish it' is not 'it is gone' — " +
|
|
377
|
-
"collapsing them reports a guess as a fact. THERE IS A " +
|
|
378
|
-
"THIRD RESULT, so do not read liveness=unsupported as 'reachable by some rail': the mailbox path " +
|
|
379
|
-
"delivers only to a DELIVERABLE citizen (a self-fetch backend whose receiver is live and armed), " +
|
|
380
|
-
"so a terminated Claude Code session — and any record whose backend has no adapter on this lane, " +
|
|
381
|
-
"e.g. codex — is rejected as mailbox-undeliverable rather than queued into an inbox nobody drains. " +
|
|
382
|
-
"unsupported means only 'this backend has no control-socket probe'. " +
|
|
383
|
-
"owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently " +
|
|
384
|
-
"backend pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor " +
|
|
385
|
-
"native-push has resume authority, but they reject under DIFFERENT reasons — self-fetch as " +
|
|
386
|
-
"backend-liveness-unsupported, native-push as native-push-no-resume-authority. " +
|
|
387
|
-
"It is NEVER auto-converted — so pick the right intent up front. " +
|
|
388
|
-
"mode applies to a CONTROL-SOCKET send only — it is the injection style for a live pi turn, and " +
|
|
389
|
-
"the mailbox, native-push, and spawn-bg plans carry no mode at all, so setting it for those " +
|
|
390
|
-
"targets changes nothing (a native-push send IS live and still ignores it). wants_reply rides " +
|
|
391
|
-
"every rail. Use entwurf_peers to discover targets. " +
|
|
392
|
-
"Payload guidance: message hard cap 16000 chars. For larger reviews/logs, write an " +
|
|
393
|
-
"artifact and dispatch its path plus a short digest; avoid multi-part sends because " +
|
|
394
|
-
"mailbox doorbells may coalesce.",
|
|
347
|
+
"CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id " +
|
|
348
|
+
"alone does not say which rail that citizen answers on. Give target + intent; the decider picks transport " +
|
|
349
|
+
"from liveness (live socket citizen → control-socket send; dormant socket citizen → spawn-bg resume; " +
|
|
350
|
+
"deliverable self-fetch citizen → meta-bridge mailbox; probe-alive native-push citizen → direct injection " +
|
|
351
|
+
"into its conversation) and reports ONE outcome (delivered / rejected / lock-retained / " +
|
|
352
|
+
"delivered-but-lock-dirty). EXISTING targets only; discover with entwurf_peers. INTENT — picking wrong is " +
|
|
353
|
+
"rejected, never auto-converted. A peer entwurf_peers shows as liveness=alive → fire-and-forget. A " +
|
|
354
|
+
"citizen with NO socket liveness (liveness=unsupported) is ALSO fire-and-forget — unsupported means only " +
|
|
355
|
+
'"no control-socket probe" — and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) ' +
|
|
356
|
+
"gets the mailbox, a native-push backend (e.g. Antigravity) gets direct injection and has NO mailbox at " +
|
|
357
|
+
"all. THERE IS A THIRD RESULT: the mailbox delivers only to a DELIVERABLE citizen, so a terminated " +
|
|
358
|
+
"session, or a backend with no adapter here (e.g. codex), is mailbox-undeliverable, not queued for an " +
|
|
359
|
+
"inbox nobody drains. The native-push probe is 3-valued: alive → injected; dead → " +
|
|
360
|
+
"native-push-target-dead; indeterminate → native-push-probe-indeterminate (unestablished ≠ gone). " +
|
|
361
|
+
"owned-outcome wakes a DORMANT socket-domain citizen by spawn-bg resume ONLY — live target → " +
|
|
362
|
+
"owned-live-no-autosend, self-fetch → backend-liveness-unsupported, native-push → " +
|
|
363
|
+
"native-push-no-resume-authority. LOCK: taken for a control-socket-DOMAIN dispatch — the live send AND " +
|
|
364
|
+
"the dormant cell's spawn-bg resume, a separate transport that still runs under that domain's lock. The " +
|
|
365
|
+
"mailbox and native-push rails are lock-free — deliverability and the adapter probe guard them. mode " +
|
|
366
|
+
"applies to a CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message " +
|
|
367
|
+
"caps at 16000 chars; send an artifact path + digest for more.",
|
|
395
368
|
{
|
|
396
369
|
target: z.string().min(1).describe("Target garden id (use entwurf_peers to discover)"),
|
|
397
370
|
intent: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"demo/demo.sh",
|
|
43
43
|
"demo/demo-baseline.sh",
|
|
44
44
|
"docs/setup-clean-host.md",
|
|
45
|
+
"docs/external-mcp-host.md",
|
|
46
|
+
"docs/fresh-cut-policy.md",
|
|
47
|
+
"docs/acp-backend-rail.md",
|
|
45
48
|
"docs/assets/entwurf-hero.jpg",
|
|
46
49
|
"docs/assets/entwurf-demo.gif",
|
|
47
50
|
"docs/assets/entwurf-doomemacs.gif",
|
|
@@ -71,23 +74,23 @@
|
|
|
71
74
|
"claudeCodeFloor": ">=2.1.217"
|
|
72
75
|
},
|
|
73
76
|
"dependencies": {
|
|
74
|
-
"@agentclientprotocol/claude-agent-acp": "0.
|
|
77
|
+
"@agentclientprotocol/claude-agent-acp": "0.64.0",
|
|
75
78
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
76
79
|
"@anthropic-ai/sdk": "0.100.1",
|
|
77
80
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
78
81
|
"zod": "^3.25.0 || ^4.0.0"
|
|
79
82
|
},
|
|
80
83
|
"peerDependencies": {
|
|
81
|
-
"@earendil-works/pi-ai": ">=0.
|
|
82
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
83
|
-
"@earendil-works/pi-tui": ">=0.
|
|
84
|
+
"@earendil-works/pi-ai": ">=0.83.0 <0.84",
|
|
85
|
+
"@earendil-works/pi-coding-agent": ">=0.83.0 <0.84",
|
|
86
|
+
"@earendil-works/pi-tui": ">=0.83.0 <0.84",
|
|
84
87
|
"typebox": "*"
|
|
85
88
|
},
|
|
86
89
|
"devDependencies": {
|
|
87
90
|
"@biomejs/biome": "^2.4.13",
|
|
88
|
-
"@earendil-works/pi-ai": "0.
|
|
89
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
90
|
-
"@earendil-works/pi-tui": "0.
|
|
91
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
92
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
93
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
91
94
|
"@types/node": "^24.3.0",
|
|
92
95
|
"husky": "^9.1.7",
|
|
93
96
|
"typescript": "^6.0.2"
|
|
@@ -132,7 +135,7 @@
|
|
|
132
135
|
"format": "biome check --write .",
|
|
133
136
|
"check-meta-manifest-schema": "./run.sh check-meta-manifest-schema",
|
|
134
137
|
"check-package-source-routing": "./run.sh check-package-source-routing",
|
|
135
|
-
"check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-install-surface && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-capability-bundle-reach && ./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-hook-launch-topology && ./run.sh check-meta-identity-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh check-meta-doctor-oracle && ./run.sh smoke-agy-install-state && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-agy-sender-identity && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-gate-qualification && ./run.sh check-pack"
|
|
138
|
+
"check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-install-surface && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-capability-bundle-reach && ./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-hook-launch-topology && ./run.sh check-meta-identity-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh check-meta-doctor-oracle && ./run.sh smoke-agy-install-state && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-agy-sender-identity && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-gate-qualification && ./run.sh check-pack"
|
|
136
139
|
},
|
|
137
140
|
"pi": {
|
|
138
141
|
"extensions": [
|
|
@@ -1322,55 +1322,26 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1322
1322
|
registerTool({
|
|
1323
1323
|
name: "entwurf_v2",
|
|
1324
1324
|
label: "Dispatch (v2)",
|
|
1325
|
-
description: `CANONICAL
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
self-fetch
|
|
1329
|
-
and
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
-
|
|
1343
|
-
-
|
|
1344
|
-
|
|
1345
|
-
- mode: steer or follow_up for a CONTROL-SOCKET send (optional). The mailbox, native-push, and
|
|
1346
|
-
spawn-bg plans carry no mode, so it has no effect on those rails — a native-push send IS live and
|
|
1347
|
-
still ignores it.
|
|
1348
|
-
- wants_reply: reply hint; it rides every rail (optional, default false).
|
|
1349
|
-
|
|
1350
|
-
CHOOSING INTENT (picking wrong is rejected, never auto-fixed): to message / reply / hand off a peer
|
|
1351
|
-
that entwurf_peers shows as liveness=alive (a live socket citizen, currently backend pi), use intent:
|
|
1352
|
-
fire-and-forget — it routes to the live control-socket; set wants_reply:true if you need an answer
|
|
1353
|
-
(wants_reply is NOT owned-outcome). Replies to a citizen with NO socket liveness
|
|
1354
|
-
(liveness=unsupported) are ALSO fire-and-forget, and the decider picks that citizen's own rail: a
|
|
1355
|
-
self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, while a native-push backend
|
|
1356
|
-
(e.g. Antigravity) gets direct injection into its live conversation and has NO mailbox at all — do
|
|
1357
|
-
not assume mailbox semantics for every unsupported citizen. A native-push target IS measured by its
|
|
1358
|
-
own adapter probe, and that probe is THREE-valued, so the send is never silently queued: alive →
|
|
1359
|
-
injected; dead → rejected as native-push-target-dead; indeterminate → rejected as
|
|
1360
|
-
native-push-probe-indeterminate. Those last two stay separate on purpose — "we could not establish
|
|
1361
|
-
it" is not "it is gone". THERE IS A THIRD RESULT, so do not read
|
|
1362
|
-
liveness=unsupported as "reachable by some rail": the mailbox path delivers only to a DELIVERABLE
|
|
1363
|
-
citizen (a self-fetch backend whose receiver is live and armed), so a terminated Claude Code session —
|
|
1364
|
-
and any record whose backend has no adapter on this lane, e.g. codex — is rejected as
|
|
1365
|
-
mailbox-undeliverable rather than queued into an inbox nobody drains. unsupported means only "this
|
|
1366
|
-
backend has no control-socket probe".
|
|
1367
|
-
owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently backend
|
|
1368
|
-
pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor native-push
|
|
1369
|
-
has resume authority, but they reject under DIFFERENT reasons — self-fetch as
|
|
1370
|
-
backend-liveness-unsupported, native-push as native-push-no-resume-authority.
|
|
1371
|
-
It is NEVER auto-converted — so pick the right intent up front.
|
|
1372
|
-
|
|
1373
|
-
The decider — not this surface — chooses the transport.`,
|
|
1325
|
+
description: `CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id alone
|
|
1326
|
+
does not say which rail that citizen answers on. Give target + intent; the decider picks transport from
|
|
1327
|
+
liveness (live socket citizen → control-socket send; dormant socket citizen → spawn-bg resume; deliverable
|
|
1328
|
+
self-fetch citizen → meta-bridge mailbox; probe-alive native-push citizen → direct injection into its
|
|
1329
|
+
conversation) and reports ONE outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
|
|
1330
|
+
EXISTING targets only; discover with entwurf_peers. INTENT — picking wrong is rejected, never
|
|
1331
|
+
auto-converted. A peer entwurf_peers shows as liveness=alive → fire-and-forget. A citizen with NO socket
|
|
1332
|
+
liveness (liveness=unsupported) is ALSO fire-and-forget — unsupported means only "no control-socket probe" —
|
|
1333
|
+
and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push
|
|
1334
|
+
backend (e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the
|
|
1335
|
+
mailbox delivers only to a DELIVERABLE citizen, so a terminated session, or a backend with no adapter here
|
|
1336
|
+
(e.g. codex), is mailbox-undeliverable, not queued for an inbox nobody drains. The native-push probe is
|
|
1337
|
+
3-valued: alive → injected; dead → native-push-target-dead; indeterminate → native-push-probe-indeterminate
|
|
1338
|
+
(unestablished ≠ gone). owned-outcome wakes a DORMANT socket-domain citizen by spawn-bg resume ONLY — live
|
|
1339
|
+
target → owned-live-no-autosend, self-fetch → backend-liveness-unsupported, native-push →
|
|
1340
|
+
native-push-no-resume-authority. LOCK: taken for a control-socket-DOMAIN dispatch — the live send AND the
|
|
1341
|
+
dormant cell's spawn-bg resume, a separate transport that still runs under that domain's lock. The mailbox
|
|
1342
|
+
and native-push rails are lock-free — deliverability and the adapter probe guard them. mode applies to a
|
|
1343
|
+
CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000
|
|
1344
|
+
chars; send an artifact path + digest for more.`,
|
|
1374
1345
|
parameters: entwurfV2Parameters,
|
|
1375
1346
|
async execute(
|
|
1376
1347
|
_toolCallId: string,
|
|
@@ -26,6 +26,17 @@ export interface AcpConnectionLike {
|
|
|
26
26
|
newSession(params: unknown): Promise<{ sessionId?: string }>;
|
|
27
27
|
prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<{ stopReason?: string }>;
|
|
28
28
|
setSessionConfigOption?(params: unknown): Promise<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* ACP `session/cancel` — the PROTOCOL way to end an in-flight prompt turn.
|
|
31
|
+
*
|
|
32
|
+
* The spec requires the agent to answer the pending `session/prompt` with
|
|
33
|
+
* `stopReason: "cancelled"` after this notification, so a user abort ends the
|
|
34
|
+
* turn as a protocol event (backend.ts maps cancelled → aborted) instead of a
|
|
35
|
+
* signal race. Fire-and-forget by contract: it is a JSON-RPC notification, so
|
|
36
|
+
* there is nothing to await and a send failure on an already-closed connection
|
|
37
|
+
* must not mask the abort the caller is executing.
|
|
38
|
+
*/
|
|
39
|
+
cancel?(params: { sessionId: string }): void;
|
|
29
40
|
/**
|
|
30
41
|
* Closes the underlying SDK connection before child process teardown. With
|
|
31
42
|
* the fluent SDK connection this is load-bearing: otherwise a successful
|
|
@@ -76,6 +87,17 @@ export function connectAcpClient(stream: Stream, handlers: AcpClientHandlers): A
|
|
|
76
87
|
prompt: (params) =>
|
|
77
88
|
agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<{ stopReason?: string }>,
|
|
78
89
|
setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params as never),
|
|
90
|
+
cancel: (params) => {
|
|
91
|
+
// Notification, not a request: nothing resolves it, and the connection
|
|
92
|
+
// may already be closing when the operator aborts. Swallow both the sync
|
|
93
|
+
// throw and the rejected send — the caller's abort path continues either
|
|
94
|
+
// way (it escalates to teardown after a bounded grace).
|
|
95
|
+
try {
|
|
96
|
+
void Promise.resolve(agent.notify(AGENT_METHODS.session_cancel, params as never)).catch(() => {});
|
|
97
|
+
} catch {
|
|
98
|
+
// connection already closed — the abort path escalates on its own.
|
|
99
|
+
}
|
|
100
|
+
},
|
|
79
101
|
close: (error) => {
|
|
80
102
|
// Best-effort by contract (see AcpConnectionLike.close): a teardown-path
|
|
81
103
|
// close that threw would mask the turn's real error and skip the child
|
|
@@ -36,10 +36,10 @@ import type { AcpTextBlock } from "./context.js";
|
|
|
36
36
|
|
|
37
37
|
const MAX_AUGMENT_BYTES = 50 * 1024;
|
|
38
38
|
|
|
39
|
-
// Carrier-less backends expose no `_meta.systemPrompt` (docs/acp-backend-rail.md
|
|
40
|
-
//
|
|
39
|
+
// Carrier-less backends expose no `_meta.systemPrompt` (docs/acp-backend-rail.md,
|
|
40
|
+
// “Cortex Code audit”). claude folds the operator engraving into that carrier; a carrier-less
|
|
41
41
|
// backend (cortex, the first — precisely: SYSTEM-PROMPT-carrier-less, since
|
|
42
|
-
// cortex does read `_meta` for an unpromoted caller-session-id seam
|
|
42
|
+
// cortex does read `_meta` for an unpromoted caller-session-id seam)
|
|
43
43
|
// has nowhere to put it — so its operator engraving
|
|
44
44
|
// rides HERE, prepended as the LEADING section of the first-user augment. It
|
|
45
45
|
// travels on the WIRE only (new-only, never the config signature), so an
|
|
@@ -48,6 +48,13 @@ const MAX_AUGMENT_BYTES = 50 * 1024;
|
|
|
48
48
|
// carrier feeds bridgeConfigSignature.
|
|
49
49
|
const CARRIER_LESS_BACKENDS: ReadonlySet<string> = new Set(["cortex"]);
|
|
50
50
|
|
|
51
|
+
// The one sentence both rails owe the model: WHAT this block is and what it is
|
|
52
|
+
// NOT. Shared so neither rail's frame can quietly lose it (and so the gate has a
|
|
53
|
+
// single anchor to mutate). Everything after it is rail-specific, because the
|
|
54
|
+
// rails really do differ — see the frame in buildPiContextAugment.
|
|
55
|
+
const PROVENANCE_LEAD =
|
|
56
|
+
"This block is prepended to the FIRST USER MESSAGE of this session. It is not your system prompt.";
|
|
57
|
+
|
|
51
58
|
// The env var an operator points at a personal engraving file. Shared surface
|
|
52
59
|
// with the claude carrier (engraving.ts) BY NAME, but read INLINE here (not
|
|
53
60
|
// imported): augment.ts is loaded raw by the strip-types carrier-augment gate,
|
|
@@ -116,6 +123,38 @@ export function buildPiContextAugment(params: PiContextAugmentParams): string {
|
|
|
116
123
|
if (engraving) sections.push(engraving);
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
// PROVENANCE FRAME — leads the bridge narrative, and it is not decoration.
|
|
127
|
+
//
|
|
128
|
+
// Measured 2026-07-30: asked where its instructions came from, the Claude ACP
|
|
129
|
+
// model asserted that the two paragraphs below were its system prompt. They
|
|
130
|
+
// are not — they are text prepended to its FIRST USER MESSAGE. Nothing in the
|
|
131
|
+
// block said otherwise, and a model has no other way to tell: on the wire a
|
|
132
|
+
// long first user message and a system prompt read the same. So the block now
|
|
133
|
+
// states its own provenance, per rail, because the rails genuinely differ
|
|
134
|
+
// (backend-adapter.ts: claude's buildSessionMeta carries `_meta.systemPrompt`,
|
|
135
|
+
// cortex's returns undefined so no `_meta` key is sent at all).
|
|
136
|
+
//
|
|
137
|
+
// Honest about scope: this makes the boundary STATEABLE, not enforced. It
|
|
138
|
+
// tells the model what it cannot otherwise observe; it does not stop a model
|
|
139
|
+
// from guessing anyway.
|
|
140
|
+
sections.push(
|
|
141
|
+
CARRIER_LESS_BACKENDS.has(params.backend)
|
|
142
|
+
? [
|
|
143
|
+
"# entwurf: where this text comes from",
|
|
144
|
+
"",
|
|
145
|
+
PROVENANCE_LEAD,
|
|
146
|
+
`The ${params.backend} rail carries no system-prompt carrier at all — entwurf sends no \`_meta.systemPrompt\`, so everything entwurf tells you, including any operator engraving above, arrives here as user-message text.`,
|
|
147
|
+
"If you are asked what your system prompt says, do not quote this block as one.",
|
|
148
|
+
].join("\n")
|
|
149
|
+
: [
|
|
150
|
+
"# entwurf: where this text comes from",
|
|
151
|
+
"",
|
|
152
|
+
PROVENANCE_LEAD,
|
|
153
|
+
`The ${params.backend} rail does have a system-prompt carrier (\`_meta.systemPrompt\`), and entwurf keeps it deliberately tiny: it carries the operator engraving only — never this narrative, never AGENTS.md.`,
|
|
154
|
+
"So: system prompt = the short engraving; everything below = first-user-message text. If you are asked what your system prompt says, do not quote this block as one.",
|
|
155
|
+
].join("\n"),
|
|
156
|
+
);
|
|
157
|
+
|
|
119
158
|
// Bridge identity + caller-side sibling stance. The closing line is a
|
|
120
159
|
// caller-perspective statement: when THIS session throws an entwurf, the peer
|
|
121
160
|
// on the other end is a sibling, not a worker.
|