@sienklogic/plan-build-run 2.26.2 → 2.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +29 -0
  3. package/dashboard/public/css/layout.css +7 -283
  4. package/dashboard/public/css/status-colors.css +7 -0
  5. package/dashboard/public/css/tokens.css +3 -3
  6. package/dashboard/public/js/sidebar-toggle.js +9 -31
  7. package/dashboard/public/js/theme-toggle.js +4 -4
  8. package/dashboard/src/views/partials/activity-feed.ejs +17 -9
  9. package/dashboard/src/views/partials/analytics-content.ejs +178 -88
  10. package/dashboard/src/views/partials/audit-detail-content.ejs +6 -4
  11. package/dashboard/src/views/partials/audits-content.ejs +28 -26
  12. package/dashboard/src/views/partials/breadcrumbs.ejs +8 -4
  13. package/dashboard/src/views/partials/config-content.ejs +98 -95
  14. package/dashboard/src/views/partials/dashboard-content.ejs +69 -60
  15. package/dashboard/src/views/partials/dependencies-content.ejs +5 -3
  16. package/dashboard/src/views/partials/empty-state.ejs +10 -5
  17. package/dashboard/src/views/partials/footer.ejs +8 -2
  18. package/dashboard/src/views/partials/head.ejs +2 -1
  19. package/dashboard/src/views/partials/header.ejs +16 -19
  20. package/dashboard/src/views/partials/layout-bottom.ejs +5 -40
  21. package/dashboard/src/views/partials/layout-top.ejs +6 -5
  22. package/dashboard/src/views/partials/logs-content.ejs +26 -29
  23. package/dashboard/src/views/partials/milestone-detail-content.ejs +5 -5
  24. package/dashboard/src/views/partials/milestones-content.ejs +40 -31
  25. package/dashboard/src/views/partials/note-detail-content.ejs +7 -5
  26. package/dashboard/src/views/partials/notes-content.ejs +4 -4
  27. package/dashboard/src/views/partials/phase-content.ejs +6 -8
  28. package/dashboard/src/views/partials/phase-doc-content.ejs +13 -15
  29. package/dashboard/src/views/partials/phase-timeline.ejs +22 -15
  30. package/dashboard/src/views/partials/phases-content.ejs +98 -84
  31. package/dashboard/src/views/partials/quick-content.ejs +34 -32
  32. package/dashboard/src/views/partials/quick-detail-content.ejs +20 -19
  33. package/dashboard/src/views/partials/requirements-content.ejs +6 -6
  34. package/dashboard/src/views/partials/research-content.ejs +14 -14
  35. package/dashboard/src/views/partials/research-detail-content.ejs +13 -11
  36. package/dashboard/src/views/partials/roadmap-content.ejs +145 -128
  37. package/dashboard/src/views/partials/sidebar.ejs +86 -140
  38. package/dashboard/src/views/partials/todo-create-content.ejs +51 -46
  39. package/dashboard/src/views/partials/todo-detail-content.ejs +26 -25
  40. package/dashboard/src/views/partials/todos-content.ejs +65 -62
  41. package/dashboard/src/views/partials/todos-done-content.ejs +33 -31
  42. package/package.json +1 -1
  43. package/plugins/copilot-pbr/plugin.json +1 -1
  44. package/plugins/copilot-pbr/skills/build/SKILL.md +12 -0
  45. package/plugins/copilot-pbr/skills/quick/SKILL.md +12 -0
  46. package/plugins/copilot-pbr/skills/review/SKILL.md +14 -0
  47. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  48. package/plugins/cursor-pbr/README.md +20 -0
  49. package/plugins/cursor-pbr/skills/build/SKILL.md +12 -0
  50. package/plugins/cursor-pbr/skills/quick/SKILL.md +12 -0
  51. package/plugins/cursor-pbr/skills/review/SKILL.md +14 -0
  52. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  53. package/plugins/pbr/scripts/local-llm/metrics.js +121 -33
  54. package/plugins/pbr/skills/build/SKILL.md +12 -0
  55. package/plugins/pbr/skills/quick/SKILL.md +12 -0
  56. package/plugins/pbr/skills/review/SKILL.md +14 -0
