@sienklogic/plan-build-run 2.19.1 → 2.19.2

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 (116) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/CLAUDE.md +29 -16
  3. package/README.md +3 -3
  4. package/dashboard/server/index.js +10 -1
  5. package/dashboard/server/routes/agents.js +23 -2
  6. package/dashboard/server/routes/health.js +7 -4
  7. package/dashboard/server/routes/telemetry.js +20 -1
  8. package/dashboard/server/services/planning-reader.js +3 -17
  9. package/package.json +1 -1
  10. package/plan-build-run/bin/config-schema.json +23 -145
  11. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  12. package/plugins/pbr/agents/advisor-researcher.md +1 -0
  13. package/plugins/pbr/agents/debugger.md +0 -4
  14. package/plugins/pbr/agents/researcher.md +0 -4
  15. package/plugins/pbr/agents/synthesizer.md +0 -4
  16. package/plugins/pbr/dist/check-config-change.js +0 -7
  17. package/plugins/pbr/dist/check-cross-plugin-sync.js +1 -1
  18. package/plugins/pbr/dist/check-plan-format.js +0 -32
  19. package/plugins/pbr/dist/check-roadmap-sync.js +15 -11
  20. package/plugins/pbr/dist/check-subagent-output.js +4 -60
  21. package/plugins/pbr/dist/check-summary-gate.js +3 -14
  22. package/plugins/pbr/dist/feedback-loop.js +12 -29
  23. package/plugins/pbr/dist/hook-server.js +58 -6
  24. package/plugins/pbr/dist/milestone-learnings.js +6 -56
  25. package/plugins/pbr/dist/pbr-tools.js +8 -91
  26. package/plugins/pbr/dist/post-bash-triage.js +5 -63
  27. package/plugins/pbr/dist/post-hoc.js +3 -52
  28. package/plugins/pbr/dist/post-write-dispatch.js +0 -36
  29. package/plugins/pbr/dist/pre-bash-dispatch.js +1 -7
  30. package/plugins/pbr/dist/pre-task-dispatch.js +0 -28
  31. package/plugins/pbr/dist/progress-tracker.js +2 -27
  32. package/plugins/pbr/dist/session-cleanup.js +1 -31
  33. package/plugins/pbr/dist/status-line.js +13 -11
  34. package/plugins/pbr/dist/suggest-compact.js +2 -10
  35. package/plugins/pbr/dist/validate-commit.js +8 -64
  36. package/plugins/pbr/dist/validate-task.js +0 -30
  37. package/plugins/pbr/references/config-reference.md +0 -96
  38. package/plugins/pbr/scripts/audit-checks/si-agent-hook-config-checks.js +2 -72
  39. package/plugins/pbr/scripts/audit-checks/workflow-compliance.js +5 -41
  40. package/plugins/pbr/scripts/check-config-change.js +0 -7
  41. package/plugins/pbr/scripts/check-cross-plugin-sync.js +1 -1
  42. package/plugins/pbr/scripts/check-plan-format.js +0 -32
  43. package/plugins/pbr/scripts/check-roadmap-sync.js +15 -11
  44. package/plugins/pbr/scripts/check-subagent-output.js +4 -60
  45. package/plugins/pbr/scripts/check-summary-gate.js +3 -14
  46. package/plugins/pbr/scripts/config-schema.json +16 -129
  47. package/plugins/pbr/scripts/feedback-loop.js +12 -29
  48. package/plugins/pbr/scripts/hook-server.js +58 -6
  49. package/plugins/pbr/scripts/lib/config.js +4 -11
  50. package/plugins/pbr/scripts/lib/contextual-help.js +5 -29
  51. package/plugins/pbr/scripts/lib/format-validators.js +1 -26
  52. package/plugins/pbr/scripts/lib/frontmatter.js +4 -4
  53. package/plugins/pbr/scripts/lib/gates/rich-agent-context.js +13 -19
  54. package/plugins/pbr/scripts/lib/health.js +4 -5
  55. package/plugins/pbr/scripts/lib/help.js +3 -54
  56. package/plugins/pbr/scripts/lib/phase.js +2 -4
  57. package/plugins/pbr/scripts/lib/pre-commit-checks.js +1 -1
  58. package/plugins/pbr/scripts/lib/pre-research.js +10 -17
  59. package/plugins/pbr/scripts/lib/roadmap.js +11 -35
  60. package/plugins/pbr/scripts/lib/smart-next-task.js +11 -20
  61. package/plugins/pbr/scripts/lib/spot-check.js +3 -106
  62. package/plugins/pbr/scripts/lib/state.js +25 -130
  63. package/plugins/pbr/scripts/lib/verify.js +56 -46
  64. package/plugins/pbr/scripts/milestone-learnings.js +6 -56
  65. package/plugins/pbr/scripts/pbr-tools.js +8 -91
  66. package/plugins/pbr/scripts/post-bash-triage.js +5 -63
  67. package/plugins/pbr/scripts/post-hoc.js +3 -52
  68. package/plugins/pbr/scripts/post-write-dispatch.js +0 -36
  69. package/plugins/pbr/scripts/pre-bash-dispatch.js +1 -7
  70. package/plugins/pbr/scripts/pre-task-dispatch.js +0 -28
  71. package/plugins/pbr/scripts/progress-tracker.js +2 -27
  72. package/plugins/pbr/scripts/session-cleanup.js +1 -31
  73. package/plugins/pbr/scripts/status-line.js +13 -11
  74. package/plugins/pbr/scripts/suggest-compact.js +2 -10
  75. package/plugins/pbr/scripts/test/state.test.js +5 -13
  76. package/plugins/pbr/scripts/validate-commit.js +8 -64
  77. package/plugins/pbr/scripts/validate-task.js +0 -30
  78. package/plugins/pbr/skills/begin/SKILL.md +1 -0
  79. package/plugins/pbr/skills/begin/templates/config.json.tmpl +0 -4
  80. package/plugins/pbr/skills/build/SKILL.md +6 -6
  81. package/plugins/pbr/skills/config/SKILL.md +1 -0
  82. package/plugins/pbr/skills/help/SKILL.md +1 -0
  83. package/plugins/pbr/skills/pause/SKILL.md +1 -0
  84. package/plugins/pbr/skills/profile-user/SKILL.md +1 -0
  85. package/plugins/pbr/skills/quick/SKILL.md +2 -1
  86. package/plugins/pbr/skills/resume/SKILL.md +1 -0
  87. package/plugins/pbr/skills/scan/SKILL.md +1 -0
  88. package/plugins/pbr/skills/setup/SKILL.md +1 -0
  89. package/plugins/pbr/skills/shared/state-update.md +2 -2
  90. package/plugins/pbr/skills/status/SKILL.md +1 -0
  91. package/plugins/pbr/references/behavioral-contexts.md +0 -53
  92. package/plugins/pbr/scripts/lib/autonomy.js +0 -91
  93. package/plugins/pbr/scripts/lib/circuit-state.js +0 -133
  94. package/plugins/pbr/scripts/lib/completion.js +0 -377
  95. package/plugins/pbr/scripts/lib/hypothesis-runner.js +0 -127
  96. package/plugins/pbr/scripts/lib/local-llm/client.js +0 -237
  97. package/plugins/pbr/scripts/lib/local-llm/health.js +0 -12
  98. package/plugins/pbr/scripts/lib/local-llm/index.js +0 -89
  99. package/plugins/pbr/scripts/lib/local-llm/metrics.js +0 -20
  100. package/plugins/pbr/scripts/lib/local-llm/operations/classify-artifact.js +0 -4
  101. package/plugins/pbr/scripts/lib/local-llm/operations/classify-commit.js +0 -4
  102. package/plugins/pbr/scripts/lib/local-llm/operations/classify-error.js +0 -4
  103. package/plugins/pbr/scripts/lib/local-llm/operations/classify-file-intent.js +0 -4
  104. package/plugins/pbr/scripts/lib/local-llm/operations/score-source.js +0 -72
  105. package/plugins/pbr/scripts/lib/local-llm/operations/summarize-context.js +0 -62
  106. package/plugins/pbr/scripts/lib/local-llm/operations/triage-test-output.js +0 -12
  107. package/plugins/pbr/scripts/lib/local-llm/operations/validate-task.js +0 -4
  108. package/plugins/pbr/scripts/lib/local-llm/router.js +0 -101
  109. package/plugins/pbr/scripts/lib/local-llm/shadow.js +0 -60
  110. package/plugins/pbr/scripts/lib/local-llm/threshold-tuner.js +0 -118
  111. package/plugins/pbr/scripts/lib/team-composer.js +0 -87
  112. package/plugins/pbr/scripts/lib/team-coordinator.js +0 -153
  113. package/plugins/pbr/scripts/lib/template.js +0 -222
  114. package/plugins/pbr/scripts/lib/test-cache.js +0 -54
  115. package/plugins/pbr/scripts/lib/trust-gate.js +0 -84
  116. package/plugins/pbr/scripts/lib/wiring-check.js +0 -196
