@sienklogic/plan-build-run 2.34.0 → 2.38.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 (160) hide show
  1. package/CHANGELOG.md +683 -0
  2. package/dashboard/public/css/command-center.css +152 -65
  3. package/dashboard/public/css/explorer.css +22 -41
  4. package/dashboard/public/css/layout.css +119 -1
  5. package/dashboard/public/css/tokens.css +13 -0
  6. package/dashboard/src/components/Layout.tsx +32 -6
  7. package/dashboard/src/components/explorer/tabs/PhasesTab.tsx +11 -1
  8. package/dashboard/src/components/explorer/tabs/TodosTab.tsx +18 -2
  9. package/dashboard/src/components/partials/AttentionPanel.tsx +7 -1
  10. package/dashboard/src/components/partials/CurrentPhaseCard.tsx +26 -24
  11. package/dashboard/src/components/partials/QuickActions.tsx +21 -11
  12. package/dashboard/src/components/partials/StatCardGrid.tsx +67 -0
  13. package/dashboard/src/components/partials/StatusHeader.tsx +1 -0
  14. package/dashboard/src/routes/command-center.routes.tsx +8 -7
  15. package/dashboard/src/routes/index.routes.tsx +32 -29
  16. package/package.json +2 -2
  17. package/plugins/copilot-pbr/agents/audit.agent.md +129 -16
  18. package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +49 -1
  19. package/plugins/copilot-pbr/agents/debugger.agent.md +50 -1
  20. package/plugins/copilot-pbr/agents/dev-sync.agent.md +23 -0
  21. package/plugins/copilot-pbr/agents/executor.agent.md +153 -8
  22. package/plugins/copilot-pbr/agents/general.agent.md +46 -1
  23. package/plugins/copilot-pbr/agents/integration-checker.agent.md +55 -2
  24. package/plugins/copilot-pbr/agents/plan-checker.agent.md +50 -2
  25. package/plugins/copilot-pbr/agents/planner.agent.md +80 -1
  26. package/plugins/copilot-pbr/agents/researcher.agent.md +50 -2
  27. package/plugins/copilot-pbr/agents/synthesizer.agent.md +49 -1
  28. package/plugins/copilot-pbr/agents/verifier.agent.md +114 -13
  29. package/plugins/copilot-pbr/commands/test.md +5 -0
  30. package/plugins/copilot-pbr/hooks/hooks.json +11 -0
  31. package/plugins/copilot-pbr/plugin.json +1 -1
  32. package/plugins/copilot-pbr/references/agent-contracts.md +27 -0
  33. package/plugins/copilot-pbr/references/checkpoints.md +32 -1
  34. package/plugins/copilot-pbr/references/context-quality-tiers.md +45 -0
  35. package/plugins/copilot-pbr/references/pbr-tools-cli.md +115 -0
  36. package/plugins/copilot-pbr/references/questioning.md +21 -1
  37. package/plugins/copilot-pbr/references/verification-patterns.md +96 -18
  38. package/plugins/copilot-pbr/skills/audit/SKILL.md +19 -3
  39. package/plugins/copilot-pbr/skills/begin/SKILL.md +57 -4
  40. package/plugins/copilot-pbr/skills/build/SKILL.md +39 -2
  41. package/plugins/copilot-pbr/skills/config/SKILL.md +12 -2
  42. package/plugins/copilot-pbr/skills/debug/SKILL.md +12 -1
  43. package/plugins/copilot-pbr/skills/explore/SKILL.md +13 -2
  44. package/plugins/copilot-pbr/skills/health/SKILL.md +13 -5
  45. package/plugins/copilot-pbr/skills/import/SKILL.md +26 -1
  46. package/plugins/copilot-pbr/skills/milestone/SKILL.md +15 -3
  47. package/plugins/copilot-pbr/skills/plan/SKILL.md +50 -0
  48. package/plugins/copilot-pbr/skills/quick/SKILL.md +21 -0
  49. package/plugins/copilot-pbr/skills/review/SKILL.md +45 -0
  50. package/plugins/copilot-pbr/skills/scan/SKILL.md +20 -0
  51. package/plugins/copilot-pbr/skills/setup/SKILL.md +9 -1
  52. package/plugins/copilot-pbr/skills/shared/context-budget.md +10 -0
  53. package/plugins/copilot-pbr/skills/shared/universal-anti-patterns.md +6 -0
  54. package/plugins/copilot-pbr/skills/test/SKILL.md +210 -0
  55. package/plugins/copilot-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  56. package/plugins/copilot-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  57. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  58. package/plugins/cursor-pbr/agents/audit.md +52 -5
  59. package/plugins/cursor-pbr/agents/codebase-mapper.md +49 -1
  60. package/plugins/cursor-pbr/agents/debugger.md +50 -1
  61. package/plugins/cursor-pbr/agents/dev-sync.md +23 -0
  62. package/plugins/cursor-pbr/agents/executor.md +153 -8
  63. package/plugins/cursor-pbr/agents/general.md +46 -1
  64. package/plugins/cursor-pbr/agents/integration-checker.md +54 -1
  65. package/plugins/cursor-pbr/agents/plan-checker.md +49 -1
  66. package/plugins/cursor-pbr/agents/planner.md +80 -1
  67. package/plugins/cursor-pbr/agents/researcher.md +49 -1
  68. package/plugins/cursor-pbr/agents/synthesizer.md +49 -1
  69. package/plugins/cursor-pbr/agents/verifier.md +113 -12
  70. package/plugins/cursor-pbr/commands/test.md +5 -0
  71. package/plugins/cursor-pbr/hooks/hooks.json +9 -0
  72. package/plugins/cursor-pbr/references/agent-contracts.md +27 -0
  73. package/plugins/cursor-pbr/references/checkpoints.md +32 -1
  74. package/plugins/cursor-pbr/references/context-quality-tiers.md +45 -0
  75. package/plugins/cursor-pbr/references/pbr-tools-cli.md +115 -0
  76. package/plugins/cursor-pbr/references/questioning.md +21 -1
  77. package/plugins/cursor-pbr/references/verification-patterns.md +96 -18
  78. package/plugins/cursor-pbr/skills/audit/SKILL.md +19 -3
  79. package/plugins/cursor-pbr/skills/begin/SKILL.md +57 -4
  80. package/plugins/cursor-pbr/skills/build/SKILL.md +37 -2
  81. package/plugins/cursor-pbr/skills/config/SKILL.md +12 -2
  82. package/plugins/cursor-pbr/skills/debug/SKILL.md +12 -1
  83. package/plugins/cursor-pbr/skills/explore/SKILL.md +13 -2
  84. package/plugins/cursor-pbr/skills/health/SKILL.md +14 -5
  85. package/plugins/cursor-pbr/skills/import/SKILL.md +26 -1
  86. package/plugins/cursor-pbr/skills/milestone/SKILL.md +15 -3
  87. package/plugins/cursor-pbr/skills/plan/SKILL.md +50 -0
  88. package/plugins/cursor-pbr/skills/quick/SKILL.md +21 -0
  89. package/plugins/cursor-pbr/skills/review/SKILL.md +45 -0
  90. package/plugins/cursor-pbr/skills/scan/SKILL.md +20 -0
  91. package/plugins/cursor-pbr/skills/setup/SKILL.md +9 -1
  92. package/plugins/cursor-pbr/skills/shared/context-budget.md +10 -0
  93. package/plugins/cursor-pbr/skills/shared/universal-anti-patterns.md +6 -0
  94. package/plugins/cursor-pbr/skills/test/SKILL.md +211 -0
  95. package/plugins/cursor-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  96. package/plugins/cursor-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  97. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  98. package/plugins/pbr/agents/audit.md +45 -0
  99. package/plugins/pbr/agents/codebase-mapper.md +48 -0
  100. package/plugins/pbr/agents/debugger.md +49 -0
  101. package/plugins/pbr/agents/dev-sync.md +23 -0
  102. package/plugins/pbr/agents/executor.md +151 -6
  103. package/plugins/pbr/agents/general.md +45 -0
  104. package/plugins/pbr/agents/integration-checker.md +53 -0
  105. package/plugins/pbr/agents/plan-checker.md +48 -0
  106. package/plugins/pbr/agents/planner.md +78 -1
  107. package/plugins/pbr/agents/researcher.md +48 -0
  108. package/plugins/pbr/agents/synthesizer.md +48 -0
  109. package/plugins/pbr/agents/verifier.md +112 -11
  110. package/plugins/pbr/commands/test.md +5 -0
  111. package/plugins/pbr/hooks/hooks.json +9 -0
  112. package/plugins/pbr/references/agent-contracts.md +27 -0
  113. package/plugins/pbr/references/checkpoints.md +32 -0
  114. package/plugins/pbr/references/context-quality-tiers.md +45 -0
  115. package/plugins/pbr/references/pbr-tools-cli.md +115 -0
  116. package/plugins/pbr/references/questioning.md +21 -0
  117. package/plugins/pbr/references/verification-patterns.md +96 -17
  118. package/plugins/pbr/scripts/check-plan-format.js +13 -1
  119. package/plugins/pbr/scripts/check-state-sync.js +26 -7
  120. package/plugins/pbr/scripts/check-subagent-output.js +30 -2
  121. package/plugins/pbr/scripts/config-schema.json +11 -1
  122. package/plugins/pbr/scripts/context-bridge.js +265 -0
  123. package/plugins/pbr/scripts/lib/config.js +271 -0
  124. package/plugins/pbr/scripts/lib/core.js +587 -0
  125. package/plugins/pbr/scripts/lib/history.js +73 -0
  126. package/plugins/pbr/scripts/lib/init.js +166 -0
  127. package/plugins/pbr/scripts/lib/migrate.js +169 -0
  128. package/plugins/pbr/scripts/lib/phase.js +364 -0
  129. package/plugins/pbr/scripts/lib/roadmap.js +175 -0
  130. package/plugins/pbr/scripts/lib/state.js +397 -0
  131. package/plugins/pbr/scripts/lib/todo.js +300 -0
  132. package/plugins/pbr/scripts/pbr-tools.js +425 -1310
  133. package/plugins/pbr/scripts/post-write-dispatch.js +5 -4
  134. package/plugins/pbr/scripts/pre-write-dispatch.js +1 -1
  135. package/plugins/pbr/scripts/progress-tracker.js +1 -1
  136. package/plugins/pbr/scripts/suggest-compact.js +1 -1
  137. package/plugins/pbr/scripts/track-context-budget.js +53 -2
  138. package/plugins/pbr/scripts/validate-task.js +20 -28
  139. package/plugins/pbr/skills/audit/SKILL.md +19 -3
  140. package/plugins/pbr/skills/begin/SKILL.md +48 -2
  141. package/plugins/pbr/skills/build/SKILL.md +39 -2
  142. package/plugins/pbr/skills/config/SKILL.md +12 -2
  143. package/plugins/pbr/skills/debug/SKILL.md +12 -1
  144. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +12 -1
  145. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +12 -5
  146. package/plugins/pbr/skills/explore/SKILL.md +13 -2
  147. package/plugins/pbr/skills/health/SKILL.md +14 -3
  148. package/plugins/pbr/skills/help/SKILL.md +2 -0
  149. package/plugins/pbr/skills/import/SKILL.md +26 -1
  150. package/plugins/pbr/skills/milestone/SKILL.md +15 -3
  151. package/plugins/pbr/skills/plan/SKILL.md +52 -2
  152. package/plugins/pbr/skills/quick/SKILL.md +21 -0
  153. package/plugins/pbr/skills/review/SKILL.md +46 -0
  154. package/plugins/pbr/skills/scan/SKILL.md +20 -0
  155. package/plugins/pbr/skills/setup/SKILL.md +9 -1
  156. package/plugins/pbr/skills/shared/context-budget.md +10 -0
  157. package/plugins/pbr/skills/shared/universal-anti-patterns.md +6 -0
  158. package/plugins/pbr/skills/test/SKILL.md +212 -0
  159. package/plugins/pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  160. package/plugins/pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
