@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,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-meta-honesty — 1.0.0 meta-bridge: the HONESTY regression gate (#30 bbot
|
|
4
|
+
# "release blockers" review). Two surfaces whose whole value is telling the truth:
|
|
5
|
+
#
|
|
6
|
+
# PART A — doorbell count (blocker #1). doorbell.sh must count EVERY queued
|
|
7
|
+
# *.msg, deliver them ALL to *.msg.delivered, and announce the real N with an
|
|
8
|
+
# honest singular/plural. The old bug saw `ls | head -1`, mv'd one, and
|
|
9
|
+
# hard-coded "1 unread" — a lying count, which for a project whose core value
|
|
10
|
+
# is honesty is a release blocker.
|
|
11
|
+
#
|
|
12
|
+
# PART B — hook level logging (blocker #2) + sender-marker evidence. A silent
|
|
13
|
+
# registration miss is the dangerous case: the session opens fine but never
|
|
14
|
+
# becomes a garden citizen. The runtime hook must STAY best-effort (no terminal
|
|
15
|
+
# scream), but it must log a failure as ` ERROR ` so the doctor — the fail-loud
|
|
16
|
+
# surface — can grep it. A normal create/attach must NOT emit ERROR (no false
|
|
17
|
+
# alarm), and must log sender-marker evidence so send-side replyable identity
|
|
18
|
+
# is not silently lost.
|
|
19
|
+
#
|
|
20
|
+
# Deterministic + offline (no `claude -p`, no network). Safe for pnpm check /
|
|
21
|
+
# pre-commit. Isolates its store via PI_CODING_AGENT_DIR in a temp dir.
|
|
22
|
+
#
|
|
23
|
+
# RUNTIME DEPS: bash + node (the hook runs under `node --experimental-strip-types`)
|
|
24
|
+
# + python3 (doorbell.sh parses the FileChanged stdin JSON with python3). NOT pure
|
|
25
|
+
# "node+bash" — the doctor (Phase 2) must verify python3 too.
|
|
26
|
+
#
|
|
27
|
+
# USAGE: ./run.sh smoke-meta-honesty
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
32
|
+
DOORBELL="$REPO/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh"
|
|
33
|
+
HOOK="$REPO/pi-extensions/meta-bridge-hook.ts"
|
|
34
|
+
|
|
35
|
+
NODE_BIN="$(command -v node || true)"
|
|
36
|
+
[ -n "$NODE_BIN" ] || { echo "FAIL: node not on PATH (the hook needs strip-types)"; exit 1; }
|
|
37
|
+
command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH (doorbell.sh parses the FileChanged JSON with it)"; exit 1; }
|
|
38
|
+
|
|
39
|
+
fail=0
|
|
40
|
+
ok() { echo " ok $*"; }
|
|
41
|
+
bad() { echo " FAIL $*"; fail=1; }
|
|
42
|
+
|
|
43
|
+
TMP="$(mktemp -d -t psa-meta-honesty.XXXXXX)"
|
|
44
|
+
cleanup() { rm -rf "$TMP"; }
|
|
45
|
+
trap cleanup EXIT
|
|
46
|
+
|
|
47
|
+
echo "smoke-meta-honesty (store=$TMP/agent)"
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# PART A — doorbell count honesty
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
echo "[A] doorbell count (blocker #1)"
|
|
53
|
+
|
|
54
|
+
# Run the doorbell against a mailbox holding $1 .msg files; capture the stderr
|
|
55
|
+
# notice + exit code. The poked signal's dirname IS the garden mailbox.
|
|
56
|
+
doorbell_run() {
|
|
57
|
+
local n="$1" gid="$2"
|
|
58
|
+
local mb="$TMP/mb/$gid"
|
|
59
|
+
rm -rf "$mb"; mkdir -p "$mb"
|
|
60
|
+
local i
|
|
61
|
+
for ((i = 1; i <= n; i++)); do printf 'body %d\n' "$i" > "$mb/msg-$i.msg"; done
|
|
62
|
+
DOORBELL_OUT="$(echo "{\"file_path\":\"$mb/inbox.signal\"}" | bash "$DOORBELL" 2>&1)" && DOORBELL_RC=0 || DOORBELL_RC=$?
|
|
63
|
+
# Count via find (pipefail-safe: no `ls` exit-2 on an empty glob killing the run).
|
|
64
|
+
DOORBELL_DELIVERED="$(find "$mb" -maxdepth 1 -name '*.msg.delivered' | wc -l | tr -d ' ')"
|
|
65
|
+
DOORBELL_LEFT="$(find "$mb" -maxdepth 1 -name '*.msg' | wc -l | tr -d ' ')"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# 2 messages -> "2 unread mailbox messages", both delivered, exit 2 (wake)
|
|
69
|
+
doorbell_run 2 "20260606T101759-aaaaaa"
|
|
70
|
+
if echo "$DOORBELL_OUT" | grep -q "2 unread mailbox messages"; then ok "2 msgs announced as '2 ... messages'"; else bad "2 msgs NOT announced honestly: $DOORBELL_OUT"; fi
|
|
71
|
+
[ "$DOORBELL_DELIVERED" = "2" ] && ok "2 msgs ALL delivered (.delivered=$DOORBELL_DELIVERED)" || bad "2 msgs: expected 2 delivered, got $DOORBELL_DELIVERED"
|
|
72
|
+
[ "$DOORBELL_LEFT" = "0" ] && ok "no undelivered .msg left behind" || bad "$DOORBELL_LEFT .msg left undelivered (count under-reach)"
|
|
73
|
+
[ "$DOORBELL_RC" = "2" ] && ok "exit 2 (wake)" || bad "expected exit 2 (wake), got $DOORBELL_RC"
|
|
74
|
+
|
|
75
|
+
# 1 message -> honest SINGULAR. Match the full phrase (not a `\b` word boundary,
|
|
76
|
+
# which is a GNU-grep extension unreliable on BSD/macOS grep — this repo is
|
|
77
|
+
# Linux+macOS), and assert the plural form is absent.
|
|
78
|
+
doorbell_run 1 "20260606T101759-bbbbbb"
|
|
79
|
+
if echo "$DOORBELL_OUT" | grep -q "1 unread mailbox message available" && ! echo "$DOORBELL_OUT" | grep -q "1 unread mailbox messages"; then
|
|
80
|
+
ok "1 msg announced as singular '1 ... message'"
|
|
81
|
+
else
|
|
82
|
+
bad "1 msg singular/plural wrong: $DOORBELL_OUT"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# 0 messages -> no wake (exit 0), nothing delivered
|
|
86
|
+
doorbell_run 0 "20260606T101759-cccccc"
|
|
87
|
+
[ "$DOORBELL_RC" = "0" ] && ok "empty mailbox: exit 0 (no false wake)" || bad "empty mailbox should exit 0, got $DOORBELL_RC"
|
|
88
|
+
|
|
89
|
+
# BACKLOG: an earlier doorbell rang for a message the model never read (still a
|
|
90
|
+
# *.msg.delivered, which entwurf_inbox_read WILL drain) + a fresh *.msg arrives.
|
|
91
|
+
# True unread = 2. Counting only the fresh one would be "1 unread" — the same lie
|
|
92
|
+
# one layer deeper. The notice must match what the read tool returns.
|
|
93
|
+
BL="$TMP/mb/20260606T101759-dddddd"; rm -rf "$BL"; mkdir -p "$BL"
|
|
94
|
+
printf 'old unread (rung, never read)\n' > "$BL/old.msg.delivered"
|
|
95
|
+
printf 'fresh arrival\n' > "$BL/new.msg"
|
|
96
|
+
BL_OUT="$(echo "{\"file_path\":\"$BL/inbox.signal\"}" | bash "$DOORBELL" 2>&1)" && BL_RC=0 || BL_RC=$?
|
|
97
|
+
BL_DELIV="$(find "$BL" -maxdepth 1 -name '*.msg.delivered' | wc -l | tr -d ' ')"
|
|
98
|
+
if echo "$BL_OUT" | grep -q "2 unread mailbox messages"; then ok "backlog .delivered + fresh .msg counted as 2 (not 1)"; else bad "backlog count lied: $BL_OUT"; fi
|
|
99
|
+
[ "$BL_DELIV" = "2" ] && ok "backlog: both end as .msg.delivered (read tool drains 2)" || bad "backlog: expected 2 .delivered, got $BL_DELIV"
|
|
100
|
+
[ "$BL_RC" = "2" ] && ok "backlog: fresh arrival triggers wake (exit 2)" || bad "backlog: expected exit 2, got $BL_RC"
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# PART B — hook level logging
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
echo "[B] hook ERROR logging (blocker #2)"
|
|
106
|
+
|
|
107
|
+
export PI_CODING_AGENT_DIR="$TMP/agent"
|
|
108
|
+
HOOK_LOG="$TMP/agent/meta-bridge-hook.log"
|
|
109
|
+
run_hook() { echo "$1" | "$NODE_BIN" --experimental-strip-types "$HOOK" >/dev/null 2>&1 || true; }
|
|
110
|
+
|
|
111
|
+
# Normal SessionStart create -> INFO, NO ERROR (no false alarm).
|
|
112
|
+
rm -f "$HOOK_LOG"
|
|
113
|
+
run_hook "{\"hook_event_name\":\"SessionStart\",\"session_id\":\"native-ok-1\",\"transcript_path\":\"$TMP/t.jsonl\",\"cwd\":\"$TMP\"}"
|
|
114
|
+
if grep -q ' INFO .*create record' "$HOOK_LOG" 2>/dev/null; then ok "normal create logs INFO"; else bad "normal create did not log INFO ($HOOK_LOG)"; fi
|
|
115
|
+
if grep -q ' INFO sender marker ' "$HOOK_LOG" 2>/dev/null; then ok "normal create writes sender marker evidence (send-side replyable meta-session)"; else bad "normal create did not log sender marker evidence ($HOOK_LOG)"; fi
|
|
116
|
+
if grep -q ' ERROR ' "$HOOK_LOG" 2>/dev/null; then bad "normal create emitted a FALSE ERROR: $(grep ' ERROR ' "$HOOK_LOG")"; else ok "normal create emits no ERROR (no false alarm)"; fi
|
|
117
|
+
|
|
118
|
+
# Silent-miss path: duplicate nativeSessionId in the store makes scanByNativeId
|
|
119
|
+
# throw -> upsert fails -> the session is NOT a garden citizen. Must log ` ERROR `
|
|
120
|
+
# so the doctor can catch it, but must NOT scream (hook still exits 0).
|
|
121
|
+
STORE="$TMP/agent/meta-sessions"
|
|
122
|
+
REC="$(ls "$STORE"/*.meta.json | head -1)"
|
|
123
|
+
cp "$REC" "$STORE/decoy-dup.meta.json" # same nativeSessionId body, different filename -> ambiguity
|
|
124
|
+
rm -f "$HOOK_LOG"
|
|
125
|
+
echo "{\"hook_event_name\":\"SessionStart\",\"session_id\":\"native-ok-1\",\"transcript_path\":\"$TMP/t.jsonl\",\"cwd\":\"$TMP\"}" \
|
|
126
|
+
| "$NODE_BIN" --experimental-strip-types "$HOOK" >/dev/null 2>&1; HOOK_RC=$?
|
|
127
|
+
if grep -q ' ERROR .*upsert failed' "$HOOK_LOG" 2>/dev/null; then ok "duplicate nativeId logs ERROR (doctor-catchable silent miss)"; else bad "duplicate nativeId did NOT log ERROR: $(cat "$HOOK_LOG" 2>/dev/null)"; fi
|
|
128
|
+
[ "$HOOK_RC" = "0" ] && ok "hook stays best-effort (exit 0, no terminal scream) on failure" || bad "hook should exit 0 on best-effort failure, got $HOOK_RC"
|
|
129
|
+
|
|
130
|
+
# Degraded SessionStart (missing transcript_path) = the session FAILED to become a
|
|
131
|
+
# garden citizen = silent registration miss -> ERROR (not WARN), still best-effort.
|
|
132
|
+
rm -f "$HOOK_LOG"
|
|
133
|
+
echo "{\"hook_event_name\":\"SessionStart\",\"session_id\":\"native-degraded\",\"cwd\":\"$TMP\"}" \
|
|
134
|
+
| "$NODE_BIN" --experimental-strip-types "$HOOK" >/dev/null 2>&1; DEG_RC=$?
|
|
135
|
+
if grep -q ' ERROR .*degraded envelope' "$HOOK_LOG" 2>/dev/null; then ok "degraded SessionStart logs ERROR (silent miss)"; else bad "degraded SessionStart did NOT log ERROR: $(cat "$HOOK_LOG" 2>/dev/null)"; fi
|
|
136
|
+
[ "$DEG_RC" = "0" ] && ok "degraded SessionStart stays best-effort (exit 0)" || bad "degraded SessionStart should exit 0, got $DEG_RC"
|
|
137
|
+
|
|
138
|
+
# Degraded UserPromptSubmit is only a best-effort backfill, never a citizenship
|
|
139
|
+
# event -> WARN, and must NOT raise a false ERROR.
|
|
140
|
+
rm -f "$HOOK_LOG"
|
|
141
|
+
echo "{\"hook_event_name\":\"UserPromptSubmit\",\"session_id\":\"native-degraded\",\"cwd\":\"$TMP\"}" \
|
|
142
|
+
| "$NODE_BIN" --experimental-strip-types "$HOOK" >/dev/null 2>&1 || true
|
|
143
|
+
if grep -q ' WARN .*degraded envelope' "$HOOK_LOG" 2>/dev/null; then ok "degraded UserPromptSubmit logs WARN"; else bad "degraded UserPromptSubmit did NOT log WARN: $(cat "$HOOK_LOG" 2>/dev/null)"; fi
|
|
144
|
+
if grep -q ' ERROR ' "$HOOK_LOG" 2>/dev/null; then bad "degraded UserPromptSubmit raised a FALSE ERROR: $(grep ' ERROR ' "$HOOK_LOG")"; else ok "degraded UserPromptSubmit raises no false ERROR"; fi
|
|
145
|
+
|
|
146
|
+
echo
|
|
147
|
+
if [ "$fail" -eq 0 ]; then echo "smoke-meta-honesty: PASS"; else echo "smoke-meta-honesty: FAIL (see above)"; exit 1; fi
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-meta-install-state — deterministic Phase-2 gate for stateful install /
|
|
3
|
+
# uninstall / doctor hardening. Offline: no real claude CLI, no user config.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
8
|
+
export REPO
|
|
9
|
+
STATE="$REPO/scripts/meta-bridge-state.py"
|
|
10
|
+
STORE_DOCTOR="$REPO/scripts/meta-bridge-store-doctor.ts"
|
|
11
|
+
# shellcheck source=scripts/meta-bridge-hook-log.sh
|
|
12
|
+
source "$REPO/scripts/meta-bridge-hook-log.sh"
|
|
13
|
+
|
|
14
|
+
command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH"; exit 1; }
|
|
15
|
+
command -v node >/dev/null || { echo "FAIL: node not on PATH"; exit 1; }
|
|
16
|
+
|
|
17
|
+
fail=0
|
|
18
|
+
ok() { echo " ok $*"; }
|
|
19
|
+
bad() { echo " FAIL $*"; fail=1; }
|
|
20
|
+
|
|
21
|
+
TMP="$(mktemp -d -t psa-meta-install-state.XXXXXX)"
|
|
22
|
+
cleanup() { rm -rf "$TMP"; }
|
|
23
|
+
trap cleanup EXIT
|
|
24
|
+
|
|
25
|
+
export HOME="$TMP/home"
|
|
26
|
+
export CLAUDE_CONFIG_DIR="$HOME/.claude"
|
|
27
|
+
mkdir -p "$CLAUDE_CONFIG_DIR"
|
|
28
|
+
ASM="$TMP/assembled-marketplace"
|
|
29
|
+
export ASM
|
|
30
|
+
mkdir -p "$ASM"
|
|
31
|
+
|
|
32
|
+
cat > "$CLAUDE_CONFIG_DIR/settings.json" <<'JSON'
|
|
33
|
+
{
|
|
34
|
+
"cleanupPeriodDays": 30,
|
|
35
|
+
"enabledPlugins": {
|
|
36
|
+
"entwurf-meta-receive@meta-bridge-local": false,
|
|
37
|
+
"keep@user": true
|
|
38
|
+
},
|
|
39
|
+
"permissions": {
|
|
40
|
+
"allow": ["Read", "CustomUserTool", "mcp__pi-tools-bridge__*"],
|
|
41
|
+
"deny": ["Agent"]
|
|
42
|
+
},
|
|
43
|
+
"env": {
|
|
44
|
+
"DISABLE_AUTOCOMPACT": "0",
|
|
45
|
+
"KEEP_ME": "yes"
|
|
46
|
+
},
|
|
47
|
+
"statusLine": {
|
|
48
|
+
"type": "command",
|
|
49
|
+
"command": "/old/user/statusline.sh"
|
|
50
|
+
},
|
|
51
|
+
"promptSuggestionEnabled": true,
|
|
52
|
+
"skipDangerousModePermissionPrompt": false,
|
|
53
|
+
"showTurnDuration": true
|
|
54
|
+
}
|
|
55
|
+
JSON
|
|
56
|
+
cat > "$HOME/.claude.json" <<'JSON'
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"entwurf-bridge": {
|
|
60
|
+
"type": "stdio",
|
|
61
|
+
"command": "old",
|
|
62
|
+
"args": ["old-start.sh"]
|
|
63
|
+
},
|
|
64
|
+
"keep-server": {
|
|
65
|
+
"type": "stdio",
|
|
66
|
+
"command": "keep"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
JSON
|
|
71
|
+
|
|
72
|
+
py() { python3 "$STATE" "$@" --repo "$REPO" --asm "$ASM"; }
|
|
73
|
+
|
|
74
|
+
py prepare >/dev/null
|
|
75
|
+
STATE_FILE="$CLAUDE_CONFIG_DIR/entwurf.install-state.json"
|
|
76
|
+
[ -f "$STATE_FILE" ] && ok "prepare writes install-state before any merge" || bad "state file missing after prepare"
|
|
77
|
+
if python3 - <<'PY'
|
|
78
|
+
import json, os, stat
|
|
79
|
+
path=os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
|
|
80
|
+
assert stat.S_IMODE(os.stat(path).st_mode) == 0o600
|
|
81
|
+
s=json.load(open(path))
|
|
82
|
+
assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['value'] is False
|
|
83
|
+
assert s['files']['settings']['keys']['cleanupPeriodDays']['original']['value'] == 30
|
|
84
|
+
assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['value'] == '0'
|
|
85
|
+
assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
|
|
86
|
+
assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
|
|
87
|
+
assert s['files']['settings']['keys']['autoCompactEnabled']['original']['existed'] is False
|
|
88
|
+
assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['value']['command'] == 'old'
|
|
89
|
+
PY
|
|
90
|
+
then ok "state captures original scalar/map values and is mode 0600"; else bad "state did not capture original values / mode 0600"; fi
|
|
91
|
+
|
|
92
|
+
py apply >/dev/null
|
|
93
|
+
if python3 - <<'PY'
|
|
94
|
+
import json, os
|
|
95
|
+
cfg=os.environ['CLAUDE_CONFIG_DIR']
|
|
96
|
+
settings=json.load(open(cfg + '/settings.json'))
|
|
97
|
+
root=json.load(open(os.environ['HOME'] + '/.claude.json'))
|
|
98
|
+
assert settings['enabledPlugins']['entwurf-meta-receive@meta-bridge-local'] is True
|
|
99
|
+
assert settings['enabledPlugins']['keep@user'] is True
|
|
100
|
+
assert settings['extraKnownMarketplaces']['meta-bridge-local']['source']['path'] == os.environ['ASM']
|
|
101
|
+
assert settings['cleanupPeriodDays'] == 365
|
|
102
|
+
assert settings['env']['DISABLE_AUTOCOMPACT'] == '1'
|
|
103
|
+
assert settings['env']['KEEP_ME'] == 'yes'
|
|
104
|
+
assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
|
|
105
|
+
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan']:
|
|
106
|
+
assert settings[key] is False, key
|
|
107
|
+
assert settings['skipDangerousModePermissionPrompt'] is True
|
|
108
|
+
for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
|
|
109
|
+
assert item in settings['permissions']['allow'], item
|
|
110
|
+
assert settings['permissions']['allow'].count('Read') == 1
|
|
111
|
+
# S2 rename cutover: the legacy pre-rename allow item is pruned, but an unrelated
|
|
112
|
+
# user-authored allow item survives (prune is targeted, not a blanket wipe).
|
|
113
|
+
assert 'mcp__pi-tools-bridge__*' not in settings['permissions']['allow']
|
|
114
|
+
assert 'CustomUserTool' in settings['permissions']['allow']
|
|
115
|
+
assert 'keep-server' in root['mcpServers']
|
|
116
|
+
assert root['mcpServers']['entwurf-bridge']['env']['ENTWURF_BRIDGE_EXTERNAL_AGENT_ID'] == 'external-mcp/claude-code'
|
|
117
|
+
PY
|
|
118
|
+
then ok "apply installs managed keyset without clobbering unrelated keys"; else bad "apply keyset check failed"; fi
|
|
119
|
+
|
|
120
|
+
# Keyset-survival guard (B1): once installed, `state.py check` is the
|
|
121
|
+
# effect-based survival assertion doctor consumes. If another consumer
|
|
122
|
+
# (agent-config merge, hand edit) overwrites a pi-owned key, check must fail loud
|
|
123
|
+
# AND name the drifted key so doctor can surface which one. Adversarial flips
|
|
124
|
+
# below; restore with apply afterward so the later cases see a clean keyset.
|
|
125
|
+
if py check >/dev/null 2>&1; then ok "survival check passes on a freshly applied keyset"; else bad "survival check failed right after apply"; fi
|
|
126
|
+
SURVIVAL_SNAP="$TMP/settings-survival-snapshot.json"
|
|
127
|
+
cp "$CLAUDE_CONFIG_DIR/settings.json" "$SURVIVAL_SNAP" # exact restore point (array-replace below would drop user items)
|
|
128
|
+
python3 - <<'PY'
|
|
129
|
+
import json, os
|
|
130
|
+
p=os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
|
|
131
|
+
d=json.load(open(p)); d['verbose']=True # a consumer/user clobbers a pi scalar
|
|
132
|
+
json.dump(d, open(p,'w'), indent=2)
|
|
133
|
+
PY
|
|
134
|
+
ERR_SCALAR="$(py check 2>&1 >/dev/null || true)"
|
|
135
|
+
if printf '%s' "$ERR_SCALAR" | grep -q 'verbose'; then ok "survival check fails and names an overwritten pi scalar (verbose)"; else bad "survival check did not name clobbered scalar: $ERR_SCALAR"; fi
|
|
136
|
+
python3 - <<'PY'
|
|
137
|
+
import json, os
|
|
138
|
+
p=os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
|
|
139
|
+
d=json.load(open(p)); d['verbose']=False; d['permissions']['allow']=['OnlyUserTool'] # array REPLACE drops pi items
|
|
140
|
+
json.dump(d, open(p,'w'), indent=2)
|
|
141
|
+
PY
|
|
142
|
+
ERR_ARRAY="$(py check 2>&1 >/dev/null || true)"
|
|
143
|
+
if printf '%s' "$ERR_ARRAY" | grep -qi 'allow'; then ok "survival check fails when permissions.allow is array-replaced (pi items dropped)"; else bad "survival check did not catch dropped permissions.allow items: $ERR_ARRAY"; fi
|
|
144
|
+
cp "$SURVIVAL_SNAP" "$CLAUDE_CONFIG_DIR/settings.json" # back to clean keyset before the legacy-reinject case
|
|
145
|
+
python3 - <<'PY'
|
|
146
|
+
import json, os
|
|
147
|
+
p=os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
|
|
148
|
+
d=json.load(open(p)); d['permissions']['allow'].append('mcp__pi-tools-bridge__*') # someone re-injects the pruned legacy item
|
|
149
|
+
json.dump(d, open(p,'w'), indent=2)
|
|
150
|
+
PY
|
|
151
|
+
ERR_LEGACY="$(py check 2>&1 >/dev/null || true)"
|
|
152
|
+
if printf '%s' "$ERR_LEGACY" | grep -qi 'legacy'; then ok "survival check fails when a pruned legacy allow item is re-injected"; else bad "survival check did not catch re-injected legacy allow item: $ERR_LEGACY"; fi
|
|
153
|
+
cp "$SURVIVAL_SNAP" "$CLAUDE_CONFIG_DIR/settings.json" # exact restore so later cases see the clean installed keyset
|
|
154
|
+
if py check >/dev/null 2>&1; then ok "survival check passes again after restoring the keyset"; else bad "keyset not restored after adversarial survival cases"; fi
|
|
155
|
+
|
|
156
|
+
# Re-run prepare/apply after install: the original snapshot must remain the
|
|
157
|
+
# pre-install values, not the already-managed values.
|
|
158
|
+
py prepare >/dev/null
|
|
159
|
+
py apply >/dev/null
|
|
160
|
+
if python3 - <<'PY'
|
|
161
|
+
import json, os
|
|
162
|
+
s=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'))
|
|
163
|
+
assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['value'] is False
|
|
164
|
+
assert s['files']['settings']['keys']['cleanupPeriodDays']['original']['value'] == 30
|
|
165
|
+
assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['value'] == '0'
|
|
166
|
+
assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
|
|
167
|
+
assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
|
|
168
|
+
PY
|
|
169
|
+
then ok "rerun preserves first pre-install snapshot"; else bad "rerun overwrote original snapshot"; fi
|
|
170
|
+
|
|
171
|
+
# User changes after install: array additions should survive uninstall; scalar
|
|
172
|
+
# managed toggles restore the original value by design.
|
|
173
|
+
python3 - <<'PY'
|
|
174
|
+
import json, os
|
|
175
|
+
p=os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
|
|
176
|
+
d=json.load(open(p))
|
|
177
|
+
d['permissions']['allow'].append('UserAfterInstall')
|
|
178
|
+
d['permissions']['deny'].append('UserDeniedAfterInstall')
|
|
179
|
+
json.dump(d, open(p,'w'), indent=2); open(p,'a').write('\n')
|
|
180
|
+
PY
|
|
181
|
+
py uninstall >/dev/null
|
|
182
|
+
if python3 - <<'PY'
|
|
183
|
+
import json, os
|
|
184
|
+
settings=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'))
|
|
185
|
+
root=json.load(open(os.environ['HOME'] + '/.claude.json'))
|
|
186
|
+
assert settings['cleanupPeriodDays'] == 30
|
|
187
|
+
assert settings['enabledPlugins']['entwurf-meta-receive@meta-bridge-local'] is False
|
|
188
|
+
assert 'meta-bridge-local' not in settings.get('extraKnownMarketplaces', {})
|
|
189
|
+
assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
|
|
190
|
+
assert settings['env']['KEEP_ME'] == 'yes'
|
|
191
|
+
assert settings['statusLine']['command'] == '/old/user/statusline.sh'
|
|
192
|
+
assert settings['promptSuggestionEnabled'] is True
|
|
193
|
+
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
194
|
+
assert settings['showTurnDuration'] is True
|
|
195
|
+
for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','terminalProgressBarEnabled','useAutoModeDuringPlan']:
|
|
196
|
+
assert key not in settings, key
|
|
197
|
+
allow=settings['permissions']['allow']
|
|
198
|
+
deny=settings['permissions']['deny']
|
|
199
|
+
assert 'CustomUserTool' in allow and 'UserAfterInstall' in allow
|
|
200
|
+
assert 'Bash' not in allow and 'mcp__entwurf-bridge__*' not in allow
|
|
201
|
+
assert 'Agent' in deny and 'UserDeniedAfterInstall' in deny and 'TaskCreate' not in deny
|
|
202
|
+
assert root['mcpServers']['entwurf-bridge']['command'] == 'old'
|
|
203
|
+
assert 'keep-server' in root['mcpServers']
|
|
204
|
+
assert not os.path.exists(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json')
|
|
205
|
+
PY
|
|
206
|
+
then ok "uninstall restores scalars/maps and removes only managed array additions"; else bad "uninstall restoration check failed"; fi
|
|
207
|
+
|
|
208
|
+
if py uninstall >/dev/null 2>&1; then bad "state manager uninstall without state should not guess"; else ok "state manager uninstall without state fails instead of guessing"; fi
|
|
209
|
+
|
|
210
|
+
# The wrapper must ALSO refuse before side effects. A fake claude records whether
|
|
211
|
+
# plugin/MCP removals were attempted; no state means the log must stay empty.
|
|
212
|
+
FAKE_BIN="$TMP/fake-bin"; mkdir -p "$FAKE_BIN"
|
|
213
|
+
cat > "$FAKE_BIN/claude" <<'SH'
|
|
214
|
+
#!/usr/bin/env bash
|
|
215
|
+
printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
|
|
216
|
+
exit 0
|
|
217
|
+
SH
|
|
218
|
+
chmod +x "$FAKE_BIN/claude"
|
|
219
|
+
FAKE_CLAUDE_LOG="$TMP/fake-claude-nostate.log"
|
|
220
|
+
if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
|
|
221
|
+
bad "wrapper uninstall without state should fail"
|
|
222
|
+
else
|
|
223
|
+
if [ ! -s "$FAKE_CLAUDE_LOG" ]; then ok "wrapper uninstall without state has zero Claude side effects"; else bad "wrapper uninstall without state touched Claude: $(cat "$FAKE_CLAUDE_LOG")"; fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# With valid state, the wrapper may remove Claude registrations and then restore
|
|
227
|
+
# JSON state. This proves the preflight gate is ordered before side effects.
|
|
228
|
+
py prepare >/dev/null
|
|
229
|
+
py apply >/dev/null
|
|
230
|
+
FAKE_CLAUDE_LOG="$TMP/fake-claude-state.log"
|
|
231
|
+
if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
|
|
232
|
+
if grep -q 'plugin uninstall entwurf-meta-receive@meta-bridge-local' "$FAKE_CLAUDE_LOG" && grep -q 'mcp remove entwurf-bridge -s user' "$FAKE_CLAUDE_LOG" && [ ! -f "$STATE_FILE" ]; then
|
|
233
|
+
ok "wrapper uninstall with valid state removes Claude registrations and restores state"
|
|
234
|
+
else
|
|
235
|
+
bad "wrapper uninstall with state missed expected side effects/state removal"
|
|
236
|
+
fi
|
|
237
|
+
else
|
|
238
|
+
bad "wrapper uninstall with valid state failed"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# Doctor hook-log recovery predicate: only a later `INFO armed watch` clears an
|
|
242
|
+
# ERROR. A UserPromptSubmit `INFO attach record` is degraded backfill, not wake
|
|
243
|
+
# recovery.
|
|
244
|
+
HOOK_SYN="$TMP/hook-log"
|
|
245
|
+
printf '2026-06-06T00:00:00.000Z ERROR arm failed\n' > "$HOOK_SYN"
|
|
246
|
+
if meta_bridge_hook_log_status "$HOOK_SYN" >/dev/null 2>&1; then bad "hook-log predicate: ERROR-only should fail"; else ok "hook-log predicate fails on ERROR-only"; fi
|
|
247
|
+
printf '2026-06-06T00:00:00.000Z ERROR arm failed\n2026-06-06T00:00:01.000Z INFO armed watch /tmp/inbox.signal\n' > "$HOOK_SYN"
|
|
248
|
+
if meta_bridge_hook_log_status "$HOOK_SYN" >/dev/null; then ok "hook-log predicate recovers on later INFO armed watch"; else bad "hook-log predicate did not recover on armed watch"; fi
|
|
249
|
+
printf '2026-06-06T00:00:00.000Z INFO armed watch /tmp/inbox.signal\n2026-06-06T00:00:01.000Z ERROR arm failed\n' > "$HOOK_SYN"
|
|
250
|
+
if meta_bridge_hook_log_status "$HOOK_SYN" >/dev/null 2>&1; then bad "hook-log predicate: ERROR after armed watch should fail"; else ok "hook-log predicate fails when ERROR is newer than armed watch"; fi
|
|
251
|
+
printf '2026-06-06T00:00:00.000Z ERROR arm failed\n2026-06-06T00:00:01.000Z INFO attach record x.meta.json (event=UserPromptSubmit, native=n)\n' > "$HOOK_SYN"
|
|
252
|
+
if meta_bridge_hook_log_status "$HOOK_SYN" >/dev/null 2>&1; then bad "hook-log predicate: UserPromptSubmit attach must not recover"; else ok "hook-log predicate does not treat UserPromptSubmit attach as recovery"; fi
|
|
253
|
+
printf '2026-06-06T00:00:00.000Z INFO attach record x.meta.json (event=UserPromptSubmit, native=n)\n' > "$HOOK_SYN"
|
|
254
|
+
if meta_bridge_hook_log_status "$HOOK_SYN" >/dev/null; then ok "hook-log predicate passes when no ERROR exists"; else bad "hook-log predicate failed no-ERROR log"; fi
|
|
255
|
+
|
|
256
|
+
# Migration: the Phase-0/1 installer may already have left exact pi-owned keys in
|
|
257
|
+
# place before a state file existed. Exact managed plugin/marketplace/MCP values
|
|
258
|
+
# are treated as legacy-owned-by-us (absent original), so uninstall removes them
|
|
259
|
+
# instead of restoring the tribal install forever.
|
|
260
|
+
python3 - <<'PY'
|
|
261
|
+
import json, os
|
|
262
|
+
cfg=os.environ['CLAUDE_CONFIG_DIR']; home=os.environ['HOME']; asm=os.environ['ASM']; repo=os.environ['REPO']
|
|
263
|
+
settings={
|
|
264
|
+
'enabledPlugins': {'entwurf-meta-receive@meta-bridge-local': True},
|
|
265
|
+
'extraKnownMarketplaces': {'meta-bridge-local': {'source': {'source': 'directory', 'path': asm}}},
|
|
266
|
+
'permissions': {'allow': ['Read'], 'deny': ['Agent']},
|
|
267
|
+
'env': {'DISABLE_AUTOCOMPACT': '1'}
|
|
268
|
+
}
|
|
269
|
+
root={'mcpServers': {'entwurf-bridge': {'type':'stdio','command':'bash','args':[repo + '/mcp/entwurf-bridge/start.sh'],'env': {'ENTWURF_BRIDGE_EXTERNAL_AGENT_ID':'external-mcp/claude-code','ENTWURF_BRIDGE_REQUIRE_META_SENDER':'1'}}}}
|
|
270
|
+
json.dump(settings, open(cfg + '/settings.json','w'), indent=2); open(cfg + '/settings.json','a').write('\n')
|
|
271
|
+
json.dump(root, open(home + '/.claude.json','w'), indent=2); open(home + '/.claude.json','a').write('\n')
|
|
272
|
+
PY
|
|
273
|
+
py prepare >/dev/null
|
|
274
|
+
if python3 - <<'PY'
|
|
275
|
+
import json, os
|
|
276
|
+
s=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'))
|
|
277
|
+
assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['existed'] is False
|
|
278
|
+
assert s['files']['settings']['keys']['extraKnownMarketplaces.meta-bridge-local']['original']['existed'] is False
|
|
279
|
+
assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['existed'] is False
|
|
280
|
+
assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['existed'] is True
|
|
281
|
+
PY
|
|
282
|
+
then ok "legacy exact plugin/marketplace/MCP migrate as pi-owned absent, policy keys remain user-owned"; else bad "legacy migration state check failed"; fi
|
|
283
|
+
py uninstall >/dev/null
|
|
284
|
+
if python3 - <<'PY'
|
|
285
|
+
import json, os
|
|
286
|
+
settings=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'))
|
|
287
|
+
root=json.load(open(os.environ['HOME'] + '/.claude.json'))
|
|
288
|
+
assert 'entwurf-meta-receive@meta-bridge-local' not in settings.get('enabledPlugins', {})
|
|
289
|
+
assert 'meta-bridge-local' not in settings.get('extraKnownMarketplaces', {})
|
|
290
|
+
assert 'entwurf-bridge' not in root.get('mcpServers', {})
|
|
291
|
+
assert settings['env']['DISABLE_AUTOCOMPACT'] == '1'
|
|
292
|
+
PY
|
|
293
|
+
then ok "legacy migration uninstall removes pi-owned wiring but preserves policy scalar"; else bad "legacy migration uninstall check failed"; fi
|
|
294
|
+
|
|
295
|
+
# Store-doctor hardening: valid store passes, corrupt/duplicate/drift fail loudly.
|
|
296
|
+
valid_record() {
|
|
297
|
+
local gid="$1" native="$2"
|
|
298
|
+
cat <<JSON
|
|
299
|
+
{
|
|
300
|
+
"schemaVersion": 1,
|
|
301
|
+
"gardenId": "$gid",
|
|
302
|
+
"backend": "claude-code",
|
|
303
|
+
"nativeSessionId": "$native",
|
|
304
|
+
"transcriptPath": "/tmp/$native.jsonl",
|
|
305
|
+
"cwd": "/tmp",
|
|
306
|
+
"createdAt": "2026-06-06T00:00:00.000Z",
|
|
307
|
+
"lastSeen": "2026-06-06T00:00:00.000Z",
|
|
308
|
+
"delivery": {
|
|
309
|
+
"wakeMode": "self-fetch",
|
|
310
|
+
"deliveryLevel": "D6",
|
|
311
|
+
"lastEnqueuedAt": null,
|
|
312
|
+
"lastDeliveredAt": null,
|
|
313
|
+
"lastReadAt": null
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
JSON
|
|
317
|
+
}
|
|
318
|
+
STORE="$TMP/store"; export STORE; mkdir -p "$STORE"
|
|
319
|
+
valid_record "20260606T000000-aaaaaa" "native-a" > "$STORE/20260606T000000-aaaaaa.meta.json"
|
|
320
|
+
valid_record "20260606T000001-bbbbbb" "native-b" > "$STORE/20260606T000001-bbbbbb.meta.json"
|
|
321
|
+
|
|
322
|
+
STATUS_INPUT_MATCH='{"session_id":"native-a","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-4-6"},"context_window":{"context_window_size":200000,"used_percentage":2,"current_usage":{"input_tokens":10}}}'
|
|
323
|
+
STATUS_INPUT_MISS='{"session_id":"native-missing","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-opus-4-8"}}'
|
|
324
|
+
STATUS_INPUT_READY='{"workspace":{"current_dir":"/tmp"},"model":{"id":"claude-haiku-4-5"}}'
|
|
325
|
+
STATUS_OUT_MATCH="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
326
|
+
if [ "$(printf '%s\n' "$STATUS_OUT_MATCH" | wc -l | tr -d ' ')" = "2" ]; then ok "statusline renders exactly two rows"; else bad "statusline should render two rows: $STATUS_OUT_MATCH"; fi
|
|
327
|
+
if printf '%s\n' "$STATUS_OUT_MATCH" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$STATUS_OUT_MATCH" | sed -n '2p' | grep -q '🪛 20260606T000000-aaaaaa cc | s'; then ok "statusline keeps row-1 work context and maps native session_id to row-2 garden-id"; else bad "statusline did not show expected two-row content for native-a: $STATUS_OUT_MATCH"; fi
|
|
328
|
+
STATUS_OUT_MISS="$(printf '%s' "$STATUS_INPUT_MISS" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
329
|
+
if printf '%s' "$STATUS_OUT_MISS" | grep -q '🪛 ? cc'; then ok "statusline no-record fallback is ?"; else bad "statusline no-record fallback wrong: $STATUS_OUT_MISS"; fi
|
|
330
|
+
STATUS_OUT_READY="$(printf '%s' "$STATUS_INPUT_READY" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
331
|
+
if printf '%s' "$STATUS_OUT_READY" | grep -q '🪛 ready cc'; then ok "statusline no-session_id fallback is ready"; else bad "statusline ready fallback wrong: $STATUS_OUT_READY"; fi
|
|
332
|
+
cp "$STORE/20260606T000000-aaaaaa.meta.json" "$STORE/20260606T000003-dddddd.meta.json"
|
|
333
|
+
STATUS_OUT_DUP="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
334
|
+
if printf '%s' "$STATUS_OUT_DUP" | grep -q '🪛 ! cc'; then ok "statusline duplicate nativeSessionId fallback is !"; else bad "statusline duplicate fallback wrong: $STATUS_OUT_DUP"; fi
|
|
335
|
+
rm "$STORE/20260606T000003-dddddd.meta.json"
|
|
336
|
+
|
|
337
|
+
if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null; then ok "store doctor accepts valid records"; else bad "store doctor rejected valid records"; fi
|
|
338
|
+
cp "$STORE/20260606T000000-aaaaaa.meta.json" "$STORE/20260606T000002-cccccc.meta.json"
|
|
339
|
+
if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null 2>&1; then bad "store doctor missed body/filename drift + duplicate"; else ok "store doctor fails on body/filename drift and duplicate nativeSessionId"; fi
|
|
340
|
+
rm "$STORE/20260606T000002-cccccc.meta.json"
|
|
341
|
+
python3 - <<'PY'
|
|
342
|
+
import json, os
|
|
343
|
+
p=os.environ['STORE'] + '/20260606T000001-bbbbbb.meta.json'
|
|
344
|
+
d=json.load(open(p)); d['delivery']['wakeMode']='direct-inject'; json.dump(d, open(p,'w'), indent=2); open(p,'a').write('\n')
|
|
345
|
+
PY
|
|
346
|
+
if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null 2>&1; then bad "store doctor missed backend↔wakeMode contradiction"; else ok "store doctor fails on backend↔wakeMode contradiction"; fi
|
|
347
|
+
|
|
348
|
+
# Doctor fail-loud regression (B1'): meta-bridge-doctor.sh must surface a managed-
|
|
349
|
+
# config drift, NOT die silently at the "[managed config state]" header. A bare
|
|
350
|
+
# CHECK_ERR="$(state.py check)" assignment under `set -e` exited the doctor the
|
|
351
|
+
# instant check returned nonzero — so the operator saw the header and nothing
|
|
352
|
+
# else; the very "which key drifted" detail the section exists to print was lost,
|
|
353
|
+
# and every later section ([plugin install], [meta-record store], the SILENT-MISS
|
|
354
|
+
# guard) never ran. The fix wraps the substitution as an `if` condition so its
|
|
355
|
+
# nonzero status is consumed instead of tripping set -e. This gate forces a real
|
|
356
|
+
# state drift behind a fully-faked claude toolchain (no real claude, no real
|
|
357
|
+
# install-meta-bridge) and proves the doctor (a) still exits 1, (b) prints
|
|
358
|
+
# "Drift detail:", (c) names the concrete drifted key, and (d) runs the WHOLE
|
|
359
|
+
# chain to its final summary line — i.e. no early set -e death anywhere.
|
|
360
|
+
DOC_HOME="$TMP/doctor-home"; DOC_CFG="$DOC_HOME/.claude"
|
|
361
|
+
DOC_AGENT="$TMP/doctor-agent"; DOC_STORE="$DOC_AGENT/meta-sessions"
|
|
362
|
+
DOC_BIN="$TMP/doctor-bin"; DOC_ASM="$REPO/pi/meta-bridge/.assembled" # SAME asm the doctor's check uses, so only the MCP drifts
|
|
363
|
+
mkdir -p "$DOC_CFG" "$DOC_STORE" "$DOC_BIN"
|
|
364
|
+
echo '{}' > "$DOC_CFG/settings.json"
|
|
365
|
+
echo '{}' > "$DOC_HOME/.claude.json"
|
|
366
|
+
# A healthy claude-code meta-record so the SessionStart-evidence section is green
|
|
367
|
+
# and the ONLY failure of record is the deliberate MCP drift.
|
|
368
|
+
valid_record "20260606T120000-eeeeee" "doctor-native" > "$DOC_STORE/20260606T120000-eeeeee.meta.json"
|
|
369
|
+
# Fake claude: a healthy toolchain so the doctor sails past every other section.
|
|
370
|
+
cat > "$DOC_BIN/claude" <<'SH'
|
|
371
|
+
#!/usr/bin/env bash
|
|
372
|
+
case "$1${2:+ $2}" in
|
|
373
|
+
"--version") echo "2.1.167 (Claude Code)" ;;
|
|
374
|
+
"plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
|
|
375
|
+
"mcp get") printf '%s\n' "Scope: User config" "Status: ✔ Connected" ;;
|
|
376
|
+
*) : ;;
|
|
377
|
+
esac
|
|
378
|
+
exit 0
|
|
379
|
+
SH
|
|
380
|
+
chmod +x "$DOC_BIN/claude"
|
|
381
|
+
# Build a valid install-state + managed keyset, then drift ONLY the user MCP.
|
|
382
|
+
env HOME="$DOC_HOME" CLAUDE_CONFIG_DIR="$DOC_CFG" python3 "$STATE" prepare --repo "$REPO" --asm "$DOC_ASM" >/dev/null
|
|
383
|
+
env HOME="$DOC_HOME" CLAUDE_CONFIG_DIR="$DOC_CFG" python3 "$STATE" apply --repo "$REPO" --asm "$DOC_ASM" >/dev/null
|
|
384
|
+
env HOME="$DOC_HOME" python3 - <<'PY'
|
|
385
|
+
import json, os
|
|
386
|
+
p = os.path.join(os.environ['HOME'], '.claude.json')
|
|
387
|
+
d = json.load(open(p))
|
|
388
|
+
d.setdefault('mcpServers', {})['entwurf-bridge'] = {'type': 'stdio', 'command': 'CLOBBERED-BY-ANOTHER-CONSUMER'}
|
|
389
|
+
json.dump(d, open(p, 'w'), indent=2)
|
|
390
|
+
PY
|
|
391
|
+
# Capture without tripping THIS script's own set -e (the very trap under test).
|
|
392
|
+
set +e
|
|
393
|
+
DOC_OUT="$(env HOME="$DOC_HOME" CLAUDE_CONFIG_DIR="$DOC_CFG" PI_CODING_AGENT_DIR="$DOC_AGENT" ENTWURF_META_SESSIONS_DIR="$DOC_STORE" PATH="$DOC_BIN:$PATH" bash "$REPO/scripts/meta-bridge-doctor.sh" 2>&1)"
|
|
394
|
+
DOC_CODE=$?
|
|
395
|
+
set -e
|
|
396
|
+
if [ "$DOC_CODE" -eq 1 ]; then ok "doctor exits 1 on a managed-config drift"; else bad "doctor exit on drift was $DOC_CODE, want 1:"$'\n'"$DOC_OUT"; fi
|
|
397
|
+
if printf '%s\n' "$DOC_OUT" | grep -q 'Drift detail:'; then ok "doctor prints 'Drift detail:' instead of dying at the managed-config header"; else bad "doctor did not print Drift detail — silent early death?:"$'\n'"$DOC_OUT"; fi
|
|
398
|
+
if printf '%s\n' "$DOC_OUT" | grep -q 'user MCP entwurf-bridge'; then ok "doctor names the concrete drifted key (user MCP entwurf-bridge)"; else bad "doctor did not name the drifted MCP key:"$'\n'"$DOC_OUT"; fi
|
|
399
|
+
if printf '%s\n' "$DOC_OUT" | grep -q '\[plugin install'; then ok "doctor continues to a later section after the drift (no early set -e death)"; else bad "doctor did not reach a later section header after the drift:"$'\n'"$DOC_OUT"; fi
|
|
400
|
+
if printf '%s\n' "$DOC_OUT" | grep -q 'meta-bridge doctor: FAIL'; then ok "doctor runs the whole chain to its final summary line"; else bad "doctor did not reach its final summary line (mid-run death):"$'\n'"$DOC_OUT"; fi
|
|
401
|
+
|
|
402
|
+
echo
|
|
403
|
+
if [ "$fail" -eq 0 ]; then echo "smoke-meta-install-state: PASS"; else echo "smoke-meta-install-state: FAIL (see above)"; exit 1; fi
|