@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
@@ -9,7 +9,7 @@
9
9
  * fresh (<60s old). CRITICAL tier always emits; others use REMINDER_INTERVAL debounce.
10
10
  *
11
11
  * Fallback: when bridge is absent or stale, uses call-count threshold.
12
- * Counter stored in .planning/.compact-counter (JSON).
12
+ * Counter stored in .planning/.compact-counter (JSON) and .session.json.
13
13
  * Threshold configurable via config.json hooks.compactThreshold (default: 50).
14
14
  * Counter resets on SessionStart (via progress-tracker.js).
15
15
  *
@@ -281,15 +281,7 @@ function resetCounter(planningDir, sessionId) {
281
281
  sessionSave(planningDir, { compactCounter: { count: 0, lastSuggested: 0 } }, sessionId);
282
282
  } catch (_e) { /* best-effort */ }
283
283
 
284
- // Legacy: also delete .compact-counter file if present
285
- const counterPath = path.join(planningDir, '.compact-counter');
286
- try {
287
- if (fs.existsSync(counterPath)) {
288
- fs.unlinkSync(counterPath);
289
- }
290
- } catch (_e) {
291
- // Best-effort
292
- }
284
+ // .compact-counter cleanup removed -- legacy file no longer used
293
285
  }
294
286
 
