codebyplan 1.13.23 → 1.13.25

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.
Files changed (31) hide show
  1. package/dist/cli.js +445 -187
  2. package/package.json +2 -2
  3. package/templates/agents/cbp-cc-executor.md +7 -7
  4. package/templates/agents/cbp-improve-round.md +2 -2
  5. package/templates/agents/cbp-round-executor.md +20 -4
  6. package/templates/agents/cbp-testing-qa-agent.md +3 -3
  7. package/templates/hooks/README.md +1 -1
  8. package/templates/hooks/cbp-statusline.mjs +106 -11
  9. package/templates/hooks/cbp-statusline.py +79 -13
  10. package/templates/hooks/cbp-statusline.sh +97 -17
  11. package/templates/hooks/validate-structure-patterns.sh +1 -1
  12. package/templates/skills/cbp-checkpoint-check/SKILL.md +2 -2
  13. package/templates/skills/cbp-checkpoint-complete/SKILL.md +2 -2
  14. package/templates/skills/cbp-merge-main/SKILL.md +1 -1
  15. package/templates/skills/cbp-round-end/SKILL.md +12 -35
  16. package/templates/skills/cbp-round-end/reference/findings-presentation.md +76 -3
  17. package/templates/skills/cbp-round-execute/SKILL.md +13 -60
  18. package/templates/skills/cbp-round-start/SKILL.md +3 -1
  19. package/templates/skills/cbp-round-update/SKILL.md +1 -1
  20. package/templates/skills/cbp-session-start/SKILL.md +2 -0
  21. package/templates/skills/cbp-ship-configure/SKILL.md +1 -1
  22. package/templates/skills/cbp-ship-configure/reference/supabase.md +2 -2
  23. package/templates/skills/cbp-ship-main/SKILL.md +2 -0
  24. package/templates/skills/cbp-standalone-task-create/SKILL.md +1 -1
  25. package/templates/skills/cbp-task-check/SKILL.md +1 -1
  26. package/templates/skills/cbp-task-complete/SKILL.md +1 -1
  27. package/templates/skills/cbp-task-create/SKILL.md +50 -1
  28. package/templates/skills/cbp-task-start/SKILL.md +2 -2
  29. package/templates/skills/cbp-task-testing/SKILL.md +2 -2
  30. package/templates/skills/cbp-todo/SKILL.md +36 -3
  31. package/templates/skills/cbp-todo/qa-regression.md +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.23",
3
+ "version": "1.13.25",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "tsc --project tsconfig.build.json",
16
16
  "build:npm": "node esbuild.npm.mjs",
17
- "prepublishOnly": "npm run build:npm",
17
+ "prepare": "node -e \"const v=process.env.CI;process.exit(v&&v!=='false'&&v!=='0'?0:1)\" || node esbuild.npm.mjs",
18
18
  "lint": "eslint",
19
19
  "lint:fix": "eslint --fix",
20
20
  "format": "prettier --write \"src/**/*.ts\"",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  scope: org-shared
3
3
  name: cbp-cc-executor
4
- description: Authoring executor for `.claude/` infrastructure. Applies approved changes across rules, skills, agents, context, CLAUDE.md, settings, hooks, and auto-memory — with update-first discipline, scope-marker enforcement, and length-limit awareness. Callable by the main conversation and `/cbp-checkpoint-end`. NEVER called by `round-executor`.
4
+ description: Authoring executor for `.claude/` infrastructure. Applies approved changes across rules, skills, agents, context, CLAUDE.md, settings, hooks, and auto-memory — with update-first discipline, scope-marker enforcement, and length-limit awareness. Callable by the main conversation, `/cbp-checkpoint-end`, and `round-executor` (for in-scope `.claude/` infra deliverables).
5
5
  tools: Read, Write, Edit, Glob, Grep, Skill, Task, AskUserQuestion, mcp__codebyplan__create_task
6
6
  model: sonnet
7
7
  effort: xhigh
@@ -28,15 +28,16 @@ One reusable primitive for applying batches of approved `.claude/` changes. Cent
28
28
  | ---------------------------- | ----------------------------------------------------- | -------------------------------------------------- |
29
29
  | Main conversation | User directly asks Claude to update a rule/skill/etc. | Claude spawns the agent instead of editing by hand |
30
30
  | `/cbp-checkpoint-end` | Post-ship documentation / CLAUDE.md updates | Future |
31
+ | `round-executor` | A round's planned deliverables include `.claude/` infra | Executor delegates the batch here (`source: 'round-executor'`) instead of looping `/cbp-build-cc-*` per file |
31
32
 
32
- **`round-executor` NEVER calls this agent.** Round execution stays code-only. Infrastructure needs discovered mid-round are captured as tasks per `rules/immediate-issue-capture.md`, addressed through fix or a subsequent task.
33
+ **`round-executor` MAY call this agent** to apply IN-SCOPE `.claude/` infrastructure deliverables — paths already in the round's approved `files_to_modify[]`. This is distinct from the immediate-issue-capture path: infrastructure needs *discovered* mid-round that fall OUTSIDE the round's scope are still captured as tasks per `cbp-task-create` Step 3.5 "Immediate Issue Capture Contract" never routed through this agent.
33
34
 