@@ -4,9 +4,9 @@ Reference patterns for deriving verification criteria from goals. Used by the pl
4
4
 
5
5
  ---
6
6
 
7
- ## The Three-Layer Check
7
+ ## The Four-Layer Check
8
8
 
9
- Every must-have is verified through three layers, checked in order:
9
+ Every must-have is verified through up to four layers, checked in order:
10
10
 
11
11
  ### Layer 1: Existence
12
12
 
@@ -62,6 +62,28 @@ grep -q "prisma" src/app.ts
62
62
  grep -q "DISCORD_CLIENT_ID" src/auth/discord.ts
63
63
  ```
64
64
 
65
+ ### Layer 4: Functional
66
+
67
+ Does the artifact actually work when executed?
68
+
69
+ ```bash
70
+ # Tests pass
71
+ npm test -- --testPathPattern auth
72
+ pytest tests/test_auth.py -v
73
+
74
+ # Build succeeds
75
+ npm run build
76
+ npx tsc --noEmit
77
+
78
+ # API returns correct data
79
+ curl -s http://localhost:3000/api/auth/login -X POST -d '{"code":"test"}' | jq '.token'
80
+
81
+ # CLI produces expected output
82
+ node src/cli.js --help | grep -q "Usage:"
83
+ ```
84
+
85
+ **When to apply L4:** Only when automated verification commands exist (test suites, build scripts, API endpoints with test data). Skip for items requiring manual/visual testing. L4 is optional — artifacts passing L1-L3 without available automated tests are reported as `PASSED (L3 only)`.
86
+
65
87
  ---
66
88
 
67
89
  ## Verification by Feature Type
@@ -69,41 +91,46 @@ grep -q "DISCORD_CLIENT_ID" src/auth/discord.ts
69
91
  ### API Endpoint
70
92
 
71
93
  ```
