@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.
Files changed (28) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/plugins/copilot-pbr/plugin.json +1 -1
  4. package/plugins/copilot-pbr/setup.sh +1 -1
  5. package/plugins/copilot-pbr/skills/import/SKILL.md +2 -2
  6. package/plugins/copilot-pbr/skills/note/SKILL.md +36 -50
  7. package/plugins/copilot-pbr/skills/quick/SKILL.md +1 -1
  8. package/plugins/copilot-pbr/skills/shared/context-loader-task.md +1 -1
  9. package/plugins/copilot-pbr/skills/status/SKILL.md +3 -3
  10. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  11. package/plugins/cursor-pbr/skills/import/SKILL.md +2 -2
  12. package/plugins/cursor-pbr/skills/note/SKILL.md +36 -50
  13. package/plugins/cursor-pbr/skills/quick/SKILL.md +1 -1
  14. package/plugins/cursor-pbr/skills/shared/context-loader-task.md +1 -1
  15. package/plugins/cursor-pbr/skills/status/SKILL.md +3 -3
  16. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  17. package/plugins/pbr/scripts/check-plan-format.js +89 -7
  18. package/plugins/pbr/scripts/check-roadmap-sync.js +21 -5
  19. package/plugins/pbr/scripts/check-skill-workflow.js +7 -0
  20. package/plugins/pbr/scripts/check-subagent-output.js +79 -3
  21. package/plugins/pbr/scripts/post-write-dispatch.js +8 -1
  22. package/plugins/pbr/scripts/progress-tracker.js +13 -9
  23. package/plugins/pbr/scripts/validate-task.js +431 -1
  24. package/plugins/pbr/skills/import/SKILL.md +2 -2
  25. package/plugins/pbr/skills/note/SKILL.md +36 -50
  26. package/plugins/pbr/skills/quick/SKILL.md +1 -1
  27. package/plugins/pbr/skills/shared/context-loader-task.md +1 -1
  28. 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. NOTES.md (if exists at .planning/NOTES.md) — check for related notes
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 NOTES.md?
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
- ## Scope Detection
30
+ ## Storage Format
31
31
 
32
- Two scopes exist. Auto-detect which to use:
32
+ Notes are stored as **individual markdown files** in a notes directory:
33
33
 
34
- 1. **Project scope**: `.planning/NOTES.md` — used when `.planning/` directory exists in cwd
35
- 2. **Global scope**: `~/.claude/notes.md` — used as fallback when no `.planning/`, or when `--global` flag is present
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
- Append a timestamped note to the target file.
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 Scope Detection above
65
- 2. Read the target file if it exists
66
- 3. If the file doesn't exist, create it with this header:
67
-
68
- ```markdown
69
- # Notes
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. Read `.planning/NOTES.md` (if exists) — these are "project" notes
98
- 2. Read `~/.claude/notes.md` (if exists) — these are "global" notes
99
- 3. Parse entries: lines matching `^- \[` are notes
100
- 4. Exclude lines containing `[promoted]` from active counts (but still show them, dimmed)
101
- 5. Number all active entries sequentially starting at 1, using plain integers (1, 2, 3...) for display (across both scopes)
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/NOTES.md):
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.md):
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 file or no entries, show: `(no notes)`
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 (everything after the timestamp)
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 original note as promoted: replace `- [` with `- [promoted] [` on that line
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.md` — works in any directory
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. **Missing newline**: Always ensure trailing newline before appending
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": respond "Use `/pbr:plan` to create a full planning cycle for this task." and stop.
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/NOTES.md (if exists)
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/NOTES.md` for active note entries
151
- - Count active notes (lines matching `^- \[` that don't contain `[promoted]`)
152
- - Also check `~/.claude/notes.md` for global 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