@@ -21,8 +21,6 @@ const fs = require('fs');
21
21
  const path = require('path');
22
22
  const { logHook } = require('./hook-logger');
23
23
  const { KNOWN_AGENTS, sessionLoad } = require('./pbr-tools');
24
- const { resolveConfig } = require('./lib/local-llm/health');
25
- const { classifyError } = require('./lib/local-llm/operations/classify-error');
26
24
  const { resolveSessionPath } = require('./lib/core');
27
25
  const { logEvent } = require('./event-logger');
28
26
  const { recordOutcome } = require('./trust-tracker');
@@ -102,15 +100,6 @@ function readStdin() {
102
100
  return {};
103
101
  }
104
102
 
105
- function loadLocalLlmConfig(cwd) {
106
- try {
107
- const configPath = path.join(cwd, '.planning', 'config.json');
108
- const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
109
- return resolveConfig(parsed.local_llm);
110
- } catch (_) {
111
- return resolveConfig(undefined);
112
- }
113
- }
114
103
 
115
104
  async function main() {
116
105
  const data = readStdin();
@@ -248,22 +237,8 @@ async function main() {
248
237
  agent_type: agentType,
249
238
  warnings: skillWarnings
250
239
  });
251
- // LLM error classification -- advisory enrichment
252
- let llmCategoryNote = '';
253
- try {
254
- const llmConfig = loadLocalLlmConfig(cwd);
255
- const errorText = (data.tool_output || '').substring(0, 500);
256
- if (errorText) {
257
- const llmResult = await classifyError(llmConfig, planningDir, errorText, agentType, data.session_id);
258
- if (llmResult && llmResult.category) {
259
- llmCategoryNote = `\nLLM error category: ${llmResult.category} (confidence: ${(llmResult.confidence * 100).toFixed(0)}%)`;
260
- }
261
- }
262
- } catch (_llmErr) {
263
- // Never propagate
264
- }
265
240
  const msg = `Warning: Agent ${agentType} completed but no ${outputSpec.description} was found.\nSkill-specific warnings:\n` +
