@sienklogic/plan-build-run 2.2.0 → 2.3.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.
@@ -267,6 +267,72 @@ details li {
267
267
  background: rgba(255, 255, 255, 0.06);
268
268
  }
269
269
 
270
+ /* --- Markdown Body (rendered markdown content) --- */
271
+ .markdown-body h1 { font-size: 1.5rem; margin-top: var(--space-xl); margin-bottom: var(--space-sm); }
272
+ .markdown-body h2 { font-size: 1.25rem; margin-top: var(--space-xl); margin-bottom: var(--space-sm); }
273
+ .markdown-body h3 { font-size: 1.05rem; margin-top: var(--space-lg); margin-bottom: var(--space-xs); }
274
+
275
+ .markdown-body table {
276
+ border-collapse: collapse;
277
+ width: 100%;
278
+ margin: var(--space-md) 0;
279
+ font-size: 0.875rem;
280
+ }
281
+
282
+ .markdown-body th,
283
+ .markdown-body td {
284
+ border: 1px solid var(--border-subtle);
285
+ padding: var(--space-xs) var(--space-sm);
286
+ text-align: left;
287
+ }
288
+
289
+ .markdown-body th {
290
+ background: rgba(255, 255, 255, 0.04);
291
+ }
292
+
293
+ .markdown-body pre {
294
+ background: rgba(0, 0, 0, 0.3);
295
+ border-radius: var(--radius-sm);
296
+ padding: var(--space-md);
297
+ overflow-x: auto;
298
+ margin: var(--space-md) 0;
299
+ }
300
+
301
+ .markdown-body pre code {
302
+ background: none;
303
+ padding: 0;
304
+ font-size: 0.85em;
305
+ }
306
+
307
+ .markdown-body blockquote {
308
+ border-left: 3px solid var(--pico-primary);
309
+ background: rgba(255, 255, 255, 0.02);
310
+ margin: var(--space-md) 0;
311
+ padding: var(--space-sm) var(--space-md);
312
+ }
313
+
314
+ .markdown-body ul.contains-task-list {
315
+ list-style: none;
316
+ padding-left: var(--space-sm);
317
+ }
318
+
319
+ .markdown-body ul.contains-task-list li {
320
+ position: relative;
321
+ padding-left: var(--space-xs);
322
+ }
323
+
324
+ .markdown-body img {
325
+ max-width: 100%;
326
+ height: auto;
327
+ border-radius: var(--radius-sm);
328
+ }
329
+
330
+ .markdown-body hr {
331
+ border: none;
332
+ border-top: 1px solid var(--border-subtle);
333
+ margin: var(--space-lg) 0;
334
+ }
335
+
270
336
  /* --- Back Link --- */
