@junghanacs/entwurf 0.12.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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-mailbox-guard — deterministic gate for the guarded mailbox enqueue
|
|
3
|
+
* (SE-1/SE-2 slice 2d). The IO orchestration that conversational-reply sites use
|
|
4
|
+
* instead of calling enqueueMetaMessage directly: it must NOT enqueue (no .msg, no
|
|
5
|
+
* doorbell poke, no mailbox mutation) when the target is undeliverable, and must
|
|
6
|
+
* enqueue exactly once when it is.
|
|
7
|
+
*
|
|
8
|
+
* Proves (GPT Q5 — both axes):
|
|
9
|
+
* 1. PURE 0-call: with injected facts/readers, an undeliverable target leaves the
|
|
10
|
+
* injected enqueue UNCALLED (dead receiver / direct-inject / absent record);
|
|
11
|
+
* a deliverable target calls it exactly once and carries the result through.
|
|
12
|
+
* 2. TMPDIR SNAPSHOT: with the REAL enqueueMetaMessage against a real mailbox dir,
|
|
13
|
+
* a refused send leaves the mailbox tree byte-for-byte unchanged (file list +
|
|
14
|
+
* content hash, not just mtime), and an accepted send writes exactly one .msg +
|
|
15
|
+
* pokes the signal.
|
|
16
|
+
* 3. fact gathering: recordBacked/wakeMode from the record+capability, ownerAlive/
|
|
17
|
+
* watchArmed from the receiver marker (present→true, absent/dead→false).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { gatherMailboxDeliverabilityFacts, guardedMailboxEnqueue } from "../pi-extensions/lib/entwurf-mailbox-guard.ts";
|
|
27
|
+
import {
|
|
28
|
+
enqueueMetaMessage,
|
|
29
|
+
type MetaIdentity,
|
|
30
|
+
mintMetaIdentity,
|
|
31
|
+
readMetaReceiverMarker,
|
|
32
|
+
serializeMetaIdentity,
|
|
33
|
+
writeMetaReceiverMarker,
|
|
34
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
35
|
+
|
|
36
|
+
let passed = 0;
|
|
37
|
+
function ok(label: string, cond: boolean): void {
|
|
38
|
+
assert.ok(cond, label);
|
|
39
|
+
console.log(` ok ${label}`);
|
|
40
|
+
passed++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
|
+
const GARDEN = "20260614T120000-aaaaaa";
|
|
45
|
+
const selfFetch = () => ({ wakeMode: "self-fetch" }) as never;
|
|
46
|
+
const directInject = () => ({ wakeMode: "direct-inject" }) as never;
|
|
47
|
+
// The record identity and the marker MUST agree on garden/backend/native id — that
|
|
48
|
+
// agreement is what SE-2 2d-3 now requires (a present marker alone is not enough). Built
|
|
49
|
+
// from a fixed garden id (NOT mintMetaIdentity, which would generate a random gardenId that
|
|
50
|
+
// never matches GARDEN — the old presence-only test silently ignored that mismatch).
|
|
51
|
+
const liveIdentity = (): MetaIdentity => ({
|
|
52
|
+
schemaVersion: 2,
|
|
53
|
+
gardenId: GARDEN,
|
|
54
|
+
backend: "claude-code",
|
|
55
|
+
nativeSessionId: "n-a",
|
|
56
|
+
cwd: "/tmp",
|
|
57
|
+
model: null,
|
|
58
|
+
transcriptPath: null,
|
|
59
|
+
parentGardenId: null,
|
|
60
|
+
isEntwurf: false,
|
|
61
|
+
createdAt: "2026-06-14T01:00:00.000Z",
|
|
62
|
+
recordUpdatedAt: "2026-06-14T01:00:00.000Z",
|
|
63
|
+
});
|
|
64
|
+
const liveMarker = () =>
|
|
65
|
+
({
|
|
66
|
+
gardenId: GARDEN,
|
|
67
|
+
backend: "claude-code",
|
|
68
|
+
nativeSessionId: "n-a",
|
|
69
|
+
ownerPid: 1,
|
|
70
|
+
ownerStartKey: "x",
|
|
71
|
+
ownerKind: "claude-code-cli",
|
|
72
|
+
armProvenance: "session-start",
|
|
73
|
+
updatedAt: "t",
|
|
74
|
+
}) as never;
|
|
75
|
+
// Same shape as liveMarker but with a drifted native id → does NOT match the record
|
|
76
|
+
// identity ("n-a") → SE-2 2d-3 treats it as a stale/foreign marker (inactive receiver).
|
|
77
|
+
const driftMarker = () =>
|
|
78
|
+
({
|
|
79
|
+
gardenId: GARDEN,
|
|
80
|
+
backend: "claude-code",
|
|
81
|
+
nativeSessionId: "n-OTHER",
|
|
82
|
+
ownerPid: 1,
|
|
83
|
+
ownerStartKey: "x",
|
|
84
|
+
ownerKind: "claude-code-cli",
|
|
85
|
+
armProvenance: "session-start",
|
|
86
|
+
updatedAt: "t",
|
|
87
|
+
}) as never;
|
|
88
|
+
|
|
89
|
+
// ── PURE 0-call (injected facts/readers) ────────────────────────────────────
|
|
90
|
+
{
|
|
91
|
+
let calls = 0;
|
|
92
|
+
const enqueue = () => {
|
|
93
|
+
calls++;
|
|
94
|
+
return "ENQUEUED";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// deliverable: self-fetch record + live marker → enqueue once.
|
|
98
|
+
const okOut = guardedMailboxEnqueue(
|
|
99
|
+
GARDEN,
|
|
100
|
+
{ readIdentity: liveIdentity, readReceiverMarker: liveMarker, capabilityFor: selfFetch },
|
|
101
|
+
enqueue,
|
|
102
|
+
);
|
|
103
|
+
ok("deliverable → enqueue called exactly once", calls === 1);
|
|
104
|
+
ok("deliverable → outcome carries the enqueue result", okOut.delivered === true && okOut.result === "ENQUEUED");
|
|
105
|
+
|
|
106
|
+
// dead receiver (marker null) → 0-call.
|
|
107
|
+
calls = 0;
|
|
108
|
+
const deadOut = guardedMailboxEnqueue(
|
|
109
|
+
GARDEN,
|
|
110
|
+
{ readIdentity: liveIdentity, readReceiverMarker: () => null, capabilityFor: selfFetch },
|
|
111
|
+
enqueue,
|
|
112
|
+
);
|
|
113
|
+
ok("dead/absent receiver → enqueue NOT called (0-call)", calls === 0);
|
|
114
|
+
ok(
|
|
115
|
+
"dead receiver → delivered:false with a reason",
|
|
116
|
+
deadOut.delivered === false && !deadOut.delivered && deadOut.reason.length > 0,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// direct-inject backend (pi) → 0-call even with a live marker (SE-1).
|
|
120
|
+
calls = 0;
|
|
121
|
+
guardedMailboxEnqueue(
|
|
122
|
+
GARDEN,
|
|
123
|
+
{ readIdentity: liveIdentity, readReceiverMarker: liveMarker, capabilityFor: directInject },
|
|
124
|
+
enqueue,
|
|
125
|
+
);
|
|
126
|
+
ok("direct-inject backend → enqueue NOT called (SE-1, 0-call)", calls === 0);
|
|
127
|
+
|
|
128
|
+
// SE-2 2d-3: a present marker whose identity has DRIFTED (native session id mismatch) is a
|
|
129
|
+
// stale/foreign marker — not THIS receiver — so presence alone does not deliver. 0-call.
|
|
130
|
+
calls = 0;
|
|
131
|
+
const driftOut = guardedMailboxEnqueue(
|
|
132
|
+
GARDEN,
|
|
133
|
+
{
|
|
134
|
+
readIdentity: liveIdentity,
|
|
135
|
+
readReceiverMarker: driftMarker,
|
|
136
|
+
capabilityFor: selfFetch,
|
|
137
|
+
},
|
|
138
|
+
enqueue,
|
|
139
|
+
);
|
|
140
|
+
ok("marker identity drift (native id) → enqueue NOT called (SE-2 2d-3, 0-call)", calls === 0);
|
|
141
|
+
ok("marker identity drift → delivered:false", driftOut.delivered === false);
|
|
142
|
+
|
|
143
|
+
// no backing record → 0-call.
|
|
144
|
+
calls = 0;
|
|
145
|
+
guardedMailboxEnqueue(
|
|
146
|
+
GARDEN,
|
|
147
|
+
{
|
|
148
|
+
readIdentity: () => {
|
|
149
|
+
throw new Error("no record");
|
|
150
|
+
},
|
|
151
|
+
readReceiverMarker: liveMarker,
|
|
152
|
+
capabilityFor: selfFetch,
|
|
153
|
+
},
|
|
154
|
+
enqueue,
|
|
155
|
+
);
|
|
156
|
+
ok("absent record → enqueue NOT called (0-call)", calls === 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── fact gathering ──────────────────────────────────────────────────────────
|
|
160
|
+
{
|
|
161
|
+
const facts = gatherMailboxDeliverabilityFacts(GARDEN, {
|
|
162
|
+
readIdentity: liveIdentity,
|
|
163
|
+
readReceiverMarker: liveMarker,
|
|
164
|
+
capabilityFor: selfFetch,
|
|
165
|
+
});
|
|
166
|
+
ok("facts: recordBacked from a present record", facts.recordBacked === true);
|
|
167
|
+
ok("facts: wakeMode from the capability registry", facts.wakeMode === "self-fetch");
|
|
168
|
+
ok("facts: ownerAlive+watchArmed true from a present marker", facts.ownerAlive === true && facts.watchArmed === true);
|
|
169
|
+
|
|
170
|
+
const absent = gatherMailboxDeliverabilityFacts(GARDEN, {
|
|
171
|
+
readIdentity: () => {
|
|
172
|
+
throw new Error("none");
|
|
173
|
+
},
|
|
174
|
+
readReceiverMarker: () => null,
|
|
175
|
+
capabilityFor: selfFetch,
|
|
176
|
+
});
|
|
177
|
+
ok(
|
|
178
|
+
"facts: absent record → recordBacked false, wakeMode undefined",
|
|
179
|
+
absent.recordBacked === false && absent.wakeMode === undefined,
|
|
180
|
+
);
|
|
181
|
+
ok("facts: absent marker → ownerAlive+watchArmed false", absent.ownerAlive === false && absent.watchArmed === false);
|
|
182
|
+
|
|
183
|
+
// A present-but-drifted marker (native id mismatch) is recordBacked (the record is fine)
|
|
184
|
+
// but the receiver is inactive — the SE-2 2d-3 identity-match gate, not presence alone.
|
|
185
|
+
const drift = gatherMailboxDeliverabilityFacts(GARDEN, {
|
|
186
|
+
readIdentity: liveIdentity,
|
|
187
|
+
readReceiverMarker: driftMarker,
|
|
188
|
+
capabilityFor: selfFetch,
|
|
189
|
+
});
|
|
190
|
+
ok(
|
|
191
|
+
"facts: drifted marker → recordBacked true but ownerAlive+watchArmed false",
|
|
192
|
+
drift.recordBacked === true && drift.ownerAlive === false && drift.watchArmed === false,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── TMPDIR SNAPSHOT (real enqueueMetaMessage) ───────────────────────────────
|
|
197
|
+
function snapshot(dir: string): string {
|
|
198
|
+
const h = createHash("sha256");
|
|
199
|
+
const walk = (d: string) => {
|
|
200
|
+
let entries: string[];
|
|
201
|
+
try {
|
|
202
|
+
entries = readdirSync(d).sort();
|
|
203
|
+
} catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const name of entries) {
|
|
207
|
+
const p = path.join(d, name);
|
|
208
|
+
const st = statSync(p);
|
|
209
|
+
h.update(`${p}\n`);
|
|
210
|
+
if (st.isDirectory()) walk(p);
|
|
211
|
+
else h.update(readFileSync(p));
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
walk(dir);
|
|
215
|
+
return h.digest("hex");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
{
|
|
219
|
+
const TMP = mkdtempSync(path.join(tmpdir(), "psa-mbguard-"));
|
|
220
|
+
const sessionsDir = path.join(TMP, "meta-sessions");
|
|
221
|
+
const mailboxDir = path.join(TMP, "meta-mailbox");
|
|
222
|
+
const receiversDir = path.join(TMP, "meta-receivers");
|
|
223
|
+
for (const d of [sessionsDir, mailboxDir, receiversDir]) mkdirSync(d, { recursive: true });
|
|
224
|
+
|
|
225
|
+
// Seed a real claude-code record (recordBacked + self-fetch), but NO receiver
|
|
226
|
+
// marker → the target is a terminated/absent receiver. A guarded send must refuse.
|
|
227
|
+
const id = mintMetaIdentity({ backend: "claude-code", nativeSessionId: "n-snap", cwd: "/tmp" });
|
|
228
|
+
writeFileSync(path.join(sessionsDir, `${id.gardenId}.meta.json`), serializeMetaIdentity(id));
|
|
229
|
+
|
|
230
|
+
const deps = {
|
|
231
|
+
readIdentity: (g: string) =>
|
|
232
|
+
JSON.parse(readFileSync(path.join(sessionsDir, `${g}.meta.json`), "utf8")) as MetaIdentity,
|
|
233
|
+
readReceiverMarker: (g: string) => readMetaReceiverMarker({ gardenId: g, receiversDir }),
|
|
234
|
+
};
|
|
235
|
+
const realEnqueue = () => enqueueMetaMessage({ gardenId: id.gardenId, body: "SNAP", sessionsDir, mailboxDir });
|
|
236
|
+
|
|
237
|
+
const before = snapshot(mailboxDir);
|
|
238
|
+
const refused = guardedMailboxEnqueue(id.gardenId, deps, realEnqueue);
|
|
239
|
+
const after = snapshot(mailboxDir);
|
|
240
|
+
ok("refused send → delivered:false", refused.delivered === false);
|
|
241
|
+
ok("refused send → mailbox tree byte-identical (no .msg, no signal poke, no state)", before === after);
|
|
242
|
+
|
|
243
|
+
// Now arm a live receiver marker (this process is the live owner) → deliverable.
|
|
244
|
+
writeMetaReceiverMarker({
|
|
245
|
+
gardenId: id.gardenId,
|
|
246
|
+
backend: "claude-code",
|
|
247
|
+
nativeSessionId: "n-snap",
|
|
248
|
+
ownerPid: process.pid,
|
|
249
|
+
armProvenance: "session-start",
|
|
250
|
+
receiversDir,
|
|
251
|
+
});
|
|
252
|
+
const accepted = guardedMailboxEnqueue(id.gardenId, deps, realEnqueue);
|
|
253
|
+
ok("active receiver → delivered:true", accepted.delivered === true);
|
|
254
|
+
const msgs = readdirSync(path.join(mailboxDir, id.gardenId)).filter((f) => f.endsWith(".msg"));
|
|
255
|
+
ok("active receiver → exactly one .msg written", msgs.length === 1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── V2-ONLY WIRING: legacy MCP entwurf_send fallback is gone ───────────────
|
|
259
|
+
// v2 mailbox delivery is covered by check-entwurf-v2-mailbox + decider gates; this
|
|
260
|
+
// file keeps the guard primitive pinned for remaining/future conversational enqueue sites.
|
|
261
|
+
const mcpSrc = readFileSync(path.join(REPO_DIR, "mcp", "entwurf-bridge", "src", "index.ts"), "utf8");
|
|
262
|
+
ok("legacy MCP entwurf_send tool removed", !mcpSrc.includes('server.tool(\n\t"entwurf_send"'));
|
|
263
|
+
|
|
264
|
+
console.log(`\ncheck-entwurf-mailbox-guard: ${passed} checks passed`);
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-peers-surface — deterministic gate for the MCP + pi-native `entwurf_peers`
|
|
3
|
+
* RENDER/PAYLOAD layer (0.11 Stage 0 step 4, slice 4c). Drives the PURE
|
|
4
|
+
* `renderEntwurfPeers` with a fabricated `EntwurfFactsResult` (no IO) and proves
|
|
5
|
+
* the surface contract (GPi + Fable 수렴):
|
|
6
|
+
*
|
|
7
|
+
* - legacy `sessions` is a PROJECTION of the facts (alive pi citizens + alive
|
|
8
|
+
* socket-only), NOT a second scan — dead / indeterminate / unsupported /
|
|
9
|
+
* non-pi never appear in it,
|
|
10
|
+
* - sessions[].socketPath === controlSocketPath(gid, dir) — SSOT, no re-concat
|
|
11
|
+
* drift of the filename↔gardenId correlation authority (동결결정3, Fable a),
|
|
12
|
+
* - count === sessions.length (the legacy projection count, NOT peers.length;
|
|
13
|
+
* Fable d),
|
|
14
|
+
* - three DISTINCT arrays (peers / socketOnly / diagnostics), never merged,
|
|
15
|
+
* - NO verb-routing field anywhere in the JSON (deep key scan) AND no
|
|
16
|
+
* verb-routing WORD in the text render (Fable e①: a "resumable peers" title
|
|
17
|
+
* leaks routing a key scan would miss),
|
|
18
|
+
* - diagnostics appear in BOTH the text and the JSON,
|
|
19
|
+
* - empty sections render "(none)"; an `unsupported` peer is shown (never
|
|
20
|
+
* dropped); socket-only enrich null renders "(not enriched)",
|
|
21
|
+
* - WIRING guard (Fable e②/6): the MCP handler and pi-native tool call
|
|
22
|
+
* listEntwurfFacts + renderEntwurfPeers, and the old getLiveSessions is gone from the bridge.
|
|
23
|
+
*
|
|
24
|
+
* No IO — the facts are fabricated; only the wiring guard reads the bridge source
|
|
25
|
+
* as text (a static assertion, not an execution).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import assert from "node:assert/strict";
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import type { EntwurfFactsResult } from "../pi-extensions/lib/entwurf-fact-provider.ts";
|
|
33
|
+
import type { PeerFact, SocketOnlyFact } from "../pi-extensions/lib/entwurf-facts.ts";
|
|
34
|
+
import { renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.ts";
|
|
35
|
+
import type { FactLiveness } from "../pi-extensions/lib/entwurf-v2-contract.ts";
|
|
36
|
+
import type { MetaBackendV2 } from "../pi-extensions/lib/meta-session.ts";
|
|
37
|
+
import { controlSocketPath } from "../pi-extensions/lib/socket-discovery.ts";
|
|
38
|
+
|
|
39
|
+
let passed = 0;
|
|
40
|
+
function ok(label: string, cond: boolean): void {
|
|
41
|
+
assert.ok(cond, label);
|
|
42
|
+
console.log(` ok ${label}`);
|
|
43
|
+
passed++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DIR = "/fake/entwurf-control";
|
|
47
|
+
const FORBIDDEN = ["sendable", "resumable", "dispatch", "action", "transport", "mailboxDeliverable"];
|
|
48
|
+
|
|
49
|
+
function peer(gardenId: string, backend: MetaBackendV2, liveness: FactLiveness): PeerFact {
|
|
50
|
+
return {
|
|
51
|
+
gardenId,
|
|
52
|
+
backend,
|
|
53
|
+
nativeSessionId: `n-${gardenId}`,
|
|
54
|
+
cwd: "/x",
|
|
55
|
+
model: null,
|
|
56
|
+
parentGardenId: null,
|
|
57
|
+
isEntwurf: false,
|
|
58
|
+
createdAt: "2026-06-11T00:00:00.000Z",
|
|
59
|
+
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
60
|
+
liveness,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function socketOnly(
|
|
65
|
+
gardenId: string,
|
|
66
|
+
liveness: SocketOnlyFact["liveness"],
|
|
67
|
+
over: Partial<SocketOnlyFact> = {},
|
|
68
|
+
): SocketOnlyFact {
|
|
69
|
+
return { kind: "socket-only", gardenId, liveness, cwd: null, model: null, idle: null, infoError: null, ...over };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Recursively collect every object key in a JSON-able value. */
|
|
73
|
+
function allKeys(value: unknown, acc: Set<string> = new Set()): Set<string> {
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
for (const v of value) allKeys(v, acc);
|
|
76
|
+
} else if (value && typeof value === "object") {
|
|
77
|
+
for (const [k, v] of Object.entries(value)) {
|
|
78
|
+
acc.add(k);
|
|
79
|
+
allKeys(v, acc);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return acc;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const GID_PI_ALIVE = "20260611T111111-aaaaaa"; // pi alive → in sessions
|
|
86
|
+
const GID_PI_DEAD = "20260611T222222-bbbbbb"; // pi dead (dormant) → NOT in sessions
|
|
87
|
+
const GID_PI_INDET = "20260611T333333-cccccc"; // pi indeterminate → NOT in sessions
|
|
88
|
+
const GID_CLAUDE = "20260611T444444-dddddd"; // claude unsupported → shown, NOT in sessions
|
|
89
|
+
const GID_SOCK_ALIVE = "20260611T555555-eeeeee"; // socket-only alive → in sessions
|
|
90
|
+
const GID_SOCK_DEAD = "20260611T666666-ffffff"; // socket-only dead → shown, NOT in sessions
|
|
91
|
+
|
|
92
|
+
function main(): void {
|
|
93
|
+
const result: EntwurfFactsResult = {
|
|
94
|
+
facts: {
|
|
95
|
+
peers: [
|
|
96
|
+
peer(GID_PI_ALIVE, "pi", "alive"),
|
|
97
|
+
peer(GID_PI_DEAD, "pi", "dead"),
|
|
98
|
+
peer(GID_PI_INDET, "pi", "indeterminate"),
|
|
99
|
+
peer(GID_CLAUDE, "claude-code", "unsupported"),
|
|
100
|
+
],
|
|
101
|
+
socketOnly: [
|
|
102
|
+
socketOnly(GID_SOCK_ALIVE, "alive", { cwd: "/work/cos", model: "gpt-5.4", idle: false }),
|
|
103
|
+
socketOnly(GID_SOCK_DEAD, "dead", { infoError: "get_info failed" }),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
diagnostics: [{ kind: "socket-symlink-rejected", gardenId: "20260611T999999-999999", message: "symlink rejected" }],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const { text, payload } = renderEntwurfPeers(result, DIR);
|
|
110
|
+
|
|
111
|
+
// ── legacy sessions = projection of facts (alive only) ──────────────────────
|
|
112
|
+
const sessGids = payload.sessions.map((s) => s.sessionId);
|
|
113
|
+
ok("sessions: alive pi citizen included", sessGids.includes(GID_PI_ALIVE));
|
|
114
|
+
ok("sessions: alive socket-only included", sessGids.includes(GID_SOCK_ALIVE));
|
|
115
|
+
ok("sessions: dead pi citizen NOT included", !sessGids.includes(GID_PI_DEAD));
|
|
116
|
+
ok("sessions: indeterminate pi citizen NOT included", !sessGids.includes(GID_PI_INDET));
|
|
117
|
+
ok("sessions: unsupported claude citizen NOT included", !sessGids.includes(GID_CLAUDE));
|
|
118
|
+
ok("sessions: dead socket-only NOT included", !sessGids.includes(GID_SOCK_DEAD));
|
|
119
|
+
ok("sessions: exactly the 2 alive entries", payload.sessions.length === 2);
|
|
120
|
+
|
|
121
|
+
// ── socketPath SSOT (Fable a) ───────────────────────────────────────────────
|
|
122
|
+
ok(
|
|
123
|
+
"sessions: socketPath built via controlSocketPath (SSOT, no drift)",
|
|
124
|
+
payload.sessions.every((s) => s.socketPath === controlSocketPath(s.sessionId, DIR)),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// ── count = legacy projection length, NOT peers.length (Fable d) ────────────
|
|
128
|
+
ok("count === sessions.length (projection, not peers)", payload.count === payload.sessions.length);
|
|
129
|
+
ok("count is not peers.length (would be 4)", payload.count !== payload.peers.length);
|
|
130
|
+
|
|
131
|
+
// ── three distinct arrays, never merged ─────────────────────────────────────
|
|
132
|
+
ok(
|
|
133
|
+
"payload keeps peers / socketOnly / diagnostics as three arrays",
|
|
134
|
+
Array.isArray(payload.peers) && Array.isArray(payload.socketOnly) && Array.isArray(payload.diagnostics),
|
|
135
|
+
);
|
|
136
|
+
ok("peers carry all 4 citizens (unsupported NOT dropped)", payload.peers.length === 4);
|
|
137
|
+
ok(
|
|
138
|
+
"unsupported citizen present in peers",
|
|
139
|
+
payload.peers.some((p) => p.gardenId === GID_CLAUDE && p.liveness === "unsupported"),
|
|
140
|
+
);
|
|
141
|
+
ok("socketOnly carries both (dead NOT dropped)", payload.socketOnly.length === 2);
|
|
142
|
+
|
|
143
|
+
// ── NO verb-routing field in JSON (deep key scan) ───────────────────────────
|
|
144
|
+
const keys = allKeys(payload as unknown);
|
|
145
|
+
for (const f of FORBIDDEN) {
|
|
146
|
+
ok(`JSON has no '${f}' key (facts-only, no verb-routing)`, !keys.has(f));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── NO verb-routing word in the TEXT render (Fable e①) ──────────────────────
|
|
150
|
+
const lowerText = text.toLowerCase();
|
|
151
|
+
for (const f of FORBIDDEN) {
|
|
152
|
+
ok(`text render has no '${f}' word (no routing leak in titles/labels)`, !lowerText.includes(f.toLowerCase()));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── diagnostics visible in BOTH surfaces ────────────────────────────────────
|
|
156
|
+
ok(
|
|
157
|
+
"diagnostic visible in JSON",
|
|
158
|
+
payload.diagnostics.length === 1 && payload.diagnostics[0]?.kind === "socket-symlink-rejected",
|
|
159
|
+
);
|
|
160
|
+
ok("diagnostic visible in text", text.includes("socket-symlink-rejected"));
|
|
161
|
+
|
|
162
|
+
// ── three section titles present + ordering ─────────────────────────────────
|
|
163
|
+
ok("text has the citizens section", text.includes("Garden citizens (meta-record):"));
|
|
164
|
+
ok("text has the socket-only section", text.includes("Socket-only control sockets (no meta-record):"));
|
|
165
|
+
ok("text has the diagnostics section", text.includes("Diagnostics:"));
|
|
166
|
+
|
|
167
|
+
// ── empty sections render "(none)" + enrich label ──────────────────────────
|
|
168
|
+
{
|
|
169
|
+
const empty = renderEntwurfPeers({ facts: { peers: [], socketOnly: [] }, diagnostics: [] }, DIR);
|
|
170
|
+
ok("empty diagnostics render '(none)' (trust signal, not hidden)", empty.text.includes("Diagnostics:\n (none)"));
|
|
171
|
+
ok("empty citizens render '(none)'", empty.text.includes("Garden citizens (meta-record):\n (none)"));
|
|
172
|
+
ok("empty surface → count 0, no sessions", empty.payload.count === 0 && empty.payload.sessions.length === 0);
|
|
173
|
+
}
|
|
174
|
+
ok(
|
|
175
|
+
"socket-only enrich appears in text",
|
|
176
|
+
text.includes(`${GID_SOCK_ALIVE} liveness=alive cwd=/work/cos model=gpt-5.4 idle=no`),
|
|
177
|
+
);
|
|
178
|
+
ok(
|
|
179
|
+
"socket-only infoError appears in text",
|
|
180
|
+
text.includes(
|
|
181
|
+
`${GID_SOCK_DEAD} liveness=dead cwd=(not enriched) model=(not enriched) infoError=get_info failed`,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
ok("unsupported peer line shown in text", text.includes(`${GID_CLAUDE} backend=claude-code liveness=unsupported`));
|
|
185
|
+
|
|
186
|
+
// ── human text is bounded: full payload remains structured, not pasted into content ──
|
|
187
|
+
{
|
|
188
|
+
const manyPeers = Array.from({ length: 40 }, (_, i) =>
|
|
189
|
+
peer(`20260612T0000${String(i).padStart(2, "0")}-aaaaaa`, "claude-code", "unsupported"),
|
|
190
|
+
);
|
|
191
|
+
const bounded = renderEntwurfPeers({ facts: { peers: manyPeers, socketOnly: [] }, diagnostics: [] }, DIR);
|
|
192
|
+
ok("bounded text omits older entries when peer list is large", bounded.text.includes("older entries omitted"));
|
|
193
|
+
ok("bounded text shows latest entries", bounded.text.includes("20260612T000039-aaaaaa"));
|
|
194
|
+
ok("bounded text omits oldest entry", !bounded.text.includes("20260612T000000-aaaaaa backend="));
|
|
195
|
+
ok("bounded payload still carries every peer", bounded.payload.peers.length === 40);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── WIRING guard: bridge handler calls the provider+render, not getLiveSessions ──
|
|
199
|
+
{
|
|
200
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
201
|
+
const bridgeSrc = readFileSync(path.join(here, "..", "mcp", "entwurf-bridge", "src", "index.ts"), "utf8");
|
|
202
|
+
const nativeSrc = readFileSync(path.join(here, "..", "pi-extensions", "entwurf-control.ts"), "utf8");
|
|
203
|
+
ok("wiring: bridge calls listEntwurfFacts(", bridgeSrc.includes("listEntwurfFacts("));
|
|
204
|
+
ok("wiring: bridge calls renderEntwurfPeers(", bridgeSrc.includes("renderEntwurfPeers("));
|
|
205
|
+
ok("wiring: native pi tool calls listEntwurfFacts(", nativeSrc.includes("listEntwurfFacts("));
|
|
206
|
+
ok("wiring: native pi tool calls renderEntwurfPeers(", nativeSrc.includes("renderEntwurfPeers("));
|
|
207
|
+
ok(
|
|
208
|
+
"wiring: native pi tool description no longer claims socket-only discovery",
|
|
209
|
+
!nativeSrc.includes("List live sessions that expose a control socket. Returns sessionIds only"),
|
|
210
|
+
);
|
|
211
|
+
ok(
|
|
212
|
+
"wiring: bridge does not paste full JSON payload into human text",
|
|
213
|
+
!bridgeSrc.includes("JSON.stringify(payload)"),
|
|
214
|
+
);
|
|
215
|
+
ok(
|
|
216
|
+
"wiring: native pi tool does not paste full JSON payload into human text",
|
|
217
|
+
!nativeSrc.includes("JSON.stringify(payload)"),
|
|
218
|
+
);
|
|
219
|
+
// `\bname\s*\(` catches a definition OR a call (tolerating a space before the
|
|
220
|
+
// paren, GPi Q4); a bare prose mention in a removal-note comment (no paren) is
|
|
221
|
+
// allowed — the guard targets the second scan, not the word.
|
|
222
|
+
ok(
|
|
223
|
+
"wiring: no getLiveSessions definition/call in bridge (no second scan)",
|
|
224
|
+
!/\bgetLiveSessions\s*\(/.test(bridgeSrc),
|
|
225
|
+
);
|
|
226
|
+
ok(
|
|
227
|
+
"wiring: no isSocketAlive definition/call in bridge (legacy probe removed)",
|
|
228
|
+
!/\bisSocketAlive\s*\(/.test(bridgeSrc),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`\n[check-entwurf-peers-surface] ${passed} assertions ok`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main();
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-resume-args — deterministic gate for the 5c-3b resume-argv SSOT
|
|
3
|
+
* (`buildResumePiArgs`). It pins the load-bearing A1 difference between the legacy one-shot
|
|
4
|
+
* worker and the v2 spawn-bg RESIDENT citizen so the two launch shapes can never drift:
|
|
5
|
+
*
|
|
6
|
+
* 1. legacy carries `--no-extensions` and NO `--entwurf-control`.
|
|
7
|
+
* 2. v2-control carries `--entwurf-control` and NO `--no-extensions`.
|
|
8
|
+
* 3. BOTH keep the headless prefix `--mode json -p` and run the prompt as the final
|
|
9
|
+
* positional (the prompt-as-turn authority is unchanged in v2 — `-p` is NOT dropped).
|
|
10
|
+
* 4. `explicitExtensionArgs` is preserved verbatim, exactly once, in BOTH variants
|
|
11
|
+
* (load-bearing for a recorded `provider=entwurf` resume; #29 footgun).
|
|
12
|
+
* 5. v2-control includes `plan.launchArgs` (`--approve` / empty) as flags BEFORE the
|
|
13
|
+
* prompt; legacy ignores launchArgs entirely.
|
|
14
|
+
* 6. provider/model identity is laid out identically in both; a null/undefined provider
|
|
15
|
+
* emits NO `--provider` flag; `--model <m>` and `<prompt>` are the last three tokens.
|
|
16
|
+
* 7. no cross-contamination: the legacy-only and v2-only flags never leak into the other.
|
|
17
|
+
*
|
|
18
|
+
* Pure string assembly — no IO, no spawn.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { buildResumePiArgs } from "../pi-extensions/lib/entwurf-resume-args.ts";
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
function ok(label: string, cond: boolean): void {
|
|
26
|
+
assert.ok(cond, label);
|
|
27
|
+
console.log(` ok ${label}`);
|
|
28
|
+
passed++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const GID = "20260613T091000-98363c";
|
|
32
|
+
// Real production shape: getEntwurfExplicitExtensions emits `-e <path>` (entwurf-core.ts),
|
|
33
|
+
// NOT `--extension`. The builder spreads verbatim, but the fixture mirrors production so the
|
|
34
|
+
// "preserved exactly once" assertion exercises the token the launcher actually passes.
|
|
35
|
+
const EXT = ["-e", "/path/to/entwurf/index.ts"] as const;
|
|
36
|
+
|
|
37
|
+
// index of token `flag`'s VALUE (the token right after it), or -1 if the flag is absent.
|
|
38
|
+
function valueAfter(args: readonly string[], flag: string): string | undefined {
|
|
39
|
+
const i = args.indexOf(flag);
|
|
40
|
+
return i === -1 ? undefined : args[i + 1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function main(): void {
|
|
44
|
+
// ── legacy variant ──────────────────────────────────────────────────────────────
|
|
45
|
+
{
|
|
46
|
+
const args = buildResumePiArgs({
|
|
47
|
+
variant: "legacy",
|
|
48
|
+
sessionId: GID,
|
|
49
|
+
explicitExtensionArgs: EXT,
|
|
50
|
+
provider: "entwurf",
|
|
51
|
+
model: "claude-opus-4-8",
|
|
52
|
+
prompt: "continue the task",
|
|
53
|
+
});
|
|
54
|
+
ok("1 legacy has --no-extensions", args.includes("--no-extensions"));
|
|
55
|
+
ok("1 legacy has NO --entwurf-control", !args.includes("--entwurf-control"));
|
|
56
|
+
ok("3 legacy headless prefix --mode json -p", args[0] === "--mode" && args[1] === "json" && args[2] === "-p");
|
|
57
|
+
ok("3 legacy prompt is the final positional", args[args.length - 1] === "continue the task");
|
|
58
|
+
ok("4 legacy keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
|
|
59
|
+
ok("6 legacy provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
60
|
+
ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-4-8");
|
|
61
|
+
ok("6 legacy session-id laid out", valueAfter(args, "--session-id") === GID);
|
|
62
|
+
// model + prompt are the last three tokens: --model <m> <prompt>
|
|
63
|
+
ok(
|
|
64
|
+
"6 legacy --model <m> <prompt> tail",
|
|
65
|
+
args[args.length - 3] === "--model" &&
|
|
66
|
+
args[args.length - 2] === "claude-opus-4-8" &&
|
|
67
|
+
args[args.length - 1] === "continue the task",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── v2-control variant ──────────────────────────────────────────────────────────
|
|
72
|
+
{
|
|
73
|
+
const args = buildResumePiArgs({
|
|
74
|
+
variant: "v2-control",
|
|
75
|
+
sessionId: GID,
|
|
76
|
+
explicitExtensionArgs: EXT,
|
|
77
|
+
provider: "entwurf",
|
|
78
|
+
model: "claude-opus-4-8",
|
|
79
|
+
prompt: "resume now",
|
|
80
|
+
launchArgs: ["--approve"],
|
|
81
|
+
});
|
|
82
|
+
ok("2 v2 has --entwurf-control", args.includes("--entwurf-control"));
|
|
83
|
+
ok("2 v2 has NO --no-extensions", !args.includes("--no-extensions"));
|
|
84
|
+
ok("3 v2 keeps -p (prompt-as-turn NOT dropped)", args.includes("-p"));
|
|
85
|
+
ok("3 v2 headless prefix --mode json -p", args[0] === "--mode" && args[1] === "json" && args[2] === "-p");
|
|
86
|
+
ok("3 v2 prompt is the final positional", args[args.length - 1] === "resume now");
|
|
87
|
+
ok("4 v2 keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
|
|
88
|
+
ok("5 v2 includes launchArgs --approve", args.includes("--approve"));
|
|
89
|
+
ok("5 v2 --approve is before the prompt", args.indexOf("--approve") < args.length - 1);
|
|
90
|
+
ok("6 v2 provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
91
|
+
ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-4-8");
|
|
92
|
+
ok("6 v2 session-id laid out", valueAfter(args, "--session-id") === GID);
|
|
93
|
+
ok(
|
|
94
|
+
"6 v2 --model <m> <prompt> tail",
|
|
95
|
+
args[args.length - 3] === "--model" &&
|
|
96
|
+
args[args.length - 2] === "claude-opus-4-8" &&
|
|
97
|
+
args[args.length - 1] === "resume now",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 5. legacy IGNORES launchArgs (no --approve leaks in) ──────────────────────────
|
|
102
|
+
{
|
|
103
|
+
const args = buildResumePiArgs({
|
|
104
|
+
variant: "legacy",
|
|
105
|
+
sessionId: GID,
|
|
106
|
+
explicitExtensionArgs: [],
|
|
107
|
+
provider: null,
|
|
108
|
+
model: "m",
|
|
109
|
+
prompt: "p",
|
|
110
|
+
launchArgs: ["--approve"], // present in input but legacy must ignore it
|
|
111
|
+
});
|
|
112
|
+
ok("5 legacy ignores launchArgs (no --approve)", !args.includes("--approve"));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 6. null/undefined provider emits NO --provider flag ───────────────────────────
|
|
116
|
+
for (const provider of [null, undefined] as const) {
|
|
117
|
+
const args = buildResumePiArgs({
|
|
118
|
+
variant: "v2-control",
|
|
119
|
+
sessionId: GID,
|
|
120
|
+
explicitExtensionArgs: [],
|
|
121
|
+
provider,
|
|
122
|
+
model: "m",
|
|
123
|
+
prompt: "p",
|
|
124
|
+
launchArgs: [],
|
|
125
|
+
});
|
|
126
|
+
ok(`6 provider=${provider}: no --provider flag`, !args.includes("--provider"));
|
|
127
|
+
ok(`6 provider=${provider}: --model <m> <prompt> still tail`, args.slice(-3).join(" ") === "--model m p");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 7. no cross-contamination across variants over the same identity ──────────────
|
|
131
|
+
{
|
|
132
|
+
const base = {
|
|
133
|
+
sessionId: GID,
|
|
134
|
+
explicitExtensionArgs: EXT,
|
|
135
|
+
provider: "entwurf",
|
|
136
|
+
model: "m",
|
|
137
|
+
prompt: "p",
|
|
138
|
+
launchArgs: ["--approve"],
|
|
139
|
+
} as const;
|
|
140
|
+
const legacy = buildResumePiArgs({ ...base, variant: "legacy" });
|
|
141
|
+
const v2 = buildResumePiArgs({ ...base, variant: "v2-control" });
|
|
142
|
+
ok("7 legacy-only flag absent from v2", !v2.includes("--no-extensions"));
|
|
143
|
+
ok("7 v2-only flag absent from legacy", !legacy.includes("--entwurf-control") && !legacy.includes("--approve"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main();
|