@sienklogic/plan-build-run 2.41.0 → 2.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/hooks/hooks.json +5 -0
- package/plugins/pbr/scripts/lib/circuit-state.js +134 -0
- package/plugins/pbr/scripts/lib/config.js +17 -0
- package/plugins/pbr/scripts/lib/core.js +5 -3
- package/plugins/pbr/scripts/lib/gates/advisories.js +125 -0
- package/plugins/pbr/scripts/lib/gates/build-dependency.js +100 -0
- package/plugins/pbr/scripts/lib/gates/build-executor.js +79 -0
- package/plugins/pbr/scripts/lib/gates/helpers.js +62 -0
- package/plugins/pbr/scripts/lib/gates/milestone-complete.js +120 -0
- package/plugins/pbr/scripts/lib/gates/plan-executor.js +36 -0
- package/plugins/pbr/scripts/lib/gates/quick-executor.js +76 -0
- package/plugins/pbr/scripts/lib/gates/review-planner.js +61 -0
- package/plugins/pbr/scripts/lib/gates/review-verifier.js +69 -0
- package/plugins/pbr/scripts/local-llm/client.js +29 -6
- package/plugins/pbr/scripts/run-hook.js +3 -0
- package/plugins/pbr/scripts/validate-task.js +10 -605
|
@@ -25,6 +25,16 @@ const { validateTask: llmValidateTask } = require('./local-llm/operations/valida
|
|
|
25
25
|
const { checkNonPbrAgent } = require('./enforce-pbr-workflow');
|
|
26
26
|
const { KNOWN_AGENTS } = require('./pbr-tools');
|
|
27
27
|
|
|
28
|
+
// Gate modules
|
|
29
|
+
const { checkQuickExecutorGate } = require('./lib/gates/quick-executor');
|
|
30
|
+
const { checkBuildExecutorGate } = require('./lib/gates/build-executor');
|
|
31
|
+
const { checkPlanExecutorGate } = require('./lib/gates/plan-executor');
|
|
32
|
+
const { checkReviewPlannerGate } = require('./lib/gates/review-planner');
|
|
33
|
+
const { checkReviewVerifierGate } = require('./lib/gates/review-verifier');
|
|
34
|
+
const { checkMilestoneCompleteGate, getVerificationStatus } = require('./lib/gates/milestone-complete');
|
|
35
|
+
const { checkBuildDependencyGate } = require('./lib/gates/build-dependency');
|
|
36
|
+
const { checkDebuggerAdvisory, checkCheckpointManifest, checkActiveSkillIntegrity } = require('./lib/gates/advisories');
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
39
|
* Load and resolve the local_llm config block from .planning/config.json.
|
|
30
40
|
* Returns a resolved config (always safe to use — disabled by default on error).
|
|
@@ -88,611 +98,6 @@ function checkTask(data) {
|
|
|
88
98
|
return warnings;
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
/**
|
|
92
|
-
* Blocking check: when the active skill is "quick" and an executor is being
|
|
93
|
-
* spawned, verify that at least one .planning/quick/{NNN}-{slug}/PLAN.md exists.
|
|
94
|
-
* Returns { block: true, reason: "..." } if the executor should be blocked,
|
|
95
|
-
* or null if it's OK to proceed.
|
|
96
|
-
*/
|
|
97
|
-
function checkQuickExecutorGate(data) {
|
|
98
|
-
const toolInput = data.tool_input || {};
|
|
99
|
-
const subagentType = toolInput.subagent_type || '';
|
|
100
|
-
|
|
101
|
-
// Only gate pbr:executor
|
|
102
|
-
if (subagentType !== 'pbr:executor') return null;
|
|
103
|
-
|
|
104
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
105
|
-
const planningDir = path.join(cwd, '.planning');
|
|
106
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
107
|
-
|
|
108
|
-
// Only gate when active skill is "quick"
|
|
109
|
-
try {
|
|
110
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
111
|
-
if (activeSkill !== 'quick') return null;
|
|
112
|
-
} catch (_e) {
|
|
113
|
-
// No .active-skill file — not in a quick task flow
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check for any PLAN.md in .planning/quick/*/
|
|
118
|
-
const quickDir = path.join(planningDir, 'quick');
|
|
119
|
-
if (!fs.existsSync(quickDir)) {
|
|
120
|
-
return {
|
|
121
|
-
block: true,
|
|
122
|
-
reason: 'Cannot spawn executor: .planning/quick/ directory does not exist.\n\nThe quick skill must create the task directory and PLAN.md before an executor can run (Steps 4-6).\n\nRe-run /pbr:quick to create the quick task directory and PLAN.md.'
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const dirs = fs.readdirSync(quickDir).filter(d => {
|
|
128
|
-
return /^\d{3}-/.test(d) && fs.statSync(path.join(quickDir, d)).isDirectory();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Look for the most recent quick task dir that has a PLAN.md
|
|
132
|
-
const hasPlan = dirs.some(d => {
|
|
133
|
-
const planFile = path.join(quickDir, d, 'PLAN.md');
|
|
134
|
-
try {
|
|
135
|
-
const stat = fs.statSync(planFile);
|
|
136
|
-
return stat.size > 0;
|
|
137
|
-
} catch (_e) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if (!hasPlan) {
|
|
143
|
-
return {
|
|
144
|
-
block: true,
|
|
145
|
-
reason: 'Cannot spawn executor: no PLAN.md found in any .planning/quick/*/ directory.\n\nThe quick skill must write a non-empty PLAN.md inside .planning/quick/{NNN}-{slug}/ before an executor can run (Steps 4-6).\n\nRe-run /pbr:quick to create the quick task directory and PLAN.md.'
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
} catch (_e) {
|
|
149
|
-
return {
|
|
150
|
-
block: true,
|
|
151
|
-
reason: 'Cannot spawn executor: failed to read .planning/quick/ directory.\n\nThe directory exists but could not be read, possibly due to a permissions issue or filesystem error.\n\nRe-run /pbr:quick to recreate the quick task directory and PLAN.md.'
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Blocking check: when the active skill is "build" and an executor is being
|
|
160
|
-
* spawned, verify that a PLAN*.md exists in the current phase directory.
|
|
161
|
-
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
162
|
-
*/
|
|
163
|
-
function checkBuildExecutorGate(data) {
|
|
164
|
-
const toolInput = data.tool_input || {};
|
|
165
|
-
const subagentType = toolInput.subagent_type || '';
|
|
166
|
-
|
|
167
|
-
// Only gate pbr:executor
|
|
168
|
-
if (subagentType !== 'pbr:executor') return null;
|
|
169
|
-
|
|
170
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
171
|
-
const planningDir = path.join(cwd, '.planning');
|
|
172
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
173
|
-
|
|
174
|
-
// Only gate when active skill is "build"
|
|
175
|
-
try {
|
|
176
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
177
|
-
if (activeSkill !== 'build') return null;
|
|
178
|
-
} catch (_e) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Read STATE.md for current phase
|
|
183
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
184
|
-
try {
|
|
185
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
186
|
-
const phaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
187
|
-
if (!phaseMatch) return null;
|
|
188
|
-
|
|
189
|
-
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
190
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
191
|
-
if (!fs.existsSync(phasesDir)) {
|
|
192
|
-
return {
|
|
193
|
-
block: true,
|
|
194
|
-
reason: 'Cannot spawn executor: .planning/phases/ directory does not exist.\n\nThe build skill requires a phases directory with at least one PLAN.md before an executor can run.\n\nRun /pbr:plan {N} to create plans first.'
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
199
|
-
if (dirs.length === 0) {
|
|
200
|
-
return {
|
|
201
|
-
block: true,
|
|
202
|
-
reason: `Cannot spawn executor: no phase directory found for phase ${currentPhase}.\n\nThe build skill needs a phase directory (e.g., .planning/phases/${currentPhase}-slug/) containing PLAN.md files.\n\nRun /pbr:plan ${currentPhase} to create plans first.`
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
207
|
-
const files = fs.readdirSync(phaseDir);
|
|
208
|
-
const hasPlan = files.some(f => {
|
|
209
|
-
if (!/^PLAN.*\.md$/i.test(f)) return false;
|
|
210
|
-
try {
|
|
211
|
-
return fs.statSync(path.join(phaseDir, f)).size > 0;
|
|
212
|
-
} catch (_e) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (!hasPlan) {
|
|
218
|
-
return {
|
|
219
|
-
block: true,
|
|
220
|
-
reason: `Cannot spawn executor: no PLAN.md found in .planning/phases/${dirs[0]}/.\n\nThe phase directory exists but contains no PLAN.md files. The executor needs at least one non-empty PLAN.md to work from.\n\nRun /pbr:plan ${currentPhase} to create plans first.`
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
} catch (_e) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Blocking check: when the active skill is "plan", block executor spawning.
|
|
232
|
-
* The plan skill should never spawn executors.
|
|
233
|
-
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
234
|
-
*/
|
|
235
|
-
function checkPlanExecutorGate(data) {
|
|
236
|
-
const toolInput = data.tool_input || {};
|
|
237
|
-
const subagentType = toolInput.subagent_type || '';
|
|
238
|
-
|
|
239
|
-
// Only gate pbr:executor
|
|
240
|
-
if (subagentType !== 'pbr:executor') return null;
|
|
241
|
-
|
|
242
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
243
|
-
const planningDir = path.join(cwd, '.planning');
|
|
244
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
248
|
-
if (activeSkill !== 'plan') return null;
|
|
249
|
-
} catch (_e) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
block: true,
|
|
255
|
-
reason: 'Plan skill cannot spawn executors.\n\nThe plan skill creates plans; the build skill executes them. Spawning an executor from the plan skill violates the separation of concerns.\n\nRun /pbr:build to execute plans instead.'
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Blocking check: when the active skill is "review" and a planner is being
|
|
261
|
-
* spawned, verify that a VERIFICATION.md exists in the current phase directory.
|
|
262
|
-
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
263
|
-
*/
|
|
264
|
-
function checkReviewPlannerGate(data) {
|
|
265
|
-
const toolInput = data.tool_input || {};
|
|
266
|
-
const subagentType = toolInput.subagent_type || '';
|
|
267
|
-
|
|
268
|
-
// Only gate pbr:planner
|
|
269
|
-
if (subagentType !== 'pbr:planner') return null;
|
|
270
|
-
|
|
271
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
272
|
-
const planningDir = path.join(cwd, '.planning');
|
|
273
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
274
|
-
|
|
275
|
-
// Only gate when active skill is "review"
|
|
276
|
-
try {
|
|
277
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
278
|
-
if (activeSkill !== 'review') return null;
|
|
279
|
-
} catch (_e) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Read STATE.md for current phase
|
|
284
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
285
|
-
try {
|
|
286
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
287
|
-
const phaseMatch = state.match(/Phase:\s*(\d+)/);
|
|
288
|
-
if (!phaseMatch) return null;
|
|
289
|
-
|
|
290
|
-
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
291
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
292
|
-
if (!fs.existsSync(phasesDir)) return null;
|
|
293
|
-
|
|
294
|
-
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
295
|
-
if (dirs.length === 0) return null;
|
|
296
|
-
|
|
297
|
-
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
298
|
-
const hasVerification = fs.existsSync(path.join(phaseDir, 'VERIFICATION.md'));
|
|
299
|
-
|
|
300
|
-
if (!hasVerification) {
|
|
301
|
-
return {
|
|
302
|
-
block: true,
|
|
303
|
-
reason: 'Review planner gate: cannot spawn planner without VERIFICATION.md.\n\nGap closure requires an existing VERIFICATION.md to identify which gaps need closing. Without it, the planner has no input.\n\nRun /pbr:review {N} to create VERIFICATION.md first.'
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
} catch (_e) {
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Blocking check: when the active skill is "review" and a verifier is being
|
|
315
|
-
* spawned, verify that a SUMMARY*.md exists in the current phase directory.
|
|
316
|
-
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
317
|
-
*/
|
|
318
|
-
function checkReviewVerifierGate(data) {
|
|
319
|
-
const toolInput = data.tool_input || {};
|
|
320
|
-
const subagentType = toolInput.subagent_type || '';
|
|
321
|
-
|
|
322
|
-
// Only gate pbr:verifier
|
|
323
|
-
if (subagentType !== 'pbr:verifier') return null;
|
|
324
|
-
|
|
325
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
326
|
-
const planningDir = path.join(cwd, '.planning');
|
|
327
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
328
|
-
|
|
329
|
-
// Only gate when active skill is "review"
|
|
330
|
-
try {
|
|
331
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
332
|
-
if (activeSkill !== 'review') return null;
|
|
333
|
-
} catch (_e) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Read STATE.md for current phase
|
|
338
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
339
|
-
try {
|
|
340
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
341
|
-
const phaseMatch = state.match(/Phase:\s*(\d+)/);
|
|
342
|
-
if (!phaseMatch) return null;
|
|
343
|
-
|
|
344
|
-
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
345
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
346
|
-
if (!fs.existsSync(phasesDir)) return null;
|
|
347
|
-
|
|
348
|
-
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
349
|
-
if (dirs.length === 0) return null;
|
|
350
|
-
|
|
351
|
-
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
352
|
-
const files = fs.readdirSync(phaseDir);
|
|
353
|
-
const hasSummary = files.some(f => {
|
|
354
|
-
if (!/^SUMMARY/i.test(f)) return false;
|
|
355
|
-
try {
|
|
356
|
-
return fs.statSync(path.join(phaseDir, f)).size > 0;
|
|
357
|
-
} catch (_e) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
if (!hasSummary) {
|
|
363
|
-
return {
|
|
364
|
-
block: true,
|
|
365
|
-
reason: 'Review verifier gate: cannot spawn verifier without SUMMARY.md.\n\nThe verifier checks executor output against the plan. Without a SUMMARY.md, there is nothing to verify.\n\nRun /pbr:build {N} to create SUMMARY.md first.'
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
} catch (_e) {
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Blocking check: when the active skill is "milestone" and a general/planner agent
|
|
377
|
-
* is being spawned for a "complete" operation, verify all milestone phases have VERIFICATION.md.
|
|
378
|
-
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
379
|
-
*/
|
|
380
|
-
function checkMilestoneCompleteGate(data) {
|
|
381
|
-
const toolInput = data.tool_input || {};
|
|
382
|
-
const subagentType = toolInput.subagent_type || '';
|
|
383
|
-
const description = toolInput.description || '';
|
|
384
|
-
|
|
385
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
386
|
-
const planningDir = path.join(cwd, '.planning');
|
|
387
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
388
|
-
|
|
389
|
-
// Only gate when active skill is "milestone"
|
|
390
|
-
try {
|
|
391
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
392
|
-
if (activeSkill !== 'milestone') return null;
|
|
393
|
-
} catch (_e) {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Only gate pbr:general and pbr:planner
|
|
398
|
-
if (subagentType !== 'pbr:general' && subagentType !== 'pbr:planner') return null;
|
|
399
|
-
|
|
400
|
-
// Only gate "complete" operations
|
|
401
|
-
if (!/complete/i.test(description)) return null;
|
|
402
|
-
|
|
403
|
-
// Read STATE.md for current phase
|
|
404
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
405
|
-
let currentPhase;
|
|
406
|
-
try {
|
|
407
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
408
|
-
const fmMatch = state.match(/current_phase:\s*(\d+)/);
|
|
409
|
-
if (fmMatch) {
|
|
410
|
-
currentPhase = parseInt(fmMatch[1], 10);
|
|
411
|
-
} else {
|
|
412
|
-
const bodyMatch = state.match(/Phase:\s*(\d+)/);
|
|
413
|
-
if (bodyMatch) currentPhase = parseInt(bodyMatch[1], 10);
|
|
414
|
-
}
|
|
415
|
-
if (!currentPhase) return null;
|
|
416
|
-
} catch (_e) {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Read ROADMAP.md and find the milestone containing the current phase
|
|
421
|
-
const roadmapFile = path.join(planningDir, 'ROADMAP.md');
|
|
422
|
-
try {
|
|
423
|
-
const roadmap = fs.readFileSync(roadmapFile, 'utf8');
|
|
424
|
-
|
|
425
|
-
// Split into milestone sections
|
|
426
|
-
const milestoneSections = roadmap.split(/^## Milestone:/m).slice(1);
|
|
427
|
-
|
|
428
|
-
for (const section of milestoneSections) {
|
|
429
|
-
// Parse phase numbers from table rows
|
|
430
|
-
const phaseNumbers = [];
|
|
431
|
-
const tableRowRegex = /^\|\s*(\d+)\s*\|/gm;
|
|
432
|
-
let match;
|
|
433
|
-
while ((match = tableRowRegex.exec(section)) !== null) {
|
|
434
|
-
phaseNumbers.push(parseInt(match[1], 10));
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Check if current phase is in this milestone
|
|
438
|
-
if (!phaseNumbers.includes(currentPhase)) continue;
|
|
439
|
-
|
|
440
|
-
// Found the right milestone — check all phases have VERIFICATION.md
|
|
441
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
442
|
-
if (!fs.existsSync(phasesDir)) return null;
|
|
443
|
-
|
|
444
|
-
for (const phaseNum of phaseNumbers) {
|
|
445
|
-
const paddedPhase = String(phaseNum).padStart(2, '0');
|
|
446
|
-
const pDirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(paddedPhase + '-'));
|
|
447
|
-
if (pDirs.length === 0) {
|
|
448
|
-
return {
|
|
449
|
-
block: true,
|
|
450
|
-
reason: `Milestone complete gate: phase ${paddedPhase} directory not found.\n\nAll milestone phases must exist and have a passing VERIFICATION.md before the milestone can be completed.\n\nRun /pbr:review ${paddedPhase} to verify the phase (it must reach status: passed).`
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
const verificationFile = path.join(phasesDir, pDirs[0], 'VERIFICATION.md');
|
|
454
|
-
const hasVerification = fs.existsSync(verificationFile);
|
|
455
|
-
if (!hasVerification) {
|
|
456
|
-
return {
|
|
457
|
-
block: true,
|
|
458
|
-
reason: `Milestone complete gate: phase ${paddedPhase} (${pDirs[0]}) lacks VERIFICATION.md.\n\nAll milestone phases must have a passing VERIFICATION.md before the milestone can be completed.\n\nRun /pbr:review ${paddedPhase} to verify the phase (it must reach status: passed).`
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
const verStatus = getVerificationStatus(verificationFile);
|
|
462
|
-
if (verStatus === 'gaps_found') {
|
|
463
|
-
return {
|
|
464
|
-
block: true,
|
|
465
|
-
reason: `Milestone complete gate: phase ${paddedPhase} VERIFICATION.md has status: gaps_found.\n\nAll gaps must be closed before the milestone can be completed. The verifier found issues that need resolution.\n\nRun /pbr:review ${paddedPhase} to close gaps (phase must reach status: passed).`
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// All phases verified
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
} catch (_e) {
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Blocking check: when the active skill is "build" and an executor is being
|
|
482
|
-
* spawned, verify that dependent phases (from ROADMAP.md) have VERIFICATION.md.
|
|
483
|
-
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
484
|
-
*/
|
|
485
|
-
function checkBuildDependencyGate(data) {
|
|
486
|
-
const toolInput = data.tool_input || {};
|
|
487
|
-
const subagentType = toolInput.subagent_type || '';
|
|
488
|
-
|
|
489
|
-
// Only gate pbr:executor
|
|
490
|
-
if (subagentType !== 'pbr:executor') return null;
|
|
491
|
-
|
|
492
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
493
|
-
const planningDir = path.join(cwd, '.planning');
|
|
494
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
495
|
-
|
|
496
|
-
// Only gate when active skill is "build"
|
|
497
|
-
try {
|
|
498
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
499
|
-
if (activeSkill !== 'build') return null;
|
|
500
|
-
} catch (_e) {
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Read STATE.md for current phase
|
|
505
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
506
|
-
let currentPhase;
|
|
507
|
-
try {
|
|
508
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
509
|
-
const phaseMatch = state.match(/Phase:\s*(\d+)/);
|
|
510
|
-
if (!phaseMatch) return null;
|
|
511
|
-
currentPhase = parseInt(phaseMatch[1], 10);
|
|
512
|
-
} catch (_e) {
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Read ROADMAP.md, find current phase section, check dependencies
|
|
517
|
-
const roadmapFile = path.join(planningDir, 'ROADMAP.md');
|
|
518
|
-
try {
|
|
519
|
-
const roadmap = fs.readFileSync(roadmapFile, 'utf8');
|
|
520
|
-
|
|
521
|
-
// Find ### Phase N: section
|
|
522
|
-
const phaseRegex = new RegExp(`### Phase ${currentPhase}:[\\s\\S]*?(?=### Phase \\d|$)`);
|
|
523
|
-
const phaseSection = roadmap.match(phaseRegex);
|
|
524
|
-
if (!phaseSection) return null;
|
|
525
|
-
|
|
526
|
-
// Look for **Depends on:** line
|
|
527
|
-
const depMatch = phaseSection[0].match(/\*\*Depends on:\*\*\s*(.*)/);
|
|
528
|
-
if (!depMatch) return null;
|
|
529
|
-
|
|
530
|
-
const depLine = depMatch[1].trim();
|
|
531
|
-
if (!depLine || /^none$/i.test(depLine)) return null;
|
|
532
|
-
|
|
533
|
-
// Parse phase numbers from "Phase 1", "Phase 1, Phase 2", etc.
|
|
534
|
-
const depPhases = [];
|
|
535
|
-
const depRegex = /Phase\s+(\d+)/gi;
|
|
536
|
-
let match;
|
|
537
|
-
while ((match = depRegex.exec(depLine)) !== null) {
|
|
538
|
-
depPhases.push(parseInt(match[1], 10));
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (depPhases.length === 0) return null;
|
|
542
|
-
|
|
543
|
-
// Check each dependent phase has VERIFICATION.md
|
|
544
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
545
|
-
if (!fs.existsSync(phasesDir)) return null;
|
|
546
|
-
|
|
547
|
-
for (const depPhase of depPhases) {
|
|
548
|
-
const paddedPhase = String(depPhase).padStart(2, '0');
|
|
549
|
-
const pDirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(paddedPhase + '-'));
|
|
550
|
-
if (pDirs.length === 0) {
|
|
551
|
-
return {
|
|
552
|
-
block: true,
|
|
553
|
-
reason: `Build dependency gate: dependent phase ${paddedPhase} lacks VERIFICATION.md.\n\nPhase ${currentPhase} depends on phase ${paddedPhase}, which must be verified before building can proceed.\n\nRun /pbr:review ${paddedPhase} to verify the dependency phase first.`
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
const hasVerification = fs.existsSync(path.join(phasesDir, pDirs[0], 'VERIFICATION.md'));
|
|
557
|
-
if (!hasVerification) {
|
|
558
|
-
return {
|
|
559
|
-
block: true,
|
|
560
|
-
reason: `Build dependency gate: dependent phase ${paddedPhase} lacks VERIFICATION.md.\n\nPhase ${currentPhase} depends on phase ${paddedPhase}, which must be verified before building can proceed.\n\nRun /pbr:review ${paddedPhase} to verify the dependency phase first.`
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
} catch (_e) {
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Parse VERIFICATION.md frontmatter to extract status field.
|
|
573
|
-
* Returns the status string or 'unknown' if not parseable.
|
|
574
|
-
*/
|
|
575
|
-
function getVerificationStatus(filePath) {
|
|
576
|
-
try {
|
|
577
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
578
|
-
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
579
|
-
if (!fmMatch) return 'unknown';
|
|
580
|
-
const statusMatch = fmMatch[1].match(/^status:\s*(\S+)/m);
|
|
581
|
-
return statusMatch ? statusMatch[1] : 'unknown';
|
|
582
|
-
} catch (_e) {
|
|
583
|
-
return 'unknown';
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Advisory check: when pbr:debugger is spawned and .active-skill is 'debug',
|
|
589
|
-
* warn if .planning/debug/ directory does not exist.
|
|
590
|
-
* Returns a warning string or null.
|
|
591
|
-
*/
|
|
592
|
-
function checkDebuggerAdvisory(data) {
|
|
593
|
-
const subagentType = data.tool_input?.subagent_type || '';
|
|
594
|
-
if (subagentType !== 'pbr:debugger') return null;
|
|
595
|
-
// Only advise when spawned from the debug skill
|
|
596
|
-
const debugCwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
597
|
-
const activeSkillPath = path.join(debugCwd, '.planning', '.active-skill');
|
|
598
|
-
try {
|
|
599
|
-
const activeSkill = fs.readFileSync(activeSkillPath, 'utf8').trim();
|
|
600
|
-
if (activeSkill !== 'debug') return null;
|
|
601
|
-
} catch (_e) {
|
|
602
|
-
return null; // No .active-skill file — skip advisory
|
|
603
|
-
}
|
|
604
|
-
const debugDir = path.join(debugCwd, '.planning', 'debug');
|
|
605
|
-
if (!fs.existsSync(debugDir)) {
|
|
606
|
-
return 'Debugger advisory: .planning/debug/ does not exist. Create it before spawning the debugger so output has a target location.';
|
|
607
|
-
}
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function checkCheckpointManifest(data) {
|
|
612
|
-
const toolInput = data.tool_input || {};
|
|
613
|
-
const subagentType = toolInput.subagent_type || '';
|
|
614
|
-
|
|
615
|
-
if (subagentType !== 'pbr:executor') return null;
|
|
616
|
-
|
|
617
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
618
|
-
const planningDir = path.join(cwd, '.planning');
|
|
619
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
623
|
-
if (activeSkill !== 'build') return null;
|
|
624
|
-
} catch (_e) {
|
|
625
|
-
return null;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Find current phase dir
|
|
629
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
630
|
-
try {
|
|
631
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
632
|
-
const phaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
633
|
-
if (!phaseMatch) return null;
|
|
634
|
-
|
|
635
|
-
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
636
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
637
|
-
if (!fs.existsSync(phasesDir)) return null;
|
|
638
|
-
|
|
639
|
-
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
640
|
-
if (dirs.length === 0) return null;
|
|
641
|
-
|
|
642
|
-
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
643
|
-
const manifestFile = path.join(phaseDir, '.checkpoint-manifest.json');
|
|
644
|
-
if (!fs.existsSync(manifestFile)) {
|
|
645
|
-
return 'Build advisory: .checkpoint-manifest.json not found in phase directory. The build skill should write this before spawning executors. To fix: Run /pbr:health to regenerate checkpoint manifest.';
|
|
646
|
-
}
|
|
647
|
-
} catch (_e) {
|
|
648
|
-
return null;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return null;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Advisory check: when any pbr:* agent is being spawned, warn if
|
|
656
|
-
* .planning/.active-skill doesn't exist. Without this file, all
|
|
657
|
-
* skill-specific enforcement is silently disabled.
|
|
658
|
-
* Returns a warning string or null.
|
|
659
|
-
*/
|
|
660
|
-
function checkActiveSkillIntegrity(data) {
|
|
661
|
-
const toolInput = data.tool_input || {};
|
|
662
|
-
const subagentType = toolInput.subagent_type || '';
|
|
663
|
-
|
|
664
|
-
if (typeof subagentType !== 'string' || !subagentType.startsWith('pbr:')) return null;
|
|
665
|
-
|
|
666
|
-
// Advisory agents that run without an active skill context — exempt from .active-skill checks
|
|
667
|
-
const EXEMPT_AGENTS = ['pbr:researcher', 'pbr:synthesizer', 'pbr:audit', 'pbr:dev-sync', 'pbr:general'];
|
|
668
|
-
if (EXEMPT_AGENTS.includes(subagentType)) return null;
|
|
669
|
-
|
|
670
|
-
const cwd = process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
671
|
-
const planningDir = path.join(cwd, '.planning');
|
|
672
|
-
|
|
673
|
-
// Only check if .planning/ exists (PBR project)
|
|
674
|
-
if (!fs.existsSync(planningDir)) return null;
|
|
675
|
-
|
|
676
|
-
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
677
|
-
if (!fs.existsSync(activeSkillFile)) {
|
|
678
|
-
return 'Active-skill integrity: .planning/.active-skill not found. Skill-specific enforcement is disabled. The invoking skill should write this file. To fix: Wait for the current skill to finish, or delete .planning/.active-skill if stale.';
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Stale lock detection: warn if .active-skill is older than 2 hours
|
|
682
|
-
try {
|
|
683
|
-
const stat = fs.statSync(activeSkillFile);
|
|
684
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
685
|
-
const TWO_HOURS = 2 * 60 * 60 * 1000;
|
|
686
|
-
if (ageMs > TWO_HOURS) {
|
|
687
|
-
const ageHours = Math.round(ageMs / (60 * 60 * 1000));
|
|
688
|
-
const skill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
689
|
-
return `Active-skill integrity: .planning/.active-skill is ${ageHours}h old (skill: "${skill}"). This may be a stale lock from a crashed session. Run /pbr:health to diagnose, or delete .planning/.active-skill if the previous session is no longer running.`;
|
|
690
|
-
}
|
|
691
|
-
} catch (_e) { /* best-effort */ }
|
|
692
|
-
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
101
|
function main() {
|
|
697
102
|
let input = '';
|
|
698
103
|
|