codebyplan 1.10.3 → 1.11.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.10.3",
3
+ "version": "1.11.1",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -436,7 +436,7 @@ If none match, skip — proceed directly to Step 4.
436
436
  - Aggregate `summary` totals into `round.context.frontend_self_review.summary` (combined critical / warning / suggestion / auto_fixed / out_of_scope_fixes).
437
437
 
438
438
  4. **Surface non-mechanical findings** to the round summary:
439
- - `baseline_regression` and `rendered_visual` findings from `cbp-frontend-ui` are NOT auto-fixed (root cause is typically in app state/data, not styling) — surface for `cbp-testing-qa-agent` Phase 4b to convert into mandatory user QA items.
439
+ - `baseline_regression` and `rendered_visual` findings from `cbp-frontend-ui` are NOT auto-fixed (root cause is typically in app state/data, not styling) — surface in `round.context.frontend_ui_review` findings; `/cbp-round-end` Step 7 surfaces baseline-regression findings as a blocking accept-or-fix gate (baselines never auto-accepted).
440
440
  - `out_of_scope_fixes` from either skill (findings whose target file is outside `files_changed`) — surface in `improvements_noted[]` for follow-up rounds; the scope gate prevented silent absorption.
441
441
 
442
442
  **Why inline (not a separate spawn)**: the post-implementation review consumes the same files the executor just touched. Spawning a separate agent doubles token cost (re-reading the files) and serialises wall time; invoking via Skill keeps both review passes inside the executor's working memory and lets fixes apply with the same Edit/Write tools that wrote the original code. The Pre-Edit Scope Gate inside each skill provides the same boundary the standalone agent enforced.
@@ -80,7 +80,6 @@ Compare task work against `checkpoint.goal`:
80
80
 
81
81
  Review all QA items across all rounds:
82
82
  - **Auto items**: Verify all passed (build, lint, types, tests)
83
- - **User items**: Verify all marked pass/skip
84
83
  - **Default items**: Verify all resolved (pass or skipped with reason)
85
84
 
86
85
  **E2E pass vs skipped distinction**: When reading `auto_qa.items[]` for `check: 'e2e'`, do NOT conflate `status: 'pass'` with `status: 'skipped'`. A spec that ran with `passed === 0 && skipped > 0` for any path touching `files_changed` is a hard fail, not a pass — verdict text MUST explicitly call this out: "E2E spec authored but assertions did not execute (skip-gated)." Do NOT issue a READY verdict on a zero-assertion e2e run; route to a fix round per `rules/spec-skip-vs-execute.md`.
@@ -289,7 +289,7 @@ For each failed test, classify into exactly one category:
289
289
  | `auth` | Login-page redirect after credential submit, 401 on authenticated request, `invalid_grant`, `email_not_confirmed` | AskUserQuestion as in Step 6.5.3 |
290
290
  | `access` | 403, 404 on a route the user should have access to, RLS policy denial text, missing seed data | AskUserQuestion: "Test failed with access error: `{error}`. Seed data or RLS policy may be missing. Options: (1) reply with steps you took to fix, (2) abort." |
291
291
  | `flake` | Timeout on first run, passes on immediate retry, network jitter | Retry up to 3 times before reclassifying to `real` |
292
- | `visual_regression` | `toHaveScreenshot` pixel-diff exceeded threshold | Do NOT retry. Include baseline + actual paths in `screenshots[]` with `baseline_diff_pct`. Do NOT auto-accept baseline — leave for `frontend-ui` (`/cbp-round-execute` Step 5b under `phase: 'screenshot_review'`) and user QA. |
292
+ | `visual_regression` | `toHaveScreenshot` pixel-diff exceeded threshold | Do NOT retry. Include baseline + actual paths in `screenshots[]` with `baseline_diff_pct`. Do NOT auto-accept baseline — leave for `frontend-ui` (`/cbp-round-execute` Step 5b under `phase: 'screenshot_review'`); baseline regressions surface at `/cbp-round-end` Step 7 as a blocking gate. |
293
293
  | `real` | Assertion failure on app behavior (text missing, wrong state, navigation broken) | Attempt fix (see Step 8), then report to executor |
294
294
 
295
295
  Failures with `category` of `env`, `auth`, or `access` MUST NOT be counted as test failures in `test_results.failed` until pre-flight passes — they block the run instead.
@@ -355,7 +355,7 @@ Populate all output contract fields. Include test file paths in `tests_written`,
355
355
  ## Integration
356
356
 
357
357
  - **Spawned by**: `/cbp-round-execute` Step 5 (parallel sibling of `testing-qa-agent`); also invoked by `/cbp-checkpoint-check` (TASK-2 deliverable) with `whole_checkpoint_mode: true`
358
- - **Parallel sibling**: `cbp-testing-qa-agent` (owns build/lint/types/unit/audit + single-source visual-QA items). **Fully independent — no cross-read.** This agent's screenshots are consumed by `/cbp-round-execute` Step 5b (`frontend-ui` skill, `phase: 'screenshot_review'`) which writes `round.context.frontend_ui_review.findings`; downstream user_qa items from those findings are aggregated at `/cbp-round-end` Step 3b.
358
+ - **Parallel sibling**: `cbp-testing-qa-agent` (owns build/lint/types/unit/audit). **Fully independent — no cross-read.** This agent's screenshots are consumed by `/cbp-round-execute` Step 5b (`frontend-ui` skill, `phase: 'screenshot_review'`) which writes `round.context.frontend_ui_review.findings`; baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (baselines never auto-accepted).
359
359
  - **Returns to**: `/cbp-round-execute` which persists output to `round.context.e2e_output`. Step 5b then invokes the `frontend-ui` skill with `phase: 'screenshot_review'` and the screenshots; Step 6 considers `e2e_output.test_results.failed > 0` and `status === 'failed'` as hard-fail signals.
