@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
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* check-entwurf-resume-args — deterministic gate for the
|
|
3
|
-
* (`buildResumePiArgs`). It pins the RESIDENT citizen launch shape so it cannot drift.
|
|
4
|
-
* This gate once carried a second `legacy` one-shot variant as a contrast; that launcher
|
|
5
|
-
* and its enum member were removed 2026-07-27, and the half of A1 that still ships is
|
|
6
|
-
* asserted directly instead — a resumed citizen stands its control socket up and never
|
|
7
|
-
* emits the one-shot `--no-extensions`:
|
|
2
|
+
* check-entwurf-resume-args — deterministic gate for the resume-argv SSOT (`buildResumePiArgs`).
|
|
8
3
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* The contract this gate holds changed with S1, and the change is the point: the builder used to
|
|
5
|
+
* emit a HEADLESS child (`--mode json -p … <prompt>`) because its only caller was the detached
|
|
6
|
+
* `spawn-bg` watcher. That transport was withdrawn, and the argv was MEASURED against the runtime
|
|
7
|
+
* before the new consumer was written (2026-08-06, private tmux + fixture): dropped into a tmux
|
|
8
|
+
* window, `-p` is pi's own "non-interactive mode" and the operator gets a JSON stream instead of a
|
|
9
|
+
* session they can type into. The shape that actually reopens a visible citizen carries neither.
|
|
10
|
+
*
|
|
11
|
+
* So this gate pins the ONE shipped posture, and pins the absences as hard as the presences —
|
|
12
|
+
* a re-introduced `-p` would still open a window, still stand a socket up, and still look green
|
|
13
|
+
* to anything that only asserted `--entwurf-control`:
|
|
14
|
+
*
|
|
15
|
+
* RESUMEARGS-CONTROL-FIRST `--entwurf-control` leads, so the resumed session is addressable
|
|
16
|
+
* RESUMEARGS-NO-HEADLESS no `--mode`, no `-p` — the window is interactive
|
|
17
|
+
* RESUMEARGS-NO-PROMPT-TAIL no positional prompt — a resume runs no turn
|
|
18
|
+
* RESUMEARGS-EXT-ONCE-BEFORE-SESSION bridge args exactly once, between control and --session
|
|
19
|
+
* RESUMEARGS-PROVIDER-OPTIONAL a recorded provider is emitted, an absent one emits no flag
|
|
20
|
+
* RESUMEARGS-SESSION-IS-FILE `--session <abs path>`, never `--session-id` (which MINTS)
|
|
18
21
|
*
|
|
19
22
|
* Pure string assembly — no IO, no spawn.
|
|
20
23
|
*/
|
|
@@ -29,89 +32,88 @@ function ok(label: string, cond: boolean): void {
|
|
|
29
32
|
passed++;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
// The resume target is a session FILE
|
|
33
|
-
//
|
|
34
|
-
// is the production shape (record.transcriptPath).
|
|
35
|
+
// The resume target is a session FILE (#50 C2), not a garden id: the record owns the address, so
|
|
36
|
+
// argv only has to name WHICH transcript to reopen.
|
|
35
37
|
const SESSION_FILE =
|
|
36
38
|
"/home/op/.pi/agent/sessions/-home-op-repo/2026-06-13T09-10-00-000Z_019e8faa-04ea-7b73-bf2c-1465d525c2e8.jsonl";
|
|
37
|
-
//
|
|
38
|
-
// NOT `--extension`. The builder spreads verbatim, but the fixture mirrors production so the
|
|
39
|
-
// "preserved exactly once" assertion exercises the token the launcher actually passes.
|
|
39
|
+
// Production shape, measured: getEntwurfExplicitExtensions emits `-e <path>`, not `--extension`.
|
|
40
40
|
const EXT = ["-e", "/path/to/entwurf/index.ts"] as const;
|
|
41
41
|
|
|
42
|
-
// index of token `flag`'s VALUE (the token right after it), or -1 if the flag is absent.
|
|
43
42
|
function valueAfter(args: readonly string[], flag: string): string | undefined {
|
|
44
43
|
const i = args.indexOf(flag);
|
|
45
44
|
return i === -1 ? undefined : args[i + 1];
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
function main(): void {
|
|
49
|
-
// ──
|
|
48
|
+
// ── The shipped posture, with a recorded ACP provider (the only shape whose ext args are
|
|
49
|
+
// non-empty — measured as `["-e", "<bridge>"]` on a real recorded provider=entwurf record).
|
|
50
50
|
{
|
|
51
51
|
const args = buildResumePiArgs({
|
|
52
|
-
variant: "v2-control",
|
|
53
52
|
sessionFile: SESSION_FILE,
|
|
54
53
|
explicitExtensionArgs: EXT,
|
|
55
54
|
provider: "entwurf",
|
|
56
|
-
model: "claude-
|
|
57
|
-
prompt: "resume now",
|
|
58
|
-
launchArgs: ["--approve"],
|
|
55
|
+
model: "claude-sonnet-5",
|
|
59
56
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
ok(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ok(
|
|
70
|
-
|
|
57
|
+
|
|
58
|
+
// Assertion order is a contract, not a reading preference: each claim must be the FIRST
|
|
59
|
+
// one a defect against it reaches, or a red would be reported under someone else's name.
|
|
60
|
+
// The absences come first because a re-introduced headless prefix also displaces the
|
|
61
|
+
// leading flag, and "the argv is headless again" is the truer diagnosis of that.
|
|
62
|
+
ok(
|
|
63
|
+
"[QK:RESUMEARGS-NO-HEADLESS] no --mode and no -p — `-p` is pi's non-interactive mode, so the headless prefix would open a window streaming JSON instead of a session the operator can use",
|
|
64
|
+
!args.includes("--mode") && !args.includes("-p") && !args.includes("--print"),
|
|
65
|
+
);
|
|
66
|
+
ok(
|
|
67
|
+
"[QK:RESUMEARGS-CONTROL-FIRST] --entwurf-control is the FIRST token — the resumed session stands its control socket up, which is the whole difference between a restored transcript and an addressable citizen",
|
|
68
|
+
args[0] === "--entwurf-control",
|
|
69
|
+
);
|
|
71
70
|
ok(
|
|
72
|
-
"
|
|
73
|
-
|
|
71
|
+
"[QK:RESUMEARGS-NO-PROMPT-TAIL] the argv ends at --model <m> with NO positional prompt — a resume reopens a conversation and runs no turn (measured: the transcript stayed byte-identical)",
|
|
72
|
+
args[args.length - 2] === "--model" && args[args.length - 1] === "claude-sonnet-5",
|
|
74
73
|
);
|
|
75
74
|
ok(
|
|
76
|
-
"
|
|
77
|
-
args
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
"no --no-extensions either: the one-shot posture that let `pi -p` exit is exactly what a resident resume must not emit",
|
|
76
|
+
!args.includes("--no-extensions"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// SESSION-IS-FILE precedes the ext-args claim because the ext claim is stated RELATIVE to
|
|
80
|
+
// `--session`: if that flag were renamed, the positional assertion below would go red about
|
|
81
|
+
// the extensions while the actual defect is the flag that MINTS a session.
|
|
82
|
+
ok(
|
|
83
|
+
"[QK:RESUMEARGS-SESSION-IS-FILE] the builder targets an exact FILE via --session and never --session-id, which would CREATE the id when missing and silently mint an empty session instead of resuming one",
|
|
84
|
+
valueAfter(args, "--session") === SESSION_FILE && !args.includes("--session-id"),
|
|
85
|
+
);
|
|
86
|
+
ok(
|
|
87
|
+
"[QK:RESUMEARGS-EXT-ONCE-BEFORE-SESSION] the bridge args appear EXACTLY once and sit between --entwurf-control and --session — dropping them re-opens the #29 'Unknown provider' footgun, and doubling them is a different broken argv",
|
|
88
|
+
args.filter((a) => a === "-e").length === 1 &&
|
|
89
|
+
args.indexOf("--entwurf-control") < args.indexOf("-e") &&
|
|
90
|
+
args.indexOf("-e") < args.indexOf("--session"),
|
|
91
|
+
);
|
|
92
|
+
ok("a recorded provider is laid out as two tokens", valueAfter(args, "--provider") === "entwurf");
|
|
93
|
+
ok("model is laid out as two tokens", valueAfter(args, "--model") === "claude-sonnet-5");
|
|
94
|
+
ok(
|
|
95
|
+
"the full measured argv is reproduced exactly",
|
|
96
|
+
args.join(" ") ===
|
|
97
|
+
`--entwurf-control -e /path/to/entwurf/index.ts --session ${SESSION_FILE} --provider entwurf --model claude-sonnet-5`,
|
|
80
98
|
);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
// ──
|
|
101
|
+
// ── The native shape: no recorded provider, no bridge. Measured as explicitExtensionArgs=[].
|
|
84
102
|
for (const provider of [null, undefined] as const) {
|
|
85
103
|
const args = buildResumePiArgs({
|
|
86
|
-
variant: "v2-control",
|
|
87
104
|
sessionFile: SESSION_FILE,
|
|
88
105
|
explicitExtensionArgs: [],
|
|
89
106
|
provider,
|
|
90
|
-
model: "
|
|
91
|
-
prompt: "p",
|
|
92
|
-
launchArgs: [],
|
|
93
|
-
});
|
|
94
|
-
ok(`6 provider=${provider}: no --provider flag`, !args.includes("--provider"));
|
|
95
|
-
ok(`6 provider=${provider}: --model <m> <prompt> still tail`, args.slice(-3).join(" ") === "--model m p");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ── 7. the resident posture is exact: the one-shot flag never reappears ───────────
|
|
99
|
-
// The `legacy` variant was removed 2026-07-27, so the old cross-contamination pair is
|
|
100
|
-
// gone. What must NOT rot is the A1 half that still ships: a resumed citizen stands its
|
|
101
|
-
// control socket up, which means `--no-extensions` (the one-shot flag that let `pi -p`
|
|
102
|
-
// exit) can never be emitted here again.
|
|
103
|
-
{
|
|
104
|
-
const v2 = buildResumePiArgs({
|
|
105
|
-
sessionFile: SESSION_FILE,
|
|
106
|
-
explicitExtensionArgs: EXT,
|
|
107
|
-
provider: "entwurf",
|
|
108
|
-
model: "m",
|
|
109
|
-
prompt: "p",
|
|
110
|
-
launchArgs: ["--approve"],
|
|
111
|
-
variant: "v2-control",
|
|
107
|
+
model: "gpt-5.6-terra",
|
|
112
108
|
});
|
|
113
|
-
ok(
|
|
114
|
-
|
|
109
|
+
ok(
|
|
110
|
+
`[QK:RESUMEARGS-PROVIDER-OPTIONAL] provider=${provider}: NO --provider flag is emitted — an empty flag value would be a different citizen's launch, not a missing one`,
|
|
111
|
+
!args.includes("--provider"),
|
|
112
|
+
);
|
|
113
|
+
ok(
|
|
114
|
+
`provider=${provider}: the argv is exactly the four tokens the native shape needs`,
|
|
115
|
+
args.join(" ") === `--entwurf-control --session ${SESSION_FILE} --model gpt-5.6-terra`,
|
|
116
|
+
);
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* identity contract. No backend, no API, no spawn. Pure-string + synthetic-fs.
|
|
4
4
|
*
|
|
5
5
|
* - T-grammar the garden-id grammar (session-id.js SSOT): validator +
|
|
6
|
-
* generator
|
|
7
|
-
* gardenId shape now, not pi's session id.
|
|
6
|
+
* generator shape + timestamp format, all on fixed inputs. This
|
|
7
|
+
* is the RECORD's gardenId shape now, not pi's session id.
|
|
8
8
|
* - T-identity readSessionIdentity: first model_change is the model authority;
|
|
9
9
|
* drift fail-fast; the session NAME is pi's and is never read
|
|
10
10
|
* (#50 C3 — the old name-mirror/`requireEntwurf` refusals are
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* build-parse round-trip / T-no-logic / T-model-immut — the name is pi's and no
|
|
16
16
|
* code assembles or parses one), the header-scan lookup (T-collision), the
|
|
17
17
|
* transcript analyzer, and the remote scope-lock. The meta-record resolves a
|
|
18
|
-
* garden id to its transcript now (see check-
|
|
18
|
+
* garden id to its transcript now (see check-resume-launch-identity, which inherited
|
|
19
|
+
* those cells from the deleted check-entwurf-v2-spawn-production §9).
|
|
19
20
|
*
|
|
20
21
|
* The sessions base is isolated to a temp dir BEFORE the module computes its
|
|
21
22
|
* load-time paths, so the real ~/.pi/agent is never touched.
|
|
@@ -73,13 +74,16 @@ try {
|
|
|
73
74
|
ok(!isValidSessionId("20260603T191245-a3f09c "), "trailing space rejected");
|
|
74
75
|
ok(!isValidSessionId(123 as unknown), "non-string rejected");
|
|
75
76
|
|
|
76
|
-
// generateSessionId:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
// generateSessionId on a FIXED Date: deterministic shape proof — the id is the
|
|
78
|
+
// formatted timestamp prefix + `-` + 6 lowercase hex, and it satisfies the
|
|
79
|
+
// validator. The suffix is 24 random bits, so batch uniqueness is PROBABILISTIC
|
|
80
|
+
// (256 ids collide ~0.19% of runs) and is deliberately NOT asserted here — a
|
|
81
|
+
// deterministic gate must not carry a birthday-bound flake. This gate proves
|
|
82
|
+
// generator grammar/validator wiring ONLY; the fail-closed production repair
|
|
83
|
+
// for a minted-gid collision at the record store is tracked in #66, not here.
|
|
84
|
+
const genId = generateSessionId(new Date(2026, 0, 9, 5, 6, 7));
|
|
85
|
+
ok(/^20260109T050607-[0-9a-f]{6}$/.test(genId), "generated id = fixed timestamp prefix + 6-hex suffix");
|
|
86
|
+
ok(isValidSessionId(genId), "generated id passes the validator");
|
|
83
87
|
eq(formatSessionTimestamp(new Date(2026, 5, 3, 19, 12, 45)), "20260603T191245", "timestamp value (local)");
|
|
84
88
|
|
|
85
89
|
// ---- T-identity: resume identity authority = FIRST model_change ----
|
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
* - R1 domain guard: the control-socket domain currently contains pi; claude-code / codex /
|
|
12
12
|
* antigravity are `unsupported` — NEVER folded into dead/indeterminate.
|
|
13
13
|
* - the 6-cell table is exhaustive and every cell is a SINGLE verdict (Q2):
|
|
14
|
-
*
|
|
14
|
+
* ONE allow cell (ff+live=send) plus two rejects — the visible-first cut removed the
|
|
15
|
+
* `owned-outcome` row, and with it the only resume/spawn verdict the table had.
|
|
15
16
|
* - N1/F3: an indeterminate target never spawns (both intents reject it).
|
|
16
|
-
* - Q2/F1: owned-outcome + live = reject (owned-live-no-autosend), not auto-send.
|
|
17
17
|
* - F-mailbox: an `unsupported` citizen routes through a SEPARATE intent-keyed
|
|
18
18
|
* mailbox mini-table (NOT the 6-cell table): ff+deliverable → meta-mailbox/
|
|
19
19
|
* ack-only, ff+undeliverable → mailbox-undeliverable (fail-closed),
|
|
20
|
-
*
|
|
20
|
+
* Deliverability is a 2nd fact ignored
|
|
21
21
|
* in-domain (pi). The N2 asymmetry (dormant-pi reject vs unsupported mailbox)
|
|
22
22
|
* is pinned side-by-side.
|
|
23
23
|
* - R3: resolveDispatch round-trip — receipt.observedLiveness === input AND
|
|
24
24
|
* action/transport/ownership/reason === the table cell, every (intent × liveness).
|
|
25
25
|
* - R5: taxonomy covers the table reasons + pre-claims bad-target /
|
|
26
|
-
*
|
|
26
|
+
* target-locked (so bucket B F2 won't reopen it), and
|
|
27
27
|
* is PRE-DISPATCH only (no send-fail-fallback member).
|
|
28
28
|
* - schema ↔ types drift guard: the TypeBox input/receipt property keysets and
|
|
29
29
|
* the StringEnum members match the hand-written contract constants.
|
|
@@ -137,9 +137,9 @@ for (const intent of ENTWURF_INTENTS) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
eq("table has
|
|
141
|
-
eq("
|
|
142
|
-
eq("
|
|
140
|
+
eq("table has 3 cells (1 intent × 3 liveness)", allowCells + rejectCells, 3);
|
|
141
|
+
eq("post-cut: exactly 1 allow cell (fire-and-forget × live)", allowCells, 1);
|
|
142
|
+
eq("post-cut: exactly 2 reject cells (dormant, indeterminate)", rejectCells, 2);
|
|
143
143
|
|
|
144
144
|
// the two allow cells are exactly the intended ones
|
|
145
145
|
eq("allow: fire-and-forget+live = send/control-socket/ack-only", DISPATCH_TABLE["fire-and-forget"].live, {
|
|
@@ -147,13 +147,8 @@ eq("allow: fire-and-forget+live = send/control-socket/ack-only", DISPATCH_TABLE[
|
|
|
147
147
|
transport: "control-socket",
|
|
148
148
|
ownership: "ack-only",
|
|
149
149
|
});
|
|
150
|
-
eq("allow: owned-outcome+dormant = resume/spawn-bg/owned", DISPATCH_TABLE["owned-outcome"].dormant, {
|
|
151
|
-
action: "resume",
|
|
152
|
-
transport: "spawn-bg",
|
|
153
|
-
ownership: "owned",
|
|
154
|
-
});
|
|
155
150
|
|
|
156
|
-
// ── N1/F3: indeterminate
|
|
151
|
+
// ── N1/F3: an indeterminate target is never dispatched into ────────────────
|
|
157
152
|
for (const intent of ENTWURF_INTENTS) {
|
|
158
153
|
const cell = DISPATCH_TABLE[intent].indeterminate;
|
|
159
154
|
eq(`N1: ${intent}+indeterminate = reject(indeterminate-no-spawn)`, cell, {
|
|
@@ -168,12 +163,6 @@ eq("N2: fire-and-forget+dormant = reject(dormant-fire-forget-unsupported)", DISP
|
|
|
168
163
|
reason: "dormant-fire-forget-unsupported",
|
|
169
164
|
});
|
|
170
165
|
|
|
171
|
-
// ── Q2/F1: owned-outcome+live = reject, never auto-send ────────────────────
|
|
172
|
-
eq("Q2: owned-outcome+live = reject(owned-live-no-autosend)", DISPATCH_TABLE["owned-outcome"].live, {
|
|
173
|
-
action: "reject",
|
|
174
|
-
reason: "owned-live-no-autosend",
|
|
175
|
-
});
|
|
176
|
-
|
|
177
166
|
// ── R3: resolveDispatch round-trip — table cell ↔ receipt, every cell ──────
|
|
178
167
|
// In-domain (pi) liveness is authoritative; the F-mailbox deliverability fact is
|
|
179
168
|
// IGNORED here, so the verdict is identical for deliverable true/false.
|
|
@@ -207,7 +196,7 @@ for (const intent of ENTWURF_INTENTS) {
|
|
|
207
196
|
// ── F-mailbox: unsupported mailbox mini-table — SEPARATE from the 6-cell table ──
|
|
208
197
|
// The unsupported domain-guard routes through UNSUPPORTED_DISPATCH_TABLE (intent-
|
|
209
198
|
// keyed), NOT the liveness table. Two cells, each a single verdict (Q2).
|
|
210
|
-
eq("mini-table:
|
|
199
|
+
eq("mini-table: 1 cell (one per intent)", Object.keys(UNSUPPORTED_DISPATCH_TABLE).length, 1);
|
|
211
200
|
eq(
|
|
212
201
|
"mini-table[fire-and-forget] = send/meta-mailbox/ack-only (deliverable path)",
|
|
213
202
|
UNSUPPORTED_DISPATCH_TABLE["fire-and-forget"],
|
|
@@ -217,10 +206,6 @@ eq(
|
|
|
217
206
|
ownership: "ack-only",
|
|
218
207
|
},
|
|
219
208
|
);
|
|
220
|
-
eq("mini-table[owned-outcome] = reject(backend-liveness-unsupported)", UNSUPPORTED_DISPATCH_TABLE["owned-outcome"], {
|
|
221
|
-
action: "reject",
|
|
222
|
-
reason: "backend-liveness-unsupported",
|
|
223
|
-
});
|
|
224
209
|
|
|
225
210
|
// ── F-mailbox: resolveDispatch over the deliverability axis ────────────────
|
|
226
211
|
// fire-and-forget + deliverable → meta-mailbox send (NOT reject); ack-only +
|
|
@@ -248,19 +233,6 @@ eq(
|
|
|
248
233
|
observedLiveness: "unsupported",
|
|
249
234
|
},
|
|
250
235
|
);
|
|
251
|
-
// owned-outcome+unsupported still rejects regardless of deliverability — a
|
|
252
|
-
// self-fetch backend has no real liveness for the caller to own (point 3).
|
|
253
|
-
for (const deliverable of [true, false]) {
|
|
254
|
-
eq(
|
|
255
|
-
`F-mailbox: owned+unsupported (deliverable=${deliverable}) → reject(backend-liveness-unsupported)`,
|
|
256
|
-
resolveDispatch("owned-outcome", "unsupported" as FactLiveness, deliverable),
|
|
257
|
-
{
|
|
258
|
-
ok: false,
|
|
259
|
-
reason: "backend-liveness-unsupported",
|
|
260
|
-
observedLiveness: "unsupported",
|
|
261
|
-
},
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
236
|
|
|
265
237
|
// ── N2 asymmetry — the two tables are NOT contradictory ────────────────────
|
|
266
238
|
// fire-and-forget+dormant-PI = reject (confirmed not-running → enqueue is silent
|
|
@@ -273,6 +245,13 @@ ok(
|
|
|
273
245
|
resolveDispatch("fire-and-forget", "unsupported" as FactLiveness, true).ok === true,
|
|
274
246
|
);
|
|
275
247
|
|
|
248
|
+
// ── Delivery transports: exact set; process launch is not a receipt rail ───
|
|
249
|
+
eq(
|
|
250
|
+
"[QK:V2-DELIVERY-EXCLUDES-MUX] transport enum is the exact four delivery outcomes; mux launch handles stay out",
|
|
251
|
+
[...ENTWURF_V2_TRANSPORTS],
|
|
252
|
+
["control-socket", "meta-mailbox", "native-push"],
|
|
253
|
+
);
|
|
254
|
+
|
|
276
255
|
// ── F-mailbox: transport + taxonomy members present ────────────────────────
|
|
277
256
|
ok(
|
|
278
257
|
"transport: meta-mailbox present (F-mailbox liveness-free delivery)",
|
|
@@ -350,9 +329,9 @@ for (const intent of ENTWURF_INTENTS) {
|
|
|
350
329
|
}
|
|
351
330
|
}
|
|
352
331
|
}
|
|
353
|
-
eq("native-push table has
|
|
332
|
+
eq("native-push table has 3 cells (1 intent × 3 liveness)", allow + reject, 3);
|
|
354
333
|
eq("native-push: exactly 1 allow cell (fire-and-forget × alive)", allow, 1);
|
|
355
|
-
eq("native-push: exactly
|
|
334
|
+
eq("native-push: exactly 2 reject cells (dead, indeterminate)", reject, 2);
|
|
356
335
|
}
|
|
357
336
|
// the one allow cell + the intended reject reasons.
|
|
358
337
|
eq("native-push allow: ff+alive = send/native-push/ack-only", NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].alive, {
|
|
@@ -369,21 +348,13 @@ eq(
|
|
|
369
348
|
NATIVE_PUSH_DISPATCH_TABLE["fire-and-forget"].indeterminate,
|
|
370
349
|
{ action: "reject", reason: "native-push-probe-indeterminate" },
|
|
371
350
|
);
|
|
372
|
-
// owned-outcome
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
`native-push: owned+${lv} = reject(native-push-no-resume-authority)`,
|
|
376
|
-
NATIVE_PUSH_DISPATCH_TABLE["owned-outcome"][lv],
|
|
377
|
-
{
|
|
378
|
-
action: "reject",
|
|
379
|
-
reason: "native-push-no-resume-authority",
|
|
380
|
-
},
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
// `backend-liveness-unsupported` is NOT reused for a native-push backend (봉인 1).
|
|
351
|
+
// The removed `owned-outcome` row rejected native-push-no-resume-authority on every
|
|
352
|
+
// liveness. Its reason left the taxonomy with the intent, so assert it is gone rather
|
|
353
|
+
// than leaving a cell that proves retired behavior.
|
|
384
354
|
ok(
|
|
385
|
-
"native-push:
|
|
386
|
-
!(NATIVE_PUSH_REJECT_REASONS as readonly string[]).includes("
|
|
355
|
+
"native-push: the no-resume-authority reason left the taxonomy with the owned-outcome intent",
|
|
356
|
+
!(NATIVE_PUSH_REJECT_REASONS as readonly string[]).includes("native-push-no-resume-authority") &&
|
|
357
|
+
!(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("native-push-no-resume-authority"),
|
|
387
358
|
);
|
|
388
359
|
// resolveNativePushDispatch round-trip: observedLiveness === probed value, verdict === cell.
|
|
389
360
|
for (const intent of ENTWURF_INTENTS) {
|
|
@@ -424,7 +395,7 @@ for (const r of NATIVE_PUSH_REJECT_REASONS) {
|
|
|
424
395
|
for (const r of RESOLVER_REJECT_REASONS) {
|
|
425
396
|
ok(`taxonomy: resolver reason '${r}' is in the enum`, (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(r));
|
|
426
397
|
}
|
|
427
|
-
for (const pre of ["bad-target", "
|
|
398
|
+
for (const pre of ["bad-target", "target-locked"]) {
|
|
428
399
|
ok(
|
|
429
400
|
`taxonomy: pre-claim '${pre}' present (R5 — bucket B won't reopen)`,
|
|
430
401
|
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(pre),
|
|
@@ -472,9 +443,13 @@ for (const r of ENTWURF_V2_REJECT_REASONS) {
|
|
|
472
443
|
for (const r of RESOLVER_REJECT_REASONS) {
|
|
473
444
|
ok(`?6: resolver reason '${r}' is post-probe (non-null observedLiveness required)`, !isPreProbeReject(r));
|
|
474
445
|
}
|
|
475
|
-
// untrusted-fail-fast is
|
|
476
|
-
//
|
|
477
|
-
|
|
446
|
+
// untrusted-fail-fast is GONE from the taxonomy: preflight ran only behind the resume
|
|
447
|
+
// verdict, so the reject it produced left with that verdict. Asserting its absence keeps
|
|
448
|
+
// the enum honest — a reason nothing can emit is not a reason.
|
|
449
|
+
ok(
|
|
450
|
+
"taxonomy: untrusted-fail-fast is gone (its only producer was the removed resume verdict)",
|
|
451
|
+
!(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("untrusted-fail-fast"),
|
|
452
|
+
);
|
|
478
453
|
|
|
479
454
|
// #50 C4: record-less-socket — a gid-shaped non-symlink control socket with NO
|
|
480
455
|
// meta-record. The record is the sole address authority, so this is refused for
|
|
@@ -506,7 +481,7 @@ for (const r of PRE_PROBE_REJECT_REASONS) {
|
|
|
506
481
|
);
|
|
507
482
|
}
|
|
508
483
|
}
|
|
509
|
-
for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS
|
|
484
|
+
for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS]) {
|
|
510
485
|
ok(
|
|
511
486
|
`?6: well-formed('${r}', null) = false (post-probe must carry a value)`,
|
|
512
487
|
!rejectObservedLivenessWellFormed(r, null),
|
|
@@ -567,7 +542,7 @@ for (const r of PRE_PROBE_REJECT_REASONS) {
|
|
|
567
542
|
passed++;
|
|
568
543
|
}
|
|
569
544
|
}
|
|
570
|
-
for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS
|
|
545
|
+
for (const r of [...RESOLVER_REJECT_REASONS, ...NATIVE_PUSH_REJECT_REASONS]) {
|
|
571
546
|
assert.throws(
|
|
572
547
|
() => makeRejectReceipt(r, null),
|
|
573
548
|
/ill-formed reject/,
|