@sienklogic/plan-build-run 2.4.1 → 2.6.0

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.
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: statusline
3
+ description: "Install or configure the PBR status line in Claude Code."
4
+ argument-hint: "[install | uninstall | preview]"
5
+ ---
6
+
7
+ **STOP — DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by Claude Code's plugin system. Using the Read tool on this SKILL.md file wastes ~3,000 tokens. Begin executing Step 0 immediately.**
8
+
9
+ ## Step 0 — Immediate Output
10
+
11
+ **Before ANY tool calls**, display this banner:
12
+
13
+ ```
14
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15
+ PLAN-BUILD-RUN ► STATUS LINE
16
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+ ```
18
+
19
+ Then proceed to Step 1.
20
+
21
+ # /pbr:statusline — Status Line Setup
22
+
23
+ The PBR status line displays live project state (phase, plan, status, git branch, context usage) in the Claude Code terminal status bar.
24
+
25
+ ---
26
+
27
+ ## Subcommand Parsing
28
+
29
+ Parse `$ARGUMENTS`:
30
+
31
+ | Argument | Action |
32
+ |----------|--------|
33
+ | `install` or empty | Install/enable the status line |
34
+ | `uninstall` or `remove` | Remove the status line configuration |
35
+ | `preview` | Show what the status line looks like without installing |
36
+
37
+ ---
38
+
39
+ ## Subcommand: install (default)
40
+
41
+ ### Step 1: Locate the status-line script
42
+
43
+ **CRITICAL: You must resolve the correct absolute path to `status-line.js`. Do NOT hardcode paths.**
44
+
45
+ 1. The script lives at `${PLUGIN_ROOT}/scripts/status-line.js`
46
+ 2. Resolve `${PLUGIN_ROOT}` to its absolute path using `pwd` or by checking the plugin root
47
+ 3. If running from a local plugin dir (`claude --plugin-dir .`), the path is the local repo's `plugins/pbr/scripts/status-line.js`
48
+ 4. If running from the installed plugin cache (`~/.claude/plugins/cache/`), use that path
49
+ 5. **Verify the script exists** with `ls` before proceeding. If it doesn't exist, show an error and stop.
50
+
51
+ Store the resolved absolute path as `SCRIPT_PATH`.
52
+
53
+ ### Step 2: Read current settings
54
+
55
+ Read `~/.claude/settings.json` (or `$HOME/.claude/settings.json`).
56
+
57
+ - If the file doesn't exist: start with an empty object `{}`
58
+ - If it exists: parse the JSON content
59
+ - Check if `statusLine` key already exists:
60
+ - If yes and points to the same script: inform user "PBR status line is already installed." and stop (unless they want to reconfigure)
61
+ - If yes but points to a different command: warn user and ask if they want to replace it
62
+
63
+ ### Step 3: Configure settings.json
64
+
65
+ Use AskUserQuestion:
66
+ question: "Install the PBR status line? This adds a `statusLine` entry to ~/.claude/settings.json."
67
+ header: "Install?"
68
+ options:
69
+ - label: "Install" description: "Enable the PBR status line in Claude Code"
70
+ - label: "Preview first" description: "Show a preview before installing"
71
+ - label: "Cancel" description: "Don't install"
72
+ multiSelect: false
73
+
74
+ If "Preview first": run the preview subcommand (show sample output), then ask again.
75
+ If "Cancel": stop.
76
+ If "Install":
77
+
78
+ **CRITICAL: Use Read tool to read the file, then Write to update it. Do NOT use sed or other text manipulation on JSON files.**
79
+
80
+ 1. Read `~/.claude/settings.json`
81
+ 2. Parse the JSON
82
+ 3. Set `statusLine` to:
83
+ ```json
84
+ {
85
+ "type": "command",
86
+ "command": "node \"SCRIPT_PATH\""
87
+ }
88
+ ```
89
+ Where `SCRIPT_PATH` is the resolved absolute path from Step 1. Use forward slashes even on Windows.
90
+ 4. Write the updated JSON back (preserve all other settings, use 2-space indentation)
91
+
92
+ ### Step 4: Verify and confirm
93
+
94
+ Display:
95
+ ```
96
+ ✓ PBR status line installed
97
+
98
+ Script: {SCRIPT_PATH}
99
+ Config: ~/.claude/settings.json
100
+
101
+ The status line will appear on your next Claude Code session.
102
+ Restart Claude Code or run `/clear` to activate it now.
103
+
104
+ Customize per-project via .planning/config.json:
105
+ "status_line": {
106
+ "sections": ["phase", "plan", "status", "git", "context"],
107
+ "brand_text": "PBR"
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Subcommand: uninstall
114
+
115
+ 1. Read `~/.claude/settings.json`
116
+ 2. If no `statusLine` key: inform user "No status line configured." and stop
117
+ 3. Remove the `statusLine` key from the JSON
118
+ 4. Write the updated file
119
+ 5. Display: `✓ PBR status line removed. Restart Claude Code to take effect.`
120
+
121
+ ---
122
+
123
+ ## Subcommand: preview
124
+
125
+ 1. Locate and run the status-line script: `node {SCRIPT_PATH}`
126
+ - Pass sample stdin JSON: `{"context_window": {"used_percentage": 35}, "model": {"display_name": "Claude Opus 4.6"}, "cost": {"total_cost_usd": 0.42}}`
127
+ 2. Display the raw output to the user
128
+ 3. Also show a description of each section:
129
+ - **Phase**: Current phase number and name from STATE.md
130
+ - **Plan**: Plan progress (N of M)
131
+ - **Status**: Phase status keyword (planning, building, built, etc.)
132
+ - **Git**: Current branch + dirty indicator
133
+ - **Context**: Unicode bar showing context window usage (green/yellow/red)
134
+
135
+ ---
136
+
137
+ ## Edge Cases
138
+
139
+ ### No .planning/ directory
140
+ The status line works even without `.planning/` — it will show only git and context sections. Installation doesn't require a PBR project.
141
+
142
+ ### Plugin installed from npm vs local
143
+ The script path differs between `~/.claude/plugins/cache/plan-build-run/pbr/{version}/scripts/status-line.js` and a local `plugins/pbr/scripts/status-line.js`. The install command must resolve the actual path at install time.
144
+
145
+ ### Existing non-PBR status line
146
+ If `statusLine` already exists with a different command, warn the user and confirm before replacing.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pbr",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Plan-Build-Run — Structured development workflow for Claude Code. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
5
5
  "author": {
6
6
  "name": "SienkLogic",
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: "Install or configure the PBR status line in Claude Code."
3
+ ---
4
+
5
+ This command is provided by the `dev:statusline` skill.
@@ -49,6 +49,20 @@ function main() {
49
49
 
50
50
  // Check for signal file
51
51
  if (!fs.existsSync(signalPath)) {
52
+ // No auto-continue signal — check for pending todos as a reminder
53
+ const todoPendingDir = path.join(planningDir, 'todos', 'pending');
54
+ try {
55
+ if (fs.existsSync(todoPendingDir)) {
56
+ const pending = fs.readdirSync(todoPendingDir).filter(f => f.endsWith('.md'));
57
+ if (pending.length > 0) {
58
+ logHook('auto-continue', 'Stop', 'pending-todos', { count: pending.length });
59
+ // Non-blocking reminder — write to stderr so it shows in hook output
60
+ process.stderr.write(`[pbr] ${pending.length} pending todo(s) in .planning/todos/pending/ — run /pbr:todo list to review\n`);
61
+ }
62
+ }
63
+ } catch (_todoErr) {
64
+ // Ignore errors scanning todos
65
+ }
52
66
  logHook('auto-continue', 'Stop', 'no-signal', {});
53
67
  process.exit(0);
54
68
  }
@@ -19,6 +19,8 @@
19
19
  * 2 = blocked (destructive command detected)
20
20
  */
21
21
 
22
+ const fs = require('fs');
23
+ const path = require('path');
22
24
  const { logHook } = require('./hook-logger');
23
25
 
24
26
  // Commands that are outright blocked
@@ -107,10 +109,52 @@ function checkDangerous(data) {
107
109
  }
108
110
  }
109
111
 
112
+ // Skill-specific checks
113
+ const skillResult = checkSkillSpecificBash(command);
114
+ if (skillResult) return skillResult;
115
+
110
116
  // No match — allow
111
117
  return null;
112
118
  }
113
119
 
120
+ /**
121
+ * Skill-specific bash command checks.
122
+ * Currently: statusline skill cannot use sed/awk/perl on JSON files.
123
+ */
124
+ function checkSkillSpecificBash(command) {
125
+ const planningDir = path.join(process.cwd(), '.planning');
126
+ const skillFile = path.join(planningDir, '.active-skill');
127
+
128
+ let activeSkill = null;
129
+ try {
130
+ activeSkill = fs.readFileSync(skillFile, 'utf8').trim();
131
+ } catch (_e) {
132
+ return null;
133
+ }
134
+
135
+ if (activeSkill !== 'statusline') return null;
136
+
137
+ // Block sed/awk/perl targeting .json files
138
+ const jsonManipPattern = /\b(sed|awk|perl)\b.*\.json/;
139
+ const echoRedirectPattern = /echo\s.*>\s*.*\.json/;
140
+
141
+ if (jsonManipPattern.test(command) || echoRedirectPattern.test(command)) {
142
+ logHook('check-dangerous-commands', 'PreToolUse', 'block', {
143
+ command: command.substring(0, 200),
144
+ reason: 'JSON shell manipulation during statusline'
145
+ });
146
+ return {
147
+ output: {
148
+ decision: 'block',
149
+ reason: 'CRITICAL: Use Read + Write tools for JSON files, not shell text manipulation. Shell tools can corrupt JSON structure.'
150
+ },
151
+ exitCode: 2
152
+ };
153
+ }
154
+
155
+ return null;
156
+ }
157
+
114
158
  function main() {
115
159
  let input = '';
116
160
 
@@ -114,6 +114,12 @@ function checkSkillRules(skill, filePath, planningDir) {
114
114
  return checkQuickRules(filePath, isInPlanning, planningDir);
115
115
  case 'build':
116
116
  return checkBuildRules(filePath, isInPlanning, planningDir);
117
+ case 'statusline':
118
+ return checkStatuslineRules(filePath, isInPlanning, planningDir);
119
+ case 'review':
120
+ case 'discuss':
121
+ case 'begin':
122
+ return checkReadOnlySkillRules(skill, filePath, isInPlanning);
117
123
  default:
118
124
  return null;
119
125
  }
@@ -205,6 +211,63 @@ function checkBuildRules(filePath, isInPlanning, planningDir) {
205
211
  }
206
212
  }
207
213
 
214
+ /**
215
+ * /pbr:statusline rules:
216
+ * - Warn when writing settings.json with hardcoded home directory paths
217
+ */
218
+ function checkStatuslineRules(filePath, _isInPlanning, _planningDir) {
219
+ const normalizedPath = filePath.replace(/\\/g, '/');
220
+
221
+ // Only check settings.json writes
222
+ if (!normalizedPath.endsWith('settings.json')) return null;
223
+
224
+ // Check tool_input content isn't available here — we only have filePath.
225
+ // The hardcoded path check needs content, which we get from the hook data.
226
+ // This function is called from checkSkillRules which only passes filePath.
227
+ // We'll check in the wrapper instead. For now, return null (pass).
228
+ return null;
229
+ }
230
+
231
+ /**
232
+ * Extended statusline check that includes content inspection.
233
+ * Called from checkWorkflow/main where we have access to full hook data.
234
+ */
235
+ function checkStatuslineContent(data) {
236
+ const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
237
+ const normalizedPath = filePath.replace(/\\/g, '/');
238
+
239
+ if (!normalizedPath.endsWith('settings.json')) return null;
240
+
241
+ // Check new_string (Edit) or content (Write) for hardcoded home paths
242
+ const content = data.tool_input?.new_string || data.tool_input?.content || '';
243
+ const oldString = data.tool_input?.old_string || '';
244
+ const textToCheck = content + ' ' + oldString;
245
+
246
+ // Hardcoded home directory paths — warn, don't block (may be legitimately resolved)
247
+ const hardcodedPathPattern = /(\/home\/|C:\\Users\\|\/Users\/)[^"'\s]*\.claude/i;
248
+ if (hardcodedPathPattern.test(textToCheck)) {
249
+ return {
250
+ rule: 'statusline-hardcoded-path',
251
+ message: `Warning: settings.json write appears to contain a hardcoded home directory path.\n\nFile: ${filePath}\n\nCRITICAL: Do NOT hardcode paths. Use dynamic path resolution to find the correct plugin installation directory.`
252
+ };
253
+ }
254
+
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Read-only skill rules (review, discuss, begin):
260
+ * - Cannot write files outside .planning/
261
+ */
262
+ function checkReadOnlySkillRules(skill, filePath, isInPlanning) {
263
+ if (isInPlanning) return null;
264
+
265
+ return {
266
+ rule: `${skill}-readonly`,
267
+ message: `Workflow violation: /pbr:${skill} should only write to .planning/ files.\n\nBlocked: ${filePath}\n\nThe ${skill} skill is not intended to modify source code.`
268
+ };
269
+ }
270
+
208
271
  /**
209
272
  * Check if any PLAN.md file exists in a directory (recursive one level).
210
273
  */
@@ -214,10 +277,10 @@ function hasPlanFile(dir) {
214
277
  try {
215
278
  const entries = fs.readdirSync(dir, { withFileTypes: true });
216
279
  for (const entry of entries) {
217
- if (entry.isFile() && entry.name.endsWith('PLAN.md')) return true;
280
+ if (entry.isFile() && /^PLAN.*\.md$/i.test(entry.name)) return true;
218
281
  if (entry.isDirectory()) {
219
282
  const subEntries = fs.readdirSync(path.join(dir, entry.name));
220
- if (subEntries.some(f => f.endsWith('PLAN.md'))) return true;
283
+ if (subEntries.some(f => /^PLAN.*\.md$/i.test(f))) return true;
221
284
  }
222
285
  }
223
286
  } catch (_e) {
@@ -255,8 +318,22 @@ function checkWorkflow(data) {
255
318
  };
256
319
  }
257
320
 
321
+ // Statusline content check (needs full data for content inspection)
322
+ if (activeSkill === 'statusline') {
323
+ const contentViolation = checkStatuslineContent(data);
324
+ if (contentViolation) {
325
+ logHook('check-skill-workflow', 'PreToolUse', 'warn', {
326
+ skill: activeSkill, file: path.basename(filePath), rule: contentViolation.rule
327
+ });
328
+ return {
329
+ exitCode: 0,
330
+ output: { additionalContext: contentViolation.message }
331
+ };
332
+ }
333
+ }
334
+
258
335
  return null;
259
336
  }
260
337
 
261
- module.exports = { readActiveSkill, checkSkillRules, hasPlanFile, checkWorkflow };
338
+ module.exports = { readActiveSkill, checkSkillRules, hasPlanFile, checkWorkflow, checkStatuslineContent, checkReadOnlySkillRules };
262
339
  if (require.main === module || process.argv[1] === __filename) { main(); }
@@ -24,8 +24,13 @@ const { logHook } = require('./hook-logger');
24
24
  // Agent type → expected output patterns
25
25
  const AGENT_OUTPUTS = {
26
26
  'pbr:executor': {
27
- description: 'SUMMARY.md in the phase directory',
28
- check: (planningDir) => findInPhaseDir(planningDir, /^SUMMARY.*\.md$/i)
27
+ description: 'SUMMARY.md in the phase or quick directory',
28
+ check: (planningDir) => {
29
+ // Check phase directory first, then quick directory
30
+ const phaseMatches = findInPhaseDir(planningDir, /^SUMMARY.*\.md$/i);
31
+ if (phaseMatches.length > 0) return phaseMatches;
32
+ return findInQuickDir(planningDir, /^SUMMARY.*\.md$/i);
33
+ }
29
34
  },
30
35
  'pbr:planner': {
31
36
  description: 'PLAN.md in the phase directory',
@@ -48,6 +53,69 @@ const AGENT_OUTPUTS = {
48
53
  return [];
49
54
  }
50
55
  }
56
+ },
57
+ 'pbr:synthesizer': {
58
+ description: 'synthesis file in .planning/research/ or CONTEXT.md update',
59
+ check: (planningDir) => {
60
+ const researchDir = path.join(planningDir, 'research');
61
+ if (fs.existsSync(researchDir)) {
62
+ try {
63
+ const files = fs.readdirSync(researchDir).filter(f => f.endsWith('.md'));
64
+ if (files.length > 0) return files.map(f => path.join('research', f));
65
+ } catch (_e) { /* best-effort */ }
66
+ }
67
+ const contextFile = path.join(planningDir, 'CONTEXT.md');
68
+ if (fs.existsSync(contextFile)) {
69
+ try {
70
+ const stat = fs.statSync(contextFile);
71
+ if (stat.size > 0) return ['CONTEXT.md'];
72
+ } catch (_e) { /* best-effort */ }
73
+ }
74
+ return [];
75
+ }
76
+ },
77
+ 'pbr:plan-checker': {
78
+ description: 'advisory output (no file expected)',
79
+ noFileExpected: true,
80
+ check: () => []
81
+ },
82
+ 'pbr:integration-checker': {
83
+ description: 'advisory output (no file expected)',
84
+ noFileExpected: true,
85
+ check: () => []
86
+ },
87
+ 'pbr:debugger': {
88
+ description: 'debug file in .planning/debug/',
89
+ check: (planningDir) => {
90
+ const debugDir = path.join(planningDir, 'debug');
91
+ if (!fs.existsSync(debugDir)) return [];
92
+ try {
93
+ return fs.readdirSync(debugDir)
94
+ .filter(f => f.endsWith('.md'))
95
+ .map(f => path.join('debug', f));
96
+ } catch (_e) {
97
+ return [];
98
+ }
99
+ }
100
+ },
101
+ 'pbr:codebase-mapper': {
102
+ description: 'codebase map in .planning/codebase/',
103
+ check: (planningDir) => {
104
+ const codebaseDir = path.join(planningDir, 'codebase');
105
+ if (!fs.existsSync(codebaseDir)) return [];
106
+ try {
107
+ return fs.readdirSync(codebaseDir)
108
+ .filter(f => f.endsWith('.md'))
109
+ .map(f => path.join('codebase', f));
110
+ } catch (_e) {
111
+ return [];
112
+ }
113
+ }
114
+ },
115
+ 'pbr:general': {
116
+ description: 'advisory output (no file expected)',
117
+ noFileExpected: true,
118
+ check: () => []
51
119
  }
52
120
  };
53
121
 
@@ -87,6 +155,39 @@ function findInPhaseDir(planningDir, pattern) {
87
155
  return matches;
88
156
  }
89
157
 
158
+ function findInQuickDir(planningDir, pattern) {
159
+ const matches = [];
160
+ const quickDir = path.join(planningDir, 'quick');
161
+ if (!fs.existsSync(quickDir)) return matches;
162
+
163
+ try {
164
+ // Find the most recent quick task directory (highest NNN)
165
+ const dirs = fs.readdirSync(quickDir)
166
+ .filter(d => /^\d{3}-/.test(d))
167
+ .sort()
168
+ .reverse();
169
+ if (dirs.length === 0) return matches;
170
+
171
+ const latestDir = path.join(quickDir, dirs[0]);
172
+ const stat = fs.statSync(latestDir);
173
+ if (!stat.isDirectory()) return matches;
174
+
175
+ const files = fs.readdirSync(latestDir);
176
+ for (const file of files) {
177
+ if (pattern.test(file)) {
178
+ const filePath = path.join(latestDir, file);
179
+ const fileStat = fs.statSync(filePath);
180
+ if (fileStat.size > 0) {
181
+ matches.push(path.join('quick', dirs[0], file));
182
+ }
183
+ }
184
+ }
185
+ } catch (_e) {
186
+ // best-effort
187
+ }
188
+ return matches;
189
+ }
190
+
90
191
  function readStdin() {
91
192
  try {
92
193
  const input = fs.readFileSync(0, 'utf8').trim();
@@ -119,7 +220,7 @@ function main() {
119
220
  // Check for expected outputs
120
221
  const found = outputSpec.check(planningDir);
121
222
 
122
- if (found.length === 0) {
223
+ if (found.length === 0 && !outputSpec.noFileExpected) {
123
224
  logHook('check-subagent-output', 'PostToolUse', 'warning', {
124
225
  agent_type: agentType,
125
226
  expected: outputSpec.description,
@@ -140,5 +241,5 @@ function main() {
140
241
  process.exit(0);
141
242
  }
142
243
 
143
- module.exports = { AGENT_OUTPUTS, findInPhaseDir };
244
+ module.exports = { AGENT_OUTPUTS, findInPhaseDir, findInQuickDir };
144
245
  if (require.main === module || process.argv[1] === __filename) { main(); }