271
337
  main > p:first-of-type > a[href="/"] {
272
338
  font-size: 0.875rem;
@@ -3,6 +3,8 @@ import { join, resolve, relative, normalize } from 'node:path';
3
3
  import matter from 'gray-matter';
4
4
  import { marked } from 'marked';
5
5
 
6
+ marked.setOptions({ gfm: true, breaks: false });
7
+
6
8
  /**
7
9
  * Strip UTF-8 BOM (Byte Order Mark) if present.
8
10
  * Windows editors (Notepad, older VS Code) may prepend BOM to UTF-8 files.
@@ -3,6 +3,7 @@ import { getPhaseDetail, getPhaseDocument } from '../services/phase.service.js';
3
3
  import { getRoadmapData } from '../services/roadmap.service.js';
4
4
  import { parseStateFile, derivePhaseStatuses } from '../services/dashboard.service.js';
5
5
  import { listPendingTodos, getTodoDetail, createTodo, completeTodo } from '../services/todo.service.js';
6
+ import { getAllMilestones, getMilestoneDetail } from '../services/milestone.service.js';
6
7
 
7
8
  const router = Router();
8
9
 
@@ -225,6 +226,61 @@ router.post('/todos/:id/done', async (req, res) => {
225
226
  }
226
227
  });
227
228
 
229
+ router.get('/milestones', async (req, res) => {
230
+ const projectDir = req.app.locals.projectDir;
231
+ const milestoneData = await getAllMilestones(projectDir);
232
+
233
+ const templateData = {
234
+ title: 'Milestones',
235
+ activePage: 'milestones',
236
+ currentPath: '/milestones',
237
+ ...milestoneData
238
+ };
239
+
240
+ res.setHeader('Vary', 'HX-Request');
241
+
242
+ if (req.get('HX-Request') === 'true') {
243
+ res.render('partials/milestones-content', templateData);
244
+ } else {
245
+ res.render('milestones', templateData);
246
+ }
247
+ });
248
+
249
+ router.get('/milestones/:version', async (req, res) => {
250
+ const { version } = req.params;
251
+
252
+ // Validate version: alphanumeric with dots and dashes
253
+ if (!/^[\w.-]+$/.test(version)) {
254
+ const err = new Error('Invalid milestone version format');
255
+ err.status = 404;
256
+ throw err;
257
+ }
258
+
259
+ const projectDir = req.app.locals.projectDir;
260
+ const detail = await getMilestoneDetail(projectDir, version);
261
+
262
+ if (detail.sections.length === 0) {
263
+ const err = new Error(`No archived files found for milestone v${version}`);
264
+ err.status = 404;
265
+ throw err;
266
+ }
267
+
268
+ const templateData = {
269
+ title: `Milestone v${version}`,
270
+ activePage: 'milestones',
271
+ currentPath: '/milestones/' + version,
272
+ ...detail
273
+ };
274
+
275
+ res.setHeader('Vary', 'HX-Request');
276
+
277
+ if (req.get('HX-Request') === 'true') {
278
+ res.render('partials/milestone-detail-content', templateData);
279
+ } else {
280
+ res.render('milestone-detail', templateData);
281
+ }
282
+ });
283
+
228
284
  router.get('/roadmap', async (req, res) => {
229
285
  const projectDir = req.app.locals.projectDir;
230
286
  const [roadmapData, stateData] = await Promise.all([
@@ -0,0 +1,103 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { readMarkdownFile } from '../repositories/planning.repository.js';
4
+ import { getRoadmapData } from './roadmap.service.js';
5
+
6
+ /**
7
+ * Scan .planning/milestones/ for archived milestone files.
8
+ * Groups files by version prefix (e.g., v1.0-ROADMAP.md, v1.0-STATS.md).
9
+ *
10
+ * @param {string} projectDir - Absolute path to the project root
11
+ * @returns {Promise<Array<{version: string, name: string, date: string, duration: string, files: string[]}>>}
12
+ */
13
+ export async function listArchivedMilestones(projectDir) {
14
+ const milestonesDir = join(projectDir, '.planning', 'milestones');
15
+
16
+ let entries;
17
+ try {
18
+ entries = await readdir(milestonesDir);
19
+ } catch (err) {
20
+ if (err.code === 'ENOENT') return [];
21
+ throw err;
22
+ }
23
+
24
+ // Group files by version prefix: v1.0-ROADMAP.md -> version "1.0"
25
+ const versionMap = new Map();
26
+ const versionPattern = /^v([\w.-]+)-(\w+)\.md$/;
27
+
28
+ for (const file of entries) {
29
+ const match = file.match(versionPattern);
30
+ if (!match) continue;
31
+
32
+ const version = match[1];
33
+ if (!versionMap.has(version)) {
34
+ versionMap.set(version, { version, name: '', date: '', duration: '', files: [] });
35
+ }
36
+ versionMap.get(version).files.push(file);
37
+ }
38
+
39
+ // Try to parse STATS.md for each version to get name/date/duration
40
+ for (const [version, milestone] of versionMap) {
41
+ const statsFile = `v${version}-STATS.md`;
42
+ if (milestone.files.includes(statsFile)) {
43
+ try {
44
+ const { frontmatter } = await readMarkdownFile(join(milestonesDir, statsFile));
45
+ milestone.name = frontmatter.milestone || frontmatter.name || `v${version}`;
46
+ milestone.date = frontmatter.completed || frontmatter.date || '';
47
+ milestone.duration = frontmatter.duration || '';
48
+ } catch (_e) {
49
+ milestone.name = `v${version}`;
50
+ }
51
+ } else {
52
+ milestone.name = `v${version}`;
53
+ }
54
+ }
55
+
56
+ // Sort by version descending (newest first)
57
+ return [...versionMap.values()].sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }));
58
+ }
59
+
60
+ /**
61
+ * Combine active milestones from ROADMAP.md with archived milestones.
62
+ *
63
+ * @param {string} projectDir - Absolute path to the project root
64
+ * @returns {Promise<{active: Array, archived: Array}>}
65
+ */
66
+ export async function getAllMilestones(projectDir) {
67
+ const [roadmapData, archived] = await Promise.all([
68
+ getRoadmapData(projectDir),
69
+ listArchivedMilestones(projectDir)
70
+ ]);
71
+
72
+ return {
73
+ active: roadmapData.milestones || [],
74
+ archived
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Read all archived files for a specific milestone version.
80
+ * Returns rendered HTML for each file type (ROADMAP, STATS, REQUIREMENTS).
81
+ *
82
+ * @param {string} projectDir - Absolute path to the project root
83
+ * @param {string} version - Milestone version (e.g., "1.0")
84
+ * @returns {Promise<{version: string, sections: Array<{type: string, frontmatter: object, html: string}>}>}
85
+ */
86
+ export async function getMilestoneDetail(projectDir, version) {
87
+ const milestonesDir = join(projectDir, '.planning', 'milestones');
88
+ const fileTypes = ['ROADMAP', 'STATS', 'REQUIREMENTS'];
89
+
90
+ const sections = [];
91
+ for (const type of fileTypes) {
92
+ const filePath = join(milestonesDir, `v${version}-${type}.md`);
93
+ try {
94
+ const result = await readMarkdownFile(filePath);
95
+ sections.push({ type, frontmatter: result.frontmatter, html: result.html });
96
+ } catch (err) {
97
+ if (err.code !== 'ENOENT') throw err;
98
+ // File doesn't exist for this type — skip
99
+ }
100
+ }
101
+
102
+ return { version, sections };
103
+ }
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: 'Milestone v' + version, activePage: 'milestones' }) %>
2
+
3
+ <%- include('partials/milestone-detail-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: 'Milestones', activePage: 'milestones' }) %>
2
+
3
+ <%- include('partials/milestones-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -4,10 +4,16 @@
4
4
  <title><%= typeof title !== 'undefined' ? title : 'Plan-Build-Run' %> - Plan-Build-Run</title>
5
5
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
6
6
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.min.css">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-600-normal.min.css">
9
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.min.css">
10
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.min.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.min.css" media="print" onload="this.media='all'">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-600-normal.min.css" media="print" onload="this.media='all'">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.min.css" media="print" onload="this.media='all'">
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.min.css" media="print" onload="this.media='all'">
11
+ <noscript>
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.min.css">
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-600-normal.min.css">
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.min.css">
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.min.css">
16
+ </noscript>
11
17
  <link rel="stylesheet" href="/css/layout.css">
12
18
  <link rel="stylesheet" href="/css/status-colors.css">
13
19
  <script
@@ -0,0 +1,19 @@
1
+ <h1>Milestone v<%= version %></h1>
2
+
3
+ <p>
4
+ <a href="/milestones"
5
+ hx-get="/milestones"
6
+ hx-target="#main-content"
7
+ hx-push-url="true">&larr; Back to Milestones</a>
8
+ </p>
9
+
10
+ <% sections.forEach(function(section) { %>
11
+ <article>
12
+ <header>
13
+ <strong><%= section.type %></strong>
14
+ </header>
15
+ <div class="markdown-body">
16
+ <%- section.html %>
17
+ </div>
18
+ </article>
19
+ <% }); %>
@@ -0,0 +1,44 @@
1
+ <h1>Milestones</h1>
2
+
3
+ <% if (active.length > 0) { %>
4
+ <h2>Active</h2>
5
+ <% active.forEach(function(m) { %>
6
+ <article>
7
+ <header>
8
+ <strong><%= m.name %></strong>
9
+ <% if (m.goal) { %><small> &mdash; <%= m.goal %></small><% } %>
10
+ </header>
11
+ <p>
12
+ Phases <%= m.startPhase %> &ndash; <%= m.endPhase %>
13
+ </p>
14
+ </article>
15
+ <% }); %>
16
+ <% } %>
17
+
18
+ <% if (archived.length > 0) { %>
19
+ <h2>Archived</h2>
20
+ <% archived.forEach(function(m) { %>
21
+ <article>
22
+ <header>
23
+ <strong>
24
+ <a href="/milestones/<%= m.version %>"
25
+ hx-get="/milestones/<%= m.version %>"
26
+ hx-target="#main-content"
27
+ hx-push-url="true">
28
+ v<%= m.version %> &mdash; <%= m.name %>
29
+ </a>
30
+ </strong>
31
+ </header>
32
+ <p>
33
+ <% if (m.date) { %><small>Completed: <%= m.date %></small><% } %>
34
+ <% if (m.duration) { %><small> &bull; Duration: <%= m.duration %></small><% } %>
35
+ <br>
36
+ <small><%= m.files.length %> archived file<%= m.files.length !== 1 ? 's' : '' %></small>
37
+ </p>
38
+ </article>
39
+ <% }); %>
40
+ <% } %>
41
+
42
+ <% if (active.length === 0 && archived.length === 0) { %>
43
+ <p>No milestones found. Use <code>/pbr:milestone new</code> to create one.</p>
44
+ <% } %>
@@ -33,6 +33,14 @@
33
33
  Roadmap
34
34
  </a>
35
35
  </li>
36
+ <li>
37
+ <a href="/milestones"
38
+ hx-get="/milestones"
39
+ hx-target="#main-content"
40
+ hx-push-url="true"<%= typeof activePage !== 'undefined' && activePage === 'milestones' ? ' aria-current="page"' : '' %>>
41
+ Milestones
42
+ </a>
43
+ </li>
36
44
  </ul>
37
45
  </nav>
38
46
  </aside>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sienklogic/plan-build-run",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Plan it, Build it, Run it — structured development workflow for Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pbr",
3
3
  "displayName": "Plan-Build-Run",
4
- "version": "2.0.0",
4
+ "version": "2.3.0",
5
5
  "description": "Plan-Build-Run — Structured development workflow for Cursor. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
6
6
  "author": {
7
7
  "name": "SienkLogic",
@@ -106,6 +106,16 @@
106
106
  "statusMessage": "Validating Task() call..."
107
107
  }
108
108
  ]
109
+ },
110
+ {
111
+ "matcher": "Skill",
112
+ "hooks": [
113
+ {
114
+ "type": "command",
115
+ "command": "node -e \"var r=process.env.CLAUDE_PLUGIN_ROOT||'',m=r.match(/^\\/([a-zA-Z])\\/(.*)/);if(m)r=m[1]+String.fromCharCode(58)+String.fromCharCode(92)+m[2];require(require('path').resolve(r,'..','pbr','scripts','run-hook.js'))\" validate-skill-args.js",
116
+ "statusMessage": "Validating skill arguments..."
117
+ }
118
+ ]
109
119
  }