72
- Existence: curl returns non-404 status
73
- Substance: curl returns expected response shape (correct fields)
74
- Wiring: endpoint calls the right service, middleware is applied
94
+ Existence: curl returns non-404 status
95
+ Substance: curl returns expected response shape (correct fields)
96
+ Wiring: endpoint calls the right service, middleware is applied
97
+ Functional: POST/GET with test data returns correct response, error cases handled
75
98
  ```
76
99
 
77
100
  ### Database Schema
78
101
 
79
102
  ```
80
- Existence: table/collection exists, can query without error
81
- Substance: columns/fields match specification, constraints are applied
82
- Wiring: application code references the schema, migrations run cleanly
103
+ Existence: table/collection exists, can query without error
104
+ Substance: columns/fields match specification, constraints are applied
105
+ Wiring: application code references the schema, migrations run cleanly
106
+ Functional: CRUD operations work end-to-end, constraints reject invalid data
83
107
  ```
84
108
 
85
109
  ### Authentication
86
110
 
87
111
  ```
88
- Existence: auth routes exist, auth module exports functions
89
- Substance: login flow returns token, invalid creds return error
90
- Wiring: protected routes use auth middleware, tokens are validated
112
+ Existence: auth routes exist, auth module exports functions
113
+ Substance: login flow returns token, invalid creds return error
114
+ Wiring: protected routes use auth middleware, tokens are validated
115
+ Functional: auth tests pass (valid token, expired token, missing token, malformed token)
91
116
  ```
92
117
 
93
118
  ### UI Component
94
119
 
95
120
  ```
