@junghanacs/entwurf 0.14.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +149 -0
  3. package/DELIVERY.md +72 -59
  4. package/README.md +120 -20
  5. package/VERIFY.md +10 -7
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +2 -2
  8. package/docs/external-mcp-host.md +26 -4
  9. package/docs/setup-clean-host.md +118 -18
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
  17. package/mcp/entwurf-bridge/src/index.ts +10 -6
  18. package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
  19. package/package.json +22 -11
  20. package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
  21. package/pi/entwurf-capabilities.json +1 -0
  22. package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
  23. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
  24. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
  25. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
  26. package/pi-extensions/entwurf-control.ts +17 -8
  27. package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
  28. package/pi-extensions/lib/meta-sender-identity.ts +10 -2
  29. package/pi-extensions/lib/meta-session.ts +102 -29
  30. package/pi-extensions/lib/mux-fresh-call.ts +82 -11
  31. package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
  32. package/run.sh +1079 -186
  33. package/scripts/agy-bridge.sh +4 -2
  34. package/scripts/check-copilot-birth-hook.ts +1174 -0
  35. package/scripts/check-copilot-launch.ts +395 -0
  36. package/scripts/check-copilot-receive-arm.ts +734 -0
  37. package/scripts/check-copilot-statusline.ts +122 -0
  38. package/scripts/check-entwurf-capabilities.ts +25 -6
  39. package/scripts/check-fresh-cut-gate.sh +104 -27
  40. package/scripts/check-gate-qualification.ts +10 -1
  41. package/scripts/check-install-container.sh +2 -2
  42. package/scripts/check-install-surface.ts +1 -1
  43. package/scripts/check-meta-capability-source.ts +89 -5
  44. package/scripts/check-meta-doctor-oracle.sh +26 -0
  45. package/scripts/check-meta-manifest-schema.py +38 -0
  46. package/scripts/check-meta-receiver-marker.ts +25 -2
  47. package/scripts/check-meta-session.ts +32 -0
  48. package/scripts/check-setup-qualification.sh +126 -0
  49. package/scripts/copilot-bridge-doctor.sh +242 -0
  50. package/scripts/copilot-bridge-install.sh +384 -0
  51. package/scripts/copilot-bridge-oracle.sh +223 -0
  52. package/scripts/copilot-bridge-uninstall.sh +137 -0
  53. package/scripts/copilot-launch.sh +236 -0
  54. package/scripts/copilot-mcp-bridge.sh +177 -0
  55. package/scripts/copilot-mcp-config.py +221 -0
  56. package/scripts/copilot-receive-bridge.sh +483 -0
  57. package/scripts/copilot-statusline-bridge.sh +149 -0
  58. package/scripts/copilot-statusline-config.py +201 -0
  59. package/scripts/copilot-statusline.sh +68 -0
  60. package/scripts/dev-bin.sh +53 -9
  61. package/scripts/fake-copilot-vendor.sh +67 -0
  62. package/scripts/meta-bridge-hook-log.sh +17 -5
  63. package/scripts/mutants/capability-cache.json +64 -0
  64. package/scripts/mutants/copilot-birth.json +244 -0
  65. package/scripts/mutants/copilot-launch.json +187 -0
  66. package/scripts/mutants/copilot-receive.json +263 -0
  67. package/scripts/mutants/fresh-cut.json +17 -0
  68. package/scripts/mutants/mux-fresh-call.json +216 -2
  69. package/scripts/mutants/pack-install.json +17 -0
  70. package/scripts/mutants/pi-package-ownership.json +92 -0
  71. package/scripts/mutants/setup-verdict.json +148 -0
  72. package/scripts/mutants/source-install.json +32 -0
  73. package/scripts/pi_settings_io.py +22 -0
  74. package/scripts/raw-async-delivery/README.md +170 -125
  75. package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
  76. package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
  77. package/scripts/register-pi-package.py +525 -7
  78. package/scripts/register-pi-provider.py +162 -12
  79. package/scripts/smoke-agy-install-state.sh +86 -30
  80. package/scripts/smoke-agy-statusline-state.sh +9 -6
  81. package/scripts/smoke-copilot-mcp-state.sh +153 -0
  82. package/scripts/smoke-copilot-statusline-state.sh +131 -0
  83. package/scripts/smoke-mux-fresh-call-live.ts +2 -0
  84. package/scripts/smoke-mux-lifecycle-live.ts +3 -1
  85. package/scripts/smoke-setup-verdict.sh +291 -0
  86. package/scripts/smoke-user-scope-citizen.sh +388 -1
  87. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-copilot-mcp-state — hermetic install/doctor/inverse contract for