34
35
  ## Input Contract
35
36
 
36
37
  ```yaml
37
38
  input:
38
39
  repo_id: string
39
- source: 'main' | 'checkpoint-end' # additional internal sources may exist in your CBP setup; extend as needed
40
+ source: 'main' | 'checkpoint-end' | 'round-executor' # additional internal sources may exist in your CBP setup; extend as needed
40
41
  changes:
41
42
  - id: string | number
42
43
  type: 'rule' | 'skill' | 'agent' | 'context' | 'architecture' | 'CLAUDE.md' | 'settings' | 'hook' | 'memory'
@@ -102,7 +103,7 @@ For each change in `input.changes`, pre-flight in order:
102
103
 
103
104
  | `source` | Expected | On missing/generic |
104
105
  | ------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
105
- | `main` / `checkpoint-end` | Often absent — caller is ad-hoc | Derive them here: use Phase 1 inventory to list candidates, and fill `why_not_existing` with the agent's own reasoning. Do not reject. |
106
+ | `main` / `checkpoint-end` / `round-executor` | Often absent — caller is ad-hoc or plan-driven | Derive them here: use Phase 1 inventory to list candidates, and fill `why_not_existing` with the agent's own reasoning. Do not reject. |
106
107
 
107
108
  4. **Length pre-flight** — for `update`, read current length; reject if the requested change would exceed the block limit in `validate-structure-lengths.sh`. Offer a split proposal via `AskUserQuestion` instead.
108
109
 
@@ -134,7 +135,7 @@ Record every applied change with `authored_via` and `status`.
134
135
 
135
136
  - **Downgrade `create` → `update`** — apply silently, note in output.
136
137
  - **Unclear fit between two existing files** — `AskUserQuestion` with the two candidates and their descriptions.
137
- - **Change exceeds this invocation's scope** (e.g. proposal implies a broader refactor) — create a task via MCP `create_task` per `rules/immediate-issue-capture.md`, record in `deferred_changes` with `task_id_created`.
138
+ - **Change exceeds this invocation's scope** (e.g. proposal implies a broader refactor) — create a task via MCP `create_task` per `cbp-task-create` Step 3.5 "Immediate Issue Capture Contract", record in `deferred_changes` with `task_id_created`.
138
139
  - **Hook or settings change with cross-environment implications** — require explicit `scope` field from caller; if missing or ambiguous, ask.
139
140
 
140
141
  ### Phase 5: Post-Apply Sanity
@@ -204,8 +205,7 @@ Block-limit violations are non-negotiable — split before applying.
204
205
 
205
206
  ## Integration
206
207
 
207
- - **Spawned by**: main conversation (ad-hoc), `/cbp-checkpoint-end` (future)
208
- - **Never spawned by**: `round-executor` (round execution stays code-only)
208
+ - **Spawned by**: main conversation (ad-hoc), `/cbp-checkpoint-end` (future), `round-executor` (in-scope `.claude/` infra deliverables, `source: 'round-executor'`)
209
209
  - **Reads**: `.claude/` inventory, `validate-structure-lengths.sh`, target files
210
210
  - **Writes**: `.claude/` files (via `/cbp-build-cc-*` skills for creates, direct Edit for updates)
211
211
  - **Calls skills**: `/cbp-build-cc-rule`, `/cbp-build-cc-skill`, `/cbp-build-cc-claude-file`, `/cbp-build-cc-settings`, `/cbp-build-cc-memory`
@@ -237,9 +237,9 @@ For each issue found:
237
237
  **Corrective-depth advisory**: Before emitting findings, check `round.number` and round provenance:
238
238
  - IF `round.number >= 3` AND the round is corrective (round requirements contain improvement/correction verbs: "fix", "address", "correct", "resolve" against a prior finding)
239
239
  - THEN prepend to the Phase 6 output: `> [advisory] This is round N. Each successive corrective round increases ship-delay risk; consider deferring low/medium findings to a follow-up TASK in the current checkpoint (not a standalone task). Findings still listed in full — your call.`
240
- - Findings remain unchanged; this is informational only. Pairs with `rules/planner-spawn-threshold.md` Path B (which keeps trivial corrective rounds cheap) — together they bound corrective-chain depth.
240
+ - Findings remain unchanged; this is informational only. Pairs with the planner's Path B trivial-corrective bypass (which keeps trivial corrective rounds cheap) — together they bound corrective-chain depth.
241
241
 
242
- **Scope-routing recommendation**: For each finding that exceeds the current round's scope, populate `finding.routing_recommendation` per `rules/immediate-issue-capture.md` "How to Capture":
242
+ **Scope-routing recommendation**: For each finding that exceeds the current round's scope, populate `finding.routing_recommendation` per `cbp-task-create` Step 3.5 "Immediate Issue Capture Contract — How to Capture":
243
243
 
244
244
  | Finding shape | `routing_recommendation` |
245
245
  |---------------|--------------------------|
@@ -2,7 +2,7 @@
2
2
  scope: org-shared
3
3
  name: cbp-round-executor
4
4
  description: Execute approved plan. Receives pre-analyzed deliverables and files list. Focuses on quality implementation. Communicates with user when blocked or needs decisions.
5
- tools: Read, Write, Edit, Glob, Grep, Bash, TaskUpdate, AskUserQuestion, Skill
5
+ tools: Read, Write, Edit, Glob, Grep, Bash, TaskUpdate, AskUserQuestion, Skill, Task
6
6
  model: sonnet
7
7
  effort: xhigh
8
8
  ---
@@ -94,6 +94,7 @@ output:
94
94
  | TaskUpdate | Mark todos as complete |
95
95
  | AskUserQuestion | **Critical** - Ask user when blocked or need decisions |
96
96
  | Skill | **Required** - Invoke routing commands for managed files |
97
+ | Task | Spawn sub-executor agents (Step 3.5): `cbp-database-agent`, `general-purpose`, `cbp-cc-executor` — NEVER `cbp-e2e-*` (orchestrator-owned) |
97
98
 
98
99
  **Key Principle:** If something is unclear or you're blocked, ASK the user. Don't make assumptions.
99
100
 
@@ -140,6 +141,8 @@ Action: {Use Skill tool for routing / Proceed with direct edits}
140
141
 
141
142
  **Critical:** If ANY file requires routing, you MUST use the Skill tool. NEVER use Edit/Write on managed files.
142
143
 
144
+ **Batch alternative:** when `files_to_modify[]` contains MULTIPLE `.claude/` infra files, you MAY delegate the whole batch to `cbp-cc-executor` (Step 3.5) instead of looping the build-cc skills here — cc-executor applies the same routing with centralized update-first / scope-marker / length discipline. Single-file managed edits stay on the Skill-tool routing above.
145
+
143
146
  #### Step 0.1: Scope-Leak Guard (MANDATORY)
144
147
 
145
148
  Before ANY Write/Edit invocation during execution, the target path MUST appear in the active scope. When running in wave mode (`wave` input is present), the scope is `wave.files[]`; otherwise it is `approved_plan.files_to_modify[].path`. Silent absorption of an out-of-scope file is forbidden — every absorbed file dilutes the round's diff and erodes plan-as-contract guarantees.
@@ -183,7 +186,7 @@ Two categories of work are NOT performed by this agent and must be returned to t
183
186
  | Action | Why excluded | Where it goes |
184
187
  |--------|--------------|---------------|
185
188
  | MCP `create_task`, `update_task`, `complete_task`, `add_round`, etc. (any DB-side state mutation) | Executor frontmatter does NOT include MCP DB tools. Tool-not-available errors force orchestrator improvisation. | Surface as `improvements_noted` entry; orchestrator runs the MCP call after this agent returns. Executor never tries to invoke MCP DB tools. |
186
- | Spawning `cbp-e2e-*` specialist agents | Executor's tools list (Read/Write/Edit/Glob/Grep/Bash/TaskUpdate/AskUserQuestion/Skill) does NOT include the `Task` / Agent tool. E2E execution is owned by the `cbp-e2e-*` specialist agents (dispatched per `context/testing/e2e.md`), spawned by `/cbp-round-execute` Step 5 (parallel with `cbp-testing-qa-agent`) and is invoked by the orchestrator. | Set `specialist_needs.review_needed.ux_review` / `ui_review` if applicable. Do NOT attempt to spawn any e2e agent from inside the executor. |
189
+ | Spawning `cbp-e2e-*` specialist agents | E2E execution is **orchestrator-owned by design** — the `cbp-e2e-*` specialist agents (dispatched per `context/testing/e2e.md`) are spawned by `/cbp-round-execute` Step 5 (parallel with `cbp-testing-qa-agent`), NOT by this executor. The executor's `Task` tool exists ONLY for the Step 3.5 sub-executor delegations (`cbp-database-agent`, `general-purpose`, `cbp-cc-executor`) — it is never used to spawn an e2e specialist even though it now physically could. | Set `specialist_needs.review_needed.ux_review` / `ui_review` if applicable. Do NOT attempt to spawn any e2e agent from inside the executor. |
187
190
 
188
191
  If the plan implies either action, complete the rest of the work and surface the carved-out steps in `improvements_noted[]` for the orchestrator to handle.
189
192
 
@@ -305,6 +308,7 @@ When the approved plan includes specialized work, delegate to sub-executor agent
305
308
  |-----------|-------|-----------------|
306
309
  | Supabase migrations, RLS, types | `cbp-database-agent` | Plan includes DB schema changes, RLS policies, or type generation |
307
310
  | Batch identical-structure file writes (≥4 files) | `general-purpose` (background) | Plan has 4+ independent files, no shared state, no ordered dependency |
311
+ | `.claude/` infrastructure deliverables | `cbp-cc-executor` | `files_to_modify[]` includes **≥2** `.claude/` files (rules, skills, agents, context, hooks, settings, CLAUDE.md). A single `.claude/` file edit stays on Step 0 Skill-tool routing |
308
312
 
309
313
  **How to delegate to `cbp-database-agent`:**
310
314
  1. Collect all DB-related steps from the plan
@@ -316,6 +320,18 @@ When the approved plan includes specialized work, delegate to sub-executor agent
316
320
  - Simple Supabase queries in application code (executor handles these)
317
321
  - Only delegate schema/migration/RLS/type generation work
318
322
 
323
+ **How to delegate to `cbp-cc-executor`:**
324
+ 1. Collect every `files_to_modify[]` entry whose path is under `.claude/` (or is `CLAUDE.md`).
325
+ 2. Build cc-executor's `input.changes[]` — one entry per file (`type`, `target`, `action`, `description`, `reasoning`, `source_of_proposal`).
326
+ 3. Spawn `cbp-cc-executor` via Agent tool with `source: 'round-executor'` and those changes.
327
+ 4. Merge its `applied_changes` / `files_changed` into executor output; surface any `deferred_changes` / `conflicts` in `improvements_noted[]`.
328
+
329
+ > **Agent-create guard**: cc-executor REJECTS `type: 'agent'` with `action: 'create'` (its No-Go table — agent creation is a planning-level decision). If `files_to_modify[]` contains an agent **create**, surface it via `AskUserQuestion` BEFORE spawning rather than letting it come back as `status: 'rejected'`. Agent **updates** delegate normally.
330
+
331
+ **When NOT to delegate to `cbp-cc-executor`:**
332
+ - A single `.claude/` file edit — the Step 0 build-cc routing (Skill tool) handles it; reserve cc-executor for multi-file batches or when its centralized update-first / scope / length discipline adds value.
333
+ - Infra needs *discovered* mid-round that are OUTSIDE `files_to_modify[]` — those become tasks per `cbp-task-create` Step 3.5, NEVER a cc-executor call.
334
+
319
335
  #### Background General-Purpose Delegation
320
336
 
321
337
  **Trigger** — all of the following must hold:
@@ -585,7 +601,7 @@ Which would you prefer?
585
601
  - **Spawned by**: `/cbp-round-execute` Step 3 (single-wave 3-AGENT path or per-wave 3-WAVE path)
586
602
  - **Returns to**: `/cbp-round-execute` which collects output and runs per-wave `cbp-testing-qa-agent`
587
603
  - **Depends on**: `cbp-task-planner` agent (provides approved plan)
588
- - **May spawn**: `cbp-database-agent` as sub-executor for Supabase operations. (NOT any `cbp-e2e-*` specialist those are owned by the `cbp-e2e-*` specialist agents (dispatched per `context/testing/e2e.md`), spawned by `/cbp-round-execute` Step 5 per Step 0.2 carve-out.)
604
+ - **May spawn**: `cbp-database-agent` (Supabase operations), `general-purpose` (background batch writes), and `cbp-cc-executor` (in-scope `.claude/` infra deliverables, `source: 'round-executor'`) as sub-executors. (NOT any `cbp-e2e-*` specialist e2e is orchestrator-owned, spawned by `/cbp-round-execute` Step 5 per the Step 0.2 carve-out.)
589
605
 
590
606
  ## Structure Knowledge
591
607
 
@@ -603,4 +619,4 @@ improvements_noted:
603
619
  suggestion: 'Add [pattern] to [skill-name] or create new skill'
604
620
  ```