96
- Existence: component file exists, exports default component
97
- Substance: component renders expected elements (test or visual check)
98
- Wiring: component is imported in parent, receives correct props, routes to it
121
+ Existence: component file exists, exports default component
122
+ Substance: component renders expected elements (test or visual check)
123
+ Wiring: component is imported in parent, receives correct props, routes to it
124
+ Functional: component tests pass, build succeeds with component included
99
125
  ```
100
126
 
101
127
  ### Configuration
102
128
 
103
129
  ```
104
- Existence: config file exists, environment variables documented
105
- Substance: config values are used (not dead code), defaults are sensible
106
- Wiring: application reads config at startup, config changes take effect
130
+ Existence: config file exists, environment variables documented
131
+ Substance: config values are used (not dead code), defaults are sensible
132
+ Wiring: application reads config at startup, config changes take effect
133
+ Functional: app starts with config, missing config produces clear error message
107
134
  ```
108
135
 
109
136
  ---
@@ -196,3 +223,55 @@ Bad: "Tests pass"
196
223
  Good: "All 5 auth middleware tests pass: valid token, expired token,
197
224
  missing token, malformed token, and correct user extraction"
198
225
  ```
226
+
227
+ ---
228
+
229
+ ## Wiring Verification Patterns
230
+
231
+ 4 concrete patterns for verifying components are actually connected, not just present.
232
+
233
+ ### Pattern 1: Component to API
234
+ 1. Find the fetch/axios call in the component
235
+ 2. Verify the call is NOT commented out
236
+ 3. Verify the response is assigned to state (not discarded)
237
+ 4. Verify error handling exists (try/catch or .catch)
238
+
239
+ ### Pattern 2: API to Database
240
+ 1. Find the database query in the route handler
241
+ 2. Verify `await` is present (not fire-and-forget)
242
+ 3. Verify the result is returned in the response (not discarded)
243
+ 4. Verify error cases return appropriate HTTP status codes
244
+
245
+ ### Pattern 3: Form to Handler
246
+ 1. Find the form's onSubmit handler
247
+ 2. Verify it calls an API function (not just preventDefault)
248
+ 3. Verify form validation runs before the API call
249
+ 4. Verify success/error feedback is shown to the user
250
+
251
+ ### Pattern 4: State to Render
252
+ 1. Find state variables (useState, store, etc.)
253
+ 2. Verify they appear in JSX/template via .map(), interpolation, or conditional rendering
254
+ 3. Verify loading/error states are rendered (not just success state)
255
+ 4. Verify empty state is handled (not just "no data" crash)
256
+
257
+ ### Quick Verification Checklists
258
+
259
+ **Component Checklist (8 items):**
260
+ - [ ] Component file exists and exports correctly
261
+ - [ ] Props/types are defined (not `any`)
262
+ - [ ] API calls use actual endpoints (not hardcoded data)
263
+ - [ ] Loading state renders something meaningful
264
+ - [ ] Error state renders something meaningful
265
+ - [ ] Empty state renders something meaningful
266
+ - [ ] User interactions trigger actual handlers
267
+ - [ ] Component is imported and rendered in parent
268
+
269
+ **API Route Checklist (8 items):**
270
+ - [ ] Route file exists and exports handler
271
+ - [ ] Route is registered in router/app
272
+ - [ ] Request validation exists (body, params, query)
273
+ - [ ] Database query uses parameterized inputs
274
+ - [ ] Success response includes expected data shape
275
+ - [ ] Error response includes status code and message
276
+ - [ ] Authentication/authorization check exists if needed
277
+ - [ ] Response matches what the frontend expects
@@ -194,7 +194,7 @@ function validatePlan(content, _filePath) {
194
194
 
195
195
  // Skip checkpoint tasks - they have different required elements
196
196
  const taskTag = taskTags[index] || '';
197
- if (taskTag.includes('checkpoint')) {
197
+ if (/\btype\s*=\s*["']?checkpoint/i.test(taskTag) || /\bcheckpoint\s*[:=]/i.test(taskTag)) {
198
198
  return; // Checkpoint tasks have different structure
199
199
  }
200
200
 
@@ -205,6 +205,18 @@ function validatePlan(content, _filePath) {
205
205
  }
206
206
  });
207
207
 
208
+ // Path traversal check: ensure <files> elements don't escape project root
209
+ const filesTags = content.match(/<files>([\s\S]*?)<\/files>/g) || [];
210
+ for (const filesTag of filesTags) {
211
+ const filesContent = filesTag.replace(/<\/?files>/g, '');
212
+ const paths = filesContent.split(/[\n,]/).map(p => p.trim()).filter(Boolean);
213
+ for (const p of paths) {
214
+ if (p.includes('..') || path.isAbsolute(p.replace(/^[A-Za-z]:/, ''))) {
215
+ warnings.push(`Path traversal risk in <files>: "${p}" — use relative paths without ".." segments`);
216
+ }
217
+ }
218
+ }
219
+
208
220
  return { errors, warnings };
209
221
  }