266
- skillWarnings.map(w => `- ${w}`).join('\n') + llmCategoryNote;
241
+ skillWarnings.map(w => `- ${w}`).join('\n');
267
242
  process.stdout.write(JSON.stringify({ additionalContext: msg }));
268
243
  } else if (genericMissing) {
269
244
  logHook('check-subagent-output', 'PostToolUse', 'warning', {
@@ -271,22 +246,8 @@ async function main() {
271
246
  expected: outputSpec.description,
272
247
  found: 'none'
273
248
  });
274
- // LLM error classification -- advisory enrichment
275
- let llmCategoryNote = '';
276
- try {
277
- const llmConfig = loadLocalLlmConfig(cwd);
278
- const errorText = (data.tool_output || '').substring(0, 500);
279
- if (errorText) {
280
- const llmResult = await classifyError(llmConfig, planningDir, errorText, agentType, data.session_id);
281
- if (llmResult && llmResult.category) {
282
- llmCategoryNote = `\nLLM error category: ${llmResult.category} (confidence: ${(llmResult.confidence * 100).toFixed(0)}%)`;
283
- }
284
- }
285
- } catch (_llmErr) {
286
- // Never propagate
287
- }
288
249
  const output = {
289
- additionalContext: `[WARN] Agent ${agentType} completed but no ${outputSpec.description} was found. Likely causes: (1) agent hit an error mid-run, (2) wrong working directory. To fix: re-run the parent skill — the executor gate will block until the output is present. Check the Task() output above for error details.` + llmCategoryNote
250
+ additionalContext: `[WARN] Agent ${agentType} completed but no ${outputSpec.description} was found. Likely causes: (1) agent hit an error mid-run, (2) wrong working directory. To fix: re-run the parent skill — the executor gate will block until the output is present. Check the Task() output above for error details.`
290
251
  };
291
252
  process.stdout.write(JSON.stringify(output));
292
253
  } else if (skillWarnings.length > 0) {
@@ -412,32 +373,15 @@ async function handleHttp(reqBody) {
412
373
  }
413
374
  }
414
375
 
415
- // LLM classification helper (advisory, never throws)
416
- async function getLlmNote() {
417
- try {
418
- const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
419
- const llmConfig = loadLocalLlmConfig(cwd);
420
- const errorText = (data.tool_output || '').substring(0, 500);
421
- if (!errorText) return '';
422
- const llmResult = await classifyError(llmConfig, planningDir, errorText, agentType, data.session_id);
423
- if (llmResult && llmResult.category) {
424
- return `\nLLM error category: ${llmResult.category} (confidence: ${(llmResult.confidence * 100).toFixed(0)}%)`;
425
- }
426
- } catch (_e) { /* never propagate */ }
427
- return '';
428
- }
429
-
430
376
  if (genericMissing && skillWarnings.length > 0) {
431
377
  logHook('check-subagent-output', 'PostToolUse', 'skill-warning', { skill: activeSkill, agent_type: agentType, warnings: skillWarnings });
432
- const llmCategoryNote = await getLlmNote();
433
378
  const msg = `Warning: Agent ${agentType} completed but no ${outputSpec.description} was found.\nSkill-specific warnings:\n` +
434
- skillWarnings.map(w => `- ${w}`).join('\n') + llmCategoryNote;
379
+ skillWarnings.map(w => `- ${w}`).join('\n');
435
380
  return { additionalContext: msg };
436
381
  } else if (genericMissing) {
437
382
  logHook('check-subagent-output', 'PostToolUse', 'warning', { agent_type: agentType, expected: outputSpec.description, found: 'none' });
438
- const llmCategoryNote = await getLlmNote();
439
383
  return {
440
- additionalContext: `[WARN] Agent ${agentType} completed but no ${outputSpec.description} was found. Likely causes: (1) agent hit an error mid-run, (2) wrong working directory. To fix: re-run the parent skill — the executor gate will block until the output is present. Check the Task() output above for error details.` + llmCategoryNote
384
+ additionalContext: `[WARN] Agent ${agentType} completed but no ${outputSpec.description} was found. Likely causes: (1) agent hit an error mid-run, (2) wrong working directory. To fix: re-run the parent skill — the executor gate will block until the output is present. Check the Task() output above for error details.`
441
385
  };
442
386
  } else if (skillWarnings.length > 0) {
443
387
  logHook('check-subagent-output', 'PostToolUse', 'skill-warning', { skill: activeSkill, agent_type: agentType, warnings: skillWarnings });
@@ -27,24 +27,13 @@
27
27
  const fs = require('fs');
28
28
  const path = require('path');
29
29
  const { logHook } = require('./hook-logger');
30
+ const { extractFrontmatter } = require('./lib/frontmatter');
30
31
 
31
32
  // Statuses that indicate a phase has been executed
32
33
  const ADVANCED_STATUSES = ['built', 'verified', 'complete'];
33
34
 
34
- /**
35
- * Extract YAML frontmatter values from markdown content.
36
- * Returns an object with parsed key-value pairs.
37
- */
38
- function parseFrontmatter(content) {
39
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
40
- if (!match) return {};
41
- const result = {};
42
- for (const line of match[1].split(/\r?\n/)) {
43
- const kv = line.match(/^(\w[\w_]*):\s*"?([^"\r\n]*)"?$/);
44
- if (kv) result[kv[1]] = kv[2].trim();
45
- }
46
- return result;
47
- }
35
+ // Re-export extractFrontmatter as parseFrontmatter for backward compat (tests import it)
36
+ const parseFrontmatter = extractFrontmatter;
48
37
 
49
38
  /**
50
39
  * Check if a SUMMARY file exists for the given phase directory.
@@ -288,7 +288,7 @@
288
288
  },
289
289
  "reverse_spec": {
290
290
  "type": "boolean",
291
- "default": true,
291
+ "default": false,
292
292
  "description": "Enable reverse spec generation from existing code."
293
293
  },
294
294
  "predictive_impact": {
@@ -330,6 +330,21 @@
330
330
  "type": "boolean",
331
331
  "default": true,
332
332
  "description": "Enable architecture consistency guard that warns on dependency violations."
333
+ },
334
+ "cross_project_patterns": {
335
+ "type": "boolean",
336
+ "default": false,
337
+ "description": "Enable cross-project pattern detection and reuse across repositories."
338
+ },
339
+ "spec_templates": {
340
+ "type": "boolean",
341
+ "default": false,
342
+ "description": "Enable spec template generation from existing code patterns."
343
+ },
344
+ "global_learnings": {
345
+ "type": "boolean",
346
+ "default": false,
347
+ "description": "Enable global learnings aggregation across phases into KNOWLEDGE.md."
333
348
  }
334
349
  },
335
350
  "additionalProperties": false
@@ -1077,134 +1092,6 @@
1077
1092
  },
1078
1093
  "additionalProperties": false
1079
1094
  },
1080
- "local_llm": {
1081
- "type": "object",
1082
- "deprecated": true,
1083
- "description": "DEPRECATED. Local LLM infrastructure removed in v14.0. Key retained for backward compatibility.",
1084
- "properties": {
1085
- "enabled": {
1086
- "type": "boolean"
1087
- },
1088
- "provider": {
1089
- "type": "string",
1090
- "enum": [
1091
- "ollama"
1092
- ]
1093
- },
1094
- "endpoint": {
1095
- "type": "string",
1096
- "format": "uri"
1097
- },
1098
- "model": {
1099
- "type": "string"
1100
- },
1101
- "timeout_ms": {
1102
- "type": "integer",
1103
- "minimum": 500
1104
- },
1105
- "max_retries": {
1106
- "type": "integer",
1107
- "minimum": 0,
1108
- "maximum": 3
1109
- },
1110
- "fallback": {
1111
- "type": "string",
1112
- "enum": [
1113
- "frontier",
1114
- "skip"
1115
- ]
1116
- },
1117
- "routing_strategy": {
1118
- "type": "string",
1119
- "enum": [
1120
- "local_first",
1121
- "frontier_first"
1122
- ]
1123
- },
1124
- "features": {
1125
- "type": "object",
1126
- "properties": {
1127
- "artifact_classification": {
1128
- "type": "boolean"
1129
- },
1130
- "task_validation": {
1131
- "type": "boolean"
1132
- },
1133
- "plan_adequacy": {
1134
- "type": "boolean"
1135
- },
1136
- "gap_detection": {
1137
- "type": "boolean"
1138
- },
1139
- "context_summarization": {
1140
- "type": "boolean"
1141
- },
1142
- "source_scoring": {
1143
- "type": "boolean"
1144
- },
1145
- "commit_classification": {
1146
- "type": "boolean"
1147
- },
1148
- "test_triage": {
1149
- "type": "boolean"
1150
- },
1151
- "file_intent_classification": {
1152
- "type": "boolean"
1153
- }
1154
- },
1155
- "additionalProperties": false
1156
- },
1157
- "metrics": {
1158
- "type": "object",
1159
- "properties": {
1160
- "enabled": {
1161
- "type": "boolean"
1162
- },
1163
- "log_file": {
1164
- "type": "string"
1165
- },
1166
- "show_session_summary": {
1167
- "type": "boolean"
1168
- },
1169
- "frontier_token_rate": {
1170
- "type": "number",
1171
- "minimum": 0
1172
- }
1173
- },
1174
- "additionalProperties": false
1175
- },
1176
- "advanced": {
1177
- "type": "object",
1178
- "properties": {
1179
- "confidence_threshold": {
1180
- "type": "number",
1181
- "minimum": 0,
1182
- "maximum": 1
1183
- },
1184
- "max_input_tokens": {
1185
- "type": "integer",
1186
- "minimum": 100
1187
- },
1188
- "keep_alive": {
1189
- "type": "string"
1190
- },
1191
- "num_ctx": {
1192
- "type": "integer",
1193
- "minimum": 512
1194
- },
1195
- "disable_after_failures": {
1196
- "type": "integer",
1197
- "minimum": 1
1198
- },
1199
- "shadow_mode": {
1200
- "type": "boolean"
1201
- }
1202
- },
1203
- "additionalProperties": false
1204
- }
1205
- },
1206
- "additionalProperties": false
1207
- },
1208
1095
  "intel": {
1209
1096
  "type": "object",
1210
1097
  "properties": {
@@ -20,36 +20,19 @@
20
20
 
21
21
  const fs = require('fs');
22
22
  const path = require('path');
23
+ const { extractFrontmatter } = require('./lib/frontmatter');
23
24
 
24
25
  /**
25
- * Parse YAML-ish frontmatter from markdown content.
26
- * Lightweight parser no dependency on pbr-tools to avoid circular deps.
26
+ * Parse frontmatter and body from markdown content.
27
+ * Delegates YAML parsing to canonical extractFrontmatter.
27
28
  * @param {string} content - Full file content
28
29
  * @returns {{ frontmatter: object, body: string }}
29
30
  */
30
- function parseFrontmatter(content) {
31
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
32
- if (!match) return { frontmatter: {}, body: content };
33
-
34
- const fm = {};
35
- const lines = match[1].split(/\r?\n/);
36
- for (const line of lines) {
37
- const kvMatch = line.match(/^(\w[\w_]*):\s*(.+)/);
38
- if (kvMatch) {
39
- const key = kvMatch[1];
40
- let val = kvMatch[2].trim();
41
- // Parse numbers
42
- if (/^\d+$/.test(val)) val = parseInt(val, 10);
43
- // Strip quotes
44
- if (typeof val === 'string' && val.startsWith('"') && val.endsWith('"')) {
45
- val = val.slice(1, -1);
46
- }
47
- fm[key] = val;
48
- }
49
- }
50
-
51
- const body = content.slice(match[0].length).trim();
52
- return { frontmatter: fm, body };
31
+ function extractFrontmatterWithBody(content) {
32
+ const frontmatter = extractFrontmatter(content);
33
+ const match = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
34
+ const body = match ? content.slice(match[0].length).trim() : content;
35
+ return { frontmatter, body };
53
36
  }
54
37
 
55
38
  /**
@@ -69,7 +52,7 @@ function extractFeedback(phaseDir) {
69
52
  return null;
70
53
  }
71
54
 
72
- const { frontmatter, body } = parseFrontmatter(content);
55
+ const { frontmatter, body } = extractFrontmatterWithBody(content);
73
56
 
74
57
  const status = frontmatter.status;
75
58
  if (!status) return null;
@@ -77,9 +60,9 @@ function extractFeedback(phaseDir) {
77
60
  // No feedback needed if verification passed
78
61
  if (status === 'passed' || status === 'all_passed') return null;
79
62
 
80
- const attempt = typeof frontmatter.attempt === 'number' ? frontmatter.attempt : 1;
81
- const passed = typeof frontmatter.must_haves_passed === 'number' ? frontmatter.must_haves_passed : 0;
82
- const total = typeof frontmatter.must_haves_total === 'number' ? frontmatter.must_haves_total : 1;
63
+ const attempt = frontmatter.attempt ? parseInt(frontmatter.attempt, 10) || 1 : 1;
64
+ const passed = frontmatter.must_haves_passed ? parseInt(frontmatter.must_haves_passed, 10) || 0 : 0;
65
+ const total = frontmatter.must_haves_total ? parseInt(frontmatter.must_haves_total, 10) || 1 : 1;
83
66
  const pass_rate = total > 0 ? passed / total : 0;
84
67
 
85
68
  // Parse gap sections from body
@@ -89,15 +89,23 @@ function readEventLogTail(logFile, maxLines) {
89
89
  if (maxLines === undefined) maxLines = 500;
90
90
  try {
91
91
  if (!fs.existsSync(logFile)) return [];
92
- const content = fs.readFileSync(logFile, 'utf8');
93
- const lines = content.split('\n').filter(l => l.trim().length > 0);
92
+ const stat = fs.statSync(logFile);
93
+ if (stat.size === 0) return [];
94
+ // Read only the last chunk instead of the entire file
95
+ const CHUNK_SIZE = 64 * 1024; // 64KB should hold 500+ lines
96
+ const start = Math.max(0, stat.size - CHUNK_SIZE);
97
+ const fd = fs.openSync(logFile, 'r');
98
+ const buffer = Buffer.alloc(Math.min(CHUNK_SIZE, stat.size));
99
+ fs.readSync(fd, buffer, 0, buffer.length, start);
100
+ fs.closeSync(fd);
101
+ const lines = buffer.toString('utf8').split('\n').filter(l => l.trim().length > 0);
94
102
  const tail = lines.slice(-maxLines);
95
103
  const events = [];
96
104
  for (const line of tail) {
97
105
  try {
98
106
  events.push(JSON.parse(line));
99
107
  } catch (_e) {
100
- // Skip malformed lines
108
+ // Skip malformed lines (including partial first line from mid-file read)
101
109
  }
102
110
  }
103
111
  return events;
@@ -106,6 +114,23 @@ function readEventLogTail(logFile, maxLines) {
106
114
  }
107
115
  }
108
116
 
117
+ /**
118
+ * Rotate the event log if it exceeds 2MB — truncate to last 1000 lines.
119
+ * Called once at server startup to prevent unbounded log growth.
120
+ */
121
+ function rotateEventLog(planningDir) {
122
+ if (!planningDir) return;
123
+ const logFile = path.join(planningDir, '.hook-events.jsonl');
124
+ try {
125
+ const stat = fs.statSync(logFile);
126
+ if (stat.size > 2 * 1024 * 1024) { // 2MB
127
+ const content = fs.readFileSync(logFile, 'utf8');
128
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
129
+ fs.writeFileSync(logFile, lines.slice(-1000).join('\n') + '\n');
130
+ }
131
+ } catch (_e) { /* best-effort — file may not exist yet */ }
132
+ }
133
+
109
134
  /** Append a JSON event object as a single line to .planning/.hook-events.jsonl */
110
135
  function appendEvent(planningDir, eventObj) {
111
136
  if (!planningDir) return;
@@ -176,6 +201,30 @@ function mergeContext(...fns) {
176
201
  };
177
202
  }
178
203
 
204
+ // ---------------------------------------------------------------------------
205
+ // PreToolUse response translation
206
+ // ---------------------------------------------------------------------------
207
+
208
+ /**
209
+ * Translate PreToolUse block responses into Claude Code's hookSpecificOutput format.
210
+ * Handlers return { decision: 'block', reason: '...' } but Claude Code expects
211
+ * { hookSpecificOutput: { hookEventName, permissionDecision, permissionDecisionReason } }.
212
+ */
213
+ function translatePreToolUseResponse(event, result) {
214
+ if (!result || event !== 'PreToolUse') return result;
215
+ if (result.decision === 'block' && result.reason) {
216
+ return {
217
+ hookSpecificOutput: {
218
+ hookEventName: 'PreToolUse',
219
+ permissionDecision: 'deny',
220
+ permissionDecisionReason: result.reason
221
+ }
222
+ };
223
+ }
224
+ // Allow decisions or non-blocking results pass through unchanged
225
+ return result;
226
+ }
227
+
179
228
  // ---------------------------------------------------------------------------
180
229
  // Handler routing table (dynamic Map populated by register/initRoutes)
181
230
  // ---------------------------------------------------------------------------
@@ -377,7 +426,7 @@ function createServer(planningDir) {
377
426
  }
378
427
  const duration_ms = Date.now() - dispatchStart;
379
428
  appendEvent(planningDir, { ts: new Date().toISOString(), type: 'dispatch', event, tool, hook: `${event}:${tool}`, duration_ms, transport: 'http' });
380
- let finalResult = dispatchResult || {};
429
+ let finalResult = translatePreToolUseResponse(event, dispatchResult) || {};
381
430
  if (duration_ms >= 100) {
382
431
  const alertMsg = `HOOK PERFORMANCE ALERT: ${event}:${tool} took ${duration_ms}ms (threshold: 100ms). Check handler for blocking I/O.`;
383
432
  if (finalResult.additionalContext) {
@@ -431,7 +480,7 @@ function createServer(planningDir) {
431
480
  }
432
481
  const legacyDurationMs = Date.now() - legacyDispatchStart;
433
482
  appendEvent(planningDir, { ts: new Date().toISOString(), type: 'dispatch', event, tool, hook: `${event}:${tool}`, duration_ms: legacyDurationMs, transport: 'http' });
434
- let legacyFinalResult = legacyDispatchResult || {};
483
+ let legacyFinalResult = translatePreToolUseResponse(event, legacyDispatchResult) || {};
435
484
  if (legacyDurationMs >= 100) {
436
485
  const alertMsg = `HOOK PERFORMANCE ALERT: ${event}:${tool} took ${legacyDurationMs}ms (threshold: 100ms). Check handler for blocking I/O.`;
437
486
  if (legacyFinalResult.additionalContext) {
@@ -573,6 +622,9 @@ function main() {
573
622
  cache.config = configLoad(planningDir);
574
623
  } catch (_e) { /* best-effort */ }
575
624
 
625
+ // Rotate event log if oversized (before any new events are appended)
626
+ rotateEventLog(planningDir);
627
+
576
628
  initRoutes();
577
629
 
578
630
  const server = createServer(planningDir);
@@ -601,6 +653,6 @@ function main() {
601
653
  process.on('SIGINT', shutdown);
602
654
  }
603
655
 
604
- module.exports = { createServer, appendEvent, readEventLogTail, mergeContext, lazyHandler, resolveHandler, register, initRoutes, triggerShutdown, tryNextPort, normalizeMsysPath, DEFAULT_PORT };
656
+ module.exports = { createServer, appendEvent, readEventLogTail, rotateEventLog, mergeContext, lazyHandler, resolveHandler, register, initRoutes, triggerShutdown, tryNextPort, normalizeMsysPath, translatePreToolUseResponse, DEFAULT_PORT };
605
657
 
606
658
  if (require.main === module || process.argv[1] === __filename) { main(); }
@@ -116,11 +116,6 @@ function configValidate(configOrDir, planningDir) {
116
116
  warnings.push(`config.json schema_version (${config.schema_version}) is behind current (${CURRENT_SCHEMA_VERSION}) — run "pbr-tools migrate" to update`);
117
117
  }
118
118
 
119
- // DEPRECATED: local_llm infrastructure removed in v14.0
120
- if (config.local_llm && config.local_llm.enabled === true) {
121
- warnings.push('local_llm feature is deprecated and has no effect. Set enabled: false to suppress this warning.');
122
- }
123
-
124
119
  // Semantic conflict detection
125
120
  if (config.mode === 'autonomous' && config.gates) {
126
121
  const activeGates = Object.entries(config.gates || {}).filter(([, v]) => v === true).map(([k]) => k);
@@ -290,7 +285,7 @@ function configLoadDefaults(planningDir) {
290
285
  // No config found — return hardcoded defaults
291
286
  return {
292
287
  version: 2,
293
- schema_version: 1,
288
+ schema_version: 3,
294
289
  mode: 'interactive',
295
290
  depth: 'standard',
296
291
  features: {
@@ -300,14 +295,14 @@ function configLoadDefaults(planningDir) {
300
295
  plan_checking: true,
301
296
  },
302
297
  planning: {
303
- commit_docs: true,
298
+ commit_docs: false,
304
299
  search_gitignored: false,
305
300
  },
306
301
  git: {
307
302
  branching: 'none',
308
303
  },
309
304
  parallelization: {
310
- enabled: true,
305
+ enabled: false,
311
306
  },
312
307
  };
313
308
  }
@@ -903,7 +898,6 @@ const CONFIG_DEFAULTS = {
903
898
  max_phases_in_context: 3
904
899
  },
905
900
  hook_server: { enabled: false, port: 19836, event_log: true },
906
- local_llm: { enabled: false }, // DEPRECATED: local_llm infrastructure removed in v14.0. Key retained for backward compat.
907
901
  intel: { enabled: false, auto_update: false, inject_on_start: false },
908
902
  context_ledger: { enabled: false, stale_after_minutes: 60 },
909
903
  learnings: { enabled: false, read_depth: 3, cross_project_knowledge: false },
@@ -1153,7 +1147,6 @@ const CONFIG_SECTIONS = [
1153
1147
  'debug.max_hypothesis_rounds: 1-20 — max hypothesis cycles for /pbr:debug',
1154
1148
  'depth_profiles: override built-in quick/standard/comprehensive defaults',
1155
1149
  'developer_profile: behavioral profiling from session history + prompt injection',
1156
- '(DEPRECATED) local_llm: offload classification tasks to local Ollama instance — removed in v14.0',
1157
1150
  'prd.auto_extract: skip confirmation gate during PRD import',
1158
1151
  'spinner_tips: custom messages shown during agent execution',
1159
1152
  'status_line: status bar appearance (sections, branding, context bar)',
@@ -1161,7 +1154,7 @@ const CONFIG_SECTIONS = [
1161
1154
  'ui.enabled: enable UI design pipeline (/pbr:ui-phase, /pbr:ui-review)',
1162
1155
  'worktree.sparse_paths: glob patterns for sparse checkout in agent worktrees'
1163
1156
  ],
1164
- keys: ['dashboard', 'debug', 'depth_profiles', 'developer_profile', 'local_llm', 'prd', 'spinner_tips', 'status_line', 'timeouts', 'ui', 'worktree']
1157
+ keys: ['dashboard', 'debug', 'depth_profiles', 'developer_profile', 'prd', 'spinner_tips', 'status_line', 'timeouts', 'ui', 'worktree']
1165
1158
  }
1166
1159
  ];
1167
1160
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const { extractFrontmatter } = require('./frontmatter');
15
16
 
16
17
  // ─── Help templates ────────────────────────────────────────────────────────────
17
18
 
@@ -79,39 +80,14 @@ const HELP_TEMPLATES = {
79
80
  * @param {string} statePath - Path to STATE.md
80
81
  * @returns {{ status: string, blockers: string[] }}
81
82
  */
82
- function parseStateFrontmatter(statePath) {
83
+ const parseStateFrontmatter = (statePath) => {
83
84
  const defaults = { status: 'unknown', blockers: [] };
84
85
  try {
85
86
  if (!fs.existsSync(statePath)) return defaults;
86
87
  const content = fs.readFileSync(statePath, 'utf8');
87
- // Extract frontmatter between --- delimiters
88
- const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
89
- if (!fmMatch) return defaults;
90
- const fm = fmMatch[1];
91
-
92
- // Parse status
93
- const statusMatch = fm.match(/^status:\s*["']?([^"'\r\n]+)["']?/m);
94
- const status = statusMatch ? statusMatch[1].trim() : 'unknown';
95
-
96
- // Parse blockers array
97
- const blockers = [];
98
- const blockersMatch = fm.match(/^blockers:\s*\[(.*?)\]/ms);
99
- if (blockersMatch) {
100
- // Inline array format: blockers: ["item1", "item2"]
101
- const items = blockersMatch[1].match(/"([^"]+)"/g) || [];
102
- blockers.push(...items.map(i => i.replace(/"/g, '')));
103
- } else {
104
- // Multi-line array format
105
- const blockersSection = fm.match(/^blockers:\s*\r?\n((?:\s+- .*\r?\n?)*)/m);
106
- if (blockersSection) {
107
- const lines = blockersSection[1].split(/\r?\n/);
108
- for (const line of lines) {
109
- const item = line.match(/^\s+- ["']?(.+?)["']?\s*$/);
110
- if (item) blockers.push(item[1]);
111
- }
112
- }
113
- }
114
-
88
+ const fm = extractFrontmatter(content);
89
+ const status = fm.status || 'unknown';
90
+ const blockers = Array.isArray(fm.blockers) ? fm.blockers : [];
115
91
  return { status, blockers };
116
92
  } catch (_e) {
117
93
  return defaults;
@@ -372,31 +372,6 @@ async function checkPlanWrite(data) {
372
372
  ? validateContext(content, filePath)
373
373
  : validateSummary(content, filePath);
374
374
 
375
- // LLM advisory enrichment -- advisory only, never blocks
376
- if ((isPlan || isSummary) && result.errors.length === 0) {
377
- try {
378
- const { resolveConfig } = require('../local-llm/health');
379
- const { classifyArtifact } = require('../local-llm/operations/classify-artifact');
380
- let llmConfig;
381
- try {
382
- const configPath = path.join(process.cwd(), '.planning', 'config.json');
383
- const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
384
- llmConfig = resolveConfig(parsed.local_llm);
385
- } catch (_e) {
386
- llmConfig = resolveConfig(undefined);
387
- }
388
- const planningDir = path.join(process.cwd(), '.planning');
389
- const fileType = isPlan ? 'PLAN' : 'SUMMARY';
390
- const llmResult = await classifyArtifact(llmConfig, planningDir, content, fileType, data.session_id);
391
- if (llmResult && llmResult.classification) {
392
- const llmNote = `Local LLM: ${fileType} classified as "${llmResult.classification}" (confidence: ${(llmResult.confidence * 100).toFixed(0)}%)${llmResult.reason ? ' — ' + llmResult.reason : ''}`;
393
- result.warnings.push(llmNote);
394
- }
395
- } catch (_llmErr) {
396
- // Never propagate LLM errors
397
- }
398
- }
399
-
400
375
  const eventType = isPlan ? 'plan-validated' : isVerification ? 'verification-validated' : isRoadmap ? 'roadmap-validated' : isLearnings ? 'learnings-validated' : isConfig ? 'config-validated' : isResearch ? 'research-validated' : isContext ? 'context-validated' : 'summary-validated';
401
376
 
402
377
  if (result.errors.length > 0) {
@@ -934,7 +909,7 @@ function validateConfig(content, _filePath) {
934
909
  }
935
910
  }
936
911
 
937
- const knownKeys = ['version', 'schema_version', 'context_strategy', 'mode', 'depth', 'session_phase_limit', 'session_cycling', 'context_window_tokens', 'agent_checkpoint_pct', 'features', 'validation_passes', 'autonomy', 'models', 'model_profiles', 'parallelization', 'teams', 'planning', 'git', 'gates', 'safety', 'timeouts', 'hooks', 'prd', 'depth_profiles', 'debug', 'developer_profile', 'spinner_tips', 'dashboard', 'status_line', 'workflow', 'hook_server', 'local_llm', 'intel', 'context_ledger', 'learnings', 'verification', 'context_budget', 'ui', 'worktree', 'ceremony_level', 'skip_rag_max_lines', 'orchestrator_budget_pct'];
912
+ const knownKeys = ['version', 'schema_version', 'context_strategy', 'mode', 'depth', 'session_phase_limit', 'session_cycling', 'context_window_tokens', 'agent_checkpoint_pct', 'features', 'autonomy', 'models', 'model_profiles', 'parallelization', 'teams', 'planning', 'git', 'gates', 'safety', 'timeouts', 'hooks', 'prd', 'depth_profiles', 'debug', 'developer_profile', 'spinner_tips', 'dashboard', 'status_line', 'workflow', 'hook_server', 'intel', 'context_ledger', 'learnings', 'verification', 'context_budget', 'ui', 'worktree', 'ceremony_level', 'skip_rag_max_lines', 'orchestrator_budget_pct'];
938
913
  for (const key of Object.keys(parsed)) {
939
914
  if (!knownKeys.includes(key)) {
940
915
  warnings.push(`Unknown top-level key: "${key}" (known: ${knownKeys.join(', ')})`);