@sienklogic/plan-build-run 2.6.0 → 2.8.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 +23 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/setup.sh +1 -1
- package/plugins/copilot-pbr/skills/import/SKILL.md +2 -2
- package/plugins/copilot-pbr/skills/note/SKILL.md +36 -50
- package/plugins/copilot-pbr/skills/quick/SKILL.md +1 -1
- package/plugins/copilot-pbr/skills/shared/context-loader-task.md +1 -1
- package/plugins/copilot-pbr/skills/status/SKILL.md +3 -3
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/skills/import/SKILL.md +2 -2
- package/plugins/cursor-pbr/skills/note/SKILL.md +36 -50
- package/plugins/cursor-pbr/skills/quick/SKILL.md +1 -1
- package/plugins/cursor-pbr/skills/shared/context-loader-task.md +1 -1
- package/plugins/cursor-pbr/skills/status/SKILL.md +3 -3
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/scripts/check-plan-format.js +89 -7
- package/plugins/pbr/scripts/check-roadmap-sync.js +21 -5
- package/plugins/pbr/scripts/check-skill-workflow.js +7 -0
- package/plugins/pbr/scripts/check-subagent-output.js +79 -3
- package/plugins/pbr/scripts/post-write-dispatch.js +8 -1
- package/plugins/pbr/scripts/progress-tracker.js +13 -9
- package/plugins/pbr/scripts/validate-task.js +431 -1
- package/plugins/pbr/skills/import/SKILL.md +2 -2
- package/plugins/pbr/skills/note/SKILL.md +36 -50
- package/plugins/pbr/skills/quick/SKILL.md +1 -1
- package/plugins/pbr/skills/shared/context-loader-task.md +1 -1
- package/plugins/pbr/skills/status/SKILL.md +3 -3
|
@@ -252,6 +252,384 @@ function checkPlanExecutorGate(data) {
|
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Blocking check: when the active skill is "review" and a planner is being
|
|
257
|
+
* spawned, verify that a VERIFICATION.md exists in the current phase directory.
|
|
258
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
259
|
+
*/
|
|
260
|
+
function checkReviewPlannerGate(data) {
|
|
261
|
+
const toolInput = data.tool_input || {};
|
|
262
|
+
const subagentType = toolInput.subagent_type || '';
|
|
263
|
+
|
|
264
|
+
// Only gate pbr:planner
|
|
265
|
+
if (subagentType !== 'pbr:planner') return null;
|
|
266
|
+
|
|
267
|
+
const cwd = process.cwd();
|
|
268
|
+
const planningDir = path.join(cwd, '.planning');
|
|
269
|
+
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
270
|
+
|
|
271
|
+
// Only gate when active skill is "review"
|
|
272
|
+
try {
|
|
273
|
+
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
274
|
+
if (activeSkill !== 'review') return null;
|
|
275
|
+
} catch (_e) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Read STATE.md for current phase
|
|
280
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
281
|
+
try {
|
|
282
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
283
|
+
const phaseMatch = state.match(/Phase:\s*(\d+)/);
|
|
284
|
+
if (!phaseMatch) return null;
|
|
285
|
+
|
|
286
|
+
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
287
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
288
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
289
|
+
|
|
290
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
291
|
+
if (dirs.length === 0) return null;
|
|
292
|
+
|
|
293
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
294
|
+
const hasVerification = fs.existsSync(path.join(phaseDir, 'VERIFICATION.md'));
|
|
295
|
+
|
|
296
|
+
if (!hasVerification) {
|
|
297
|
+
return {
|
|
298
|
+
block: true,
|
|
299
|
+
reason: 'Review planner gate: Cannot spawn planner for gap closure without a VERIFICATION.md. Run /pbr:review first to generate verification results.'
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
} catch (_e) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Blocking check: when the active skill is "review" and a verifier is being
|
|
311
|
+
* spawned, verify that a SUMMARY*.md exists in the current phase directory.
|
|
312
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
313
|
+
*/
|
|
314
|
+
function checkReviewVerifierGate(data) {
|
|
315
|
+
const toolInput = data.tool_input || {};
|
|
316
|
+
const subagentType = toolInput.subagent_type || '';
|
|
317
|
+
|
|
318
|
+
// Only gate pbr:verifier
|
|
319
|
+
if (subagentType !== 'pbr:verifier') return null;
|
|
320
|
+
|
|
321
|
+
const cwd = process.cwd();
|
|
322
|
+
const planningDir = path.join(cwd, '.planning');
|
|
323
|
+
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
324
|
+
|
|
325
|
+
// Only gate when active skill is "review"
|
|
326
|
+
try {
|
|
327
|
+
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
328
|
+
if (activeSkill !== 'review') return null;
|
|
329
|
+
} catch (_e) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Read STATE.md for current phase
|
|
334
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
335
|
+
try {
|
|
336
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
337
|
+
const phaseMatch = state.match(/Phase:\s*(\d+)/);
|
|
338
|
+
if (!phaseMatch) return null;
|
|
339
|
+
|
|
340
|
+
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
341
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
342
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
343
|
+
|
|
344
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
345
|
+
if (dirs.length === 0) return null;
|
|
346
|
+
|
|
347
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
348
|
+
const files = fs.readdirSync(phaseDir);
|
|
349
|
+
const hasSummary = files.some(f => {
|
|
350
|
+
if (!/^SUMMARY/i.test(f)) return false;
|
|
351
|
+
try {
|
|
352
|
+
return fs.statSync(path.join(phaseDir, f)).size > 0;
|
|
353
|
+
} catch (_e) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (!hasSummary) {
|
|
359
|
+
return {
|
|
360
|
+
block: true,
|
|
361
|
+
reason: 'Review verifier gate: Cannot spawn verifier without SUMMARY.md in phase directory. Run /pbr:build first.'
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
} catch (_e) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Blocking check: when the active skill is "milestone" and a general/planner agent
|
|
373
|
+
* is being spawned for a "complete" operation, verify all milestone phases have VERIFICATION.md.
|
|
374
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
375
|
+
*/
|
|
376
|
+
function checkMilestoneCompleteGate(data) {
|
|
377
|
+
const toolInput = data.tool_input || {};
|
|
378
|
+
const subagentType = toolInput.subagent_type || '';
|
|
379
|
+
const description = toolInput.description || '';
|
|
380
|
+
|
|
381
|
+
const cwd = process.cwd();
|
|
382
|
+
const planningDir = path.join(cwd, '.planning');
|
|
383
|
+
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
384
|
+
|
|
385
|
+
// Only gate when active skill is "milestone"
|
|
386
|
+
try {
|
|
387
|
+
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
388
|
+
if (activeSkill !== 'milestone') return null;
|
|
389
|
+
} catch (_e) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Only gate pbr:general and pbr:planner
|
|
394
|
+
if (subagentType !== 'pbr:general' && subagentType !== 'pbr:planner') return null;
|
|
395
|
+
|
|
396
|
+
// Only gate "complete" operations
|
|
397
|
+
if (!/complete/i.test(description)) return null;
|
|
398
|
+
|
|
399
|
+
// Read STATE.md for current phase
|
|
400
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
401
|
+
let currentPhase;
|
|
402
|
+
try {
|
|
403
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
404
|
+
const fmMatch = state.match(/current_phase:\s*(\d+)/);
|
|
405
|
+
if (fmMatch) {
|
|
406
|
+
currentPhase = parseInt(fmMatch[1], 10);
|
|
407
|
+
} else {
|
|
408
|
+
const bodyMatch = state.match(/Phase:\s*(\d+)/);
|
|
409
|
+
if (bodyMatch) currentPhase = parseInt(bodyMatch[1], 10);
|
|
410
|
+
}
|
|
411
|
+
if (!currentPhase) return null;
|
|
412
|
+
} catch (_e) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Read ROADMAP.md and find the milestone containing the current phase
|
|
417
|
+
const roadmapFile = path.join(planningDir, 'ROADMAP.md');
|
|
418
|
+
try {
|
|
419
|
+
const roadmap = fs.readFileSync(roadmapFile, 'utf8');
|
|
420
|
+
|
|
421
|
+
// Split into milestone sections
|
|
422
|
+
const milestoneSections = roadmap.split(/^## Milestone:/m).slice(1);
|
|
423
|
+
|
|
424
|
+
for (const section of milestoneSections) {
|
|
425
|
+
// Parse phase numbers from table rows
|
|
426
|
+
const phaseNumbers = [];
|
|
427
|
+
const tableRowRegex = /^\|\s*(\d+)\s*\|/gm;
|
|
428
|
+
let match;
|
|
429
|
+
while ((match = tableRowRegex.exec(section)) !== null) {
|
|
430
|
+
phaseNumbers.push(parseInt(match[1], 10));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Check if current phase is in this milestone
|
|
434
|
+
if (!phaseNumbers.includes(currentPhase)) continue;
|
|
435
|
+
|
|
436
|
+
// Found the right milestone — check all phases have VERIFICATION.md
|
|
437
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
438
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
439
|
+
|
|
440
|
+
for (const phaseNum of phaseNumbers) {
|
|
441
|
+
const paddedPhase = String(phaseNum).padStart(2, '0');
|
|
442
|
+
const pDirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(paddedPhase + '-'));
|
|
443
|
+
if (pDirs.length === 0) {
|
|
444
|
+
return {
|
|
445
|
+
block: true,
|
|
446
|
+
reason: `Milestone complete gate: Phase ${paddedPhase} directory not found. All milestone phases must be verified before completing milestone.`
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const hasVerification = fs.existsSync(path.join(phasesDir, pDirs[0], 'VERIFICATION.md'));
|
|
450
|
+
if (!hasVerification) {
|
|
451
|
+
return {
|
|
452
|
+
block: true,
|
|
453
|
+
reason: `Milestone complete gate: Phase ${paddedPhase} (${pDirs[0]}) lacks VERIFICATION.md. All milestone phases must be verified before completing milestone.`
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// All phases verified
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
} catch (_e) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Blocking check: when the active skill is "build" and an executor is being
|
|
470
|
+
* spawned, verify that dependent phases (from ROADMAP.md) have VERIFICATION.md.
|
|
471
|
+
* Returns { block: true, reason: "..." } if blocked, or null if OK.
|
|
472
|
+
*/
|
|
473
|
+
function checkBuildDependencyGate(data) {
|
|
474
|
+
const toolInput = data.tool_input || {};
|
|
475
|
+
const subagentType = toolInput.subagent_type || '';
|
|
476
|
+
|
|
477
|
+
// Only gate pbr:executor
|
|
478
|
+
if (subagentType !== 'pbr:executor') return null;
|
|
479
|
+
|
|
480
|
+
const cwd = process.cwd();
|
|
481
|
+
const planningDir = path.join(cwd, '.planning');
|
|
482
|
+
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
483
|
+
|
|
484
|
+
// Only gate when active skill is "build"
|
|
485
|
+
try {
|
|
486
|
+
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
487
|
+
if (activeSkill !== 'build') return null;
|
|
488
|
+
} catch (_e) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Read STATE.md for current phase
|
|
493
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
494
|
+
let currentPhase;
|
|
495
|
+
try {
|
|
496
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
497
|
+
const phaseMatch = state.match(/Phase:\s*(\d+)/);
|
|
498
|
+
if (!phaseMatch) return null;
|
|
499
|
+
currentPhase = parseInt(phaseMatch[1], 10);
|
|
500
|
+
} catch (_e) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Read ROADMAP.md, find current phase section, check dependencies
|
|
505
|
+
const roadmapFile = path.join(planningDir, 'ROADMAP.md');
|
|
506
|
+
try {
|
|
507
|
+
const roadmap = fs.readFileSync(roadmapFile, 'utf8');
|
|
508
|
+
|
|
509
|
+
// Find ### Phase N: section
|
|
510
|
+
const phaseRegex = new RegExp(`### Phase ${currentPhase}:[\\s\\S]*?(?=### Phase \\d|$)`);
|
|
511
|
+
const phaseSection = roadmap.match(phaseRegex);
|
|
512
|
+
if (!phaseSection) return null;
|
|
513
|
+
|
|
514
|
+
// Look for **Depends on:** line
|
|
515
|
+
const depMatch = phaseSection[0].match(/\*\*Depends on:\*\*\s*(.*)/);
|
|
516
|
+
if (!depMatch) return null;
|
|
517
|
+
|
|
518
|
+
const depLine = depMatch[1].trim();
|
|
519
|
+
if (!depLine || /^none$/i.test(depLine)) return null;
|
|
520
|
+
|
|
521
|
+
// Parse phase numbers from "Phase 1", "Phase 1, Phase 2", etc.
|
|
522
|
+
const depPhases = [];
|
|
523
|
+
const depRegex = /Phase\s+(\d+)/gi;
|
|
524
|
+
let match;
|
|
525
|
+
while ((match = depRegex.exec(depLine)) !== null) {
|
|
526
|
+
depPhases.push(parseInt(match[1], 10));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (depPhases.length === 0) return null;
|
|
530
|
+
|
|
531
|
+
// Check each dependent phase has VERIFICATION.md
|
|
532
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
533
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
534
|
+
|
|
535
|
+
for (const depPhase of depPhases) {
|
|
536
|
+
const paddedPhase = String(depPhase).padStart(2, '0');
|
|
537
|
+
const pDirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(paddedPhase + '-'));
|
|
538
|
+
if (pDirs.length === 0) {
|
|
539
|
+
return {
|
|
540
|
+
block: true,
|
|
541
|
+
reason: `Build dependency gate: Dependent phase ${paddedPhase} lacks VERIFICATION.md. Run /pbr:review on dependent phases first.`
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const hasVerification = fs.existsSync(path.join(phasesDir, pDirs[0], 'VERIFICATION.md'));
|
|
545
|
+
if (!hasVerification) {
|
|
546
|
+
return {
|
|
547
|
+
block: true,
|
|
548
|
+
reason: `Build dependency gate: Dependent phase ${paddedPhase} lacks VERIFICATION.md. Run /pbr:review on dependent phases first.`
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} catch (_e) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Advisory check: when active skill is "build" and an executor is being
|
|
561
|
+
* spawned, warn if .checkpoint-manifest.json is missing in the phase dir.
|
|
562
|
+
* Returns a warning string or null.
|
|
563
|
+
*/
|
|
564
|
+
function checkCheckpointManifest(data) {
|
|
565
|
+
const toolInput = data.tool_input || {};
|
|
566
|
+
const subagentType = toolInput.subagent_type || '';
|
|
567
|
+
|
|
568
|
+
if (subagentType !== 'pbr:executor') return null;
|
|
569
|
+
|
|
570
|
+
const cwd = process.cwd();
|
|
571
|
+
const planningDir = path.join(cwd, '.planning');
|
|
572
|
+
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const activeSkill = fs.readFileSync(activeSkillFile, 'utf8').trim();
|
|
576
|
+
if (activeSkill !== 'build') return null;
|
|
577
|
+
} catch (_e) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Find current phase dir
|
|
582
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
583
|
+
try {
|
|
584
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
585
|
+
const phaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
586
|
+
if (!phaseMatch) return null;
|
|
587
|
+
|
|
588
|
+
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
589
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
590
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
591
|
+
|
|
592
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase + '-'));
|
|
593
|
+
if (dirs.length === 0) return null;
|
|
594
|
+
|
|
595
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
596
|
+
const manifestFile = path.join(phaseDir, '.checkpoint-manifest.json');
|
|
597
|
+
if (!fs.existsSync(manifestFile)) {
|
|
598
|
+
return 'Build advisory: .checkpoint-manifest.json not found in phase directory. The build skill should write this before spawning executors.';
|
|
599
|
+
}
|
|
600
|
+
} catch (_e) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Advisory check: when any pbr:* agent is being spawned, warn if
|
|
609
|
+
* .planning/.active-skill doesn't exist. Without this file, all
|
|
610
|
+
* skill-specific enforcement is silently disabled.
|
|
611
|
+
* Returns a warning string or null.
|
|
612
|
+
*/
|
|
613
|
+
function checkActiveSkillIntegrity(data) {
|
|
614
|
+
const toolInput = data.tool_input || {};
|
|
615
|
+
const subagentType = toolInput.subagent_type || '';
|
|
616
|
+
|
|
617
|
+
if (typeof subagentType !== 'string' || !subagentType.startsWith('pbr:')) return null;
|
|
618
|
+
|
|
619
|
+
const cwd = process.cwd();
|
|
620
|
+
const planningDir = path.join(cwd, '.planning');
|
|
621
|
+
|
|
622
|
+
// Only check if .planning/ exists (PBR project)
|
|
623
|
+
if (!fs.existsSync(planningDir)) return null;
|
|
624
|
+
|
|
625
|
+
const activeSkillFile = path.join(planningDir, '.active-skill');
|
|
626
|
+
if (!fs.existsSync(activeSkillFile)) {
|
|
627
|
+
return 'Active-skill integrity: .planning/.active-skill not found. Skill-specific enforcement is disabled. The invoking skill should write this file.';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
255
633
|
function main() {
|
|
256
634
|
let input = '';
|
|
257
635
|
|
|
@@ -285,6 +663,18 @@ function main() {
|
|
|
285
663
|
return;
|
|
286
664
|
}
|
|
287
665
|
|
|
666
|
+
// Blocking gate: review skill planner needs VERIFICATION.md
|
|
667
|
+
const reviewGate = checkReviewPlannerGate(data);
|
|
668
|
+
if (reviewGate && reviewGate.block) {
|
|
669
|
+
logHook('validate-task', 'PreToolUse', 'blocked', { reason: reviewGate.reason });
|
|
670
|
+
process.stdout.write(JSON.stringify({
|
|
671
|
+
decision: 'block',
|
|
672
|
+
reason: reviewGate.reason
|
|
673
|
+
}));
|
|
674
|
+
process.exit(2);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
288
678
|
// Blocking gate: plan skill cannot spawn executors
|
|
289
679
|
const planGate = checkPlanExecutorGate(data);
|
|
290
680
|
if (planGate && planGate.block) {
|
|
@@ -297,8 +687,48 @@ function main() {
|
|
|
297
687
|
return;
|
|
298
688
|
}
|
|
299
689
|
|
|
690
|
+
// Blocking gate: review verifier needs SUMMARY.md
|
|
691
|
+
const reviewVerifierGate = checkReviewVerifierGate(data);
|
|
692
|
+
if (reviewVerifierGate && reviewVerifierGate.block) {
|
|
693
|
+
logHook('validate-task', 'PreToolUse', 'blocked', { reason: reviewVerifierGate.reason });
|
|
694
|
+
process.stdout.write(JSON.stringify({
|
|
695
|
+
decision: 'block',
|
|
696
|
+
reason: reviewVerifierGate.reason
|
|
697
|
+
}));
|
|
698
|
+
process.exit(2);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Blocking gate: milestone complete needs all phases verified
|
|
703
|
+
const milestoneGate = checkMilestoneCompleteGate(data);
|
|
704
|
+
if (milestoneGate && milestoneGate.block) {
|
|
705
|
+
logHook('validate-task', 'PreToolUse', 'blocked', { reason: milestoneGate.reason });
|
|
706
|
+
process.stdout.write(JSON.stringify({
|
|
707
|
+
decision: 'block',
|
|
708
|
+
reason: milestoneGate.reason
|
|
709
|
+
}));
|
|
710
|
+
process.exit(2);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Blocking gate: build dependency check
|
|
715
|
+
const buildDepGate = checkBuildDependencyGate(data);
|
|
716
|
+
if (buildDepGate && buildDepGate.block) {
|
|
717
|
+
logHook('validate-task', 'PreToolUse', 'blocked', { reason: buildDepGate.reason });
|
|
718
|
+
process.stdout.write(JSON.stringify({
|
|
719
|
+
decision: 'block',
|
|
720
|
+
reason: buildDepGate.reason
|
|
721
|
+
}));
|
|
722
|
+
process.exit(2);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
300
726
|
// Advisory warnings
|
|
301
727
|
const warnings = checkTask(data);
|
|
728
|
+
const manifestWarning = checkCheckpointManifest(data);
|
|
729
|
+
if (manifestWarning) warnings.push(manifestWarning);
|
|
730
|
+
const activeSkillWarning = checkActiveSkillIntegrity(data);
|
|
731
|
+
if (activeSkillWarning) warnings.push(activeSkillWarning);
|
|
302
732
|
|
|
303
733
|
if (warnings.length > 0) {
|
|
304
734
|
for (const warning of warnings) {
|
|
@@ -317,5 +747,5 @@ function main() {
|
|
|
317
747
|
});
|
|
318
748
|
}
|
|
319
749
|
|
|
320
|
-
module.exports = { checkTask, checkQuickExecutorGate, checkBuildExecutorGate, checkPlanExecutorGate, KNOWN_AGENTS, MAX_DESCRIPTION_LENGTH };
|
|
750
|
+
module.exports = { checkTask, checkQuickExecutorGate, checkBuildExecutorGate, checkPlanExecutorGate, checkReviewPlannerGate, checkReviewVerifierGate, checkMilestoneCompleteGate, checkBuildDependencyGate, checkCheckpointManifest, checkActiveSkillIntegrity, KNOWN_AGENTS, MAX_DESCRIPTION_LENGTH };
|
|
321
751
|
if (require.main === module || process.argv[1] === __filename) { main(); }
|
|
@@ -80,7 +80,7 @@ Read all relevant context files. This context is used for conflict detection in
|
|
|
80
80
|
- trigger equals the phase number as integer
|
|
81
81
|
- trigger equals * (always matches)
|
|
82
82
|
9. Pending todos — scan .planning/todos/pending/ for items related to this phase
|
|
83
|
-
10.
|
|
83
|
+
10. Notes (if .planning/notes/ exists) — check for related notes
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Collect all of this into a context bundle for use in Steps 4 and 5.
|
|
@@ -154,7 +154,7 @@ Run each of these checks. If any matches, record a `[WARNING]`:
|
|
|
154
154
|
#### INFO checks (supplementary context):
|
|
155
155
|
Run each of these checks. If any matches, record an `[INFO]`:
|
|
156
156
|
|
|
157
|
-
1. **Related notes**: Are there related notes in
|
|
157
|
+
1. **Related notes**: Are there related notes in `.planning/notes/`?
|
|
158
158
|
2. **Matching seeds**: Are there matching seeds in `.planning/seeds/` that could enhance the plan?
|
|
159
159
|
3. **Prior phase patterns**: What patterns from prior phases (from SUMMARY.md `patterns` fields) should the imported plan follow?
|
|
160
160
|
|
|
@@ -27,12 +27,23 @@ This skill runs **inline** — no Task, no AskUserQuestion, no Bash.
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Storage Format
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Notes are stored as **individual markdown files** in a notes directory:
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
- **Project scope**: `.planning/notes/{YYYY-MM-DD}-{slug}.md` — used when `.planning/` directory exists in cwd
|
|
35
|
+
- **Global scope**: `~/.claude/notes/{YYYY-MM-DD}-{slug}.md` — used as fallback when no `.planning/`, or when `--global` flag is present
|
|
36
|
+
|
|
37
|
+
Each note file has this format:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
---
|
|
41
|
+
date: "YYYY-MM-DD HH:mm"
|
|
42
|
+
promoted: false
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
{note text verbatim}
|
|
46
|
+
```
|
|
36
47
|
|
|
37
48
|
**`--global` flag**: Strip `--global` from anywhere in `$ARGUMENTS` before parsing. When present, force global scope regardless of whether `.planning/` exists.
|
|
38
49
|
|
|
@@ -57,27 +68,17 @@ Parse `$ARGUMENTS` after stripping `--global`:
|
|
|
57
68
|
|
|
58
69
|
## Subcommand: append
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
Create a timestamped note file in the target directory.
|
|
61
72
|
|
|
62
73
|
### Steps
|
|
63
74
|
|
|
64
|
-
1. Determine scope (project or global) per
|
|
65
|
-
2.
|
|
66
|
-
3.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Quick captures from `/pbr:note`. Ideas worth remembering.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
4. Ensure the file content ends with a newline before appending
|
|
78
|
-
5. Append: `- [YYYY-MM-DD HH:mm] {note text verbatim}`
|
|
79
|
-
6. Write the file
|
|
80
|
-
7. Confirm with exactly one line: `Noted ({scope}): {note text}`
|
|
75
|
+
1. Determine scope (project or global) per Storage Format above
|
|
76
|
+
2. Ensure the notes directory exists (`.planning/notes/` or `~/.claude/notes/`)
|
|
77
|
+
3. Generate slug: first ~4 meaningful words of the note text, lowercase, hyphen-separated (strip articles/prepositions from the start)
|
|
78
|
+
4. Generate filename: `{YYYY-MM-DD}-{slug}.md`
|
|
79
|
+
- If a file with that name already exists, append `-2`, `-3`, etc.
|
|
80
|
+
5. Write the file with frontmatter and note text (see Storage Format)
|
|
81
|
+
6. Confirm with exactly one line: `Noted ({scope}): {note text}`
|
|
81
82
|
- Where `{scope}` is "project" or "global"
|
|
82
83
|
|
|
83
84
|
### Constraints
|
|
@@ -94,11 +95,11 @@ Show notes from both project and global scopes.
|
|
|
94
95
|
|
|
95
96
|
### Steps
|
|
96
97
|
|
|
97
|
-
1.
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
100
|
-
4. Exclude
|
|
101
|
-
5.
|
|
98
|
+
1. Glob `.planning/notes/*.md` (if directory exists) — these are "project" notes
|
|
99
|
+
2. Glob `~/.claude/notes/*.md` (if directory exists) — these are "global" notes
|
|
100
|
+
3. For each file, read frontmatter to get `date` and `promoted` status
|
|
101
|
+
4. Exclude files where `promoted: true` from active counts (but still show them, dimmed)
|
|
102
|
+
5. Sort by date, number all active entries sequentially starting at 1
|
|
102
103
|
6. If total active entries > 20, show only the last 10 with a note about how many were omitted
|
|
103
104
|
|
|
104
105
|
### Display Format
|
|
@@ -106,18 +107,18 @@ Show notes from both project and global scopes.
|
|
|
106
107
|
```
|
|
107
108
|
Notes:
|
|
108
109
|
|
|
109
|
-
Project (.planning/
|
|
110
|
+
Project (.planning/notes/):
|
|
110
111
|
1. [2026-02-08 14:32] refactor the hook system to support async validators
|
|
111
112
|
2. [promoted] [2026-02-08 14:40] add rate limiting to the API endpoints
|
|
112
113
|
3. [2026-02-08 15:10] consider adding a --dry-run flag to build
|
|
113
114
|
|
|
114
|
-
Global (~/.claude/notes
|
|
115
|
+
Global (~/.claude/notes/):
|
|
115
116
|
4. [2026-02-08 10:00] cross-project idea about shared config
|
|
116
117
|
|
|
117
118
|
{count} active note(s). Use `/pbr:note promote <N>` to convert to a todo.
|
|
118
119
|
```
|
|
119
120
|
|
|
120
|
-
If a scope has no
|
|
121
|
+
If a scope has no directory or no entries, show: `(no notes)`
|
|
121
122
|
|
|
122
123
|
---
|
|
123
124
|
|
|
@@ -133,7 +134,7 @@ Convert a note into a todo file.
|
|
|
133
134
|
4. **Requires `.planning/` directory** — if it doesn't exist, warn: "Todos require a Plan-Build-Run project. Run `/pbr:begin` to initialize one, or use `/pbr:todo add` in an existing project."
|
|
134
135
|
5. Ensure `.planning/todos/pending/` directory exists
|
|
135
136
|
6. Generate todo ID: `{NNN}-{slug}` where NNN is the next sequential number (scan both `.planning/todos/pending/` and `.planning/todos/done/` for the highest existing number, increment by 1, zero-pad to 3 digits) and slug is the first ~4 meaningful words of the note text, lowercase, hyphen-separated
|
|
136
|
-
7. Extract the note text (
|
|
137
|
+
7. Extract the note text from the source file (body after frontmatter)
|
|
137
138
|
8. Create `.planning/todos/pending/{id}.md`:
|
|
138
139
|
|
|
139
140
|
```yaml
|
|
@@ -159,34 +160,18 @@ Promoted from quick note captured on {original date}.
|
|
|
159
160
|
- [ ] {primary criterion derived from note text}
|
|
160
161
|
```
|
|
161
162
|
|
|
162
|
-
9. Mark the
|
|
163
|
+
9. Mark the source note file as promoted: update its frontmatter to `promoted: true`
|
|
163
164
|
10. Confirm: `Promoted note {N} to todo {id}: {note text}`
|
|
164
165
|
|
|
165
166
|
---
|
|
166
167
|
|
|
167
|
-
## NOTES.md Format Reference
|
|
168
|
-
|
|
169
|
-
```markdown
|
|
170
|
-
# Notes
|
|
171
|
-
|
|
172
|
-
Quick captures from `/pbr:note`. Ideas worth remembering.
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
- [2026-02-08 14:32] refactor the hook system to support async validators
|
|
177
|
-
- [promoted] [2026-02-08 14:40] add rate limiting to the API endpoints
|
|
178
|
-
- [2026-02-08 15:10] consider adding a --dry-run flag to build
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
168
|
## Edge Cases
|
|
184
169
|
|
|
185
170
|
1. **"list" as note text**: `/pbr:note list of things` → saves note "list of things" (subcommand only when `list` is the entire arg)
|
|
186
|
-
2. **No `.planning/`**: Falls back to global `~/.claude/notes
|
|
171
|
+
2. **No `.planning/`**: Falls back to global `~/.claude/notes/` — works in any directory
|
|
187
172
|
3. **Promote without project**: Warns that todos require `.planning/`, suggests `/pbr:begin`
|
|
188
173
|
4. **Large files**: `list` shows last 10 when >20 active entries
|
|
189
|
-
5. **
|
|
174
|
+
5. **Duplicate slugs**: Append `-2`, `-3` etc. to filename if slug already used on same date
|
|
190
175
|
6. **`--global` position**: Stripped from anywhere — `--global my idea` and `my idea --global` both save "my idea" globally
|
|
191
176
|
7. **Promote already-promoted**: Tell user "Note {N} is already promoted" and stop
|
|
192
177
|
8. **Empty note text after stripping flags**: Treat as `list` subcommand
|
|
@@ -229,3 +214,4 @@ Note {N} not found. Valid range: 1-{max}.
|
|
|
229
214
|
4. **DO NOT** create `.planning/` if it doesn't exist — fall back to global
|
|
230
215
|
5. **DO NOT** number promoted notes in the active count (but still display them)
|
|
231
216
|
6. **DO NOT** over-format the confirmation — one line is enough
|
|
217
|
+
7. **DO NOT** use a flat NOTES.md file — always use individual files in notes directory
|
|
@@ -86,7 +86,7 @@ Use AskUserQuestion:
|
|
|
86
86
|
multiSelect: false
|
|
87
87
|
|
|
88
88
|
If user selects "Quick task": continue to Step 4.
|
|
89
|
-
If user selects "Full plan":
|
|
89
|
+
If user selects "Full plan": clean up `.active-skill` if it exists, then chain directly: `Skill({ skill: "pbr:plan", args: "" })`. The user's task description carries over in conversation context — the plan skill will pick it up.
|
|
90
90
|
If user selects "Revise": go back to Step 2 to get a new task description.
|
|
91
91
|
If user types something else (freeform): interpret their response and proceed accordingly.
|
|
92
92
|
|
|
@@ -31,7 +31,7 @@ Task({
|
|
|
31
31
|
- .planning/CONTEXT.md
|
|
32
32
|
- Any .planning/phases/*/CONTEXT.md files
|
|
33
33
|
- .planning/research/SUMMARY.md (if exists)
|
|
34
|
-
- .planning/
|
|
34
|
+
- .planning/notes/*.md (if notes directory exists — read frontmatter for date/promoted status)
|
|
35
35
|
- .planning/HISTORY.md (if exists — scan for decisions relevant to current work only, do NOT summarize all history)
|
|
36
36
|
|
|
37
37
|
Return ONLY the briefing text. No preamble, no suggestions."
|
|
@@ -147,9 +147,9 @@ If any discrepancy found, add: `Run /pbr:resume to auto-reconcile STATE.md.`
|
|
|
147
147
|
- Count and summarize if any exist
|
|
148
148
|
|
|
149
149
|
#### Quick Notes
|
|
150
|
-
- Check `.planning/
|
|
151
|
-
- Count active notes (
|
|
152
|
-
- Also check `~/.claude/notes
|
|
150
|
+
- Check `.planning/notes/` directory for note files (individual `.md` files)
|
|
151
|
+
- Count active notes (files where frontmatter does NOT contain `promoted: true`)
|
|
152
|
+
- Also check `~/.claude/notes/` for global notes
|
|
153
153
|
|
|
154
154
|
#### Quick Tasks
|
|
155
155
|
- Check `.planning/quick/` for recent quick tasks
|