360
360
  - **Reads**: `.claude/context/testing/e2e.md`, page/screen source files, existing specs, `.env.local`, `.codebyplan/server.json` `port_allocations`, MCP `get_repos` (for `tech_stack` reconciliation at Step 1.5)
361
361
  - **May modify source**: Only to add testID/data-testid attributes
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  scope: org-shared
3
3
  name: cbp-testing-qa-agent
4
- description: Combined testing, QA generation, and default checklists. Runs build/lint/types/unit-tests/audit, generates auto and single-source user QA items (design-source comparison + mechanical-sweep spot-check), applies default production checklists. Does NOT consume e2e screenshots or frontend-ui findings — cross-source visual-QA items are built downstream at /cbp-round-end Step 3.
4
+ description: Combined testing, QA generation, and default checklists. Runs build/lint/types/unit-tests/audit, generates auto QA items, applies default production checklists. Does NOT consume e2e screenshots or frontend-ui findings.
5
5
  tools: Read, Glob, Grep, Bash, AskUserQuestion
6
6
  model: sonnet
7
7
  effort: xhigh
@@ -16,11 +16,11 @@ Combined testing, QA generation, and default production checklists in a single a
16
16
  Single agent that handles non-e2e quality validation in the per-wave validation phase of `/cbp-round-execute` Step 5:
17
17
  - Run all 18 automated checks (work + quality verification)
18
18
  - **EXECUTE** automated testing commands (build, lint, types, unit tests, visual checks, audit)
19
- - Generate auto and user QA items
19
+ - Generate auto QA items
20
20
  - Apply default production checklist items
21
21
  - Detect unrelated issues and missing tests
22
22
 
23
- E2E execution (Playwright / Maestro / WebDriverIO / XCUITest / vscode-test) is owned by `cbp-test-e2e-agent`, spawned in parallel with this agent by `/cbp-round-execute` Step 5. **The two agents are fully independent — this agent does NOT read `round.context.e2e_output` or `round.context.frontend_ui_review`.** Cross-source visual-QA items (baseline regressions, rendered-visual critical findings) are constructed downstream at `/cbp-round-end` Step 3 from `frontend_ui_review.findings`. This agent emits only single-source visual-QA items (Phase 4b.1 design-source comparison + Phase 4b.2 mechanical-sweep spot-check).
23
+ E2E execution (Playwright / Maestro / WebDriverIO / XCUITest / vscode-test) is owned by `cbp-test-e2e-agent`, spawned in parallel with this agent by `/cbp-round-execute` Step 5. **The two agents are fully independent — this agent does NOT read `round.context.e2e_output` or `round.context.frontend_ui_review`.** This agent emits auto QA items and default checklist items. Baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (an explicit accept-or-fix user decision; baselines are NEVER auto-accepted).
24
24
 
25
25
  ## Input Contract
26
26
 
@@ -63,13 +63,6 @@ output:
63
63
  stdout: string # captured command output
64
64
  stderr: string # captured error output
65
65
  screenshots: [{page, viewport, status, file}] # visual check only
66
- user_qa:
67
- items:
68
- - type: 'user'
69
- check: string
70
- status: 'pending'
71
- instructions: string
72
- round_number: number
73
66
  default_checklist:
74
67
  items:
75
68
  - type: 'default'
@@ -256,7 +249,7 @@ Report findings in `build_analysis` even if the build succeeded.
256
249
 
257
250
  When `files_changed` includes a new route file under any `apps/*/src/app/api/` or `apps/*/src/app/mcp/` directory:
258
251
  - If dev server is running: curl the endpoint without credentials, assert response is 401/403 (not 200). Log as auto QA item `auth_enforcement`.
259
- - If dev server is not running: generate a user QA item with the exact curl command and expected 401 status.
252
+ - If dev server is not running: log a skipped auto QA item with the exact curl command noted in `notes` for reference.
260
253
 
261
254
  ### Phase 3.58: Missing Unit Tests for New API Routes
262
255
 
