codebyplan 1.13.26 → 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
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.13.26";
17
+ VERSION = "1.13.28";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.26",
3
+ "version": "1.13.28",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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) — SessionStart, PreToolUse, and PostToolUse events are wired. (`Notification`, `SessionEnd`, `Stop`, and `SubagentStop` are also schema-permitted but unused here.)
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}"
@@ -1,5 +1,15 @@
1
1
  {
2
2
  "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-context-window-notify.sh"
9
+ }
10
+ ]
11
+ }
12
+ ],
3
13
  "PreToolUse": [
4
14
  {
5
15
  "matcher": "Edit|Write|MultiEdit",
@@ -2,7 +2,7 @@
2
2
  scope: org-shared
3
3
  paths:
4
4
  - "apps/todo-worker/**"
5
- - "apps/web/src/lib/mcp/**"
5
+ - "packages/mcp-tools/src/**"
6
6
  - "supabase/migrations/*todos*"
7
7
  - "supabase/migrations/*worktrees*"
8
8
  ---
@@ -31,7 +31,7 @@ The worker is a passive cross-checker (`apps/todo-worker/src/invariants/check.ts
31
31
  `todos_jobs` is the work queue drained by the worker.
32
32
 
33
33
  ```
34
- MCP write → enqueueTodoJob → todos_jobs (status='pending')
34
+ MCP write → enqueueTodosJob → todos_jobs (status='pending')
35
35
 
36
36
  worker claim_todos_job (SELECT … FOR UPDATE SKIP LOCKED)
37
37
 
@@ -72,23 +72,36 @@ The queue head (`get_todos` `rows[0]`) maps to one of these slash commands. The
72
72
 
73
73
  ## 5. Heartbeat policy
74
74
 
75
- The worker's `node-cron` heartbeat runs at `0 0 * * *` (UTC midnight). It enumerates every `(repo, worktree, user)` tuple with an active checkpoint OR in-progress standalone task and enqueues a `HEARTBEAT_SWEEP` todos_jobs row for each. This catches drift from missed `enqueueTodoJob` calls in MCP writers.
75
+ The worker's `node-cron` heartbeat runs at `0 0 * * *` (UTC midnight). It enumerates every `(repo, worktree, user)` tuple with an active checkpoint OR in-progress standalone task and enqueues a `HEARTBEAT_SWEEP` todos_jobs row for each. This catches drift from missed `enqueueTodosJob` calls in MCP writers.
76
76
 
77
77
  Backoff: a failed job retries at `now + 2^attempts minutes` (cap 60min). After 3 attempts, the job stays `failed` and the heartbeat picks it up again at the next sweep.
78
78
 
79
79
  ## 6. Writer obligations — every MCP write enqueues
80
80
 
81
- `apps/web/src/lib/mcp/enqueueTodoJob.ts` exposes:
81
+ The shared enqueue helper lives at `packages/mcp-tools/src/tools/enqueue-todos.ts`:
82
82
 
83
83
  ```ts
84
- enqueueTodoJob(client, { repoId, worktreeId, userId, reason }): Promise<void>
84
+ enqueueTodosJob(
85
+ client: SupabaseClient,
86
+ repoId: string,
87
+ callerWorktreeId: string | undefined,
88
+ userId: string | null,
89
+ reason: string
90
+ ): Promise<void>
85
91
  ```
86
92
 
87
- Every workflow mutator in `apps/web/src/lib/mcp/tools/write.ts` MUST call this after the mutation succeeds. The 11 writers as of CHK-122:
93
+ Two write modules import this helper:
94
+
95
+ - **`packages/mcp-tools/src/tools/write.ts`** — checkpoint-bound writers (11 tools)
96
+ - **`packages/mcp-tools/src/tools/standalone-write.ts`** — standalone task writers
97
+
98
+ Every workflow mutator MUST call `void enqueueTodosJob(...)` after the mutation succeeds. The 11 checkpoint-bound writers in `write.ts` (CHK-122 + CHK-189):
88
99
 
89
100
  `create_checkpoint, update_checkpoint, complete_checkpoint, create_task, update_task, complete_task, add_round, update_round, complete_round, create_session_log, update_session_log`
90
101
 
91
- Plus the 2 dedicated enqueue tools: `enqueue_todo_job`, `bind_worktree_user`.
102
+ **Dead code note**: `apps/web/src/lib/mcp/enqueueTodoJob.ts` and its companion test are orphaned dead code — `apps/web` no longer registers any MCP write tools (they live in `packages/mcp-tools`). Do not add new callers there.
103
+
104
+ **Removed tools**: `enqueue_todo_job` and `bind_worktree_user` no longer exist. Do not reference or recreate them.
92
105
 
93
106
  **Contract**: best-effort. The helper logs and swallows failures — the heartbeat catches anything that slips through. Atomic in-txn enqueue is NOT supported (supabase-js limitation); the worker's bounded staleness (next heartbeat ≤ 24h) is the safety net.
94
107