605
621
 
606
- **Do NOT edit rules/skills during execution** - `cbp-improve-claude` handles `.claude/` updates after task completion.
622
+ **Do NOT make self-directed rules/skills edits based on gaps you discover mid-execution** those go through `cbp-improve-claude` after task completion. This is distinct from approved `.claude/` deliverables already in `files_to_modify[]`, which ARE applied this round via the Step 3.5 `cbp-cc-executor` delegation path (or Step 0 Skill-tool routing for a single file).
@@ -300,11 +300,11 @@ Mandatory dependency vulnerability scan:
300
300
 
301
301
  ### Phase 3.8: Capture Unrelated Issues — Default to Current Scope
302
302
 
303
- For each entry in `unrelated_issues[]` with severity `warning` or `critical`, route per `immediate-issue-capture.md` "How to Capture" — DO NOT default to standalone task creation.
303
+ For each entry in `unrelated_issues[]` with severity `warning` or `critical`, route per `cbp-task-create` Step 3.5 "Immediate Issue Capture Contract — How to Capture" — DO NOT default to standalone task creation.
304
304
 
305
305
  **Routing logic** (walk top-down; use the first row that fits):
306
306
 
307
- 1. **Trivial inline fix** (≤5 min, mechanical, scope-clean per `infra-issue-absorption.md` Trivial-Resolution Exception) — leave the issue in `unrelated_issues[]` with `routing: "inline"` and let the orchestrator absorb it into the current round before `/cbp-round-end`.
307
+ 1. **Trivial inline fix** (≤5 min, mechanical, scope-clean per `cbp-round-end` reference `findings-presentation.md` "Infra Issue Absorption Contract — Trivial-Resolution Exception") — leave the issue in `unrelated_issues[]` with `routing: "inline"` and let the orchestrator absorb it into the current round before `/cbp-round-end`.
308
308
 
