@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,433 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-resident-garden-guard — LIVE gate for the 0.9.0 resident garden-native
|
|
4
|
+
# session enforcement on `--entwurf-control` (NEXT.md "operator session garden
|
|
5
|
+
# identity"). Mirrors GLG's requirement: "native이든 entwurf이든 --entwurf-
|
|
6
|
+
# control 켜면 내 스타일로 고정 … 비-garden id가 보이면 바로 터져야 돼."
|
|
7
|
+
#
|
|
8
|
+
# NEGATIVE (0 tokens, the must-have): raw `pi --entwurf-control` with NO
|
|
9
|
+
# --session-id → pi mints a uuidv7 → the entwurf-control guard must BLOW UP
|
|
10
|
+
# at session_start BEFORE any model turn:
|
|
11
|
+
# - nonzero exit
|
|
12
|
+
# - NO model turn (no `agent_start`; zero tokens)
|
|
13
|
+
# - NO control socket created for that uuid session
|
|
14
|
+
# - the "Non-garden session id" reason on stderr
|
|
15
|
+
# Why this smoke exists: verified live 2026-06-04 that `ctx.shutdown()` alone
|
|
16
|
+
# does NOT stop the in-flight turn (26k tokens leaked through). The guard now
|
|
17
|
+
# hard-exits via process.exit(1); this smoke locks that guarantee so a
|
|
18
|
+
# regression to shutdown-only (silent token leak) fails the gate.
|
|
19
|
+
#
|
|
20
|
+
# REPLACEMENT (0 tokens): builtin /new + /clone (RPC) must be CANCELLED — they
|
|
21
|
+
# would mint a non-garden uuid in-process; the pre-switch guard cancels them.
|
|
22
|
+
#
|
|
23
|
+
# RESUME-INTO-UUID (0 tokens): an in-process resume (RPC switch_session) into a
|
|
24
|
+
# SYNTHETIC legacy-uuid session file must be pre-cancelled FRIENDLY at
|
|
25
|
+
# session_before_switch reason="resume" — proving that path directly, not just
|
|
26
|
+
# via the session_start hard-guard backstop. Asserts: switch_session
|
|
27
|
+
# cancelled:true, the "resume is blocked … not garden-native" guidance on
|
|
28
|
+
# stderr, the hard guard never fires, 0 tokens, resident stays on its garden
|
|
29
|
+
# id, no socket for the uuid.
|
|
30
|
+
#
|
|
31
|
+
# GNEW (0 tokens): /gnew is the garden-native in-process replacement for the
|
|
32
|
+
# blocked /new. Driven via RPC `prompt "/gnew"` (session.prompt intercepts the
|
|
33
|
+
# slash → the registered command, BEFORE any model turn). It pre-creates an
|
|
34
|
+
# empty garden session file and ctx.switchSession()es into it, so the new
|
|
35
|
+
# session is born on a garden id (header / control socket / PI_SESSION_ID),
|
|
36
|
+
# with no torn uuid. Asserts: new garden id ≠ old, 0 tokens, messageCount 0 /
|
|
37
|
+
# no conversation messages, no-rewrite-mint, socket rebound to the new id +
|
|
38
|
+
# old dropped + no uuid leak.
|
|
39
|
+
#
|
|
40
|
+
# POSITIVE (opt-in, ~1 cheap turn; set SMOKE_RGG_POSITIVE=1): garden
|
|
41
|
+
# --session-id "$(./run.sh new-session-id)" → guard passes, the session file
|
|
42
|
+
# header id is the garden id, and the resident name carries the `control`
|
|
43
|
+
# tag (NEVER `entwurf` — that tag is the entwurf_resume marker).
|
|
44
|
+
#
|
|
45
|
+
# GNEW T3 (opt-in, ~1 turn; under SMOKE_RGG_POSITIVE=1): after /gnew, one turn
|
|
46
|
+
# calls entwurf_self; the backend MCP child must report the NEW garden id —
|
|
47
|
+
# proving PI_SESSION_ID propagated through the in-process switch end to end.
|
|
48
|
+
#
|
|
49
|
+
# Cost: NEGATIVE + REPLACEMENT + RESUME-INTO-UUID + GNEW = 0 tokens. POSITIVE + GNEW T3 = ~2 cheap turns.
|
|
50
|
+
#
|
|
51
|
+
set -euo pipefail
|
|
52
|
+
|
|
53
|
+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
54
|
+
ENTWURF_DIR="$HOME/.pi/entwurf-control"
|
|
55
|
+
SESSIONS_BASE="$HOME/.pi/agent/sessions"
|
|
56
|
+
# Default target stays pi-native (openai-codex/gpt-5.4), but honor the SAME target
|
|
57
|
+
# knob as the other live smokes: ENTWURF_LIVE_TARGET="<provider>/<model>". On
|
|
58
|
+
# acp-on-v2 the ACP `entwurf` provider is back, and `./run.sh smoke-acp-rgg-live`
|
|
59
|
+
# drives THIS runner against it (deterministic half) — the guard logic (garden-id
|
|
60
|
+
# enforcement) is provider-agnostic, no assertion checks the provider name, only the
|
|
61
|
+
# garden sessionId. RGG-specific SMOKE_RGG_PROVIDER/MODEL still override.
|
|
62
|
+
if [ -n "${ENTWURF_LIVE_TARGET:-}" ]; then
|
|
63
|
+
case "$ENTWURF_LIVE_TARGET" in
|
|
64
|
+
*/*) _rgg_provider="${ENTWURF_LIVE_TARGET%%/*}"; _rgg_model="${ENTWURF_LIVE_TARGET#*/}" ;;
|
|
65
|
+
*) echo "[smoke-resident-garden-guard] ENTWURF_LIVE_TARGET must be \"<provider>/<model>\", got: $ENTWURF_LIVE_TARGET" >&2; exit 1 ;;
|
|
66
|
+
esac
|
|
67
|
+
else
|
|
68
|
+
_rgg_provider="${ENTWURF_LIVE_PROVIDER:-openai-codex}"
|
|
69
|
+
_rgg_model="${ENTWURF_LIVE_MODEL:-gpt-5.4}"
|
|
70
|
+
fi
|
|
71
|
+
PROVIDER="${SMOKE_RGG_PROVIDER:-$_rgg_provider}"
|
|
72
|
+
MODEL="${SMOKE_RGG_MODEL:-$_rgg_model}"
|
|
73
|
+
TIMEOUT="${SMOKE_RGG_TIMEOUT:-90}"
|
|
74
|
+
# Release-gate topology: this is a repo-under-test smoke, not a deployment smoke.
|
|
75
|
+
# Load ONLY this checkout's extension so results do not depend on device-local global
|
|
76
|
+
# packages / current branch wiring.
|
|
77
|
+
REPO_EXTENSION_ARGS=(--no-extensions -e "$REPO")
|
|
78
|
+
|
|
79
|
+
pass=0
|
|
80
|
+
fail=0
|
|
81
|
+
note() { printf ' %s\n' "$*"; }
|
|
82
|
+
ok() {
|
|
83
|
+
pass=$((pass + 1))
|
|
84
|
+
printf ' PASS %s\n' "$1"
|
|
85
|
+
}
|
|
86
|
+
bad() {
|
|
87
|
+
fail=$((fail + 1))
|
|
88
|
+
printf ' FAIL %s\n' "$1"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# ─── NEGATIVE — raw uuid session must blow up (0 tokens) ────────────────────
|
|
92
|
+
echo "[smoke-resident-garden-guard] NEGATIVE: raw 'pi --entwurf-control' (no --session-id)"
|
|
93
|
+
neg_out=""
|
|
94
|
+
neg_ec=0
|
|
95
|
+
neg_out=$(timeout "$TIMEOUT" pi "${REPO_EXTENSION_ARGS[@]}" --entwurf-control --provider "$PROVIDER" --model "$MODEL" \
|
|
96
|
+
--mode json -p 'RGG_NEGATIVE_SHOULD_NOT_RUN' 2>&1) || neg_ec=$?
|
|
97
|
+
|
|
98
|
+
# The session header pi minted (uuidv7) is printed on the --mode json stream.
|
|
99
|
+
neg_sid=$(printf '%s\n' "$neg_out" | grep -o '"type":"session"[^}]*"id":"[^"]*"' | head -1 | grep -o '"id":"[^"]*"' | head -1 | sed 's/.*:"//;s/"$//' || true)
|
|
100
|
+
|
|
101
|
+
if [ "$neg_ec" -ne 0 ]; then
|
|
102
|
+
ok "nonzero exit ($neg_ec)"
|
|
103
|
+
else
|
|
104
|
+
bad "expected nonzero exit, got 0 (guard did not blow up)"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if printf '%s\n' "$neg_out" | grep -q "Non-garden session id"; then
|
|
108
|
+
ok "guard reason on stderr (Non-garden session id)"
|
|
109
|
+
else
|
|
110
|
+
bad "guard reason 'Non-garden session id' not found in output"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if printf '%s\n' "$neg_out" | grep -q '"type":"agent_start"'; then
|
|
114
|
+
bad "agent_start present — the model turn RAN (token leak; shutdown-only regression?)"
|
|
115
|
+
else
|
|
116
|
+
ok "no agent_start — model turn never started (0 tokens)"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if printf '%s\n' "$neg_out" | grep -qE '"totalTokens":[1-9]'; then
|
|
120
|
+
bad "nonzero token usage observed — turn leaked through the guard"
|
|
121
|
+
else
|
|
122
|
+
ok "no token usage observed"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ -n "$neg_sid" ] && [ -e "$ENTWURF_DIR/$neg_sid.sock" ]; then
|
|
126
|
+
bad "control socket created for the uuid session ($neg_sid.sock) — server not refused"
|
|
127
|
+
else
|
|
128
|
+
ok "no control socket created for the refused session"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# The refused uuid session must not have been left on disk either (it exits
|
|
132
|
+
# before the first assistant turn, so _persist never writes the file).
|
|
133
|
+
if [ -n "$neg_sid" ] && find "$SESSIONS_BASE" -name "*_${neg_sid}.jsonl" 2>/dev/null | grep -q .; then
|
|
134
|
+
bad "uuid session file written ($neg_sid) — turn progressed past session_start"
|
|
135
|
+
else
|
|
136
|
+
ok "no uuid session file written"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# ─── REPLACEMENT — in-process new/clone must be CANCELLED, not hard-exit ─────
|
|
140
|
+
# Under --entwurf-control, /new and /fork|/clone mint a non-garden uuid in-process
|
|
141
|
+
# (no --session-id reaches an in-process switch, and the pre-switch hook result
|
|
142
|
+
# carries only { cancel } — it cannot inject a garden id). Before 0.9.0's pre-event
|
|
143
|
+
# guard these reached the session_start hard guard and process.exit(1) the WHOLE
|
|
144
|
+
# pi process — a routine /new killed the session. Now session_before_switch /
|
|
145
|
+
# session_before_fork cancel the mint: the session survives on its garden id at
|
|
146
|
+
# zero tokens. (GPT zero-token RPC repro, 2026-06-04.)
|
|
147
|
+
echo "[smoke-resident-garden-guard] REPLACEMENT: in-process new/clone cancelled (RPC, 0 tokens)"
|
|
148
|
+
rep_sid=$(bash "$REPO/run.sh" new-session-id)
|
|
149
|
+
rep_err=$(mktemp)
|
|
150
|
+
rep_out=$(printf '%s\n' '{"type":"get_state"}' '{"type":"new_session"}' '{"type":"clone"}' '{"type":"get_state"}' |
|
|
151
|
+
timeout "$TIMEOUT" pi "${REPO_EXTENSION_ARGS[@]}" --session-id "$rep_sid" --entwurf-control --provider "$PROVIDER" \
|
|
152
|
+
--model "$MODEL" --mode rpc 2>"$rep_err") || true
|
|
153
|
+
|
|
154
|
+
if printf '%s\n' "$rep_out" | grep -q '"command":"new_session","success":true,"data":{"cancelled":true}'; then
|
|
155
|
+
ok "/new (new_session) cancelled in-process"
|
|
156
|
+
else
|
|
157
|
+
bad "/new was NOT cancelled — in-process uuid mint reached the hard guard"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if printf '%s\n' "$rep_out" | grep -q '"command":"clone","success":true,"data":{"cancelled":true}'; then
|
|
161
|
+
ok "/clone (fork) cancelled in-process"
|
|
162
|
+
else
|
|
163
|
+
bad "/clone was NOT cancelled — fork mint reached the hard guard"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Both get_state calls must report the original garden id (process survived, the
|
|
167
|
+
# session was never replaced by a uuid).
|
|
168
|
+
rep_ids=$(printf '%s\n' "$rep_out" | grep -o '"sessionId":"[^"]*"' | sort -u)
|
|
169
|
+
if [ "$rep_ids" = "\"sessionId\":\"$rep_sid\"" ]; then
|
|
170
|
+
ok "session stayed on the garden id ($rep_sid) — process survived, not replaced"
|
|
171
|
+
else
|
|
172
|
+
bad "sessionId drifted from $rep_sid — got: ${rep_ids:-<none>}"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# The hard guard's "Non-garden session id" must NOT appear — the pre-switch cancel
|
|
176
|
+
# caught it first (our friendly "blocked under --entwurf-control" guidance is fine).
|
|
177
|
+
if grep -q "Non-garden session id" "$rep_err"; then
|
|
178
|
+
bad "hard guard fired (Non-garden session id) — pre-switch cancel missed a path"
|
|
179
|
+
else
|
|
180
|
+
ok "hard guard never fired (pre-switch cancel caught it; no process exit)"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# No control socket may exist for any uuid (the cancelled mints never booted).
|
|
184
|
+
if find "$ENTWURF_DIR" -name '*-*-*-*-*.sock' 2>/dev/null | grep -q .; then
|
|
185
|
+
bad "a uuid-shaped control socket exists — a cancelled mint leaked a server"
|
|
186
|
+
else
|
|
187
|
+
ok "no uuid control socket leaked"
|
|
188
|
+
fi
|
|
189
|
+
rm -f "$rep_err"
|
|
190
|
+
|
|
191
|
+
# ─── RESUME-INTO-UUID — pre-cancel a resume INTO a legacy (non-garden) session ─
|
|
192
|
+
# A garden resident must refuse an IN-PROCESS resume that would land on a
|
|
193
|
+
# non-garden (legacy uuidv7) session id. session_before_switch reason="resume"
|
|
194
|
+
# reads the TARGET session header id; a non-garden target is cancelled FRIENDLY
|
|
195
|
+
# (0 tokens, process survives) so the resume never reaches the session_start hard
|
|
196
|
+
# guard that would process.exit(1) the whole resident. Driven via RPC
|
|
197
|
+
# switch_session into a SYNTHETIC legacy-uuid session file: runtime switchSession()
|
|
198
|
+
# calls emitBeforeSwitch("resume", path) BEFORE SessionManager.open, so the
|
|
199
|
+
# fixture only needs a readable {type:"session", id:<uuid>} header line. This is
|
|
200
|
+
# the missing direct proof of the friendly pre-cancel path (it was previously only
|
|
201
|
+
# backstopped by the session_start hard guard, never exercised on its own).
|
|
202
|
+
echo "[smoke-resident-garden-guard] RESUME-INTO-UUID: legacy-uuid resume pre-cancelled (RPC, 0 tokens)"
|
|
203
|
+
res_sid=$(bash "$REPO/run.sh" new-session-id)
|
|
204
|
+
legacy_uuid="0192f1a0-1234-7abc-89de-0123456789ab" # uuidv7 shape; NOT garden-native
|
|
205
|
+
legacy_dir=$(mktemp -d)
|
|
206
|
+
legacy_file="$legacy_dir/legacy-resume-target.jsonl"
|
|
207
|
+
printf '%s\n' "{\"type\":\"session\",\"id\":\"$legacy_uuid\",\"cwd\":\"$legacy_dir\"}" >"$legacy_file"
|
|
208
|
+
res_err=$(mktemp)
|
|
209
|
+
res_out=$(printf '%s\n' '{"type":"get_state"}' "{\"type\":\"switch_session\",\"sessionPath\":\"$legacy_file\"}" '{"type":"get_state"}' |
|
|
210
|
+
timeout "$TIMEOUT" pi "${REPO_EXTENSION_ARGS[@]}" --session-id "$res_sid" --entwurf-control --provider "$PROVIDER" \
|
|
211
|
+
--model "$MODEL" --mode rpc 2>"$res_err") || true
|
|
212
|
+
|
|
213
|
+
if printf '%s\n' "$res_out" | grep -q '"command":"switch_session","success":true,"data":{"cancelled":true}'; then
|
|
214
|
+
ok "resume-into-uuid: switch_session cancelled (friendly pre-cancel fired)"
|
|
215
|
+
else
|
|
216
|
+
bad "resume-into-uuid: switch_session was NOT cancelled — pre-switch guard missed the resume path"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# Friendly guidance is the refuse path; assert BOTH the blocked-line and the
|
|
220
|
+
# garden-native reason so a generic block can't pass for the resume-specific one.
|
|
221
|
+
if grep -q "resume is blocked under --entwurf-control" "$res_err" && grep -q "is not garden-native" "$res_err"; then
|
|
222
|
+
ok "resume-into-uuid: friendly refuse message on stderr (garden-native reason)"
|
|
223
|
+
else
|
|
224
|
+
bad "resume-into-uuid: friendly refuse message not found (resume blocked + not garden-native)"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# The session_start hard guard must NOT fire — the pre-switch cancel caught it first.
|
|
228
|
+
if grep -q "Non-garden session id" "$res_err"; then
|
|
229
|
+
bad "resume-into-uuid: hard guard fired (Non-garden session id) — pre-cancel missed; resident would exit"
|
|
230
|
+
else
|
|
231
|
+
ok "resume-into-uuid: hard guard never fired (pre-cancel caught it; resident survives)"
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# 0 tokens — the cancel happens before any model turn.
|
|
235
|
+
if printf '%s\n' "$res_out" | grep -q '"type":"agent_start"'; then
|
|
236
|
+
bad "resume-into-uuid: agent_start seen — a model turn RAN (should be a 0-token cancel)"
|
|
237
|
+
else
|
|
238
|
+
ok "resume-into-uuid: no agent_start — 0-token cancel path"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# Resident stayed on its original garden id (both get_state report it, never the uuid).
|
|
242
|
+
res_ids=$(printf '%s\n' "$res_out" | grep -o '"sessionId":"[^"]*"' | sort -u)
|
|
243
|
+
if [ "$res_ids" = "\"sessionId\":\"$res_sid\"" ]; then
|
|
244
|
+
ok "resume-into-uuid: resident stayed on the garden id ($res_sid)"
|
|
245
|
+
else
|
|
246
|
+
bad "resume-into-uuid: sessionId drifted from $res_sid — got: ${res_ids:-<none>}"
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# No control socket for the legacy uuid (the cancelled resume never booted a server).
|
|
250
|
+
if [ -e "$ENTWURF_DIR/$legacy_uuid.sock" ]; then
|
|
251
|
+
bad "resume-into-uuid: control socket created for the legacy uuid — server booted"
|
|
252
|
+
else
|
|
253
|
+
ok "resume-into-uuid: no control socket for the legacy uuid"
|
|
254
|
+
fi
|
|
255
|
+
rm -f "$res_err"
|
|
256
|
+
rm -rf "$legacy_dir"
|
|
257
|
+
|
|
258
|
+
# ─── GNEW — /gnew births a NEW garden session IN-PROCESS (RPC, 0 tokens) ─────
|
|
259
|
+
# Builtin /new stays blocked (REPLACEMENT above); /gnew is its garden-native
|
|
260
|
+
# replacement. /gnew pre-creates an EMPTY garden session file and ctx.switchSession()es
|
|
261
|
+
# into it — switchSession→SessionManager.open() reads the garden header id BEFORE
|
|
262
|
+
# session_start, so the identity (file header, control socket, PI_SESSION_ID) is the
|
|
263
|
+
# garden id from the first bind: no torn uuid (unlike ctx.newSession, which mints a
|
|
264
|
+
# uuid first). Driven headless via RPC prompt "/gnew" — session.prompt intercepts the
|
|
265
|
+
# leading slash and runs the registered command BEFORE any model turn, so 0 tokens.
|
|
266
|
+
echo "[smoke-resident-garden-guard] GNEW: /gnew in-process garden birth (RPC prompt, 0 tokens)"
|
|
267
|
+
GARDEN_RE='^[0-9]{8}T[0-9]{6}-[0-9a-f]{6}$'
|
|
268
|
+
gnew_sid=$(bash "$REPO/run.sh" new-session-id)
|
|
269
|
+
gnew_json=$(node --experimental-strip-types "$REPO/scripts/gnew-rpc-drive.ts" "$gnew_sid" "$PROVIDER" "$MODEL" "$((TIMEOUT * 1000))") || true
|
|
270
|
+
|
|
271
|
+
jstr() { printf '%s' "$gnew_json" | grep -o "\"$1\":\"[^\"]*\"" | head -1 | sed 's/.*:"//;s/"$//'; }
|
|
272
|
+
g_before=$(jstr before)
|
|
273
|
+
g_after=$(jstr after)
|
|
274
|
+
|
|
275
|
+
if [ "$g_before" = "$gnew_sid" ]; then
|
|
276
|
+
ok "/gnew: session started on the launched garden id ($gnew_sid)"
|
|
277
|
+
else
|
|
278
|
+
bad "/gnew: before id ('$g_before') != launched ($gnew_sid)"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
if [ -n "$g_after" ] && [ "$g_after" != "$g_before" ] && [[ "$g_after" =~ $GARDEN_RE ]]; then
|
|
282
|
+
ok "/gnew: switched to a NEW garden id ($g_after)"
|
|
283
|
+
else
|
|
284
|
+
bad "/gnew: after id ('$g_after') is not a fresh garden id"
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
if printf '%s' "$gnew_json" | grep -q '"promptOk":true'; then
|
|
288
|
+
ok "/gnew: prompt accepted (slash command intercepted, not modeled)"
|
|
289
|
+
else
|
|
290
|
+
bad "/gnew: prompt not acknowledged — command may not be registered"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
if printf '%s' "$gnew_json" | grep -q '"agentStartSeen":true'; then
|
|
294
|
+
bad "/gnew: agent_start seen — a model turn RAN (should be a 0-token command)"
|
|
295
|
+
else
|
|
296
|
+
ok "/gnew: no agent_start — 0-token command path"
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if printf '%s' "$gnew_json" | grep -q '"afterMsgCount":0'; then
|
|
300
|
+
ok "/gnew: new session is empty (messageCount 0, no turn)"
|
|
301
|
+
else
|
|
302
|
+
bad "/gnew: new session not empty — afterMsgCount != 0"
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
if printf '%s' "$gnew_json" | grep -q '"extensionErrors":\[\]'; then
|
|
306
|
+
ok "/gnew: no extension error (handler did not throw)"
|
|
307
|
+
else
|
|
308
|
+
bad "/gnew: extension error surfaced — handler threw (switch likely did not happen)"
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# New session file on disk: header id IS the garden id (no-rewrite-mint). The file
|
|
312
|
+
# may carry session-metadata entries (model_change / session_info) but NEVER a
|
|
313
|
+
# conversation message — that is asserted separately below.
|
|
314
|
+
gfile=$(find "$SESSIONS_BASE" -name "*_${g_after}.jsonl" 2>/dev/null | head -1)
|
|
315
|
+
if [ -n "$gfile" ] && head -1 "$gfile" | grep -q "\"type\":\"session\"[^}]*\"id\":\"$g_after\""; then
|
|
316
|
+
ok "/gnew: new session file header id == $g_after (no uuid re-mint)"
|
|
317
|
+
else
|
|
318
|
+
bad "/gnew: garden header id not found on disk for $g_after"
|
|
319
|
+
fi
|
|
320
|
+
# no conversation ran: the file may carry session-metadata entries
|
|
321
|
+
# (model_change / session_info) but NEVER a user/assistant message.
|
|
322
|
+
if [ -n "$gfile" ] && ! grep -qE '"type":"(message|user_message)"|"role":"assistant"' "$gfile"; then
|
|
323
|
+
ok "/gnew: new session carries no conversation messages (clean birth)"
|
|
324
|
+
else
|
|
325
|
+
bad "/gnew: new session has conversation messages — a turn leaked"
|
|
326
|
+
fi
|
|
327
|
+
# /gnew sessions are first-class residents: the control-tagged garden name is set
|
|
328
|
+
# at switch (the file already exists), and it must NEVER carry the entwurf tag.
|
|
329
|
+
if [ -n "$gfile" ] && grep -q "\"name\":\"${g_after}==[^\"]*__control\"" "$gfile"; then
|
|
330
|
+
ok "/gnew: new session carries the 'control' resident name"
|
|
331
|
+
else
|
|
332
|
+
bad "/gnew: new session missing the 'control' resident name"
|
|
333
|
+
fi
|
|
334
|
+
# Tag-position check ONLY: the `entwurf` RESUME-MARKER lives in the tag segment
|
|
335
|
+
# (after `__`), never the title slug. After the repo rename the cwd basename is
|
|
336
|
+
# `entwurf`, so the title slug legitimately IS `entwurf` — parseSessionName treats
|
|
337
|
+
# title:"entwurf"+tags:["control"] as NOT an Entwurf session. A bare substring
|
|
338
|
+
# grep false-positives on that slug; match the `__...entwurf...` tag segment only.
|
|
339
|
+
if [ -n "$gfile" ] && grep -qE "\"name\":\"${g_after}==[^\"]*__([a-z0-9-]+_)*entwurf(_[a-z0-9-]+)*\"" "$gfile"; then
|
|
340
|
+
bad "/gnew: new session name carries the 'entwurf' tag — would be resumable as a child"
|
|
341
|
+
else
|
|
342
|
+
ok "/gnew: new session name does NOT carry the 'entwurf' tag"
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
# Control socket rebound to the new garden id; old one dropped; no uuid leak.
|
|
346
|
+
# (socketsAfterSwitch is snapshotted by the driver while pi is still alive.)
|
|
347
|
+
if printf '%s' "$gnew_json" | grep -q "\"${g_after}.sock\""; then
|
|
348
|
+
ok "/gnew: control socket rebound to the new garden id"
|
|
349
|
+
else
|
|
350
|
+
bad "/gnew: no control socket for the new garden id after switch"
|
|
351
|
+
fi
|
|
352
|
+
if printf '%s' "$gnew_json" | grep -q "\"${g_before}.sock\""; then
|
|
353
|
+
bad "/gnew: old garden socket still present — switch did not drop it"
|
|
354
|
+
else
|
|
355
|
+
ok "/gnew: old garden socket dropped after switch"
|
|
356
|
+
fi
|
|
357
|
+
if printf '%s' "$gnew_json" | grep -qE '"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.sock"'; then
|
|
358
|
+
bad "/gnew: a uuid-shaped control socket exists — torn identity"
|
|
359
|
+
else
|
|
360
|
+
ok "/gnew: no uuid-shaped control socket (garden identity intact)"
|
|
361
|
+
fi
|
|
362
|
+
# clean up the empty garden session file /gnew created (litter hygiene)
|
|
363
|
+
[ -n "$gfile" ] && rm -f "$gfile"
|
|
364
|
+
|
|
365
|
+
# ─── GNEW T3 — backend identity after /gnew (entwurf_self, opt-in ~1 turn) ───
|
|
366
|
+
if [ "${SMOKE_RGG_POSITIVE:-0}" = "1" ]; then
|
|
367
|
+
echo "[smoke-resident-garden-guard] GNEW T3: backend identity after /gnew (entwurf_self, 1 turn)"
|
|
368
|
+
gself_sid=$(bash "$REPO/run.sh" new-session-id)
|
|
369
|
+
self_prompt='Call the entwurf_self tool now, then reply with only the sessionId it returned.'
|
|
370
|
+
gself_json=$(node --experimental-strip-types "$REPO/scripts/gnew-rpc-drive.ts" "$gself_sid" "$PROVIDER" "$MODEL" "$((TIMEOUT * 1000))" "$self_prompt") || true
|
|
371
|
+
gs_after=$(printf '%s' "$gself_json" | grep -o '"after":"[^"]*"' | head -1 | sed 's/.*:"//;s/"$//')
|
|
372
|
+
gs_ids=$(printf '%s' "$gself_json" | grep -o '"selfEnvelopeSessionIds":\[[^]]*\]' | head -1)
|
|
373
|
+
|
|
374
|
+
if printf '%s' "$gself_json" | grep -q '"selfTurnEnded":true'; then
|
|
375
|
+
ok "/gnew T3: entwurf_self turn completed"
|
|
376
|
+
else
|
|
377
|
+
bad "/gnew T3: entwurf_self turn did not complete (timeout?)"
|
|
378
|
+
fi
|
|
379
|
+
# The envelope sessionId the backend MCP child reports MUST be the NEW garden
|
|
380
|
+
# id — proves PI_SESSION_ID propagated through streamShellAcp after the switch.
|
|
381
|
+
if [ -n "$gs_after" ] && printf '%s' "$gs_ids" | grep -q "\"$gs_after\""; then
|
|
382
|
+
ok "/gnew T3: backend entwurf_self sessionId == new garden id ($gs_after)"
|
|
383
|
+
else
|
|
384
|
+
bad "/gnew T3: backend identity mismatch — envelope did not report $gs_after (got $gs_ids)"
|
|
385
|
+
fi
|
|
386
|
+
# ...and NOT the pre-switch launched id (that would mean a torn/stale identity).
|
|
387
|
+
if printf '%s' "$gs_ids" | grep -q "\"$gself_sid\""; then
|
|
388
|
+
bad "/gnew T3: backend reported the PRE-switch id ($gself_sid) — torn identity"
|
|
389
|
+
else
|
|
390
|
+
ok "/gnew T3: backend did not report the pre-switch id (no torn identity)"
|
|
391
|
+
fi
|
|
392
|
+
[ -n "$gs_after" ] && find "$SESSIONS_BASE" -name "*_${gs_after}.jsonl" -delete 2>/dev/null || true
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# ─── POSITIVE — garden session passes + control name (opt-in, costs a turn) ──
|
|
396
|
+
if [ "${SMOKE_RGG_POSITIVE:-0}" = "1" ]; then
|
|
397
|
+
echo "[smoke-resident-garden-guard] POSITIVE: garden --session-id (SMOKE_RGG_POSITIVE=1)"
|
|
398
|
+
pos_sid=$(bash "$REPO/run.sh" new-session-id)
|
|
399
|
+
pos_ec=0
|
|
400
|
+
pos_out=$(timeout "$TIMEOUT" pi "${REPO_EXTENSION_ARGS[@]}" --session-id "$pos_sid" --entwurf-control --provider "$PROVIDER" \
|
|
401
|
+
--model "$MODEL" --mode json -p 'reply OK only' 2>&1) || pos_ec=$?
|
|
402
|
+
|
|
403
|
+
if [ "$pos_ec" -eq 0 ] && ! printf '%s\n' "$pos_out" | grep -q "Non-garden session id"; then
|
|
404
|
+
ok "garden session not refused (exit 0, no guard)"
|
|
405
|
+
else
|
|
406
|
+
bad "garden session was refused (exit=$pos_ec) — false positive in the guard"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
pos_file=$(find "$SESSIONS_BASE" -name "*_${pos_sid}.jsonl" 2>/dev/null | head -1)
|
|
410
|
+
if [ -n "$pos_file" ] && grep -q "\"type\":\"session\"[^}]*\"id\":\"$pos_sid\"" "$pos_file"; then
|
|
411
|
+
ok "garden header id on disk ($pos_sid)"
|
|
412
|
+
else
|
|
413
|
+
bad "garden header id not found on disk for $pos_sid"
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
if [ -n "$pos_file" ] && grep -q "\"name\":\"${pos_sid}==[^\"]*__control\"" "$pos_file"; then
|
|
417
|
+
ok "resident name carries the 'control' tag"
|
|
418
|
+
else
|
|
419
|
+
bad "resident name with 'control' tag not found"
|
|
420
|
+
fi
|
|
421
|
+
# Tag-position check only (see /gnew block above): match the `__...entwurf...`
|
|
422
|
+
# tag segment, not the title slug (which is legitimately `entwurf` post-rename).
|
|
423
|
+
if [ -n "$pos_file" ] && grep -qE "\"name\":\"${pos_sid}==[^\"]*__([a-z0-9-]+_)*entwurf(_[a-z0-9-]+)*\"" "$pos_file"; then
|
|
424
|
+
bad "resident name carries the 'entwurf' tag — would be resumable as a child"
|
|
425
|
+
else
|
|
426
|
+
ok "resident name does NOT carry the 'entwurf' tag"
|
|
427
|
+
fi
|
|
428
|
+
else
|
|
429
|
+
note "POSITIVE skipped (set SMOKE_RGG_POSITIVE=1 to run; costs ~1 cheap turn)"
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
echo "[smoke-resident-garden-guard] PASS=$pass FAIL=$fail"
|
|
433
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smoke-session-id-name — LIVE 3-turn substrate smoke for the Pi 0.78
|
|
3
|
+
* `--session-id` + `--name` primitives, exercised with a pi-native provider
|
|
4
|
+
* but WITHOUT touching the Entwurf tool surface (Phase 3a).
|
|
5
|
+
*
|
|
6
|
+
* It dogfoods the locked identity helpers (generateSessionId / buildSessionName
|
|
7
|
+
* / readSessionHeader / findSessionFilesById / analyzeSessionFileLike) against a
|
|
8
|
+
* real `pi` process, proving the substrate the Entwurf rewrite will sit on:
|
|
9
|
+
*
|
|
10
|
+
* T1 same-cwd turn 1 : header id == sessionId, header cwd == launch cwd,
|
|
11
|
+
* session_info.name == the denote-style name (info layer).
|
|
12
|
+
* T2 same-cwd turn 2 : NO --name re-supplied → exactly one session file,
|
|
13
|
+
* append-not-recreate (analyze turns grow), session name
|
|
14
|
+
* unchanged (spawn-only name).
|
|
15
|
+
* T3 wrong-cwd turn : same --session-id from a DIFFERENT cwd creates a SECOND
|
|
16
|
+
* session with the same header id but a different header
|
|
17
|
+
* cwd. Recorded as footgun EVIDENCE (not a failure) — this
|
|
18
|
+
* is exactly what the 0.9.0 resume guard must prevent by
|
|
19
|
+
* forcing child cwd to the saved header cwd.
|
|
20
|
+
*
|
|
21
|
+
* LIVE: spawns real `pi` turns (auth + tokens). Cheap sonnet `-p 'ok'`. Isolated
|
|
22
|
+
* via a temp PI_CODING_AGENT_DIR so the real ~/.pi/agent is never touched.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
const PI_BIN = process.env.PI_BIN ?? "pi";
|
|
34
|
+
|
|
35
|
+
// v2-only retarget: the substrate smoke no longer rides the (removed) ACP
|
|
36
|
+
// `entwurf` provider — the --session-id/--name primitives under test are
|
|
37
|
+
// provider-agnostic. Drive a real pi-native provider/model instead, sharing the
|
|
38
|
+
// v2 live-smoke env (ENTWURF_LIVE_TARGET, default openai-codex/gpt-5.4).
|
|
39
|
+
function resolveTarget(): { provider: string; model: string } {
|
|
40
|
+
const combined = process.env.ENTWURF_LIVE_TARGET?.trim();
|
|
41
|
+
if (combined) {
|
|
42
|
+
const slash = combined.indexOf("/");
|
|
43
|
+
if (slash <= 0 || slash === combined.length - 1) {
|
|
44
|
+
throw new Error(`ENTWURF_LIVE_TARGET must be "<provider>/<model>", got: ${JSON.stringify(combined)}`);
|
|
45
|
+
}
|
|
46
|
+
return { provider: combined.slice(0, slash), model: combined.slice(slash + 1) };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
provider: process.env.ENTWURF_LIVE_PROVIDER?.trim() || "openai-codex",
|
|
50
|
+
model: process.env.ENTWURF_LIVE_MODEL?.trim() || "gpt-5.4",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const { provider: LIVE_PROVIDER, model: LIVE_MODEL } = resolveTarget();
|
|
54
|
+
|
|
55
|
+
// Isolate session storage BEFORE importing entwurf-core (it computes
|
|
56
|
+
// SESSIONS_BASE at module load from PI_CODING_AGENT_DIR).
|
|
57
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "psa-sidname-agent-"));
|
|
58
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
59
|
+
process.env.ENTWURF_TARGETS_PATH = path.join(REPO_ROOT, "pi", "entwurf-targets.json");
|
|
60
|
+
|
|
61
|
+
// The native retarget provider authenticates via <PI_CODING_AGENT_DIR>/auth.json.
|
|
62
|
+
// The isolated temp dir has none (old ACP `entwurf` rode Claude Code OAuth, so
|
|
63
|
+
// isolation was free); copy the real OAuth creds in so real turns authenticate while
|
|
64
|
+
// SESSIONS stay isolated — the whole point — and the dir is removed on exit.
|
|
65
|
+
const realAuth = path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
66
|
+
if (fs.existsSync(realAuth)) fs.copyFileSync(realAuth, path.join(agentDir, "auth.json"));
|
|
67
|
+
|
|
68
|
+
const { generateSessionId, buildSessionName, readSessionHeader, findSessionFilesById, analyzeSessionFileLike } =
|
|
69
|
+
await import("../pi-extensions/lib/entwurf-core.ts");
|
|
70
|
+
|
|
71
|
+
const cwd1 = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-sidname-cwd1-")));
|
|
72
|
+
const cwd2 = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-sidname-cwd2-")));
|
|
73
|
+
|
|
74
|
+
function cleanup() {
|
|
75
|
+
for (const d of [agentDir, cwd1, cwd2]) {
|
|
76
|
+
try {
|
|
77
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
78
|
+
} catch {
|
|
79
|
+
/* best-effort */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Latest `session_info` name wins (matches pi's getSessionName). */
|
|
85
|
+
function readSessionInfoName(file: string): string | null {
|
|
86
|
+
let name: string | null = null;
|
|
87
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
88
|
+
const t = line.trim();
|
|
89
|
+
if (!t) continue;
|
|
90
|
+
try {
|
|
91
|
+
const e = JSON.parse(t) as { type?: string; name?: unknown };
|
|
92
|
+
if (e.type === "session_info" && typeof e.name === "string") name = e.name;
|
|
93
|
+
} catch {
|
|
94
|
+
/* skip */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runPiTurn(turnCwd: string, sessionId: string, name: string | null, prompt: string): void {
|
|
101
|
+
const args = [
|
|
102
|
+
"--no-extensions",
|
|
103
|
+
"-e",
|
|
104
|
+
REPO_ROOT,
|
|
105
|
+
"--session-id",
|
|
106
|
+
sessionId,
|
|
107
|
+
"--provider",
|
|
108
|
+
LIVE_PROVIDER,
|
|
109
|
+
"--model",
|
|
110
|
+
LIVE_MODEL,
|
|
111
|
+
"--mode",
|
|
112
|
+
"json",
|
|
113
|
+
];
|
|
114
|
+
if (name !== null) args.splice(args.indexOf("--provider"), 0, "--name", name);
|
|
115
|
+
args.push("-p", prompt);
|
|
116
|
+
|
|
117
|
+
const res = spawnSync(PI_BIN, args, {
|
|
118
|
+
cwd: turnCwd,
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
timeout: 180_000,
|
|
121
|
+
env: { ...process.env, PI_CODING_AGENT_DIR: agentDir },
|
|
122
|
+
});
|
|
123
|
+
if (res.status !== 0) {
|
|
124
|
+
const tail = `${res.stdout ?? ""}\n${res.stderr ?? ""}`.split("\n").slice(-12).join("\n");
|
|
125
|
+
throw new Error(`pi turn failed (status=${res.status}, signal=${res.signal}) in ${turnCwd}:\n${tail}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const sessionId = generateSessionId();
|
|
131
|
+
const name = buildSessionName({
|
|
132
|
+
sessionId,
|
|
133
|
+
provider: LIVE_PROVIDER,
|
|
134
|
+
model: LIVE_MODEL,
|
|
135
|
+
rawTitle: "substrate smoke session-id name",
|
|
136
|
+
tags: ["entwurf", "smoke"],
|
|
137
|
+
});
|
|
138
|
+
console.log(`[smoke-session-id-name] sessionId=${sessionId}`);
|
|
139
|
+
console.log(`[smoke-session-id-name] name=${name}`);
|
|
140
|
+
|
|
141
|
+
// ---- T1: same-cwd turn 1 (spawn with --session-id + --name) ----
|
|
142
|
+
runPiTurn(cwd1, sessionId, name, "ok");
|
|
143
|
+
const afterT1 = findSessionFilesById(sessionId);
|
|
144
|
+
assert.equal(afterT1.length, 1, `[T1] exactly one session file (got ${afterT1.length})`);
|
|
145
|
+
const fileT1 = afterT1[0] as string;
|
|
146
|
+
const headerT1 = readSessionHeader(fileT1);
|
|
147
|
+
assert.equal(headerT1?.id, sessionId, "[T1] header id == sessionId");
|
|
148
|
+
assert.equal(headerT1?.cwd, cwd1, `[T1] header cwd == launch cwd (${headerT1?.cwd} vs ${cwd1})`);
|
|
149
|
+
assert.equal(readSessionInfoName(fileT1), name, "[T1] session_info.name == denote name (info layer, not header)");
|
|
150
|
+
const turnsT1 = analyzeSessionFileLike(fileT1).turns;
|
|
151
|
+
assert.ok(turnsT1 >= 1, `[T1] at least one assistant turn (got ${turnsT1})`);
|
|
152
|
+
console.log(`[smoke-session-id-name] [T1] ok — file=${path.basename(fileT1)} turns=${turnsT1}`);
|
|
153
|
+
|
|
154
|
+
// ---- T2: same-cwd turn 2, NO --name (append, spawn-only name) ----
|
|
155
|
+
runPiTurn(cwd1, sessionId, null, "ok again");
|
|
156
|
+
const afterT2 = findSessionFilesById(sessionId);
|
|
157
|
+
assert.equal(afterT2.length, 1, `[T2] still exactly one session file — append, not recreate (got ${afterT2.length})`);
|
|
158
|
+
assert.equal(afterT2[0], fileT1, "[T2] same session file path");
|
|
159
|
+
const headerT2 = readSessionHeader(fileT1);
|
|
160
|
+
assert.equal(headerT2?.id, sessionId, "[T2] header id unchanged");
|
|
161
|
+
assert.equal(headerT2?.cwd, cwd1, "[T2] header cwd unchanged");
|
|
162
|
+
assert.equal(readSessionInfoName(fileT1), name, "[T2] session name unchanged without --name (spawn-only)");
|
|
163
|
+
const turnsT2 = analyzeSessionFileLike(fileT1).turns;
|
|
164
|
+
assert.ok(turnsT2 > turnsT1, `[T2] turns grew ${turnsT1} → ${turnsT2} (appended to existing session)`);
|
|
165
|
+
console.log(`[smoke-session-id-name] [T2] ok — append confirmed, turns=${turnsT2}`);
|
|
166
|
+
|
|
167
|
+
// ---- T3: wrong-cwd turn, same --session-id (footgun EVIDENCE) ----
|
|
168
|
+
runPiTurn(cwd2, sessionId, null, "ok");
|
|
169
|
+
const afterT3 = findSessionFilesById(sessionId);
|
|
170
|
+
assert.equal(
|
|
171
|
+
afterT3.length,
|
|
172
|
+
2,
|
|
173
|
+
`[T3] same id from a different cwd created a SECOND session (footgun; got ${afterT3.length})`,
|
|
174
|
+
);
|
|
175
|
+
const cwds = afterT3.map((f) => readSessionHeader(f)?.cwd).sort();
|
|
176
|
+
assert.deepEqual(cwds, [cwd1, cwd2].sort(), "[T3] the two sessions carry different header cwds");
|
|
177
|
+
console.log(
|
|
178
|
+
`[smoke-session-id-name] [T3] footgun evidence recorded — same sessionId now exists under 2 cwds ` +
|
|
179
|
+
`(${cwd1}, ${cwd2}). 0.9.0 resume MUST force child cwd to the saved header cwd to avoid this.`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
console.log(
|
|
183
|
+
"[smoke-session-id-name] PASS — substrate proven (append + spawn-only name + wrong-cwd footgun documented)",
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
cleanup();
|
|
187
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Sub-config for verification scripts. Same shape as mcp/tsconfig.json:
|
|
3
|
+
// these files run under Node's `--experimental-strip-types` resolver, so
|
|
4
|
+
// they import sibling .ts files with explicit `.ts` extensions. tsc pairs
|
|
5
|
+
// allowImportingTsExtensions with noEmit, which matches the typecheck-only
|
|
6
|
+
// role of these scripts. See AGENTS.md § Typecheck Boundary for the full
|
|
7
|
+
// fence policy — every .ts in this repo must be reached by exactly one
|
|
8
|
+
// `tsc --noEmit` pass.
|
|
9
|
+
//
|
|
10
|
+
// Why scripts/ is split off from the root config:
|
|
11
|
+
// - Root config is emit-capable (`./run.sh check-models` tsc-emits the
|
|
12
|
+
// project entry). It cannot enable allowImportingTsExtensions.
|
|
13
|
+
// - These scripts must use explicit .ts on the wire (strip-types runtime).
|
|
14
|
+
// - Same trade-off mcp/* already resolved. This split is the smallest
|
|
15
|
+
// expression that covers the runtime modes without weakening the fence.
|
|
16
|
+
"extends": "../tsconfig.json",
|
|
17
|
+
"compilerOptions": {
|
|
18
|
+
"allowImportingTsExtensions": true,
|
|
19
|
+
"noEmit": true,
|
|
20
|
+
"rootDir": ".."
|
|
21
|
+
},
|
|
22
|
+
// Override the root `exclude` (which lists "scripts"): without this, the
|
|
23
|
+
// inherited exclude filters out everything `include` adds under scripts/, so
|
|
24
|
+
// the verification scripts were silently NOT typechecked (fence gap found in
|
|
25
|
+
// Phase 3b). Re-list the root's other exclusions, minus "scripts".
|
|
26
|
+
"exclude": ["../node_modules", "../mcp", "../plugins"],
|
|
27
|
+
"include": [
|
|
28
|
+
"./**/*.ts",
|
|
29
|
+
"../pi-extensions/lib/**/*.ts",
|
|
30
|
+
"../pi-extensions/model-lock.ts",
|
|
31
|
+
"../pi-extensions/meta-bridge-hook.ts",
|
|
32
|
+
"../mcp/entwurf-bridge/src/resume-mode.ts"
|
|
33
|
+
]
|
|
34
|
+
}
|