codebyplan 1.13.27 → 1.13.28
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/dist/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The `codebyplan` npm package ships a small, portable set of Claude Code hooks. They run in your project, use only generic primitives (`git rev-parse`, `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_PLUGIN_ROOT}`), and degrade gracefully (exit 0) when their preconditions aren't met.
|
|
4
4
|
|
|
5
|
-
Hook registration lives in [`hooks/hooks.json`](./hooks.json) —
|
|
5
|
+
Hook registration lives in [`hooks/hooks.json`](./hooks.json) — PreToolUse, PostToolUse, and UserPromptSubmit events are wired. (`Notification`, `SessionStart`, `SessionEnd`, `Stop`, and `SubagentStop` are also schema-permitted but unused here.)
|
|
6
6
|
|
|
7
7
|
**`cbp-statusline.sh` is auto-wired via `settings.project.base.json`.** The `statusLine` block is shipped inside `templates/settings.project.base.json` and merged into the consumer's `.claude/settings.json` automatically by `codebyplan claude install` (and on every `codebyplan claude update`). No manual copy-paste is required.
|
|
8
8
|
|
|
@@ -224,13 +224,32 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
|
|
|
224
224
|
|
|
225
225
|
---
|
|
226
226
|
|
|
227
|
+
### `cbp-context-window-notify.sh` — UserPromptSubmit
|
|
228
|
+
|
|
229
|
+
Injects a one-time notice into Claude's context when the session's total token usage
|
|
230
|
+
(input + cache-creation + cache-read tokens from the last assistant message) crosses
|
|
231
|
+
`CBP_CONTEXT_WARN_TOKENS` (default: 200000). Reads the JSONL transcript directly — not the
|
|
232
|
+
statusline payload — so the model itself, not just the status bar, is aware of a large window.
|
|
233
|
+
|
|
234
|
+
**Blocks vs warns**: never blocks — exits 0 on every path. Advisory only.
|
|
235
|
+
|
|
236
|
+
**Skips when**: `transcript_path` or `session_id` is absent from stdin, the transcript file does
|
|
237
|
+
not exist, or usage cannot be parsed (degrades to 0, below threshold).
|
|
238
|
+
|
|
239
|
+
**Re-arms**: the latch `${TMPDIR:-/tmp}/cbp-ctxwin-<session_id>.latched` is removed when usage
|
|
240
|
+
drops below threshold (e.g. after `/compact` or `/clear`), so the notice re-fires on the next crossing.
|
|
241
|
+
|
|
242
|
+
**Opt out**: set `CBP_CONTEXT_WARN_TOKENS` very high, or remove the `UserPromptSubmit` entry from settings.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
227
246
|
## Supporting (not registered)
|
|
228
247
|
|
|
229
248
|
### `test-hooks.sh` — invoked by `auto-test-hooks.sh`
|
|
230
249
|
|
|
231
250
|
Test suite for the plugin's 9 registered hooks. Runs two passes:
|
|
232
251
|
|
|
233
|
-
1. **Header check** — every registered hook (`lint-format-on-edit`, `test-coverage-gate`, `pre-commit-quality-gate`, `maestro-yaml-validate`, `auto-test-hooks`, `mcp-migration-guard`, `validate-git-stash-deny`, `cbp-mcp-round-sync`) carries the required `# Hook:` and `# Purpose:` header comments. `statusline` uses its own `# Claude Code Status Line` marker.
|
|
252
|
+
1. **Header check** — every registered hook (`lint-format-on-edit`, `test-coverage-gate`, `pre-commit-quality-gate`, `maestro-yaml-validate`, `auto-test-hooks`, `mcp-migration-guard`, `validate-git-stash-deny`, `cbp-mcp-round-sync`, `cbp-context-window-notify`) carries the required `# Hook:` and `# Purpose:` header comments. `statusline` uses its own `# Claude Code Status Line` marker.
|
|
234
253
|
2. **Functional smoke tests** — each hook is invoked with synthetic stdin matching its fast-path / graceful-degrade input; all must exit 0.
|
|
235
254
|
|
|
236
255
|
Not in `hooks.json` — invoked indirectly via `auto-test-hooks.sh` on hook edits, or directly via `bash ${CLAUDE_PLUGIN_ROOT}/hooks/test-hooks.sh`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# @scope: org-shared
|
|
3
|
+
# Hook: UserPromptSubmit
|
|
4
|
+
# Purpose: Emit a one-time notice into Claude's context when the session's total
|
|
5
|
+
# context-window usage crosses CBP_CONTEXT_WARN_TOKENS (default 200000).
|
|
6
|
+
# Reads the last assistant message.usage from the JSONL transcript
|
|
7
|
+
# (input + cache_creation + cache_read) — independent of the statusline,
|
|
8
|
+
# so the model itself, not just the status bar, knows the window is large.
|
|
9
|
+
# Latches per session so the notice fires once; re-arms after /clear or
|
|
10
|
+
# /compact drops usage below the threshold. Always exits 0 — never blocks.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null)
|
|
17
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
[ -z "$TRANSCRIPT" ] && exit 0
|
|
20
|
+
[ -z "$SESSION_ID" ] && exit 0
|
|
21
|
+
[ ! -f "$TRANSCRIPT" ] && exit 0
|
|
22
|
+
|
|
23
|
+
THRESHOLD="${CBP_CONTEXT_WARN_TOKENS:-200000}"
|
|
24
|
+
LATCH="${TMPDIR:-/tmp}/cbp-ctxwin-${SESSION_ID}.latched"
|
|
25
|
+
|
|
26
|
+
TOTAL=$(tail -n 400 "$TRANSCRIPT" \
|
|
27
|
+
| jq -rR 'fromjson? | select(.message.usage != null)
|
|
28
|
+
| (.message.usage
|
|
29
|
+
| ((.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0)))' \
|
|
30
|
+
2>/dev/null | tail -1) || TOTAL=0
|
|
31
|
+
|
|
32
|
+
TOTAL="${TOTAL:-0}"
|
|
33
|
+
|
|
34
|
+
if [ "$TOTAL" -ge "$THRESHOLD" ] 2>/dev/null; then
|
|
35
|
+
[ -f "$LATCH" ] && exit 0
|
|
36
|
+
touch "$LATCH"
|
|
37
|
+
jq -n --argjson tokens "$TOTAL" --argjson threshold "$THRESHOLD" \
|
|
38
|
+
'{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:("Context-window notice: total token usage has reached \($tokens) (threshold \($threshold)). The window is large — consider /compact or /clear, and be deliberate about large new reads.")}}'
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
rm -f "$LATCH" 2>/dev/null || true
|
|
43
|
+
exit 0
|
|
@@ -255,6 +255,125 @@ fi
|
|
|
255
255
|
|
|
256
256
|
echo ""
|
|
257
257
|
|
|
258
|
+
# ===== HOOK SMOKE TESTS — cbp-context-window-notify =====
|
|
259
|
+
echo "## Hook Smoke Tests — cbp-context-window-notify"
|
|
260
|
+
|
|
261
|
+
HOOK="$HOOKS_DIR/cbp-context-window-notify.sh"
|
|
262
|
+
FIXTURES_CTX="$HOOKS_DIR/__test-fixtures__/cbp-context-window-notify"
|
|
263
|
+
|
|
264
|
+
if [ ! -f "$HOOK" ]; then
|
|
265
|
+
test_result "cbp-context-window-notify.sh present" "passed" "missing"
|
|
266
|
+
else
|
|
267
|
+
|
|
268
|
+
# Case 1: over-threshold → exit 0 AND stdout has .hookSpecificOutput.additionalContext
|
|
269
|
+
ISO=$(mktemp -d)
|
|
270
|
+
STDIN=$(jq -n --arg t "$FIXTURES_CTX/over-threshold.jsonl" --arg s "sid-over-$$" \
|
|
271
|
+
'{transcript_path:$t,session_id:$s}')
|
|
272
|
+
OUTPUT=$(echo "$STDIN" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" 2>/dev/null)
|
|
273
|
+
EXIT_CODE=$?
|
|
274
|
+
if [ "$EXIT_CODE" = "0" ] \
|
|
275
|
+
&& echo "$OUTPUT" | jq -e '.hookSpecificOutput.additionalContext' >/dev/null 2>&1; then
|
|
276
|
+
test_result "cbp-context-window-notify.sh over-threshold exits 0 + emits additionalContext JSON" "passed" "passed"
|
|
277
|
+
else
|
|
278
|
+
test_result "cbp-context-window-notify.sh over-threshold exits 0 + emits additionalContext JSON" "passed" "failed (exit=$EXIT_CODE output=$(echo "$OUTPUT" | head -c 80))"
|
|
279
|
+
fi
|
|
280
|
+
rm -rf "$ISO"
|
|
281
|
+
|
|
282
|
+
# Case 2: under-threshold → exit 0 AND empty stdout
|
|
283
|
+
ISO=$(mktemp -d)
|
|
284
|
+
STDIN=$(jq -n --arg t "$FIXTURES_CTX/under-threshold.jsonl" --arg s "sid-under-$$" \
|
|
285
|
+
'{transcript_path:$t,session_id:$s}')
|
|
286
|
+
OUTPUT=$(echo "$STDIN" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" 2>/dev/null)
|
|
287
|
+
EXIT_CODE=$?
|
|
288
|
+
if [ "$EXIT_CODE" = "0" ] && [ -z "$OUTPUT" ]; then
|
|
289
|
+
test_result "cbp-context-window-notify.sh under-threshold exits 0 + empty stdout" "passed" "passed"
|
|
290
|
+
else
|
|
291
|
+
test_result "cbp-context-window-notify.sh under-threshold exits 0 + empty stdout" "passed" "failed (exit=$EXIT_CODE)"
|
|
292
|
+
fi
|
|
293
|
+
rm -rf "$ISO"
|
|
294
|
+
|
|
295
|
+
# Case 3: latch — fire once (over), second identical call (same session_id, same TMPDIR) → empty stdout
|
|
296
|
+
ISO=$(mktemp -d)
|
|
297
|
+
SID="sid-latch-$$"
|
|
298
|
+
STDIN=$(jq -n --arg t "$FIXTURES_CTX/over-threshold.jsonl" --arg s "$SID" \
|
|
299
|
+
'{transcript_path:$t,session_id:$s}')
|
|
300
|
+
echo "$STDIN" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" >/dev/null 2>&1
|
|
301
|
+
OUTPUT=$(echo "$STDIN" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" 2>/dev/null)
|
|
302
|
+
EXIT_CODE=$?
|
|
303
|
+
if [ "$EXIT_CODE" = "0" ] && [ -z "$OUTPUT" ]; then
|
|
304
|
+
test_result "cbp-context-window-notify.sh latch suppresses second over-threshold call" "passed" "passed"
|
|
305
|
+
else
|
|
306
|
+
test_result "cbp-context-window-notify.sh latch suppresses second over-threshold call" "passed" "failed (exit=$EXIT_CODE)"
|
|
307
|
+
fi
|
|
308
|
+
rm -rf "$ISO"
|
|
309
|
+
|
|
310
|
+
# Case 4: re-arm — fire over (creates latch), then under → latch file removed
|
|
311
|
+
ISO=$(mktemp -d)
|
|
312
|
+
SID="sid-rearm-$$"
|
|
313
|
+
STDIN_OVER=$(jq -n --arg t "$FIXTURES_CTX/over-threshold.jsonl" --arg s "$SID" \
|
|
314
|
+
'{transcript_path:$t,session_id:$s}')
|
|
315
|
+
STDIN_UNDER=$(jq -n --arg t "$FIXTURES_CTX/under-threshold.jsonl" --arg s "$SID" \
|
|
316
|
+
'{transcript_path:$t,session_id:$s}')
|
|
317
|
+
echo "$STDIN_OVER" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" >/dev/null 2>&1
|
|
318
|
+
echo "$STDIN_UNDER" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" >/dev/null 2>&1
|
|
319
|
+
LATCH_FILE="$ISO/cbp-ctxwin-${SID}.latched"
|
|
320
|
+
if [ ! -f "$LATCH_FILE" ]; then
|
|
321
|
+
test_result "cbp-context-window-notify.sh re-arm removes latch on under-threshold" "passed" "passed"
|
|
322
|
+
else
|
|
323
|
+
test_result "cbp-context-window-notify.sh re-arm removes latch on under-threshold" "passed" "failed (latch still present)"
|
|
324
|
+
fi
|
|
325
|
+
rm -rf "$ISO"
|
|
326
|
+
|
|
327
|
+
# Case 5: cache-expired → exit 0 AND emits additionalContext JSON
|
|
328
|
+
ISO=$(mktemp -d)
|
|
329
|
+
STDIN=$(jq -n --arg t "$FIXTURES_CTX/cache-expired.jsonl" --arg s "sid-cache-$$" \
|
|
330
|
+
'{transcript_path:$t,session_id:$s}')
|
|
331
|
+
OUTPUT=$(echo "$STDIN" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" 2>/dev/null)
|
|
332
|
+
EXIT_CODE=$?
|
|
333
|
+
if [ "$EXIT_CODE" = "0" ] \
|
|
334
|
+
&& echo "$OUTPUT" | jq -e '.hookSpecificOutput.additionalContext' >/dev/null 2>&1; then
|
|
335
|
+
test_result "cbp-context-window-notify.sh cache-expired exits 0 + emits additionalContext JSON" "passed" "passed"
|
|
336
|
+
else
|
|
337
|
+
test_result "cbp-context-window-notify.sh cache-expired exits 0 + emits additionalContext JSON" "passed" "failed (exit=$EXIT_CODE)"
|
|
338
|
+
fi
|
|
339
|
+
rm -rf "$ISO"
|
|
340
|
+
|
|
341
|
+
# Case 6: graceful-degrade — empty stdin → exit 0
|
|
342
|
+
ISO=$(mktemp -d)
|
|
343
|
+
EXIT_CODE=$(echo '' | TMPDIR="$ISO" bash "$HOOK" >/dev/null 2>&1; echo $?)
|
|
344
|
+
if [ "$EXIT_CODE" = "0" ]; then
|
|
345
|
+
test_result "cbp-context-window-notify.sh graceful-degrade empty stdin exits 0" "passed" "passed"
|
|
346
|
+
else
|
|
347
|
+
test_result "cbp-context-window-notify.sh graceful-degrade empty stdin exits 0" "passed" "failed (exit=$EXIT_CODE)"
|
|
348
|
+
fi
|
|
349
|
+
rm -rf "$ISO"
|
|
350
|
+
|
|
351
|
+
# Case 7: malformed-line tolerance — a malformed/partial transcript line must NOT
|
|
352
|
+
# abort the hook (D3 "always exit 0"); the valid over-threshold line is still parsed
|
|
353
|
+
# (jq -rR 'fromjson?') and the notice still fires. The fixture lives only under
|
|
354
|
+
# .claude/hooks/__test-fixtures__; guard on its presence so this case stays
|
|
355
|
+
# byte-identical with the templates copy (which ships no __test-fixtures__ tree).
|
|
356
|
+
if [ -f "$FIXTURES_CTX/malformed.jsonl" ]; then
|
|
357
|
+
ISO=$(mktemp -d)
|
|
358
|
+
STDIN=$(jq -n --arg t "$FIXTURES_CTX/malformed.jsonl" --arg s "sid-malformed-$$" \
|
|
359
|
+
'{transcript_path:$t,session_id:$s}')
|
|
360
|
+
OUTPUT=$(echo "$STDIN" | TMPDIR="$ISO" CBP_CONTEXT_WARN_TOKENS=200000 bash "$HOOK" 2>/dev/null)
|
|
361
|
+
EXIT_CODE=$?
|
|
362
|
+
if [ "$EXIT_CODE" = "0" ] \
|
|
363
|
+
&& echo "$OUTPUT" | jq -e '.hookSpecificOutput.additionalContext' >/dev/null 2>&1; then
|
|
364
|
+
test_result "cbp-context-window-notify.sh malformed-line tolerance exits 0 + emits additionalContext JSON" "passed" "passed"
|
|
365
|
+
else
|
|
366
|
+
test_result "cbp-context-window-notify.sh malformed-line tolerance exits 0 + emits additionalContext JSON" "passed" "failed (exit=$EXIT_CODE output=$(echo "$OUTPUT" | head -c 80))"
|
|
367
|
+
fi
|
|
368
|
+
rm -rf "$ISO"
|
|
369
|
+
else
|
|
370
|
+
test_result "cbp-context-window-notify.sh malformed-line tolerance (fixture absent — templates runtime)" "passed" "passed"
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
echo ""
|
|
376
|
+
|
|
258
377
|
# ===== SUMMARY =====
|
|
259
378
|
echo "=== TEST SUMMARY ==="
|
|
260
379
|
echo -e "Passed: ${GREEN}$PASSED${NC}"
|