@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
|
@@ -98,13 +98,17 @@ PERMISSION_STATE_SCHEMA_VERSION = 2
|
|
|
98
98
|
# EVERY call. The bridge was registered and two thirds of it was unusable — the same failure the
|
|
99
99
|
# doctor already names for entwurf_v2, just never checked for the other two.
|
|
100
100
|
#
|
|
101
|
-
# The
|
|
101
|
+
# The four tools deliberately NOT here, because a grant we do not need is a grant we should not take:
|
|
102
102
|
# entwurf_inbox_read — native-push has no inbox to drain. An agy citizen has no meta-mailbox
|
|
103
103
|
# directory at all (measured), so granting it would pre-approve a rail this
|
|
104
104
|
# backend does not have.
|
|
105
105
|
# entwurf_register_native — explicit/manual fallback, not the normal birth path (DELIVERY.md):
|
|
106
106
|
# agy births automatically from PreInvocation. Auto-approving a registration
|
|
107
107
|
# verb the normal path never calls is exactly the excess this list avoids.
|
|
108
|
+
# entwurf_fresh_call / entwurf_resume_call — both launch into the CALLER's own tmux session and
|
|
109
|
+
# refuse without a pane anchor. An agy conversation is not a tmux client,
|
|
110
|
+
# so these are not on its normal path either; the count moved from two to
|
|
111
|
+
# four when 0.14 shipped the mux verbs, and the granted set did not.
|
|
108
112
|
ALLOW_RULES = tuple(f"mcp({SERVER_KEY}/{tool})" for tool in ("entwurf_v2", "entwurf_peers", "entwurf_self"))
|
|
109
113
|
# Rules that MATCH one of our tools: its exact grant, the server-wide rule, and the action wildcard.
|
|
110
114
|
# Membership here is a statement about agy's matcher, not about which list the rule sits in — the
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// exclude a built-in the Claude child still exposes (`read`); the turn must fail
|
|
7
7
|
// fast into the returned stream as an `error` event — never reach a spawn, never
|
|
8
8
|
// emit `done`. No live backend is launched (the preflight throws first), so this
|
|
9
|
-
// stays deterministic and IN pnpm check.
|
|
9
|
+
// stays deterministic and IN pnpm run check:full.
|
|
10
10
|
//
|
|
11
11
|
// backend.ts imports its siblings with `.js` suffixes (the root/jiti runtime
|
|
12
12
|
// convention), which `node --experimental-strip-types` cannot resolve directly.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Pure + temp-dir fs, no spawn/child — IN pnpm check.
|
|
16
16
|
|
|
17
17
|
import { strict as assert } from "node:assert";
|
|
18
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
@@ -38,6 +38,11 @@ const BRIDGE_MARK = "operating through entwurf";
|
|
|
38
38
|
|
|
39
39
|
// ===========================================================================
|
|
40
40
|
// 1) loadEngraving is pure/deterministic + interpolates backend/mcp (sorted)
|
|
41
|
+
//
|
|
42
|
+
// This cell owns the BODY of the render. The carrier's leading boundary is cell
|
|
43
|
+
// 11's axis, so the body assertions here match on the tail — a cell that pinned
|
|
44
|
+
// the whole string would fire first on any boundary regression and steal the
|
|
45
|
+
// attribution from the claim that actually owns it.
|
|
41
46
|
// ===========================================================================
|
|
42
47
|
{
|
|
43
48
|
const file = join(tmp, "engraving.md");
|
|
@@ -48,16 +53,15 @@ const BRIDGE_MARK = "operating through entwurf";
|
|
|
48
53
|
const a = loadEngraving({ backend: "claude", mcpServerNames: ["zebra", "alpha"] });
|
|
49
54
|
const b = loadEngraving({ backend: "claude", mcpServerNames: ["zebra", "alpha"] });
|
|
50
55
|
assert.equal(a, b, "loadEngraving is deterministic: same inputs → same output");
|
|
51
|
-
assert.
|
|
56
|
+
assert.ok(a?.endsWith("backend=claude mcp=alpha, zebra"), "interpolates {{backend}} and SORTED {{mcp_servers}}");
|
|
52
57
|
// Order of the input must not change the render (signature-stability guard).
|
|
53
58
|
assert.equal(
|
|
54
59
|
loadEngraving({ backend: "claude", mcpServerNames: ["alpha", "zebra"] }),
|
|
55
60
|
a,
|
|
56
61
|
"mcpServerNames order does not drift the rendered carrier (sorted)",
|
|
57
62
|
);
|
|
58
|
-
assert.
|
|
59
|
-
loadEngraving({ backend: "claude", mcpServerNames: [] }),
|
|
60
|
-
"backend=claude mcp=(none registered)",
|
|
63
|
+
assert.ok(
|
|
64
|
+
loadEngraving({ backend: "claude", mcpServerNames: [] })?.endsWith("backend=claude mcp=(none registered)"),
|
|
61
65
|
"no mcp servers → (none registered)",
|
|
62
66
|
);
|
|
63
67
|
} finally {
|
|
@@ -86,7 +90,15 @@ const BRIDGE_MARK = "operating through entwurf";
|
|
|
86
90
|
const prev = process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
87
91
|
process.env.ENTWURF_ACP_ENGRAVING_PATH = whitespace;
|
|
88
92
|
try {
|
|
89
|
-
assert.equal(
|
|
93
|
+
assert.equal(
|
|
94
|
+
loadEngraving({ backend: "claude", mcpServerNames: [] }),
|
|
95
|
+
null,
|
|
96
|
+
"[QK:CARRIER-OPT-OUT-SURVIVES-SEPARATOR] a whitespace-only override must still be the operator OPT-OUT (null), " +
|
|
97
|
+
"never a carrier made of nothing but the A-join boundary. Emptiness is decided on the TRIMMED BODY, before " +
|
|
98
|
+
"the leading separator is attached — attach it first and an emptied engraving file silently becomes a " +
|
|
99
|
+
'non-empty "\\n\\n" carrier: the opt-out disappears, the claude_code preset gets replaced by whitespace, and ' +
|
|
100
|
+
"bridgeConfigSignature folds a string the operator never wrote",
|
|
101
|
+
);
|
|
90
102
|
} finally {
|
|
91
103
|
if (prev === undefined) delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
92
104
|
else process.env.ENTWURF_ACP_ENGRAVING_PATH = prev;
|
|
@@ -106,9 +118,9 @@ const BRIDGE_MARK = "operating through entwurf";
|
|
|
106
118
|
// `_meta.systemPrompt` → full preset replacement). That replacement is what
|
|
107
119
|
// strips the preset's auto-memory section so the model never learns it has a
|
|
108
120
|
// per-session memory store — the memory containment v1 shipped, restored here.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"# Engraving Here",
|
|
121
|
+
// (`includes`, not `equal`: the leading A-join boundary is cell 11's axis.)
|
|
122
|
+
assert.ok(
|
|
123
|
+
loadEngraving({ backend: "claude", mcpServerNames: [] })?.includes("# Engraving Here"),
|
|
112
124
|
"shipped default engraving is the non-empty v1 lever → string carrier (preset replaced, auto-memory stripped)",
|
|
113
125
|
);
|
|
114
126
|
|
|
@@ -320,10 +332,234 @@ function ctxWith(firstUser: string): Context {
|
|
|
320
332
|
);
|
|
321
333
|
}
|
|
322
334
|
|
|
335
|
+
// ===========================================================================
|
|
336
|
+
// 10) CARRIER PROVENANCE — the augment states what it is, per rail.
|
|
337
|
+
//
|
|
338
|
+
// Measured 2026-07-30: asked where its instructions came from, the Claude ACP
|
|
339
|
+
// model reported the bridge-identity and task-stance paragraphs as its SYSTEM
|
|
340
|
+
// prompt. They are a first-user-message prepend. The model was not lying — on
|
|
341
|
+
// the wire it cannot tell a long first user message from a system prompt, and
|
|
342
|
+
// nothing in the block said which it was. The rails also differ for real
|
|
343
|
+
// (backend-adapter.ts: claude's buildSessionMeta carries `_meta.systemPrompt`;
|
|
344
|
+
// cortex's returns undefined so no `_meta` is sent), so ONE generic sentence
|
|
345
|
+
// would be false on one of them.
|
|
346
|
+
//
|
|
347
|
+
// What this pins is a STATEABILITY claim, not an obedience claim: the boundary
|
|
348
|
+
// must be present and rail-correct in the text the model reads. It cannot make
|
|
349
|
+
// a model answer honestly — it removes the excuse that it had no way to know.
|
|
350
|
+
// ===========================================================================
|
|
351
|
+
{
|
|
352
|
+
const claudeAug = buildPiContextAugment({ backend: "claude", cwd: tmp, mcpServerNames: [], homeDir: tmp });
|
|
353
|
+
const cortexAug = buildPiContextAugment({ backend: "cortex", cwd: tmp, mcpServerNames: [], homeDir: tmp });
|
|
354
|
+
|
|
355
|
+
for (const [rail, aug] of [
|
|
356
|
+
["claude", claudeAug],
|
|
357
|
+
["cortex", cortexAug],
|
|
358
|
+
] as const) {
|
|
359
|
+
assert.ok(
|
|
360
|
+
aug.includes("prepended to the FIRST USER MESSAGE of this session. It is not your system prompt."),
|
|
361
|
+
`[QK:CARRIER-PROVENANCE-STATED] the ${rail} augment must say what it IS (first-user-message text) and what it ` +
|
|
362
|
+
"is NOT (the system prompt) — without that line a model reading it has no way to attribute it, and the " +
|
|
363
|
+
"measured failure was exactly that misattribution",
|
|
364
|
+
);
|
|
365
|
+
assert.ok(
|
|
366
|
+
aug.indexOf("# entwurf: where this text comes from") < aug.indexOf(BRIDGE_MARK),
|
|
367
|
+
`the ${rail} provenance frame precedes the bridge narrative it is about`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Rail-correct, not generic: claude names its carrier and scopes it to the
|
|
372
|
+
// engraving; cortex denies having one at all.
|
|
373
|
+
assert.ok(
|
|
374
|
+
claudeAug.includes("does have a system-prompt carrier (`_meta.systemPrompt`)") &&
|
|
375
|
+
claudeAug.includes("it carries the operator engraving only"),
|
|
376
|
+
"[QK:CARRIER-FRAME-NAMES-CLAUDE-CARRIER] the claude frame must name the real carrier AND scope it to the engraving — " +
|
|
377
|
+
`saying only "this is not your system prompt" leaves the model to guess what the system prompt then is. Got: ${JSON.stringify(claudeAug.slice(0, 500))}`,
|
|
378
|
+
);
|
|
379
|
+
assert.ok(
|
|
380
|
+
cortexAug.includes("carries no system-prompt carrier at all") &&
|
|
381
|
+
!cortexAug.includes("does have a system-prompt carrier"),
|
|
382
|
+
"[QK:CARRIER-FRAME-DENIES-CORTEX-CARRIER] the cortex frame must deny the carrier — cortex's buildSessionMeta returns " +
|
|
383
|
+
`undefined so no _meta is sent; claiming a tiny carrier there would be a lie. Got: ${JSON.stringify(cortexAug.slice(0, 500))}`,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// The rail difference is not a doc claim — it is in the adapter source.
|
|
387
|
+
const adapterSrc = readFileSync(join(REPO_DIR, "pi-extensions/lib/acp/backend-adapter.ts"), "utf8");
|
|
388
|
+
const cortexAt = adapterSrc.indexOf('backend: "cortex"');
|
|
389
|
+
assert.ok(cortexAt > 0, "backend-adapter.ts still declares the cortex adapter");
|
|
390
|
+
assert.match(
|
|
391
|
+
adapterSrc.slice(cortexAt),
|
|
392
|
+
/buildSessionMeta\(\)\s*\{\s*return undefined;/,
|
|
393
|
+
"[QK:CARRIER-RAIL-DIFF-IS-SOURCE-PINNED] cortex's buildSessionMeta must still return undefined — the source fact the " +
|
|
394
|
+
"cortex frame's 'no carrier at all' sentence rests on. If this ever grows a carrier, the frame becomes a lie.",
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// DISJOINT SURFACES: the thing the model is told is its system prompt (the
|
|
398
|
+
// engraving) must not contain the narrative it is told is user text. If the
|
|
399
|
+
// two ever overlapped, the frame would be unfalsifiable prose.
|
|
400
|
+
const shippedCarrier = loadEngraving({ backend: "claude", mcpServerNames: [] });
|
|
401
|
+
assert.ok(shippedCarrier, "shipped carrier is present (fail-loud lever)");
|
|
402
|
+
assert.ok(
|
|
403
|
+
!(shippedCarrier as string).includes(BRIDGE_MARK) && !claudeAug.includes(shippedCarrier as string),
|
|
404
|
+
`the claude carrier and the augment must stay disjoint — the carrier carries the ` +
|
|
405
|
+
`engraving only and the augment carries the narrative, which is precisely what the provenance frame tells the ` +
|
|
406
|
+
`model. Carrier: ${JSON.stringify(shippedCarrier)}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ===========================================================================
|
|
411
|
+
// 11) A-JOIN — the carrier owns its own leading boundary.
|
|
412
|
+
//
|
|
413
|
+
// Measured LIVE 2026-07-31 (0.64.0 adapter, fresh Claude ACP): the system prompt
|
|
414
|
+
// reached the model as
|
|
415
|
+
// `You are a Claude agent, built on Anthropic's Claude Agent SDK.# Engraving Here`
|
|
416
|
+
// A string-form `_meta.systemPrompt` REPLACES the claude_code preset
|
|
417
|
+
// (acp-agent.js), but the SDK still prefixes its own fixed identity sentence and
|
|
418
|
+
// joins the two with NOTHING — the operator's heading was swallowed into the tail
|
|
419
|
+
// of that sentence.
|
|
420
|
+
//
|
|
421
|
+
// The fix cannot live in engraving.md. The render is trimmed (cell 1's
|
|
422
|
+
// determinism guard: operator file whitespace must not drift
|
|
423
|
+
// bridgeConfigSignature), so a leading blank line in the markdown is eaten before
|
|
424
|
+
// it ever reaches the wire. These cells pin the boundary where it can survive —
|
|
425
|
+
// in the LOADER — and pin the two ways it can be lost again: an opt-out turned
|
|
426
|
+
// into a whitespace carrier (cell 2), and a downstream normalize at the meta hop.
|
|
427
|
+
// ===========================================================================
|
|
428
|
+
{
|
|
429
|
+
// The exact sentence measured on the wire. Only its CONCATENATION is our
|
|
430
|
+
// contract — the SDK owns the wording and may change it; what may not change
|
|
431
|
+
// is that our carrier starts a block of its own after whatever precedes it.
|
|
432
|
+
const SDK_FIXED_SENTENCE = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
433
|
+
|
|
434
|
+
const bare = join(tmp, "bare-engraving.md");
|
|
435
|
+
writeFileSync(bare, "# Operator Engraving");
|
|
436
|
+
const padded = join(tmp, "padded-engraving.md");
|
|
437
|
+
writeFileSync(padded, "\n\n\n# Operator Engraving\n\n");
|
|
438
|
+
|
|
439
|
+
const prev = process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
440
|
+
let bareCarrier: string | null;
|
|
441
|
+
let paddedCarrier: string | null;
|
|
442
|
+
try {
|
|
443
|
+
process.env.ENTWURF_ACP_ENGRAVING_PATH = bare;
|
|
444
|
+
bareCarrier = loadEngraving({ backend: "claude", mcpServerNames: [] });
|
|
445
|
+
process.env.ENTWURF_ACP_ENGRAVING_PATH = padded;
|
|
446
|
+
paddedCarrier = loadEngraving({ backend: "claude", mcpServerNames: [] });
|
|
447
|
+
} finally {
|
|
448
|
+
if (prev === undefined) delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
449
|
+
else process.env.ENTWURF_ACP_ENGRAVING_PATH = prev;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const shipped = loadEngraving({ backend: "claude", mcpServerNames: [] });
|
|
453
|
+
assert.ok(shipped, "shipped carrier is present (fail-loud lever)");
|
|
454
|
+
assert.equal(
|
|
455
|
+
loadEngraving({ backend: "claude", mcpServerNames: [] }),
|
|
456
|
+
shipped,
|
|
457
|
+
"the bounded shipped carrier is still a pure function of its inputs (no per-call drift → no per-turn rebuild)",
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Reproduce the SDK join with NO glue of our own — the exact concatenation
|
|
461
|
+
// acp-agent.js performs — and read the result the way the model does. The
|
|
462
|
+
// operator-override row is what proves the boundary is the LOADER's: that file
|
|
463
|
+
// carries no leading whitespace at all.
|
|
464
|
+
for (const [what, carrier] of [
|
|
465
|
+
["shipped default", shipped as string],
|
|
466
|
+
["operator override with no leading whitespace", bareCarrier as string],
|
|
467
|
+
] as const) {
|
|
468
|
+
const joined = `${SDK_FIXED_SENTENCE}${carrier}`;
|
|
469
|
+
const lines = joined.split("\n");
|
|
470
|
+
assert.ok(
|
|
471
|
+
lines[0] === SDK_FIXED_SENTENCE && lines[1] === "" && (lines[2]?.length ?? 0) > 0,
|
|
472
|
+
`[QK:CARRIER-LEADS-ITS-OWN-BLOCK] the ${what} must open its own block after the fixed SDK sentence. The SDK ` +
|
|
473
|
+
"concatenates that sentence with a string `_meta.systemPrompt` and puts NOTHING between them, and the render " +
|
|
474
|
+
"is trimmed, so the boundary has to come from the loader — measured 2026-07-31 as the swallowed heading " +
|
|
475
|
+
`\`…Claude Agent SDK.# Engraving Here\`, which is what taught the model its engraving was part of the SDK's ` +
|
|
476
|
+
`own sentence. Joined: ${JSON.stringify(joined.slice(0, 160))}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// …and the boundary is a loader CONSTANT: whatever the operator's file leads
|
|
481
|
+
// with, the carrier reads the same. Otherwise file whitespace would drift the
|
|
482
|
+
// carrier and, through appendSystemPrompt, the reuse signature.
|
|
483
|
+
assert.equal(
|
|
484
|
+
paddedCarrier,
|
|
485
|
+
bareCarrier,
|
|
486
|
+
"template leading/trailing whitespace never reaches the carrier — the boundary is ours, not the file's",
|
|
487
|
+
);
|
|
488
|
+
assert.equal(bareCarrier, "\n\n# Operator Engraving", "the boundary is exactly one blank line + the trimmed body");
|
|
489
|
+
|
|
490
|
+
// TINY is the billing axis, not a style rule (engraving.ts header): size, not
|
|
491
|
+
// shape, is what reclassifies a Claude OAuth subscription call as metered
|
|
492
|
+
// "extra usage". The boundary costs 2 bytes; the shipped carrier stays a
|
|
493
|
+
// placeholder and rich context keeps riding the first-user augment.
|
|
494
|
+
const CARRIER_BUDGET_BYTES = 512;
|
|
495
|
+
const shippedBytes = Buffer.byteLength(shipped as string, "utf8");
|
|
496
|
+
assert.ok(
|
|
497
|
+
shippedBytes <= CARRIER_BUDGET_BYTES,
|
|
498
|
+
`[QK:CARRIER-STAYS-TINY] the SHIPPED carrier must stay under ${CARRIER_BUDGET_BYTES} bytes — a carrier that grows ` +
|
|
499
|
+
"materially past the SDK-default size routes subscription (OAuth) calls to metered extra usage, which is an " +
|
|
500
|
+
"HTTP 400 for an operator with no metered balance. AGENTS.md, the bridge narrative and tool catalogs ride the " +
|
|
501
|
+
`first-user-message augment, never this carrier. Got ${shippedBytes} bytes: ` +
|
|
502
|
+
`${JSON.stringify((shipped as string).slice(0, 200))}`,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// The LAST hop. backend.ts folds the loadCarrier result into
|
|
506
|
+
// bridgeConfigSignature (`appendSystemPrompt`) and hands the SAME string to
|
|
507
|
+
// buildSessionMeta. A normalize here would strip the boundary back off on the
|
|
508
|
+
// wire while the signature still folded the bounded value: the A-join returns
|
|
509
|
+
// invisibly AND reuse keys on a string that was never sent.
|
|
510
|
+
const metaFromShipped = buildClaudeSessionMeta(
|
|
511
|
+
{
|
|
512
|
+
modelId: "claude-x",
|
|
513
|
+
tools: ["Read"],
|
|
514
|
+
permissionAllow: ["Read(*)"],
|
|
515
|
+
disallowedTools: [],
|
|
516
|
+
settingSources: [],
|
|
517
|
+
strictMcpConfig: false,
|
|
518
|
+
skillPlugins: [],
|
|
519
|
+
},
|
|
520
|
+
shipped as string,
|
|
521
|
+
);
|
|
522
|
+
assert.equal(
|
|
523
|
+
metaFromShipped.systemPrompt,
|
|
524
|
+
shipped,
|
|
525
|
+
"[QK:CARRIER-META-SENDS-CARRIER-VERBATIM] `_meta.systemPrompt` must be the loader's string BYTE-FOR-BYTE. " +
|
|
526
|
+
"backend.ts folds that same string into bridgeConfigSignature's appendSystemPrompt slot, so any normalize at " +
|
|
527
|
+
"this hop desynchronizes the wire from the signature — reuse would key on a carrier the backend never received, " +
|
|
528
|
+
`and a trim in particular re-opens the A-join with every gate above still green. Got ${JSON.stringify(metaFromShipped.systemPrompt)}`,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
// The boundary is signature-relevant, which is what makes the desync above
|
|
532
|
+
// detectable at all: a build that silently dropped it is judged INCOMPATIBLE
|
|
533
|
+
// (fresh ACP session with the corrected carrier), never quietly reused.
|
|
534
|
+
const sigBase = {
|
|
535
|
+
backend: "claude" as const,
|
|
536
|
+
modelId: "claude-x",
|
|
537
|
+
nativeModelId: "claude-x",
|
|
538
|
+
mcpServersHash: "deadbeef",
|
|
539
|
+
settingSources: [],
|
|
540
|
+
strictMcpConfig: true,
|
|
541
|
+
tools: ["Read", "Bash", "Edit", "Write"],
|
|
542
|
+
skillPlugins: [],
|
|
543
|
+
permissionAllow: ["Read(*)"],
|
|
544
|
+
disallowedTools: [],
|
|
545
|
+
};
|
|
546
|
+
assert.notEqual(
|
|
547
|
+
bridgeConfigSignature({ ...sigBase, appendSystemPrompt: shipped as string }),
|
|
548
|
+
bridgeConfigSignature({ ...sigBase, appendSystemPrompt: (shipped as string).trim() }),
|
|
549
|
+
"the boundary is part of the reuse signature — dropping it invalidates a live session instead of passing unnoticed",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
323
553
|
console.log(
|
|
324
554
|
"[check-acp-carrier-augment] ok — engraving carrier: pure/deterministic, sorted mcp interpolation, " +
|
|
325
555
|
"empty/whitespace/missing → null, shipped-default → non-empty v1 lever (preset replaced), carrier absent → no _meta.systemPrompt key, carrier change → " +
|
|
326
556
|
"signature change (stable carrier → stable signature); augment: prepended on `new` only (reuse delta has none), " +
|
|
327
557
|
"wire-only so it never enters contextMessageSignatures, entwurf cwd/AGENTS.md de-dup (present → drop only that " +
|
|
328
|
-
"section, home kept; absent → kept), day-granularity date, 50KB truncation marker, shipped AGENTS budget"
|
|
558
|
+
"section, home kept; absent → kept), day-granularity date, 50KB truncation marker, shipped AGENTS budget; " +
|
|
559
|
+
"provenance: both rails state the augment is first-user-message text and not the system prompt, claude names its " +
|
|
560
|
+
"tiny engraving-only carrier while cortex denies having one (pinned against cortexAdapter.buildSessionMeta → " +
|
|
561
|
+
"undefined), and carrier/augment stay disjoint; A-join: the carrier opens its own block after the SDK's fixed " +
|
|
562
|
+
"sentence, the boundary is a loader constant (not the template's whitespace, which trim eats) and does not " +
|
|
563
|
+
"resurrect an opted-out carrier, the shipped carrier stays under the tiny budget, and the meta hop sends the " +
|
|
564
|
+
"loader string byte-for-byte so the wire and the reuse signature cannot desynchronize",
|
|
329
565
|
);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Deterministic gate for the Cortex (Snowflake Cortex Code) ACP backend — the
|
|
2
|
-
// first non-claude adapter on the rail (docs/acp-backend-rail.md
|
|
2
|
+
// first non-claude adapter on the rail (docs/acp-backend-rail.md, Cortex audit D1–D10).
|
|
3
3
|
// The cortex source lives in one `cortexAdapter` object (backend-adapter.ts) +
|
|
4
4
|
// its curated surface (models.ts) + the dual-HOME overlay (overlay.ts); the
|
|
5
|
-
// 결합 규칙 requires the gate to land WITH it, and
|
|
5
|
+
// 결합 규칙 requires the gate to land WITH it, and the rail's “Shipped adapters” said EXTEND the
|
|
6
6
|
// `check-acp-*` family, so cortex's whole deterministic axis lives here.
|
|
7
7
|
//
|
|
8
8
|
// The contract under test is the CP0-measured one (v1.1.52, 2026-07-29), not
|
|
9
|
-
// PR #40's v1.1.8-era shape — the D-numbers refer to
|
|
9
|
+
// PR #40's v1.1.8-era shape — the D-numbers refer to the current Cortex audit:
|
|
10
10
|
// 1. curated surface is the GLG-decided 4-row set, riding real registry bases;
|
|
11
11
|
// 2. `cortex-` prefix routes to cortexAdapter; prefix-strip recovers the
|
|
12
12
|
// native id; the unprefixed claude ids are never claimed;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// session-scoped dirs with exact rewrite + dead-pid sweep;
|
|
23
23
|
// 6. CORTEX_HOME ambient presence (empty string INCLUDED) refuses the spawn
|
|
24
24
|
// (D3 — upstream consumers disagree about empty);
|
|
25
|
-
// 7. system-prompt-carrier-less augment
|
|
25
|
+
// 7. system-prompt-carrier-less augment — the operator engraving OVERRIDE rides the
|
|
26
26
|
// first-user augment; a carrier backend (claude) never folds it in.
|
|
27
27
|
//
|
|
28
28
|
// [QK:*] labels mark the claims kill-qualified by scripts/mutants/acp-cortex.json
|
|
@@ -329,7 +329,7 @@ const enrichedServers: AcpMcpServer[] = [
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
// ---------------------------------------------------------------------------
|
|
332
|
-
// Layer A.3 — system-prompt-carrier-less augment
|
|
332
|
+
// Layer A.3 — system-prompt-carrier-less augment: the operator engraving override
|
|
333
333
|
// rides the FIRST-USER AUGMENT for cortex; claude (a carrier backend) never
|
|
334
334
|
// folds it in; no override → no engraving injected.
|
|
335
335
|
// ---------------------------------------------------------------------------
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// Drives ensureClaudeConfigOverlay against INJECTED temp realDir/overlayDir
|
|
4
4
|
// (no operator ~/.claude touched), then asserts the overlay shape the ACP
|
|
5
5
|
// child must see:
|
|
6
|
-
// - settings.json: permissions.defaultMode "
|
|
7
|
-
// false, hooks === {}
|
|
6
|
+
// - settings.json: permissions.defaultMode "bypassPermissions" (unattended
|
|
7
|
+
// ACP turns never pause on a prompt), autoMemoryEnabled false, hooks === {}
|
|
8
|
+
// (configured-but-empty → mailbox absence by design);
|
|
8
9
|
// - whitelisted operator entries symlinked to their real path;
|
|
9
10
|
// - projects/sessions are overlay-PRIVATE real dirs, NOT symlinks;
|
|
10
11
|
// - non-whitelist operator entries (CLAUDE.md, settings.local.json, plugins,
|
|
@@ -73,7 +74,16 @@ try {
|
|
|
73
74
|
|
|
74
75
|
// settings.json
|
|
75
76
|
const settings = JSON.parse(readFileSync(join(overlayDir, "settings.json"), "utf8"));
|
|
76
|
-
assert.equal(
|
|
77
|
+
assert.equal(
|
|
78
|
+
settings.permissions?.defaultMode,
|
|
79
|
+
"bypassPermissions",
|
|
80
|
+
"[QK:ACP-OVERLAY-UNATTENDED-BYPASS] the overlay we author must pin permissions.defaultMode to " +
|
|
81
|
+
"'bypassPermissions'. The isolated overlay is the ONLY authority here — CLAUDE_CONFIG_DIR redirects the " +
|
|
82
|
+
"SDK away from the operator's native settings, so an unattended entwurf ACP turn inherits nothing and a " +
|
|
83
|
+
"regression to 'default' suspends that turn on an interactive permission prompt with no operator at the " +
|
|
84
|
+
"keyboard. The callable surface stays bounded by explicit tools/disallowedTools and backend auth, not by " +
|
|
85
|
+
"this mode.",
|
|
86
|
+
);
|
|
77
87
|
assert.equal(settings.autoMemoryEnabled, false, "settings.autoMemoryEnabled must be false");
|
|
78
88
|
assert.ok(
|
|
79
89
|
settings.hooks && typeof settings.hooks === "object" && Object.keys(settings.hooks).length === 0,
|