210
222
 
@@ -26,7 +26,25 @@ const fs = require('fs');
26
26
  const path = require('path');
27
27
  const { logHook } = require('./hook-logger');
28
28
  const { logEvent } = require('./event-logger');
29
- const { lockedFileUpdate } = require('./pbr-tools');
29
+
30
+ /**
31
+ * Write content to a file atomically using write-then-rename.
32
+ * Writes to a PID-stamped temp file, then renames over the original.
33
+ * If the rename fails, cleans up the temp file and re-throws.
34
+ *
35
+ * @param {string} filePath - Target file path
36
+ * @param {string} content - Content to write
37
+ */
38
+ function atomicWriteFile(filePath, content) {
39
+ const tmpPath = filePath + '.tmp.' + process.pid;
40
+ try {
41
+ fs.writeFileSync(tmpPath, content, 'utf8');
42
+ fs.renameSync(tmpPath, filePath);
43
+ } catch (e) {
44
+ try { fs.unlinkSync(tmpPath); } catch (_) { /* best effort cleanup */ }
45
+ throw e;
46
+ }
47
+ }
30
48
 
31
49
  /**
32
50
  * Extract phase number from a phase directory name.
@@ -326,7 +344,7 @@ function checkStateSync(data) {
326
344
  return null;
327
345
  }
328
346
 
329
- const cwd = process.cwd();
347
+ const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
330
348
  const planningDir = path.join(cwd, '.planning');
331
349
  const roadmapPath = path.join(planningDir, 'ROADMAP.md');
332
350
  const statePath = path.join(planningDir, 'STATE.md');
@@ -370,7 +388,7 @@ function checkStateSync(data) {
370
388
  } else {
371
389
  const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, newStatus, completedDate);
372
390
  if (updatedRoadmap !== roadmapContent) {
373
- lockedFileUpdate(roadmapPath, () => updatedRoadmap);
391
+ atomicWriteFile(roadmapPath, updatedRoadmap);
374
392
  messages.push(`ROADMAP.md: Phase ${phaseNum} → ${plansComplete} plans, ${newStatus}`);
375
393
  }
376
394
  }
@@ -410,7 +428,7 @@ function checkStateSync(data) {
410
428
 
411
429
  const updatedState = updateStatePosition(stateContent, stateUpdates);
412
430
  if (updatedState !== stateContent) {
413
- lockedFileUpdate(statePath, () => updatedState);
431
+ atomicWriteFile(statePath, updatedState);
414
432
  messages.push(`STATE.md: ${artifacts.completeSummaries}/${artifacts.plans} plans, ${overallPct}%`);
415
433
  }
416
434
  } catch (e) {
@@ -455,7 +473,7 @@ function checkStateSync(data) {
455
473
  } else {
456
474
  const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, roadmapStatus, completedDate);
457
475
  if (updatedRoadmap !== roadmapContent) {
458
- lockedFileUpdate(roadmapPath, () => updatedRoadmap);
476
+ atomicWriteFile(roadmapPath, updatedRoadmap);
459
477
  messages.push(`ROADMAP.md: Phase ${phaseNum} → ${roadmapStatus}`);
460
478
  }
461
479
  }
@@ -493,7 +511,7 @@ function checkStateSync(data) {
493
511
 
494
512
  const updatedState = updateStatePosition(stateContent, stateUpdates);
495
513
  if (updatedState !== stateContent) {
496
- lockedFileUpdate(statePath, () => updatedState);
514
+ atomicWriteFile(statePath, updatedState);
497
515
  messages.push(`STATE.md: ${stateStatus}, ${overallPct}%`);
498
516
  }
499
517
  } catch (e) {
@@ -543,7 +561,7 @@ function checkStateSync(data) {
543
561
  const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
544
562
  const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, 'Planning', null);
545
563
  if (updatedRoadmap !== roadmapContent) {
546
- lockedFileUpdate(roadmapPath, () => updatedRoadmap);
564
+ atomicWriteFile(roadmapPath, updatedRoadmap);
547
565
  messages.push(`ROADMAP.md: Phase ${phaseNum} → Planning`);
548
566
  }
549
567
  }
@@ -588,6 +606,7 @@ function main() {
588
606
 
589
607
  if (require.main === module || process.argv[1] === __filename) { main(); }
590
608
  module.exports = {
609
+ atomicWriteFile,
591
610
  extractPhaseNum,
592
611
  countPhaseArtifacts,
593
612
  updateProgressTable,
@@ -20,6 +20,7 @@
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
22
  const { logHook } = require('./hook-logger');
23
+ const { KNOWN_AGENTS } = require('./pbr-tools');
23
24
  const { resolveConfig } = require('./local-llm/health');
24
25
  const { classifyError } = require('./local-llm/operations/classify-error');
25
26
 
@@ -27,7 +28,7 @@ const { classifyError } = require('./local-llm/operations/classify-error');
27
28
  * Check if a file was modified recently (within thresholdMs).
28
29
  * Returns false if file doesn't exist or on error.
29
30
  */