@@ -29,6 +29,15 @@ function logMetric(planningDir, entry) {
29
29
  const logFile = path.join(logsDir, 'local-llm-metrics.jsonl');
30
30
 
31
31
  fs.mkdirSync(logsDir, { recursive: true });
32
+
33
+ // Update lifetime totals BEFORE appending to JSONL so seeding
34
+ // (which reads the JSONL) doesn't double-count this entry
35
+ try {
36
+ updateLifetimeTotals(logsDir, entry);
37
+ } catch (_) {
38
+ // Totals update failure is non-fatal
39
+ }
40
+
32
41
  fs.appendFileSync(logFile, JSON.stringify(entry) + '\n', 'utf8');
33
42
 
34
43
  // Rotate if over MAX_ENTRIES
@@ -47,6 +56,63 @@ function logMetric(planningDir, entry) {
47
56
  }
48
57
  }
49
58
 
59
+ /**
60
+ * Atomically increments the lifetime-totals.json running counters.
61
+ * This file persists across JSONL log rotations so lifetime metrics never plateau.
62
+ *
63
+ * @param {string} logsDir - path to the .planning/logs directory
64
+ * @param {object} entry - the metric entry being logged
65
+ */
66
+ function updateLifetimeTotals(logsDir, entry) {
67
+ const totalsFile = path.join(logsDir, 'lifetime-totals.json');
68
+ let totals = null;
69
+
70
+ try {
71
+ totals = JSON.parse(fs.readFileSync(totalsFile, 'utf8'));
72
+ } catch (_) {
73
+ // File doesn't exist yet or is corrupt — seed from existing JSONL
74
+ totals = seedTotalsFromJsonl(logsDir);
75
+ }
76
+
77
+ totals.total_calls = (totals.total_calls || 0) + 1;
78
+ totals.fallback_count = (totals.fallback_count || 0) + (entry.fallback_used ? 1 : 0);
79
+ totals.tokens_saved = (totals.tokens_saved || 0) + (entry.tokens_saved_frontier || 0);
80
+ totals.total_latency_ms = (totals.total_latency_ms || 0) + (entry.latency_ms || 0);
81
+
82
+ fs.writeFileSync(totalsFile, JSON.stringify(totals) + '\n', 'utf8');
83
+ }
84
+
85
+ /**
86
+ * Seeds lifetime totals by scanning the existing JSONL log.
87
+ * Called once when lifetime-totals.json doesn't exist yet (migration from pre-totals installs).
88
+ * Returns the accumulated totals from whatever entries remain in the JSONL.
89
+ *
90
+ * @param {string} logsDir - path to the .planning/logs directory
91
+ * @returns {{ total_calls: number, fallback_count: number, tokens_saved: number, total_latency_ms: number }}
92
+ */
93
+ function seedTotalsFromJsonl(logsDir) {
94
+ const seed = { total_calls: 0, fallback_count: 0, tokens_saved: 0, total_latency_ms: 0 };
95
+ try {
96
+ const logFile = path.join(logsDir, 'local-llm-metrics.jsonl');
97
+ const contents = fs.readFileSync(logFile, 'utf8');
98
+ const lines = contents.split(/\r?\n/).filter((l) => l.trim() !== '');
99
+ for (const line of lines) {
100
+ try {
101
+ const e = JSON.parse(line);
102
+ seed.total_calls += 1;
103
+ seed.fallback_count += e.fallback_used ? 1 : 0;
104
+ seed.tokens_saved += e.tokens_saved_frontier || 0;
105
+ seed.total_latency_ms += e.latency_ms || 0;
106
+ } catch (_) {
107
+ // Skip malformed lines
108
+ }
109
+ }
110
+ } catch (_) {
111
+ // No JSONL file — start at zero
112
+ }
113
+ return seed;
114
+ }
115
+
50
116
  /**
51
117
  * Reads metric entries from the JSONL log that occurred at or after sessionStartTime.
52
118
  *
@@ -131,47 +197,69 @@ function computeLifetimeMetrics(planningDir, frontierTokenRate) {
131
197
  };
132
198
 
133
199
  try {
134
- const logFile = path.join(planningDir, 'logs', 'local-llm-metrics.jsonl');
135
- let contents;
200
+ const rate = frontierTokenRate != null ? frontierTokenRate : 3.0;
201
+ const logsDir = path.join(planningDir, 'logs');
202
+ const totalsFile = path.join(logsDir, 'lifetime-totals.json');
203
+
204
+ // Primary path: read from lifetime-totals.json (survives log rotation)
205
+ let totals = null;
136
206
  try {
137
- contents = fs.readFileSync(logFile, 'utf8');
207
+ totals = JSON.parse(fs.readFileSync(totalsFile, 'utf8'));
138
208
  } catch (_) {
139
- return zero;
209
+ // No totals file — fall back to JSONL scan (migration path for existing installs)
140
210
  }
141
211
 
142
- const entries = contents
143
- .split(/\r?\n/)
144
- .filter((l) => l.trim() !== '')
145
- .map((l) => {
146
- try {
147
- return JSON.parse(l);
148
- } catch (_) {
149
- return null;
150
- }
151
- })
152
- .filter((e) => e !== null);
153
-
154
- if (entries.length === 0) return zero;
212
+ // Build by_operation from the JSONL (only covers recent entries, but still useful)
213
+ const by_operation = {};
214
+ const logFile = path.join(logsDir, 'local-llm-metrics.jsonl');
215
+ try {
216
+ const contents = fs.readFileSync(logFile, 'utf8');
217
+ const entries = contents
218
+ .split(/\r?\n/)
219
+ .filter((l) => l.trim() !== '')
220
+ .map((l) => {
221
+ try {
222
+ return JSON.parse(l);
223
+ } catch (_) {
224
+ return null;
225
+ }
226
+ })
227
+ .filter((e) => e !== null);
155
228
 
156
- const rate = frontierTokenRate != null ? frontierTokenRate : 3.0;
157
- const total_calls = entries.length;
158
- const fallback_count = entries.filter((e) => e.fallback_used).length;
159
- const totalLatency = entries.reduce((sum, e) => sum + (e.latency_ms || 0), 0);
160
- const avg_latency_ms = total_calls > 0 ? totalLatency / total_calls : 0;
161
- const tokens_saved = entries.reduce((sum, e) => sum + (e.tokens_saved_frontier || 0), 0);
162
- const cost_saved_usd = tokens_saved * (rate / 1_000_000);
229
+ for (const e of entries) {
230
+ const op = e.operation || 'unknown';
231
+ if (!by_operation[op]) {
232
+ by_operation[op] = { calls: 0, fallbacks: 0, tokens_saved: 0 };
233
+ }
234
+ by_operation[op].calls += 1;
235
+ if (e.fallback_used) by_operation[op].fallbacks += 1;
236
+ by_operation[op].tokens_saved += e.tokens_saved_frontier || 0;
237
+ }
163
238
 
164
- const by_operation = {};
165
- for (const e of entries) {
166
- const op = e.operation || 'unknown';
167
- if (!by_operation[op]) {
168
- by_operation[op] = { calls: 0, fallbacks: 0, tokens_saved: 0 };
239
+ // If no totals file, compute from JSONL (legacy/migration path)
240
+ if (!totals) {
241
+ if (entries.length === 0) return zero;
242
+ const total_calls = entries.length;
243
+ const fallback_count = entries.filter((e) => e.fallback_used).length;
244
+ const totalLatency = entries.reduce((sum, e) => sum + (e.latency_ms || 0), 0);
245
+ const avg_latency_ms = total_calls > 0 ? totalLatency / total_calls : 0;
246
+ const tokens_saved = entries.reduce((sum, e) => sum + (e.tokens_saved_frontier || 0), 0);
247
+ const cost_saved_usd = tokens_saved * (rate / 1_000_000);
248
+ return { total_calls, fallback_count, avg_latency_ms, tokens_saved, cost_saved_usd, by_operation };
169
249
  }
170
- by_operation[op].calls += 1;
171
- if (e.fallback_used) by_operation[op].fallbacks += 1;
172
- by_operation[op].tokens_saved += e.tokens_saved_frontier || 0;
250
+ } catch (_) {
251
+ // No JSONL file — if we have totals, continue; otherwise return zero
252
+ if (!totals) return zero;
173
253
  }
174
254
 
255
+ // Use lifetime totals for the headline numbers
256
+ const total_calls = totals.total_calls || 0;
257
+ const fallback_count = totals.fallback_count || 0;
258
+ const tokens_saved = totals.tokens_saved || 0;
259
+ const total_latency_ms = totals.total_latency_ms || 0;
260
+ const avg_latency_ms = total_calls > 0 ? total_latency_ms / total_calls : 0;
261
+ const cost_saved_usd = tokens_saved * (rate / 1_000_000);
262
+
175
263
  return { total_calls, fallback_count, avg_latency_ms, tokens_saved, cost_saved_usd, by_operation };
176
264
  } catch (_) {
177
265
  return zero;
@@ -249,4 +337,4 @@ function logAgreement(planningDir, entry) {
249
337
  }
250
338
  }
251
339
 
252
- module.exports = { logMetric, readSessionMetrics, summarizeMetrics, computeLifetimeMetrics, formatSessionSummary, logAgreement };
340
+ module.exports = { logMetric, readSessionMetrics, summarizeMetrics, computeLifetimeMetrics, formatSessionSummary, logAgreement, updateLifetimeTotals, seedTotalsFromJsonl };
@@ -244,6 +244,18 @@ For each wave, in order (Wave 1, then Wave 2, etc.):
244
244
 
245
245
  For each plan in the current wave (excluding skipped plans):
246
246
 
247
+ **Local LLM plan quality check (optional, advisory):**
248
+
249
+ Before spawning executors for this wave, if `config.local_llm.enabled` is `true`, run a quick classification on each plan to catch stubs before wasting an executor spawn:
250
+
251
+ ```bash
252
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js llm classify PLAN ".planning/phases/{NN}-{slug}/{plan_id}-PLAN.md"
253
+ ```
254
+
255
+ - If classification is `"stub"` or `"partial"` with confidence >= 0.7: warn the user before spawning: `"⚠ Plan {plan_id} classified as {classification} (confidence {conf}) — consider refining before building."`
256
+ - If the command fails or returns null: skip silently (local LLM unavailable — not an error)
257
+ - This is advisory only — never block on the result
258
+
247
259
  **Present plan narrative before spawning:**
248
260
 
249
261
  Display to the user before spawning:
@@ -148,6 +148,18 @@ Before proceeding to Step 7, confirm these exist on disk:
148
148
 
149
149
  If either check fails, you have skipped steps. Go back and complete Steps 4-6. Do NOT proceed to spawning an executor.
150
150
 
151
+ ### Step 6b: Local LLM Task Validation (optional, advisory)
152
+
153
+ If `config.local_llm.enabled` is `true`, run a quick scope validation before spawning:
154
+
155
+ ```bash
156
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js llm classify PLAN ".planning/quick/{NNN}-{slug}/PLAN.md"
157
+ ```
158
+
159
+ - If classification is `"stub"` with confidence >= 0.7: warn `"⚠ Plan looks like a stub — executor may struggle. Consider adding more detail to task descriptions."`
160
+ - If the command fails or returns null: skip silently (local LLM unavailable)
161
+ - This is advisory only — never block on the result
162
+
151
163
  ### Step 7: Spawn Executor
152
164
 
153
165
  **Pre-spawn check** — Verify `.planning/quick/{NNN}-{slug}/PLAN.md` exists and contains at least one `<task>` block. If missing, STOP and complete Steps 4-6 first.
@@ -202,6 +202,20 @@ Then display the overall verdict (`PASSED`, `GAPS FOUND`, or `HUMAN NEEDED`) bef
202
202
 
203
203
  ---
204
204
 
205
+ ### Step 3b: Local LLM Verification Quality Check (optional, advisory)
206
+
207
+ After the verifier completes and writes VERIFICATION.md, if `config.local_llm.enabled` is `true`, run a quality classification:
208
+
209
+ ```bash
210
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js llm classify SUMMARY ".planning/phases/{NN}-{slug}/VERIFICATION.md"
211
+ ```
212
+
213
+ - If classification is `"thin"` with confidence >= 0.7: warn `"⚠ Verification report appears thin on details — UAT may not catch all gaps. Consider re-running with /pbr:review {N}."`
214
+ - If the command fails or returns null: skip silently (local LLM unavailable)
215
+ - This is advisory only — never block on the result
216
+
217
+ ---
218
+
205
219
  ### Step 4: Present Verification Results (inline)
206
220
 
207
221
  Read the VERIFICATION.md frontmatter. Check the `attempt` counter.