@junghanacs/entwurf 0.51.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/BASELINE.md +116 -397
- package/CHANGELOG.md +191 -2
- package/CONTRIBUTING.md +4 -4
- package/DELIVERY.md +2 -2
- package/ENTWURF-CLI.json +1 -1
- package/ENTWURF.json +10 -4
- package/README.md +11 -8
- package/VERIFY.md +333 -143
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -1
- package/demo/demo.sh +1 -1
- package/docs/acp-backend-rail.md +248 -34
- package/docs/setup-clean-host.md +75 -80
- package/mcp/entwurf-bridge/dist/ENTWURF.json +10 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +4 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
- package/package.json +14 -14
- package/pi/pi-durable/carrier/harness-setup.js +1 -1
- package/pi/pi-durable/carrier/subagent.js +1 -1
- package/pi/pi-durable/overlay/upstream-pin.json +15 -15
- package/pi/settings.reference.json +0 -1
- package/pi-extensions/acp-provider.ts +12 -6
- package/pi-extensions/lib/acp/acp-client.ts +6 -7
- package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
- package/pi-extensions/lib/acp/backend.ts +261 -32
- package/pi-extensions/lib/acp/config.ts +4 -9
- package/pi-extensions/lib/acp/context.ts +17 -0
- package/pi-extensions/lib/acp/event-mapper.ts +236 -17
- package/pi-extensions/lib/acp/model-ids.js +9 -12
- package/pi-extensions/lib/acp/models.ts +46 -57
- package/pi-extensions/lib/acp/tool-surface.ts +34 -6
- package/pi-extensions/lib/entwurf-control-rpc.ts +5 -1
- package/pi-extensions/lib/fresh-call-composition.ts +37 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
- package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -1
- package/pi-extensions/meta-bridge-pi-durable.ts +1 -1
- package/run.sh +62 -120
- package/scripts/check-acp-carrier-augment.ts +3 -21
- package/scripts/check-acp-prompt-lifecycle.ts +24 -3
- package/scripts/check-acp-provider-surface.ts +17 -26
- package/scripts/check-acp-session-reuse.ts +23 -4
- package/scripts/check-acp-stop-reason.ts +22 -3
- package/scripts/check-acp-stream-hooks.ts +24 -3
- package/scripts/check-acp-usage-accounting.ts +53 -24
- package/scripts/check-copilot-receive-arm.ts +19 -2
- package/scripts/check-entwurf-v2-surface.ts +7 -21
- package/scripts/check-fresh-call-dispatch.ts +9 -3
- package/scripts/check-gate-qualification.ts +6 -6
- package/scripts/check-herdr-fresh-call.ts +1 -0
- package/scripts/check-install-surface.ts +6 -0
- package/scripts/check-pi-durable-send.ts +4 -1
- package/scripts/check-probe-ordering.ts +34 -24
- package/scripts/check-release-gate-outcomes.ts +9 -23
- package/scripts/lib/codex-fresh-source-receipts.ts +12 -6
- package/scripts/lib/probe-cli-target.ts +3 -2
- package/scripts/mutants/acp-augment.json +0 -26
- package/scripts/mutants/acp-cortex.json +10 -183
- package/scripts/mutants/acp-foreground-only.json +11 -0
- package/scripts/mutants/acp-launch-namespace.json +3 -1
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-set-model-readback.json +488 -0
- package/scripts/mutants/agent-guidance.json +2 -2
- package/scripts/mutants/codex-native.json +2 -2
- package/scripts/mutants/control-send-receipt.json +47 -7
- package/scripts/mutants/mux-fresh-call.json +12 -1
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +8 -10
- package/scripts/mutants/release-gate.json +3 -3
- package/scripts/mutants/v2-surface.json +4 -6
- package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
- package/scripts/smoke-acp-cortex-live.ts +4 -408
- package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
- package/scripts/smoke-acp-session-reuse-live.ts +11 -2
- package/scripts/smoke-acp-v2-send-live.ts +1 -1
- package/scripts/smoke-herdr-raw-install-live.sh +4 -4
- package/scripts/check-acp-cortex.ts +0 -710
|
@@ -659,13 +659,24 @@
|
|
|
659
659
|
"claim": "FRESHCOMP-PEERS-TOOL-DIALECT",
|
|
660
660
|
"title": "the corroboration the framing offers goes back to one bare spelling, so four of the five backends are offered a tool name their own session does not expose",
|
|
661
661
|
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
662
|
-
"find": ["\tconst peersTool = FRESH_CALL_PEERS_TOOL[
|
|
662
|
+
"find": ["\tconst peersTool = FRESH_CALL_PEERS_TOOL[dialect];"],
|
|
663
663
|
"replace": ["\tconst peersTool = \"entwurf_peers\";"],
|
|
664
664
|
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
665
665
|
"timeoutSeconds": 120,
|
|
666
666
|
"signature": "[QK:FRESHCOMP-PEERS-TOOL-DIALECT]",
|
|
667
667
|
"signatureSource": "test/fresh-call-composition.contract.test.ts"
|
|
668
668
|
},
|
|
669
|
+
{
|
|
670
|
+
"claim": "FRESHCOMP-ACP-CLAUDE-TOOL-DIALECT",
|
|
671
|
+
"title": "the framing goes back to keying tool names on the backend alone, so a pi sibling opened on an ACP Claude model is told Pi's underscore spelling its Claude tool surface does not expose",
|
|
672
|
+
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
673
|
+
"find": ["\tconst dialect = freshCallToolDialect(params.backend, params.model);"],
|
|
674
|
+
"replace": ["\tconst dialect = params.backend;"],
|
|
675
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
676
|
+
"timeoutSeconds": 120,
|
|
677
|
+
"signature": "[QK:FRESHCOMP-ACP-CLAUDE-TOOL-DIALECT]",
|
|
678
|
+
"signatureSource": "test/fresh-call-composition.contract.test.ts"
|
|
679
|
+
},
|
|
669
680
|
{
|
|
670
681
|
"claim": "FRESHCOMP-RESULT-GOES-TO-CALLER",
|
|
671
682
|
"title": "the first turn stops naming where the result goes, so a sibling finishes its task, prints the answer in its own window, and the caller never learns it is done",
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
|
|
7
7
|
"title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes \u2014 @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
|
|
8
8
|
"subject": "run.sh",
|
|
9
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.5\\.
|
|
10
|
-
"replace": [" grep '^@earendil-works+' | grep -v '@1\\.5\\.
|
|
9
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.5\\.2(_|$)' || true"],
|
|
10
|
+
"replace": [" grep '^@earendil-works+' | grep -v '@1\\.5\\.2' || true"],
|
|
11
11
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
12
12
|
"timeoutSeconds": 120,
|
|
13
13
|
"signature": "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED]",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
|
|
18
18
|
"title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure \u2014 @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
|
|
19
19
|
"subject": "run.sh",
|
|
20
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.5\\.
|
|
21
|
-
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.5\\.
|
|
20
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.5\\.2(_|$)' || true"],
|
|
21
|
+
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.5\\.2(_|$)' || true"],
|
|
22
22
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
23
23
|
"timeoutSeconds": 120,
|
|
24
24
|
"signature": "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE]",
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"title": "one row of the pnpm workspace closure is left on the previous floor while every other surface moved — the shape a version bump actually fails in. Nothing else reads that file, so the stranded row surfaces only at install time, in a different command, far from the change that stranded it",
|
|
8
8
|
"subject": "pnpm-workspace.yaml",
|
|
9
9
|
"find": [
|
|
10
|
-
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0 || 1.2.0 || 1.3.0 || 1.4.0 || 1.5.0'"
|
|
10
|
+
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0 || 1.2.0 || 1.3.0 || 1.4.0 || 1.5.0 || 1.5.2'"
|
|
11
11
|
],
|
|
12
12
|
"replace": [
|
|
13
|
-
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0 || 1.2.0 || 1.3.0 || 1.4.0'"
|
|
13
|
+
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0 || 1.2.0 || 1.3.0 || 1.4.0 || 1.5.0'"
|
|
14
14
|
],
|
|
15
15
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
16
16
|
"timeoutSeconds": 300,
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"claim": "PI-SDK-PRODUCTION-SET",
|
|
22
22
|
"title": "the durable library's production pin falls one release behind the SDK set the pin declares, so the carrier would bind a pi-durable nobody verified it against",
|
|
23
23
|
"subject": "package.json",
|
|
24
|
-
"find": ["\"@earendil-works/pi-durable\": \"1.5.
|
|
25
|
-
"replace": ["\"@earendil-works/pi-durable\": \"1.
|
|
24
|
+
"find": ["\"@earendil-works/pi-durable\": \"1.5.2\","],
|
|
25
|
+
"replace": ["\"@earendil-works/pi-durable\": \"1.5.0\","],
|
|
26
26
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
27
27
|
"timeoutSeconds": 300,
|
|
28
28
|
"signature": "[QK:PI-SDK-PRODUCTION-SET]",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"title": "an @earendil-works package returns as a dev dependency beside its production pin, one name in two dependency classes",
|
|
34
34
|
"subject": "package.json",
|
|
35
35
|
"find": ["\"@biomejs/biome\": \"^2.4.13\","],
|
|
36
|
-
"replace": ["\"@biomejs/biome\": \"^2.4.13\",\n\t\t\"@earendil-works/pi-ai\": \"1.5.
|
|
36
|
+
"replace": ["\"@biomejs/biome\": \"^2.4.13\",\n\t\t\"@earendil-works/pi-ai\": \"1.5.2\","],
|
|
37
37
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
38
38
|
"timeoutSeconds": 300,
|
|
39
39
|
"signature": "[QK:PI-SDK-NO-DUAL-DECLARATION]",
|
|
@@ -41,15 +41,13 @@
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"claim": "PI-FLOOR-PUBLISHED-SCOPE",
|
|
44
|
-
"title": "a published-scope region in an operator doc names a package the frozen published manifest does not record, so the 1.
|
|
44
|
+
"title": "a published-scope region in an operator doc names a package the frozen published manifest does not record, so the 1.5.0 instructions inside it would be held to a pairing no artifact ships — the doc can no longer prove which published package its install steps belong to",
|
|
45
45
|
"subject": "docs/setup-clean-host.md",
|
|
46
46
|
"find": [
|
|
47
|
-
"<!-- entwurf:published @junghanacs/entwurf@0.
|
|
48
|
-
"| Component | Requirement | Needed for |"
|
|
47
|
+
"| pi | optional-by-presence — current 0.52.0: `>=1.5.2 <1.6`; historical 0.51.0: <!-- entwurf:published @junghanacs/entwurf@0.51.0 pi 1.5.0 -->"
|
|
49
48
|
],
|
|
50
49
|
"replace": [
|
|
51
|
-
"<!-- entwurf:published @junghanacs/entwurf@0.
|
|
52
|
-
"| Component | Requirement | Needed for |"
|
|
50
|
+
"| pi | optional-by-presence — current 0.52.0: `>=1.5.2 <1.6`; historical 0.51.0: <!-- entwurf:published @junghanacs/entwurf@0.50.0 pi 1.5.0 -->"
|
|
53
51
|
],
|
|
54
52
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
55
53
|
"timeoutSeconds": 300,
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"claim": "WRAPPER-SKIP-IS-PROTOCOL-EXIT",
|
|
65
|
-
"title": "
|
|
65
|
+
"title": "the matrix smoke wrapper returns 0 on a missing prerequisite before its .ts is reached",
|
|
66
66
|
"subject": "run.sh",
|
|
67
67
|
"find": [
|
|
68
|
-
" echo \"[entwurf:skip] smoke-
|
|
68
|
+
" echo \"[entwurf:skip] smoke-entwurf-v2-matrix-live — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket).\"",
|
|
69
69
|
" return \"$ENTWURF_STEP_SKIP_EXIT\""
|
|
70
70
|
],
|
|
71
71
|
"replace": [
|
|
72
|
-
" echo \"[entwurf:skip] smoke-
|
|
72
|
+
" echo \"[entwurf:skip] smoke-entwurf-v2-matrix-live — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket).\"",
|
|
73
73
|
" return 0"
|
|
74
74
|
],
|
|
75
75
|
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"title": "MCP long description reverts to the false \"socket paths take a per-target lock\" claim",
|
|
23
23
|
"subject": "ENTWURF.json",
|
|
24
24
|
"find": [
|
|
25
|
-
"
|
|
25
|
+
"A control-socket-DOMAIN dispatch takes a target lock; mailbox and native-push rails are lock-free. mode "
|
|
26
26
|
],
|
|
27
27
|
"replace": ["LOCK POLICY: socket paths take a per-target lock; the mailbox path does not. mode "],
|
|
28
28
|
"gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"title": "MCP long description advertises owned-outcome as a selectable dormant verb again and drops the reason a dormant target actually returns",
|
|
82
82
|
"subject": "ENTWURF.json",
|
|
83
83
|
"find": [
|
|
84
|
-
"A dormant socket rejects this delivery verb as dormant-fire-forget-unsupported.
|
|
84
|
+
"A dormant socket rejects this delivery verb as dormant-fire-forget-unsupported. This delivery verb never launches or resumes a process. Only on an explicit operator request, entwurf_resume_call may visibly reopen a proven-dormant Pi, pi-durable or Claude Code citizen under the same id, submitting no prompt; that lifecycle is separate from delivery. "
|
|
85
85
|
],
|
|
86
86
|
"replace": [
|
|
87
87
|
"owned-outcome wakes a DORMANT socket-domain citizen by spawn-bg resume ONLY and is never auto-converted. "
|
|
@@ -132,11 +132,9 @@
|
|
|
132
132
|
"claim": "V2SURF-DESC-FITS-HOST-CAP",
|
|
133
133
|
"title": "the v2 tool description grows back past the host's 2048-char cap — the INTENT contract is silently cut off before the model that must choose an intent ever reads it",
|
|
134
134
|
"subject": "ENTWURF.json",
|
|
135
|
-
"find": [
|
|
136
|
-
"CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id "
|
|
137
|
-
],
|
|
135
|
+
"find": ["Send, reply or hand off to an existing garden id; the decider selects the live rail "],
|
|
138
136
|
"replace": [
|
|
139
|
-
"
|
|
137
|
+
"Send, reply or hand off to an existing garden id; the decider selects the live rail PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING "
|
|
140
138
|
],
|
|
141
139
|
"gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
|
|
142
140
|
"timeoutSeconds": 60,
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
// It asserts nothing and blocks nothing. The receipt goes in README.md next to it.
|
|
19
19
|
|
|
20
20
|
import { type ChildProcessByStdio, spawn } from "node:child_process";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
21
22
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
22
23
|
import { createRequire } from "node:module";
|
|
23
24
|
import { tmpdir } from "node:os";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
25
26
|
import { Readable, Writable } from "node:stream";
|
|
26
27
|
import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
|
27
28
|
import { connectAcpClient } from "../../pi-extensions/lib/acp/acp-client.ts";
|
|
@@ -47,7 +48,12 @@ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const require = createRequire(import.meta.url);
|
|
50
|
-
|
|
51
|
+
// 0.103.0 exports only `.` and `./package.json`; take the bin path from the manifest.
|
|
52
|
+
const acpPkgJson = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
53
|
+
const bin = join(
|
|
54
|
+
dirname(acpPkgJson),
|
|
55
|
+
(JSON.parse(readFileSync(acpPkgJson, "utf8")) as { bin: Record<string, string> }).bin["claude-agent-acp"],
|
|
56
|
+
);
|
|
51
57
|
|
|
52
58
|
const scratch = await mkdtemp(join(tmpdir(), "raw-acp-compaction-"));
|
|
53
59
|
const child: ChildProcessByStdio<Writable, Readable, Readable> = spawn(process.execPath, [bin], {
|
|
@@ -1,408 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
// LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live
|
|
7
|
-
//
|
|
8
|
-
// WHY this exact axis. The CP0 audit (docs/acp-backend-rail.md, Cortex D1–D10) measured
|
|
9
|
-
// three things no offline gate can prove:
|
|
10
|
-
// - D9/D10: cortex ignores the wire `mcpServers` param, so the bundled
|
|
11
|
-
// entwurf-bridge reaches a cortex session ONLY through the overlay-private
|
|
12
|
-
// mcp.json projection — and only the dual-HOME env restore keeps the bridge
|
|
13
|
-
// child's garden store real (an isolated-HOME bridge saw "(none)" citizens).
|
|
14
|
-
// The CP0 LIVE run proved tool exposure + entwurf_peers; the outbound
|
|
15
|
-
// `entwurf_v2` send was DELIBERATELY not exercised there ("흉내내지 않았다")
|
|
16
|
-
// and is owed to THIS smoke.
|
|
17
|
-
// - CP0-M: set-model is accepted live, but "the prompt actually runs on the
|
|
18
|
-
// selected model" needed a model turn — this smoke drives one.
|
|
19
|
-
// - teardown: an MCP-configured cortex child does NOT exit on stdin EOF
|
|
20
|
-
// (measured twice); only process-group signalling reclaims it. This smoke
|
|
21
|
-
// asserts the reclaim actually happens after the resident dies.
|
|
22
|
-
//
|
|
23
|
-
// METHOD — mirrors smoke-acp-v2-send-live: seed an isolated garden world (store
|
|
24
|
-
// + mailbox + receivers) with ONE armed self-fetch receiver, launch a real
|
|
25
|
-
// `pi --entwurf-control --mode rpc` resident on entwurf/cortex-claude-sonnet-5
|
|
26
|
-
// with that world in env, drive one turn telling the model to call
|
|
27
|
-
// `mcp__entwurf-bridge__entwurf_v2` at the seeded receiver with a nonce, then
|
|
28
|
-
// assert ON DISK: the landed .msg, the envelope naming the resident's own gid +
|
|
29
|
-
// entwurf/<cortex model>, the overlay scope dir + its mcp.json projection, and
|
|
30
|
-
// (after teardown) that no process still lives inside this run's overlay.
|
|
31
|
-
//
|
|
32
|
-
// LIVE-only and OUTSIDE the claude-only AGGREGATE release floor (capability
|
|
33
|
-
// dignity): a host that runs no cortex must not redden a claude release. That is
|
|
34
|
-
// a wiring decision — shipping cortex still owes a deliberate run of this smoke,
|
|
35
|
-
// and the aggregate gate's silence is not a pass (ACP rail verification boundary).
|
|
36
|
-
// Honest skip when LIVE!=1, when `cortex` is not on PATH, or when
|
|
37
|
-
// ENTWURF_ACP_CORTEX_CONNECTION is unset — the overlay denies the operator's
|
|
38
|
-
// cortex settings.json (where a default connection would live), so the
|
|
39
|
-
// connection must arrive through the adapter's own env/settings seam, and
|
|
40
|
-
// running without one would measure a config hole, not the rail. This gate
|
|
41
|
-
// touches the real host exactly where LIVE gates may: the operator's real
|
|
42
|
-
// cortex auth (read-only through symlinks) and ~/.pi/agent/cortex-overlays.
|
|
43
|
-
//
|
|
44
|
-
// Model override: ENTWURF_ACP_CORTEX_MODEL (default cortex-claude-sonnet-5).
|
|
45
|
-
|
|
46
|
-
import { type ChildProcess, spawn } from "node:child_process";
|
|
47
|
-
import * as crypto from "node:crypto";
|
|
48
|
-
import { existsSync } from "node:fs";
|
|
49
|
-
import * as fsp from "node:fs/promises";
|
|
50
|
-
import * as os from "node:os";
|
|
51
|
-
import * as path from "node:path";
|
|
52
|
-
import * as readline from "node:readline";
|
|
53
|
-
import { fileURLToPath } from "node:url";
|
|
54
|
-
import {
|
|
55
|
-
upsertMetaSession,
|
|
56
|
-
writeMetaReceiverMarker,
|
|
57
|
-
writeMetaSenderMarker,
|
|
58
|
-
} from "../pi-extensions/lib/meta-session.ts";
|
|
59
|
-
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
60
|
-
import { skipLive } from "./lib/live-skip.ts";
|
|
61
|
-
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
62
|
-
|
|
63
|
-
const ACP_PROVIDER = "entwurf";
|
|
64
|
-
const ACP_MODEL = process.env.ENTWURF_ACP_CORTEX_MODEL?.trim() || "cortex-claude-sonnet-5";
|
|
65
|
-
|
|
66
|
-
const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
67
|
-
const CORTEX_OVERLAYS_ROOT = path.join(os.homedir(), ".pi", "agent", "cortex-overlays");
|
|
68
|
-
const SOCKET_SUFFIX = ".sock";
|
|
69
|
-
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
70
|
-
// Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
|
|
71
|
-
// `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
|
|
72
|
-
// registration needs (scripts/resident-rpc-drive.ts has the measurement).
|
|
73
|
-
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
|
|
74
|
-
|
|
75
|
-
const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
|
|
76
|
-
// The cortex CLI self-extracts on first launch and its newSession alone is
|
|
77
|
-
// ~2.5 s; keep the generous default of the sibling smoke.
|
|
78
|
-
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_CORTEX_TIMEOUT_MS) || 300_000;
|
|
79
|
-
const RECLAIM_TIMEOUT_MS = 20_000;
|
|
80
|
-
const POLL_MS = 100;
|
|
81
|
-
|
|
82
|
-
let passed = 0;
|
|
83
|
-
function ok(label: string, cond: boolean): void {
|
|
84
|
-
if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
|
|
85
|
-
console.log(` ok ${label}`);
|
|
86
|
-
passed++;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function writeFailureArtifact(cap: { stream: string } | null, stderrTail: string, err: unknown): Promise<void> {
|
|
90
|
-
try {
|
|
91
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
92
|
-
const file = path.join(os.tmpdir(), `entwurf-smoke-acp-cortex-live-FAIL-${stamp}.log`);
|
|
93
|
-
await fsp.writeFile(
|
|
94
|
-
file,
|
|
95
|
-
`# smoke-acp-cortex-live FAILURE\n# ${err instanceof Error ? err.message : String(err)}\n` +
|
|
96
|
-
`# model: ${ACP_PROVIDER}/${ACP_MODEL}\n\n` +
|
|
97
|
-
`## event stream\n${cap?.stream ?? "(no turn captured)"}\n\n` +
|
|
98
|
-
`## resident stderr tail\n${stderrTail || "(empty)"}\n`,
|
|
99
|
-
"utf8",
|
|
100
|
-
);
|
|
101
|
-
console.error(`[smoke-acp-cortex-live] FAILURE transcript: ${file}`);
|
|
102
|
-
} catch {
|
|
103
|
-
console.error("[smoke-acp-cortex-live] could not write the failure transcript (reporting the original error)");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function sleep(ms: number): Promise<void> {
|
|
108
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function waitForSocket(sockPath: string, timeoutMs: number): Promise<boolean> {
|
|
112
|
-
const deadline = Date.now() + timeoutMs;
|
|
113
|
-
while (Date.now() < deadline) {
|
|
114
|
-
if (existsSync(sockPath)) return true;
|
|
115
|
-
await sleep(POLL_MS);
|
|
116
|
-
}
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** pids (other than ours) whose environ carries `needle` — the overlay scope
|
|
121
|
-
* path is unique per run, so this finds exactly the cortex child + its
|
|
122
|
-
* grandchildren, never an unrelated operator cortex. Foreign-user /proc
|
|
123
|
-
* entries EACCES-skip (they cannot be our children). */
|
|
124
|
-
async function pidsWithEnvNeedle(needle: string): Promise<number[]> {
|
|
125
|
-
const hits: number[] = [];
|
|
126
|
-
const entries = await fsp.readdir("/proc").catch(() => [] as string[]);
|
|
127
|
-
for (const entry of entries) {
|
|
128
|
-
if (!/^\d+$/.test(entry)) continue;
|
|
129
|
-
const pid = Number(entry);
|
|
130
|
-
if (pid === process.pid) continue;
|
|
131
|
-
try {
|
|
132
|
-
const environ = await fsp.readFile(path.join("/proc", entry, "environ"), "utf8");
|
|
133
|
-
if (environ.includes(needle)) hits.push(pid);
|
|
134
|
-
} catch {
|
|
135
|
-
// gone or not ours — either way not a leak we own
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return hits;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
interface TurnCapture {
|
|
142
|
-
agentStartSeen: boolean;
|
|
143
|
-
agentEndSeen: boolean;
|
|
144
|
-
extensionErrors: Array<{ path: unknown; event: unknown; error: unknown }>;
|
|
145
|
-
promptAccepted: boolean;
|
|
146
|
-
stream: string;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Drive exactly one model turn over the resident's stdin RPC and capture the
|
|
150
|
-
// stdout event stream until `agent_end` (or the hard turn timeout).
|
|
151
|
-
function driveTurn(child: ChildProcess, prompt: string): Promise<TurnCapture> {
|
|
152
|
-
return new Promise((resolve) => {
|
|
153
|
-
const cap: TurnCapture = {
|
|
154
|
-
agentStartSeen: false,
|
|
155
|
-
agentEndSeen: false,
|
|
156
|
-
extensionErrors: [],
|
|
157
|
-
promptAccepted: false,
|
|
158
|
-
stream: "",
|
|
159
|
-
};
|
|
160
|
-
let settled = false;
|
|
161
|
-
const finish = (): void => {
|
|
162
|
-
if (settled) return;
|
|
163
|
-
settled = true;
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
rl.close();
|
|
166
|
-
resolve(cap);
|
|
167
|
-
};
|
|
168
|
-
const timer = setTimeout(finish, TURN_TIMEOUT_MS);
|
|
169
|
-
child.once("exit", finish);
|
|
170
|
-
|
|
171
|
-
const rl = readline.createInterface({ input: child.stdout! });
|
|
172
|
-
rl.on("line", (line: string) => {
|
|
173
|
-
const trimmed = line.trim();
|
|
174
|
-
if (!trimmed) return;
|
|
175
|
-
cap.stream += `${trimmed}\n`;
|
|
176
|
-
let evt: Record<string, unknown>;
|
|
177
|
-
try {
|
|
178
|
-
evt = JSON.parse(trimmed) as Record<string, unknown>;
|
|
179
|
-
} catch {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (evt.type === "agent_start") cap.agentStartSeen = true;
|
|
183
|
-
if (evt.type === "extension_error") {
|
|
184
|
-
cap.extensionErrors.push({ path: evt.extensionPath, event: evt.event, error: evt.error });
|
|
185
|
-
}
|
|
186
|
-
if (evt.type === "response" && evt.command === "prompt") cap.promptAccepted = evt.success === true;
|
|
187
|
-
if (evt.type === "agent_end") {
|
|
188
|
-
cap.agentEndSeen = true;
|
|
189
|
-
finish();
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
child.stdin?.write(`${JSON.stringify({ type: "prompt", message: prompt, id: "cortexv2" })}\n`);
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function main(): Promise<void> {
|
|
198
|
-
if (process.env.LIVE !== "1") {
|
|
199
|
-
skipLive("smoke-acp-cortex-live", "set LIVE=1 to run (spawns a real cortex ACP resident + one model turn).");
|
|
200
|
-
}
|
|
201
|
-
const connection = process.env.ENTWURF_ACP_CORTEX_CONNECTION?.trim();
|
|
202
|
-
if (!connection) {
|
|
203
|
-
skipLive(
|
|
204
|
-
"smoke-acp-cortex-live",
|
|
205
|
-
"set ENTWURF_ACP_CORTEX_CONNECTION=<snowflake connection> (the dual-HOME overlay denies the operator cortex settings.json, so the connection must ride the adapter's own seam).",
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
if ("CORTEX_HOME" in process.env) {
|
|
209
|
-
skipLive("smoke-acp-cortex-live", "unset CORTEX_HOME first (the adapter refuses its presence, CP0 D3).");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// One temp root holding the whole garden this smoke can see: store, mailbox
|
|
213
|
-
// and receiver markers. Only these move — the operator's real cortex auth
|
|
214
|
-
// (via overlay symlinks) and the control dir stay real.
|
|
215
|
-
const world = await fsp.mkdtemp(path.join(os.tmpdir(), "acp-cortex-"));
|
|
216
|
-
const sessionsDir = path.join(world, "sessions");
|
|
217
|
-
const mailboxDir = path.join(world, "mailbox");
|
|
218
|
-
const receiversDir = path.join(world, "receivers");
|
|
219
|
-
const sendersDir = path.join(world, "senders");
|
|
220
|
-
for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
|
|
221
|
-
|
|
222
|
-
const nonce = `CORTEXV2-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
|
|
223
|
-
|
|
224
|
-
console.error(`[smoke-acp-cortex-live] repo: ${REPO_ROOT}`);
|
|
225
|
-
console.error(`[smoke-acp-cortex-live] model: ${ACP_PROVIDER}/${ACP_MODEL} (connection ${connection})`);
|
|
226
|
-
console.error(`[smoke-acp-cortex-live] world: ${world}`);
|
|
227
|
-
|
|
228
|
-
// ── the peer: an armed self-fetch citizen, deliverable by the mailbox rail ──
|
|
229
|
-
const receiver = upsertMetaSession({
|
|
230
|
-
input: { backend: "claude-code", nativeSessionId: `acp-cortex-${process.pid}`, cwd: world },
|
|
231
|
-
dir: sessionsDir,
|
|
232
|
-
});
|
|
233
|
-
const receiverGid = receiver.record.gardenId;
|
|
234
|
-
// The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli` watch
|
|
235
|
-
// owner is deliverable only while ITS OWN sender marker still names the garden it is armed
|
|
236
|
-
// for — that join is what refuses a session which switched away in place. A receiver marker
|
|
237
|
-
// with no sender marker beside it models a state the product now treats as retired, so this
|
|
238
|
-
// smoke would fail on its own fixture instead of on the rail it exists to prove. Same shape
|
|
239
|
-
// as smoke-mux-lifecycle-live / smoke-omp-fresh-live, which already seeded both.
|
|
240
|
-
writeMetaReceiverMarker({
|
|
241
|
-
gardenId: receiverGid,
|
|
242
|
-
backend: "claude-code",
|
|
243
|
-
nativeSessionId: receiver.record.nativeSessionId,
|
|
244
|
-
ownerPid: process.pid,
|
|
245
|
-
armProvenance: "session-start",
|
|
246
|
-
receiversDir,
|
|
247
|
-
});
|
|
248
|
-
writeMetaSenderMarker({
|
|
249
|
-
backend: "claude-code",
|
|
250
|
-
gardenId: receiverGid,
|
|
251
|
-
nativeSessionId: receiver.record.nativeSessionId,
|
|
252
|
-
cwd: world,
|
|
253
|
-
ownerPid: process.pid,
|
|
254
|
-
sendersDir,
|
|
255
|
-
});
|
|
256
|
-
console.error(`[smoke-acp-cortex-live] peer: ${receiverGid} (armed mailbox receiver)`);
|
|
257
|
-
|
|
258
|
-
let senderGid = "";
|
|
259
|
-
let stderrTail = "";
|
|
260
|
-
let resident: ChildProcess | null = null;
|
|
261
|
-
let cap: TurnCapture | null = null;
|
|
262
|
-
let overlayScopeDir = "";
|
|
263
|
-
try {
|
|
264
|
-
resident = spawn(
|
|
265
|
-
"pi",
|
|
266
|
-
[...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", ACP_PROVIDER, "--model", ACP_MODEL, "--mode", "rpc"],
|
|
267
|
-
{
|
|
268
|
-
cwd: world,
|
|
269
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
270
|
-
detached: false,
|
|
271
|
-
env: {
|
|
272
|
-
...process.env,
|
|
273
|
-
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
274
|
-
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
275
|
-
ENTWURF_META_RECEIVERS_DIR: receiversDir,
|
|
276
|
-
ENTWURF_META_SENDERS_DIR: sendersDir,
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
resident.stderr?.on("data", (b: Buffer) => {
|
|
281
|
-
stderrTail = (stderrTail + b.toString()).slice(-4000);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const bornGid = await waitForPiRecord(sessionsDir, BOOT_TIMEOUT_MS);
|
|
285
|
-
ok(`cortex-model resident birthed its own V3 record (${ACP_PROVIDER}/${ACP_MODEL})`, bornGid !== null);
|
|
286
|
-
senderGid = bornGid as string;
|
|
287
|
-
console.error(`[smoke-acp-cortex-live] sender:${senderGid} (never told to the model)`);
|
|
288
|
-
ok("sender and receiver are distinct citizens", senderGid !== receiverGid);
|
|
289
|
-
const sockPath = path.join(REAL_CONTROL_DIR, `${senderGid}${SOCKET_SUFFIX}`);
|
|
290
|
-
ok(
|
|
291
|
-
"resident stood up a control socket keyed on its record gardenId",
|
|
292
|
-
await waitForSocket(sockPath, BOOT_TIMEOUT_MS),
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
// The prompt names the TARGET but never the sender's own gid — that can
|
|
296
|
-
// only reach the mailbox via the real envelope the projection carried.
|
|
297
|
-
const prompt =
|
|
298
|
-
`Call the mcp__entwurf-bridge__entwurf_v2 tool exactly once with these arguments: ` +
|
|
299
|
-
`target="${receiverGid}", intent="fire-and-forget", message="${nonce}". ` +
|
|
300
|
-
`Then reply with ONLY the tool's outcome line.`;
|
|
301
|
-
cap = await driveTurn(resident, prompt);
|
|
302
|
-
|
|
303
|
-
ok("the prompt RPC command was accepted", cap.promptAccepted);
|
|
304
|
-
ok("a real model turn ran over the stdin RPC (agent_start)", cap.agentStartSeen);
|
|
305
|
-
ok("the turn completed cleanly over RPC (agent_end — no hang)", cap.agentEndSeen);
|
|
306
|
-
ok("no extension_error during the turn", cap.extensionErrors.length === 0);
|
|
307
|
-
|
|
308
|
-
// ── the dual-HOME overlay is a disk fact, not an implementation detail ──
|
|
309
|
-
// scope id = `<resident pid>-<hash>` (the resident hosts the ACP spawn).
|
|
310
|
-
const scopeEntries = (await fsp.readdir(CORTEX_OVERLAYS_ROOT).catch(() => [] as string[])).filter((e) =>
|
|
311
|
-
e.startsWith(`${resident?.pid}-`),
|
|
312
|
-
);
|
|
313
|
-
ok(
|
|
314
|
-
`the resident materialized exactly one overlay scope dir (got ${scopeEntries.length})`,
|
|
315
|
-
scopeEntries.length === 1,
|
|
316
|
-
);
|
|
317
|
-
overlayScopeDir = path.join(CORTEX_OVERLAYS_ROOT, scopeEntries[0] as string);
|
|
318
|
-
const ovlCortex = path.join(overlayScopeDir, "home", ".snowflake", "cortex");
|
|
319
|
-
const ovlSettings = JSON.parse(await fsp.readFile(path.join(ovlCortex, "settings.json"), "utf8"));
|
|
320
|
-
ok("the overlay pinned autoUpdate:false (D4)", ovlSettings.autoUpdate === false);
|
|
321
|
-
const ovlMcp = JSON.parse(await fsp.readFile(path.join(ovlCortex, "mcp.json"), "utf8"));
|
|
322
|
-
const bridge = ovlMcp.mcpServers?.["entwurf-bridge"];
|
|
323
|
-
ok("the overlay mcp.json projected the entwurf-bridge entry (D9)", Boolean(bridge));
|
|
324
|
-
ok("the projected bridge entry restored the REAL operator HOME (D10 dual-HOME)", bridge.env?.HOME === os.homedir());
|
|
325
|
-
ok(
|
|
326
|
-
"the projected bridge entry carries the live PI_SESSION_ID envelope",
|
|
327
|
-
typeof bridge.env?.PI_SESSION_ID === "string" && bridge.env.PI_SESSION_ID.length > 0,
|
|
328
|
-
);
|
|
329
|
-
ok(
|
|
330
|
-
`the projected bridge entry carries PI_AGENT_ID=entwurf/${ACP_MODEL}`,
|
|
331
|
-
bridge.env?.PI_AGENT_ID === `${ACP_PROVIDER}/${ACP_MODEL}`,
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
// ── the outbound proof is on disk, not in the transcript ──
|
|
335
|
-
const boxDir = path.join(mailboxDir, receiverGid);
|
|
336
|
-
const entries = await fsp.readdir(boxDir).catch(() => [] as string[]);
|
|
337
|
-
const msgs = entries.filter((f) => f.endsWith(".msg"));
|
|
338
|
-
ok(`the model's entwurf_v2 call enqueued exactly one .msg to the peer (got ${msgs.length})`, msgs.length === 1);
|
|
339
|
-
ok("the doorbell inbox.signal was poked", existsSync(path.join(boxDir, "inbox.signal")));
|
|
340
|
-
|
|
341
|
-
const body = await fsp.readFile(path.join(boxDir, msgs[0] as string), "utf8");
|
|
342
|
-
const SEPARATOR = "────────────────────────────────────────";
|
|
343
|
-
const sepAt = body.indexOf(`\n${SEPARATOR}\n`);
|
|
344
|
-
ok("the delivered body carries the rendered envelope separator", sepAt !== -1);
|
|
345
|
-
const header = body.slice(0, sepAt + 1);
|
|
346
|
-
const payload = body.slice(sepAt + 1 + SEPARATOR.length + 1);
|
|
347
|
-
|
|
348
|
-
const rx = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
349
|
-
ok("the payload is EXACTLY the nonce (no envelope text folded into it)", payload === `${nonce}\n`);
|
|
350
|
-
ok(
|
|
351
|
-
"the landed sender agentId is the cortex ACP model (anchored envelope line)",
|
|
352
|
-
new RegExp(`^ {2}from: {8}${rx(ACP_PROVIDER)}/${rx(ACP_MODEL)} @ \\S`, "m").test(header),
|
|
353
|
-
);
|
|
354
|
-
ok(
|
|
355
|
-
"the landed sender session IS the resident's own garden id, replyable (anchored envelope line)",
|
|
356
|
-
new RegExp(
|
|
357
|
-
`^ {2}session: {5}${rx(senderGid)} \\(replyable — reply via entwurf_v2 to this sessionId, intent=fire-and-forget\\)$`,
|
|
358
|
-
"m",
|
|
359
|
-
).test(header),
|
|
360
|
-
);
|
|
361
|
-
ok("the sender gid appears ONLY in the envelope, never in the payload", !payload.includes(senderGid));
|
|
362
|
-
ok("the landed sender renders as a pi-session, not a meta-session", !header.includes("(meta-session"));
|
|
363
|
-
|
|
364
|
-
// ── process-group reclaim (CP0 teardown finding) ──
|
|
365
|
-
// While the resident lives, its retained cortex child inhabits this run's
|
|
366
|
-
// overlay (SNOWFLAKE_HOME env is unique per scope). An MCP-configured
|
|
367
|
-
// cortex child ignores stdin EOF, so only the resident's process-group
|
|
368
|
-
// teardown can reclaim it — kill the resident and require extinction.
|
|
369
|
-
const liveBefore = await pidsWithEnvNeedle(overlayScopeDir);
|
|
370
|
-
ok(
|
|
371
|
-
`the cortex child (group) is alive inside this run's overlay pre-teardown (got ${liveBefore.length})`,
|
|
372
|
-
liveBefore.length > 0,
|
|
373
|
-
);
|
|
374
|
-
await terminateChild(resident);
|
|
375
|
-
resident = null;
|
|
376
|
-
let leaked: number[] = [];
|
|
377
|
-
const deadline = Date.now() + RECLAIM_TIMEOUT_MS;
|
|
378
|
-
for (;;) {
|
|
379
|
-
leaked = await pidsWithEnvNeedle(overlayScopeDir);
|
|
380
|
-
if (leaked.length === 0 || Date.now() > deadline) break;
|
|
381
|
-
await sleep(250);
|
|
382
|
-
}
|
|
383
|
-
ok(
|
|
384
|
-
`process-group teardown reclaimed every process in this run's overlay (leaked: ${leaked.join(",") || "none"})`,
|
|
385
|
-
leaked.length === 0,
|
|
386
|
-
);
|
|
387
|
-
} catch (err) {
|
|
388
|
-
await writeFailureArtifact(cap, stderrTail, err);
|
|
389
|
-
throw err;
|
|
390
|
-
} finally {
|
|
391
|
-
if (resident) await terminateChild(resident);
|
|
392
|
-
if (overlayScopeDir) await fsp.rm(overlayScopeDir, { recursive: true, force: true }).catch(() => {});
|
|
393
|
-
if (process.env.ENTWURF_KEEP_SMOKE_WORLD === "1") {
|
|
394
|
-
console.error(`[smoke-acp-cortex-live] kept world: ${world}`);
|
|
395
|
-
} else {
|
|
396
|
-
await fsp.rm(world, { recursive: true, force: true }).catch(() => {});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (stderrTail && process.env.ENTWURF_SMOKE_VERBOSE === "1") {
|
|
401
|
-
console.error(`[smoke-acp-cortex-live] resident stderr tail:\n${stderrTail}`);
|
|
402
|
-
}
|
|
403
|
-
console.log(
|
|
404
|
-
`smoke-acp-cortex-live: PASS (${passed} assertions) — a cortex ACP model delivered to a peer as itself and was reclaimed`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
await main();
|
|
1
|
+
// Cortex ACP is no longer a supported Entwurf backend. Keep the old developer
|
|
2
|
+
// entry as a named refusal, never a vendor launch or a successful/skipped probe.
|
|
3
|
+
console.error("[entwurf:cortex-acp-disabled] Cortex ACP is not supported; no live probe was started.");
|
|
4
|
+
process.exitCode = 2;
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
import { type ChildProcessByStdio, execFileSync, spawn } from "node:child_process";
|
|
42
42
|
import { copyFileSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
43
43
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
44
|
+
import { createRequire } from "node:module";
|
|
44
45
|
import { tmpdir } from "node:os";
|
|
45
46
|
import { dirname, join, resolve } from "node:path";
|
|
46
47
|
import { Readable, Writable } from "node:stream";
|
|
@@ -576,7 +577,12 @@ async function main(): Promise<void> {
|
|
|
576
577
|
CLI_TARGET = await resolveProbeCliTarget({
|
|
577
578
|
env: process.env,
|
|
578
579
|
resolveNative: async () => {
|
|
579
|
-
|
|
580
|
+
// claude-agent-acp 0.103.0 narrowed `exports` to `.` and `./package.json`, so
|
|
581
|
+
// a bare deep specifier now fails ERR_PACKAGE_PATH_NOT_EXPORTED. The resolver
|
|
582
|
+
// still ships at `dist/claude-cli.js`; reach it by file URL from the exported
|
|
583
|
+
// package.json, the same anchor the production launcher uses.
|
|
584
|
+
const pkgJson = createRequire(import.meta.url).resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
585
|
+
const mod = (await import(pathToFileURL(join(dirname(pkgJson), "dist", "claude-cli.js")).href)) as {
|
|
580
586
|
claudeCliPath: () => Promise<string>;
|
|
581
587
|
};
|
|
582
588
|
return mod.claudeCliPath();
|
|
@@ -114,8 +114,17 @@ async function main(): Promise<void> {
|
|
|
114
114
|
// pick `process-scoped`, so turn 2 reuses the in-memory child.
|
|
115
115
|
if (!process.argv.includes("--entwurf-control")) process.argv.push("--entwurf-control");
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// #142: the Claude ACP rail maps pi's thinking level through the REGISTERED row's
|
|
118
|
+
// `thinkingLevelMap` and refuses a turn with no level (pi sends none for `off` or an unset
|
|
119
|
+
// level). So this smoke takes the curated row's map and states a level, as an operator's pi
|
|
120
|
+
// does through its registered model and defaultThinkingLevel.
|
|
121
|
+
const models = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/models.js")).href)) as {
|
|
122
|
+
curatedClaudeModels: () => Array<{ id: string; thinkingLevelMap?: unknown }>;
|
|
123
|
+
};
|
|
124
|
+
const row = models.curatedClaudeModels().find((m) => m.id === MODEL);
|
|
125
|
+
if (!row) fail(`model ${MODEL} is not a curated Claude ACP row`);
|
|
126
|
+
const model = { id: MODEL, thinkingLevelMap: row.thinkingLevelMap } as unknown as Model<Api>;
|
|
127
|
+
const options = { cwd: scratch, sessionId: "reuse-smoke", reasoning: "medium" as const };
|
|
119
128
|
|
|
120
129
|
console.error(`[smoke-acp-session-reuse-live] repo: ${REPO_ROOT}`);
|
|
121
130
|
console.error(`[smoke-acp-session-reuse-live] cwd: ${scratch}`);
|
|
@@ -345,7 +345,7 @@ async function main(): Promise<void> {
|
|
|
345
345
|
ok(
|
|
346
346
|
"the landed sender session IS the resident's own garden id, replyable (anchored envelope line)",
|
|
347
347
|
new RegExp(
|
|
348
|
-
`^ {2}session: {5}${rx(senderGid)}
|
|
348
|
+
`^ {2}session: {5}${rx(senderGid)} ${rx("(replyable — when a reply is requested, send the result via entwurf_v2 (intent=fire-and-forget) to the requested recipient, defaulting to this sessionId; a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment)")}$`,
|
|
349
349
|
"m",
|
|
350
350
|
).test(header),
|
|
351
351
|
);
|