@junghanacs/entwurf 0.13.0 → 0.14.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 +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- 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 +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- 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 +366 -38
- 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 +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- 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 +10 -20
- 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 +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Currently exposed tools (scope is deliberately narrow — anything that can live
|
|
12
12
|
* as a local skill should live as a skill, not here):
|
|
13
13
|
* - entwurf_v2 — canonical delivery surface for existing garden citizens; the decider
|
|
14
|
-
* chooses live control-socket send /
|
|
14
|
+
* chooses live control-socket send / meta-mailbox enqueue / native-push.
|
|
15
15
|
* - entwurf_peers — entwurf fact surface: garden citizens (meta-records) with liveness +
|
|
16
16
|
* diagnostics (#50 C4: record-less sockets surface THERE, never as identity).
|
|
17
17
|
* Brain = pi-extensions/lib/entwurf-fact-provider (listEntwurfFacts) +
|
|
@@ -24,6 +24,18 @@
|
|
|
24
24
|
* garden record may call this — so the surface is "drain the inbox you were
|
|
25
25
|
* pointed at", not "drain your own". A rung doorbell is a wake attempt;
|
|
26
26
|
* this read is the receipt.
|
|
27
|
+
* - entwurf_register_native — explicit/manual fallback binding an ALREADY-RUNNING native
|
|
28
|
+
* conversation (antigravity) to a garden id. Never a spawn.
|
|
29
|
+
* - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session;
|
|
30
|
+
* returns a LAUNCH receipt only, and the new address arrives later as the
|
|
31
|
+
* sender envelope of the sibling's nonce callback.
|
|
32
|
+
* - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
|
|
33
|
+
* window; target-only, runs no turn, LAUNCH and OBSERVATION receipts stay
|
|
34
|
+
* apart.
|
|
35
|
+
*
|
|
36
|
+
* That list is the WHOLE public surface — seven verbs — and `check-entwurf-bridge-boot`
|
|
37
|
+
* (G1f) holds it as an exact set on the runtime tools/list, so a verb added or dropped
|
|
38
|
+
* here without a decision is red rather than merely undocumented.
|
|
27
39
|
*
|
|
28
40
|
* Removed from this v2-only surface: legacy MCP `entwurf`, `entwurf_resume`, and
|
|
29
41
|
* `entwurf_send`. Use `entwurf_v2` for delivery to existing garden citizens.
|
|
@@ -57,8 +69,11 @@ import { renderEntwurfPeers } from "../../../pi-extensions/lib/entwurf-peers-ren
|
|
|
57
69
|
import { computeSelfAddressability } from "../../../pi-extensions/lib/entwurf-self-address.js";
|
|
58
70
|
import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.js";
|
|
59
71
|
import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.js";
|
|
72
|
+
import { makeVisibleResumeDeps, renderVisibleResume, visibleResume, } from "../../../pi-extensions/lib/entwurf-v2-visible-resume.js";
|
|
60
73
|
import { probeNativeSenderAlive, resolveTrustedMetaSenderIdentity, } from "../../../pi-extensions/lib/meta-sender-identity.js";
|
|
61
74
|
import { defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaInbox, readMetaReceiverMarker, } from "../../../pi-extensions/lib/meta-session.js";
|
|
75
|
+
import { freshCall, renderFreshCall } from "../../../pi-extensions/lib/mux-fresh-call.js";
|
|
76
|
+
import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.js";
|
|
62
77
|
import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.js";
|
|
63
78
|
const HOME = os.homedir();
|
|
64
79
|
// Directory SOURCE is this adapter's own policy — the bridge honours an explicit
|
|
@@ -286,77 +301,49 @@ function abbreviateHomeMcp(cwd) {
|
|
|
286
301
|
}
|
|
287
302
|
// entwurf_v2 — the unified v2 dispatch verb (0.11 step 5d-3b). It hands the
|
|
288
303
|
// target + intent to the 5b decider, which chooses the transport (live
|
|
289
|
-
// control-socket send /
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// native-push branches carry `lock: null` (entwurf-v2-decider.ts). It runs
|
|
304
|
+
// control-socket send / meta-mailbox enqueue / native-push direct injection) and
|
|
305
|
+
// reports one outcome. The per-target lock is NOT taken by every rail: the decider
|
|
306
|
+
// locks only a control-socket-domain dispatch; the mailbox and native-push branches
|
|
307
|
+
// carry `lock: null` (entwurf-v2-decider.ts). It runs
|
|
294
308
|
// IN-PROCESS here (the same production runner pi-native uses) — NOT a delegating
|
|
295
|
-
// RPC — so control, mailbox
|
|
309
|
+
// RPC — so control, mailbox and native-push all flow through
|
|
296
310
|
// `runEntwurfV2`. The sender envelope is
|
|
297
311
|
// `buildSendSenderEnvelope()` verbatim (origin/replyable as resolved) — v2 does
|
|
298
312
|
// NOT gate on replyability (a `wants_reply` from an external/non-replyable caller
|
|
299
313
|
// is surfaced honestly, not rejected; the decider routes on target + intent, not
|
|
300
314
|
// sender replyability).
|
|
301
|
-
server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"mailbox-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"wants_reply:true if you need an answer (wants_reply is NOT owned-outcome). Replies to a citizen " +
|
|
323
|
-
"with NO socket liveness (liveness=unsupported) are ALSO fire-and-forget, and the decider picks " +
|
|
324
|
-
"that citizen's own rail: a self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, " +
|
|
325
|
-
"while a native-push backend (e.g. Antigravity) gets direct injection into its live conversation " +
|
|
326
|
-
"and has NO mailbox at all — do not assume mailbox semantics for every unsupported citizen. A " +
|
|
327
|
-
"native-push target IS measured by its own adapter probe, and that probe is THREE-valued, so " +
|
|
328
|
-
"the send is never silently queued: alive → injected; dead → rejected as " +
|
|
329
|
-
"native-push-target-dead; indeterminate → rejected as native-push-probe-indeterminate. " +
|
|
330
|
-
"Those two rejects stay distinct because 'we could not establish it' is not 'it is gone' — " +
|
|
331
|
-
"collapsing them reports a guess as a fact. THERE IS A " +
|
|
332
|
-
"THIRD RESULT, so do not read liveness=unsupported as 'reachable by some rail': the mailbox path " +
|
|
333
|
-
"delivers only to a DELIVERABLE citizen (a self-fetch backend whose receiver is live and armed), " +
|
|
334
|
-
"so a terminated Claude Code session — and any record whose backend has no adapter on this lane, " +
|
|
335
|
-
"e.g. codex — is rejected as mailbox-undeliverable rather than queued into an inbox nobody drains. " +
|
|
336
|
-
"unsupported means only 'this backend has no control-socket probe'. " +
|
|
337
|
-
"owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently " +
|
|
338
|
-
"backend pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor " +
|
|
339
|
-
"native-push has resume authority, but they reject under DIFFERENT reasons — self-fetch as " +
|
|
340
|
-
"backend-liveness-unsupported, native-push as native-push-no-resume-authority. " +
|
|
341
|
-
"It is NEVER auto-converted — so pick the right intent up front. " +
|
|
342
|
-
"mode applies to a CONTROL-SOCKET send only — it is the injection style for a live pi turn, and " +
|
|
343
|
-
"the mailbox, native-push, and spawn-bg plans carry no mode at all, so setting it for those " +
|
|
344
|
-
"targets changes nothing (a native-push send IS live and still ignores it). wants_reply rides " +
|
|
345
|
-
"every rail. Use entwurf_peers to discover targets. " +
|
|
346
|
-
"Payload guidance: message hard cap 16000 chars. For larger reviews/logs, write an " +
|
|
347
|
-
"artifact and dispatch its path plus a short digest; avoid multi-part sends because " +
|
|
348
|
-
"mailbox doorbells may coalesce.", {
|
|
315
|
+
server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id " +
|
|
316
|
+
"alone does not say which rail that citizen answers on. Give target + intent; the decider picks transport " +
|
|
317
|
+
"from liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge " +
|
|
318
|
+
"mailbox; probe-alive native-push citizen → direct injection into its conversation) and reports ONE " +
|
|
319
|
+
"outcome (delivered / rejected / delivered-but-lock-dirty). EXISTING targets only; discover with " +
|
|
320
|
+
"entwurf_peers. A peer entwurf_peers shows as liveness=alive → fire-and-forget. A " +
|
|
321
|
+
"citizen with NO socket liveness (liveness=unsupported) is ALSO fire-and-forget — unsupported means only " +
|
|
322
|
+
'"no control-socket probe" — and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) ' +
|
|
323
|
+
"gets the mailbox, a native-push backend (e.g. Antigravity) gets direct injection and has NO mailbox at " +
|
|
324
|
+
"all. THERE IS A THIRD RESULT: the mailbox delivers only to a DELIVERABLE citizen, so a terminated " +
|
|
325
|
+
"session, or a backend with no adapter here (e.g. codex), is mailbox-undeliverable, not queued for an " +
|
|
326
|
+
"inbox nobody drains. The native-push probe is 3-valued: alive → injected; dead → " +
|
|
327
|
+
"native-push-target-dead; indeterminate → native-push-probe-indeterminate (unestablished ≠ gone). " +
|
|
328
|
+
"DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets dormant-fire-forget-unsupported " +
|
|
329
|
+
"— same receiver rule as the mailbox, no active drainer means no delivery. " +
|
|
330
|
+
"The intent that used to answer there, owned-outcome, resumed it by launching a hidden background child " +
|
|
331
|
+
"and was withdrawn under the visible-first rule — re-open the session yourself, then dispatch again. " +
|
|
332
|
+
"LOCK: taken for a control-socket-DOMAIN dispatch. The " +
|
|
333
|
+
"mailbox and native-push rails are lock-free — deliverability and the adapter probe guard them. mode " +
|
|
334
|
+
"applies to a CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message " +
|
|
335
|
+
"caps at 16000 chars; send an artifact path + digest for more.", {
|
|
349
336
|
target: z.string().min(1).describe("Target garden id (use entwurf_peers to discover)"),
|
|
350
337
|
intent: z
|
|
351
|
-
.enum(["fire-and-forget"
|
|
338
|
+
.enum(["fire-and-forget"])
|
|
352
339
|
.describe("fire-and-forget = send/reply/hand-off to a LIVE socket target or to any citizen with no " +
|
|
353
340
|
"socket liveness — the decider picks its rail, and a rail can also REJECT (self-fetch → " +
|
|
354
341
|
"mailbox when deliverable, else mailbox-undeliverable; native-push → alive: direct injection, " +
|
|
355
342
|
"dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate); set " +
|
|
356
|
-
"wants_reply for an answer. owned-outcome
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
343
|
+
"wants_reply for an answer. This is the ONLY intent: owned-outcome, which resumed a dormant " +
|
|
344
|
+
"citizen by launching a hidden background child, was withdrawn under the visible-first rule. " +
|
|
345
|
+
"It is not selectable, not deprecated-but-tolerated; a dormant citizen is currently " +
|
|
346
|
+
"unreachable by this verb and rejects as dormant-fire-forget-unsupported."),
|
|
360
347
|
message: z
|
|
361
348
|
.string()
|
|
362
349
|
.min(1)
|
|
@@ -366,8 +353,8 @@ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids. When you h
|
|
|
366
353
|
.enum(["steer", "follow_up"])
|
|
367
354
|
.optional()
|
|
368
355
|
.describe("Injection style for a CONTROL-SOCKET send only: steer (interrupt the current turn) or " +
|
|
369
|
-
"follow_up (queue after it). The mailbox
|
|
370
|
-
"
|
|
356
|
+
"follow_up (queue after it). The mailbox and native-push plans carry no mode, so it has no " +
|
|
357
|
+
"effect on those rails."),
|
|
371
358
|
wants_reply: z.boolean().optional().describe("Human-conversation reply hint (default false)"),
|
|
372
359
|
}, async ({ target, intent, message, mode, wants_reply }) => {
|
|
373
360
|
try {
|
|
@@ -375,8 +362,9 @@ server.tool("entwurf_v2", "CANONICAL DELIVERY SURFACE for garden ids. When you h
|
|
|
375
362
|
// + the mailbox body sender share one envelope. No replyability gate (see above).
|
|
376
363
|
const sender = await buildSendSenderEnvelope();
|
|
377
364
|
const rendered = await runAndRenderEntwurfV2FromSurface({ target, intent, message, mode, wants_reply },
|
|
378
|
-
//
|
|
379
|
-
//
|
|
365
|
+
// No trust-preflight inputs are passed, and none exist to pass: the preflight on this
|
|
366
|
+
// path guarded the resume verdict and left with `owned-outcome`. `senderProvider` is
|
|
367
|
+
// the whole options surface now (see the pi-native surface for the same note).
|
|
380
368
|
{ senderProvider: () => sender });
|
|
381
369
|
return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
|
|
382
370
|
}
|
|
@@ -459,7 +447,9 @@ server.tool("entwurf_peers", "List the entwurf fact surface: garden citizens (fr
|
|
|
459
447
|
"`unsupported` for a backend with no control-socket probe such as claude-code); the dispatch " +
|
|
460
448
|
"decision (send vs resume) is computed LATER by the entwurf_v2 contract from that liveness, " +
|
|
461
449
|
"not here — this surface carries no per-row routing field, so do not read a transport off a " +
|
|
462
|
-
"row.
|
|
450
|
+
"row. A `dead` row is a REPORTED FACT and nothing more: that citizen is dormant and is " +
|
|
451
|
+
"currently unreachable by any verb, so listing it grants no action — appearing here is not " +
|
|
452
|
+
"an invitation to dispatch. In particular `unsupported` does NOT mean mailbox: it means this backend has no " +
|
|
463
453
|
"control-socket probe. Which rail it answers on is a capability the decider resolves at dispatch " +
|
|
464
454
|
"time, and there are THREE possible answers, not two — a self-fetch mailbox (only while that " +
|
|
465
455
|
"mailbox is deliverable), native-push direct injection (only while its adapter probe is alive), " +
|
|
@@ -469,8 +459,8 @@ server.tool("entwurf_peers", "List the entwurf fact surface: garden citizens (fr
|
|
|
469
459
|
"is not a departed host. So " +
|
|
470
460
|
"`unsupported` does not promise reachability either: a record whose backend has no adapter on " +
|
|
471
461
|
"this lane resolves to that reject. " +
|
|
472
|
-
"Note: this is the *active* world
|
|
473
|
-
"existing garden id to entwurf_v2.", {}, async () => {
|
|
462
|
+
"Note: this is the *active* world and it is facts-only — it creates nothing. Pass an " +
|
|
463
|
+
"existing garden id to entwurf_v2; to open a NEW sibling use entwurf_fresh_call.", {}, async () => {
|
|
474
464
|
try {
|
|
475
465
|
// Meta-store axis: entries WITH their kind (ENOENT = fresh install = empty; any
|
|
476
466
|
// other readdir failure is a real error, not a silent empty). The name-only
|
|
@@ -562,6 +552,109 @@ server.tool("entwurf_register_native", "Register an ALREADY-RUNNING native conve
|
|
|
562
552
|
return textErr(`entwurf_register_native error: ${err instanceof Error ? err.message : String(err)}`);
|
|
563
553
|
}
|
|
564
554
|
});
|
|
555
|
+
// The caller identity for THIS surface comes from the same authoritative resolution
|
|
556
|
+
// `entwurf_self` uses — BOTH of its certified paths: the pi carrier this child inherited
|
|
557
|
+
// (PI_SESSION_ID + PI_AGENT_ID, which a record minted at birth), or a trusted meta-sender marker
|
|
558
|
+
// the native host's own hook wrote. Neither is a guess and neither is deprecated here.
|
|
559
|
+
//
|
|
560
|
+
// What is refused is a caller-supplied id. The measured failure mode is exactly that: asked for
|
|
561
|
+
// its own garden id, a fresh cell answered with its uuidv7 `PI_SESSION_ID` value read out of the
|
|
562
|
+
// environment by an MCP server it had spawned itself — confidently, and wrong. A sibling launched
|
|
563
|
+
// against that answer would call home to a garden id nobody holds.
|
|
564
|
+
server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operator's own tmux session and hand it a first task. Two fixed " +
|
|
565
|
+
"backends only: pi, claude-code. The sibling's FIRST action is a callback to you carrying a nonce, and the " +
|
|
566
|
+
"sender envelope of that callback is its garden id — that is how you learn the address of something that " +
|
|
567
|
+
"did not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus that nonce) and nothing " +
|
|
568
|
+
"else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
|
|
569
|
+
"for the callback; if it never arrives the window is visible and can be read directly. For EXISTING " +
|
|
570
|
+
"citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
|
|
571
|
+
"is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code); there are no " +
|
|
572
|
+
"arbitrary command/cwd/env knobs. Do not put secrets in the task — model and task argv are visible to " +
|
|
573
|
+
"same-user processes on this host. Requires that this agent itself runs " +
|
|
574
|
+
"inside tmux: without a pane anchor there is no session to open a sibling beside.", {
|
|
575
|
+
backend: z
|
|
576
|
+
.enum(["pi", "claude-code"])
|
|
577
|
+
.describe("Which fixed runtime to open. Only these two; there is no arbitrary command."),
|
|
578
|
+
model: z
|
|
579
|
+
.string()
|
|
580
|
+
.min(1)
|
|
581
|
+
.max(200)
|
|
582
|
+
// The `\[` below is USELESS TO JS AND LOAD-BEARING TO THE HOST. zod emits this source
|
|
583
|
+
// text verbatim as the JSON Schema `pattern`, and the host's tool-schema validator is a
|
|
584
|
+
// Rust regex engine that rejects an unescaped `[` inside a character class as "unclosed
|
|
585
|
+
// character class" — a 400 on tools/list that stops EVERY Claude session from opening
|
|
586
|
+
// (7-M-fix). Biome's safe fix removes it; do not accept that fix. The suppression must
|
|
587
|
+
// stay on the line DIRECTLY above the expression, or it attaches to nothing and reads
|
|
588
|
+
// as an unused suppression while the escape goes back to being fixable.
|
|
589
|
+
// biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
|
|
590
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
|
|
591
|
+
.describe("Required runtime model: canonical provider/model for pi, or a Claude Code model id/alias."),
|
|
592
|
+
task: z
|
|
593
|
+
.string()
|
|
594
|
+
.min(1)
|
|
595
|
+
.max(16000)
|
|
596
|
+
.describe("What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description)."),
|
|
597
|
+
}, async ({ backend, model, task }) => {
|
|
598
|
+
let callerGardenId = null;
|
|
599
|
+
try {
|
|
600
|
+
const self = await buildAuthoritativeSelfEnvelope();
|
|
601
|
+
callerGardenId = self.envelope.sessionId;
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
// ONE error is a legitimate answer here: this host has no authoritative identity at all
|
|
605
|
+
// (no pi carrier inherited, no trusted marker written), so the sibling would have nowhere
|
|
606
|
+
// to call home. That normalises to the named refusal.
|
|
607
|
+
//
|
|
608
|
+
// Everything else — a corrupt record, an unreadable store, a broken marker — keeps its
|
|
609
|
+
// own diagnosis and fails loud. Folding those into "you are anonymous" would relabel a
|
|
610
|
+
// store defect as a wiring choice, and the operator would go looking in the wrong place.
|
|
611
|
+
if (!(err instanceof EntwurfEnvelopeWiringError))
|
|
612
|
+
throw err;
|
|
613
|
+
callerGardenId = null;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const rendered = renderFreshCall(freshCall({ backend, model, task, callerGardenId }));
|
|
617
|
+
return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
return textErr(`entwurf_fresh_call error: ${err instanceof Error ? err.message : String(err)}`);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
// The composition root for a visible resume: this surface is the only layer that may know BOTH
|
|
624
|
+
// halves, so it is where the mux launcher is handed to the v2 composition. `entwurf-v2-visible-
|
|
625
|
+
// resume` cannot open a window on its own and `mux-resume-call` cannot read a record — that is
|
|
626
|
+
// the import fence in docs/mux-launch-rail.md §11, expressed as a function argument.
|
|
627
|
+
const visibleResumeLaunch = (input) => {
|
|
628
|
+
const launched = resumeCall({ cwd: input.cwd, runtimeArgs: input.runtimeArgs });
|
|
629
|
+
return launched.ok
|
|
630
|
+
? { ok: true, handle: launched.receipt }
|
|
631
|
+
: { ok: false, reason: launched.reason, hint: RESUME_CALL_REJECT_HINT[launched.reason] };
|
|
632
|
+
};
|
|
633
|
+
server.tool("entwurf_resume_call", "Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux " +
|
|
634
|
+
"session. The record supplies everything — which transcript, which model, which provider, which cwd — so " +
|
|
635
|
+
"the only input is the target id: there is no model override, no task, and no prompt. This runs NO turn: " +
|
|
636
|
+
"the window comes back with the conversation and waits, and talking to it is still entwurf_v2 " +
|
|
637
|
+
"fire-and-forget on the socket this call stands up. You get TWO receipts and they mean different things: a " +
|
|
638
|
+
"LAUNCH receipt (tmux made a window and was asked to start pi) and an OBSERVATION receipt (the control " +
|
|
639
|
+
"socket answered under the same id, or resume-unobserved). Unobserved is a real outcome, not an error to " +
|
|
640
|
+
"retry — the window is visible, so read it. A citizen that is already LIVE is refused: address it with " +
|
|
641
|
+
"entwurf_v2 instead. Only pi citizens have a same-id resume, because only they stand a control socket up. " +
|
|
642
|
+
"Requires that this agent itself runs inside tmux.", {
|
|
643
|
+
target: z
|
|
644
|
+
.string()
|
|
645
|
+
.min(1)
|
|
646
|
+
.regex(/^\d{8}T\d{6}-[0-9a-f]{6}$/)
|
|
647
|
+
.describe("Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers)."),
|
|
648
|
+
}, async ({ target }) => {
|
|
649
|
+
try {
|
|
650
|
+
const result = await visibleResume(target, makeVisibleResumeDeps(visibleResumeLaunch));
|
|
651
|
+
const rendered = renderVisibleResume(result);
|
|
652
|
+
return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
565
658
|
// ============================================================================
|
|
566
659
|
// Main
|
|
567
660
|
// ============================================================================
|
|
@@ -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
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// claude-agent-acp's SettingsManager loads the operator's `~/.claude/settings.json`
|
|
4
4
|
// DIRECTLY (CLAUDE_CONFIG_DIR is the only knob that redirects that read). So the
|
|
5
|
-
// operator's native `permissions.defaultMode` (
|
|
6
|
-
// per-cwd memory/projects state would otherwise leak into entwurf ACP
|
|
5
|
+
// operator's native `permissions.defaultMode` (whatever its current value), hooks,
|
|
6
|
+
// plugins, and per-cwd memory/projects state would otherwise leak into entwurf ACP
|
|
7
7
|
// sessions. The overlay redirects SettingsManager at a pi-owned directory whose
|
|
8
8
|
// `settings.json` WE author (minimal, `hooks:{}`), while keeping exactly the
|
|
9
9
|
// operator entries a backend needs (credentials, caches, built-in skills)
|
|
@@ -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
|
//
|
|
@@ -86,9 +86,11 @@ export const OVERLAY_EMPTY_DIRS = new Set(["projects", "sessions"]);
|
|
|
86
86
|
export const OVERLAY_BINARY_OWNED = new Set([".claude.json", "backups", "settings.json"]);
|
|
87
87
|
/**
|
|
88
88
|
* Minimal overlay settings.json. Only fields with a reason to pin:
|
|
89
|
-
* - `permissions.defaultMode: "
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* - `permissions.defaultMode: "bypassPermissions"` is deliberate unattended
|
|
90
|
+
* ACP operation: a tool call must never suspend a model turn on an interactive
|
|
91
|
+
* permission prompt. This does not widen the callable surface — explicit
|
|
92
|
+
* `tools`/`disallowedTools` still shape it, and `permissionAllow` still rides
|
|
93
|
+
* the inline Claude settings — or bypass backend authentication.
|
|
92
94
|
* - `autoMemoryEnabled: false` — SDK opt-out for auto-memory (defense in
|
|
93
95
|
* depth; the tiny non-empty engraving/preset replacement is the primary
|
|
94
96
|
* write-containment lever for Claude ACP).
|
|
@@ -97,7 +99,7 @@ export const OVERLAY_BINARY_OWNED = new Set([".claude.json", "backups", "setting
|
|
|
97
99
|
*/
|
|
98
100
|
export function overlaySettingsJson() {
|
|
99
101
|
return `${JSON.stringify({
|
|
100
|
-
permissions: { defaultMode: "
|
|
102
|
+
permissions: { defaultMode: "bypassPermissions" },
|
|
101
103
|
autoMemoryEnabled: false,
|
|
102
104
|
hooks: {},
|
|
103
105
|
}, null, 2)}\n`;
|
|
@@ -201,11 +203,11 @@ export function ensureClaudeConfigOverlay(realDir = CLAUDE_REAL_CONFIG_DIR, over
|
|
|
201
203
|
}
|
|
202
204
|
// ============================================================================
|
|
203
205
|
// 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
|
|
206
|
+
// (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md “Cortex Code audit”).
|
|
205
207
|
// ============================================================================
|
|
206
208
|
//
|
|
207
209
|
// Why this is NOT the claude-shaped `SNOWFLAKE_HOME`-only overlay PR #40 shipped
|
|
208
|
-
// (each point is a measured defect, not a preference —
|
|
210
|
+
// (each point is a measured defect, not a preference — Cortex audit D-numbers):
|
|
209
211
|
//
|
|
210
212
|
// - D2: cortex reads `CONFIG_DIRS = [".claude", ".cortex"]` at `homedir()` and
|
|
211
213
|
// `~/.claude/skills` — a SNOWFLAKE_HOME redirect cannot move that axis. An
|
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
import * as net from "node:net";
|
|
17
17
|
/**
|
|
18
18
|
* THE `<sender_info>` synthesis — the one place the sender envelope becomes
|
|
19
|
-
* message text.
|
|
19
|
+
* message text. ONE consumer since the visible-first cut (#50 C3):
|
|
20
20
|
* - the live socket rail's RECEIVER (entwurf-control handleCommand("send")
|
|
21
|
-
* appends it to the delivered customMessage)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* appends it to the delivered customMessage).
|
|
22
|
+
*
|
|
23
|
+
* The second consumer was the dormant spawn-resume rail's SENDER, which appended
|
|
24
|
+
* the same shape to a resume prompt so a resumed citizen woke knowing who called.
|
|
25
|
+
* That rail is gone. The shape stays single-sourced here so a future VISIBLE
|
|
26
|
+
* resume renders an identical envelope rather than inventing a second one.
|
|
25
27
|
* `wants_reply` is emitted only when explicitly true — an unset/false marker
|
|
26
28
|
* renders nothing (etiquette marker, not transport contract).
|
|
27
29
|
*/
|