309
309
  2. **Related to current task's domain** (most cases) — emit the finding in `unrelated_issues[]` with `routing: "new_round_in_current_task"`. The agent does NOT call `create_task`. `/cbp-round-end` consumes these and includes them as requirements for the next round of the current task.
310
310
 
@@ -318,7 +318,7 @@ For routings 1-4, include each finding in `unrelated_issues[]` with the routing
318
318
 
319
319
  The agent's job is **classification + recommendation**, not unilateral task creation. Standalone creation outside the timed-re-check case requires explicit user confirmation at `/cbp-round-end`.
320
320
 
321
- This aligns with `immediate-issue-capture.md` (resolve-in-current-scope by default; standalone is rare) and `infra-issue-absorption.md` (absorb-by-default since the flip from defer-by-default).
321
+ This aligns with `cbp-task-create` Step 3.5 "Immediate Issue Capture Contract" (resolve-in-current-scope by default; standalone is rare) and `cbp-round-end` reference `findings-presentation.md` "Infra Issue Absorption Contract" (absorb-by-default since the flip from defer-by-default).
322
322
 
323
323
  ### Phase 4: QA Generation
324
324
 
@@ -18,7 +18,7 @@ Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON fro
18
18
 
19
19
  **Render lines:**
20
20
 
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]`
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); `style:NAME` (when output style is not "default"); `[VIM_MODE]`
22
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
23
23
  - **Line 3 — Cost**: session cost in USD; total wall duration (`dur:`) and API-only duration (`api:`) via human-readable format; lines added/removed
24
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
@@ -77,7 +77,6 @@ function main() {
77
77
  );
78
78
  const EXCEEDS_200K = get(data, ["exceeds_200k_tokens"], false);
79
79
  const EFFORT = get(data, ["effort", "level"], "");
80
- const THINKING = get(data, ["thinking", "enabled"], false);
81
80
  const RATE_5H_PCT = get(
82
81
  data,
83
82
  ["rate_limits", "five_hour", "used_percentage"],
@@ -119,6 +118,7 @@ function main() {
119
118
  repo_pr: true,
120
119
  worktree: true,
121
120
  infra_drift: true,
121
+ package_freshness: true,
122
122
  no_color: false,
123
123
  };
124
124
  try {
@@ -138,6 +138,7 @@ function main() {
138
138
  "repo_pr",
139
139
  "worktree",
140
140
  "infra_drift",
141
+ "package_freshness",
141
142
  ]) {
142
143
  if (typeof parsed.lines[k] === "boolean") cfg[k] = parsed.lines[k];
143
144
  }
@@ -190,6 +191,9 @@ function main() {
190
191
  return String(n);
191
192
  };
192
193
 
194
+ // Percentage formatter (integer round-half-up; cross-runtime identical).
195
+ const fmtPct = (n) => String(Math.floor(Number(n) + 0.5));
196
+
193
197
  const fmtK = (val) => {
194
198
  const v = Number(val);
195
199
  if (v >= 1000000) {
@@ -203,9 +207,9 @@ function main() {
203
207
  };
204
208
 
205
209
  const fmtCost = (c) => {
206
- const n = Math.floor(Number(c) * 10000 + 0.5);
207
- const frac = String(n % 10000).padStart(4, "0");
208
- return `$${Math.floor(n / 10000)}.${frac}`;
210
+ const n = Math.floor(Number(c) * 100 + 0.5);
211
+ const frac = String(n % 100).padStart(2, "0");
212
+ return `$${Math.floor(n / 100)}.${frac}`;
209
213
  };
210
214
 
211
215
  const fmtDur = (ms) => {
@@ -281,7 +285,6 @@ function main() {
281
285
  L1 += `${C.BOLD}${C.CYAN}${MODEL_ID}${C.RST}`;
282
286
  }
283
287
  if (EFFORT) L1 += ` ${C.DIM}effort:${C.RST}${EFFORT}`;
284
- if (THINKING === true) L1 += ` ${C.YELLOW}thinking:on${C.RST}`;
285
288
  if (OUTPUT_STYLE && OUTPUT_STYLE !== "default")
286
289
  L1 += ` ${C.DIM}style:${C.RST}${OUTPUT_STYLE}`;
287
290
  if (VIM_MODE) L1 += ` ${C.DIM}[${VIM_MODE}]${C.RST}`;
@@ -325,18 +328,20 @@ function main() {
325
328
  if (has5h || has7d) {
326
329
  let L4 = "";
327
330
  if (has5h) {
331
+ const r5 = fmtPct(RATE_5H_PCT);
328
332
  let c5;
329
- if (gte(RATE_5H_PCT, 80)) c5 = C.RED;
330
- else if (gte(RATE_5H_PCT, 60)) c5 = C.YELLOW;
333
+ if (gte(r5, 80)) c5 = C.RED;
334
+ else if (gte(r5, 60)) c5 = C.YELLOW;
331
335
  else c5 = C.GREEN;
332
- L4 = `${C.DIM}5h:${C.RST}${c5}${numStr(RATE_5H_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
336
+ L4 = `${C.DIM}5h:${C.RST}${c5}${r5}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
333
337
  }
334
338
  if (has7d) {
339
+ const r7 = fmtPct(RATE_7D_PCT);
335
340
  let c7;
336
- if (gte(RATE_7D_PCT, 80)) c7 = C.RED;
337
- else if (gte(RATE_7D_PCT, 60)) c7 = C.YELLOW;
341
+ if (gte(r7, 80)) c7 = C.RED;
342
+ else if (gte(r7, 60)) c7 = C.YELLOW;
338
343
  else c7 = C.GREEN;
339
- const seg7 = `${C.DIM}7d:${C.RST}${c7}${numStr(RATE_7D_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
344
+ const seg7 = `${C.DIM}7d:${C.RST}${c7}${r7}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
340
345
  L4 = L4 ? `${L4} ${C.DIM}|${C.RST} ${seg7}` : seg7;
341
346
  }
342
347
  out.push(L4);
@@ -418,6 +423,96 @@ function main() {
418
423
  }
419
424
  }
420
425
 
426
+ // ============================================================
427
+ // LINE 8 — Package freshness (codebyplan version / sync state)
428
+ // ============================================================
429
+ // Source: .codebyplan/claude-status.local.json (written by background refresh).
430
+ // Inline fallback (cache absent): read .claude/.cbp.manifest.json vs
431
+ // node_modules/codebyplan/package.json. HIDE when guarded (canonical_source /
432
+ // no_manifest / unknown) or when manifest absent (not a managed consumer).
433
+ if (shouldShow("PACKAGE_FRESHNESS", cfg.package_freshness)) {
434
+ let guarded = false;
435
+ let installed = "";
436
+ let newer = false;
437
+ let latest = "";
438
+ let inSync = true;
439
+
440
+ const cachePath = path.join(
441
+ root,
442
+ ".codebyplan",
443
+ "claude-status.local.json"
444
+ );
445
+ if (fs.existsSync(cachePath)) {
446
+ try {
447
+ const cacheRaw = fs.readFileSync(cachePath, "utf8");
448
+ const cache = JSON.parse(cacheRaw);
449
+ if (cache && typeof cache === "object") {
450
+ const gr = cache.guard_reason;
451
+ if (
452
+ gr === "canonical_source" ||
453
+ gr === "no_manifest" ||
454
+ gr === "unknown"
455
+ ) {
456
+ guarded = true;
457
+ } else {
458
+ installed =
459
+ typeof cache.installed === "string" ? cache.installed : "";
460
+ newer = cache.newer === true;
461
+ latest = typeof cache.latest === "string" ? cache.latest : "";
462
+ inSync = cache.in_sync !== false;
463
+ }
464
+ }
465
+ } catch {
466
+ // Unreadable / invalid → keep guarded=false, installed=""
467
+ }
468
+ } else {
469
+ // Inline fallback: no cache — read-only file reads only, no network.
470
+ const manifestPath = path.join(root, ".claude", ".cbp.manifest.json");
471
+ const pkgPath = path.join(
472
+ root,
473
+ "node_modules",
474
+ "codebyplan",
475
+ "package.json"
476
+ );
477
+ if (!fs.existsSync(manifestPath)) {
478
+ // No manifest → not a managed consumer → hide.
479
+ guarded = true;
480
+ } else {
481
+ try {
482
+ const mRaw = fs.readFileSync(manifestPath, "utf8");
483
+ const mParsed = JSON.parse(mRaw);
484
+ const mVer =
485
+ typeof mParsed?.version === "string" ? mParsed.version : "";
486
+ const pRaw = fs.readFileSync(pkgPath, "utf8");
487
+ const pParsed = JSON.parse(pRaw);
488
+ const iVer =
489
+ typeof pParsed?.version === "string" ? pParsed.version : "";
490
+ installed = iVer;
491
+ if (mVer && iVer && mVer !== iVer) {
492
+ // manifest ≠ installed → .claude is out of sync → ⟳ run claude update
493
+ // (mirrors the doctor's version_skip → in_sync:false). No npm info in
494
+ // the offline fallback, so never the ↑ newer-available marker.
495
+ inSync = false;
496
+ }
497
+ } catch {
498
+ // Can't read files → hide segment.
499
+ guarded = true;
500
+ }
501
+ }
502
+ }
503
+
504
+ if (!guarded && installed) {
505
+ let L8 = `${C.DIM}cbp${C.RST} ${installed}`;
506
+ if (newer && latest) {
507
+ L8 += ` ${C.YELLOW}↑${latest}${C.RST}`;
508
+ }
509
+ if (!inSync) {
510
+ L8 += ` ${C.YELLOW}⟳ run claude update${C.RST}`;
511
+ }
512
+ out.push(L8);
513
+ }
514
+ }
515
+
421
516
  process.stdout.write(out.length ? out.join("\n") + "\n" : "");
422
517
  }
423
518
 
@@ -60,7 +60,6 @@ def main():
60
60
  CACHE_READ = _get(data, ["context_window", "current_usage", "cache_read_input_tokens"], 0)
61
61
  EXCEEDS_200K = _get(data, ["exceeds_200k_tokens"], False)
62
62
  EFFORT = _get(data, ["effort", "level"], "")
63
- THINKING = _get(data, ["thinking", "enabled"], False)
64
63
  RATE_5H_PCT = _get(data, ["rate_limits", "five_hour", "used_percentage"], "")
65
64
  RATE_5H_RESETS = _get(data, ["rate_limits", "five_hour", "resets_at"], 0)
66
65
  RATE_7D_PCT = _get(data, ["rate_limits", "seven_day", "used_percentage"], "")
@@ -81,7 +80,7 @@ def main():
81
80
  cfg = {
82
81
  "identity": True, "context": True, "cost": True,
83
82
  "rate_limits": True, "repo_pr": True, "worktree": True,
84
- "infra_drift": True, "no_color": False,
83
+ "infra_drift": True, "package_freshness": True, "no_color": False,
85
84
  }
86
85
  try:
87
86
  with open(os.path.join(root, ".codebyplan", "statusline.json"), "r", encoding="utf-8") as fh:
@@ -91,7 +90,7 @@ def main():
91
90
  cfg["no_color"] = parsed["no_color"]
92
91
  lines = parsed.get("lines")
93
92
  if isinstance(lines, dict):
94
- for k in ["identity", "context", "cost", "rate_limits", "repo_pr", "worktree", "infra_drift"]:
93
+ for k in ["identity", "context", "cost", "rate_limits", "repo_pr", "worktree", "infra_drift", "package_freshness"]:
95
94
  if isinstance(lines.get(k), bool):
96
95
  cfg[k] = lines[k]
97
96
  except Exception:
@@ -134,6 +133,10 @@ def main():
134
133
  return str(int(x))
135
134
  return str(n)
136
135
 
136
+ # Percentage formatter (integer round-half-up; cross-runtime identical).
137
+ def fmt_pct(n):
138
+ return "%d" % int(float(n) + 0.5)
139
+
137
140
  def fmt_k(val):
138
141
  v = float(val)
139
142
  if v >= 1000000:
@@ -145,8 +148,8 @@ def main():
145
148
  return str(int(v))
146
149
 
147
150
  def fmt_cost(c):
148
- n = math.floor(float(c) * 10000 + 0.5)
149
- return "$%d.%04d" % (n // 10000, n % 10000)
151
+ n = math.floor(float(c) * 100 + 0.5)
152
+ return "$%d.%02d" % (n // 100, n % 100)
150
153
 
151
154
  def fmt_dur(ms):
152
155
  secs = math.trunc(float(ms) / 1000)
@@ -217,8 +220,6 @@ def main():
217
220
  l1 += "%s%s%s%s" % (BOLD, CYAN, MODEL_ID, RST)
218
221
  if EFFORT:
219
222
  l1 += " %seffort:%s%s" % (DIM, RST, EFFORT)
220
- if THINKING is True:
221
- l1 += " %sthinking:on%s" % (YELLOW, RST)
222
223
  if OUTPUT_STYLE and OUTPUT_STYLE != "default":
223
224
  l1 += " %sstyle:%s%s" % (DIM, RST, OUTPUT_STYLE)
224
225
  if VIM_MODE:
@@ -273,24 +274,26 @@ def main():
273
274
  if has_5h or has_7d:
274
275
  l4 = ""
275
276
  if has_5h:
276
- if gte(RATE_5H_PCT, 80):
277
+ r5 = fmt_pct(RATE_5H_PCT)
278
+ if gte(r5, 80):
277
279
  c5 = RED
278
- elif gte(RATE_5H_PCT, 60):
280
+ elif gte(r5, 60):
279
281
  c5 = YELLOW
280
282
  else:
281
283
  c5 = GREEN
282
284
  l4 = "%s5h:%s%s%s%%%s %s(resets in %s)%s" % (
283
- DIM, RST, c5, num_str(RATE_5H_PCT), RST, DIM, fmt_rel_time(RATE_5H_RESETS), RST,
285
+ DIM, RST, c5, r5, RST, DIM, fmt_rel_time(RATE_5H_RESETS), RST,
284
286
  )
285
287
  if has_7d:
286
- if gte(RATE_7D_PCT, 80):
288
+ r7 = fmt_pct(RATE_7D_PCT)
289
+ if gte(r7, 80):
287
290
  c7 = RED
288
- elif gte(RATE_7D_PCT, 60):
291
+ elif gte(r7, 60):
289
292
  c7 = YELLOW
290
293
  else:
291
294
  c7 = GREEN
292
295
  seg7 = "%s7d:%s%s%s%%%s %s(resets in %s)%s" % (
293
- DIM, RST, c7, num_str(RATE_7D_PCT), RST, DIM, fmt_rel_time(RATE_7D_RESETS), RST,
296
+ DIM, RST, c7, r7, RST, DIM, fmt_rel_time(RATE_7D_RESETS), RST,
294
297
  )
295
298
  l4 = ("%s %s|%s %s" % (l4, DIM, RST, seg7)) if l4 else seg7
296
299
  out.append(l4)
@@ -343,6 +346,69 @@ def main():
343
346
  if behind > 0:
344
347
  out.append("%s⚠ infra %d behind%s %s→ /cbp-refresh-infra%s" % (YELLOW, behind, RST, DIM, RST))
345
348
 
349
+ # ===== LINE 8 — Package freshness (codebyplan version / sync state) =====
350
+ # Source: .codebyplan/claude-status.local.json (written by background refresh).
351
+ # Inline fallback (cache absent): read .claude/.cbp.manifest.json vs
352
+ # node_modules/codebyplan/package.json. HIDE when guarded (canonical_source /
353
+ # no_manifest / unknown) or when manifest absent (not a managed consumer).
354
+ if should_show("PACKAGE_FRESHNESS", cfg["package_freshness"]):
355
+ _guarded = False
356
+ _installed = ""
357
+ _newer = False
358
+ _latest = ""
359
+ _in_sync = True
360
+
361
+ cache_path = os.path.join(root, ".codebyplan", "claude-status.local.json")
362
+ if os.path.isfile(cache_path):
363
+ try:
364
+ with open(cache_path, "r", encoding="utf-8") as fh:
365
+ cache = json.load(fh)
366
+ if isinstance(cache, dict):
367
+ gr = cache.get("guard_reason")
368
+ if gr in ("canonical_source", "no_manifest", "unknown"):
369
+ _guarded = True
370
+ else:
371
+ _installed = cache.get("installed") if isinstance(cache.get("installed"), str) else ""
372
+ _newer = cache.get("newer") is True
373
+ _latest = cache.get("latest") if isinstance(cache.get("latest"), str) else ""
374
+ _in_sync = cache.get("in_sync") is not False
375
+ except Exception:
376
+ pass # Unreadable / invalid → keep _installed=""
377
+ else:
378
+ # Inline fallback: no cache — read-only file reads only, no network.
379
+ manifest_path = os.path.join(root, ".claude", ".cbp.manifest.json")
380
+ pkg_path = os.path.join(root, "node_modules", "codebyplan", "package.json")
381
+ if not os.path.isfile(manifest_path):
382
+ # No manifest → not a managed consumer → hide.
383
+ _guarded = True
384
+ else:
385
+ try:
386
+ with open(manifest_path, "r", encoding="utf-8") as fh:
387
+ m_data = json.load(fh)
388
+ m_ver = m_data.get("version") if isinstance(m_data, dict) else ""
389
+ m_ver = m_ver if isinstance(m_ver, str) else ""
390
+ with open(pkg_path, "r", encoding="utf-8") as fh:
391
+ p_data = json.load(fh)
392
+ i_ver = p_data.get("version") if isinstance(p_data, dict) else ""
393
+ i_ver = i_ver if isinstance(i_ver, str) else ""
394
+ _installed = i_ver
395
+ if m_ver and i_ver and m_ver != i_ver:
396
+ # manifest != installed -> .claude is out of sync -> run claude update
397
+ # (mirrors the doctor's version_skip -> in_sync:false). No npm info
398
+ # in the offline fallback, so never the up-arrow newer marker.
399
+ _in_sync = False
400
+ except Exception:
401
+ # Can't read files → hide segment.
402
+ _guarded = True
403
+
404
+ if not _guarded and _installed:
405
+ l8 = "%scbp%s %s" % (DIM, RST, _installed)
406
+ if _newer and _latest:
407
+ l8 += " %s↑%s%s" % (YELLOW, _latest, RST)
408
+ if not _in_sync:
409
+ l8 += " %s⟳ run claude update%s" % (YELLOW, RST)
410
+ out.append(l8)
411
+
346
412
  sys.stdout.write(("\n".join(out) + "\n") if out else "")
347
413
 
348
414