30
- function isRecent(filePath, thresholdMs = 300000) {
31
+ function isRecent(filePath, thresholdMs = 1800000) {
31
32
  try {
32
33
  const stat = fs.statSync(filePath);
33
34
  return (Date.now() - stat.mtimeMs) < thresholdMs;
@@ -151,6 +152,25 @@ const AGENT_OUTPUTS = {
151
152
  description: 'advisory output (no file expected)',
152
153
  noFileExpected: true,
153
154
  check: () => []
155
+ },
156
+ 'pbr:audit': {
157
+ description: 'audit report in .planning/audits/',
158
+ check: (planningDir) => {
159
+ const auditsDir = path.join(planningDir, 'audits');
160
+ if (!fs.existsSync(auditsDir)) return [];
161
+ try {
162
+ return fs.readdirSync(auditsDir)
163
+ .filter(f => f.endsWith('.md'))
164
+ .map(f => path.join('audits', f));
165
+ } catch (_e) {
166
+ return [];
167
+ }
168
+ }
169
+ },
170
+ 'pbr:dev-sync': {
171
+ description: 'advisory output (no file expected)',
172
+ noFileExpected: true,
173
+ check: () => []
154
174
  }
155
175
  };
156
176
 
@@ -324,7 +344,7 @@ function loadLocalLlmConfig(cwd) {
324
344
 
325
345
  async function main() {
326
346
  const data = readStdin();
327
- const cwd = process.cwd();
347
+ const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
328
348
  const planningDir = path.join(cwd, '.planning');
329
349
 
330
350
  // Only relevant for Plan-Build-Run projects
@@ -338,6 +358,14 @@ async function main() {
338
358
  // Only check known Plan-Build-Run agent types
339
359
  const outputSpec = AGENT_OUTPUTS[agentType];
340
360
  if (!outputSpec) {
361
+ // Log when agent is in KNOWN_AGENTS but missing from AGENT_OUTPUTS
362
+ const shortName = agentType.startsWith('pbr:') ? agentType.slice(4) : agentType;
363
+ if (KNOWN_AGENTS && KNOWN_AGENTS.includes && KNOWN_AGENTS.includes(shortName)) {
364
+ logHook('check-subagent-output', 'PostToolUse', 'missing-output-spec', {
365
+ agent_type: agentType,
366
+ message: `Agent ${agentType} is in KNOWN_AGENTS but has no AGENT_OUTPUTS entry. Add one to check-subagent-output.js.`
367
+ });
368
+ }
341
369
  process.exit(0);
342
370
  }
343
371
 
@@ -4,7 +4,8 @@
4
4
  "description": "Configuration schema for .planning/config.json",
5
5
  "type": "object",
6
6
  "properties": {
7
- "version": { "type": "integer", "enum": [1, 2] },
7
+ "version": { "type": ["integer", "string"], "enum": [1, 2, "1", "2"], "description": "Planning format version. v2 is current." },
8
+ "schema_version": { "type": "integer", "enum": [1], "description": "Config schema version for migration detection. Increment when config structure changes." },
8
9
  "context_strategy": { "type": "string", "enum": ["aggressive", "conservative", "balanced"] },
9
10
  "mode": { "type": "string", "enum": ["interactive", "autonomous"] },
10
11
  "depth": { "type": "string", "enum": ["quick", "standard", "comprehensive"], "description": "Workflow depth: quick = budget mode (fewer spawns, skip optional stages), standard = balanced mode (conditional spawns), comprehensive = thorough mode (all spawns, current default)" },
@@ -131,6 +132,15 @@
131
132
  },
132
133
  "additionalProperties": false
133
134
  },
135
+ "timeouts": {
136
+ "type": "object",
137
+ "properties": {
138
+ "task_default_ms": { "type": "integer", "minimum": 30000, "description": "Default timeout per task in milliseconds (default: 300000 = 5 min)" },
139
+ "build_max_ms": { "type": "integer", "minimum": 60000, "description": "Maximum time for entire build command in milliseconds" },
140
+ "verify_max_ms": { "type": "integer", "minimum": 30000, "description": "Maximum time for verification in milliseconds" }
141
+ },
142
+ "additionalProperties": false
143
+ },
134
144
  "hooks": {
135
145
  "type": "object",
136
146
  "properties": {
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse hook: Context monitor bridge.
5
+ *
6
+ * Replaces heuristic context budget estimation with real data when available.
7
+ * Writes context state to .planning/.context-budget.json for consumption by
8
+ * track-context-budget.js and suggest-compact.js.
9
+ *
10
+ * Context tiers:
11
+ * PEAK (0-30%) — no warnings
12
+ * GOOD (30-50%) — no warnings
13
+ * DEGRADING (50-70%) — suggest subagent delegation
14
+ * POOR (70-85%) — recommend /pbr:pause
15
+ * CRITICAL (85%+) — urgent stop, context rot imminent
16
+ *
17
+ * Debounce: same-tier warnings suppressed for 5 tool calls (2 for CRITICAL).
18
+ * Tier escalation always warns immediately.
19
+ *
20
+ * Exit codes:
21
+ * 0 = always (PostToolUse hook, advisory only)
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { logHook } = require('./hook-logger');
27
+
28
+ const TIERS = [
29
+ { name: 'PEAK', min: 0, max: 30 },
30
+ { name: 'GOOD', min: 30, max: 50 },
31
+ { name: 'DEGRADING', min: 50, max: 70 },
32
+ { name: 'POOR', min: 70, max: 85 },
33
+ { name: 'CRITICAL', min: 85, max: 100 }
34
+ ];
35
+
36
+ const TIER_MESSAGES = {
37
+ DEGRADING: 'Context at ~50-70%. Delegate heavy reads and analysis to Task() subagents to preserve orchestrator quality.',
38
+ POOR: 'Context at ~70-85%. Run /pbr:pause soon to save state before quality degrades.',
39
+ CRITICAL: 'STOP — Context at 85%+. Run /pbr:pause NOW. Context rot is imminent — further work risks hallucinations and skipped steps.'
40
+ };
41
+
42
+ const DEBOUNCE_INTERVAL = 5; // tool calls between same-tier warnings
43
+ const CRITICAL_DEBOUNCE_INTERVAL = 2; // shorter debounce for CRITICAL tier
44
+
45
+ /**
46
+ * Determine the context tier for a given percentage.
47
+ * @param {number} percent - Context usage percentage (0-100)
48
+ * @returns {{ name: string, min: number, max: number }}
49
+ */
50
+ function getTier(percent) {
51
+ for (const tier of TIERS) {
52
+ if (percent < tier.max) return tier;
53
+ }
54
+ return TIERS[TIERS.length - 1];
55
+ }
56
+
57
+ /**
58
+ * Load the bridge state file.
59
+ * @param {string} bridgePath - Absolute path to .context-budget.json
60
+ * @returns {Object} Bridge state
61
+ */
62
+ function loadBridge(bridgePath) {
63
+ try {
64
+ const content = fs.readFileSync(bridgePath, 'utf8');
65
+ return JSON.parse(content);
66
+ } catch (_e) {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Save the bridge state file atomically.
73
+ * @param {string} bridgePath - Absolute path to .context-budget.json
74
+ * @param {Object} data - Bridge state to persist
75
+ */
76
+ function saveBridge(bridgePath, data) {
77
+ try {
78
+ const tmpPath = bridgePath + '.' + process.pid;
79
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8');
80
+ fs.renameSync(tmpPath, bridgePath);
81
+ } catch (_e) {
82
+ // Best-effort — clean up temp file if rename failed
83
+ try { fs.unlinkSync(bridgePath + '.' + process.pid); } catch (_e2) { /* best-effort */ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Estimate context percentage from heuristic tracker data.
89
+ * Uses the .context-tracker file written by track-context-budget.js.
90
+ * Assumes 200k token context window (~800k chars).
91
+ *
92
+ * @param {string} planningDir - Path to .planning/
93
+ * @returns {number} Estimated context usage percentage (0-100)
94
+ */
95
+ function estimateFromHeuristic(planningDir) {
96
+ const trackerPath = path.join(planningDir, '.context-tracker');
97
+ try {
98
+ const content = fs.readFileSync(trackerPath, 'utf8');
99
+ const tracker = JSON.parse(content);
100
+ const totalChars = tracker.total_chars || 0;
101
+ // 200k tokens ~ 800k chars; use 800000 as denominator
102
+ const percent = Math.min(100, Math.round((totalChars / 800000) * 100));
103
+ return percent;
104
+ } catch (_e) {
105
+ return 0;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Check if a tier warning should fire, applying debounce logic.
111
+ *
112
+ * @param {Object} bridge - Current bridge state
113
+ * @param {string} tierName - Current tier name
114
+ * @returns {boolean} True if warning should fire
115
+ */
116
+ function shouldWarn(bridge, tierName) {
117
+ // No warning for PEAK or GOOD
118
+ if (tierName === 'PEAK' || tierName === 'GOOD') return false;
119
+
120
+ const prevTier = bridge.last_warned_tier || 'PEAK';
121
+ const callsSinceWarn = bridge.calls_since_warn || 0;
122
+
123
+ // Tier escalation — always warn
124
+ const tierOrder = { PEAK: 0, GOOD: 1, DEGRADING: 2, POOR: 3, CRITICAL: 4 };
125
+ if ((tierOrder[tierName] || 0) > (tierOrder[prevTier] || 0)) {
126
+ return true;
127
+ }
128
+
129
+ // Same tier — debounce (CRITICAL uses shorter interval)
130
+ const interval = tierName === 'CRITICAL' ? CRITICAL_DEBOUNCE_INTERVAL : DEBOUNCE_INTERVAL;
131
+ if (callsSinceWarn >= interval) {
132
+ return true;
133
+ }
134
+
135
+ return false;
136
+ }
137
+
138
+ /**
139
+ * Core bridge logic: update bridge state, return warning if applicable.
140
+ *
141
+ * @param {string} planningDir - Path to .planning/
142
+ * @param {Object} stdinData - Parsed stdin JSON from Claude Code
143
+ * @returns {{ bridge: Object, output: Object|null }} Updated bridge and optional warning output
144
+ */
145
+ function updateBridge(planningDir, stdinData) {
146
+ const bridgePath = path.join(planningDir, '.context-budget.json');
147
+
148
+ // Check if Claude Code provides real context percentage in stdin
149
+ // Look for context_percent, usage_percent, context_usage, or similar fields
150
+ const contextPercent = stdinData.context_percent
151
+ || stdinData.usage_percent
152
+ || (stdinData.context && stdinData.context.percent)
153
+ || null;
154
+
155
+ const source = contextPercent !== null ? 'bridge' : 'heuristic';
156
+ const estimatedPercent = contextPercent !== null
157
+ ? Math.round(contextPercent)
158
+ : estimateFromHeuristic(planningDir);
159
+
160
+ // Load existing bridge state
161
+ let bridge = loadBridge(bridgePath) || {
162
+ timestamp: new Date().toISOString(),
163
+ estimated_percent: 0,
164
+ source: 'heuristic',
165
+ chars_read: 0,
166
+ warnings_issued: [],
167
+ last_warned_tier: 'PEAK',
168
+ calls_since_warn: 0,
169
+ tool_calls: 0
170
+ };
171
+
172
+ // Update bridge
173
+ bridge.timestamp = new Date().toISOString();
174
+ bridge.estimated_percent = estimatedPercent;
175
+ bridge.source = source;
176
+ bridge.tool_calls = (bridge.tool_calls || 0) + 1;
177
+ bridge.calls_since_warn = (bridge.calls_since_warn || 0) + 1;
178
+
179
+ // Read chars from tracker if available
180
+ const trackerPath = path.join(planningDir, '.context-tracker');
181
+ try {
182
+ const tracker = JSON.parse(fs.readFileSync(trackerPath, 'utf8'));
183
+ bridge.chars_read = tracker.total_chars || 0;
184
+ } catch (_e) {
185
+ // Keep existing value
186
+ }
187
+
188
+ const tier = getTier(estimatedPercent);
189
+ let output = null;
190
+
191
+ if (shouldWarn(bridge, tier.name)) {
192
+ const msg = TIER_MESSAGES[tier.name];
193
+ if (msg) {
194
+ bridge.last_warned_tier = tier.name;
195
+ bridge.calls_since_warn = 0;
196
+ bridge.warnings_issued = bridge.warnings_issued || [];
197
+ bridge.warnings_issued.push({
198
+ tier: tier.name,
199
+ percent: estimatedPercent,
200
+ timestamp: new Date().toISOString()
201
+ });
202
+
203
+ // Keep only last 20 warnings
204
+ if (bridge.warnings_issued.length > 20) {
205
+ bridge.warnings_issued = bridge.warnings_issued.slice(-20);
206
+ }
207
+
208
+ output = {
209
+ additionalContext: `[Context Monitor — ${tier.name}] ${estimatedPercent}% used (${source}). ${msg}`
210
+ };
211
+ }
212
+ }
213
+
214
+ // Save bridge state
215
+ saveBridge(bridgePath, bridge);
216
+
217
+ return { bridge, output };
218
+ }
219
+
220
+ function main() {
221
+ let input = '';
222
+
223
+ process.stdin.setEncoding('utf8');
224
+ process.stdin.on('data', (chunk) => { input += chunk; });
225
+ process.stdin.on('end', () => {
226
+ try {
227
+ const cwd = process.cwd();
228
+ const planningDir = path.join(cwd, '.planning');
229
+ if (!fs.existsSync(planningDir)) {
230
+ process.exit(0);
231
+ }
232
+
233
+ const data = input ? JSON.parse(input) : {};
234
+ const { output } = updateBridge(planningDir, data);
235
+
236
+ if (output) {
237
+ logHook('context-bridge', 'PostToolUse', 'warn', {
238
+ percent: output.additionalContext.match(/(\d+)%/)?.[1],
239
+ source: 'bridge'
240
+ });
241
+ process.stdout.write(JSON.stringify(output));
242
+ }
243
+
244
+ process.exit(0);
245
+ } catch (_e) {
246
+ // Never block on tracking errors
247
+ process.exit(0);
248
+ }
249
+ });
250
+ }
251
+
252
+ module.exports = {
253
+ getTier,
254
+ loadBridge,
255
+ saveBridge,
256
+ estimateFromHeuristic,
257
+ shouldWarn,
258
+ updateBridge,
259
+ TIERS,
260
+ TIER_MESSAGES,
261
+ DEBOUNCE_INTERVAL,
262
+ CRITICAL_DEBOUNCE_INTERVAL
263
+ };
264
+
265
+ if (require.main === module || process.argv[1] === __filename) { main(); }