@junghanacs/entwurf 0.12.5 → 0.12.7
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 +31 -17
- package/BASELINE.md +42 -8
- package/CHANGELOG.md +44 -0
- package/DELIVERY.md +71 -14
- package/README.md +77 -45
- package/VERIFY.md +33 -38
- package/docs/setup-clean-host.md +80 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
- package/mcp/entwurf-bridge/src/index.ts +101 -67
- package/mcp/entwurf-bridge/test.sh +1 -1
- package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
- package/package.json +10 -6
- package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
- package/pi-extensions/lib/entwurf-self-address.ts +58 -15
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
- package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
- package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
- package/pi-extensions/lib/meta-sender-identity.ts +154 -0
- package/pi-extensions/lib/native-push/adapter.ts +255 -0
- package/pi-extensions/lib/native-push/register.ts +99 -0
- package/run.sh +974 -248
- package/scripts/agy-bridge-config.py +446 -0
- package/scripts/agy-bridge.sh +359 -0
- package/scripts/agy-hooks-bridge.sh +193 -0
- package/scripts/agy-hooks-config.py +257 -0
- package/scripts/agy-imprint.sh +28 -0
- package/scripts/agy-imprint.ts +193 -0
- package/scripts/agy-statusline-bridge.sh +176 -0
- package/scripts/agy-statusline-config.py +213 -0
- package/scripts/agy-statusline.sh +256 -0
- package/scripts/build-bridge.sh +20 -0
- package/scripts/check-agy-sender-identity.ts +364 -0
- package/scripts/check-entwurf-bridge-boot.ts +8 -2
- package/scripts/check-entwurf-deliverability.ts +34 -0
- package/scripts/check-entwurf-self-address.ts +78 -11
- package/scripts/check-entwurf-v2-contract.ts +136 -1
- package/scripts/check-entwurf-v2-decider.ts +95 -1
- package/scripts/check-entwurf-v2-matrix.ts +14 -3
- package/scripts/check-entwurf-v2-native-push.ts +193 -0
- package/scripts/check-entwurf-v2-production.ts +68 -1
- package/scripts/check-entwurf-v2-runner.ts +58 -0
- package/scripts/check-entwurf-v2-surface.ts +35 -0
- package/scripts/check-install-surface.ts +357 -0
- package/scripts/check-native-push-adapter.ts +319 -0
- package/scripts/check-native-push-register.ts +130 -0
- package/scripts/dev-bin.sh +195 -0
- package/scripts/doctor-pi-provider.ts +140 -0
- package/scripts/meta-bridge-doctor.sh +47 -9
- package/scripts/meta-bridge-install.sh +20 -17
- package/scripts/meta-bridge-state.py +40 -2
- package/scripts/meta-bridge-uninstall.sh +23 -12
- package/scripts/register-pi-package.py +183 -0
- package/scripts/register-pi-provider.py +287 -0
- package/scripts/smoke-agy-hooks-state.sh +172 -0
- package/scripts/smoke-agy-install-state.sh +660 -0
- package/scripts/smoke-agy-native-push-live.ts +243 -0
- package/scripts/smoke-agy-statusline-state.sh +300 -0
- package/scripts/smoke-meta-async-drift.sh +9 -2
- package/scripts/smoke-meta-install-state.sh +178 -8
- package/scripts/smoke-pi-provider-state.sh +182 -0
- package/scripts/smoke-user-scope-citizen.sh +191 -0
- package/scripts/with-dist-lock.sh +81 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-user-scope-citizen — deterministic gate for pi packages[] citizen
|
|
3
|
+
# registration/removal (register-pi-package.py). Offline/hermetic: a fake
|
|
4
|
+
# settings file + a fake REPO_DIR under $TMP, no pi, no network, no ~/.pi touched.
|
|
5
|
+
#
|
|
6
|
+
# Guards the wiring that dropped when `pi install` was removed from setup
|
|
7
|
+
# (2026-07-03: `--entwurf-control` / `--emacs-agent-socket` unknown in a foreign
|
|
8
|
+
# cwd because entwurf was absent from ~/.pi/agent/settings.json packages[]).
|
|
9
|
+
# statusline `?` had a tripwire; THIS omission had none and hid until GLG hit it
|
|
10
|
+
# in another repo. This gate is that missing tripwire: if the registration ever
|
|
11
|
+
# regresses, `pnpm check` goes red.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
16
|
+
REG="$REPO/scripts/register-pi-package.py"
|
|
17
|
+
|
|
18
|
+
fail=0
|
|
19
|
+
ok() { echo " ok $*"; }
|
|
20
|
+
bad() { echo " FAIL $*"; fail=1; }
|
|
21
|
+
|
|
22
|
+
command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH"; exit 1; }
|
|
23
|
+
|
|
24
|
+
TMP="$(mktemp -d -t psa-user-citizen.XXXXXX)"
|
|
25
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
26
|
+
|
|
27
|
+
# Isolation is a property of every root run.sh writes through, not of the one this gate happens
|
|
28
|
+
# to override. `remove-user-scope` today reads PI_CODING_AGENT_DIR (settings) and XDG_DATA_HOME
|
|
29
|
+
# (ownership state) — passing the operator's real XDG_DATA_HOME is what let this gate delete the
|
|
30
|
+
# live MCP key during `pnpm check` (2026-07-14). It does not touch HOME *today*; sandbox HOME and
|
|
31
|
+
# the whole XDG trio anyway, so the next root run.sh reaches for is already fenced (AGENTS rule 11).
|
|
32
|
+
export HOME="$TMP/home"
|
|
33
|
+
export XDG_DATA_HOME="$TMP/xdg"
|
|
34
|
+
export XDG_STATE_HOME="$TMP/state"
|
|
35
|
+
export XDG_CACHE_HOME="$TMP/cache"
|
|
36
|
+
mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"
|
|
37
|
+
|
|
38
|
+
S="$TMP/settings.json"
|
|
39
|
+
FAKE_REPO="$TMP/fake/entwurf"; mkdir -p "$FAKE_REPO"
|
|
40
|
+
RESOLVED="$(cd "$FAKE_REPO" && pwd)"
|
|
41
|
+
|
|
42
|
+
has_pkg() { python3 -c "import json,sys;p=json.load(open('$S'))['packages'];sys.exit(0 if '$1' in p else 1)"; }
|
|
43
|
+
count_entwurf() { python3 -c "import json;p=json.load(open('$S'))['packages'];print(sum(1 for x in p if isinstance((x if isinstance(x,str) else x.get('source')),str) and (x if isinstance(x,str) else x.get('source')).rstrip('/').split('/')[-1]=='entwurf' or (x if isinstance(x,str) else x.get('source'))=='$RESOLVED'))"; }
|
|
44
|
+
|
|
45
|
+
# 1. absent settings file → created with entwurf registered
|
|
46
|
+
python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
|
|
47
|
+
if [ -f "$S" ] && has_pkg "$RESOLVED"; then ok "absent settings → entwurf registered (file created)"; else bad "entwurf not registered into a fresh settings file"; fi
|
|
48
|
+
|
|
49
|
+
# 2. idempotent: second run is a no-op AND does not even rewrite the file (mtime stable)
|
|
50
|
+
MT1="$(stat -c %Y "$S")"
|
|
51
|
+
sleep 1
|
|
52
|
+
OUT2="$(python3 "$REG" "$S" "$FAKE_REPO")"
|
|
53
|
+
MT2="$(stat -c %Y "$S")"
|
|
54
|
+
if printf '%s' "$OUT2" | grep -q 'no-op'; then ok "second run is a no-op (idempotent)"; else bad "second run was not a no-op: $OUT2"; fi
|
|
55
|
+
if [ "$MT1" = "$MT2" ]; then ok "no-op does NOT rewrite the file (mtime stable — strongest idempotence)"; else bad "no-op rewrote the file (mtime changed $MT1 -> $MT2)"; fi
|
|
56
|
+
if [ "$(count_entwurf)" = "1" ]; then ok "no duplicate entwurf entry after idempotent re-run"; else bad "duplicate entwurf entries: $(count_entwurf)"; fi
|
|
57
|
+
|
|
58
|
+
# 3. preserves unrelated packages AND unrelated keys — including look-alike repos
|
|
59
|
+
# whose path merely CONTAINS 'entwurf' and remote repos whose last segment is
|
|
60
|
+
# 'entwurf' but are not THIS package.
|
|
61
|
+
cat > "$S" <<'JSON'
|
|
62
|
+
{
|
|
63
|
+
"defaultProvider": "openai-codex",
|
|
64
|
+
"theme": "glg-dark",
|
|
65
|
+
"packages": ["../../repos/gh/andenken", "git:github.com/badlogic/pi-telegram",
|
|
66
|
+
"/home/me/entwurf-notes", "/x/openclaw-entwurf",
|
|
67
|
+
"git:github.com/someone/entwurf", "https://github.com/someone/entwurf"]
|
|
68
|
+
}
|
|
69
|
+
JSON
|
|
70
|
+
python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
|
|
71
|
+
if python3 -c "
|
|
72
|
+
import json
|
|
73
|
+
d=json.load(open('$S')); p=d['packages']
|
|
74
|
+
assert '../../repos/gh/andenken' in p, 'andenken dropped'
|
|
75
|
+
assert 'git:github.com/badlogic/pi-telegram' in p, 'pi-telegram dropped'
|
|
76
|
+
assert '/home/me/entwurf-notes' in p, 'entwurf-notes wrongly filtered (substring false-positive)'
|
|
77
|
+
assert '/x/openclaw-entwurf' in p, 'openclaw-entwurf wrongly filtered (substring false-positive)'
|
|
78
|
+
assert 'git:github.com/someone/entwurf' in p, 'foreign git repo named entwurf wrongly filtered'
|
|
79
|
+
assert 'https://github.com/someone/entwurf' in p, 'foreign URL repo named entwurf wrongly filtered'
|
|
80
|
+
assert '$RESOLVED' in p, 'entwurf missing'
|
|
81
|
+
assert d['defaultProvider']=='openai-codex' and d['theme']=='glg-dark', 'unrelated key dropped'
|
|
82
|
+
" 2>/dev/null; then ok "preserves unrelated packages incl. look-alikes/foreign entwurf repos and keys"; else bad "clobbered unrelated packages/keys or a look-alike repo"; fi
|
|
83
|
+
|
|
84
|
+
# 4. stale entwurf paths (bare local 'entwurf' dir, npm install path, explicit npm
|
|
85
|
+
# package source) normalize to one canonical REPO_DIR.
|
|
86
|
+
cat > "$S" <<'JSON'
|
|
87
|
+
{"packages": ["/old/moved/entwurf", "/some/root/node_modules/@junghanacs/entwurf",
|
|
88
|
+
"npm:@junghanacs/entwurf@0.12.4", "../../repos/gh/andenken"]}
|
|
89
|
+
JSON
|
|
90
|
+
python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
|
|
91
|
+
if python3 -c "
|
|
92
|
+
import json
|
|
93
|
+
p=json.load(open('$S'))['packages']
|
|
94
|
+
assert '/old/moved/entwurf' not in p, 'stale bare-entwurf path not dropped'
|
|
95
|
+
assert '/some/root/node_modules/@junghanacs/entwurf' not in p, 'stale npm path not dropped'
|
|
96
|
+
assert 'npm:@junghanacs/entwurf@0.12.4' not in p, 'stale explicit npm source not dropped'
|
|
97
|
+
assert '$RESOLVED' in p, 'entwurf not normalized to REPO_DIR'
|
|
98
|
+
assert '../../repos/gh/andenken' in p, 'andenken dropped'
|
|
99
|
+
ent=[x for x in p if isinstance((x if isinstance(x,str) else x.get('source')),str) and ((x if isinstance(x,str) else x.get('source')).rstrip('/').split('/')[-1]=='entwurf' or (x if isinstance(x,str) else x.get('source')).endswith('/node_modules/@junghanacs/entwurf') or (x if isinstance(x,str) else x.get('source'))=='$RESOLVED')]
|
|
100
|
+
assert len(ent)==1, f'expected exactly 1 entwurf entry, got {ent}'
|
|
101
|
+
" 2>/dev/null; then ok "stale entwurf paths/sources (bare + npm) normalized to a single REPO_DIR"; else bad "stale entwurf normalization failed"; fi
|
|
102
|
+
|
|
103
|
+
# 5. object-form entwurf entries collapse to one canonical string (no duplicate)
|
|
104
|
+
cat > "$S" <<JSON
|
|
105
|
+
{"packages": [{"source": "$RESOLVED"}, "../../repos/gh/andenken"]}
|
|
106
|
+
JSON
|
|
107
|
+
python3 "$REG" "$S" "$FAKE_REPO" >/dev/null
|
|
108
|
+
python3 "$REG" "$S" "$FAKE_REPO" >/dev/null # twice → still one
|
|
109
|
+
if python3 -c "
|
|
110
|
+
import json
|
|
111
|
+
p=json.load(open('$S'))['packages']
|
|
112
|
+
srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
|
|
113
|
+
assert srcs.count('$RESOLVED')==1, f'object-form produced duplicate/none: {p}'
|
|
114
|
+
assert all(not isinstance(x,dict) or x.get('source')!='$RESOLVED' for x in p), 'object-form entwurf survived (should collapse to string)'
|
|
115
|
+
assert '../../repos/gh/andenken' in p, 'andenken dropped'
|
|
116
|
+
" 2>/dev/null; then ok "object-form entwurf collapses to a single canonical string (no dup across re-runs)"; else bad "object-form entwurf caused a duplicate or survived as object"; fi
|
|
117
|
+
|
|
118
|
+
# 6. a non-object settings file fails loud (never silently wipes operator config)
|
|
119
|
+
echo '[]' > "$S"
|
|
120
|
+
if python3 "$REG" "$S" "$FAKE_REPO" >/dev/null 2>&1; then bad "non-object settings should fail, not overwrite"; else ok "non-object settings fails loud (no silent clobber)"; fi
|
|
121
|
+
|
|
122
|
+
# 7. a non-array packages value fails loud (never silently coerced to [])
|
|
123
|
+
echo '{"packages": {"broken": true}}' > "$S"
|
|
124
|
+
if python3 "$REG" "$S" "$FAKE_REPO" >/dev/null 2>&1; then bad "non-array packages should fail, not coerce to []"; else ok "non-array packages fails loud (no silent drop of operator packages)"; fi
|
|
125
|
+
|
|
126
|
+
# 8. --remove drops every entwurf entry but preserves unrelated + look-alikes
|
|
127
|
+
# (symmetric with install: same is_entwurf_source predicate, no over-delete).
|
|
128
|
+
cat > "$S" <<JSON
|
|
129
|
+
{"packages": ["$RESOLVED", "../../repos/gh/andenken", "/home/me/entwurf-notes"]}
|
|
130
|
+
JSON
|
|
131
|
+
python3 "$REG" "$S" "$FAKE_REPO" --remove >/dev/null
|
|
132
|
+
if python3 -c "
|
|
133
|
+
import json
|
|
134
|
+
p=json.load(open('$S'))['packages']
|
|
135
|
+
assert '$RESOLVED' not in p, 'entwurf not removed'
|
|
136
|
+
assert '../../repos/gh/andenken' in p, 'andenken over-removed'
|
|
137
|
+
assert '/home/me/entwurf-notes' in p, 'entwurf-notes over-removed (substring over-delete)'
|
|
138
|
+
" 2>/dev/null; then ok "--remove drops entwurf, preserves unrelated + look-alikes (symmetric with install)"; else bad "--remove over-deleted a look-alike or missed entwurf"; fi
|
|
139
|
+
|
|
140
|
+
# 9. --dry-run remove REPORTS the count and writes NOTHING (backs run.sh's project
|
|
141
|
+
# `remove` pointer note that suggests the global inverse only when relevant).
|
|
142
|
+
cat > "$S" <<JSON
|
|
143
|
+
{"packages": ["$RESOLVED", "../../repos/gh/andenken"]}
|
|
144
|
+
JSON
|
|
145
|
+
MT_DR="$(stat -c %Y "$S")"; sleep 1
|
|
146
|
+
OUT_DR="$(python3 "$REG" "$S" "$FAKE_REPO" --remove --dry-run)"
|
|
147
|
+
MT_DR2="$(stat -c %Y "$S")"
|
|
148
|
+
if printf '%s' "$OUT_DR" | grep -q 'would remove 1'; then ok "--dry-run reports 'would remove' count"; else bad "--dry-run did not report the would-remove count: $OUT_DR"; fi
|
|
149
|
+
if [ "$MT_DR" = "$MT_DR2" ] && has_pkg "$RESOLVED"; then ok "--dry-run writes nothing (entry intact, mtime stable)"; else bad "--dry-run mutated settings (mtime $MT_DR -> $MT_DR2)"; fi
|
|
150
|
+
OUT_DR_NONE="$(python3 "$REG" "$TMP/absent.json" "$FAKE_REPO" --remove --dry-run)"
|
|
151
|
+
if printf '%s' "$OUT_DR_NONE" | grep -q 'no entwurf'; then ok "--dry-run on an entwurf-free file reports nothing to remove"; else bad "--dry-run false-positive on absent entry: $OUT_DR_NONE"; fi
|
|
152
|
+
|
|
153
|
+
# 9b. --dry-run WITHOUT --remove must FAIL LOUD and write nothing. A flag named
|
|
154
|
+
# "dry-run" that falls through to the register write path and mutates settings
|
|
155
|
+
# is an install-hygiene footgun (GPT blocker, 2026-07-03).
|
|
156
|
+
cat > "$S" <<'JSON'
|
|
157
|
+
{"packages": ["../../repos/gh/andenken"]}
|
|
158
|
+
JSON
|
|
159
|
+
MT_DRR="$(stat -c %Y "$S")"; sleep 1
|
|
160
|
+
if python3 "$REG" "$S" "$FAKE_REPO" --dry-run >/dev/null 2>&1; then bad "--dry-run without --remove should fail loud, not register"; else ok "--dry-run without --remove fails loud (never a silent write)"; fi
|
|
161
|
+
MT_DRR2="$(stat -c %Y "$S")"
|
|
162
|
+
if [ "$MT_DRR" = "$MT_DRR2" ] && ! has_pkg "$RESOLVED"; then ok "--dry-run without --remove wrote nothing (mtime stable, no entwurf entry added)"; else bad "--dry-run without --remove mutated settings (mtime $MT_DRR -> $MT_DRR2)"; fi
|
|
163
|
+
if python3 "$REG" "$S" "$FAKE_REPO" --bogus >/dev/null 2>&1; then bad "unknown flag should be rejected"; else ok "unknown flag rejected (fail-loud parser)"; fi
|
|
164
|
+
|
|
165
|
+
# 10. the run.sh SHELL path `remove-user-scope` reaches the SAME SSOT against a
|
|
166
|
+
# PI_CODING_AGENT_DIR-overridden ~/.pi/agent — proves install's user-scope
|
|
167
|
+
# inverse is REACHABLE from run.sh, not only from the python SSOT (the gap GPT
|
|
168
|
+
# flagged: remove symmetry existed at the SSOT but no run.sh path exercised it).
|
|
169
|
+
# REPO_DIR (run.sh's own dir) is the real checkout, so seed THAT as the entry.
|
|
170
|
+
RUN="$REPO/run.sh"
|
|
171
|
+
AGENT_DIR="$TMP/agent"; mkdir -p "$AGENT_DIR"
|
|
172
|
+
US="$AGENT_DIR/settings.json"
|
|
173
|
+
cat > "$US" <<JSON
|
|
174
|
+
{"defaultProvider": "openai-codex", "packages": ["$REPO", "../../repos/gh/andenken"]}
|
|
175
|
+
JSON
|
|
176
|
+
# remove-user-scope uses BOTH PI_CODING_AGENT_DIR (settings target) and XDG_DATA_HOME
|
|
177
|
+
# (ownership-state authority). Isolating only the former lets the fake inverse consume the
|
|
178
|
+
# operator's real state, follow its recorded managedSettingsPath, and remove the live MCP key.
|
|
179
|
+
XDG_DATA_HOME="$TMP/xdg" PI_CODING_AGENT_DIR="$AGENT_DIR" bash "$RUN" remove-user-scope >/dev/null 2>&1 || bad "run.sh remove-user-scope exited non-zero"
|
|
180
|
+
if python3 -c "
|
|
181
|
+
import json
|
|
182
|
+
d=json.load(open('$US')); p=d['packages']
|
|
183
|
+
assert '$REPO' not in p, 'run.sh remove-user-scope did not drop the global entwurf entry'
|
|
184
|
+
assert '../../repos/gh/andenken' in p, 'run.sh remove-user-scope over-removed an unrelated package'
|
|
185
|
+
assert d['defaultProvider']=='openai-codex', 'run.sh remove-user-scope dropped an unrelated key'
|
|
186
|
+
" 2>/dev/null; then ok "run.sh remove-user-scope drops the global citizen, preserves unrelated (SSOT reached via shell)"; else bad "run.sh remove-user-scope path failed"; fi
|
|
187
|
+
# idempotent: a second remove-user-scope is a clean no-op (no crash on absent entry)
|
|
188
|
+
if XDG_DATA_HOME="$TMP/xdg" PI_CODING_AGENT_DIR="$AGENT_DIR" bash "$RUN" remove-user-scope >/dev/null 2>&1; then ok "run.sh remove-user-scope is idempotent (no-op second run)"; else bad "run.sh remove-user-scope second run crashed"; fi
|
|
189
|
+
|
|
190
|
+
echo
|
|
191
|
+
if [ "$fail" -eq 0 ]; then echo "smoke-user-scope-citizen: PASS"; else echo "smoke-user-scope-citizen: FAIL (see above)"; exit 1; fi
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Serialize a command that rebuilds OR reads mcp/entwurf-bridge/dist against every
|
|
4
|
+
# other such command: `with-dist-lock.sh <cmd...>` runs <cmd> holding an atomic
|
|
5
|
+
# lock over the shared dist dir.
|
|
6
|
+
#
|
|
7
|
+
# Why the lock has to wrap the WHOLE command, not just the emit:
|
|
8
|
+
# build-bridge does `rm -rf dist` then a tsc emit; `npm pack` runs build-bridge as
|
|
9
|
+
# its prepack hook and then, AFTER prepack returns, walks the tree to read dist into
|
|
10
|
+
# the tarball. So the window where dist must stay intact is [prepack build ‥ npm's
|
|
11
|
+
# post-build read] — the entire pack, not the emit alone. A lock scoped to just the
|
|
12
|
+
# emit (released when build-bridge exits) is not merely insufficient, it BACKFIRES:
|
|
13
|
+
# it staggers concurrent packs so each run's lock-free read lands squarely inside
|
|
14
|
+
# the next run's `rm -rf dist`, turning an occasional race into a near-deterministic
|
|
15
|
+
# phantom "dist missing" pack failure (measured: 7/8 packs MISSING). Wrapping the
|
|
16
|
+
# whole pack fixes it — the read is now inside the lock (2026-07-03).
|
|
17
|
+
#
|
|
18
|
+
# Reentrancy: the wrapped `npm pack` re-invokes build-bridge as its prepack, which
|
|
19
|
+
# is itself wrapped by this script (package.json). ENTWURF_BUILD_LOCK_HELD, exported
|
|
20
|
+
# once the outer wrapper acquires, tells the nested invocation to run through
|
|
21
|
+
# without re-acquiring, so the prepack never self-deadlocks against its own gate.
|
|
22
|
+
#
|
|
23
|
+
# mkdir is the atomic primitive (POSIX, no `flock` dependency — portable across the
|
|
24
|
+
# npm/pnpm lifecycle on the Linux publish host). Mirrors the entwurf-v2 dispatch
|
|
25
|
+
# lock discipline: atomic acquire, holder pid recorded for human cleanup, same-host
|
|
26
|
+
# stale reclaim (a SIGKILL'd holder never wedges every future build), and a
|
|
27
|
+
# bounded wait that fails loud rather than hanging or building into a half-wiped dir.
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
if [ "$#" -eq 0 ]; then
|
|
31
|
+
echo "usage: with-dist-lock.sh <command> [args...]" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# An ancestor already holds the dist lock (e.g. this is `npm pack`'s prepack
|
|
36
|
+
# build-bridge under a gate that wraps the whole pack) — run reentrantly.
|
|
37
|
+
if [ -n "${ENTWURF_BUILD_LOCK_HELD:-}" ]; then
|
|
38
|
+
exec "$@"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
REPO_DIR=$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|
42
|
+
LOCK_DIR="$REPO_DIR/.tmp-verify/dist.lock"
|
|
43
|
+
mkdir -p "$REPO_DIR/.tmp-verify"
|
|
44
|
+
|
|
45
|
+
try_acquire() {
|
|
46
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
# Same-host stale reclaim: a holder killed with SIGKILL leaves the dir behind.
|
|
50
|
+
# If its recorded pid is gone, the lock is dead — reclaim it so a crashed build
|
|
51
|
+
# never wedges every future one. (Builds/packs always run on the local host, so
|
|
52
|
+
# a same-host pid probe is authoritative.)
|
|
53
|
+
local holder
|
|
54
|
+
holder=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo "")
|
|
55
|
+
if [ -n "$holder" ] && ! kill -0 "$holder" 2>/dev/null; then
|
|
56
|
+
rm -rf "$LOCK_DIR"
|
|
57
|
+
mkdir "$LOCK_DIR" 2>/dev/null && return 0
|
|
58
|
+
fi
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Bounded wait (~120s): a full pack (prepack tsc emit + npm tree walk) is a few
|
|
63
|
+
# seconds, so this clears several serialized contenders with headroom, then fails
|
|
64
|
+
# loud rather than hanging forever.
|
|
65
|
+
acquired=0
|
|
66
|
+
for _ in $(seq 1 1200); do
|
|
67
|
+
if try_acquire; then acquired=1; break; fi
|
|
68
|
+
sleep 0.1
|
|
69
|
+
done
|
|
70
|
+
if [ "$acquired" != 1 ]; then
|
|
71
|
+
echo "[with-dist-lock] could not acquire dist lock after ~120s: $LOCK_DIR" >&2
|
|
72
|
+
echo "[with-dist-lock] if no other build/pack is running, remove it: rm -rf \"$LOCK_DIR\"" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
echo "$$" > "$LOCK_DIR/pid" 2>/dev/null || true
|
|
76
|
+
# Release on ANY exit (incl. the wrapped command failing) so a red run never wedges
|
|
77
|
+
# the next. The nested prepack build-bridge sees this and skips re-acquiring.
|
|
78
|
+
export ENTWURF_BUILD_LOCK_HELD=$$
|
|
79
|
+
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
80
|
+
|
|
81
|
+
"$@"
|
|
Binary file
|