@junghanacs/entwurf 0.16.1 → 0.17.2
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 +4 -2
- package/CHANGELOG.md +333 -0
- package/VERIFY.md +8 -1
- package/docs/acp-backend-rail.md +25 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +18 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +23 -7
- package/package.json +4 -4
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
- package/pi-extensions/lib/acp/acp-client.ts +57 -4
- package/pi-extensions/lib/acp/backend-adapter.ts +59 -0
- package/pi-extensions/lib/acp/backend.ts +453 -11
- package/pi-extensions/lib/acp/event-mapper.ts +43 -6
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
- package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +18 -3
- package/pi-extensions/lib/meta-session.ts +32 -7
- package/run.sh +21 -0
- package/scripts/check-acp-stop-reason.ts +8 -2
- package/scripts/check-acp-usage-accounting.ts +1074 -0
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- package/scripts/check-entwurf-v2-runner.ts +18 -0
- package/scripts/check-entwurf-v2-send.ts +13 -1
- package/scripts/check-entwurf-v2-surface.ts +75 -3
- package/scripts/check-gate-qualification.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +70 -1
- package/scripts/check-mailbox-receipt-state.ts +6 -0
- package/scripts/check-meta-doctor-oracle.sh +73 -0
- package/scripts/check-meta-mailbox-state-write.ts +9 -2
- package/scripts/check-meta-manifest-schema.py +19 -1
- package/scripts/check-meta-session.ts +10 -2
- package/scripts/lib/pi-record-discovery.ts +47 -0
- package/scripts/meta-bridge-doctor.sh +18 -3
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/acp-stream-hooks.json +4 -2
- package/scripts/mutants/acp-usage-accounting.json +181 -0
- package/scripts/raw-async-delivery/README.md +280 -9
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
- package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
- package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
- package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +2 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
- package/scripts/smoke-meta-async-drift.sh +31 -8
- package/scripts/smoke-meta-install-state.sh +170 -11
- package/scripts/smoke-meta-keyset-guard.sh +4 -1
- package/scripts/smoke-mux-lifecycle-live.ts +46 -2
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# probe-delivery-transparency.sh — issue #98 Phase 1, probes P1 + P2a + P2b.
|
|
3
|
+
#
|
|
4
|
+
# MEASUREMENT ONLY -- but read the next paragraph before believing that phrase.
|
|
5
|
+
#
|
|
6
|
+
# It edits no product FILE: nothing under pi/meta-bridge/**, pi-extensions/**,
|
|
7
|
+
# mcp/** or scripts/meta-bridge-*.sh, and it installs nothing.
|
|
8
|
+
#
|
|
9
|
+
# Isolation from operator STATE took two tries to get right, and both failures are
|
|
10
|
+
# worth carrying:
|
|
11
|
+
#
|
|
12
|
+
# 1. Without `--setting-sources project,local` the session inherits
|
|
13
|
+
# ~/.claude/settings.json, so the PRODUCT meta-bridge plugin loads next to the
|
|
14
|
+
# lab one and its SessionStart hook mints a REAL garden citizen (a record in
|
|
15
|
+
# ~/.pi/agent/meta-sessions/, a mailbox in ~/.pi/agent/meta-mailbox/).
|
|
16
|
+
# MEASURED: three runs left three citizens behind.
|
|
17
|
+
# 2. Worse, the two doorbells DO cross-fire, and an earlier version of this
|
|
18
|
+
# header claimed they did not. `doorbell.sh` takes `dirname(file_path)` as
|
|
19
|
+
# "its" mailbox unconditionally, so the product doorbell happily processed the
|
|
20
|
+
# LAB mailbox and raced the lab hook to exit 2. When the product hook won, the
|
|
21
|
+
# operator got the DEFAULT "Stop hook feedback" -- from a hook that carries no
|
|
22
|
+
# rewakeSummary -- while hooks.json under test was perfectly correct.
|
|
23
|
+
# MEASURED: two consecutive runs failed P1 that way. The transcript proved it:
|
|
24
|
+
# product wording ("[entwurf inbox] ... entwurf_inbox_read ... lastReadAt")
|
|
25
|
+
# pointing at the lab path /tmp/cc-p98-probe/mailbox/<sid>/.
|
|
26
|
+
#
|
|
27
|
+
# So P1 was non-deterministic until the setting sources were cut. Dropping user
|
|
28
|
+
# settings fixes both: no product plugin, no minted citizen, no doorbell race.
|
|
29
|
+
# `cleanup_citizens` stays as a belt-and-braces sweep for anything a run under the
|
|
30
|
+
# old flags left behind; it deletes only records whose cwd matches this probe's
|
|
31
|
+
# throwaway /tmp path, and reports rather than removes a non-empty mailbox.
|
|
32
|
+
#
|
|
33
|
+
# Claude still writes ~/.claude/projects/<cwd>/<sid>.jsonl and
|
|
34
|
+
# ~/.claude/sessions/<pid>.json of its own accord -- receipt (ii) below IS one of
|
|
35
|
+
# those files. That is Claude's own bookkeeping and is left alone.
|
|
36
|
+
#
|
|
37
|
+
# WHAT EACH PROBE DECIDES
|
|
38
|
+
# P1 hooks.json `rewakeSummary` / `rewakeMessage` on an asyncRewake hook:
|
|
39
|
+
# does the operator's row stop saying "Stop hook feedback", and does the
|
|
40
|
+
# model stop being told "Stop hook blocking error"? Binary reads say both
|
|
41
|
+
# fields are ungated for a local plugin (only the stdout-JSON rewakeSummary
|
|
42
|
+
# is first-party gated). This is the live confirmation, and the receipt
|
|
43
|
+
# that retires README lesson #4.
|
|
44
|
+
# P2a Does Claude RE-EXECUTE the statusline command on the turn an asyncRewake
|
|
45
|
+
# doorbell creates? If not, a statusline unread badge cannot be the primary
|
|
46
|
+
# surface.
|
|
47
|
+
# P2b Does it re-execute again after a MID-TURN tool call drains the mailbox?
|
|
48
|
+
# If not, the badge would keep showing unread after the model already read
|
|
49
|
+
# -- the badge would lie in the direction that matters, so option B would
|
|
50
|
+
# WEAKEN the doorbell-rang-vs-model-read distinction instead of showing it.
|
|
51
|
+
#
|
|
52
|
+
# P2a/P2b need no badge on screen: lab-statusline.sh logs one line per
|
|
53
|
+
# invocation with the count it observed, so the log alone answers both.
|
|
54
|
+
#
|
|
55
|
+
# COST: one interactive subscription session; the wake itself is a continuation,
|
|
56
|
+
# not a `claude -p` spawn.
|
|
57
|
+
# USAGE: ./probe-delivery-transparency.sh [keep] (`keep` leaves tmux alive)
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
|
+
PLUGIN="$HERE/plugin-entwurf-receive"
|
|
62
|
+
T="${CC_PROBE_DIR:-/tmp/cc-p98-probe}"
|
|
63
|
+
ROOT="$T/mailbox"
|
|
64
|
+
CWD="$T/cwd"
|
|
65
|
+
SLOG="$T/statusline.log"
|
|
66
|
+
SESSION="ccp98"
|
|
67
|
+
KEEP="${1:-}"
|
|
68
|
+
|
|
69
|
+
pass=0 fail=0
|
|
70
|
+
ok() {
|
|
71
|
+
echo " PASS: $*"
|
|
72
|
+
pass=$((pass + 1))
|
|
73
|
+
}
|
|
74
|
+
bad() {
|
|
75
|
+
echo " FAIL: $*"
|
|
76
|
+
fail=$((fail + 1))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
rm -rf "$T"
|
|
80
|
+
mkdir -p "$ROOT" "$CWD/.claude"
|
|
81
|
+
: >"$SLOG"
|
|
82
|
+
|
|
83
|
+
# The lab statusline is wired through the PROJECT settings of a throwaway /tmp
|
|
84
|
+
# cwd, so it overrides the operator's statusline for THIS session only -- no file
|
|
85
|
+
# in ~/.claude is edited. (Claude still WRITES session/transcript files there of
|
|
86
|
+
# its own accord; see the header.)
|
|
87
|
+
cat >"$CWD/.claude/settings.json" <<JSON
|
|
88
|
+
{
|
|
89
|
+
"statusLine": {
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "$HERE/lab-statusline.sh"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
JSON
|
|
95
|
+
|
|
96
|
+
# Remove the garden citizens this probe's session minted. Matching is on the
|
|
97
|
+
# record's own `cwd` field against this probe's throwaway /tmp cwd -- an exact
|
|
98
|
+
# string compare on a path no real citizen can have. A record that does not match
|
|
99
|
+
# is never touched, and anything unexpected is printed instead of deleted.
|
|
100
|
+
cleanup_citizens() {
|
|
101
|
+
local cwd="$1"
|
|
102
|
+
python3 - "$cwd" <<'PY'
|
|
103
|
+
import json, shutil, sys
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
cwd = sys.argv[1]
|
|
107
|
+
recs = Path.home() / ".pi" / "agent" / "meta-sessions"
|
|
108
|
+
boxes = Path.home() / ".pi" / "agent" / "meta-mailbox"
|
|
109
|
+
if not recs.is_dir():
|
|
110
|
+
sys.exit(0)
|
|
111
|
+
|
|
112
|
+
removed = []
|
|
113
|
+
for f in sorted(recs.glob("*.meta.json")):
|
|
114
|
+
try:
|
|
115
|
+
d = json.loads(f.read_text())
|
|
116
|
+
except Exception:
|
|
117
|
+
continue
|
|
118
|
+
if d.get("cwd") != cwd:
|
|
119
|
+
continue
|
|
120
|
+
gid = d.get("gardenId") or f.name.removesuffix(".meta.json")
|
|
121
|
+
box = boxes / gid
|
|
122
|
+
# Refuse to delete a mailbox that holds real traffic: only the SessionStart
|
|
123
|
+
# signal file is expected here. Anything else means this citizen was actually
|
|
124
|
+
# used, and a probe must not destroy evidence -- say so and leave it.
|
|
125
|
+
leftovers = sorted(p.name for p in box.iterdir()) if box.is_dir() else []
|
|
126
|
+
if [n for n in leftovers if n != "inbox.signal" and not n.endswith(".log")]:
|
|
127
|
+
print(f" ! left {gid}: mailbox is not empty ({', '.join(leftovers)})")
|
|
128
|
+
continue
|
|
129
|
+
if box.is_dir():
|
|
130
|
+
shutil.rmtree(box)
|
|
131
|
+
f.unlink()
|
|
132
|
+
removed.append(gid)
|
|
133
|
+
|
|
134
|
+
if removed:
|
|
135
|
+
print(f" cleaned {len(removed)} probe-minted citizen(s): {', '.join(removed)}")
|
|
136
|
+
else:
|
|
137
|
+
print(" no probe-minted citizens to clean")
|
|
138
|
+
PY
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
sid_for_cwd() {
|
|
142
|
+
python3 - "$1" <<'PY'
|
|
143
|
+
import json, glob, os, sys
|
|
144
|
+
cwd = sys.argv[1]
|
|
145
|
+
for f in glob.glob(os.path.expanduser("~/.claude/sessions/*.json")):
|
|
146
|
+
try: d = json.load(open(f))
|
|
147
|
+
except Exception: continue
|
|
148
|
+
if d.get("cwd") == cwd and d.get("kind") == "interactive":
|
|
149
|
+
print(d.get("sessionId")); break
|
|
150
|
+
PY
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
echo "== SETUP =="
|
|
154
|
+
echo " claude: $(claude --version 2>/dev/null)"
|
|
155
|
+
echo " plugin: $PLUGIN"
|
|
156
|
+
echo " mailbox: $ROOT"
|
|
157
|
+
echo " slog: $SLOG"
|
|
158
|
+
echo " hooks.json FileChanged entry under test:"
|
|
159
|
+
python3 -c 'import json,sys;print(" "+json.dumps(json.load(open(sys.argv[1]))["hooks"]["FileChanged"][0]["hooks"][0],ensure_ascii=False))' \
|
|
160
|
+
"$PLUGIN/hooks/hooks.json"
|
|
161
|
+
|
|
162
|
+
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
|
163
|
+
tmux new-session -d -s "$SESSION" -x 220 -y 50
|
|
164
|
+
tmux send-keys -t "$SESSION" "export CC_MAILBOX_ROOT='$ROOT' CC_LAB_STATUSLINE_LOG='$SLOG'" Enter
|
|
165
|
+
# --setting-sources project,local DROPS ~/.claude/settings.json, which is where
|
|
166
|
+
# the product meta-bridge plugin is enabled (extraKnownMarketplaces +
|
|
167
|
+
# enabledPlugins). Without it the probe was NOT isolated in two ways, both
|
|
168
|
+
# measured: the product SessionStart hook minted a real garden citizen, and the
|
|
169
|
+
# product doorbell -- which takes dirname(file_path) as "its" mailbox
|
|
170
|
+
# unconditionally -- processed the LAB mailbox and won the race to exit 2, so the
|
|
171
|
+
# operator saw the DEFAULT "Stop hook feedback" from a hook that has no
|
|
172
|
+
# rewakeSummary. Two runs failed P1 that way while hooks.json was correct.
|
|
173
|
+
# The project settings under $CWD/.claude still load, so the lab statusline stays.
|
|
174
|
+
tmux send-keys -t "$SESSION" "cd '$CWD' && claude --plugin-dir '$PLUGIN' --setting-sources project,local --dangerously-skip-permissions" Enter
|
|
175
|
+
|
|
176
|
+
echo "== ARM =="
|
|
177
|
+
SID=""
|
|
178
|
+
for _ in $(seq 1 45); do
|
|
179
|
+
[ -z "$SID" ] && SID=$(sid_for_cwd "$CWD")
|
|
180
|
+
[ -n "$SID" ] && [ -f "$ROOT/$SID/hook.log" ] && break
|
|
181
|
+
sleep 1
|
|
182
|
+
done
|
|
183
|
+
[ -n "$SID" ] || {
|
|
184
|
+
echo "FAIL: session never started"
|
|
185
|
+
exit 1
|
|
186
|
+
}
|
|
187
|
+
[ -f "$ROOT/$SID/hook.log" ] || {
|
|
188
|
+
echo "FAIL: watch never armed"
|
|
189
|
+
exit 1
|
|
190
|
+
}
|
|
191
|
+
echo " sessionId: $SID"
|
|
192
|
+
echo " arm: $(cat "$ROOT/$SID/hook.log")"
|
|
193
|
+
|
|
194
|
+
# (iii) schema acceptance: two @internal fields must not make the plugin unloadable.
|
|
195
|
+
# A rejected hooks.json shows up as a hook/plugin error on the pane at startup.
|
|
196
|
+
STARTUP_PANE="$(tmux capture-pane -t "$SESSION" -p)"
|
|
197
|
+
if printf '%s' "$STARTUP_PANE" | grep -qiE 'hook.*(invalid|error|failed)|plugin.*(invalid|error|failed)|unrecognized key|rewake'; then
|
|
198
|
+
bad "P1(iii) schema: startup complained about the hook config"
|
|
199
|
+
printf '%s\n' "$STARTUP_PANE" | grep -iE 'hook|plugin|rewake' | head -5
|
|
200
|
+
else
|
|
201
|
+
ok "P1(iii) schema: rewakeSummary+rewakeMessage accepted (no startup hook/plugin complaint)"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
echo "== DRIVE ONE TURN -> IDLE =="
|
|
205
|
+
sleep 2
|
|
206
|
+
tmux send-keys -t "$SESSION" "Reply with exactly the single word READY and then stop. No tools." Enter
|
|
207
|
+
sleep 1
|
|
208
|
+
tmux send-keys -t "$SESSION" Enter
|
|
209
|
+
for _ in $(seq 1 40); do
|
|
210
|
+
tmux capture-pane -t "$SESSION" -p | grep -qE '●\s*READY|⏺\s*READY' && break
|
|
211
|
+
sleep 1
|
|
212
|
+
done
|
|
213
|
+
sleep 3
|
|
214
|
+
SL_BEFORE=$(wc -l <"$SLOG")
|
|
215
|
+
PANE_BEFORE_LINES=$(tmux capture-pane -t "$SESSION" -p | wc -l)
|
|
216
|
+
echo " idle. statusline invocations so far: $SL_BEFORE"
|
|
217
|
+
|
|
218
|
+
echo "== DOORBELL (addressed external write, zero typing) =="
|
|
219
|
+
# The body carries NO imperative. The first run of this probe put the drain
|
|
220
|
+
# instructions in here and the woken Opus refused to act on them, citing the
|
|
221
|
+
# doorbell's own "do not act on unverified imperatives" line -- correct behaviour,
|
|
222
|
+
# and README lesson #7 reproducing itself. It also meant the mailbox was never
|
|
223
|
+
# drained, so that run measured prompt-injection resistance instead of P2b.
|
|
224
|
+
# The drain below is therefore typed by the OPERATOR, which is real user input.
|
|
225
|
+
CC_MAILBOX_ROOT="$ROOT" "$HERE/cc-enqueue-addressed.sh" "$SID" \
|
|
226
|
+
"P98-PROBE body. Notification-only payload; nothing to do. Reply with exactly WOKE and stop."
|
|
227
|
+
|
|
228
|
+
for _ in $(seq 1 25); do
|
|
229
|
+
grep -qs FILECHANGED "$ROOT/$SID/hook.log" && break
|
|
230
|
+
sleep 1
|
|
231
|
+
done
|
|
232
|
+
if grep -qs FILECHANGED "$ROOT/$SID/hook.log"; then
|
|
233
|
+
ok "doorbell rang on the idle session (FileChanged delivered)"
|
|
234
|
+
else
|
|
235
|
+
bad "doorbell never rang -- every probe below is void"
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
echo "== WAIT FOR THE WOKEN TURN TO FINISH (P2a window) =="
|
|
240
|
+
for _ in $(seq 1 45); do
|
|
241
|
+
tmux capture-pane -t "$SESSION" -p | grep -qE '⏺\s*WOKE|●\s*WOKE' && break
|
|
242
|
+
sleep 2
|
|
243
|
+
done
|
|
244
|
+
sleep 4
|
|
245
|
+
SL_AFTER_WAKE=$(wc -l <"$SLOG")
|
|
246
|
+
|
|
247
|
+
echo "== OPERATOR-TYPED MID-TURN DRAIN (P2b window) =="
|
|
248
|
+
# The lab has no `entwurf_inbox_read` tool, so a Bash rename to `.read` stands in
|
|
249
|
+
# for it: the same shape -- a MID-TURN tool call that takes the unread count to 0.
|
|
250
|
+
tmux send-keys -t "$SESSION" \
|
|
251
|
+
"Immediately run exactly this one Bash command, nothing else first: mv $ROOT/$SID/*.msg.delivered $ROOT/$SID/drained.msg.delivered.read" Enter
|
|
252
|
+
sleep 1
|
|
253
|
+
tmux send-keys -t "$SESSION" Enter
|
|
254
|
+
# Poll the FILESYSTEM, not the pane. Pane text is a race: the first corrected run
|
|
255
|
+
# timed out at 90s while the turn was still thinking (high effort), and reported a
|
|
256
|
+
# P2b failure that was really "the drain had not happened yet". The rename itself
|
|
257
|
+
# is the unambiguous signal that the mid-turn tool call landed.
|
|
258
|
+
DRAINED=0
|
|
259
|
+
for _ in $(seq 1 90); do
|
|
260
|
+
if ls "$ROOT/$SID"/*.msg.delivered.read >/dev/null 2>&1; then
|
|
261
|
+
DRAINED=1
|
|
262
|
+
break
|
|
263
|
+
fi
|
|
264
|
+
sleep 2
|
|
265
|
+
done
|
|
266
|
+
if [ "$DRAINED" -eq 1 ]; then
|
|
267
|
+
ok "mid-turn drain landed (mailbox archived to .read)"
|
|
268
|
+
else
|
|
269
|
+
bad "mid-turn drain never landed -- P2b below is VOID, not a measurement"
|
|
270
|
+
fi
|
|
271
|
+
# Give the status area time to render at least once after the tool result.
|
|
272
|
+
sleep 6
|
|
273
|
+
|
|
274
|
+
PANE="$(tmux capture-pane -t "$SESSION" -p)"
|
|
275
|
+
JSONL="$HOME/.claude/projects/$(printf '%s' "$CWD" | sed 's#/#-#g')/$SID.jsonl"
|
|
276
|
+
|
|
277
|
+
echo
|
|
278
|
+
echo "===================== P1: OPERATOR-VISIBLE ROW ====================="
|
|
279
|
+
printf '%s\n' "$PANE" | grep -nE 'Stop hook feedback|LAB-P1' || echo " (neither string on the pane)"
|
|
280
|
+
if printf '%s' "$PANE" | grep -q 'LAB-P1 entwurf inbox: sibling mail arrived'; then
|
|
281
|
+
ok "P1(i) pane row shows our rewakeSummary"
|
|
282
|
+
else
|
|
283
|
+
bad "P1(i) pane row does NOT show our rewakeSummary"
|
|
284
|
+
fi
|
|
285
|
+
if printf '%s' "$PANE" | grep -q 'Stop hook feedback'; then
|
|
286
|
+
bad "P1(i) pane still shows the default 'Stop hook feedback'"
|
|
287
|
+
else
|
|
288
|
+
ok "P1(i) default 'Stop hook feedback' is gone from the pane"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
echo
|
|
292
|
+
echo "===================== P1: MODEL-VISIBLE PREFIX ====================="
|
|
293
|
+
if [ -f "$JSONL" ]; then
|
|
294
|
+
echo " transcript: $JSONL"
|
|
295
|
+
python3 - "$JSONL" <<'PY'
|
|
296
|
+
import json, sys
|
|
297
|
+
hits = []
|
|
298
|
+
for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
|
|
299
|
+
if "meta-session notice" not in line and "Stop hook" not in line and "LAB-P1" not in line:
|
|
300
|
+
continue
|
|
301
|
+
try: rec = json.loads(line)
|
|
302
|
+
except Exception: continue
|
|
303
|
+
txt = json.dumps(rec, ensure_ascii=False)
|
|
304
|
+
for needle in ("Stop hook blocking error", "LAB-P1 entwurf mailbox notice"):
|
|
305
|
+
if needle in txt:
|
|
306
|
+
i = txt.index(needle)
|
|
307
|
+
hits.append((needle, txt[max(0, i - 60):i + 190]))
|
|
308
|
+
for needle, ctx in hits[:4]:
|
|
309
|
+
print(f" [{needle}]\n …{ctx}…")
|
|
310
|
+
if not hits:
|
|
311
|
+
print(" (neither prefix found in the transcript)")
|
|
312
|
+
PY
|
|
313
|
+
if grep -q 'LAB-P1 entwurf mailbox notice' "$JSONL"; then
|
|
314
|
+
ok "P1(ii) model prefix replaced by our rewakeMessage"
|
|
315
|
+
else
|
|
316
|
+
bad "P1(ii) our rewakeMessage did NOT reach the model"
|
|
317
|
+
fi
|
|
318
|
+
if grep -q 'Stop hook blocking error' "$JSONL"; then
|
|
319
|
+
bad "P1(ii) model still framed with 'Stop hook blocking error'"
|
|
320
|
+
else
|
|
321
|
+
ok "P1(ii) 'Stop hook blocking error' framing is gone"
|
|
322
|
+
fi
|
|
323
|
+
else
|
|
324
|
+
bad "P1(ii) transcript not found at $JSONL"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
echo
|
|
328
|
+
echo "===================== P2a / P2b: STATUSLINE RE-EXECUTION ====================="
|
|
329
|
+
echo " invocations before doorbell: $SL_BEFORE"
|
|
330
|
+
echo " --- statusline log lines added after idle ---"
|
|
331
|
+
tail -n +$((SL_BEFORE + 1)) "$SLOG" | sed 's/^/ /'
|
|
332
|
+
SL_AFTER=$(wc -l <"$SLOG")
|
|
333
|
+
WAKE_ADDED=$((SL_AFTER_WAKE - SL_BEFORE))
|
|
334
|
+
DRAIN_ADDED=$((SL_AFTER - SL_AFTER_WAKE))
|
|
335
|
+
echo " added by the doorbell turn: $WAKE_ADDED"
|
|
336
|
+
echo " added by the drain turn: $DRAIN_ADDED"
|
|
337
|
+
|
|
338
|
+
if [ "$WAKE_ADDED" -gt 0 ]; then
|
|
339
|
+
ok "P2a statusline RE-EXECUTED on the asyncRewake turn ($WAKE_ADDED invocation(s))"
|
|
340
|
+
else
|
|
341
|
+
bad "P2a statusline did NOT re-execute -- a badge would not appear when mail lands"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# P2a value check: the invocations during the doorbell turn must have SEEN the
|
|
345
|
+
# mail. A re-execution that still reports unread=0 would draw no badge.
|
|
346
|
+
if tail -n +$((SL_BEFORE + 1)) "$SLOG" | head -n "$WAKE_ADDED" | grep -q 'unread=[1-9]'; then
|
|
347
|
+
ok "P2a those invocations observed unread>=1 (a badge would have been drawn)"
|
|
348
|
+
else
|
|
349
|
+
bad "P2a re-executed but never observed the unread mail"
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
# P2b: after the mid-turn rename, an invocation must observe unread=0. If every
|
|
353
|
+
# line still says unread>=1, the badge would keep claiming mail the model already
|
|
354
|
+
# read -- option B would then weaken the rang-vs-read distinction, not show it.
|
|
355
|
+
if tail -n +$((SL_AFTER_WAKE + 1)) "$SLOG" | grep -q 'unread=0'; then
|
|
356
|
+
ok "P2b an invocation observed unread=0 after the mid-turn drain (badge clears)"
|
|
357
|
+
else
|
|
358
|
+
bad "P2b no invocation observed unread=0 -- badge would stay stale after the read"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
echo
|
|
362
|
+
echo "===================== MAILBOX FINAL STATE ====================="
|
|
363
|
+
ls -1 "$ROOT/$SID/" | sed 's/^/ /'
|
|
364
|
+
echo " hook.log: $(cat "$ROOT/$SID/hook.log")"
|
|
365
|
+
|
|
366
|
+
echo
|
|
367
|
+
echo "===================== PANE (tail) ====================="
|
|
368
|
+
printf '%s\n' "$PANE" | grep -vE '^\s*$' | tail -22 | sed 's/^/ /'
|
|
369
|
+
echo "======================================================="
|
|
370
|
+
|
|
371
|
+
echo
|
|
372
|
+
echo "SUMMARY: $pass pass, $fail fail (claude $(claude --version 2>/dev/null | awk '{print $1}'))"
|
|
373
|
+
echo "receipts: pane above, transcript $JSONL, statusline log $SLOG"
|
|
374
|
+
if [ "$KEEP" = "keep" ]; then
|
|
375
|
+
echo "(tmux session '$SESSION' left alive)"
|
|
376
|
+
else
|
|
377
|
+
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# The session inherited the operator's settings, so the PRODUCT meta-bridge minted
|
|
381
|
+
# a real citizen for this throwaway cwd. Give it back.
|
|
382
|
+
echo
|
|
383
|
+
echo "== OPERATOR-STATE CLEANUP =="
|
|
384
|
+
cleanup_citizens "$CWD"
|
|
385
|
+
echo " (left in place, written by Claude itself: ~/.claude/projects/$(printf '%s' "$CWD" | sed 's#/#-#g')/ and ~/.claude/sessions/*.json)"
|
|
386
|
+
|
|
387
|
+
[ "$fail" -eq 0 ]
|
|
@@ -53,7 +53,7 @@ import { fileURLToPath } from "node:url";
|
|
|
53
53
|
import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensions/lib/entwurf-control-rpc.ts";
|
|
54
54
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
55
55
|
import { skipLive } from "./lib/live-skip.ts";
|
|
56
|
-
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
56
|
+
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
57
57
|
|
|
58
58
|
const ACP_PROVIDER = "entwurf";
|
|
59
59
|
const ACP_MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
|
|
@@ -64,7 +64,7 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
|
|
|
64
64
|
// Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
|
|
65
65
|
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
66
66
|
|
|
67
|
-
const BOOT_TIMEOUT_MS =
|
|
67
|
+
const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
|
|
68
68
|
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
69
69
|
const POLL_MS = 100;
|
|
70
70
|
|
|
@@ -54,7 +54,7 @@ import { fileURLToPath } from "node:url";
|
|
|
54
54
|
import { upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
55
55
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
56
56
|
import { skipLive } from "./lib/live-skip.ts";
|
|
57
|
-
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
57
|
+
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
58
58
|
|
|
59
59
|
const ACP_PROVIDER = "entwurf";
|
|
60
60
|
const ACP_MODEL = process.env.ENTWURF_ACP_CORTEX_MODEL?.trim() || "cortex-claude-sonnet-5";
|
|
@@ -66,7 +66,7 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
|
|
|
66
66
|
// Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
|
|
67
67
|
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
68
68
|
|
|
69
|
-
const BOOT_TIMEOUT_MS =
|
|
69
|
+
const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
|
|
70
70
|
// The cortex CLI self-extracts on first launch and its newSession alone is
|
|
71
71
|
// ~2.5 s; keep the generous default of the sibling smoke.
|
|
72
72
|
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_CORTEX_TIMEOUT_MS) || 300_000;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// LIVE=1 ./run.sh smoke-acp-raw-turn-live
|
|
4
4
|
//
|
|
5
5
|
// What this proves (and ONLY this): the pinned Claude ACP adapter
|
|
6
|
-
// (@agentclientprotocol/claude-agent-acp@0.
|
|
6
|
+
// (@agentclientprotocol/claude-agent-acp@0.73.0) spawns, speaks the ACP wire
|
|
7
7
|
// protocol over stdio NDJSON, and returns one real model turn. It is the
|
|
8
8
|
// bytes-flow proof that the S2a dep surface is not just installable but
|
|
9
9
|
// actually drivable — before any provider/overlay/streamSimple code (S2b+).
|
|
@@ -32,7 +32,7 @@ import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensi
|
|
|
32
32
|
import { scanSocketProbes } from "../pi-extensions/lib/socket-discovery.ts";
|
|
33
33
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
34
34
|
import { skipLive } from "./lib/live-skip.ts";
|
|
35
|
-
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
35
|
+
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
36
36
|
|
|
37
37
|
const ACP_PROVIDER = "entwurf";
|
|
38
38
|
const ACP_MODEL = process.env.ENTWURF_S1_MODEL?.trim() || "claude-opus-5";
|
|
@@ -43,7 +43,7 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
|
|
|
43
43
|
// Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
|
|
44
44
|
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
45
45
|
|
|
46
|
-
const BOOT_TIMEOUT_MS =
|
|
46
|
+
const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
|
|
47
47
|
const POLL_MS = 100;
|
|
48
48
|
|
|
49
49
|
let passed = 0;
|
|
@@ -82,7 +82,7 @@ import { fileURLToPath } from "node:url";
|
|
|
82
82
|
import { upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
83
83
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
84
84
|
import { skipLive } from "./lib/live-skip.ts";
|
|
85
|
-
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
85
|
+
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
86
86
|
|
|
87
87
|
const ACP_PROVIDER = "entwurf";
|
|
88
88
|
const ACP_MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
|
|
@@ -93,7 +93,7 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
|
|
|
93
93
|
// Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
|
|
94
94
|
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
95
95
|
|
|
96
|
-
const BOOT_TIMEOUT_MS =
|
|
96
|
+
const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
|
|
97
97
|
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
98
98
|
const POLL_MS = 100;
|
|
99
99
|
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
66
66
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
67
67
|
import { skipLive } from "./lib/live-skip.ts";
|
|
68
|
-
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
68
|
+
import { describePiLockResidue, PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
69
69
|
|
|
70
70
|
// pi's control socket lives at the canonical dir keyed by the RECORD's garden id (#50 C4: the
|
|
71
71
|
// record is the sole address authority — never a transcript/session id; :196 below proves it),
|
|
@@ -78,7 +78,14 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
|
|
|
78
78
|
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
79
79
|
|
|
80
80
|
// Staged timeouts (automation): short and per-stage so a stall is attributable.
|
|
81
|
-
|
|
81
|
+
//
|
|
82
|
+
// The boot bound is SHARED and carries its own receipt (see `PI_BOOT_TIMEOUT_MS` in
|
|
83
|
+
// pi-record-discovery). Short version, because this smoke is where it was found: this cell used to
|
|
84
|
+
// wait 30_000ms, which is EXACTLY pi's `proper-lockfile` stale window, and C1's own SIGTERM can
|
|
85
|
+
// orphan that lock — so C1b's resident spent the whole wait queueing behind it and birthed its
|
|
86
|
+
// record at a measured 30_148ms, 148ms after the smoke had stopped looking. That is what blocked
|
|
87
|
+
// two of the three 0.17.0 `--cut` runs.
|
|
88
|
+
const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // pi --entwurf-control record + socket appear
|
|
82
89
|
const POLL_MS = 100;
|
|
83
90
|
|
|
84
91
|
let passed = 0;
|
|
@@ -90,6 +97,43 @@ function ok(label: string, cond: boolean): void {
|
|
|
90
97
|
passed++;
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
/** What a spawned resident DID while the smoke waited on it — the fact a bare boot timeout
|
|
101
|
+
* throws away. Each resident owns its own stderr here: a single shared tail cannot say which
|
|
102
|
+
* of the two children spoke, and the C1b post-mortem is exactly that question. */
|
|
103
|
+
interface ResidentWatch {
|
|
104
|
+
label: string;
|
|
105
|
+
pid: number | undefined;
|
|
106
|
+
startedAt: number;
|
|
107
|
+
exit: string | null;
|
|
108
|
+
stderr: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function watchResident(child: ChildProcess, label: string): ResidentWatch {
|
|
112
|
+
const watch: ResidentWatch = { label, pid: child.pid, startedAt: Date.now(), exit: null, stderr: "" };
|
|
113
|
+
child.once("exit", (code, signal) => {
|
|
114
|
+
watch.exit = `code=${code} signal=${signal} at +${Date.now() - watch.startedAt}ms`;
|
|
115
|
+
});
|
|
116
|
+
child.stderr?.on("data", (b: Buffer) => {
|
|
117
|
+
watch.stderr = (watch.stderr + b.toString()).slice(-2000);
|
|
118
|
+
});
|
|
119
|
+
return watch;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read at FAILURE time, before the `finally` reaps anything: a resident that is still alive
|
|
123
|
+
* having birthed nothing is a different defect from one that exited silently, and "no pid" is a
|
|
124
|
+
* third (the spawn itself never took). */
|
|
125
|
+
function describeResident(watch: ResidentWatch): string {
|
|
126
|
+
const waited = `${Date.now() - watch.startedAt}ms after spawn`;
|
|
127
|
+
if (watch.pid === undefined) return `never acquired a pid — the spawn did not take (${waited})`;
|
|
128
|
+
if (watch.exit) return `pid=${watch.pid} EXITED ${watch.exit} — it was gone before the wait ended`;
|
|
129
|
+
try {
|
|
130
|
+
process.kill(watch.pid, 0);
|
|
131
|
+
return `pid=${watch.pid} still ALIVE ${waited} and had birthed no record`;
|
|
132
|
+
} catch {
|
|
133
|
+
return `pid=${watch.pid} is GONE with no exit event seen (reaped elsewhere), ${waited}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
93
137
|
function resolveTarget(): { provider: string; model: string } {
|
|
94
138
|
const combined = process.env.ENTWURF_LIVE_TARGET?.trim();
|
|
95
139
|
if (combined) {
|
|
@@ -153,7 +197,7 @@ async function main(): Promise<void> {
|
|
|
153
197
|
let resident: ChildProcess | null = null;
|
|
154
198
|
let residentGid = "";
|
|
155
199
|
let c1bGid = "";
|
|
156
|
-
|
|
200
|
+
const residents: ResidentWatch[] = [];
|
|
157
201
|
let succeeded = false;
|
|
158
202
|
|
|
159
203
|
const prodDeps = (sender: SenderEnvelope) =>
|
|
@@ -182,9 +226,7 @@ async function main(): Promise<void> {
|
|
|
182
226
|
[...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", provider, "--model", model, "--mode", "rpc"],
|
|
183
227
|
{ cwd: tmp, stdio: ["pipe", "ignore", "pipe"], detached: false },
|
|
184
228
|
);
|
|
185
|
-
|
|
186
|
-
stderrTail = (stderrTail + b.toString()).slice(-2000);
|
|
187
|
-
});
|
|
229
|
+
residents.push(watchResident(resident, "C1"));
|
|
188
230
|
|
|
189
231
|
const bornGid = await waitForPiRecord(sessionsDir, BOOT_TIMEOUT_MS);
|
|
190
232
|
ok("C1 the resident BIRTHED its own V3 backend:pi record (the address authority)", bornGid !== null);
|
|
@@ -241,9 +283,7 @@ async function main(): Promise<void> {
|
|
|
241
283
|
env: { ...process.env, ENTWURF_META_SESSIONS_DIR: hiddenStore },
|
|
242
284
|
},
|
|
243
285
|
);
|
|
244
|
-
|
|
245
|
-
stderrTail = (stderrTail + b.toString()).slice(-2000);
|
|
246
|
-
});
|
|
286
|
+
residents.push(watchResident(resident, "C1b"));
|
|
247
287
|
|
|
248
288
|
const bornGid = await waitForPiRecord(hiddenStore, BOOT_TIMEOUT_MS);
|
|
249
289
|
ok("C1b the resident birthed its record into the HIDDEN store", bornGid !== null);
|
|
@@ -358,7 +398,17 @@ async function main(): Promise<void> {
|
|
|
358
398
|
} catch (err) {
|
|
359
399
|
console.error("\n[smoke-entwurf-v2-matrix-live] FAILED — diagnostic artifacts:");
|
|
360
400
|
for (const [k, v] of Object.entries(artifacts)) console.error(` ${k} = ${v}`);
|
|
361
|
-
|
|
401
|
+
// Probed HERE, not in `finally`: the reaper runs after this block, so this is the last
|
|
402
|
+
// moment the residents' real state can still be read. An empty stderr is itself evidence
|
|
403
|
+
// and is printed as "(empty)" rather than skipped — silence that is never stated reads as
|
|
404
|
+
// a missing diagnostic instead of the observation it is.
|
|
405
|
+
console.error(` pi locks held right now: ${describePiLockResidue()}`);
|
|
406
|
+
for (const watch of residents) {
|
|
407
|
+
console.error(` resident ${watch.label}: ${describeResident(watch)}`);
|
|
408
|
+
console.error(
|
|
409
|
+
` resident ${watch.label} stderr: ${watch.stderr ? `\n${watch.stderr.replace(/^/gm, " ")}` : "(empty)"}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
362
412
|
throw err;
|
|
363
413
|
} finally {
|
|
364
414
|
if (resident) {
|
|
@@ -6,13 +6,24 @@
|
|
|
6
6
|
#
|
|
7
7
|
# WHY THIS EXISTS
|
|
8
8
|
# The whole Claude async-delivery path rides on UNDOCUMENTED Claude Code behavior:
|
|
9
|
-
# `asyncRewake`
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
9
|
+
# `asyncRewake` names the operator row and the model prefix from `rewakeSummary` /
|
|
10
|
+
# `rewakeMessage` (both read from a local plugin's hooks.json, both @internal, and
|
|
11
|
+
# the product doorbell now DEPENDS on both); the doorbell writes its notice on
|
|
12
|
+
# stderr; `watchPaths` can arm from only 3 hook events (SessionStart / CwdChanged /
|
|
13
|
+
# FileChanged). Half the Gotchas in scripts/raw-async-delivery/README.md are
|
|
14
|
+
# reverse-engineered. Claude ships ~weekly. So the path can break SILENTLY on any
|
|
15
|
+
# upgrade. This gate makes it SCREAM instead — direct lineage of the 0.8.x fail-fast
|
|
16
|
+
# tool-surface gates ("don't let a surface break quietly, make the gate cry out").
|
|
17
|
+
#
|
|
18
|
+
# THIS PARAGRAPH USED TO BE WRONG, AND THAT IS THE POINT (#98, corrected
|
|
19
|
+
# 2026-09-03). It said `asyncRewake` "force-prepends `Stop hook feedback:` and
|
|
20
|
+
# ignores `rewakeMessage`", and that the payload channel was "stderr-only". Both are
|
|
21
|
+
# FALSE — measured on 2.1.236/2.1.258/2.1.259: `rewakeMessage` REPLACES the model
|
|
22
|
+
# prefix, `rewakeSummary` REPLACES the operator row, and the model body is
|
|
23
|
+
# `stderr || stdout`. The marker strings below stayed green the whole time, because
|
|
24
|
+
# a marker pins a STRING and cannot see the sentence next to it rot. Nobody tried
|
|
25
|
+
# the two fields for months on the strength of this header. Receipts:
|
|
26
|
+
# scripts/raw-async-delivery/README.md "Inherited facts corrected".
|
|
16
27
|
#
|
|
17
28
|
# TWO TIERS (mirrors smoke-compaction-policy: deterministic default + LIVE=1 add-on)
|
|
18
29
|
# DETERMINISTIC (default; free, offline, CI/pre-commit safe):
|
|
@@ -70,7 +81,19 @@ MARKERS=(
|
|
|
70
81
|
flushPendingAsyncRewakeHooks # engine turn-boundary flush (edge-bound delivery)
|
|
71
82
|
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP # native re-wake cap
|
|
72
83
|
FileChanged # idle-wake event
|
|
73
|
-
|
|
84
|
+
# rewakeMessage/rewakeSummary — CORRECTED 2026-09-03 (#98 Phase 1). The comment here used
|
|
85
|
+
# to read "the field asyncRewake IGNORES (doorbell-only proof)". That was FALSE, and it is
|
|
86
|
+
# the reason nobody tried the field for months: measured on 2.1.236/2.1.258/2.1.259, BOTH
|
|
87
|
+
# are read from a local plugin's hooks.json with no first-party gate. `rewakeMessage`
|
|
88
|
+
# REPLACES the model-facing `Stop hook blocking error from command "…":` prefix;
|
|
89
|
+
# `rewakeSummary` REPLACES the operator-visible row (default `Stop hook feedback`). Only
|
|
90
|
+
# the stdout-JSON form of rewakeSummary is first-party gated. The marker string was right
|
|
91
|
+
# while the reason next to it was dead — which is why these two are pinned HERE: the
|
|
92
|
+
# product doorbell now DEPENDS on both (pi/meta-bridge/.../hooks/hooks.json), the fields
|
|
93
|
+
# are @internal, and if either disappears the surface silently reverts to
|
|
94
|
+
# `Stop hook feedback` + "blocking error" with no other gate to notice.
|
|
95
|
+
rewakeMessage # model-facing prefix replacement (product depends on it)
|
|
96
|
+
rewakeSummary # operator-facing row replacement (product depends on it)
|
|
74
97
|
hookSpecificOutput # the watchPaths emit envelope
|
|
75
98
|
CwdChanged # the 2nd of the 3 watch-arming events
|
|
76
99
|
)
|