@sienklogic/plan-build-run 2.22.2 → 2.23.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dashboard/package.json +2 -1
  3. package/dashboard/src/middleware/errorHandler.js +12 -2
  4. package/dashboard/src/repositories/planning.repository.js +23 -1
  5. package/dashboard/src/routes/pages.routes.js +65 -2
  6. package/dashboard/src/services/local-llm-metrics.service.js +81 -0
  7. package/dashboard/src/services/quick.service.js +62 -0
  8. package/dashboard/src/views/partials/analytics-content.ejs +61 -0
  9. package/dashboard/src/views/partials/quick-content.ejs +40 -0
  10. package/dashboard/src/views/partials/quick-detail-content.ejs +29 -0
  11. package/dashboard/src/views/partials/sidebar.ejs +8 -0
  12. package/dashboard/src/views/quick-detail.ejs +5 -0
  13. package/dashboard/src/views/quick.ejs +5 -0
  14. package/package.json +1 -1
  15. package/plugins/copilot-pbr/agents/debugger.agent.md +15 -0
  16. package/plugins/copilot-pbr/agents/researcher.agent.md +20 -0
  17. package/plugins/copilot-pbr/agents/synthesizer.agent.md +12 -0
  18. package/plugins/copilot-pbr/plugin.json +1 -1
  19. package/plugins/copilot-pbr/references/config-reference.md +89 -0
  20. package/plugins/copilot-pbr/skills/health/SKILL.md +8 -1
  21. package/plugins/copilot-pbr/skills/help/SKILL.md +4 -4
  22. package/plugins/copilot-pbr/skills/milestone/SKILL.md +12 -12
  23. package/plugins/copilot-pbr/skills/status/SKILL.md +37 -1
  24. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  25. package/plugins/cursor-pbr/agents/debugger.md +15 -0
  26. package/plugins/cursor-pbr/agents/researcher.md +20 -0
  27. package/plugins/cursor-pbr/agents/synthesizer.md +12 -0
  28. package/plugins/cursor-pbr/references/config-reference.md +89 -0
  29. package/plugins/cursor-pbr/skills/health/SKILL.md +8 -1
  30. package/plugins/cursor-pbr/skills/help/SKILL.md +4 -4
  31. package/plugins/cursor-pbr/skills/milestone/SKILL.md +12 -12
  32. package/plugins/cursor-pbr/skills/status/SKILL.md +37 -1
  33. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  34. package/plugins/pbr/agents/debugger.md +15 -0
  35. package/plugins/pbr/agents/researcher.md +20 -0
  36. package/plugins/pbr/agents/synthesizer.md +12 -0
  37. package/plugins/pbr/references/config-reference.md +89 -0
  38. package/plugins/pbr/scripts/check-config-change.js +33 -0
  39. package/plugins/pbr/scripts/check-plan-format.js +52 -4
  40. package/plugins/pbr/scripts/check-subagent-output.js +43 -3
  41. package/plugins/pbr/scripts/config-schema.json +48 -0
  42. package/plugins/pbr/scripts/local-llm/client.js +214 -0
  43. package/plugins/pbr/scripts/local-llm/health.js +217 -0
  44. package/plugins/pbr/scripts/local-llm/metrics.js +252 -0
  45. package/plugins/pbr/scripts/local-llm/operations/classify-artifact.js +76 -0
  46. package/plugins/pbr/scripts/local-llm/operations/classify-error.js +75 -0
  47. package/plugins/pbr/scripts/local-llm/operations/score-source.js +72 -0
  48. package/plugins/pbr/scripts/local-llm/operations/summarize-context.js +62 -0
  49. package/plugins/pbr/scripts/local-llm/operations/validate-task.js +59 -0
  50. package/plugins/pbr/scripts/local-llm/router.js +101 -0
  51. package/plugins/pbr/scripts/local-llm/shadow.js +60 -0
  52. package/plugins/pbr/scripts/local-llm/threshold-tuner.js +118 -0
  53. package/plugins/pbr/scripts/pbr-tools.js +120 -3
  54. package/plugins/pbr/scripts/post-write-dispatch.js +2 -2
  55. package/plugins/pbr/scripts/progress-tracker.js +29 -3
  56. package/plugins/pbr/scripts/session-cleanup.js +36 -1
  57. package/plugins/pbr/scripts/validate-task.js +30 -1
  58. package/plugins/pbr/skills/health/SKILL.md +8 -1
  59. package/plugins/pbr/skills/help/SKILL.md +4 -4
  60. package/plugins/pbr/skills/milestone/SKILL.md +12 -12
  61. package/plugins/pbr/skills/status/SKILL.md +38 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to Plan-Build-Run will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.23.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.22.2...plan-build-run-v2.23.0) (2026-02-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * **28-01:** add local LLM foundation — client, health, metrics, config schema, hook integrations, tests ([44b5a77](https://github.com/SienkLogic/plan-build-run/commit/44b5a773fbb22d77ac0dbf3325201980dd1e9635))
14
+ * **29-01:** integrate local LLM into hooks — artifact classification, task validation, error classification, CLI ([785a708](https://github.com/SienkLogic/plan-build-run/commit/785a708039b0c363589e7151c40df989bb2a7959))
15
+ * **30-01:** add metrics display — session summaries, status skill, CLI, dashboard analytics ([d4ae4a4](https://github.com/SienkLogic/plan-build-run/commit/d4ae4a400932a324f61872169c1dae73cc2923ce))
16
+ * **30-03:** add local-llm-metrics.service.js with getLlmMetrics and Vitest tests ([fc1dd8f](https://github.com/SienkLogic/plan-build-run/commit/fc1dd8f6fbada707543dea848e7355ff52896667))
17
+ * **30-03:** wire getLlmMetrics into /analytics route and add Local LLM Offload section to EJS template ([5fcb61e](https://github.com/SienkLogic/plan-build-run/commit/5fcb61e25848395a4d59f4c555bbb8cf913efb72))
18
+ * **31-01:** add adaptive router — complexity heuristic, confidence gate, 3 routing strategies ([7905462](https://github.com/SienkLogic/plan-build-run/commit/7905462a5dc11063813c1c31400a17f4d2314a23))
19
+ * **32-01:** add agent support — source scoring, error classification, context summarization, prompt injection ([3693660](https://github.com/SienkLogic/plan-build-run/commit/36936609200596fe04cbf27f6eff41566733a74e))
20
+ * **33-01:** add shadow mode, threshold tuner, comprehensive tests, docs, cross-plugin sync ([dbacfed](https://github.com/SienkLogic/plan-build-run/commit/dbacfed98bd6819babad2f07df588cf545d5b99c))
21
+ * **34-01:** add config.features.source_scoring feature flag guard to score-source.js ([a0945a2](https://github.com/SienkLogic/plan-build-run/commit/a0945a2ffd1067ab3644063a8deee4e06a42ea9e))
22
+ * **34-01:** wire runShadow() into router.js post-call path for all 3 routing strategies ([f233948](https://github.com/SienkLogic/plan-build-run/commit/f23394810ab0f647f8eb1ba1d8f78a5eb7d048d8))
23
+ * **35-01:** GREEN - add escapeHtml helper and use it in HTMX error handler path ([a1830c9](https://github.com/SienkLogic/plan-build-run/commit/a1830c9230ba7ab1ea693a7ff2d36e786d5878d3))
24
+ * **35-01:** GREEN - add sanitize-html post-processing to planning.repository ([6d8122b](https://github.com/SienkLogic/plan-build-run/commit/6d8122b83cf1ed62101a3e9fc2f436caf6b0858e))
25
+ * **35-03:** add Quick Tasks view with /quick and /quick/:id routes ([22abe29](https://github.com/SienkLogic/plan-build-run/commit/22abe29e3951ea8e4fbd045116f6216326ee3907))
26
+
27
+
28
+ ### Documentation
29
+
30
+ * **34-01:** mark all LLM-01 through LLM-34 traceability entries as Verified ([4311497](https://github.com/SienkLogic/plan-build-run/commit/4311497e1e91f55a575da4468924d21903457cdf))
31
+ * **quick-002:** add .active-skill stale detection to health Check 10 ([3f95b16](https://github.com/SienkLogic/plan-build-run/commit/3f95b16461478bf4271ef4f7ce94582188869bae))
32
+ * **quick-002:** fix NEXT UP banner indentation in milestone SKILL.md ([2983000](https://github.com/SienkLogic/plan-build-run/commit/2983000bd4df5e95c63cdc8f79601c7894883dbb))
33
+ * **quick-002:** replace arrow-list with bullet style in help SKILL.md ([ad78663](https://github.com/SienkLogic/plan-build-run/commit/ad7866375c6f7fd7dda3e195413df95f3ee191bc))
34
+
8
35
  ## [2.22.2](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.22.1...plan-build-run-v2.22.2) (2026-02-24)
9
36
 
10
37
 
@@ -26,7 +26,8 @@
26
26
  "express": "^5.1.0",
27
27
  "gray-matter": "^4.0.3",
28
28
  "helmet": "^8.1.0",
29
- "marked": "^17.0.1"
29
+ "marked": "^17.0.1",
30
+ "sanitize-html": "^2.13.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@vitest/coverage-v8": "^4.0.18",
@@ -3,6 +3,16 @@
3
3
  * MUST have exactly 4 parameters for Express to recognize it as an error handler.
4
4
  */
5
5
 
6
+ function escapeHtml(str) {
7
+ if (!str) return '';
8
+ return str
9
+ .replace(/&/g, '&')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#39;');
14
+ }
15
+
6
16
  // eslint-disable-next-line no-unused-vars
7
17
  export default function errorHandler(err, req, res, next) {
8
18
  // If headers already sent, delegate to Express default handler
@@ -40,9 +50,9 @@ export default function errorHandler(err, req, res, next) {
40
50
 
41
51
  // Render response
42
52
  if (isHtmx) {
43
- let html = `<h1>Error ${status}</h1><p>${templateData.message}</p>`;
53
+ let html = `<h1>Error ${status}</h1><p>${escapeHtml(templateData.message)}</p>`;
44
54
  if (templateData.stack) {
45
- html += `<pre><code>${templateData.stack}</code></pre>`;
55
+ html += `<pre><code>${escapeHtml(templateData.stack)}</code></pre>`;
46
56
  }
47
57
  html += '<p><a href="/">Return to Dashboard</a></p>';
48
58
  return res.status(status).send(html);
@@ -2,6 +2,7 @@ import { readFile, readdir } from 'node:fs/promises';
2
2
  import { join, resolve, relative, normalize } from 'node:path';
3
3
  import matter from 'gray-matter';
4
4
  import { marked } from 'marked';
5
+ import sanitizeHtml from 'sanitize-html';
5
6
 
6
7
  marked.setOptions({ gfm: true, breaks: false });
7
8
 
@@ -76,9 +77,30 @@ export async function readMarkdownFile(filePath) {
76
77
 
77
78
  const html = marked.parse(content);
78
79
 
80
+ const sanitizedHtml = sanitizeHtml(html, {
81
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat([
82
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
83
+ 'img', 'details', 'summary', 'del', 'ins',
84
+ 'sup', 'sub', 'abbr', 'kbd', 'mark'
85
+ ]),
86
+ allowedAttributes: {
87
+ ...sanitizeHtml.defaults.allowedAttributes,
88
+ 'a': ['href', 'name', 'target', 'rel'],
89
+ 'img': ['src', 'alt', 'title', 'width', 'height'],
90
+ 'code': ['class'],
91
+ 'pre': ['class'],
92
+ 'span': ['class'],
93
+ 'div': ['class'],
94
+ 'td': ['align'],
95
+ 'th': ['align', 'scope'],
96
+ 'h1': ['id'], 'h2': ['id'], 'h3': ['id'],
97
+ 'h4': ['id'], 'h5': ['id'], 'h6': ['id']
98
+ }
99
+ });
100
+
79
101
  return {
80
102
  frontmatter: data,
81
- html,
103
+ html: sanitizedHtml,
82
104
  rawContent: content
83
105
  };
84
106
  }
@@ -5,7 +5,9 @@ import { parseStateFile, derivePhaseStatuses } from '../services/dashboard.servi
5
5
  import { listPendingTodos, getTodoDetail, createTodo, completeTodo } from '../services/todo.service.js';
6
6
  import { getAllMilestones, getMilestoneDetail } from '../services/milestone.service.js';
7
7
  import { getProjectAnalytics } from '../services/analytics.service.js';
8
+ import { getLlmMetrics } from '../services/local-llm-metrics.service.js';
8
9
  import { listNotes } from '../services/notes.service.js';
10
+ import { listQuickTasks, getQuickTask } from '../services/quick.service.js';
9
11
 
10
12
  const router = Router();
11
13
 
@@ -358,14 +360,18 @@ router.get('/dependencies', async (req, res) => {
358
360
 
359
361
  router.get('/analytics', async (req, res) => {
360
362
  const projectDir = req.app.locals.projectDir;
361
- const analytics = await getProjectAnalytics(projectDir);
363
+ const [analytics, llmMetrics] = await Promise.all([
364
+ getProjectAnalytics(projectDir),
365
+ getLlmMetrics(projectDir)
366
+ ]);
362
367
 
363
368
  const templateData = {
364
369
  title: 'Analytics',
365
370
  activePage: 'analytics',
366
371
  currentPath: '/analytics',
367
372
  breadcrumbs: [{ label: 'Analytics' }],
368
- analytics
373
+ analytics,
374
+ llmMetrics
369
375
  };
370
376
 
371
377
  res.setHeader('Vary', 'HX-Request');
@@ -423,4 +429,61 @@ router.get('/roadmap', async (req, res) => {
423
429
  }
424
430
  });
425
431
 
432
+ router.get('/quick', async (req, res) => {
433
+ const projectDir = req.app.locals.projectDir;
434
+ const tasks = await listQuickTasks(projectDir);
435
+
436
+ const templateData = {
437
+ title: 'Quick Tasks',
438
+ activePage: 'quick',
439
+ currentPath: '/quick',
440
+ breadcrumbs: [{ label: 'Quick Tasks' }],
441
+ tasks
442
+ };
443
+
444
+ res.setHeader('Vary', 'HX-Request');
445
+
446
+ if (req.get('HX-Request') === 'true') {
447
+ res.render('partials/quick-content', templateData);
448
+ } else {
449
+ res.render('quick', templateData);
450
+ }
451
+ });
452
+
453
+ router.get('/quick/:id', async (req, res) => {
454
+ const { id } = req.params;
455
+
456
+ // Validate ID format: must be exactly three digits
457
+ if (!/^\d{3}$/.test(id)) {
458
+ const err = new Error('Quick Task ID must be a three-digit number (e.g., 001, 005, 042)');
459
+ err.status = 404;
460
+ throw err;
461
+ }
462
+
463
+ const projectDir = req.app.locals.projectDir;
464
+ const task = await getQuickTask(projectDir, id);
465
+
466
+ if (!task) {
467
+ const err = new Error(`Quick task ${id} not found`);
468
+ err.status = 404;
469
+ throw err;
470
+ }
471
+
472
+ const templateData = {
473
+ title: `Quick Task ${task.id}: ${task.title}`,
474
+ activePage: 'quick',
475
+ currentPath: '/quick/' + id,
476
+ breadcrumbs: [{ label: 'Quick Tasks', url: '/quick' }, { label: 'Task ' + id }],
477
+ ...task
478
+ };
479
+
480
+ res.setHeader('Vary', 'HX-Request');
481
+
482
+ if (req.get('HX-Request') === 'true') {
483
+ res.render('partials/quick-detail-content', templateData);
484
+ } else {
485
+ res.render('quick-detail', templateData);
486
+ }
487
+ });
488
+
426
489
  export default router;
@@ -0,0 +1,81 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Read and aggregate local LLM metrics from .planning/logs/local-llm-metrics.jsonl.
6
+ *
7
+ * @param {string} projectDir - Absolute path to the project root
8
+ * @returns {Promise<{summary: object, byOperation: Array, baseline: object}|null>}
9
+ * Returns null if the file does not exist, is empty, or has no valid entries.
10
+ */
11
+ export async function getLlmMetrics(projectDir) {
12
+ try {
13
+ const filePath = join(projectDir, '.planning', 'logs', 'local-llm-metrics.jsonl');
14
+
15
+ let raw;
16
+ try {
17
+ raw = await readFile(filePath, 'utf8');
18
+ } catch (err) {
19
+ if (err.code === 'ENOENT') return null;
20
+ throw err;
21
+ }
22
+
23
+ // Parse valid JSON lines, skip malformed
24
+ const entries = [];
25
+ for (const line of raw.split('\n')) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed) continue;
28
+ try {
29
+ entries.push(JSON.parse(trimmed));
30
+ } catch {
31
+ // skip malformed lines
32
+ }
33
+ }
34
+
35
+ if (entries.length === 0) return null;
36
+
37
+ // Compute aggregate summary
38
+ const total_calls = entries.length;
39
+ const fallback_count = entries.filter(e => e.fallback_used === true).length;
40
+ const fallback_rate_pct = Math.round(fallback_count / total_calls * 100);
41
+ const avg_latency_ms = Math.round(
42
+ entries.reduce((sum, e) => sum + (e.latency_ms || 0), 0) / total_calls
43
+ );
44
+ const tokens_saved = entries.reduce((sum, e) => sum + (e.tokens_saved_frontier || 0), 0);
45
+ const cost_saved_usd = parseFloat((tokens_saved * (3.0 / 1_000_000)).toFixed(4));
46
+
47
+ const summary = {
48
+ total_calls,
49
+ fallback_count,
50
+ fallback_rate_pct,
51
+ avg_latency_ms,
52
+ tokens_saved,
53
+ cost_saved_usd
54
+ };
55
+
56
+ // Compute byOperation: group entries by operation field
57
+ const opMap = new Map();
58
+ for (const e of entries) {
59
+ const op = e.operation || 'unknown';
60
+ if (!opMap.has(op)) {
61
+ opMap.set(op, { operation: op, calls: 0, fallbacks: 0, tokens_saved: 0 });
62
+ }
63
+ const rec = opMap.get(op);
64
+ rec.calls += 1;
65
+ if (e.fallback_used === true) rec.fallbacks += 1;
66
+ rec.tokens_saved += e.tokens_saved_frontier || 0;
67
+ }
68
+
69
+ const byOperation = Array.from(opMap.values()).sort((a, b) => b.calls - a.calls);
70
+
71
+ // Compute baseline (LLM-15)
72
+ const baseline = {
73
+ hook_invocations: total_calls,
74
+ estimated_frontier_tokens_without_local: tokens_saved
75
+ };
76
+
77
+ return { summary, byOperation, baseline };
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
@@ -0,0 +1,62 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { readMarkdownFile } from '../repositories/planning.repository.js';
4
+
5
+ export async function listQuickTasks(projectDir) {
6
+ const quickDir = join(projectDir, '.planning', 'quick');
7
+ let entries;
8
+ try {
9
+ entries = await readdir(quickDir, { withFileTypes: true });
10
+ } catch (err) {
11
+ if (err.code === 'ENOENT') return [];
12
+ throw err;
13
+ }
14
+
15
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
16
+ const tasks = [];
17
+ for (const dirName of dirs) {
18
+ const match = dirName.match(/^(\d{3})-(.+)$/);
19
+ if (!match) continue;
20
+ const [, id, slug] = match;
21
+ const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
22
+ let status = 'in-progress';
23
+ try {
24
+ const summaryPath = join(quickDir, dirName, 'SUMMARY.md');
25
+ const { frontmatter } = await readMarkdownFile(summaryPath);
26
+ if (frontmatter.status) status = frontmatter.status;
27
+ } catch { /* No SUMMARY.md yet */ }
28
+ tasks.push({ id, slug, dirName, title, status });
29
+ }
30
+ return tasks;
31
+ }
32
+
33
+ export async function getQuickTask(projectDir, id) {
34
+ const quickDir = join(projectDir, '.planning', 'quick');
35
+ let entries;
36
+ try {
37
+ entries = await readdir(quickDir, { withFileTypes: true });
38
+ } catch (err) {
39
+ if (err.code === 'ENOENT') return null;
40
+ throw err;
41
+ }
42
+ const dirEntry = entries.filter(e => e.isDirectory()).find(e => e.name.startsWith(id + '-'));
43
+ if (!dirEntry) return null;
44
+
45
+ const taskDir = join(quickDir, dirEntry.name);
46
+ const match = dirEntry.name.match(/^(\d{3})-(.+)$/);
47
+ const slug = match ? match[2] : dirEntry.name;
48
+ const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
49
+
50
+ let planHtml = null, summaryHtml = null, summaryFrontmatter = {};
51
+ try {
52
+ const plan = await readMarkdownFile(join(taskDir, 'PLAN.md'));
53
+ planHtml = plan.html;
54
+ } catch { /* missing */ }
55
+ try {
56
+ const summary = await readMarkdownFile(join(taskDir, 'SUMMARY.md'));
57
+ summaryHtml = summary.html;
58
+ summaryFrontmatter = summary.frontmatter;
59
+ } catch { /* missing */ }
60
+
61
+ return { id, slug, dirName: dirEntry.name, title, status: summaryFrontmatter.status || 'in-progress', planHtml, summaryHtml };
62
+ }
@@ -88,3 +88,64 @@
88
88
  <% } else { %>
89
89
  <p>No phase data available.</p>
90
90
  <% } %>
91
+
92
+ <% if (typeof llmMetrics !== 'undefined' && llmMetrics) { %>
93
+ <article>
94
+ <header>Local LLM Offload</header>
95
+ <div class="grid">
96
+ <article>
97
+ <header>Total Calls</header>
98
+ <strong class="stat-value"><%= llmMetrics.summary.total_calls %></strong>
99
+ <span class="stat-unit">calls</span>
100
+ </article>
101
+ <article>
102
+ <header>Tokens Saved</header>
103
+ <strong class="stat-value"><%= llmMetrics.summary.tokens_saved.toLocaleString() %></strong>
104
+ <span class="stat-unit">frontier tokens</span>
105
+ </article>
106
+ <article>
107
+ <header>Est. Cost Saved</header>
108
+ <strong class="stat-value">$<%= llmMetrics.summary.cost_saved_usd.toFixed(4) %></strong>
109
+ <span class="stat-unit">at $3/M tokens</span>
110
+ </article>
111
+ <article>
112
+ <header>Fallback Rate</header>
113
+ <strong class="stat-value"><%= llmMetrics.summary.fallback_rate_pct %>%</strong>
114
+ <span class="stat-unit"><%= llmMetrics.summary.fallback_count %> fallbacks</span>
115
+ </article>
116
+ <article>
117
+ <header>Avg Latency</header>
118
+ <strong class="stat-value"><%= llmMetrics.summary.avg_latency_ms %></strong>
119
+ <span class="stat-unit">ms/call</span>
120
+ </article>
121
+ </div>
122
+ <% if (llmMetrics.byOperation && llmMetrics.byOperation.length > 0) { %>
123
+ <div class="overflow-auto" style="margin-top: var(--space-md);">
124
+ <table>
125
+ <thead>
126
+ <tr>
127
+ <th>Operation</th>
128
+ <th>Calls</th>
129
+ <th>Fallbacks</th>
130
+ <th>Tokens Saved</th>
131
+ </tr>
132
+ </thead>
133
+ <tbody>
134
+ <% llmMetrics.byOperation.forEach(op => { %>
135
+ <tr>
136
+ <td><%= op.operation %></td>
137
+ <td><%= op.calls %></td>
138
+ <td><%= op.fallbacks %></td>
139
+ <td><%= op.tokens_saved.toLocaleString() %></td>
140
+ </tr>
141
+ <% }) %>
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ <% } %>
146
+ <footer style="color: var(--pico-muted-color); font-size: 0.85em;">
147
+ Baseline estimate: each local call replaced ~<%= llmMetrics.baseline.estimated_frontier_tokens_without_local.toLocaleString() %> frontier tokens total.
148
+ Advisory only — no data collected when local LLM is disabled.
149
+ </footer>
150
+ </article>
151
+ <% } %>
@@ -0,0 +1,40 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Quick Tasks</h1>
3
+
4
+ <% if (typeof tasks !== 'undefined' && tasks.length > 0) { %>
5
+ <article>
6
+ <div class="table-wrap">
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th scope="col">ID</th>
11
+ <th scope="col">Title</th>
12
+ <th scope="col">Status</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% tasks.forEach(function(task) { %>
17
+ <tr>
18
+ <td><%= task.id %></td>
19
+ <td>
20
+ <a href="/quick/<%= task.id %>"
21
+ hx-get="/quick/<%= task.id %>"
22
+ hx-target="#main-content"
23
+ hx-push-url="true">
24
+ <%= task.title %>
25
+ </a>
26
+ </td>
27
+ <td>
28
+ <span class="status-badge" data-status="<%= task.status %>">
29
+ <%= task.status %>
30
+ </span>
31
+ </td>
32
+ </tr>
33
+ <% }); %>
34
+ </tbody>
35
+ </table>
36
+ </div>
37
+ </article>
38
+ <% } else { %>
39
+ <%- include('empty-state', { icon: '⚡', title: 'No quick tasks found', action: '' }) %>
40
+ <% } %>
@@ -0,0 +1,29 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1><%= title %></h1>
3
+
4
+ <p><a href="/quick">&larr; Back to Quick Tasks</a></p>
5
+
6
+ <article>
7
+ <header>
8
+ <strong>Quick Task <%= id %></strong>
9
+ &nbsp;
10
+ <span class="status-badge" data-status="<%= status %>">
11
+ <%= status %>
12
+ </span>
13
+ </header>
14
+
15
+ <% if (planHtml) { %>
16
+ <section>
17
+ <h2>Plan</h2>
18
+ <%- planHtml %>
19
+ </section>
20
+ <% } %>
21
+
22
+ <% if (summaryHtml) { %>
23
+ <hr>
24
+ <section>
25
+ <h2>Summary</h2>
26
+ <%- summaryHtml %>
27
+ </section>
28
+ <% } %>
29
+ </article>
@@ -79,6 +79,14 @@
79
79
  Notes
80
80
  </a>
81
81
  </li>
82
+ <li>
83
+ <a href="/quick"
84
+ hx-get="/quick"
85
+ hx-target="#main-content"
86
+ hx-push-url="true"<%= typeof activePage !== 'undefined' && activePage === 'quick' ? ' aria-current="page"' : '' %>>
87
+ Quick Tasks
88
+ </a>
89
+ </li>
82
90
  </ul>
83
91
  </details>
84
92
 
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: title, activePage: 'quick' }) %>
2
+
3
+ <%- include('partials/quick-detail-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: 'Quick Tasks', activePage: 'quick' }) %>
2
+
3
+ <%- include('partials/quick-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sienklogic/plan-build-run",
3
- "version": "2.22.2",
3
+ "version": "2.23.0",
4
4
  "description": "Plan it, Build it, Run it — structured development workflow for Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -138,6 +138,21 @@ Then emit a `DECISION` checkpoint asking the user to approve, modify, or reject
138
138
 
139
139
  **Commit format**: `fix({scope}): {description}` with body: `Root cause: ...` and `Debug session: .planning/debug/{slug}.md`
140
140
 
141
+ ## Local LLM Error Classification (Optional)
142
+
143
+ When you receive an error message or stack trace, you MAY use the local LLM to classify it before starting hypothesis generation. This is advisory — skip it if unavailable.
144
+
145
+ ```bash
146
+ # Write the error to a temp file, then classify:
147
+ echo "Error text here" > /tmp/debug-error.txt
148
+ node "${PLUGIN_ROOT}/scripts/pbr-tools.js" llm classify-error /tmp/debug-error.txt debugger 2>/dev/null
149
+ # Returns: {"category":"missing_output","confidence":0.91,"latency_ms":1840,"fallback_used":false}
150
+ ```
151
+
152
+ Categories: `connection_refused`, `timeout`, `missing_output`, `wrong_output_format`, `permission_error`, `unknown`.
153
+
154
+ If classification succeeds, use the returned category to bias your initial hypothesis ranking. If it returns null or fails, proceed with manual hypothesis generation as normal.
155
+
141
156
  ## Common Bug Patterns
142
157
 
143
158
  Reference: `references/common-bug-patterns.md` — covers off-by-one, null/undefined, async/timing, state management, import/module, environment, and data shape patterns.
@@ -54,6 +54,26 @@ All claims must be attributed to a source level. Higher levels override lower le
54
54
 
55
55
  **Offline Fallback**: If web tools are unavailable (air-gapped environment, MCP not configured), rely on local sources: codebase analysis via Glob/Grep, existing documentation, and README files. Assign these S3-S4 confidence levels. Do not attempt WebFetch or WebSearch — note in the output header that external sources were unavailable.
56
56
 
57
+ ## Local LLM Source Scoring (Optional)
58
+
59
+ If local LLM offload is configured, you MAY use it to score source credibility instead of manually assigning S-levels. This is advisory — never wait on it or fail if it returns null.
60
+
61
+ Check availability first:
62
+
63
+ ```bash
64
+ node "${PLUGIN_ROOT}/scripts/pbr-tools.js" llm status 2>/dev/null
65
+ ```
66
+
67
+ If `enabled: true`, score a source excerpt:
68
+
69
+ ```bash
70
+ echo "Source URL and content excerpt" > /tmp/source-excerpt.txt
71
+ node "${PLUGIN_ROOT}/scripts/pbr-tools.js" llm score-source "https://example.com/docs" /tmp/source-excerpt.txt 2>/dev/null
72
+ # Returns: {"level":"S2","confidence":0.87,"reason":"Official library documentation page"}
73
+ ```
74
+
75
+ Use the returned `level` to set your source tag. If the call fails or returns `null`, assign the level manually per the hierarchy table above.
76
+
57
77
  ---
58
78
 
59
79
  ## Confidence Levels
@@ -98,6 +98,18 @@ conflicts: N
98
98
  - **Research gaps**: Add `[RESEARCH GAP]` flag, add to Open Questions with high impact, never fabricate
99
99
  - **Duplicates**: Consolidate into one entry, note multi-source agreement, reference all documents
100
100
 
101
+ ## Local LLM Context Summarization (Optional)
102
+
103
+ When input research documents are large (>2000 words combined), you MAY use the local LLM to pre-summarize each document before synthesis. This reduces your own context consumption. Advisory only — if unavailable, read documents normally.
104
+
105
+ ```bash
106
+ # Pre-summarize a large research document to ~150 words:
107
+ node "${PLUGIN_ROOT}/scripts/pbr-tools.js" llm summarize /path/to/RESEARCH.md 150 2>/dev/null
108
+ # Returns: {"summary":"...plain text summary under 150 words...","latency_ms":2100,"fallback_used":false}
109
+ ```
110
+
111
+ Use the returned `summary` string as your working copy of that document's findings. Still read the original for any specific version numbers, code examples, or direct quotes needed in the output.
112
+
101
113
  ## Anti-Patterns
102
114
 
103
115
  ### Universal Anti-Patterns
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pbr",
3
3
  "displayName": "Plan-Build-Run",
4
- "version": "2.22.2",
4
+ "version": "2.23.0",
5
5
  "description": "Plan-Build-Run — Structured development workflow for GitHub Copilot CLI. Solves context rot through disciplined agent delegation, structured planning, atomic execution, and goal-backward verification.",
6
6
  "author": {
7
7
  "name": "SienkLogic",