@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.
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +29 -16
- package/README.md +3 -3
- package/dashboard/server/index.js +10 -1
- package/dashboard/server/routes/agents.js +23 -2
- package/dashboard/server/routes/health.js +7 -4
- package/dashboard/server/routes/telemetry.js +20 -1
- package/dashboard/server/services/planning-reader.js +3 -17
- package/package.json +1 -1
- package/plan-build-run/bin/config-schema.json +23 -145
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/agents/advisor-researcher.md +1 -0
- package/plugins/pbr/agents/debugger.md +0 -4
- package/plugins/pbr/agents/researcher.md +0 -4
- package/plugins/pbr/agents/synthesizer.md +0 -4
- package/plugins/pbr/dist/check-config-change.js +0 -7
- package/plugins/pbr/dist/check-cross-plugin-sync.js +1 -1
- package/plugins/pbr/dist/check-plan-format.js +0 -32
- package/plugins/pbr/dist/check-roadmap-sync.js +15 -11
- package/plugins/pbr/dist/check-subagent-output.js +4 -60
- package/plugins/pbr/dist/check-summary-gate.js +3 -14
- package/plugins/pbr/dist/feedback-loop.js +12 -29
- package/plugins/pbr/dist/hook-server.js +58 -6
- package/plugins/pbr/dist/milestone-learnings.js +6 -56
- package/plugins/pbr/dist/pbr-tools.js +8 -91
- package/plugins/pbr/dist/post-bash-triage.js +5 -63
- package/plugins/pbr/dist/post-hoc.js +3 -52
- package/plugins/pbr/dist/post-write-dispatch.js +0 -36
- package/plugins/pbr/dist/pre-bash-dispatch.js +1 -7
- package/plugins/pbr/dist/pre-task-dispatch.js +0 -28
- package/plugins/pbr/dist/progress-tracker.js +2 -27
- package/plugins/pbr/dist/session-cleanup.js +1 -31
- package/plugins/pbr/dist/status-line.js +13 -11
- package/plugins/pbr/dist/suggest-compact.js +2 -10
- package/plugins/pbr/dist/validate-commit.js +8 -64
- package/plugins/pbr/dist/validate-task.js +0 -30
- package/plugins/pbr/references/config-reference.md +0 -96
- package/plugins/pbr/scripts/audit-checks/si-agent-hook-config-checks.js +2 -72
- package/plugins/pbr/scripts/audit-checks/workflow-compliance.js +5 -41
- package/plugins/pbr/scripts/check-config-change.js +0 -7
- package/plugins/pbr/scripts/check-cross-plugin-sync.js +1 -1
- package/plugins/pbr/scripts/check-plan-format.js +0 -32
- package/plugins/pbr/scripts/check-roadmap-sync.js +15 -11
- package/plugins/pbr/scripts/check-subagent-output.js +4 -60
- package/plugins/pbr/scripts/check-summary-gate.js +3 -14
- package/plugins/pbr/scripts/config-schema.json +16 -129
- package/plugins/pbr/scripts/feedback-loop.js +12 -29
- package/plugins/pbr/scripts/hook-server.js +58 -6
- package/plugins/pbr/scripts/lib/config.js +4 -11
- package/plugins/pbr/scripts/lib/contextual-help.js +5 -29
- package/plugins/pbr/scripts/lib/format-validators.js +1 -26
- package/plugins/pbr/scripts/lib/frontmatter.js +4 -4
- package/plugins/pbr/scripts/lib/gates/rich-agent-context.js +13 -19
- package/plugins/pbr/scripts/lib/health.js +4 -5
- package/plugins/pbr/scripts/lib/help.js +3 -54
- package/plugins/pbr/scripts/lib/phase.js +2 -4
- package/plugins/pbr/scripts/lib/pre-commit-checks.js +1 -1
- package/plugins/pbr/scripts/lib/pre-research.js +10 -17
- package/plugins/pbr/scripts/lib/roadmap.js +11 -35
- package/plugins/pbr/scripts/lib/smart-next-task.js +11 -20
- package/plugins/pbr/scripts/lib/spot-check.js +3 -106
- package/plugins/pbr/scripts/lib/state.js +25 -130
- package/plugins/pbr/scripts/lib/verify.js +56 -46
- package/plugins/pbr/scripts/milestone-learnings.js +6 -56
- package/plugins/pbr/scripts/pbr-tools.js +8 -91
- package/plugins/pbr/scripts/post-bash-triage.js +5 -63
- package/plugins/pbr/scripts/post-hoc.js +3 -52
- package/plugins/pbr/scripts/post-write-dispatch.js +0 -36
- package/plugins/pbr/scripts/pre-bash-dispatch.js +1 -7
- package/plugins/pbr/scripts/pre-task-dispatch.js +0 -28
- package/plugins/pbr/scripts/progress-tracker.js +2 -27
- package/plugins/pbr/scripts/session-cleanup.js +1 -31
- package/plugins/pbr/scripts/status-line.js +13 -11
- package/plugins/pbr/scripts/suggest-compact.js +2 -10
- package/plugins/pbr/scripts/test/state.test.js +5 -13
- package/plugins/pbr/scripts/validate-commit.js +8 -64
- package/plugins/pbr/scripts/validate-task.js +0 -30
- package/plugins/pbr/skills/begin/SKILL.md +1 -0
- package/plugins/pbr/skills/begin/templates/config.json.tmpl +0 -4
- package/plugins/pbr/skills/build/SKILL.md +6 -6
- package/plugins/pbr/skills/config/SKILL.md +1 -0
- package/plugins/pbr/skills/help/SKILL.md +1 -0
- package/plugins/pbr/skills/pause/SKILL.md +1 -0
- package/plugins/pbr/skills/profile-user/SKILL.md +1 -0
- package/plugins/pbr/skills/quick/SKILL.md +2 -1
- package/plugins/pbr/skills/resume/SKILL.md +1 -0
- package/plugins/pbr/skills/scan/SKILL.md +1 -0
- package/plugins/pbr/skills/setup/SKILL.md +1 -0
- package/plugins/pbr/skills/shared/state-update.md +2 -2
- package/plugins/pbr/skills/status/SKILL.md +1 -0
- package/plugins/pbr/references/behavioral-contexts.md +0 -53
- package/plugins/pbr/scripts/lib/autonomy.js +0 -91
- package/plugins/pbr/scripts/lib/circuit-state.js +0 -133
- package/plugins/pbr/scripts/lib/completion.js +0 -377
- package/plugins/pbr/scripts/lib/hypothesis-runner.js +0 -127
- package/plugins/pbr/scripts/lib/local-llm/client.js +0 -237
- package/plugins/pbr/scripts/lib/local-llm/health.js +0 -12
- package/plugins/pbr/scripts/lib/local-llm/index.js +0 -89
- package/plugins/pbr/scripts/lib/local-llm/metrics.js +0 -20
- package/plugins/pbr/scripts/lib/local-llm/operations/classify-artifact.js +0 -4
- package/plugins/pbr/scripts/lib/local-llm/operations/classify-commit.js +0 -4
- package/plugins/pbr/scripts/lib/local-llm/operations/classify-error.js +0 -4
- package/plugins/pbr/scripts/lib/local-llm/operations/classify-file-intent.js +0 -4
- package/plugins/pbr/scripts/lib/local-llm/operations/score-source.js +0 -72
- package/plugins/pbr/scripts/lib/local-llm/operations/summarize-context.js +0 -62
- package/plugins/pbr/scripts/lib/local-llm/operations/triage-test-output.js +0 -12
- package/plugins/pbr/scripts/lib/local-llm/operations/validate-task.js +0 -4
- package/plugins/pbr/scripts/lib/local-llm/router.js +0 -101
- package/plugins/pbr/scripts/lib/local-llm/shadow.js +0 -60
- package/plugins/pbr/scripts/lib/local-llm/threshold-tuner.js +0 -118
- package/plugins/pbr/scripts/lib/team-composer.js +0 -87
- package/plugins/pbr/scripts/lib/team-coordinator.js +0 -153
- package/plugins/pbr/scripts/lib/template.js +0 -222
- package/plugins/pbr/scripts/lib/test-cache.js +0 -54
- package/plugins/pbr/scripts/lib/trust-gate.js +0 -84
- 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')
|
|
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.`
|
|
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')
|
|
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.`
|
|
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
|
-
|
|
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":
|
|
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
|
|
26
|
-
*
|
|
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
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 } =
|
|
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 =
|
|
81
|
-
const passed =
|
|
82
|
-
const total =
|
|
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
|
|
93
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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', '
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
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', '
|
|
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(', ')})`);
|