@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
|
@@ -25,6 +25,7 @@ import { tmpdir } from "node:os";
|
|
|
25
25
|
import { dirname, resolve } from "node:path";
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
27
|
import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
|
|
28
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
28
29
|
|
|
29
30
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
31
|
const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
|
|
@@ -46,8 +47,7 @@ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
if (process.env.LIVE !== "1") {
|
|
49
|
-
|
|
50
|
-
process.exit(0);
|
|
50
|
+
skipLive("smoke-acp-session-reuse-live", "set LIVE=1 to run the real 2-turn reuse acceptance.");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
type Stream = AsyncIterable<AssistantMessageEvent> & {
|
|
@@ -28,6 +28,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
29
29
|
import { dirname, join, resolve } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
31
32
|
|
|
32
33
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
34
|
const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
|
|
@@ -40,8 +41,7 @@ function fail(msg: string): never {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
if (process.env.LIVE !== "1") {
|
|
43
|
-
|
|
44
|
-
process.exit(0);
|
|
44
|
+
skipLive("smoke-acp-skill-live", "set LIVE=1 to run the real pi provider turn.");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
|
|
@@ -31,6 +31,7 @@ import { fileURLToPath } from "node:url";
|
|
|
31
31
|
import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensions/lib/entwurf-control-rpc.ts";
|
|
32
32
|
import { scanSocketProbes } from "../pi-extensions/lib/socket-discovery.ts";
|
|
33
33
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
34
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
34
35
|
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
35
36
|
|
|
36
37
|
const ACP_PROVIDER = "entwurf";
|
|
@@ -76,10 +77,10 @@ async function waitForGone(sockPath: string, timeoutMs: number): Promise<boolean
|
|
|
76
77
|
|
|
77
78
|
async function main(): Promise<void> {
|
|
78
79
|
if (process.env.LIVE !== "1") {
|
|
79
|
-
|
|
80
|
-
"
|
|
80
|
+
skipLive(
|
|
81
|
+
"smoke-acp-socket-citizen-live",
|
|
82
|
+
"set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket).",
|
|
81
83
|
);
|
|
82
|
-
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
const tmp = os.tmpdir();
|
|
@@ -81,6 +81,7 @@ import * as readline from "node:readline";
|
|
|
81
81
|
import { fileURLToPath } from "node:url";
|
|
82
82
|
import { upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
83
83
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
84
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
84
85
|
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
85
86
|
|
|
86
87
|
const ACP_PROVIDER = "entwurf";
|
|
@@ -196,10 +197,10 @@ function driveTurn(child: ChildProcess, prompt: string): Promise<TurnCapture> {
|
|
|
196
197
|
|
|
197
198
|
async function main(): Promise<void> {
|
|
198
199
|
if (process.env.LIVE !== "1") {
|
|
199
|
-
|
|
200
|
-
"
|
|
200
|
+
skipLive(
|
|
201
|
+
"smoke-acp-v2-send-live",
|
|
202
|
+
"set LIVE=1 to run (spawns a real pi --entwurf-control resident + drives one model turn).",
|
|
201
203
|
);
|
|
202
|
-
return;
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
// One temp root holding the whole garden this smoke can see: store, mailbox and
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* adapter (pgrep/ss/`agentapi get-conversation-metadata`+`send-message`), the register core,
|
|
5
5
|
* and `runEntwurfV2` with production deps — against a REAL, already-running agy conversation,
|
|
6
6
|
* and asserts the rail end to end: probe finds the volatile route, register create/re-attach is
|
|
7
|
-
* idempotent, a fire-and-forget dispatches over native-push (delivered),
|
|
8
|
-
*
|
|
7
|
+
* idempotent, a fire-and-forget dispatches over native-push (delivered), and a bogus conversation
|
|
8
|
+
* is rejected (probe-indeterminate).
|
|
9
9
|
*
|
|
10
10
|
* This automates what the GPT manual L1~L5 pass proved by hand (register create/attach, v2
|
|
11
|
-
* native-push delivered ×2,
|
|
11
|
+
* native-push delivered ×2, mailbox/marker/pi-domain non-intrusion) — same
|
|
12
12
|
* production MCP surface, now a repeatable gate.
|
|
13
13
|
*
|
|
14
14
|
* STORE ISOLATION (페블-approved): only the agy CONVERSATION round-trip is real (that is the
|
|
@@ -54,6 +54,7 @@ import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
|
54
54
|
import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
55
55
|
import { antigravityAdapter } from "../pi-extensions/lib/native-push/adapter.ts";
|
|
56
56
|
import { registerNativeConversation } from "../pi-extensions/lib/native-push/register.ts";
|
|
57
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
57
58
|
|
|
58
59
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
59
60
|
|
|
@@ -79,10 +80,10 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
|
|
|
79
80
|
|
|
80
81
|
async function main(): Promise<void> {
|
|
81
82
|
if (process.env.LIVE !== "1") {
|
|
82
|
-
|
|
83
|
-
"
|
|
83
|
+
skipLive(
|
|
84
|
+
"smoke-agy-native-push-live",
|
|
85
|
+
"set LIVE=1 (+ AGY_CONVERSATION_ID) to run (sends into a real agy conversation).",
|
|
84
86
|
);
|
|
85
|
-
return;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
// ── doctor-static preflight (runs FIRST; a dangling agy config FAILs here — the ③ gate) ──
|
|
@@ -188,20 +189,9 @@ async function main(): Promise<void> {
|
|
|
188
189
|
const post = await antigravityAdapter.probe(conversationId);
|
|
189
190
|
ok("post-send re-probe still alive (delivery target reachable, D7 partial)", post.status === "alive");
|
|
190
191
|
|
|
191
|
-
// ── 5. owned-outcome
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
prodDeps,
|
|
195
|
-
);
|
|
196
|
-
ok(
|
|
197
|
-
"owned-outcome rejected as native-push-no-resume-authority",
|
|
198
|
-
owned.kind === "rejected" && owned.receipt.reason === "native-push-no-resume-authority",
|
|
199
|
-
);
|
|
200
|
-
ok(
|
|
201
|
-
"owned reject stamped observedLiveness (post-probe, non-null)",
|
|
202
|
-
owned.kind === "rejected" && owned.receipt.observedLiveness !== null,
|
|
203
|
-
);
|
|
204
|
-
|
|
192
|
+
// ── 5. (retired) the owned-outcome × native-push refusal cell. The intent was removed
|
|
193
|
+
// from the contract in the visible-first cut, so there is no longer a call to make here:
|
|
194
|
+
// a caller cannot declare it, and the reason it produced left the taxonomy with it. ──
|
|
205
195
|
// ── 6. bogus conversation → rejected native-push-probe-indeterminate ──
|
|
206
196
|
// agy is alive but no LS port serves this fabricated conv id → INDETERMINATE (never coerced
|
|
207
197
|
// to dead). A meta-record is upserted DIRECTLY (register would refuse a non-live probe), so
|
|
@@ -18,11 +18,21 @@ set -euo pipefail
|
|
|
18
18
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
19
19
|
|
|
20
20
|
if [ "${LIVE:-0}" != "1" ]; then
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
. "$REPO/scripts/lib/step-outcome.sh"
|
|
22
|
+
echo "[entwurf:skip] smoke-claude-native-resume-live — set LIVE=1 to run (spawns two real Claude Code native turns)."
|
|
23
|
+
exit "$ENTWURF_STEP_SKIP_EXIT"
|
|
23
24
|
fi
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
# A missing `claude` is a PREREQUISITE, not a defect: this smoke drives the real
|
|
27
|
+
# native binary, so a host without it has nothing to prove wrong. Under the P1
|
|
28
|
+
# outcome protocol that is a SKIP the aggregate reports and `--cut` refuses —
|
|
29
|
+
# reporting it as FAIL would make a host's own toolchain look like a release
|
|
30
|
+
# defect, and the old exit 1 did exactly that once this smoke joined the gate.
|
|
31
|
+
command -v claude >/dev/null || {
|
|
32
|
+
. "$REPO/scripts/lib/step-outcome.sh"
|
|
33
|
+
echo "[entwurf:skip] smoke-claude-native-resume-live — claude not on PATH (install Claude Code and complete its own login flow)."
|
|
34
|
+
exit "$ENTWURF_STEP_SKIP_EXIT"
|
|
35
|
+
}
|
|
26
36
|
command -v node >/dev/null || { echo "FAIL: node not on PATH" >&2; exit 1; }
|
|
27
37
|
command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH" >&2; exit 1; }
|
|
28
38
|
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// P3 — the cross-harness delivery CHAIN, on real authenticated rails.
|
|
2
|
+
//
|
|
3
|
+
// LIVE=1 ./run.sh smoke-entwurf-chain-live
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS. Every v2 delivery proof we had covered ONE hop between TWO
|
|
6
|
+
// citizens on ONE rail: matrix-live dispatches programmatically, v2-send-live has
|
|
7
|
+
// an ACP model send into a mailbox, bundled-mcp-live only reads identity. The
|
|
8
|
+
// claim the project actually makes — that independent harnesses address each
|
|
9
|
+
// other by garden id — is a CHAIN claim, and nothing exercised a chain. A gate
|
|
10
|
+
// that never links three harnesses cannot catch the failure where hop 2 loses the
|
|
11
|
+
// sender identity hop 1 carried, or where a rail change breaks only the seam
|
|
12
|
+
// between two backends.
|
|
13
|
+
//
|
|
14
|
+
// THE CHAIN (each arrow is a real model turn on a real authenticated backend):
|
|
15
|
+
//
|
|
16
|
+
// A native Claude Code --entwurf_v2--> B pi / openai-codex (GPT)
|
|
17
|
+
// B --entwurf_v2--> C pi / entwurf claude-sonnet-5 (ACP)
|
|
18
|
+
// C --entwurf_v2--> D mailbox-backed self-fetch citizen
|
|
19
|
+
//
|
|
20
|
+
// Three harnesses, three rails: A is a native self-fetch citizen born by its own
|
|
21
|
+
// SessionStart hook, B and C are live control-socket citizens, and D is the
|
|
22
|
+
// mailbox terminus. The terminus is what makes the READ-RECEIPT axis observable
|
|
23
|
+
// at all — a control-socket delivery has no receipt to stamp, so a chain that
|
|
24
|
+
// ended at C could not produce one. Draining D through the production inbox
|
|
25
|
+
// reader stamps `lastReadAt`, and that is the receipt.
|
|
26
|
+
//
|
|
27
|
+
// WHAT IS REAL AND WHAT IS SMOKE-OWNED (state this honestly or the evidence is
|
|
28
|
+
// worth nothing):
|
|
29
|
+
// - REAL: A's meta-record and its first arming are minted by the operator's own
|
|
30
|
+
// Claude Code SessionStart hook; all three deliveries are production
|
|
31
|
+
// `entwurf_v2` dispatches chosen by the decider; all three model turns are
|
|
32
|
+
// real authenticated calls; every sender envelope is the real one.
|
|
33
|
+
// - SMOKE-OWNED: the isolated world (store / mailbox / receivers under one temp
|
|
34
|
+
// root) so the operator's live garden is never paged, and D's arming marker.
|
|
35
|
+
// D is a seeded citizen exactly as smoke-acp-v2-send-live seeds its peer.
|
|
36
|
+
// - NOT CLAIMED: this proves the chain LINKS, not that any model chooses the
|
|
37
|
+
// entwurf surface on its own. Each hop is TOLD which tool to call, which is
|
|
38
|
+
// why this is a MUST-tier delivery gate and not a BEHAVIOR-lane signal (the
|
|
39
|
+
// lane note in run.sh draws exactly that line).
|
|
40
|
+
//
|
|
41
|
+
// The nonce is the thread: it is minted here, embedded in A's instruction, and
|
|
42
|
+
// must arrive at D having been re-sent by B and again by C. A nonce at D proves
|
|
43
|
+
// the payload traversed every hop — no hop can fabricate it.
|
|
44
|
+
//
|
|
45
|
+
// Sender identity is proven the same way v2-send-live proves it: no hop is ever
|
|
46
|
+
// told its OWN garden id, so a gid can only appear downstream because the real
|
|
47
|
+
// envelope carried it.
|
|
48
|
+
|
|
49
|
+
import { strict as assert } from "node:assert";
|
|
50
|
+
import { type ChildProcess, spawn, spawnSync } from "node:child_process";
|
|
51
|
+
import crypto from "node:crypto";
|
|
52
|
+
import { existsSync } from "node:fs";
|
|
53
|
+
import fsp from "node:fs/promises";
|
|
54
|
+
import os from "node:os";
|
|
55
|
+
import path from "node:path";
|
|
56
|
+
import { fileURLToPath } from "node:url";
|
|
57
|
+
import { readMetaInbox, upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
58
|
+
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
59
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
60
|
+
|
|
61
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
62
|
+
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
63
|
+
const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
64
|
+
const SOCKET_SUFFIX = ".sock";
|
|
65
|
+
|
|
66
|
+
const GPT_TARGET = process.env.ENTWURF_CHAIN_GPT_TARGET?.trim() || "openai-codex/gpt-5.4";
|
|
67
|
+
const ACP_TARGET = process.env.ENTWURF_CHAIN_ACP_TARGET?.trim() || "entwurf/claude-sonnet-5";
|
|
68
|
+
const BOOT_TIMEOUT_MS = 45_000;
|
|
69
|
+
const CLAUDE_TURN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_CLAUDE_TIMEOUT_MS) || 240_000;
|
|
70
|
+
const CHAIN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_TIMEOUT_MS) || 420_000;
|
|
71
|
+
const POLL_MS = 250;
|
|
72
|
+
|
|
73
|
+
let passed = 0;
|
|
74
|
+
function ok(label: string, cond: boolean): void {
|
|
75
|
+
assert.ok(cond, `smoke-entwurf-chain-live: ${label}`);
|
|
76
|
+
console.log(` ok ${label}`);
|
|
77
|
+
passed++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function splitTarget(t: string): [provider: string, model: string] {
|
|
81
|
+
const i = t.indexOf("/");
|
|
82
|
+
assert.ok(i > 0, `target must be <provider>/<model>, got ${t}`);
|
|
83
|
+
return [t.slice(0, i), t.slice(i + 1)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
87
|
+
|
|
88
|
+
async function readRecords(dir: string): Promise<Array<{ gardenId: string; backend: string }>> {
|
|
89
|
+
const out: Array<{ gardenId: string; backend: string }> = [];
|
|
90
|
+
for (const f of await fsp.readdir(dir).catch(() => [] as string[])) {
|
|
91
|
+
if (!f.endsWith(".meta.json")) continue;
|
|
92
|
+
try {
|
|
93
|
+
const r = JSON.parse(await fsp.readFile(path.join(dir, f), "utf8")) as Record<string, unknown>;
|
|
94
|
+
if (typeof r.gardenId === "string" && typeof r.backend === "string") {
|
|
95
|
+
out.push({ gardenId: r.gardenId, backend: r.backend });
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
/* a half-written record on the next poll is not an error */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Wait for ONE new record of `backend` that is not already in `known`. */
|
|
105
|
+
async function waitForNewRecord(dir: string, backend: string, known: Set<string>, ms: number): Promise<string | null> {
|
|
106
|
+
const until = Date.now() + ms;
|
|
107
|
+
while (Date.now() < until) {
|
|
108
|
+
for (const r of await readRecords(dir)) {
|
|
109
|
+
if (r.backend === backend && !known.has(r.gardenId)) return r.gardenId;
|
|
110
|
+
}
|
|
111
|
+
await sleep(POLL_MS);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitForSocket(p: string, ms: number): Promise<boolean> {
|
|
117
|
+
const until = Date.now() + ms;
|
|
118
|
+
while (Date.now() < until) {
|
|
119
|
+
if (existsSync(p)) return true;
|
|
120
|
+
await sleep(POLL_MS);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
if (process.env.LIVE !== "1") {
|
|
127
|
+
skipLive(
|
|
128
|
+
"smoke-entwurf-chain-live",
|
|
129
|
+
"set LIVE=1 to run (drives three real authenticated model turns across Claude Code, pi GPT and pi ACP).",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (spawnSync("bash", ["-lc", "command -v claude"], { encoding: "utf8" }).status !== 0) {
|
|
133
|
+
skipLive("smoke-entwurf-chain-live", "claude not on PATH — hop 1 is a real native Claude Code turn.");
|
|
134
|
+
}
|
|
135
|
+
const [gptProvider, gptModel] = splitTarget(GPT_TARGET);
|
|
136
|
+
const [acpProvider, acpModel] = splitTarget(ACP_TARGET);
|
|
137
|
+
// pi holds each backend's credentials in its own auth store; a missing one is a
|
|
138
|
+
// PREREQUISITE (the operator has not logged that backend in), never a defect.
|
|
139
|
+
const authPath = path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
140
|
+
let auth: Record<string, unknown> = {};
|
|
141
|
+
try {
|
|
142
|
+
auth = JSON.parse(await fsp.readFile(authPath, "utf8")) as Record<string, unknown>;
|
|
143
|
+
} catch {
|
|
144
|
+
skipLive("smoke-entwurf-chain-live", `no pi auth store at ${authPath} — log in the chain's backends first.`);
|
|
145
|
+
}
|
|
146
|
+
if (!(gptProvider in auth)) {
|
|
147
|
+
skipLive(
|
|
148
|
+
"smoke-entwurf-chain-live",
|
|
149
|
+
`pi has no '${gptProvider}' credentials (hop 2 target ${GPT_TARGET}); log that backend in, or point ` +
|
|
150
|
+
"ENTWURF_CHAIN_GPT_TARGET at one you have.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const world = await fsp.mkdtemp(path.join(os.tmpdir(), "entwurf-chain-"));
|
|
155
|
+
const sessionsDir = path.join(world, "sessions");
|
|
156
|
+
const mailboxDir = path.join(world, "mailbox");
|
|
157
|
+
const receiversDir = path.join(world, "receivers");
|
|
158
|
+
for (const d of [sessionsDir, mailboxDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
|
|
159
|
+
const worldEnv = {
|
|
160
|
+
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
161
|
+
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
162
|
+
ENTWURF_META_RECEIVERS_DIR: receiversDir,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const nonce = `ENTWURF-CHAIN-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
|
|
166
|
+
console.error(`[smoke-entwurf-chain-live] repo: ${REPO_ROOT}`);
|
|
167
|
+
console.error(`[smoke-entwurf-chain-live] world: ${world}`);
|
|
168
|
+
console.error(`[smoke-entwurf-chain-live] hop2: ${GPT_TARGET}`);
|
|
169
|
+
console.error(`[smoke-entwurf-chain-live] hop3: ${ACP_TARGET}`);
|
|
170
|
+
console.error(`[smoke-entwurf-chain-live] nonce: ${nonce}`);
|
|
171
|
+
|
|
172
|
+
// ── D: the mailbox terminus (seeded + armed by this process, like v2-send) ──
|
|
173
|
+
const terminus = upsertMetaSession({
|
|
174
|
+
input: { backend: "claude-code", nativeSessionId: `chain-terminus-${process.pid}`, cwd: world },
|
|
175
|
+
dir: sessionsDir,
|
|
176
|
+
});
|
|
177
|
+
const gidD = terminus.record.gardenId;
|
|
178
|
+
writeMetaReceiverMarker({
|
|
179
|
+
gardenId: gidD,
|
|
180
|
+
backend: "claude-code",
|
|
181
|
+
nativeSessionId: terminus.record.nativeSessionId,
|
|
182
|
+
ownerPid: process.pid,
|
|
183
|
+
armProvenance: "session-start",
|
|
184
|
+
receiversDir,
|
|
185
|
+
});
|
|
186
|
+
console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
|
|
187
|
+
|
|
188
|
+
const children: ChildProcess[] = [];
|
|
189
|
+
let streamB = "";
|
|
190
|
+
let streamC = "";
|
|
191
|
+
try {
|
|
192
|
+
// ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
|
|
193
|
+
const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
|
|
194
|
+
const c = spawn(
|
|
195
|
+
"pi",
|
|
196
|
+
[...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", acpProvider, "--model", acpModel, "--mode", "rpc"],
|
|
197
|
+
{ cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
|
|
198
|
+
);
|
|
199
|
+
children.push(c);
|
|
200
|
+
c.stdout?.on("data", (b: Buffer) => {
|
|
201
|
+
streamC = (streamC + b.toString()).slice(-200_000);
|
|
202
|
+
});
|
|
203
|
+
c.stderr?.on("data", (b: Buffer) => {
|
|
204
|
+
streamC = (streamC + b.toString()).slice(-200_000);
|
|
205
|
+
});
|
|
206
|
+
const gidC = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
|
|
207
|
+
ok(`C birthed its own pi record (${ACP_TARGET})`, gidC !== null);
|
|
208
|
+
known.add(gidC as string);
|
|
209
|
+
ok(
|
|
210
|
+
"C stood up a control socket",
|
|
211
|
+
await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidC}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// ── B: the pi GPT citizen ────────────────────────────────────────────────
|
|
215
|
+
const b = spawn(
|
|
216
|
+
"pi",
|
|
217
|
+
[...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", gptProvider, "--model", gptModel, "--mode", "rpc"],
|
|
218
|
+
{ cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
|
|
219
|
+
);
|
|
220
|
+
children.push(b);
|
|
221
|
+
b.stdout?.on("data", (x: Buffer) => {
|
|
222
|
+
streamB = (streamB + x.toString()).slice(-200_000);
|
|
223
|
+
});
|
|
224
|
+
b.stderr?.on("data", (x: Buffer) => {
|
|
225
|
+
streamB = (streamB + x.toString()).slice(-200_000);
|
|
226
|
+
});
|
|
227
|
+
const gidB = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
|
|
228
|
+
ok(`B birthed its own pi record (${GPT_TARGET})`, gidB !== null);
|
|
229
|
+
known.add(gidB as string);
|
|
230
|
+
ok(
|
|
231
|
+
"B stood up a control socket",
|
|
232
|
+
await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidB}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
|
|
233
|
+
);
|
|
234
|
+
ok("B, C and D are three distinct citizens", new Set([gidB, gidC, gidD]).size === 3);
|
|
235
|
+
|
|
236
|
+
// ── A: one real native Claude Code turn that opens the chain ─────────────
|
|
237
|
+
// The instruction each hop receives is the message the previous hop sends.
|
|
238
|
+
// No hop is told its own garden id — that is what makes the downstream
|
|
239
|
+
// sender assertions meaningful.
|
|
240
|
+
const hop3 = [
|
|
241
|
+
`${nonce} hop3. You are the third hop of a delivery chain.`,
|
|
242
|
+
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidD}, intent=fire-and-forget,`,
|
|
243
|
+
`and message=${nonce} terminus. Then reply with only the tool's outcome line.`,
|
|
244
|
+
].join(" ");
|
|
245
|
+
const hop2 = [
|
|
246
|
+
`${nonce} hop2. You are the second hop of a delivery chain.`,
|
|
247
|
+
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidC}, intent=fire-and-forget,`,
|
|
248
|
+
"and message set to exactly the text between INNER-BEGIN and INNER-END.",
|
|
249
|
+
"Then reply with only the tool's outcome line.",
|
|
250
|
+
"INNER-BEGIN",
|
|
251
|
+
hop3,
|
|
252
|
+
"INNER-END",
|
|
253
|
+
].join("\n");
|
|
254
|
+
const hop1Prompt = [
|
|
255
|
+
"You are the first hop of a delivery chain. Do exactly this and nothing else.",
|
|
256
|
+
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidB}, intent=fire-and-forget,`,
|
|
257
|
+
"and message set to exactly the text between BEGIN and END.",
|
|
258
|
+
"Then reply with only the tool's outcome line.",
|
|
259
|
+
"BEGIN",
|
|
260
|
+
hop2,
|
|
261
|
+
"END",
|
|
262
|
+
].join("\n");
|
|
263
|
+
|
|
264
|
+
const claudeRun = spawnSync("claude", ["-p", "--output-format=json", hop1Prompt], {
|
|
265
|
+
cwd: world,
|
|
266
|
+
encoding: "utf8",
|
|
267
|
+
timeout: CLAUDE_TURN_TIMEOUT_MS,
|
|
268
|
+
env: { ...process.env, ...worldEnv },
|
|
269
|
+
});
|
|
270
|
+
const claudeOut = `${claudeRun.stdout ?? ""}\n${claudeRun.stderr ?? ""}`;
|
|
271
|
+
ok("A's native Claude Code turn ran", claudeRun.status === 0);
|
|
272
|
+
|
|
273
|
+
const gidA = await waitForNewRecord(sessionsDir, "claude-code", new Set([gidD]), BOOT_TIMEOUT_MS);
|
|
274
|
+
ok("A was born a garden citizen by its OWN SessionStart hook (not seeded here)", gidA !== null);
|
|
275
|
+
ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
|
|
276
|
+
console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
|
|
277
|
+
|
|
278
|
+
// ── the chain runs on its own from here; wait for the terminus ───────────
|
|
279
|
+
const boxDir = path.join(mailboxDir, gidD);
|
|
280
|
+
const until = Date.now() + CHAIN_TIMEOUT_MS;
|
|
281
|
+
let msgs: string[] = [];
|
|
282
|
+
while (Date.now() < until) {
|
|
283
|
+
msgs = (await fsp.readdir(boxDir).catch(() => [] as string[])).filter(
|
|
284
|
+
(f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"),
|
|
285
|
+
);
|
|
286
|
+
if (msgs.length > 0) break;
|
|
287
|
+
await sleep(POLL_MS);
|
|
288
|
+
}
|
|
289
|
+
if (msgs.length === 0) {
|
|
290
|
+
console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut.slice(0, 1500)}`);
|
|
291
|
+
console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
|
|
292
|
+
console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
|
|
293
|
+
}
|
|
294
|
+
ok(`the chain reached the mailbox terminus (${msgs.length} .msg)`, msgs.length === 1);
|
|
295
|
+
ok("the terminus doorbell was poked", existsSync(path.join(boxDir, "inbox.signal")));
|
|
296
|
+
|
|
297
|
+
const body = await fsp.readFile(path.join(boxDir, msgs[0] as string), "utf8");
|
|
298
|
+
|
|
299
|
+
// ── hop-by-hop evidence ─────────────────────────────────────────────────
|
|
300
|
+
// hop 1: B saw the payload AND the real sender identity of a native
|
|
301
|
+
// Claude Code citizen. A never learned its own gid, so its presence in B's
|
|
302
|
+
// stream can only come from the envelope entwurf_v2 rendered.
|
|
303
|
+
ok("hop 1 — B received the chain payload", streamB.includes(nonce));
|
|
304
|
+
ok("hop 1 — B saw A's real garden id as the sender", streamB.includes(gidA as string));
|
|
305
|
+
ok("hop 1 — the sender was rendered replyable to B", /replyable/i.test(streamB));
|
|
306
|
+
|
|
307
|
+
// hop 2: C saw the payload and B's identity, so the chain did not collapse
|
|
308
|
+
// into A talking to C directly.
|
|
309
|
+
ok("hop 2 — C received the chain payload", streamC.includes(nonce));
|
|
310
|
+
ok("hop 2 — C saw B's real garden id as the sender", streamC.includes(gidB as string));
|
|
311
|
+
|
|
312
|
+
// hop 3: the terminus body carries C's identity, its model, and replyable.
|
|
313
|
+
ok("hop 3 — the terminus payload is the original nonce", body.includes(nonce));
|
|
314
|
+
ok("hop 3 — the terminus names C as the sender", body.includes(gidC as string));
|
|
315
|
+
ok(`hop 3 — the terminus names C's backend model (${acpModel})`, body.includes(acpModel));
|
|
316
|
+
ok("hop 3 — the terminus sender is replyable", /replyable/i.test(body));
|
|
317
|
+
ok(
|
|
318
|
+
"the terminus was not reached by a shortcut — A and B are not its sender",
|
|
319
|
+
!body.includes(gidA as string) && !body.includes(gidB as string),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// ── the read receipt: the one axis only the mailbox rail can produce ─────
|
|
323
|
+
// The PRODUCTION reader, not a re-implementation: reading is what stamps the
|
|
324
|
+
// receipt, so a hand-rolled drain would prove nothing about the real path.
|
|
325
|
+
const statePath = path.join(mailboxDir, gidD, "state.json");
|
|
326
|
+
const beforeRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
|
|
327
|
+
const drained = readMetaInbox({ gardenId: gidD, sessionsDir, mailboxDir });
|
|
328
|
+
ok(
|
|
329
|
+
"draining the terminus returned the chain payload",
|
|
330
|
+
drained.messages.some((m) => m.body.includes(nonce)),
|
|
331
|
+
);
|
|
332
|
+
ok(
|
|
333
|
+
"the drain stamped a read receipt (lastReadAt)",
|
|
334
|
+
typeof drained.readAt === "string" && drained.readAt.length > 0,
|
|
335
|
+
);
|
|
336
|
+
const afterRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
|
|
337
|
+
ok(
|
|
338
|
+
"the receipt is durable on disk, not just in the return value",
|
|
339
|
+
/lastReadAt/.test(afterRaw) && afterRaw !== beforeRaw,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
|
|
343
|
+
} finally {
|
|
344
|
+
for (const ch of children) await terminateChild(ch).catch(() => undefined);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(
|
|
348
|
+
`[smoke-entwurf-chain-live] ok — ${passed} assertions; three harnesses linked on real authenticated rails`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await main();
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
writeMetaReceiverMarker,
|
|
65
65
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
66
66
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
67
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
67
68
|
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
68
69
|
|
|
69
70
|
// pi's control socket lives at the canonical dir keyed by the RECORD's garden id (#50 C4: the
|
|
@@ -130,8 +131,7 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
|
|
|
130
131
|
|
|
131
132
|
async function main(): Promise<void> {
|
|
132
133
|
if (process.env.LIVE !== "1") {
|
|
133
|
-
|
|
134
|
-
return;
|
|
134
|
+
skipLive("smoke-entwurf-v2-matrix-live", "set LIVE=1 to run (spawns a real pi + opens a real socket).");
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const { provider, model } = resolveTarget();
|
|
@@ -260,7 +260,7 @@ async function main(): Promise<void> {
|
|
|
260
260
|
const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
|
|
261
261
|
ok("C1b real record-less `pi --entwurf-control` stood up a control socket", up);
|
|
262
262
|
|
|
263
|
-
for (const intent of ["fire-and-forget"
|
|
263
|
+
for (const intent of ["fire-and-forget"] as const) {
|
|
264
264
|
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
265
265
|
{ target: c1bGid, intent, message: `matrix-live C1b record-less probe (${intent})` },
|
|
266
266
|
prodDeps(smokeSender(c1bGid, tmp)),
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# alarm), and must log sender-marker evidence so send-side replyable identity
|
|
18
18
|
# is not silently lost.
|
|
19
19
|
#
|
|
20
|
-
# Deterministic + offline (no `claude -p`, no network). Safe for pnpm check /
|
|
20
|
+
# Deterministic + offline (no `claude -p`, no network). Safe for pnpm run check:full /
|
|
21
21
|
# pre-commit. Isolates its store via PI_CODING_AGENT_DIR in a temp dir.
|
|
22
22
|
#
|
|
23
23
|
# RUNTIME DEPS: bash + node (the hook runs under `node --experimental-strip-types`)
|
|
@@ -87,6 +87,10 @@ case "$1${2:+ $2}" in
|
|
|
87
87
|
"mcp get")
|
|
88
88
|
printf '%s\n' "Scope: User config" "Status: ✔ Connected"
|
|
89
89
|
[ "${FAKE_MCP_RC:-0}" != 0 ] && exit "$FAKE_MCP_RC"
|
|
90
|
+
# Give grep -q a scheduling window to consume the match and close the pipe
|
|
91
|
+
# before the long tail starts. Without this, a fast host can buffer the whole
|
|
92
|
+
# tail before grep exits, making the old broken probe spuriously green.
|
|
93
|
+
sleep 0.05
|
|
90
94
|
# Keep writing well past the pipe buffer: a reader that leaves early SIGPIPEs us.
|
|
91
95
|
cat "$FAKE_MCP_TAIL"
|
|
92
96
|
exit $? ;;
|