@@ -331,22 +324,7 @@ This aligns with `immediate-issue-capture.md` (resolve-in-current-scope by defau
331
324
 
332
325
  **4a. Auto QA items**: Generate from Phase 3 results. One item per test category. Include stdout/stderr.
333
326
 
334
- **4b. User QA items**: Targeted verification items only a human can check.
335
-
336
- **4b.0. Connection smoke test suppression**: Before emitting any connection smoke test user QA item (MCP connection, server health, service wiring), check whether the governing config file is unchanged. Governing config map: MCP (Claude Code) → `.mcp.json`; Dev server → `.env.local`, `.codebyplan/server.json` port_allocations; API integrations → `.env.local`. **Suppression rule**: if the governing config is NOT in `files_changed` AND `git diff HEAD -- <config>` is empty, log `{type:"user", check:"<name>", status:"skipped", notes:"Governing config <file> unchanged in this round; connection behavior is unaffected."}` — do NOT emit a pending user QA item.
337
-
338
- **4b.1. Design source comparison** (mandatory when `has_ui_work` is true): Search the project's design-sources directory (e.g., `docs/design/`, `docs/development/product/sources/design/`) for PNG files matching the page or component being changed. If design PNGs exist, add a mandatory user QA item with check: "Design source fidelity" and instructions: "Compare rendered output against design source PNG. Verify: column layout matches, control shapes match (flat vs pill vs toggle), background colors match, row structure and dividers match, action controls are in the correct column."
339
-
340
- **4b.2. Volume-gated mechanical-sweep spot-check** (volume-triggered, runs regardless of `has_ui_work`): when `files_changed.length > 100` AND the round is mechanical (`work_type == 'mechanical'` OR round requirements match `/sweep|auto.?fix|batch|backlog/i`), emit a mandatory user QA item:
341
-
342
- - `check`: `"High-volume mechanical round spot-check"`
343
- - `status`: `"pending"`
344
- - `instructions`: "This round modified {N} files mechanically. Open 3–5 changed files in the running app and verify behavior is unchanged. Prioritize files with business logic (services, hooks, reducers) over pure presentation. Spot-check at least one file from each touched module."
345
- - `round_number`: current
346
-
347
- Volume gating exists because automated checks (build/lint/types/unit) verify shape but not behaviour preservation; large mechanical sweeps (auto-fix, codemod, refactor) can pass all gates while silently changing semantics in code paths the test suite doesn't cover.
348
-
349
- **4c. Default checklist items**: See Phase 5.
327
+ **4b. Default checklist items**: See Phase 5.
350
328
 
351
329
  ### Phase 5: Default Production Checklist
352
330
 
@@ -379,7 +357,7 @@ Return complete output contract.
379
357
  - Build output analyzed for warnings/deprecations/console.logs (with client/server classification)
380
358
  - npm audit executed, vulnerabilities reported by severity, critical/high contribute to hard_fail
381
359
  - Unrelated issues discovered and logged
382
- - Auto, user, and default QA items generated
360
+ - Auto and default QA items generated
383
361
  - `hard_fail` flag correctly set
384
362
  - **Vitest/Jest/Cargo unit-test hard_fail enforced** when source files changed
385
363
  - E2E execution + preflight delegated entirely to `test-e2e-agent` (this agent never runs Playwright/Maestro/wdio/etc.)
@@ -397,4 +375,4 @@ Return complete output contract.
397
375
 
398
376
  - **Spawned by**: `/cbp-round-execute` Step 5 (per-wave; runs in parallel with `test-e2e-agent` and may also run in parallel with next wave's executor)
399
377
  - **Parallel sibling**: `cbp-test-e2e-agent` (fully independent — no cross-read; both agents complete on their own timeline using only their own inputs)
400
- - **Output consumed by**: `/cbp-round-execute` Step 6 (hard-fail routing — this agent's `totals.hard_fail` is OR'd with `e2e_output.test_results.failed > 0` and `e2e_output.status === 'failed'`), `/cbp-round-end` Step 3a (reads this agent's `user_qa[]` for single-source items: design-source comparison, mechanical-sweep spot-check, connection smoke). Note: round-end Step 3b independently reads `round.context.frontend_ui_review.findings` for cross-source baseline-regression + rendered-visual user_qa that read is unrelated to this agent's output. The two sub-steps run independently; this agent has zero coupling to frontend-ui findings.
378
+ - **Output consumed by**: `/cbp-round-execute` Step 6 (hard-fail routing — this agent's `totals.hard_fail` is OR'd with `e2e_output.test_results.failed > 0` and `e2e_output.status === 'failed'`), `/cbp-round-end` Step 3 (reads this agent's `auto_qa[]` and `default_checklist[]`). This agent does not emit `user_qa` items; baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (an explicit accept-or-fix user decision; baselines are NEVER auto-accepted).
@@ -6,41 +6,33 @@ Hook registration lives in [`hooks/hooks.json`](./hooks.json) — PreToolUse, Po
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
 
9
+ **Multi-runtime renderers.** Both `cbp-statusline.sh` and `cbp-subagent-statusline.sh` are *dispatchers*: each reads a per-device renderer choice and execs a `bash`, `node` (`.mjs`), or `python` (`.py`) variant that produces **byte-identical** output. The selected `.sh` always remains the wired entry point, so the bash renderer is a universal, never-error fallback when the chosen runtime is unavailable. Selection + display options come from two `.codebyplan/` files (see [Statusline configuration](#statusline-configuration) below).
10
+
9
11
  ---
10
12
 
11
13
  ## Hooks included
12
14
 
13
15
  ### `cbp-statusline.sh` — auto-registered via `settings.project.base.json`
14
16
 
15
- Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON from stdin (Claude Code's `statusLine` stream) and emits ANSI-coloured output. All fields from the Claude Code `statusLine` schema are parsed in a single `jq` invocation.
17
+ Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON from stdin (Claude Code's `statusLine` stream) and emits ANSI-coloured output. The file is a *dispatcher + bash renderer*: it selects the configured runtime (bash / node / python), execs the matching renderer when available, and otherwise renders inline in bash. The `node` (`cbp-statusline.mjs`) and `python` (`cbp-statusline.py`) variants produce **byte-identical** output.
16
18
 
17
19
  **Render lines:**
18
20
 
19
- - **Line 1 — Identity**: single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model display name and id; `effort:LEVEL` (when set); `thinking:on` (only when thinking is enabled); `style:NAME` (when output style is not "default"); `v:VERSION`; `[VIM_MODE]`
21
+ - **Line 1 — Identity**: **folder name** (basename of `cwd`) + **git branch** (always shown — taken from `worktree.branch`, else derived via `git -C <cwd> rev-parse --abbrev-ref HEAD`, so it appears even without a registered worktree); single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model **display name only**; `effort:LEVEL` (when set); `thinking:on` (only when thinking is enabled); `style:NAME` (when output style is not "default"); `[VIM_MODE]`
20
22
  - **Line 2 — Context**: 20-character progress bar (green / yellow / red at 50% / 75%); `used% / ctx_size`; per-call token breakdown — `in:N out:N cache_cr:N cache_rd:N` (from `context_window.current_usage`); `⚠ 200k+` banner when `exceeds_200k_tokens` is true
21
23
  - **Line 3 — Cost**: session cost in USD; total wall duration (`dur:`) and API-only duration (`api:`) via human-readable format; lines added/removed
22
24
  - **Line 4 — Rate limits**: `5h: PCT% (resets in Xh)` and `7d: PCT% (resets in Xd)` with relative-time helper; colour-coded green / yellow / red at 60% / 80%; emitted only when at least one window has a non-zero `resets_at` epoch
23
25
  - **Line 5 — Repo / PR**: `host/owner/name` (from `workspace.repo`); `PR #N <review_state> <url>` (when `pr.number` is present); emitted when at least one segment is present
24
26
  - **Line 6 — Worktree**: `name @ branch (was: original_branch)` and truncated path; emitted only when `worktree.name` is present
25
27
 
26
- **Env-var toggles** set any to `1` to suppress that section:
27
-
28
- | Variable | Suppresses |
29
- |---|---|
30
- | `CBP_STATUSLINE_HIDE_IDENTITY` | Line 1 — model / session / agent identity |
31
- | `CBP_STATUSLINE_HIDE_CONTEXT` | Line 2 — context bar, token breakdown |
32
- | `CBP_STATUSLINE_HIDE_COST` | Line 3 — cost, duration, lines changed |
33
- | `CBP_STATUSLINE_HIDE_RATE_LIMITS` | Line 4 — 5h / 7d rate-limit windows |
34
- | `CBP_STATUSLINE_HIDE_REPO_PR` | Line 5 — repo identity and PR |
35
- | `CBP_STATUSLINE_HIDE_WORKTREE` | Line 6 — worktree name / branch / path |
36
- | `CBP_STATUSLINE_NO_COLOR` | Strip all ANSI colour codes (also honoured by `$NO_COLOR`) |
28
+ > **Removed in the multi-runtime rewrite:** the `v:VERSION` field, the `sid:…` session-id field, the `log:…` transcript field, and the redundant model-id-in-parens beside the display name.
37
29
 
38
30
  **Sample invocation** (pipe mock JSON, run from the hooks directory; `NO_COLOR=1` shown so the comment-output below matches verbatim without ANSI codes):
39
31
 
40
32
  ```bash
41
- echo '{"model":{"display_name":"Opus","id":"claude-opus-4"},"context_window":{"used_percentage":25,"context_window_size":200000,"current_usage":{"input_tokens":50000,"output_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' \
33
+ echo '{"model":{"display_name":"Opus 4.8","id":"claude-opus-4-8[1m]"},"cwd":"/work/myrepo","worktree":{"branch":"feat/x"},"context_window":{"used_percentage":25,"context_window_size":200000,"current_usage":{"input_tokens":50000,"output_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' \
42
34
  | NO_COLOR=1 bash cbp-statusline.sh
43
- # Opus (claude-opus-4)
35
+ # myrepo ⎇feat/x Opus 4.8
44
36
  # ▓▓▓▓▓░░░░░░░░░░░░░░░ 25%/200.0K in:50.0K out:1.0K cache_cr:0 cache_rd:0
45
37
  # $0.0000 dur:0s api:0s +0 -0 lines
46
38
  ```
@@ -49,7 +41,57 @@ Pure renderer — no project state read, no commands run. Works on any host.
49
41
 
50
42
  **Blocks vs warns**: neither — it only renders. Cannot fail your workflow.
51
43
 
52
- **Opt out**: set `CBP_STATUSLINE_HIDE_*` env vars to suppress individual sections, or remove the `statusLine` block from your `.claude/settings.json`.
44
+ **Opt out**: hide individual lines via config or `CBP_STATUSLINE_HIDE_*` env vars (see below), or remove the `statusLine` block from your `.claude/settings.json`.
45
+
46
+ ---
47
+
48
+ ### `cbp-subagent-statusline.sh` — auto-registered via `settings.project.base.json`
49
+
50
+ Renders one row per active subagent task below the main statusline. Reads JSON from stdin (Claude Code's `subagentStatusLine` stream, with a `tasks[]` array) and emits the upstream **JSON-per-line** protocol — one `{"id":…,"content":…}` object per task. Like the main statusline it is a dispatcher with byte-identical `bash` / `node` (`cbp-subagent-statusline.mjs`) / `python` (`cbp-subagent-statusline.py`) renderers. Each row carries a status icon + colour (running `●` / pending `○` / completed `✓` / failed `✗`), name, type, description, `tokens:N`, and `age:X`, ANSI-aware-truncated to the terminal's `columns`.
51
+
52
+ ```bash
53
+ echo '{"columns":0,"tasks":[{"id":"a1","name":"Explorer","type":"general","status":"running","description":"scanning files","startTime":0,"tokenCount":12000}]}' \
54
+ | NO_COLOR=1 bash cbp-subagent-statusline.sh
55
+ # {"id":"a1","content":"● Explorer (general) — scanning files · tokens:12.0K"}
56
+ ```
57
+
58
+ ---
59
+
60
+ ### Statusline configuration
61
+
62
+ Both statuslines are driven by two `.codebyplan/` files plus environment-variable overrides. Precedence is **env > config > default**.
63
+
64
+ | File | Scope | Shape | Purpose |
65
+ |---|---|---|---|
66
+ | `.codebyplan/statusline.json` | **committed** (team-shared) | `{ "lines": { "identity", "context", "cost", "rate_limits", "repo_pr", "worktree": bool }, "no_color": bool }` | Which of the 6 main lines render, and whether colour is stripped. All lines default `true`; `no_color` defaults `false`. |
67
+ | `.codebyplan/statusline.local.json` | **gitignored** (per-device) | `{ "renderer": "bash" \| "node" \| "python" }` | Which runtime renders. Per-device because runtime availability is machine-specific. Default `bash`. |
68
+
69
+ Set or inspect the renderer with the CLI (writes the gitignored local file):
70
+
71
+ ```bash
72
+ codebyplan statusline # print the current renderer + which runtimes are available
73
+ codebyplan statusline node # switch this device to the node renderer (or: bash | python)
74
+ codebyplan statusline --python # flag form is equivalent
75
+ ```
76
+
77
+ The same `--bash` / `--node` / `--python` flags are accepted on `codebyplan claude install` and `codebyplan claude update`, and `codebyplan setup` asks once when the renderer is unset. When the chosen runtime is missing at render time, the statusline silently falls back to bash (it never errors).
78
+
79
+ **Env-var overrides** — set to `1`; these win over the committed config:
80
+
81
+ | Variable | Effect |
82
+ |---|---|
83
+ | `CBP_STATUSLINE_HIDE_IDENTITY` | Hide line 1 — folder / branch / model / identity |
84
+ | `CBP_STATUSLINE_HIDE_CONTEXT` | Hide line 2 — context bar, token breakdown |
85
+ | `CBP_STATUSLINE_HIDE_COST` | Hide line 3 — cost, duration, lines changed |
86
+ | `CBP_STATUSLINE_HIDE_RATE_LIMITS` | Hide line 4 — 5h / 7d rate-limit windows |
87
+ | `CBP_STATUSLINE_HIDE_REPO_PR` | Hide line 5 — repo identity and PR |
88
+ | `CBP_STATUSLINE_HIDE_WORKTREE` | Hide line 6 — worktree name / branch / path |
89
+ | `CBP_STATUSLINE_NO_COLOR` | Strip all ANSI colour codes (also honoured by `$NO_COLOR`) |
90
+ | `CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION` | Subagent rows: omit the description segment |
91
+ | `CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS` | Subagent rows: omit the `tokens:N` segment |
92
+ | `CBP_SUBAGENT_STATUSLINE_NO_COLOR` | Subagent rows: strip ANSI colour codes |
93
+
94
+ The committed `statusline.json` line toggles apply only to the six main-statusline lines; the subagent rows honour only the colour / description / tokens controls above.
53
95
 
54
96
  ---
55
97
 
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ // @hook: NOT-A-HOOK (statusLine renderer, invoked via the cbp-statusline.sh dispatcher)
3
+ // Claude Code Status Line — node renderer (ESM; .mjs forces ESM regardless of the
4
+ // host repo's package.json "type", so the script is portable into any consumer).
5
+ // Byte-identical output to the bash renderer in cbp-statusline.sh and the python
6
+ // renderer in cbp-statusline.py. See that .sh file for the full option/env/seam
7
+ // contract. Selected via .codebyplan/statusline.local.json -> {"renderer":"node"}.
8
+ //
9
+ // Reads stdin JSON; resolves .codebyplan/ root from CBP_STATUSLINE_ROOT (set by the
10
+ // dispatcher) or argv[2], else the script's ../.. directory. Never throws to stdout.
11
+
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { execFileSync } from "node:child_process";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function main() {
20
+ const input = fs.readFileSync(0, "utf8");
21
+ let data;
22
+ try {
23
+ data = JSON.parse(input);
24
+ } catch {
25
+ data = {};
26
+ }
27
+
28
+ // ---- Root resolution (matches the dispatcher contract) --------------------
29
+ let root = process.env.CBP_STATUSLINE_ROOT || process.argv[2] || "";
30
+ if (!root) {
31
+ root = path.resolve(__dirname, "..", "..");
32
+ }
33
+
34
+ // ---- jq `//` semantics: default on null/undefined (and false → false) -----
35
+ const get = (obj, keys, dflt) => {
36
+ let cur = obj;
37
+ for (const k of keys) {
38
+ if (cur == null || typeof cur !== "object") return dflt;
39
+ cur = cur[k];
40
+ }
41
+ return cur == null ? dflt : cur;
42
+ };
43
+
44
+ const MODEL_ID = get(data, ["model", "id"], "");
45
+ const MODEL_NAME = get(data, ["model", "display_name"], "");
46
+ const CWD = get(data, ["cwd"], "");
47
+ const WS_CURRENT_DIR = get(data, ["workspace", "current_dir"], "");
48
+ const WS_REPO_HOST = get(data, ["workspace", "repo", "host"], "");
49
+ const WS_REPO_OWNER = get(data, ["workspace", "repo", "owner"], "");
50
+ const WS_REPO_NAME = get(data, ["workspace", "repo", "name"], "");
51
+ const COST = get(data, ["cost", "total_cost_usd"], 0);
52
+ const DURATION = get(data, ["cost", "total_duration_ms"], 0);
53
+ const API_DURATION = get(data, ["cost", "total_api_duration_ms"], 0);
54
+ const LINES_ADD = get(data, ["cost", "total_lines_added"], 0);
55
+ const LINES_DEL = get(data, ["cost", "total_lines_removed"], 0);
56
+ const CTX_SIZE = get(data, ["context_window", "context_window_size"], 200000);
57
+ const CTX_PCT = get(data, ["context_window", "used_percentage"], 0);
58
+ const CUR_IN = get(
59
+ data,
60
+ ["context_window", "current_usage", "input_tokens"],
61
+ 0
62
+ );
63
+ const CUR_OUT = get(
64
+ data,
65
+ ["context_window", "current_usage", "output_tokens"],
66
+ 0
67
+ );
68
+ const CACHE_CREATE = get(
69
+ data,
70
+ ["context_window", "current_usage", "cache_creation_input_tokens"],
71
+ 0
72
+ );
73
+ const CACHE_READ = get(
74
+ data,
75
+ ["context_window", "current_usage", "cache_read_input_tokens"],
76
+ 0
77
+ );
78
+ const EXCEEDS_200K = get(data, ["exceeds_200k_tokens"], false);
79
+ const EFFORT = get(data, ["effort", "level"], "");
80
+ const THINKING = get(data, ["thinking", "enabled"], false);
81
+ const RATE_5H_PCT = get(
82
+ data,
83
+ ["rate_limits", "five_hour", "used_percentage"],
84
+ ""
85
+ );
86
+ const RATE_5H_RESETS = get(
87
+ data,
88
+ ["rate_limits", "five_hour", "resets_at"],
89
+ 0
90
+ );
91
+ const RATE_7D_PCT = get(
92
+ data,
93
+ ["rate_limits", "seven_day", "used_percentage"],
94
+ ""
95
+ );
96
+ const RATE_7D_RESETS = get(
97
+ data,
98
+ ["rate_limits", "seven_day", "resets_at"],
99
+ 0
100
+ );
101
+ const SESSION_NAME = get(data, ["session_name"], "");
102
+ const OUTPUT_STYLE = get(data, ["output_style", "name"], "");
103
+ const VIM_MODE = get(data, ["vim", "mode"], "");
104
+ const AGENT_NAME = get(data, ["agent", "name"], "");
105
+ const PR_NUMBER = get(data, ["pr", "number"], "");
106
+ const PR_URL = get(data, ["pr", "url"], "");
107
+ const PR_REVIEW_STATE = get(data, ["pr", "review_state"], "");
108
+ const WT_NAME = get(data, ["worktree", "name"], "");
109
+ const WT_PATH = get(data, ["worktree", "path"], "");
110
+ const WT_BRANCH = get(data, ["worktree", "branch"], "");
111
+ const WT_ORIG_BRANCH = get(data, ["worktree", "original_branch"], "");
112
+
113
+ // ---- Config: line toggles + no_color --------------------------------------
114
+ const cfg = {
115
+ identity: true,
116
+ context: true,
117
+ cost: true,
118
+ rate_limits: true,
119
+ repo_pr: true,
120
+ worktree: true,
121
+ no_color: false,
122
+ };
123
+ try {
124
+ const raw = fs.readFileSync(
125
+ path.join(root, ".codebyplan", "statusline.json"),
126
+ "utf8"
127
+ );
128
+ const parsed = JSON.parse(raw);
129
+ if (parsed && typeof parsed === "object") {
130
+ if (typeof parsed.no_color === "boolean") cfg.no_color = parsed.no_color;
131
+ if (parsed.lines && typeof parsed.lines === "object") {
132
+ for (const k of [
133
+ "identity",
134
+ "context",
135
+ "cost",
136
+ "rate_limits",
137
+ "repo_pr",
138
+ "worktree",
139
+ ]) {
140
+ if (typeof parsed.lines[k] === "boolean") cfg[k] = parsed.lines[k];
141
+ }
142
+ }
143
+ }
144
+ } catch {
145
+ // absent / invalid → keep defaults
146
+ }
147
+
148
+ // env HIDE=1 > config false > default show
149
+ const shouldShow = (envSuffix, cfgValue) => {
150
+ if (process.env[`CBP_STATUSLINE_HIDE_${envSuffix}`] === "1") return false;
151
+ if (cfgValue === false) return false;
152
+ return true;
153
+ };
154
+
155
+ // ---- Colour setup (env > config) ------------------------------------------
156
+ const noColor =
157
+ (process.env.NO_COLOR != null && process.env.NO_COLOR !== "") ||
158
+ process.env.CBP_STATUSLINE_NO_COLOR === "1" ||
159
+ cfg.no_color === true;
160
+ const C = noColor
161
+ ? {
162
+ RST: "",
163
+ DIM: "",
164
+ BOLD: "",
165
+ GREEN: "",
166
+ YELLOW: "",
167
+ RED: "",
168
+ CYAN: "",
169
+ MAGENTA: "",
170
+ BLUE: "",
171
+ }
172
+ : {
173
+ RST: "\x1b[0m",
174
+ DIM: "\x1b[2m",
175
+ BOLD: "\x1b[1m",
176
+ GREEN: "\x1b[32m",
177
+ YELLOW: "\x1b[33m",
178
+ RED: "\x1b[31m",
179
+ CYAN: "\x1b[36m",
180
+ MAGENTA: "\x1b[35m",
181
+ BLUE: "\x1b[34m",
182
+ };
183
+
184
+ // ---- Numeric helpers (integer round-half-up; cross-runtime identical) ------
185
+ const numStr = (n) => {
186
+ const x = Number(n);
187
+ if (Number.isFinite(x) && Number.isInteger(x)) return String(x);
188
+ return String(n);
189
+ };
190
+
191
+ const fmtK = (val) => {
192
+ const v = Number(val);
193
+ if (v >= 1000000) {
194
+ const t = Math.floor((v + 50000) / 100000);
195
+ return `${Math.floor(t / 10)}.${t % 10}M`;
196
+ } else if (v >= 1000) {
197
+ const t = Math.floor((v + 50) / 100);
198
+ return `${Math.floor(t / 10)}.${t % 10}K`;
199
+ }
200
+ return String(Math.trunc(v));
201
+ };
202
+
203
+ const fmtCost = (c) => {
204
+ const n = Math.floor(Number(c) * 10000 + 0.5);
205
+ const frac = String(n % 10000).padStart(4, "0");
206
+ return `$${Math.floor(n / 10000)}.${frac}`;
207
+ };
208
+
209
+ const fmtDur = (ms) => {
210
+ const secs = Math.trunc(Number(ms) / 1000);
211
+ if (secs >= 3600)
212
+ return `${Math.floor(secs / 3600)}h${Math.floor((secs % 3600) / 60)}m`;
213
+ if (secs >= 60) return `${Math.floor(secs / 60)}m${secs % 60}s`;
214
+ return `${secs}s`;
215
+ };
216
+
217
+ const cbpNow = () => {
218
+ if (
219
+ process.env.CBP_STATUSLINE_NOW != null &&
220
+ process.env.CBP_STATUSLINE_NOW !== ""
221
+ ) {
222
+ return Math.trunc(Number(process.env.CBP_STATUSLINE_NOW));
223
+ }
224
+ return Math.floor(Date.now() / 1000);
225
+ };
226
+
227
+ const fmtRelTime = (epoch) => {
228
+ const delta = Math.trunc(Number(epoch)) - cbpNow();
229
+ if (delta <= 0) return "now";
230
+ if (delta >= 86400) return `${Math.floor(delta / 86400)}d`;
231
+ if (delta >= 3600) return `${Math.floor(delta / 3600)}h`;
232
+ return `${Math.floor(delta / 60)}m`;
233
+ };
234
+
235
+ const gte = (v, t) => Number(v) >= t;
236
+
237
+ // ---- Folder + branch ------------------------------------------------------
238
+ let FOLDER = "";
239
+ if (CWD) FOLDER = path.basename(CWD);
240
+ else if (WS_CURRENT_DIR) FOLDER = path.basename(WS_CURRENT_DIR);
241
+ let BRANCH = WT_BRANCH;
242
+ if (!BRANCH && CWD) {
243
+ try {
244
+ BRANCH = execFileSync(
245
+ "git",
246
+ ["-C", CWD, "rev-parse", "--abbrev-ref", "HEAD"],
247
+ {
248
+ encoding: "utf8",
249
+ stdio: ["ignore", "pipe", "ignore"],
250
+ }
251
+ ).replace(/\s+$/, "");
252
+ } catch {
253
+ BRANCH = "";
254
+ }
255
+ }
256
+
257
+ const out = [];
258
+
259
+ // ============================================================
260
+ // LINE 1 — Identity
261
+ // ============================================================
262
+ if (shouldShow("IDENTITY", cfg.identity)) {
263
+ let L1 = "";
264
+ if (FOLDER) {
265
+ L1 = `${C.BOLD}${C.BLUE}${FOLDER}${C.RST}`;
266
+ if (BRANCH) L1 += ` ${C.DIM}⎇${C.RST}${C.CYAN}${BRANCH}${C.RST}`;
267
+ L1 += " ";
268
+ }
269
+ if (WT_NAME) {
270
+ L1 += `${C.DIM}wt:${C.RST}${C.MAGENTA}${WT_NAME}${C.RST} `;
271
+ } else if (SESSION_NAME) {
272
+ L1 += `${C.DIM}session:${C.RST}${C.MAGENTA}${SESSION_NAME}${C.RST} `;
273
+ } else if (AGENT_NAME) {
274
+ L1 += `${C.DIM}agent:${C.RST}${C.MAGENTA}${AGENT_NAME}${C.RST} `;
275
+ }
276
+ if (MODEL_NAME) {
277
+ L1 += `${C.BOLD}${C.CYAN}${MODEL_NAME}${C.RST}`;
278
+ } else if (MODEL_ID) {
279
+ L1 += `${C.BOLD}${C.CYAN}${MODEL_ID}${C.RST}`;
280
+ }
281
+ if (EFFORT) L1 += ` ${C.DIM}effort:${C.RST}${EFFORT}`;
282
+ if (THINKING === true) L1 += ` ${C.YELLOW}thinking:on${C.RST}`;
283
+ if (OUTPUT_STYLE && OUTPUT_STYLE !== "default")
284
+ L1 += ` ${C.DIM}style:${C.RST}${OUTPUT_STYLE}`;
285
+ if (VIM_MODE) L1 += ` ${C.DIM}[${VIM_MODE}]${C.RST}`;
286
+ if (L1) out.push(L1);
287
+ }
288
+
289
+ // ============================================================
290
+ // LINE 2 — Context window
291
+ // ============================================================
292
+ if (shouldShow("CONTEXT", cfg.context)) {
293
+ let barColor;
294
+ if (gte(CTX_PCT, 75)) barColor = C.RED;
295
+ else if (gte(CTX_PCT, 50)) barColor = C.YELLOW;
296
+ else barColor = C.GREEN;
297
+
298
+ let filled = Math.floor((Math.trunc(Number(CTX_PCT)) + 4) / 5);
299
+ if (filled > 20) filled = 20;
300
+ const empty = 20 - filled;
301
+ const bar = "▓".repeat(filled) + "░".repeat(empty);
302
+
303
+ let L2 = `${barColor}${bar}${C.RST} ${barColor}${numStr(CTX_PCT)}%${C.RST}${C.DIM}/${fmtK(CTX_SIZE)}${C.RST}`;
304
+ L2 += ` ${C.DIM}in:${C.RST}${C.BLUE}${fmtK(CUR_IN)}${C.RST} ${C.DIM}out:${C.RST}${C.MAGENTA}${fmtK(CUR_OUT)}${C.RST} ${C.DIM}cache_cr:${C.RST}${fmtK(CACHE_CREATE)} ${C.DIM}cache_rd:${C.RST}${fmtK(CACHE_READ)}`;
305
+ if (EXCEEDS_200K === true) L2 += ` ${C.YELLOW}⚠ 200k+${C.RST}`;
306
+ out.push(L2);
307
+ }
308
+
309
+ // ============================================================
310
+ // LINE 3 — Cost
311
+ // ============================================================
312
+ if (shouldShow("COST", cfg.cost)) {
313
+ const L3 = `${C.GREEN}${fmtCost(COST)}${C.RST} ${C.DIM}dur:${C.RST}${fmtDur(DURATION)} ${C.DIM}api:${C.RST}${fmtDur(API_DURATION)} ${C.GREEN}+${numStr(LINES_ADD)}${C.RST} ${C.RED}-${numStr(LINES_DEL)}${C.RST} ${C.DIM}lines${C.RST}`;
314
+ out.push(L3);
315
+ }
316
+
317
+ // ============================================================
318
+ // LINE 4 — Rate limits
319
+ // ============================================================
320
+ if (shouldShow("RATE_LIMITS", cfg.rate_limits)) {
321
+ const has5h = RATE_5H_PCT !== "" && String(RATE_5H_RESETS) !== "0";
322
+ const has7d = RATE_7D_PCT !== "" && String(RATE_7D_RESETS) !== "0";
323
+ if (has5h || has7d) {
324
+ let L4 = "";
325
+ if (has5h) {
326
+ let c5;
327
+ if (gte(RATE_5H_PCT, 80)) c5 = C.RED;
328
+ else if (gte(RATE_5H_PCT, 60)) c5 = C.YELLOW;
329
+ else c5 = C.GREEN;
330
+ L4 = `${C.DIM}5h:${C.RST}${c5}${numStr(RATE_5H_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
331
+ }
332
+ if (has7d) {
333
+ let c7;
334
+ if (gte(RATE_7D_PCT, 80)) c7 = C.RED;
335
+ else if (gte(RATE_7D_PCT, 60)) c7 = C.YELLOW;
336
+ else c7 = C.GREEN;
337
+ const seg7 = `${C.DIM}7d:${C.RST}${c7}${numStr(RATE_7D_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
338
+ L4 = L4 ? `${L4} ${C.DIM}|${C.RST} ${seg7}` : seg7;
339
+ }
340
+ out.push(L4);
341
+ }
342
+ }
343
+
344
+ // ============================================================
345
+ // LINE 5 — Repo / PR
346
+ // ============================================================
347
+ if (shouldShow("REPO_PR", cfg.repo_pr)) {
348
+ let L5 = "";
349
+ if (WS_REPO_HOST) {
350
+ L5 = `${C.DIM}${WS_REPO_HOST}/${WS_REPO_OWNER}/${WS_REPO_NAME}${C.RST}`;
351
+ }
352
+ if (PR_NUMBER !== "") {
353
+ let prSeg = `${C.DIM}PR${C.RST} ${C.CYAN}#${numStr(PR_NUMBER)}${C.RST}`;
354
+ if (PR_REVIEW_STATE) prSeg += ` ${C.DIM}${PR_REVIEW_STATE}${C.RST}`;
355
+ if (PR_URL) prSeg += ` ${C.BLUE}${PR_URL}${C.RST}`;
356
+ L5 = L5 ? `${L5} ${C.DIM}|${C.RST} ${prSeg}` : prSeg;
357
+ }
358
+ if (L5) out.push(L5);
359
+ }
360
+
361
+ // ============================================================
362
+ // LINE 6 — Worktree
363
+ // ============================================================
364
+ if (shouldShow("WORKTREE", cfg.worktree)) {
365
+ if (WT_NAME) {
366
+ let L6 = `${C.MAGENTA}${WT_NAME}${C.RST} ${C.DIM}@${C.RST} ${C.CYAN}${WT_BRANCH}${C.RST}`;
367
+ if (WT_ORIG_BRANCH && WT_ORIG_BRANCH !== WT_BRANCH) {
368
+ L6 += ` ${C.DIM}(was: ${WT_ORIG_BRANCH})${C.RST}`;
369
+ }
370
+ let wtPathDisp = WT_PATH;
371
+ if (WT_PATH.length > 60) wtPathDisp = "..." + WT_PATH.slice(-57);
372
+ L6 += ` ${C.DIM}${wtPathDisp}${C.RST}`;
373
+ out.push(L6);
374
+ }
375
+ }
376
+
377
+ process.stdout.write(out.length ? out.join("\n") + "\n" : "");
378
+ }
379
+
380
+ try {
381
+ main();
382
+ } catch {
383
+ // Statusline must never error to stdout.
384
+ process.exit(0);
385
+ }