110
120
  ],
111
121
  "PreCompact": [
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: dashboard
3
+ description: "Launch the PBR web dashboard for the current project."
4
+ argument-hint: "[--port N]"
5
+ ---
6
+
7
+ ## Behavior
8
+
9
+ 1. **Parse arguments**: Extract `--port N` from the user's input. Default to `3000`.
10
+
11
+ 2. **Check dependencies**: Check if the dashboard `node_modules/` exists in the plugin's dashboard directory. If not, run `npm install` in that directory.
12
+
13
+ 3. **Launch dashboard**: Run in background:
14
+ ```
15
+ node <plugin-root>/dashboard/bin/cli.js --dir <cwd> --port <port> &
16
+ ```
17
+
18
+ 4. **Output to user**:
19
+ ```
20
+ Dashboard running at http://localhost:<port>
21
+ Open this URL in your browser to view your project's planning state.
22
+ ```
@@ -63,9 +63,9 @@ Parse the phase number and optional flags:
63
63
  | `insert <N>` | Insert a new phase at position N (uses decimal numbering) |
64
64
  | `remove <N>` | Remove phase N from the roadmap |
65
65
 
66
- ### Freeform Text Guard
66
+ ### Freeform Text Guard — CRITICAL
67
67
 
68
- **Before any context loading**, check whether `$ARGUMENTS` looks like freeform text rather than a valid invocation. Valid patterns are:
68
+ **STOP. Before ANY context loading or Step 1 work**, you MUST check whether `$ARGUMENTS` looks like freeform text rather than a valid invocation. This check is non-negotiable. Valid patterns are:
69
69
 
70
70
  - Empty (no arguments)
71
71
  - A phase number: integer (`3`, `03`) or decimal (`3.1`)
@@ -97,6 +97,8 @@ Then suggest the appropriate skill based on the text content:
97
97
 
98
98
  Do NOT proceed with planning. The user needs to use the correct skill.
99
99
 
100
+ **Self-check**: If you reach Step 1 without having matched a valid argument pattern above, you have a bug. Stop immediately and show the usage block.
101
+
100
102
  ---
101
103
 
102
104
  ## Orchestration Flow: Standard Planning
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: todo
3
3
  description: "File-based persistent todos. Add, list, complete — survives sessions."
4
- argument-hint: "add <description> | list [theme] | done <NNN>"
4
+ argument-hint: "add <description> | list [theme] | done <NNN> | work <NNN>"
5
5
  ---
6
6
 
7
7
  ## Step 0 — Immediate Output
@@ -121,12 +121,12 @@ Pending Todos:
121
121
 
122
122
  **Pick a todo** — mark one done or start working
123
123
 
124
- `/pbr:todo done <NNN>`
124
+ `/pbr:todo work <NNN>` — start working on a todo
125
+ `/pbr:todo done <NNN>` — mark a todo as complete
125
126
 
126
127
  ───────────────────────────────────────────────────────────────
127
128
 
128
129
  **Also available:**
129
- - `/pbr:quick` — work on one now
130
130
  - `/pbr:status` — see project status
131
131
 
132
132
  ───────────────────────────────────────────────────────────────
@@ -178,6 +178,45 @@ Todo {NNN} not found in pending todos.
178
178
  ───────────────────────────────────────────────────────────────
179
179
  ```
180
180
 
181
+ ### `work <NNN>`
182
+
183
+ 1. Find `.planning/todos/pending/{NNN}-*.md` (match by number prefix)
184
+ 2. If not found, display the same error block as `done` — suggest `/pbr:todo list`
185
+ 3. Read the todo file content (frontmatter + body)
186
+ 4. Extract the `title` from frontmatter and the full body (Goal, Scope, Acceptance Criteria sections)
187
+ 5. Display branded output:
188
+ ```
189
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
190
+ PLAN-BUILD-RUN ► WORKING ON TODO {NNN}
191
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
192
+
193
+ **Todo {NNN}:** {title}
194
+
195
+ Launching /pbr:quick with todo context...
196
+ ```
197
+ 6. Invoke the Skill tool: `skill: "pbr:quick"` with `args` set to the todo title followed by the body content. Format the args as:
198
+
199
+ ```
200
+ {title}
201
+
202
+ Context from todo {NNN}:
203
+ {body content — Goal, Scope, Acceptance Criteria sections}
204
+ ```
205
+
206
+ This hands off execution to `/pbr:quick`, which will spawn an executor agent, make atomic commits, and track the work. When quick completes, remind the user:
207
+
208
+ ```
209
+ ───────────────────────────────────────────────────────────────
210
+
211
+ ## ▶ Next Up
212
+
213
+ **Mark this todo as done if the work is complete**
214
+
215
+ `/pbr:todo done {NNN}`
216
+
217
+ ───────────────────────────────────────────────────────────────
218
+ ```
219
+
181
220
  ### No arguments
182
221
 
183
222
  Show a brief summary: count of pending todos, grouped by theme, plus usage hint.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pbr",
3
- "version": "2.0.0",
3
+ "version": "2.3.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: "Launch the PBR web dashboard for the current project."
3
+ ---
4
+
5
+ This command is provided by the `dev:dashboard` skill.
@@ -106,6 +106,16 @@
106
106
  "statusMessage": "Validating Task() call..."
107
107
  }
108
108
  ]
109
+ },
110
+ {
111
+ "matcher": "Skill",
112
+ "hooks": [
113
+ {
114
+ "type": "command",
115
+ "command": "node -e \"var r=process.env.CLAUDE_PLUGIN_ROOT||'',m=r.match(/^\\/([a-zA-Z])\\/(.*)/);if(m)r=m[1]+String.fromCharCode(58)+String.fromCharCode(92)+m[2];require(require('path').resolve(r,'scripts','run-hook.js'))\" validate-skill-args.js",
116
+ "statusMessage": "Validating skill arguments..."
117
+ }
118
+ ]
109
119
  }
110
120
  ],
111
121
  "PreCompact": [
@@ -211,6 +211,14 @@
211
211
  },
212
212
  "additionalProperties": false
213
213
  },
214
+ "dashboard": {
215
+ "type": "object",
216
+ "properties": {
217
+ "auto_launch": { "type": "boolean" },
218
+ "port": { "type": "integer", "minimum": 1024, "maximum": 65535 }
219
+ },
220
+ "additionalProperties": false
221
+ },
214
222
  "status_line": {
215
223
  "type": "object",
216
224
  "properties": {
@@ -32,6 +32,12 @@ function main() {
32
32
 
33
33
  const context = buildContext(planningDir, stateFile);
34
34
 
35
+ // Auto-launch dashboard if configured
36
+ const config = configLoad(planningDir);
37
+ if (config && config.dashboard && config.dashboard.auto_launch) {
38
+ tryLaunchDashboard(config.dashboard.port || 3000, planningDir, cwd);
39
+ }
40
+
35
41
  if (context) {
36
42
  const output = {
37
43
  additionalContext: context
@@ -275,7 +281,46 @@ function getHookHealthSummary(planningDir) {
275
281
  }
276
282
  }
277
283
 
284
+ /**
285
+ * Attempt to launch the dashboard in a detached background process.
286
+ * Checks if the port is already in use before spawning.
287
+ */
288
+ function tryLaunchDashboard(port, _planningDir, projectDir) {
289
+ const net = require('net');
290
+ const { spawn } = require('child_process');
291
+
292
+ // Quick port probe — if something is already listening, skip launch
293
+ const probe = net.createConnection({ port, host: '127.0.0.1' });
294
+ probe.on('connect', () => {
295
+ probe.destroy();
296
+ logHook('progress-tracker', 'SessionStart', 'dashboard-already-running', { port });
297
+ });
298
+ probe.on('error', () => {
299
+ // Port is free — launch dashboard
300
+ const cliPath = path.join(__dirname, '..', 'dashboard', 'bin', 'cli.js');
301
+ if (!fs.existsSync(cliPath)) {
302
+ logHook('progress-tracker', 'SessionStart', 'dashboard-cli-missing', { cliPath });
303
+ return;
304
+ }
305
+
306
+ try {
307
+ const child = spawn(process.execPath, [cliPath, '--dir', projectDir, '--port', String(port)], {
308
+ detached: true,
309
+ stdio: 'ignore',
310
+ cwd: projectDir
311
+ });
312
+ child.unref();
313
+ logHook('progress-tracker', 'SessionStart', 'dashboard-launched', { port, pid: child.pid });
314
+ } catch (e) {
315
+ logHook('progress-tracker', 'SessionStart', 'dashboard-launch-error', { error: e.message });
316
+ }
317
+ });
318
+
319
+ // Don't let the probe keep the process alive
320
+ probe.unref();
321
+ }
322
+
278
323
  // Exported for testing
279
- module.exports = { getHookHealthSummary, FAILURE_DECISIONS, HOOK_HEALTH_MAX_ENTRIES };
324
+ module.exports = { getHookHealthSummary, FAILURE_DECISIONS, HOOK_HEALTH_MAX_ENTRIES, tryLaunchDashboard };
280
325
 
281
326
  main();
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PreToolUse hook: Validates Skill tool arguments before execution.
5
+ *
6
+ * Currently validates:
7
+ * - /pbr:plan — blocks freeform text arguments that don't match
8
+ * valid patterns (phase number, subcommand, flags).
9
+ *
10
+ * This provides structural enforcement that catches cases where the
11
+ * LLM ignores the prompt-based freeform text guard in SKILL.md.
12
+ *
13
+ * Exit codes:
14
+ * 0 = allowed (valid args or non-plan skill)
15
+ * 2 = blocked (freeform text detected for /pbr:plan)
16
+ */
17
+
18
+ const { logHook } = require('./hook-logger');
19
+
20
+ /**
21
+ * Valid argument patterns for /pbr:plan.
22
+ *
23
+ * Matches:
24
+ * - Empty / whitespace only
25
+ * - Phase number: "3", "03", "3.1"
26
+ * - Phase number + flags: "3 --skip-research", "3 --assumptions", "3 --gaps", "3 --teams"
27
+ * - Subcommands: "add", "insert 3", "remove 3"
28
+ * - Legacy: "check"
29
+ */
30
+ const PLAN_VALID_PATTERN = /^\s*$|^\s*\d+(\.\d+)?\s*(--(?:skip-research|assumptions|gaps|teams)\s*)*$|^\s*(?:add|check)\s*$|^\s*(?:insert|remove)\s+\d+(\.\d+)?\s*$/i;
31
+
32
+ /**
33
+ * Check whether a Skill tool call has valid arguments.
34
+ * Returns null if valid, or { output, exitCode } if blocked.
35
+ */
36
+ function checkSkillArgs(data) {
37
+ const toolInput = data.tool_input || {};
38
+ const skill = toolInput.skill || '';
39
+ const args = toolInput.args || '';
40
+
41
+ // Only validate /pbr:plan for now
42
+ if (skill !== 'pbr:plan') {
43
+ return null;
44
+ }
45
+
46
+ // Test against valid patterns
47
+ if (PLAN_VALID_PATTERN.test(args)) {
48
+ return null;
49
+ }
50
+
51
+ // Freeform text detected — block
52
+ logHook('validate-skill-args', 'PreToolUse', 'blocked', {
53
+ skill,
54
+ args: args.substring(0, 100),
55
+ reason: 'freeform-text'
56
+ });
57
+
58
+ return {
59
+ output: {
60
+ additionalContext: [
61
+ 'BLOCKED: /pbr:plan received freeform text instead of a phase number.',
62
+ '',
63
+ 'The arguments "' + args.substring(0, 80) + (args.length > 80 ? '...' : '') + '" do not match any valid pattern.',
64
+ '',
65
+ 'Valid usage:',
66
+ ' /pbr:plan <N> Plan phase N',
67
+ ' /pbr:plan <N> --gaps Create gap-closure plans',
68
+ ' /pbr:plan add Add a new phase',
69
+ ' /pbr:plan insert <N> Insert a phase at position N',
70
+ ' /pbr:plan remove <N> Remove phase N',
71
+ '',
72
+ 'For freeform tasks, use:',
73
+ ' /pbr:todo add <description> Capture as a todo',
74
+ ' /pbr:quick <description> Execute immediately',
75
+ ' /pbr:explore <topic> Investigate an idea'
76
+ ].join('\n')
77
+ },
78
+ exitCode: 2
79
+ };
80
+ }
81
+
82
+ function main() {
83
+ let input = '';
84
+
85
+ process.stdin.setEncoding('utf8');
86
+ process.stdin.on('data', (chunk) => { input += chunk; });
87
+ process.stdin.on('end', () => {
88
+ try {
89
+ const data = JSON.parse(input);
90
+ const result = checkSkillArgs(data);
91
+
92
+ if (result) {
93
+ process.stdout.write(JSON.stringify(result.output));
94
+ process.exit(result.exitCode);
95
+ }
96
+
97
+ process.exit(0);
98
+ } catch (_e) {
99
+ // Don't block on errors
100
+ process.exit(0);
101
+ }
102
+ });
103
+ }
104
+
105
+ module.exports = { checkSkillArgs, PLAN_VALID_PATTERN };
106
+ if (require.main === module || process.argv[1] === __filename) { main(); }
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: dashboard
3
+ description: "Launch the PBR web dashboard for the current project."
4
+ allowed-tools: Bash, Read
5
+ argument-hint: "[--port N]"
6
+ ---
7
+
8
+ **STOP — DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by Claude Code's plugin system. Begin executing immediately.**
9
+
10
+ ## Behavior
11
+
12
+ 1. **Parse arguments**: Extract `--port N` from the user's input. Default to `3000`.
13
+
14
+ 2. **Check dependencies**: Check if `${CLAUDE_PLUGIN_ROOT}/dashboard/node_modules/` exists. If not, run:
15
+ ```
16
+ npm install --prefix ${CLAUDE_PLUGIN_ROOT}/dashboard
17
+ ```
18
+
19
+ 3. **Launch dashboard**: Run in background via Bash:
20
+ ```
21
+ node ${CLAUDE_PLUGIN_ROOT}/dashboard/bin/cli.js --dir <cwd> --port <port> &
22
+ ```
23
+ Use `&` to background the process so it doesn't block the session.
24
+
25
+ 4. **Output to user**:
26
+ ```
27
+ Dashboard running at http://localhost:<port>
28
+ Open this URL in your browser to view your project's planning state.
29
+ ```
30
+
31
+ ## Notes
32
+
33
+ - If the port is already in use, the dashboard will fail to start — suggest the user try a different port with `--port`.
34
+ - The dashboard watches `.planning/` for live updates via SSE.
@@ -66,9 +66,9 @@ Parse the phase number and optional flags:
66
66
  | `insert <N>` | Insert a new phase at position N (uses decimal numbering) |
67
67
  | `remove <N>` | Remove phase N from the roadmap |
68
68
 
69
- ### Freeform Text Guard
69
+ ### Freeform Text Guard — CRITICAL
70
70
 
71
- **Before any context loading**, check whether `$ARGUMENTS` looks like freeform text rather than a valid invocation. Valid patterns are:
71
+ **STOP. Before ANY context loading or Step 1 work**, you MUST check whether `$ARGUMENTS` looks like freeform text rather than a valid invocation. This check is non-negotiable. Valid patterns are:
72
72
 
73
73
  - Empty (no arguments)
74
74
  - A phase number: integer (`3`, `03`) or decimal (`3.1`)
@@ -100,6 +100,8 @@ Then suggest the appropriate skill based on the text content:
100
100
 
101
101
  Do NOT proceed with planning. The user needs to use the correct skill.
102
102
 
103
+ **Self-check**: If you reach Step 1 without having matched a valid argument pattern above, you have a bug. Stop immediately and show the usage block.
104
+
103
105
  ---
104
106
 
105
107
  ## Orchestration Flow: Standard Planning
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: todo
3
3
  description: "File-based persistent todos. Add, list, complete — survives sessions."
4
- allowed-tools: Read, Write, Bash, Glob, Grep
5
- argument-hint: "add <description> | list [theme] | done <NNN>"
4
+ allowed-tools: Read, Write, Bash, Glob, Grep, Skill
5
+ argument-hint: "add <description> | list [theme] | done <NNN> | work <NNN>"
6
6
  ---
7
7
 
8
8
  **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 ~7,600 tokens. Begin executing Step 1 immediately.**
@@ -124,12 +124,12 @@ Pending Todos:
124
124
 
125
125
  **Pick a todo** — mark one done or start working
126
126
 
127
- `/pbr:todo done <NNN>`
127
+ `/pbr:todo work <NNN>` — start working on a todo
128
+ `/pbr:todo done <NNN>` — mark a todo as complete
128
129
 
129
130
  ───────────────────────────────────────────────────────────────
130
131
 
131
132
  **Also available:**
132
- - `/pbr:quick` — work on one now
133
133
  - `/pbr:status` — see project status
134
134
 
135
135
  ───────────────────────────────────────────────────────────────
@@ -181,6 +181,45 @@ Todo {NNN} not found in pending todos.
181
181
  ───────────────────────────────────────────────────────────────
182
182
  ```
183
183
 
184
+ ### `work <NNN>`
185
+
186
+ 1. Find `.planning/todos/pending/{NNN}-*.md` (match by number prefix)
187
+ 2. If not found, display the same error block as `done` — suggest `/pbr:todo list`
188
+ 3. Read the todo file content (frontmatter + body)
189
+ 4. Extract the `title` from frontmatter and the full body (Goal, Scope, Acceptance Criteria sections)
190
+ 5. Display branded output:
191
+ ```
192
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
193
+ PLAN-BUILD-RUN ► WORKING ON TODO {NNN}
194
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
195
+
196
+ **Todo {NNN}:** {title}
197
+
198
+ Launching /pbr:quick with todo context...
199
+ ```
200
+ 6. Invoke the Skill tool: `skill: "pbr:quick"` with `args` set to the todo title followed by the body content. Format the args as:
201
+
202
+ ```
203
+ {title}
204
+
205
+ Context from todo {NNN}:
206
+ {body content — Goal, Scope, Acceptance Criteria sections}
207
+ ```
208
+
209
+ This hands off execution to `/pbr:quick`, which will spawn an executor agent, make atomic commits, and track the work. When quick completes, remind the user:
210
+
211
+ ```
212
+ ───────────────────────────────────────────────────────────────
213
+
214
+ ## ▶ Next Up
215
+
216
+ **Mark this todo as done if the work is complete**
217
+
218
+ `/pbr:todo done {NNN}`
219
+
220
+ ───────────────────────────────────────────────────────────────
221
+ ```
222
+
184
223
  ### No arguments
185
224
 
186
225
  Show a brief summary: count of pending todos, grouped by theme, plus usage hint.