3
+ # the Copilot MCP-config adapter. No Copilot process or model turn.
4
+ set -euo pipefail
5
+
6
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ RUN="$REPO_DIR/run.sh"
8
+ pass=0
9
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
10
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
11
+ want() { eval "$2" && ok "$1" || die "$1"; }
12
+
13
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
14
+ SB="$(mktemp -d)"
15
+ trap 'rm -rf "$SB"' EXIT
16
+ export HOME="$SB/home"
17
+ export XDG_DATA_HOME="$SB/xdg"
18
+ export COPILOT_MCP_CONFIG="$HOME/.copilot/mcp-config.json"
19
+ export COPILOT_MCP_COMMAND="entwurf-bridge"
20
+ STATE="$XDG_DATA_HOME/entwurf/copilot-mcp/install-state.json"
21
+ mkdir -p "$(dirname "$COPILOT_MCP_CONFIG")" "$SB/bin"
22
+
23
+ write_mcp_fake() {
24
+ cat > "$1" <<'FAKE'
25
+ #!/usr/bin/env bash
26
+ while IFS= read -r line; do
27
+ case "$line" in
28
+ *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
29
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
30
+ esac
31
+ done
32
+ FAKE
33
+ chmod +x "$1"
34
+ }
35
+ write_dead_fake() {
36
+ cat > "$1" <<'FAKE'
37
+ #!/usr/bin/env bash
38
+ echo "bash: /nonexistent/entwurf-bridge: No such file or directory" >&2
39
+ exit 127
40
+ FAKE
41
+ chmod +x "$1"
42
+ }
43
+ write_mcp_fake "$SB/bin/$COPILOT_MCP_COMMAND"
44
+
45
+ _clean_path=""
46
+ while IFS= read -r dir; do
47
+ [ -n "$dir" ] || continue
48
+ [ -e "$dir/$COPILOT_MCP_COMMAND" ] || [ -L "$dir/$COPILOT_MCP_COMMAND" ] && continue
49
+ _clean_path="${_clean_path:+$_clean_path:}$dir"
50
+ done < <(printf '%s\n' "$PATH" | tr ':' '\n')
51
+ export PATH="$SB/bin:$_clean_path"
52
+
53
+ write_config() {
54
+ printf '{\n "mcpServers": {\n "other": { "type": "local", "command": "keep-me" },\n "entwurf-bridge": { "type": "local", "command": "manual-bridge", "args": [] }\n }\n}\n' > "$COPILOT_MCP_CONFIG"
55
+ }
56
+ json() { python3 - "$COPILOT_MCP_CONFIG" "$@" <<'PY'
57
+ import json, sys
58
+ data = json.load(open(sys.argv[1]))
59
+ mode = sys.argv[2]
60
+ servers = data["mcpServers"]
61
+ ours = {"type": "local", "command": "entwurf-bridge", "args": []}
62
+ if mode == "installed":
63
+ assert servers["entwurf-bridge"] == ours
64
+ assert servers["other"] == {"type": "local", "command": "keep-me"}
65
+ assert set(data.keys()) == {"mcpServers"}
66
+ elif mode == "manual":
67
+ assert servers["entwurf-bridge"] == {"type": "local", "command": "manual-bridge", "args": []}
68
+ assert servers["other"] == {"type": "local", "command": "keep-me"}
69
+ elif mode == "created":
70
+ assert data == {"mcpServers": {"entwurf-bridge": ours}}
71
+ else:
72
+ raise AssertionError(mode)
73
+ PY
74
+ }
75
+
76
+ # Existing matching/manual config is adopted, not treated as a fresh file.
77
+ write_config
78
+ "$RUN" doctor-copilot-mcp >/dev/null
79
+ want "unowned manual config doctor is an honest note" "[ $? -eq 0 ]"
80
+ "$RUN" install-copilot-mcp >/dev/null
81
+ json installed
82
+ want "install adopts regular config and preserves unrelated servers" "[ $? -eq 0 ]"
83
+ want "install writes state" "[ -f '$STATE' ]"
84
+ "$RUN" doctor-copilot-mcp >/dev/null
85
+ want "owned configured doctor is green" "[ $? -eq 0 ]"
86
+ python3 - "$STATE" <<'PY'
87
+ import json, sys
88
+ state = json.load(open(sys.argv[1]))
89
+ assert state["preimage"] == {"type": "local", "command": "manual-bridge", "args": []}
90
+ assert state["serverKey"] == "entwurf-bridge"
91
+ PY
92
+ ok "first install captures manual entwurf-bridge preimage"
93
+ "$RUN" install-copilot-mcp >/dev/null
94
+ "$RUN" uninstall-copilot-mcp >/dev/null
95
+ json manual
96
+ want "reinstall keeps the first preimage for inverse" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
97
+
98
+ # The live-host shape is already our command with no state: adopt that exact preimage too.
99
+ write_config
100
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["mcpServers"]["entwurf-bridge"]={"type":"local","command":"entwurf-bridge","args":[]}; json.dump(d,open(p,"w"))' "$COPILOT_MCP_CONFIG"
101
+ "$RUN" install-copilot-mcp >/dev/null
102
+ python3 - "$STATE" <<'PY'
103
+ import json, sys
104
+ state = json.load(open(sys.argv[1]))
105
+ assert state["preimage"] == {"type": "local", "command": "entwurf-bridge", "args": []}
106
+ PY
107
+ "$RUN" uninstall-copilot-mcp >/dev/null
108
+ json installed
109
+ want "matching manual Copilot MCP config is adopted and restored unchanged" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
110
+
111
+ # State turns a later config drift into a red doctor rather than a silent pass.
112
+ write_config
113
+ "$RUN" install-copilot-mcp >/dev/null
114
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["mcpServers"]["entwurf-bridge"]={"type":"stdio","command":"entwurf-bridge","args":[]}; json.dump(d,open(p,"w"))' "$COPILOT_MCP_CONFIG"
115
+ if "$RUN" doctor-copilot-mcp >/dev/null 2>&1; then die "type=stdio doctor should fail"; fi
116
+ ok "state-present type=stdio entry is red"
117
+ "$RUN" install-copilot-mcp >/dev/null
118
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["mcpServers"]["entwurf-bridge"]={"type":"local","command":"other","args":[]}; json.dump(d,open(p,"w"))' "$COPILOT_MCP_CONFIG"
119
+ if "$RUN" doctor-copilot-mcp >/dev/null 2>&1; then die "command drift doctor should fail"; fi
120
+ ok "state-present command drift is red"
121
+ "$RUN" install-copilot-mcp >/dev/null
122
+
123
+ # A stable-bin command that boots then dies is red without touching config.
124
+ write_dead_fake "$SB/bin/$COPILOT_MCP_COMMAND"
125
+ if "$RUN" doctor-copilot-mcp >/dev/null 2>&1; then die "dead command doctor should fail"; fi
126
+ ok "state-present command that does not serve MCP is red"
127
+ write_mcp_fake "$SB/bin/$COPILOT_MCP_COMMAND"
128
+ "$RUN" uninstall-copilot-mcp >/dev/null
129
+ json manual
130
+ want "inverse restores manual config after drift repairs" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
131
+
132
+ # Created-new is removed only when it still contains only the adapter's key.
133
+ rm -f "$COPILOT_MCP_CONFIG"
134
+ "$RUN" install-copilot-mcp >/dev/null
135
+ json created
136
+ "$RUN" uninstall-copilot-mcp >/dev/null
137
+ want "created-new empty mcp-config is removed by inverse" "[ ! -e '$COPILOT_MCP_CONFIG' ] && [ ! -f '$STATE' ]"
138
+
139
+ # A foreign/corrupt config file is never adopted or given a state record.
140
+ printf '{"mcpServers":{}}\n' > "$SB/foreign.json"
141
+ ln -s "$SB/foreign.json" "$COPILOT_MCP_CONFIG"
142
+ if "$RUN" install-copilot-mcp >/dev/null 2>&1; then die "symlink install should refuse"; fi
143
+ ok "symlink config is refused"
144
+ want "symlink refusal leaves foreign config and no state" "! grep -q entwurf-bridge '$SB/foreign.json' && [ ! -f '$STATE' ]"
145
+ rm -f "$COPILOT_MCP_CONFIG"
146
+ printf 'not-json{{' > "$COPILOT_MCP_CONFIG"
147
+ if "$RUN" install-copilot-mcp >/dev/null 2>&1; then die "corrupt install should fail"; fi
148
+ ok "corrupt config fails loud"
149
+ want "corrupt refusal writes no state" "[ ! -f '$STATE' ]"
150
+
151
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
152
+ [ "$REPO_BEFORE" = "$REPO_AFTER" ] || die "smoke changed the checkout"
153
+ printf '\nsmoke-copilot-mcp-state: %d checks passed\n' "$pass"
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-copilot-statusline-state — hermetic install/doctor/inverse contract for
3
+ # the Copilot custom-footer settings adapter. No Copilot process or model turn.
4
+ set -euo pipefail
5
+
6
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ RUN="$REPO_DIR/run.sh"
8
+ pass=0
9
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
10
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
11
+ want() { eval "$2" && ok "$1" || die "$1"; }
12
+
13
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
14
+ SB="$(mktemp -d)"
15
+ trap 'rm -rf "$SB"' EXIT
16
+ export HOME="$SB/home"
17
+ export XDG_DATA_HOME="$SB/xdg"
18
+ export COPILOT_SETTINGS_CONFIG="$HOME/.copilot/settings.json"
19
+ export COPILOT_STATUSLINE_COMMAND="entwurf-copilot-statusline"
20
+ STATE="$XDG_DATA_HOME/entwurf/copilot-statusline/install-state.json"
21
+ mkdir -p "$(dirname "$COPILOT_SETTINGS_CONFIG")" "$SB/bin"
22
+ printf '#!/usr/bin/env bash\necho fake-copilot-statusline\n' > "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
23
+ chmod +x "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
24
+
25
+ _clean_path=""
26
+ while IFS= read -r dir; do
27
+ [ -n "$dir" ] || continue
28
+ [ -e "$dir/$COPILOT_STATUSLINE_COMMAND" ] || [ -L "$dir/$COPILOT_STATUSLINE_COMMAND" ] && continue
29
+ _clean_path="${_clean_path:+$_clean_path:}$dir"
30
+ done < <(printf '%s\n' "$PATH" | tr ':' '\n')
31
+ export PATH="$SB/bin:$_clean_path"
32
+
33
+ write_settings() {
34
+ printf '{\n "enabledPlugins": { "other@marketplace": true },\n "footer": { "showCustom": true, "showDirectory": true },\n "statusLine": { "command": "manual-statusline" }\n}\n' > "$COPILOT_SETTINGS_CONFIG"
35
+ }
36
+ json() { python3 - "$COPILOT_SETTINGS_CONFIG" "$@" <<'PY'
37
+ import json, sys
38
+ settings = json.load(open(sys.argv[1]))
39
+ mode = sys.argv[2]
40
+ if mode == "installed":
41
+ assert settings["statusLine"] == {"command": "entwurf-copilot-statusline"}
42
+ assert settings["footer"] == {"showCustom": True, "showDirectory": True}
43
+ assert settings["enabledPlugins"] == {"other@marketplace": True}
44
+ elif mode == "manual":
45
+ assert settings["statusLine"] == {"command": "manual-statusline"}
46
+ assert settings["footer"] == {"showCustom": True, "showDirectory": True}
47
+ elif mode == "created":
48
+ assert settings == {"statusLine": {"command": "entwurf-copilot-statusline"}, "footer": {"showCustom": True}}
49
+ else:
50
+ raise AssertionError(mode)
51
+ PY
52
+ }
53
+
54
+ # Existing matching/manual settings are adopted, not treated as a fresh file.
55
+ write_settings
56
+ "$RUN" doctor-copilot-statusline >/dev/null
57
+ want "unowned manual settings doctor is an honest note" "[ $? -eq 0 ]"
58
+ "$RUN" install-copilot-statusline >/dev/null
59
+ json installed
60
+ want "install adopts regular settings and preserves unrelated keys" "[ $? -eq 0 ]"
61
+ want "install writes state" "[ -f '$STATE' ]"
62
+ python3 - "$STATE" <<'PY'
63
+ import json, sys
64
+ state = json.load(open(sys.argv[1]))
65
+ assert state["statusLinePreimage"] == {"command": "manual-statusline"}
66
+ assert state["showCustomPreimage"] is True
67
+ PY
68
+ ok "first install captures manual statusLine preimage"
69
+ "$RUN" install-copilot-statusline >/dev/null
70
+ "$RUN" uninstall-copilot-statusline >/dev/null
71
+ json manual
72
+ want "reinstall keeps the first preimage for inverse" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
73
+
74
+ # The live-host shape is already our command with no state: adopt that exact preimage too.
75
+ write_settings
76
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["statusLine"]={"command":"entwurf-copilot-statusline"}; json.dump(d,open(p,"w"))' "$COPILOT_SETTINGS_CONFIG"
77
+ "$RUN" install-copilot-statusline >/dev/null
78
+ python3 - "$STATE" <<'PY'
79
+ import json, sys
80
+ state = json.load(open(sys.argv[1]))
81
+ assert state["statusLinePreimage"] == {"command": "entwurf-copilot-statusline"}
82
+ assert state["showCustomPreimage"] is True
83
+ PY
84
+ "$RUN" uninstall-copilot-statusline >/dev/null
85
+ json installed
86
+ want "matching manual Copilot config is adopted and restored unchanged" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
87
+
88
+ # State turns a later setting drift into a red doctor rather than a silent pass.
89
+ write_settings
90
+ "$RUN" install-copilot-statusline >/dev/null
91
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["footer"]["showCustom"]=False; json.dump(d,open(p,"w"))' "$COPILOT_SETTINGS_CONFIG"
92
+ if "$RUN" doctor-copilot-statusline >/dev/null 2>&1; then die "custom-disabled doctor should fail"; fi
93
+ ok "state-present custom-disabled footer is red"
94
+ "$RUN" install-copilot-statusline >/dev/null
95
+ python3 -c 'import json,sys; p=sys.argv[1]; d=json.load(open(p)); d["statusLine"]={"command":"other"}; json.dump(d,open(p,"w"))' "$COPILOT_SETTINGS_CONFIG"
96
+ if "$RUN" doctor-copilot-statusline >/dev/null 2>&1; then die "statusLine drift doctor should fail"; fi
97
+ ok "state-present statusLine drift is red"
98
+ "$RUN" install-copilot-statusline >/dev/null
99
+
100
+ # A stable-bin command that stops resolving is red without touching settings.
101
+ rm -f "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
102
+ if "$RUN" doctor-copilot-statusline >/dev/null 2>&1; then die "dangling command doctor should fail"; fi
103
+ ok "dangling bare command is red"
104
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
105
+ chmod +x "$SB/bin/$COPILOT_STATUSLINE_COMMAND"
106
+ "$RUN" uninstall-copilot-statusline >/dev/null
107
+ json manual
108
+ want "inverse restores manual settings after drift repairs" "[ $? -eq 0 ] && [ ! -f '$STATE' ]"
109
+
110
+ # Created-new is removed only when it still contains only the adapter's keys.
111
+ rm -f "$COPILOT_SETTINGS_CONFIG"
112
+ "$RUN" install-copilot-statusline >/dev/null
113
+ json created
114
+ "$RUN" uninstall-copilot-statusline >/dev/null
115
+ want "created-new empty settings file is removed by inverse" "[ ! -e '$COPILOT_SETTINGS_CONFIG' ] && [ ! -f '$STATE' ]"
116
+
117
+ # A foreign/corrupt settings file is never adopted or given a state record.
118
+ printf '{"footer":{}}\n' > "$SB/foreign.json"
119
+ ln -s "$SB/foreign.json" "$COPILOT_SETTINGS_CONFIG"
120
+ if "$RUN" install-copilot-statusline >/dev/null 2>&1; then die "symlink install should refuse"; fi
121
+ ok "symlink settings are refused"
122
+ want "symlink refusal leaves foreign settings and no state" "! grep -q statusLine '$SB/foreign.json' && [ ! -f '$STATE' ]"
123
+ rm -f "$COPILOT_SETTINGS_CONFIG"
124
+ printf 'not-json{{' > "$COPILOT_SETTINGS_CONFIG"
125
+ if "$RUN" install-copilot-statusline >/dev/null 2>&1; then die "corrupt install should fail"; fi
126
+ ok "corrupt settings fail loud"
127
+ want "corrupt refusal writes no state" "[ ! -f '$STATE' ]"
128
+
129
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
130
+ [ "$REPO_BEFORE" = "$REPO_AFTER" ] || die "smoke changed the checkout"
131
+ printf '\nsmoke-copilot-statusline-state: %d checks passed\n' "$pass"
@@ -213,6 +213,8 @@ async function main(): Promise<void> {
213
213
  // onboarding instead of exercising the configured runtime. Separate servers preserve
214
214
  // those backend-native environments without adding an env carrier to the product.
215
215
  const nonces = new Map<string, string>();
216
+ // Intentional: pi + claude-code only. Copilot clause-7 LIVE is operator-metered
217
+ // and is not a release MUST — do not add `copilot` to this loop (VERIFY.md).
216
218
  for (const backend of ["pi", "claude-code"] as const) {
217
219
  const socket = path.join(root, `${backend}.sock`);
218
220
  privateSockets.add(socket);
@@ -557,7 +557,9 @@ async function main(): Promise<void> {
557
557
  return sender;
558
558
  }
559
559
 
560
- /** One private tmux server per CELL, plus the client env that anchors into it. */
560
+ /** One private tmux server per CELL, plus the client env that anchors into it.
561
+ * Intentional: pi + claude-code only. Copilot clause-7 LIVE is operator-metered
562
+ * and is not a release MUST — do not widen this type (VERIFY.md). */
561
563
  function startPrivateServer(
562
564
  cell: string,
563
565
  backend: "pi" | "claude-code",
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-setup-verdict.sh — the smallest aggregate `setup` verdict fixture (#86 C1).
3
+ #
4
+ # The aggregate composition had ZERO automated consumers before this file: no
5
+ # living layer executed `entwurf setup` at all, so the false-green shape (WARN
6
+ # swallowing + unconditional "DONE ... green") had only a human operator as its
7
+ # gate. This fixture drives the REAL `run.sh setup` end to end in a sandbox and
8
+ # pins exactly the #86 C1 acceptance cells:
9
+ #
10
+ # S-1 all-harness-absent → core PASS, pi/claude/agy SKIP, computed green,
11
+ # zero harness writes, credential store untouched
12
+ # S-2 pi below floor → detected FAIL (never SKIP), nonzero, no Pi wiring
13
+ # S-3 pi at floor → presence completes project+user wiring, green,
14
+ # credential store still byte-identical
15
+ # S-4 agy detected+corrupt→ named component FAIL + NON-GREEN + nonzero exit,
16
+ # core and later components still attempted
17
+ # S-5 installed mode → the installed-vs-source branch is a NAMED verdict
18
+ # printed before anything else, and never reaches
19
+ # the source pnpm bootstrap
20
+ #
21
+ # Mutation attribution does NOT live here (round 3): the setup-verdict lane's
22
+ # QK signatures and kill cells moved to the snapshot-safe oracle
23
+ # scripts/check-setup-qualification.sh, so this file carries no [QK] tokens.
24
+ # This fixture is the BEHAVIOR authority only — S-2/S-4 still exercise the same
25
+ # below-floor and false-green shapes as living end-to-end evidence.
26
+ #
27
+ # Deterministic: no model, no network, no cost. Presence probes are pinned via
28
+ # PI_BIN / CLAUDE_BIN / AGY_BIN (the same hermetic seam smoke-agy-install-state
29
+ # uses), and every write root is sandboxed: HOME, XDG roots, the pi agent dir,
30
+ # and the dev-bin dir. The source bootstrap (`pnpm install --frozen-lockfile`)
31
+ # runs against the ALREADY-INSTALLED checkout, which is a verified no-op under a
32
+ # sandbox HOME (measured 2026-08-26).
33
+ set -euo pipefail
34
+
35
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ REPO_DIR="$(cd "$HERE/.." && pwd)"
37
+
38
+ case "$REPO_DIR" in
39
+ */node_modules/*)
40
+ echo "entwurf: 'smoke-setup-verdict' is a dev-clone-only surface — run it from a checkout." >&2
41
+ exit 1
42
+ ;;
43
+ esac
44
+
45
+ # Same snapshot-purity rule as the sibling install smokes: run.sh paths import
46
+ # scripts/*.py and CPython would mint scripts/__pycache__/*.pyc inside the
47
+ # gate-qualification snapshot, ending the run IMPURE.
48
+ export PYTHONDONTWRITEBYTECODE=1
49
+
50
+ SB="$(mktemp -d -t entwurf-setup-verdict.XXXXXX)"
51
+ cleanup() { rm -rf "$SB"; }
52
+ trap cleanup EXIT
53
+
54
+ export HOME="$SB/home"
55
+ export XDG_DATA_HOME="$SB/home/.local/share"
56
+ export XDG_STATE_HOME="$SB/home/.local/state"
57
+ export XDG_CACHE_HOME="$SB/home/.cache"
58
+ export XDG_CONFIG_HOME="$SB/home/.config"
59
+ export PI_CODING_AGENT_DIR="$SB/home/.pi/agent"
60
+ export ENTWURF_DEV_BIN_DIR="$SB/bin"
61
+ export PATH="$SB/bin:$PATH"
62
+ mkdir -p "$HOME" "$PI_CODING_AGENT_DIR" "$SB/bin" "$SB/harness"
63
+
64
+ # Presence pins: default every harness to a definitely-absent path; each cell
65
+ # re-pins what it needs. Production leaves these unset.
66
+ ABSENT="$SB/harness/definitely-absent"
67
+ export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT"
68
+
69
+ PASS=0
70
+ ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$*"; }
71
+ die() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
72
+ want() { # $1 = label, $2 = shell condition
73
+ if eval "$2"; then ok "$1"; else die "$1"; fi
74
+ }
75
+
76
+ AUTH="$PI_CODING_AGENT_DIR/auth.json"
77
+ seed_auth() {
78
+ printf '{\n "anthropic": {\n "type": "oauth",\n "access": "sandbox-oauth-token"\n }\n}\n' > "$AUTH"
79
+ AUTH_BEFORE="$(sha256sum "$AUTH" | cut -d' ' -f1)"
80
+ }
81
+ want_auth_untouched() { # $1 = cell label prefix
82
+ want "$1: credential store is byte-identical (no read-modify-write)" \
83
+ "[ \"\$(sha256sum '$AUTH' | cut -d' ' -f1)\" = '$AUTH_BEFORE' ]"
84
+ want "$1: no auth.json.bak was written" "[ ! -e '$AUTH.bak' ]"
85
+ want "$1: no 'entwurf' credential alias appeared" \
86
+ "! grep -q '\"entwurf\"' '$AUTH'"
87
+ }
88
+
89
+ # ── S-1: all harnesses absent → core PASS, three SKIPs, computed green, zero writes ──
90
+ echo "[smoke-setup-verdict] S-1 all-harness-absent setup"
91
+ PROJ1="$SB/proj1"; mkdir -p "$PROJ1"
92
+ seed_auth
93
+ set +e; OUT="$(bash "$REPO_DIR/run.sh" setup "$PROJ1" 2>&1)"; RC=$?; set -e
94
+ want "S-1: all-absent setup exits 0 (core green is still green)" "[ '$RC' -eq 0 ]"
95
+ want "S-1: mode is a named source-checkout branch, printed first" \
96
+ "printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: source checkout'"
97
+ want "S-1: pi absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP'"
98
+ want "S-1: claude absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'claude: SKIP'"
99
+ want "S-1: agy absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'agy: SKIP'"
100
+ want "S-1: copilot absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP'"
101
+ want "S-1: core bridge boundary validated (PASS)" "printf '%s' \"\$OUT\" | grep -q 'core: PASS'"
102
+ want "S-1: final verdict is computed, not unconditional" \
103
+ "printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
104
+ want "S-1: the retired unconditional green line is gone" \
105
+ "! printf '%s' \"\$OUT\" | grep -q 'pi adapter + detected native bridges + v2 install smoke) green'"
106
+ want "S-1: zero Pi wiring written for an absent pi" "[ ! -e '$PROJ1/.pi' ]"
107
+ want "S-1: no user-scope pi settings were created" "[ ! -e '$PI_CODING_AGENT_DIR/settings.json' ]"
108
+ want "S-1: no agy config was created" "[ ! -e '$HOME/.gemini' ]"
109
+ want "S-1: no Copilot config/units were created" "[ ! -e '$HOME/.copilot' ]"
110
+ want_auth_untouched "S-1"
111
+
112
+ # ── S-2: pi resolvable but BELOW floor → detected FAIL, never SKIP, no writes ──
113
+ echo "[smoke-setup-verdict] S-2 pi below floor"
114
+ PROJ2="$SB/proj2"; mkdir -p "$PROJ2"
115
+ printf '#!/usr/bin/env bash\necho 0.1.0\n' > "$SB/harness/pi-stale"; chmod +x "$SB/harness/pi-stale"
116
+ seed_auth
117
+ set +e; OUT="$(PI_BIN="$SB/harness/pi-stale" bash "$REPO_DIR/run.sh" setup "$PROJ2" 2>&1)"; RC=$?; set -e
118
+ # Assertion ORDER is part of the kill contract: the verdict-label check runs
119
+ # FIRST so the PI-FLOOR-SKIP mutant dies on its own token, and the exit-code
120
+ # check runs SECOND so the FALSE-GREEN mutant (labels intact, exit flipped)
121
+ # dies on its token instead of a mislabeled line.
122
+ want "S-2: below-floor pi is a detected FAIL naming the supported range" \
123
+ "printf '%s' \"\$OUT\" | grep -q 'pi: FAIL' && printf '%s' \"\$OUT\" | grep -q 'outside the supported range'"
124
+ want "S-2: a detected component FAIL owns a nonzero setup exit" "[ '$RC' -ne 0 ]"
125
+ want "S-2: a stale pi is never relabeled SKIP" "! printf '%s' \"\$OUT\" | grep -q 'pi: SKIP'"
126
+ want "S-2: summary is NON-GREEN and names the failed component" \
127
+ "printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: pi)'"
128
+ want "S-2: no Pi wiring was written for the refused pi" "[ ! -e '$PROJ2/.pi' ]"
129
+ want_auth_untouched "S-2"
130
+
131
+ # ── S-2b: pi resolvable but --version CRASHES → computed FAIL, never a set -e abort ──
132
+ # Review defect (2026-08-26): an unreadable detected pi must reach the verdict
133
+ # engine and the summary, not kill setup before them.
134
+ echo "[smoke-setup-verdict] S-2b pi present but version unreadable"
135
+ PROJ2B="$SB/proj2b"; mkdir -p "$PROJ2B"
136
+ printf '#!/usr/bin/env bash\necho "pi exploded" >&2\nexit 1\n' > "$SB/harness/pi-broken"; chmod +x "$SB/harness/pi-broken"
137
+ seed_auth
138
+ set +e; OUT="$(PI_BIN="$SB/harness/pi-broken" bash "$REPO_DIR/run.sh" setup "$PROJ2B" 2>&1)"; RC=$?; set -e
139
+ want "S-2b: unreadable pi version makes setup exit nonzero via the computed verdict" "[ '$RC' -ne 0 ]"
140
+ want "S-2b: unreadable pi is a detected FAIL naming the unreadable version" \
141
+ "printf '%s' \"\$OUT\" | grep -q 'pi: FAIL' && printf '%s' \"\$OUT\" | grep -q 'unreadable version'"
142
+ want "S-2b: later components were still attempted (core bridge smoke ran)" \
143
+ "printf '%s' \"\$OUT\" | grep -q 'core: PASS'"
144
+ want "S-2b: summary is NON-GREEN (no set -e abort before the summary)" \
145
+ "printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: pi)'"
146
+ want "S-2b: no Pi wiring was written for the unreadable pi" "[ ! -e '$PROJ2B/.pi' ]"
147
+ want_auth_untouched "S-2b"
148
+
149
+ # ── S-3: pi at the pinned floor → presence completes project + user wiring ──
150
+ echo "[smoke-setup-verdict] S-3 pi present at floor"
151
+ PROJ3="$SB/proj3"; mkdir -p "$PROJ3"
152
+ PI_PIN="$(node -e 'console.log(require(process.argv[1]).devDependencies["@earendil-works/pi-coding-agent"])' "$REPO_DIR/package.json")"
153
+ printf '#!/usr/bin/env bash\necho %s\n' "$PI_PIN" > "$SB/harness/pi-ok"; chmod +x "$SB/harness/pi-ok"
154
+ seed_auth
155
+ set +e; OUT="$(PI_BIN="$SB/harness/pi-ok" bash "$REPO_DIR/run.sh" setup "$PROJ3" 2>&1)"; RC=$?; set -e
156
+ want "S-3: pi-present setup exits 0" "[ '$RC' -eq 0 ]"
157
+ want "S-3: pi wiring completed as PASS at pin $PI_PIN" "printf '%s' \"\$OUT\" | grep -q 'pi: PASS'"
158
+ want "S-3: project .pi/settings.json written (Pi-owned wiring gated on presence)" \
159
+ "[ -f '$PROJ3/.pi/settings.json' ] && grep -q 'entwurf' '$PROJ3/.pi/settings.json'"
160
+ want "S-3: user-scope citizen registered" \
161
+ "[ -f '$PI_CODING_AGENT_DIR/settings.json' ] && grep -q 'entwurf' '$PI_CODING_AGENT_DIR/settings.json'"
162
+ want_auth_untouched "S-3"
163
+
164
+ # ── S-4: agy detected + corrupt config → named component FAIL, NON-GREEN, later components attempted ──
165
+ echo "[smoke-setup-verdict] S-4 agy detected but corrupt"
166
+ PROJ4="$SB/proj4"; mkdir -p "$PROJ4"
167
+ printf '#!/usr/bin/env bash\necho fake-agy\n' > "$SB/harness/agy"; chmod +x "$SB/harness/agy"
168
+ mkdir -p "$HOME/.gemini/config"
169
+ printf 'this is not json{{{' > "$HOME/.gemini/config/mcp_config.json"
170
+ seed_auth
171
+ set +e; OUT="$(AGY_BIN="$SB/harness/agy" bash "$REPO_DIR/run.sh" setup "$PROJ4" 2>&1)"; RC=$?; set -e
172
+ want "S-4: detected agy failure owns a nonzero setup exit" "[ '$RC' -ne 0 ]"
173
+ want "S-4: the corrupt agy bridge is a NAMED component FAIL" \
174
+ "printf '%s' \"\$OUT\" | grep -q 'agy-bridge: FAIL'"
175
+ want "S-4: the reason is corrupt-specific (invalid JSON), not a silent skip" \
176
+ "printf '%s' \"\$OUT\" | grep -qi 'invalid JSON'"
177
+ want "S-4: later components were still attempted (core bridge smoke ran)" \
178
+ "printf '%s' \"\$OUT\" | grep -q 'core: PASS'"
179
+ want "S-4: summary is NON-GREEN and computed from the outcomes" \
180
+ "printf '%s' \"\$OUT\" | grep -q 'NON-GREEN' && ! printf '%s' \"\$OUT\" | grep -q 'result: green'"
181
+ want "S-4: the corrupt config was preserved, not clobbered" \
182
+ "[ \"\$(cat '$HOME/.gemini/config/mcp_config.json')\" = 'this is not json{{{' ]"
183
+ want_auth_untouched "S-4"
184
+ rm -rf "$HOME/.gemini" "$SB/harness/agy"
185
+
186
+ # ── S-6: copilot PRESENT → all FOUR units compose independently as PASS (#86 C3b) ──
187
+ # The fake vendor is the shared factory (scripts/fake-copilot-vendor.sh, the
188
+ # same measured answer shapes check-copilot-birth-hook.ts bakes), so the birth
189
+ # installer's real preflight/list/add/install sequence runs end to end against
190
+ # it. The three writer units (MCP/receiver/footer) never touch the vendor; they
191
+ # land in the sandbox HOME/XDG.
192
+ echo "[smoke-setup-verdict] S-6 copilot present — four-unit composition"
193
+ PROJ6="$SB/proj6"; mkdir -p "$PROJ6"
194
+ FAKE_COP="$SB/harness/copilot-fake"
195
+ bash "$REPO_DIR/scripts/fake-copilot-vendor.sh" "$FAKE_COP" "$REPO_DIR/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json"
196
+ seed_auth
197
+ set +e; OUT="$(COPILOT_BIN="$FAKE_COP/copilot" PATH="$FAKE_COP:$PATH" bash "$REPO_DIR/run.sh" setup "$PROJ6" 2>&1)"; RC=$?; set -e
198
+ want "S-6: copilot-present setup exits 0 (four units + core all green)" "[ '$RC' -eq 0 ]"
199
+ want "S-6: birth unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'copilot-birth: PASS'"
200
+ want "S-6: MCP unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'copilot-mcp: PASS'"
201
+ want "S-6: receiver unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'copilot-receive: PASS'"
202
+ want "S-6: visible-footer unit composed as PASS" "printf '%s' \"\$OUT\" | grep -q 'copilot-statusline: PASS'"
203
+ want "S-6: computed summary is green" \
204
+ "printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
205
+ want "S-6: the birth install drove the real vendor sequence (marketplace add before plugin install)" \
206
+ "grep -q '^plugin marketplace add ' '$FAKE_COP/calls.log' && grep -q '^plugin install entwurf-meta-receive-copilot@meta-bridge-copilot-local$' '$FAKE_COP/calls.log'"
207
+ want "S-6: all four package-owned install-states exist (each unit keeps its inverse authority)" \
208
+ "[ -f '$XDG_DATA_HOME/entwurf/copilot-bridge/install-state.json' ] && [ -f '$XDG_DATA_HOME/entwurf/copilot-mcp/install-state.json' ] && [ -f '$XDG_DATA_HOME/entwurf/copilot-receive/install-state.json' ] && [ -f '$XDG_DATA_HOME/entwurf/copilot-statusline/install-state.json' ]"
209
+ want "S-6: the receiver unit was deployed from the compiled dist closure" \
210
+ "[ -f '$HOME/.copilot/extensions/entwurf-receive/extension.mjs' ] && [ -f '$HOME/.copilot/extensions/entwurf-receive/lib/meta-session.js' ]"
211
+ want "S-6: MCP config and footer settings landed in the sandbox HOME" \
212
+ "grep -q 'entwurf-bridge' '$HOME/.copilot/mcp-config.json' && grep -q 'entwurf-copilot-statusline' '$HOME/.copilot/settings.json'"
213
+ want_auth_untouched "S-6"
214
+ # Reset the composed Copilot state so the next cell decides on a clean host.
215
+ rm -rf "$HOME/.copilot" "$XDG_DATA_HOME/entwurf/copilot-bridge" "$XDG_DATA_HOME/entwurf/copilot-mcp" \
216
+ "$XDG_DATA_HOME/entwurf/copilot-receive" "$XDG_DATA_HOME/entwurf/copilot-statusline" \
217
+ "$XDG_DATA_HOME/entwurf/meta-bridge-copilot"
218
+
219
+ # ── S-7: copilot present but vendor lists FAIL → birth is a NAMED FAIL, the other ──
220
+ # three units are still attempted (independent outcomes), and the aggregate is
221
+ # NON-GREEN + nonzero. A failing `plugin list` is UNKNOWN, never absence (#86
222
+ # C3a), so the composition must not relabel it SKIP or cosmetic PASS.
223
+ echo "[smoke-setup-verdict] S-7 copilot present, vendor list failing"
224
+ PROJ7="$SB/proj7"; mkdir -p "$PROJ7"
225
+ BROKEN_COP="$SB/harness/copilot-broken"; mkdir -p "$BROKEN_COP"
226
+ printf '#!/usr/bin/env bash\necho "not authenticated" >&2\nexit 1\n' > "$BROKEN_COP/copilot"
227
+ chmod +x "$BROKEN_COP/copilot"
228
+ seed_auth
229
+ set +e; OUT="$(COPILOT_BIN="$BROKEN_COP/copilot" PATH="$BROKEN_COP:$PATH" bash "$REPO_DIR/run.sh" setup "$PROJ7" 2>&1)"; RC=$?; set -e
230
+ want "S-7: the failing-vendor birth is a NAMED component FAIL, never SKIP or PASS" \
231
+ "printf '%s' \"\$OUT\" | grep -q 'copilot-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP'"
232
+ want "S-7: the other three units were still attempted independently" \
233
+ "printf '%s' \"\$OUT\" | grep -q 'copilot-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'copilot-receive: PASS' && printf '%s' \"\$OUT\" | grep -q 'copilot-statusline: PASS'"
234
+ want "S-7: the detected birth failure owns a nonzero setup exit" "[ '$RC' -ne 0 ]"
235
+ want "S-7: summary is NON-GREEN naming copilot-birth" \
236
+ "printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: copilot-birth)'"
237
+ want "S-7: later components were still attempted (core bridge smoke ran)" \
238
+ "printf '%s' \"\$OUT\" | grep -q 'core: PASS'"
239
+ want "S-7: the refused birth wrote no vendor ownership state" \
240
+ "[ ! -e '$XDG_DATA_HOME/entwurf/copilot-bridge/install-state.json' ]"
241
+ want_auth_untouched "S-7"
242
+ rm -rf "$HOME/.copilot" "$XDG_DATA_HOME/entwurf/copilot-mcp" "$XDG_DATA_HOME/entwurf/copilot-receive" \
243
+ "$XDG_DATA_HOME/entwurf/copilot-statusline"
244
+
245
+ # ── S-5: installed mode is a NAMED first verdict, never the source bootstrap ──
246
+ # A bare copy of run.sh under a fake node_modules root pins the mode seam
247
+ # cheaply: the copy is NOT a runnable installed package (no dist), so this cell
248
+ # asserts only that the installed-vs-source branch is decided and printed before
249
+ # any prerequisite check and the source-only pnpm bootstrap is never reached.
250
+ # The REAL packed installed-package `setup` consumer (all-absent SKIP, computed
251
+ # green, zero writes) lives in check-pack-install's installed all-absent setup
252
+ # row; the no-bootstrap KILL attribution lives in the oracle
253
+ # scripts/check-setup-qualification.sh (round 3).
254
+ echo "[smoke-setup-verdict] S-5 installed-mode named branch (pnpm-scrubbed)"
255
+ FAKE_PKG="$SB/node_modules/@junghanacs/entwurf"
256
+ mkdir -p "$FAKE_PKG" "$SB/proj5"
257
+ cp "$REPO_DIR/run.sh" "$FAKE_PKG/run.sh"; chmod +x "$FAKE_PKG/run.sh"
258
+ cp "$REPO_DIR/package.json" "$FAKE_PKG/package.json"
259
+ # The Missing-pnpm assertion was vacuous while the fixture inherited a PATH that
260
+ # still resolved pnpm (review round 2, 2026-08-26). Build a GENUINELY scrubbed
261
+ # PATH: shim the tools the pre-death setup path needs (resolved BEFORE the
262
+ # scrub, so a host that co-locates pnpm with node — e.g. corepack — still keeps
263
+ # node), then keep only the PATH directories that do NOT carry an executable
264
+ # pnpm, and PROVE the scrub before running the cell.
265
+ SCRUB_SHIMS="$SB/pnpm-scrub-shims"; mkdir -p "$SCRUB_SHIMS"
266
+ for _tool in bash node python3 head tr cut sed grep dirname readlink cat mkdir cp chmod rm env sha256sum; do
267
+ _src="$(command -v "$_tool" 2>/dev/null || true)"
268
+ [ -n "$_src" ] && ln -s "$_src" "$SCRUB_SHIMS/$_tool"
269
+ done
270
+ SCRUBBED_PATH="$SCRUB_SHIMS"
271
+ IFS=':' read -r -a _path_dirs <<<"$PATH"
272
+ for _d in "${_path_dirs[@]}"; do
273
+ [ -n "$_d" ] || continue
274
+ [ -x "$_d/pnpm" ] && continue
275
+ SCRUBBED_PATH="$SCRUBBED_PATH:$_d"
276
+ done
277
+ unset _path_dirs _d _tool _src
278
+ want "S-5: the scrub itself holds — pnpm does NOT resolve on the scrubbed PATH" \
279
+ "! PATH=\"$SCRUBBED_PATH\" command -v pnpm >/dev/null 2>&1"
280
+ want "S-5: the scrub preserved the tools the cell needs (bash/node/python3)" \
281
+ "PATH=\"$SCRUBBED_PATH\" command -v bash >/dev/null && PATH=\"$SCRUBBED_PATH\" command -v node >/dev/null && PATH=\"$SCRUBBED_PATH\" command -v python3 >/dev/null"
282
+ set +e; OUT="$(PATH="$SCRUBBED_PATH" bash "$FAKE_PKG/run.sh" setup "$SB/proj5" 2>&1)"; RC=$?; set -e
283
+ want "S-5: installed mode is the FIRST named line, before any prerequisite check" \
284
+ "printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: installed package'"
285
+ want "S-5: installed mode never runs the source pnpm bootstrap" \
286
+ "! printf '%s' \"\$OUT\" | grep -qi 'pnpm install\|Lockfile is up to date\|Progress: resolved'"
287
+ want "S-5: installed setup never requires pnpm merely to decide/compose" \
288
+ "! printf '%s' \"\$OUT\" | grep -q 'Missing command: pnpm'"
289
+
290
+ echo ""
291
+ echo "smoke-setup-verdict: $PASS checks passed"