295
287
  /**
@@ -26,8 +26,6 @@ const path = require('path');
26
26
  const { execSync } = require('child_process');
27
27
  const { logHook } = require('./hook-logger');
28
28
  const { logEvent } = require('./event-logger');
29
- const { resolveConfig } = require('./lib/local-llm/health');
30
- const { classifyCommit } = require('./lib/local-llm/operations/classify-commit');
31
29
 
32
30
  const VALID_TYPES = ['feat', 'fix', 'refactor', 'test', 'docs', 'chore', 'wip', 'revert', 'perf', 'ci', 'build'];
33
31
 
@@ -75,7 +73,13 @@ function checkAiCoAuthorResult(command, sessionId) {
75
73
 
76
74
  function checkSensitiveFilesResult(sessionId) {
77
75
  try {
78
- const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
76
+ let output;
77
+ try {
78
+ output = execSync('git diff --cached --name-only', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
79
+ } catch (_e) {
80
+ // git diff --cached fails during git tag or in --no-index contexts
81
+ return null;
82
+ }
79
83
  const files = output.trim().split('\n').filter(Boolean);
80
84
 
81
85
  const matched = files.filter((file) => {
@@ -157,58 +161,6 @@ function checkCommit(data) {
157
161
  return null;
158
162
  }
159
163
 
160
- /**
161
- * Load and resolve the local_llm config block from .planning/config.json.
162
- * Returns a resolved config (always safe to use — disabled by default on error).
163
- */
164
- function loadLocalLlmConfig(cwd) {
165
- try {
166
- const configPath = path.join(cwd || process.cwd(), '.planning', 'config.json');
167
- const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
168
- return resolveConfig(parsed.local_llm);
169
- } catch (_e) {
170
- return resolveConfig(undefined);
171
- }
172
- }
173
-
174
- /**
175
- * Async LLM enrichment for commit messages. Returns an advisory string or null.
176
- * Called after checkCommit passes (valid format) to provide semantic classification.
177
- *
178
- * @param {object} data - parsed hook data
179
- * @returns {Promise<string|null>}
180
- */
181
- async function enrichCommitLlm(data) {
182
- try {
183
- const command = data.tool_input?.command || '';
184
- const message = extractCommitMessage(command);
185
- if (!message) return null;
186
-
187
- const cwd = process.cwd();
188
- const llmConfig = loadLocalLlmConfig(cwd);
189
- const planningDir = path.join(cwd, '.planning');
190
-
191
- // Get staged files for scope validation
192
- let stagedFiles = [];
193
- try {
194
- const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
195
- stagedFiles = output.trim().split('\n').filter(Boolean);
196
- } catch (_e) {
197
- // Not in a git repo — skip staged files context
198
- }
199
-
200
- const llmResult = await classifyCommit(llmConfig, planningDir, message, stagedFiles, data.session_id);
201
- if (llmResult && llmResult.classification !== 'correct') {
202
- return 'LLM commit advisory: ' + llmResult.classification +
203
- ' (confidence: ' + (llmResult.confidence * 100).toFixed(0) + '%)';
204
- }
205
- return null;
206
- } catch (_llmErr) {
207
- // Never propagate LLM errors
208
- return null;
209
- }
210
- }
211
-
212
164
  function main() {
213
165
  let input = '';
214
166
 
@@ -223,14 +175,6 @@ function main() {
223
175
  process.exit(result.exitCode);
224
176
  }
225
177
 
226
- // LLM semantic classification — advisory only (after format validation passes)
227
- const llmAdvisory = await enrichCommitLlm(data);
228
- if (llmAdvisory) {
229
- process.stdout.write(JSON.stringify({
230
- additionalContext: '[pbr] ' + llmAdvisory
231
- }));
232
- }
233
-
234
178
  process.exit(0);
235
179
  } catch (_e) {
236
180
  // Parse error - don't block
@@ -267,5 +211,5 @@ function extractCommitMessage(command) {
267
211
  return null;
268
212
  }
269
213
 
270
- module.exports = { checkCommit, enrichCommitLlm };
214
+ module.exports = { checkCommit };
271
215
  if (require.main === module || process.argv[1] === __filename) { main(); }
@@ -20,8 +20,6 @@
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
22
  const { logHook } = require('./hook-logger');
23
- const { resolveConfig } = require('./lib/local-llm/health');
24
- const { validateTask: llmValidateTask } = require('./lib/local-llm/operations/validate-task');
25
23
  const { checkNonPbrAgent } = require('./enforce-pbr-workflow');
26
24
  const { KNOWN_AGENTS } = require('./lib/core');
27
25
 
@@ -39,21 +37,6 @@ const { checkDebuggerAdvisory, checkCheckpointManifest, checkActiveSkillIntegrit
39
37
  const { checkDocExistence } = require('./lib/gates/doc-existence');
40
38
  const { checkUserConfirmationGate } = require('./lib/gates/user-confirmation');
41
39
 
42
- /**
43
- * Load and resolve the local_llm config block from .planning/config.json.
44
- * Returns a resolved config (always safe to use — disabled by default on error).
45
- * @param {string} cwd - working directory to resolve .planning/config.json from
46
- */
47
- function loadLocalLlmConfig(cwd) {
48
- try {
49
- const configPath = path.join(cwd, '.planning', 'config.json');
50
- const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
51
- return resolveConfig(parsed.local_llm);
52
- } catch (_e) {
53
- return resolveConfig(undefined);
54
- }
55
- }
56
-
57
40
  const MAX_DESCRIPTION_LENGTH = 100;
58
41
 
59
42
  /**
@@ -264,19 +247,6 @@ function main() {
264
247
  if (nonPbrAgentResult) warnings.push(nonPbrAgentResult.output.additionalContext);
265
248
  if (planValGate && planValGate.warning) warnings.push(planValGate.warning);
266
249
 
267
- // LLM task coherence check — advisory only
268
- try {
269
- const llmCwd = process.env.PBR_PROJECT_ROOT || process.cwd();
270
- const llmConfig = loadLocalLlmConfig(llmCwd);
271
- const planningDir = path.join(llmCwd, '.planning');
272
- const llmResult = await llmValidateTask(llmConfig, planningDir, data.tool_input || {}, data.session_id);
273
- if (llmResult && !llmResult.coherent) {
274
- warnings.push('LLM task coherence advisory: ' + (llmResult.issue || 'Task description may not match intended operation.') + ' (confidence: ' + (llmResult.confidence * 100).toFixed(0) + '%)');
275
- }
276
- } catch (_llmErr) {
277
- // Never propagate LLM errors
278
- }
279
-
280
250
  if (warnings.length > 0) {
281
251
  for (const warning of warnings) {
282
252
  logHook('validate-task', 'PreToolUse', 'warn', { warning });
@@ -118,7 +118,6 @@ Boolean toggles that enable or disable specific workflow capabilities. All defau
118
118
  | `auto_advance` | `false` | Chain build, review, and plan automatically (requires `mode: autonomous`) |
119
119
  | `team_discussions` | `false` | Enable team-based discussion workflows (never used for execution) |
120
120
  | `inline_verify` | `false` | Per-task verification after each executor commit; adds ~10-20s latency per plan |
121
- | `multi_layer_validation` | `false` | **(DEPRECATED)** Parallel BugBot-style review passes. Never integrated; removed in plan 100-01. Key retained for backward compatibility. |
122
121
  | `extended_context` | `false` | Enable aggressive 1M context optimizations: higher concurrency (5 agents vs 3), default team review, always-parallel scan, pre-load build steps. Auto-set by quality profile. Safe optimizations (parallel research, full SUMMARY reads) use `context_window_tokens >= 500000` instead. |
123
122
 
124
123
  **Notable interactions:**
@@ -468,14 +467,6 @@ When `confidence_gate` is `true` and the executor SUMMARY reports all must-haves
468
467
 
469
468
  ---
470
469
 
471
- ## validation_passes (DEPRECATED)
472
-
473
- > **DEPRECATED**: The `multi_layer_validation` feature was never integrated and was removed in plan 100-01. This config key is retained for backward compatibility. The array has no effect.
474
-
475
- Defined which review passes would run with the multi-layer validation feature. Options included: `correctness`, `security`, `performance`, `style`, `tests`, `accessibility`, `docs`, `deps`.
476
-
477
- ---
478
-
479
470
  ## context_budget
480
471
 
481
472
  Controls context budget warning thresholds.
@@ -814,90 +805,3 @@ Run validation with: `node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js config val
814
805
  | `tdd_mode: true` + `depth: quick` | quick depth skips verification, which conflicts with TDD's verify-first approach |
815
806
  | `git.mode: disabled` + `atomic_commits: true` | atomic_commits has no effect when git is disabled |
816
807
  | `git.branching: phase` + `git.mode: disabled` | Branching settings are ignored when git is disabled |
817
-
818
- ---
819
-
820
- ## local_llm (DEPRECATED)
821
-
822
- > **DEPRECATED**: This feature has been removed as of v14.0. The config key is retained for backward compatibility. Setting `enabled: true` has no effect.
823
-
824
- Offloads selected PBR inference tasks to a locally running Ollama instance, reducing frontier model usage and latency for fast classification calls. The key `enabled` defaults to `false`, so users without Ollama see no change — all LLM calls continue routing to Claude as normal. When enabled, PBR uses a `local_first` routing strategy: fast tasks (artifact classification, task validation) go to the local model; complex tasks (planning, execution) stay on the frontier model.
825
-
826
- ### Quick setup
827
-
828
- 1. Install Ollama:
829
- - **Linux/macOS**: `curl -fsSL https://ollama.com/install.sh | sh`
830
- - **Windows**: Download from [ollama.com/download](https://ollama.com/download) and run the installer
831
- 2. Pull the recommended model: `ollama pull qwen2.5-coder:7b`
832
- 3. Add to `.planning/config.json`:
833
-
834
- ```json
835
- "local_llm": {
836
- "enabled": true,
837
- "model": "qwen2.5-coder:7b"
838
- }
839
- ```
840
-
841
- 4. Verify connectivity: `node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js llm health` (DEPRECATED -- returns `{ deprecated: true }` as a no-op)
842
-
843
- ### Field reference
844
-
845
- | Property | Type | Default | Description |
846
- |----------|------|---------|-------------|
847
- | `local_llm.enabled` | boolean | `false` | Enable local LLM offloading; `false` = all calls use frontier |
848
- | `local_llm.provider` | string | `"ollama"` | Backend provider; only `"ollama"` is supported |
849
- | `local_llm.endpoint` | string | `"http://localhost:11434"` | Ollama API base URL |
850
- | `local_llm.model` | string | `"qwen2.5-coder:7b"` | Model tag to use for local inference |
851
- | `local_llm.timeout_ms` | integer | `3000` | Per-request timeout in milliseconds; >= 500 |
852
- | `local_llm.max_retries` | integer | `1` | Number of retry attempts on failure before falling back |
853
- | `local_llm.fallback` | string | `"frontier"` | What to use when local LLM fails: `"frontier"` or `"skip"` |
854
- | `local_llm.routing_strategy` | string | `"local_first"` | `"local_first"` sends fast tasks local; `"always_local"` routes everything |
855
-
856
- ### features sub-table
857
-
858
- Controls which PBR tasks are eligible for local LLM offloading.
859
-
860
- | Property | Default | Description |
861
- |----------|---------|-------------|
862
- | `artifact_classification` | `true` | Classify artifact types (PLAN, SUMMARY, VERIFICATION) locally |
863
- | `task_validation` | `true` | Validate task scope and completeness locally |
864
- | `context_summarization` | `false` | Summarize context windows locally (higher token demand) |
865
- | `source_scoring` | `false` | Score source files by relevance locally |
866
-
867
- ### advanced sub-table
868
-
869
- | Property | Default | Description |
870
- |----------|---------|-------------|
871
- | `confidence_threshold` | `0.9` | Minimum confidence (0–1) for local output to be accepted; below this, falls back to frontier |
872
- | `shadow_mode` | `false` | Run local LLM in parallel with frontier but discard local results — useful for tuning confidence thresholds without affecting output |
873
- | `max_input_tokens` | `2000` | Truncate inputs longer than this before sending to local model |
874
- | `keep_alive` | `"30m"` | How long Ollama keeps the model loaded between requests (Ollama format: `"5m"`, `"1h"`) |
875
- | `num_ctx` | `4096` | Context window size passed to Ollama; **must be 4096 on Windows** (see Windows gotchas) |
876
- | `disable_after_failures` | `3` | Automatically disable local LLM for the session after this many consecutive failures |
877
-
878
- ### Hardware requirements
879
-
880
- | Tier | Hardware | Notes |
881
- |------|----------|-------|
882
- | Recommended | RTX 3060+ with 8 GB VRAM | Full GPU acceleration; qwen2.5-coder:7b loads entirely in VRAM |
883
- | Functional | GTX 1660+ with 6 GB VRAM | GPU acceleration with slight layer offload to RAM |
884
- | Marginal | CPU only, 32 GB RAM | Works but adds 5-20s latency per call; disable context-heavy features |
885
-
886
- For GPU acceleration, ensure NVIDIA drivers are 520+ and CUDA 11.8+ is installed. AMD GPU support is available via ROCm on Linux only.
887
-
888
- ### Windows gotchas
889
-
890
- - **Smart App Control**: May block `ollama_llama_server.exe` on first run. Allow it via Security settings or disable Smart App Control.
891
- - **Windows Defender**: Add an exclusion for `%LOCALAPPDATA%\Programs\Ollama\ollama_llama_server.exe` to prevent Defender from scanning inference calls in real time.
892
- - **`num_ctx` must be 4096**: Higher values cause GPU memory fragmentation on Windows and result in OOM errors mid-session. Always set `advanced.num_ctx: 4096` in your config.
893
- - **Firewall**: Ollama listens on `localhost:11434` by default. If you see connection refused errors, check that Windows Firewall is not blocking loopback connections.
894
-
895
- ### Viewing metrics (DEPRECATED)
896
-
897
- The local LLM feature was removed in v14.0. The `llm metrics` and `llm adjust-thresholds` subcommands are no-ops that return `{ deprecated: true }`. No metrics are collected.
898
-
899
- ```bash
900
- # These commands return { deprecated: true } and perform no action:
901
- node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js llm metrics
902
- node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js llm adjust-thresholds
903
- ```
@@ -9,82 +9,12 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const { extractFrontmatter } = require('../lib/frontmatter');
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // Helpers
15
16
  // ---------------------------------------------------------------------------
16
17
 
17
- /**
18
- * Parse YAML frontmatter from a markdown file.
19
- * Returns an object with extracted key-value pairs (simple flat parsing).
20
- */
21
- function parseFrontmatter(content) {
22
- const lines = content.replace(/\r\n/g, '\n').split('\n');
23
- if (lines[0] !== '---') return {};
24
-
25
- const endIdx = lines.indexOf('---', 1);
26
- if (endIdx === -1) return {};
27
-
28
- const fmLines = lines.slice(1, endIdx);
29
- const result = {};
30
- let currentKey = null;
31
- let inArray = false;
32
-
33
- for (const line of fmLines) {
34
- // Array item
35
- if (inArray && /^\s+-\s+/.test(line)) {
36
- const val = line.replace(/^\s+-\s+/, '').trim();
37
- result[currentKey].push(val);
38
- continue;
39
- }
40
-
41
- // Key-value pair
42
- const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)/);
43
- if (kvMatch) {
44
- const key = kvMatch[1];
45
- const rawVal = kvMatch[2].trim();
46
-
47
- if (rawVal === '' || rawVal === '[]') {
48
- // Could be start of array or empty value
49
- result[key] = [];
50
- currentKey = key;
51
- inArray = true;
52
- continue;
53
- }
54
-
55
- // Inline array like [Read, Write, Edit]
56
- if (rawVal.startsWith('[') && rawVal.endsWith(']')) {
57
- result[key] = rawVal.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
58
- currentKey = key;
59
- inArray = false;
60
- continue;
61
- }
62
-
63
- // Scalar value
64
- result[key] = rawVal.replace(/^["']|["']$/g, '');
65
- currentKey = key;
66
- inArray = false;
67
- continue;
68
- }
69
-
70
- // Array item under current key (indented with -)
71
- if (currentKey && /^\s+-\s/.test(line)) {
72
- if (!Array.isArray(result[currentKey])) {
73
- result[currentKey] = [];
74
- }
75
- const val = line.replace(/^\s+-\s+/, '').trim().replace(/^["']|["']$/g, '');
76
- result[currentKey].push(val);
77
- inArray = true;
78
- continue;
79
- }
80
-
81
- // Not a recognized line — end array mode
82
- inArray = false;
83
- }
84
-
85
- return result;
86
- }
87
-
88
18
  /**
89
19
  * Read all agent .md files and return array of { file, frontmatter, content }.
90
20
  */
@@ -97,7 +27,7 @@ function readAgentFiles(pluginRoot) {
97
27
  .map(f => {
98
28
  const filePath = path.join(agentsDir, f);
99
29
  const content = fs.readFileSync(filePath, 'utf8');
100
- return { file: f, frontmatter: parseFrontmatter(content), content };
30
+ return { file: f, frontmatter: extractFrontmatter(content), content };
101
31
  });
102
32
  }
103
33
 
@@ -25,6 +25,7 @@
25
25
  const fs = require('fs');
26
26
  const path = require('path');
27
27
  const { execSync } = require('child_process');
28
+ const { extractFrontmatter } = require('../lib/frontmatter');
28
29
 
29
30
  // ---------------------------------------------------------------------------
30
31
  // Result helper
@@ -47,43 +48,6 @@ function result(dimCode, status, message, evidence) {
47
48
  };
48
49
  }
49
50
 
50
- // ---------------------------------------------------------------------------
51
- // YAML frontmatter parser (lightweight, no external deps)
52
- // ---------------------------------------------------------------------------
53
-
54
- /**
55
- * Extract YAML frontmatter from markdown content as key-value pairs.
56
- * Handles both \n and \r\n line endings.
57
- * @param {string} content
58
- * @returns {object} Parsed frontmatter fields
59
- */
60
- function parseFrontmatter(content) {
61
- const lines = content.replace(/\r\n/g, '\n').split('\n');
62
- if (lines[0] !== '---') return {};
63
-
64
- const fields = {};
65
- for (let i = 1; i < lines.length; i++) {
66
- if (lines[i] === '---') break;
67
- const match = lines[i].match(/^(\w[\w_-]*):\s*(.*)$/);
68
- if (match) {
69
- let value = match[2].trim();
70
- // Strip surrounding quotes
71
- if ((value.startsWith('"') && value.endsWith('"')) ||
72
- (value.startsWith("'") && value.endsWith("'"))) {
73
- value = value.slice(1, -1);
74
- }
75
- // Parse arrays (simple single-line format)
76
- if (value === '[]') {
77
- value = [];
78
- } else if (value === 'null' || value === '') {
79
- value = value === 'null' ? null : value;
80
- }
81
- fields[match[1]] = value;
82
- }
83
- }
84
- return fields;
85
- }
86
-
87
51
  // ---------------------------------------------------------------------------
88
52
  // Session log helper (shared by WC-01, WC-07, WC-09, WC-10)
89
53
  // ---------------------------------------------------------------------------
@@ -285,7 +249,7 @@ function checkCompactionQuality(planningDir, _config) {
285
249
  const statePath = path.join(planningDir, 'STATE.md');
286
250
  try {
287
251
  const stateContent = fs.readFileSync(statePath, 'utf8');
288
- const fm = parseFrontmatter(stateContent);
252
+ const fm = extractFrontmatter(stateContent);
289
253
  const criticalFields = ['current_phase', 'status', 'last_activity'];
290
254
  const missing = criticalFields.filter(f => fm[f] === undefined || fm[f] === '');
291
255
  if (missing.length > 0) {
@@ -330,7 +294,7 @@ function checkStateFileIntegrity(planningDir, _config) {
330
294
  ]);
331
295
  }
332
296
 
333
- const fm = parseFrontmatter(content);
297
+ const fm = extractFrontmatter(content);
334
298
  const evidence = [];
335
299
 
336
300
  // Get actual phase directories
@@ -429,7 +393,7 @@ function checkStateFrontmatterIntegrity(planningDir, _config) {
429
393
  ]);
430
394
  }
431
395
 
432
- const fm = parseFrontmatter(content);
396
+ const fm = extractFrontmatter(content);
433
397
  const evidence = [];
434
398
 
435
399
  // Required fields
@@ -799,7 +763,7 @@ function checkArtifactFormatValidation(planningDir, _config) {
799
763
  for (const sf of summaryFiles) {
800
764
  try {
801
765
  const content = fs.readFileSync(path.join(dirPath, sf), 'utf8');
802
- const fm = parseFrontmatter(content);
766
+ const fm = extractFrontmatter(content);
803
767
  const missingFields = [];
804
768
 
805
769
  if (fm.requires === undefined) missingFields.push('requires');
@@ -64,8 +64,6 @@ function validateConfig(configPath) {
64
64
  }
65
65
  }
66
66
 
67
- // local_llm is deprecated — no advisory for missing key
68
-
69
67
  // Check version
70
68
  if (config.version && config.version < 2) {
71
69
  warnings.push(`Config version ${config.version} is outdated — expected version 2+`);
@@ -92,11 +90,6 @@ function validateConfig(configPath) {
92
90
  }
93
91
  }
94
92
 
95
- // DEPRECATED: local_llm infrastructure removed in v14.0
96
- if (config.local_llm && config.local_llm.enabled === true) {
97
- warnings.push('local_llm feature is deprecated and has no effect. Set enabled: false to suppress this warning.');
98
- }
99
-
100
93
  return warnings;
101
94
  }
102
95
 
@@ -29,7 +29,7 @@ function checkCrossPluginSync(data) {
29
29
 
30
30
  let stagedFiles;
31
31
  try {
32
- stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim().split('\n').filter(Boolean);
32
+ stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n').filter(Boolean);
33
33
  } catch (_e) {
34
34
  return null;
35
35
  }
@@ -25,8 +25,6 @@ const fs = require('fs');
25
25
  const path = require('path');
26
26
  const { logHook } = require('./hook-logger');
27
27
  const { logEvent } = require('./event-logger');
28
- const { resolveConfig } = require('./lib/local-llm/health');
29
- const { classifyArtifact } = require('./lib/local-llm/operations/classify-artifact');
30
28
 
31
29
  // Import all validators from extracted module
32
30
  const {
@@ -49,20 +47,6 @@ const {
49
47
  validateContext
50
48
  } = require('./lib/format-validators');
51
49
 
52
- /**
53
- * Load and resolve the local_llm config block from .planning/config.json.
54
- * Returns a resolved config (always safe to use -- disabled by default on error).
55
- */
56
- function loadLocalLlmConfig() {
57
- try {
58
- const configPath = path.join(process.cwd(), '.planning', 'config.json');
59
- const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
60
- return resolveConfig(parsed.local_llm);
61
- } catch (_e) {
62
- return resolveConfig(undefined);
63
- }
64
- }
65
-
66
50
  async function main() {
67
51
  let input = '';
68
52
 
@@ -119,22 +103,6 @@ async function main() {
119
103
  result.warnings.push(`Plan file "${basename}" uses non-standard naming. Expected format: PLAN-{NN}.md or PLAN.md. Phase-prefixed names (e.g., "10-02-PLAN.md") bypass some validation checks.`);
120
104
  }
121
105
 
122
- // LLM advisory enrichment -- advisory only, never blocks
123
- if ((isPlan || isSummary) && result.errors.length === 0) {
124
- try {
125
- const llmConfig = loadLocalLlmConfig();
126
- const planningDir = path.join(process.cwd(), '.planning');
127
- const fileType = isPlan ? 'PLAN' : 'SUMMARY';
128
- const llmResult = await classifyArtifact(llmConfig, planningDir, content, fileType, data.session_id);
129
- if (llmResult && llmResult.classification) {
130
- const llmNote = `Local LLM: ${fileType} classified as "${llmResult.classification}" (confidence: ${(llmResult.confidence * 100).toFixed(0)}%)${llmResult.reason ? ' — ' + llmResult.reason : ''}`;
131
- result.warnings.push(llmNote);
132
- }
133
- } catch (_llmErr) {
134
- // Never propagate LLM errors
135
- }
136
- }
137
-
138
106
  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';
139
107
 
140
108
  // Detect Write vs Edit: Write = full creation/overwrite (likely first attempt)
@@ -17,6 +17,7 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const { logHook } = require('./hook-logger');
19
19
  const { logEvent } = require('./event-logger');
20
+ const { extractFrontmatter } = require('./lib/frontmatter');
20
21
 
21
22
  const LIFECYCLE_STATUSES = ['planned', 'built', 'partial', 'verified'];
22
23
 
@@ -197,29 +198,32 @@ function main() {
197
198
 
198
199
  /**
199
200
  * Extract current phase number and status from STATE.md.
200
- * Handles common formats:
201
- * "**Phase**: 03 - slug-name"
202
- * "Phase: 3"
203
- * "Current phase: 03-slug-name"
204
- * "**Status**: planned"
205
- * "Phase status: built"
201
+ * Tries frontmatter first (v2), falls back to body regex (legacy).
202
+ * Delegates frontmatter parsing to canonical extractFrontmatter.
203
+ * Returns { phase, status } or null if unparseable.
206
204
  */
207
- function parseState(content) {
205
+ const parseState = (content) => {
206
+ // Try frontmatter first (v2 format)
207
+ const fm = extractFrontmatter(content);
208
+ if (fm.current_phase && fm.status) {
209
+ return {
210
+ phase: normalizePhaseNum(String(fm.current_phase)),
211
+ status: fm.status.toLowerCase()
212
+ };
213
+ }
214
+ // Fallback: body regex for legacy formats
208
215
  const phaseMatch = content.match(
209
216
  /\*{0,2}(?:Current\s+)?Phase\*{0,2}:\s*(\d+(?:\.\d+)?)/i
210
217
  );
211
-
212
218
  const statusMatch = content.match(
213
219
  /\*{0,2}(?:Phase\s+)?Status\*{0,2}:\s*["']?(\w+)["']?/i
214
220
  );
215
-
216
221
  if (!phaseMatch || !statusMatch) return null;
217
-
218
222
  return {
219
223
  phase: normalizePhaseNum(phaseMatch[1]),
220
224
  status: statusMatch[1].toLowerCase()
221
225
  };
222
- }
226
+ };
223
227
 
224
228
  /**
225
229
  * Find the status for a given phase in ROADMAP.md's Phase Overview table.