@junghanacs/entwurf 0.12.9 → 0.13.0
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 +135 -248
- package/BASELINE.md +46 -7
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +4 -2
- package/DELIVERY.md +5 -4
- package/README.md +46 -18
- package/VERIFY.md +10 -6
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +10 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +148 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
- package/mcp/entwurf-bridge/src/index.ts +190 -82
- package/package.json +7 -3
- package/pi/settings.reference.json +1 -1
- package/pi-extensions/acp-provider.ts +20 -10
- package/pi-extensions/entwurf-control.ts +69 -27
- package/pi-extensions/lib/acp/augment.ts +60 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +183 -8
- package/pi-extensions/lib/acp/backend.ts +5 -1
- package/pi-extensions/lib/acp/config.ts +19 -5
- package/pi-extensions/lib/acp/engraving.ts +3 -1
- package/pi-extensions/lib/acp/event-mapper.ts +10 -3
- package/pi-extensions/lib/acp/models.ts +69 -7
- package/pi-extensions/lib/acp/overlay.ts +234 -3
- package/pi-extensions/lib/acp/tool-surface.ts +12 -4
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
- package/pi-extensions/lib/meta-session.ts +17 -20
- package/run.sh +200 -62
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-cortex.ts +668 -0
- package/scripts/check-acp-provider-surface.ts +50 -6
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +64 -1
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +7 -7
- package/scripts/check-entwurf-facts.ts +12 -12
- package/scripts/check-entwurf-resume-args.ts +22 -60
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +5 -5
- package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-gate-qualification.ts +787 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-probe-cli-shim.ts +879 -0
- package/scripts/check-probe-ordering.ts +2450 -0
- package/scripts/check-shell-quote.ts +5 -4
- package/scripts/fixtures/probe-cli-shim +20 -0
- package/scripts/fixtures/probe-mcp-server.ts +168 -12
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/lib/probe-acp-turn.ts +207 -0
- package/scripts/lib/probe-cli-shim.ts +464 -0
- package/scripts/lib/probe-cli-target.ts +165 -0
- package/scripts/lib/probe-event-log.ts +383 -0
- package/scripts/lib/probe-verdict.ts +1213 -0
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/acp-cortex.json +196 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/probe-ordering.json +1032 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/smoke-acp-cortex-live.ts +392 -0
- package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -266
|
@@ -108,9 +108,10 @@ const SESSION_MESSAGE_TYPE = "entwurf-message";
|
|
|
108
108
|
// with the receive-side [entwurf received ⟵] box. The provider-level context
|
|
109
109
|
// filter in index.ts drops this customType before the LLM sees it — colocated
|
|
110
110
|
// with the emitter so sessions without --entwurf-control are still protected.
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
111
|
+
// There is no second emitter: the v1 native send path ("Layer A") that also drew
|
|
112
|
+
// this box through renderSentMessage() was removed in the 0.12 cutover, and
|
|
113
|
+
// renderSentMessage is now registered for exactly one customType (see
|
|
114
|
+
// registerMessageRenderer below). Do not describe a native sender-side box.
|
|
114
115
|
const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
|
|
115
116
|
|
|
116
117
|
// ============================================================================
|
|
@@ -509,8 +510,8 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
509
510
|
};
|
|
510
511
|
|
|
511
512
|
// Sender-side payload — what renderSentMessage needs to draw the [entwurf sent →]
|
|
512
|
-
// box. Carried
|
|
513
|
-
//
|
|
513
|
+
// box. Carried by the ACP path's CustomMessage `details` — the ONLY carrier since
|
|
514
|
+
// the v1 native renderResult path was removed (0.12 cutover). All four envelope fields are intentionally
|
|
514
515
|
// echoed in the box even though the sender is "this same session" — operators
|
|
515
516
|
// reading a busy multi-session transcript should be able to verify at a glance
|
|
516
517
|
// which 담당자 is on the wire (cwd) and which model identity (agentId) actually
|
|
@@ -520,9 +521,10 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
520
521
|
// re-renders (resize, expand toggle) keep showing the moment the message was
|
|
521
522
|
// actually delivered rather than drifting forward to "now".
|
|
522
523
|
//
|
|
523
|
-
// wants_reply mirrors the receive-side etiquette badge.
|
|
524
|
-
//
|
|
525
|
-
//
|
|
524
|
+
// wants_reply mirrors the receive-side etiquette badge. It stays optional because
|
|
525
|
+
// the box is drawn from a CustomMessage whose `details` may predate the field; the
|
|
526
|
+
// v1 `registerSessionTool` / `entwurfSendParameters` schema this note used to point
|
|
527
|
+
// at is gone (0.12 cutover), so there is no native call site left to grow.
|
|
526
528
|
interface SentBoxData {
|
|
527
529
|
to: string; // target sessionId
|
|
528
530
|
from?: string; // sender agentId, e.g. "entwurf/claude-opus-5"
|
|
@@ -1274,9 +1276,18 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1274
1276
|
target: Type.String({ description: "Target garden id (use entwurf_peers to discover)" }),
|
|
1275
1277
|
intent: StringEnum(["fire-and-forget", "owned-outcome"] as const, {
|
|
1276
1278
|
description:
|
|
1277
|
-
"fire-and-forget = send/reply/hand-off to a LIVE
|
|
1278
|
-
"
|
|
1279
|
-
"(
|
|
1279
|
+
"fire-and-forget = send/reply/hand-off to a LIVE socket target (currently backend pi) or to any " +
|
|
1280
|
+
"citizen with no socket liveness — the decider picks that citizen's rail, and a rail can also " +
|
|
1281
|
+
"REJECT: self-fetch (e.g. Claude Code) → meta-bridge mailbox while that mailbox is DELIVERABLE, " +
|
|
1282
|
+
"else rejected as mailbox-undeliverable; native-push (e.g. Antigravity) → direct injection into " +
|
|
1283
|
+
"its live conversation, which has NO mailbox, and its probe is three-valued — alive: injected, " +
|
|
1284
|
+
"dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate (two rejects, " +
|
|
1285
|
+
"not one). " +
|
|
1286
|
+
"Set wants_reply for an answer. " +
|
|
1287
|
+
"owned-outcome = wake a DORMANT socket-domain citizen via spawn-bg resume ONLY — on a live target " +
|
|
1288
|
+
"rejected as owned-live-no-autosend, on self-fetch as backend-liveness-unsupported, on " +
|
|
1289
|
+
"native-push as native-push-no-resume-authority (both lack resume authority, but the reasons " +
|
|
1290
|
+
"differ because native-push IS probe-measured), and never auto-converted",
|
|
1280
1291
|
}),
|
|
1281
1292
|
message: Type.String({
|
|
1282
1293
|
description:
|
|
@@ -1285,7 +1296,9 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1285
1296
|
}),
|
|
1286
1297
|
mode: Type.Optional(
|
|
1287
1298
|
StringEnum(["steer", "follow_up"] as const, {
|
|
1288
|
-
description:
|
|
1299
|
+
description:
|
|
1300
|
+
"Injection style for a CONTROL-SOCKET send only: steer (immediate) or follow_up (after task). " +
|
|
1301
|
+
"The mailbox, native-push, and spawn-bg plans carry no mode, so it has no effect on those rails.",
|
|
1289
1302
|
}),
|
|
1290
1303
|
),
|
|
1291
1304
|
wants_reply: Type.Optional(Type.Boolean({ description: "Human-conversation reply hint (default false)" })),
|
|
@@ -1299,6 +1312,11 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1299
1312
|
wants_reply?: boolean;
|
|
1300
1313
|
};
|
|
1301
1314
|
|
|
1315
|
+
// TS2589 ("type instantiation is excessively deep") workaround: pi's registerTool
|
|
1316
|
+
// generic infers the handler signature from the TypeBox schema, and this schema is
|
|
1317
|
+
// deep enough to blow the instantiation budget. Casting the FUNCTION (not the
|
|
1318
|
+
// argument) keeps the schema itself typed while stopping the inference walk.
|
|
1319
|
+
// Revisit when pi's registerTool takes an explicit params type parameter.
|
|
1302
1320
|
const registerTool = pi.registerTool as (def: any) => void;
|
|
1303
1321
|
|
|
1304
1322
|
registerTool({
|
|
@@ -1306,28 +1324,51 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1306
1324
|
label: "Dispatch (v2)",
|
|
1307
1325
|
description: `CANONICAL delivery surface for a garden id. When you have a garden id and want to
|
|
1308
1326
|
reach whoever it names — message / reply / hand-off — use THIS verb. A garden id alone does not
|
|
1309
|
-
reveal
|
|
1327
|
+
reveal which rail that citizen answers on — a live socket session, a dormant one, a mailbox-backed
|
|
1328
|
+
self-fetch session, or a native-push session —
|
|
1310
1329
|
and entwurf_v2 is the one surface that reads that and routes correctly (so "when unsure which
|
|
1311
1330
|
transport, use entwurf_v2"). It dispatches to EXISTING targets; brand-new sibling creation is deferred
|
|
1312
1331
|
to a later v2 lane. Dispatch to a garden citizen through the unified entwurf_v2 verb: the 5b decider
|
|
1313
|
-
picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue
|
|
1314
|
-
target's liveness + your intent, runs it under the v2 lock policy
|
|
1315
|
-
|
|
1316
|
-
|
|
1332
|
+
picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue / native-push
|
|
1333
|
+
direct injection) from the target's liveness + your intent, runs it under the v2 lock policy, and reports
|
|
1334
|
+
one outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
|
|
1335
|
+
|
|
1336
|
+
LOCK POLICY (do not over-generalize it): the per-target lock is taken for a control-socket-DOMAIN
|
|
1337
|
+
dispatch, which is BOTH the live send AND the dormant cell's spawn-bg resume — spawn-bg is a separate
|
|
1338
|
+
relaunch transport yet it still runs under that domain's lock. The mailbox and native-push rails are
|
|
1339
|
+
lock-free: the mailbox is guarded instead by active-receiver deliverability, and native-push by its
|
|
1340
|
+
adapter probe.
|
|
1317
1341
|
|
|
1318
1342
|
- target: the garden id of the citizen to reach (required).
|
|
1319
1343
|
- intent: fire-and-forget (a send with no owned result) or owned-outcome (you own the result).
|
|
1320
1344
|
- message: the message/prompt to dispatch (required).
|
|
1321
|
-
- mode: steer or follow_up for a
|
|
1322
|
-
-
|
|
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).
|
|
1323
1349
|
|
|
1324
1350
|
CHOOSING INTENT (picking wrong is rejected, never auto-fixed): to message / reply / hand off a peer
|
|
1325
|
-
that entwurf_peers shows as liveness=alive (a live
|
|
1326
|
-
— it routes to the live control-socket; set wants_reply:true if you need an answer
|
|
1327
|
-
owned-outcome).
|
|
1328
|
-
fire-and-forget
|
|
1329
|
-
|
|
1330
|
-
|
|
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.
|
|
1331
1372
|
|
|
1332
1373
|
The decider — not this surface — chooses the transport.`,
|
|
1333
1374
|
parameters: entwurfV2Parameters,
|
|
@@ -1433,8 +1474,9 @@ async function renderEntwurfPeersForSurface(): Promise<{ text: string; payload:
|
|
|
1433
1474
|
}
|
|
1434
1475
|
|
|
1435
1476
|
function registerListSessionsTool(pi: ExtensionAPI): void {
|
|
1436
|
-
// Same TS2589 workaround as
|
|
1437
|
-
//
|
|
1477
|
+
// Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there
|
|
1478
|
+
// for the revisit conditions. (It used to point at registerSessionTool, which was
|
|
1479
|
+
// removed in the 0.12 cutover, so the pointer dangled.)
|
|
1438
1480
|
const registerTool = pi.registerTool as (def: any) => void;
|
|
1439
1481
|
registerTool({
|
|
1440
1482
|
name: "entwurf_peers",
|
|
@@ -30,14 +30,64 @@
|
|
|
30
30
|
|
|
31
31
|
import { existsSync, readFileSync } from "node:fs";
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
|
-
import { join } from "node:path";
|
|
33
|
+
import { join, resolve } from "node:path";
|
|
34
34
|
import { ENTWURF_PROJECT_CONTEXT_OPEN_TAG } from "../../../protocol.js";
|
|
35
35
|
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
|
+
// §9-4). claude folds the operator engraving into that carrier; a carrier-less
|
|
41
|
+
// backend (cortex, the first — precisely: SYSTEM-PROMPT-carrier-less, since
|
|
42
|
+
// cortex does read `_meta` for an unpromoted caller-session-id seam, §11-8)
|
|
43
|
+
// has nowhere to put it — so its operator engraving
|
|
44
|
+
// rides HERE, prepended as the LEADING section of the first-user augment. It
|
|
45
|
+
// travels on the WIRE only (new-only, never the config signature), so an
|
|
46
|
+
// engraving change applies to the NEXT new cortex session rather than
|
|
47
|
+
// invalidating a live reused one — the documented asymmetry vs claude, where the
|
|
48
|
+
// carrier feeds bridgeConfigSignature.
|
|
49
|
+
const CARRIER_LESS_BACKENDS: ReadonlySet<string> = new Set(["cortex"]);
|
|
50
|
+
|
|
51
|
+
// The env var an operator points at a personal engraving file. Shared surface
|
|
52
|
+
// with the claude carrier (engraving.ts) BY NAME, but read INLINE here (not
|
|
53
|
+
// imported): augment.ts is loaded raw by the strip-types carrier-augment gate,
|
|
54
|
+
// which cannot resolve a `.js` VALUE import of the sibling `.ts` engraving
|
|
55
|
+
// module (augment.ts only value-imports real `.js` files). So the carrier-less
|
|
56
|
+
// override read is self-contained. Unlike engraving.ts's shipped-default path,
|
|
57
|
+
// this ONLY ever reads the operator OVERRIDE file: the shipped claude default
|
|
58
|
+
// (a preset-replacement / auto-memory lever) is meaningless for a non-claude
|
|
59
|
+
// backend and is never injected into a carrier-less augment.
|
|
60
|
+
const ENGRAVING_OVERRIDE_ENV = "ENTWURF_ACP_ENGRAVING_PATH";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render the operator engraving OVERRIDE for a carrier-less backend, or null
|
|
64
|
+
* when unset / empty / whitespace / unreadable. Pure w.r.t. (override file
|
|
65
|
+
* contents, backend, sorted mcpServerNames). Mirrors engraving.ts's `{{backend}}`
|
|
66
|
+
* / `{{mcp_servers}}` token substitution so the operator's file behaves the same
|
|
67
|
+
* on the augment as it would on the claude carrier.
|
|
68
|
+
*/
|
|
69
|
+
function loadCarrierlessOperatorEngraving(backend: string, mcpServerNames: readonly string[]): string | null {
|
|
70
|
+
const envPath = process.env[ENGRAVING_OVERRIDE_ENV]?.trim();
|
|
71
|
+
if (!envPath) return null;
|
|
72
|
+
let source: string;
|
|
73
|
+
try {
|
|
74
|
+
source = readFileSync(resolve(envPath), "utf8");
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const names = [...mcpServerNames].sort();
|
|
79
|
+
const mcpList = names.length > 0 ? names.join(", ") : "(none registered)";
|
|
80
|
+
const rendered = source
|
|
81
|
+
.replace(/\{\{backend\}\}/g, backend)
|
|
82
|
+
.replace(/\{\{mcp_servers\}\}/g, mcpList)
|
|
83
|
+
.trim();
|
|
84
|
+
return rendered.length > 0 ? rendered : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
39
87
|
export interface PiContextAugmentParams {
|
|
40
|
-
/**
|
|
88
|
+
/** The routed backend ("claude" | "cortex"). Load-bearing, not decorative: the
|
|
89
|
+
* narrative names it AND `CARRIER_LESS_BACKENDS` keys off it to decide whether
|
|
90
|
+
* the operator engraving leads the augment. */
|
|
41
91
|
backend: string;
|
|
42
92
|
cwd: string;
|
|
43
93
|
mcpServerNames: readonly string[];
|
|
@@ -58,6 +108,14 @@ export function buildPiContextAugment(params: PiContextAugmentParams): string {
|
|
|
58
108
|
|
|
59
109
|
const sections: string[] = [];
|
|
60
110
|
|
|
111
|
+
// System-prompt-carrier-less backend (cortex): the operator engraving leads the augment,
|
|
112
|
+
// since there is no `_meta.systemPrompt` carrier to hold it. null when no
|
|
113
|
+
// override is configured — the shipped claude default is never injected here.
|
|
114
|
+
if (CARRIER_LESS_BACKENDS.has(params.backend)) {
|
|
115
|
+
const engraving = loadCarrierlessOperatorEngraving(params.backend, params.mcpServerNames);
|
|
116
|
+
if (engraving) sections.push(engraving);
|
|
117
|
+
}
|
|
118
|
+
|
|
61
119
|
// Bridge identity + caller-side sibling stance. The closing line is a
|
|
62
120
|
// caller-perspective statement: when THIS session throws an entwurf, the peer
|
|
63
121
|
// on the other end is a sibling, not a worker.
|
|
@@ -19,15 +19,32 @@
|
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import { createRequire } from "node:module";
|
|
22
|
+
import { homedir } from "node:os";
|
|
22
23
|
import { dirname, join } from "node:path";
|
|
23
24
|
|
|
24
25
|
import type { AcpConnectionLike } from "./acp-client.js";
|
|
25
|
-
import type
|
|
26
|
+
import { enrichMcpServersWithEnvelope, type ResolvedAcpConfig } from "./config.js";
|
|
26
27
|
import { loadEngraving } from "./engraving.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
28
|
+
import {
|
|
29
|
+
CORTEX_MODEL_PREFIX,
|
|
30
|
+
curatedClaudeModels,
|
|
31
|
+
curatedCortexModels,
|
|
32
|
+
SUPPORTED_ANTHROPIC_MODEL_IDS,
|
|
33
|
+
SUPPORTED_CORTEX_MODEL_IDS,
|
|
34
|
+
} from "./models.js";
|
|
35
|
+
import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay, ensureCortexDualHomeOverlay } from "./overlay.js";
|
|
29
36
|
import { buildClaudeSessionMeta } from "./tool-surface.js";
|
|
30
37
|
|
|
38
|
+
// POSIX-safe single-quote wrapper for shell arg interpolation. Byte-for-byte
|
|
39
|
+
// identical to the reference in entwurf-core.ts; PARITY-PINNED by
|
|
40
|
+
// scripts/check-shell-quote.ts (SOURCE_SITES). Used only by the cortex override
|
|
41
|
+
// path below, where operator-configured connection/model tokens are appended to
|
|
42
|
+
// an operator `bash -lc` string — quoting keeps a connection name with shell
|
|
43
|
+
// metacharacters from being reinterpreted by the shell.
|
|
44
|
+
function shellQuote(value: string): string {
|
|
45
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
// ---------------------------------------------------------------------------
|
|
32
49
|
// Shared shapes
|
|
33
50
|
// ---------------------------------------------------------------------------
|
|
@@ -64,13 +81,19 @@ export interface AcpCarrierParams {
|
|
|
64
81
|
|
|
65
82
|
/** ensureOverlay input — cwd + (native) model id + the resolved config. A backend
|
|
66
83
|
* whose overlay/env depends on its OWN settings reads them off `config.adapterSettings`
|
|
67
|
-
* here; backend.ts never inspects config. Same shape as AcpLaunchParams
|
|
68
|
-
* launch are distinct phases, so they keep distinct names). */
|
|
84
|
+
* here; backend.ts never inspects config. Same shape as AcpLaunchParams plus the
|
|
85
|
+
* session key (overlay and launch are distinct phases, so they keep distinct names). */
|
|
69
86
|
export interface AcpOverlayParams {
|
|
70
87
|
cwd: string;
|
|
71
88
|
modelId: string;
|
|
72
89
|
nativeModelId: string;
|
|
73
90
|
config: ResolvedAcpConfig;
|
|
91
|
+
/** The AUTHORITATIVE per-session key backend.ts already computed
|
|
92
|
+
* (resolveSessionKey: opts.sessionId → PI_SESSION_ID → cwd). A session-scoped
|
|
93
|
+
* overlay MUST scope on this value, never on an ambient re-derivation — the
|
|
94
|
+
* re-derived form drops `opts.sessionId`, so two sessions in one process/cwd
|
|
95
|
+
* would alias one overlay (GPT review 2026-07-29, cortex P0-1). */
|
|
96
|
+
sessionKey: string;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
/** buildSessionMeta input — mirrors the newSession `_meta` inputs. */
|
|
@@ -270,9 +293,161 @@ export const claudeAdapter: AcpBackendAdapter = {
|
|
|
270
293
|
// ---------------------------------------------------------------------------
|
|
271
294
|
|
|
272
295
|
/** Registered adapters. Order carries NO routing authority — routeModel decides.
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
|
|
296
|
+
* claude (unprefixed ids) + cortex (the `cortex-` prefix). A further backend
|
|
297
|
+
* appends here with its own reserved prefix; the fail-fast below proves no two
|
|
298
|
+
* adapters claim one id. */
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
|
|
301
|
+
// rail (docs/acp-backend-rail.md §4/§6). It adds ZERO to the common layer:
|
|
302
|
+
// everything cortex-specific lives here + models.ts + overlay.ts + the gates.
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(SUPPORTED_CORTEX_MODEL_IDS);
|
|
306
|
+
|
|
307
|
+
/** Cortex's OWN settings (§10 B): a Snowflake connection name, or null. Opaque to
|
|
308
|
+
* config.ts / backend.ts — only cortexAdapter reads it (casting back). */
|
|
309
|
+
export interface CortexAdapterSettings {
|
|
310
|
+
cortexConnection: string | null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The env var an operator sets to pin a Snowflake connection per-shell without
|
|
314
|
+
* editing settings.json. Wins over `entwurfProvider.cortexConnection`. The
|
|
315
|
+
* ENTWURF_ACP_* convention (PR #40's legacy PI_SHELL_ACP* cortex-connection var renamed). */
|
|
316
|
+
export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
|
|
317
|
+
|
|
318
|
+
export const cortexAdapter: AcpBackendAdapter = {
|
|
319
|
+
backend: "cortex",
|
|
320
|
+
|
|
321
|
+
// Cortex owns the reserved `cortex-` prefix (§9-1). routeModel strips it to the
|
|
322
|
+
// native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
|
|
323
|
+
routeModel(modelId) {
|
|
324
|
+
if (!SUPPORTED_CORTEX_IDS.has(modelId)) return undefined;
|
|
325
|
+
return { nativeModelId: modelId.slice(CORTEX_MODEL_PREFIX.length) };
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
curatedModels() {
|
|
329
|
+
return curatedCortexModels();
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// Cortex's ONLY own setting is the connection name. env override wins over
|
|
333
|
+
// settings (per-shell pin); empty/whitespace → null (Cortex falls back to its
|
|
334
|
+
// own default connection). A non-string settings value fails loud.
|
|
335
|
+
resolveAdapterSettings({ mergedBlock, projectBlock, globalPath, projectPath }): CortexAdapterSettings {
|
|
336
|
+
const raw = mergedBlock.cortexConnection;
|
|
337
|
+
if (raw !== undefined && typeof raw !== "string") {
|
|
338
|
+
const offending = projectBlock.cortexConnection !== undefined ? projectPath : globalPath;
|
|
339
|
+
throw new Error(`${offending}: invalid entwurfProvider settings: cortexConnection must be a string`);
|
|
340
|
+
}
|
|
341
|
+
const envConn = process.env[CORTEX_CONNECTION_ENV]?.trim();
|
|
342
|
+
const settingsConn = raw?.trim();
|
|
343
|
+
const cortexConnection = envConn || settingsConn || null;
|
|
344
|
+
return { cortexConnection };
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// `cortex acp serve` resolved from PATH (the CLI itself IS the ACP server — no
|
|
348
|
+
// `*-acp` npm package, unlike claude). `-c <conn>` appended when a connection
|
|
349
|
+
// is pinned. NO `-m`: the model is enforced per-turn via
|
|
350
|
+
// session/set_config_option (enforceModel below, CP0-M measured GO) and a
|
|
351
|
+
// launch pin would be a SECOND model authority that drifts from it.
|
|
352
|
+
// CORTEX_ACP_COMMAND override runs via `bash -lc` with the selection flags
|
|
353
|
+
// appended so the bridge's choice wins (later yargs args override earlier ones).
|
|
354
|
+
resolveLaunch({ config }) {
|
|
355
|
+
const settings = config.adapterSettings as CortexAdapterSettings | undefined;
|
|
356
|
+
const connection = settings?.cortexConnection?.trim() || undefined;
|
|
357
|
+
const selectionArgs: string[] = [];
|
|
358
|
+
if (connection) selectionArgs.push("-c", connection);
|
|
359
|
+
const override = process.env.CORTEX_ACP_COMMAND?.trim();
|
|
360
|
+
if (override) {
|
|
361
|
+
const command = selectionArgs.length > 0 ? `${override} ${selectionArgs.map(shellQuote).join(" ")}` : override;
|
|
362
|
+
return { command: "bash", args: ["-lc", command] };
|
|
363
|
+
}
|
|
364
|
+
return { command: "cortex", args: ["acp", "serve", ...selectionArgs] };
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
// The overlay location is SESSION-SCOPED (never static), so the spawn env
|
|
368
|
+
// rides ensureOverlay(...).envOverrides; there is no static launch env. The
|
|
369
|
+
// v1.1.8-era CORTEX_DISABLE_AUTO_APPLY_PROFILES knob was retired with the
|
|
370
|
+
// dual-HOME redesign: profiles now live inside the overlay-owned isolated
|
|
371
|
+
// home (empty by construction), and the knob is unmeasured on v1.1.52.
|
|
372
|
+
launchEnvDefaults() {
|
|
373
|
+
return {};
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
// Dual-HOME containment (CP0 D2/D3/D9/D10 — see the overlay module header):
|
|
377
|
+
// refuse an ambient CORTEX_HOME outright, then materialize the session-scoped
|
|
378
|
+
// isolated HOME with auth symlinks, `autoUpdate:false`, and the mcp.json
|
|
379
|
+
// projection of the envelope-enriched explicit servers (cortex ignores the
|
|
380
|
+
// wire mcpServers param, so this file IS how tools reach a cortex session).
|
|
381
|
+
ensureOverlay({ modelId, config, sessionKey }) {
|
|
382
|
+
// D3 — presence refusal, empty string included: upstream's resolver treats
|
|
383
|
+
// a set-but-empty CORTEX_HOME differently from unset, and one ambient value
|
|
384
|
+
// would silently bypass SNOWFLAKE_HOME (the probe's CLAUDE_CODE_EXECUTABLE
|
|
385
|
+
// precondition is the same family). Refuse the ambiguity; never pick a side.
|
|
386
|
+
if ("CORTEX_HOME" in process.env) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
"entwurf: CORTEX_HOME is present in the environment (empty string included) — it overrides " +
|
|
389
|
+
"SNOWFLAKE_HOME inside cortex and would bypass the dual-HOME overlay entirely (CP0 D3). " +
|
|
390
|
+
"Unset it to run a cortex ACP turn.",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
// The scope authority is the AUTHORITATIVE params.sessionKey backend.ts
|
|
394
|
+
// computed — never an ambient re-derivation, which would drop opts.sessionId
|
|
395
|
+
// and alias two same-process/cwd sessions onto one overlay (P0-1). The
|
|
396
|
+
// envelope below still reads PI_SESSION_ID: that is the identity CARRIER for
|
|
397
|
+
// the bridge child (the same source the turn loop's wire enrichment uses),
|
|
398
|
+
// a different axis from overlay-dir scoping.
|
|
399
|
+
const piSessionId = process.env.PI_SESSION_ID?.trim() || undefined;
|
|
400
|
+
const enriched = enrichMcpServersWithEnvelope(config.mcpServers, { modelId, piSessionId });
|
|
401
|
+
const overlay = ensureCortexDualHomeOverlay({
|
|
402
|
+
scopeKey: sessionKey,
|
|
403
|
+
mcpServers: enriched,
|
|
404
|
+
realHome: homedir(),
|
|
405
|
+
});
|
|
406
|
+
return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
// System-prompt-carrier-less (§9-4/§11-8): Cortex ACP exposes no
|
|
410
|
+
// `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
|
|
411
|
+
// equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
|
|
412
|
+
// unexplored and deliberately not part of this contract.) loadCarrier returns
|
|
413
|
+
// null WITHOUT calling loadEngraving, so the cortex turn never touches the
|
|
414
|
+
// shipped-engraving / appendSystemPrompt signature; buildSessionMeta returns
|
|
415
|
+
// undefined so backend.ts omits the `_meta` key entirely. The operator
|
|
416
|
+
// engraving instead rides the first-user augment (augment.ts).
|
|
417
|
+
loadCarrier() {
|
|
418
|
+
return null;
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
buildSessionMeta() {
|
|
422
|
+
return undefined;
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
// Per-turn enforcement via session/set_config_option — the SAME wire call the
|
|
426
|
+
// claude adapter makes, measured live against cortex v1.1.52 (CP0-M): the
|
|
427
|
+
// option id is "model", accepted values are the NATIVE ids (`auto`,
|
|
428
|
+
// `claude-sonnet-5`, `openai-gpt-5.4`, …), and a value cortex no longer
|
|
429
|
+
// serves fails loud BEFORE the prompt (`Unsupported model: …`). PR #40's
|
|
430
|
+
// launch-time `-m` pin was retired for this: set-model is the single model
|
|
431
|
+
// authority (resolveLaunch never passes `-m`), and "auto" is set explicitly
|
|
432
|
+
// rather than treated as an unspoken default.
|
|
433
|
+
async enforceModel({ connection, acpSessionId, nativeModelId, modelId }) {
|
|
434
|
+
const setConfig = connection.setSessionConfigOption;
|
|
435
|
+
if (typeof setConfig !== "function") {
|
|
436
|
+
throw new Error(`setSessionConfigOption unsupported — cannot enforce model ${modelId}`);
|
|
437
|
+
}
|
|
438
|
+
await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// A connection change must invalidate a reused session (§4/§7). Flat,
|
|
442
|
+
// sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
|
|
443
|
+
// + `nativeModelId` are added by backend.ts.
|
|
444
|
+
configSignatureFields(adapterSettings) {
|
|
445
|
+
const settings = adapterSettings as CortexAdapterSettings | undefined;
|
|
446
|
+
return { cortexConnection: settings?.cortexConnection ?? null };
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
|
|
276
451
|
|
|
277
452
|
/**
|
|
278
453
|
* Resolve the backend adapter that owns `modelId`.
|
|
@@ -644,7 +644,11 @@ export function streamAcpTurn(
|
|
|
644
644
|
pushAcpLifecycleNotice(state, `preparing ${adapter.backend} session`);
|
|
645
645
|
// GPT §9-5: materialize the overlay first, then spawn with launchEnvDefaults
|
|
646
646
|
// + overlay.envOverrides merged over process.env (defaultDeps spawnChild).
|
|
647
|
-
|
|
647
|
+
// sessionKey is the AUTHORITATIVE per-session identity (resolveSessionKey:
|
|
648
|
+
// opts.sessionId → PI_SESSION_ID → cwd) — a session-scoped overlay must
|
|
649
|
+
// scope on it, so it rides the params explicitly and the adapter never
|
|
650
|
+
// re-derives a weaker key from ambient env (P0-1).
|
|
651
|
+
const overlay = adapter.ensureOverlay({ cwd, modelId: model.id, nativeModelId, config, sessionKey });
|
|
648
652
|
const launch = adapter.resolveLaunch({ cwd, modelId: model.id, nativeModelId, config });
|
|
649
653
|
child = deps.spawnChild(launch, cwd, { ...adapter.launchEnvDefaults(), ...overlay.envOverrides });
|
|
650
654
|
const spawned = child;
|
|
@@ -8,11 +8,23 @@
|
|
|
8
8
|
// config and hands it to the backend so the documented passthrough actually
|
|
9
9
|
// reaches `newSession`.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// Scope. Ported from the v0.11.0 behavior oracle (index.ts
|
|
12
|
+
// `loadProviderSettings`/`readSettingsFile` + acp-bridge.ts
|
|
13
13
|
// `normalizeMcpServers`/`enrichMcpServersWithEnvelope`) — structure new, behavior
|
|
14
|
-
// preserved. Codex/Gemini fields (codexDisabledFeatures, …) are
|
|
15
|
-
//
|
|
14
|
+
// preserved. Retired Codex/Gemini-era fields (codexDisabledFeatures, …) are NOT
|
|
15
|
+
// carried and are silently ignored if present.
|
|
16
|
+
//
|
|
17
|
+
// The COMMON keys parsed here are Claude-SHAPED by origin (tools / permissionAllow /
|
|
18
|
+
// disallowedTools / settingSources / skillPlugins are claude-agent-acp's surface).
|
|
19
|
+
// What varies per backend is whether they are PROJECTED ONTO THE BACKEND SESSION —
|
|
20
|
+
// not whether they are read at all. Cortex runs native tools and reaches MCP through
|
|
21
|
+
// its own overlay mcp.json projection, so none of those keys shape a cortex session;
|
|
22
|
+
// they nonetheless stay live on the common path, where backend.ts reads `tools` for
|
|
23
|
+
// the backend-invariant exclude-tools preflight and folds every one of them into
|
|
24
|
+
// `bridgeConfigSignature` (so changing one still invalidates a reused cortex
|
|
25
|
+
// session). Backend-OWNED settings never land here: they ride the opaque
|
|
26
|
+
// `adapterSettings` seam (§10 B), which is what keeps a backend-named key like
|
|
27
|
+
// `cortexConnection` off this common shape.
|
|
16
28
|
//
|
|
17
29
|
// PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
|
|
18
30
|
// - `normalizeMcpServers` is pure: a SORTED, validated server list + a sha256
|
|
@@ -71,7 +83,9 @@ export interface ProviderSettings {
|
|
|
71
83
|
disallowedTools?: string[];
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
/** The fully-resolved
|
|
86
|
+
/** The fully-resolved common provider config the turn loop hands to newSession.
|
|
87
|
+
* Claude-shaped by origin; a backend reads only the fields it consumes, and its
|
|
88
|
+
* OWN keys ride `adapterSettings`. */
|
|
75
89
|
export interface ResolvedAcpConfig {
|
|
76
90
|
/** Operator-declared backend (diagnostic guard only — see ProviderSettings.backend).
|
|
77
91
|
* backend.ts asserts this matches the adapter the model id routes to. */
|
|
@@ -44,7 +44,9 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
44
44
|
const DEFAULT_ENGRAVING_PATH = join(HERE, "prompts", "engraving.md");
|
|
45
45
|
|
|
46
46
|
export interface EngravingParams {
|
|
47
|
-
/**
|
|
47
|
+
/** Always "claude" in practice — a system-prompt-carrier-less backend (cortex)
|
|
48
|
+
* returns null from `loadCarrier` WITHOUT calling this loader, so claudeAdapter is
|
|
49
|
+
* its only caller. Kept as a field so the `{{backend}}` token interpolates. */
|
|
48
50
|
backend: string;
|
|
49
51
|
/** MCP server names exposed to the session. SORTED before render for determinism. */
|
|
50
52
|
mcpServerNames: readonly string[];
|
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
// AssistantMessageEvent protocol (text/thinking blocks + tool/permission
|
|
5
5
|
// notices + usage), maintaining a running `partial: AssistantMessage`.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// Single-dialect collapse (NEXT §스코프 + oracle F): the 0.11.0 event-mapper
|
|
8
8
|
// reconciled three backend dialects (Claude rawOutput=array / Codex
|
|
9
9
|
// CallToolResult / Gemini content[]) plus an entwurf_v2 sent-box custom promotion.
|
|
10
|
-
// This lane
|
|
11
|
-
//
|
|
10
|
+
// This lane collapsed that to ONE dialect (rawOutput=array) and dropped the
|
|
11
|
+
// entwurf/gemini/codex special-casing.
|
|
12
|
+
//
|
|
13
|
+
// This module is COMMON layer, not a claude module: the cortex landing reused THIS
|
|
14
|
+
// FILE with no adapter branch and no edit, and the CP2 LIVE turn ran through it.
|
|
15
|
+
// (That is a fact about this mapper only — the landing did touch other common files,
|
|
16
|
+
// e.g. backend.ts's overlay call site.) The collapse is therefore a standing bet,
|
|
17
|
+
// not a proof: a future backend whose session_notification dialect differs from
|
|
18
|
+
// rawOutput=array reopens this file rather than forking it per backend.
|
|
12
19
|
//
|
|
13
20
|
// CRITICAL boundary (GPT S2c Q3): an ACP `tool_call` / `tool_call_update` is
|
|
14
21
|
// rendered as an INFORMATIONAL TEXT NOTICE, never a structured pi `toolcall_*`
|