@sienklogic/plan-build-run 2.38.1 → 2.40.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 (37) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +1 -1
  3. package/plugins/copilot-pbr/agents/executor.agent.md +13 -0
  4. package/plugins/copilot-pbr/plugin.json +1 -1
  5. package/plugins/copilot-pbr/references/config-reference.md +22 -0
  6. package/plugins/copilot-pbr/references/git-integration.md +30 -0
  7. package/plugins/copilot-pbr/references/plan-format.md +4 -0
  8. package/plugins/copilot-pbr/skills/begin/SKILL.md +22 -0
  9. package/plugins/copilot-pbr/skills/build/SKILL.md +45 -0
  10. package/plugins/copilot-pbr/skills/explore/SKILL.md +17 -0
  11. package/plugins/copilot-pbr/skills/milestone/SKILL.md +54 -0
  12. package/plugins/copilot-pbr/templates/pr-body.md.tmpl +22 -0
  13. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  14. package/plugins/cursor-pbr/agents/executor.md +13 -0
  15. package/plugins/cursor-pbr/references/config-reference.md +22 -0
  16. package/plugins/cursor-pbr/references/git-integration.md +30 -0
  17. package/plugins/cursor-pbr/references/plan-format.md +4 -0
  18. package/plugins/cursor-pbr/skills/begin/SKILL.md +22 -0
  19. package/plugins/cursor-pbr/skills/build/SKILL.md +45 -0
  20. package/plugins/cursor-pbr/skills/explore/SKILL.md +17 -0
  21. package/plugins/cursor-pbr/skills/milestone/SKILL.md +54 -0
  22. package/plugins/cursor-pbr/templates/pr-body.md.tmpl +22 -0
  23. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  24. package/plugins/pbr/agents/executor.md +13 -0
  25. package/plugins/pbr/references/config-reference.md +22 -0
  26. package/plugins/pbr/references/git-integration.md +30 -0
  27. package/plugins/pbr/references/plan-format.md +4 -0
  28. package/plugins/pbr/scripts/lib/learnings.js +312 -0
  29. package/plugins/pbr/scripts/milestone-learnings.js +290 -0
  30. package/plugins/pbr/scripts/pbr-tools.js +43 -1
  31. package/plugins/pbr/scripts/progress-tracker.js +24 -1
  32. package/plugins/pbr/skills/begin/SKILL.md +23 -0
  33. package/plugins/pbr/skills/build/SKILL.md +45 -0
  34. package/plugins/pbr/skills/explore/SKILL.md +16 -0
  35. package/plugins/pbr/skills/milestone/SKILL.md +54 -0
  36. package/plugins/pbr/skills/plan/SKILL.md +23 -0
  37. package/plugins/pbr/templates/pr-body.md.tmpl +22 -0
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * milestone-learnings.js — Auto-aggregate learnings from milestone phase SUMMARY.md files.
4
+ * Called by the milestone complete flow after archiving phases.
5
+ *
6
+ * Usage: node milestone-learnings.js <milestone-archive-path> [--project <name>]
7
+ * e.g. node milestone-learnings.js .planning/milestones/v2.0 --project my-app
8
+ *
9
+ * Env: PBR_LEARNINGS_FILE — override the learnings file path (for testing)
10
+ */
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { logHook } = require('./hook-logger');
16
+ const { learningsIngest } = require('./lib/learnings');
17
+
18
+ // --- Helpers ---
19
+
20
+ /**
21
+ * Parse YAML frontmatter from a markdown file.
22
+ * Returns an object with string/array field values, or null if no frontmatter.
23
+ * Only handles simple YAML: scalar strings and dash-list arrays.
24
+ * @param {string} content
25
+ * @returns {object|null}
26
+ */
27
+ function parseFrontmatter(content) {
28
+ // Normalize line endings
29
+ const normalized = content.replace(/\r\n/g, '\n');
30
+ const match = normalized.match(/^---\n([\s\S]*?)\n---/);
31
+ if (!match) return null;
32
+
33
+ const yaml = match[1];
34
+ const result = {};
35
+ const lines = yaml.split('\n');
36
+ let currentKey = null;
37
+
38
+ for (const line of lines) {
39
+ // List item (must check before key match so " - item" doesn't match as key)
40
+ const listMatch = line.match(/^\s+-\s+"?([^"]+?)"?\s*$/);
41
+ if (listMatch) {
42
+ if (currentKey !== null) {
43
+ if (!Array.isArray(result[currentKey])) {
44
+ result[currentKey] = [];
45
+ }
46
+ result[currentKey].push(listMatch[1].trim());
47
+ }
48
+ continue;
49
+ }
50
+
51
+ // Key: value pair
52
+ const kvMatch = line.match(/^(\w[\w_-]*):\s*(.*)/);
53
+ if (kvMatch) {
54
+ currentKey = kvMatch[1];
55
+ const rawVal = kvMatch[2].trim();
56
+
57
+ if (rawVal === '' || rawVal === '[]') {
58
+ // Empty scalar or empty inline array — may be followed by list items
59
+ result[currentKey] = [];
60
+ } else if (rawVal.startsWith('[')) {
61
+ // Inline array (basic): [a, b]
62
+ const inner = rawVal.slice(1, rawVal.lastIndexOf(']'));
63
+ result[currentKey] = inner.split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
64
+ } else {
65
+ result[currentKey] = rawVal.replace(/^["']|["']$/g, '');
66
+ }
67
+ }
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Extract learning entries from a SUMMARY.md file's frontmatter.
75
+ * @param {string} summaryContent — raw file content
76
+ * @param {string} sourceProject — project name
77
+ * @returns {object[]} array of raw learning entry objects
78
+ */
79
+ function extractLearningsFromSummary(summaryContent, sourceProject) {
80
+ const fm = parseFrontmatter(summaryContent);
81
+ if (!fm) return [];
82
+
83
+ const entries = [];
84
+
85
+ // provides items → tech-pattern
86
+ const provides = Array.isArray(fm.provides) ? fm.provides : [];
87
+ for (const item of provides) {
88
+ if (!item || typeof item !== 'string') continue;
89
+ entries.push({
90
+ source_project: sourceProject,
91
+ type: 'tech-pattern',
92
+ tags: ['stack:inferred'],
93
+ confidence: 'low',
94
+ occurrences: 1,
95
+ summary: `Built: ${item}`,
96
+ detail: item
97
+ });
98
+ }
99
+
100
+ // key_decisions items → process-win
101
+ const decisions = Array.isArray(fm.key_decisions) ? fm.key_decisions : [];
102
+ for (const item of decisions) {
103
+ if (!item || typeof item !== 'string') continue;
104
+ entries.push({
105
+ source_project: sourceProject,
106
+ type: 'process-win',
107
+ tags: ['decision'],
108
+ confidence: 'low',
109
+ occurrences: 1,
110
+ summary: `Decision: ${item}`,
111
+ detail: item
112
+ });
113
+ }
114
+
115
+ // patterns items → tech-pattern
116
+ const patterns = Array.isArray(fm.patterns) ? fm.patterns : [];
117
+ for (const item of patterns) {
118
+ if (!item || typeof item !== 'string') continue;
119
+ entries.push({
120
+ source_project: sourceProject,
121
+ type: 'tech-pattern',
122
+ tags: ['pattern'],
123
+ confidence: 'low',
124
+ occurrences: 1,
125
+ summary: `Pattern: ${item}`,
126
+ detail: item
127
+ });
128
+ }
129
+
130
+ // deferred items → deferred-item
131
+ const deferred = Array.isArray(fm.deferred) ? fm.deferred : [];
132
+ for (const item of deferred) {
133
+ if (!item || typeof item !== 'string') continue;
134
+ entries.push({
135
+ source_project: sourceProject,
136
+ type: 'deferred-item',
137
+ tags: ['deferred'],
138
+ confidence: 'low',
139
+ occurrences: 1,
140
+ summary: `Deferred: ${item}`,
141
+ detail: item
142
+ });
143
+ }
144
+
145
+ // issues items → planning-failure or anti-pattern
146
+ const issues = Array.isArray(fm.issues) ? fm.issues : [];
147
+ for (const item of issues) {
148
+ if (!item || typeof item !== 'string') continue;
149
+ entries.push({
150
+ source_project: sourceProject,
151
+ type: 'planning-failure',
152
+ tags: ['issue'],
153
+ confidence: 'low',
154
+ occurrences: 1,
155
+ summary: `Issue: ${item}`,
156
+ detail: item
157
+ });
158
+ }
159
+
160
+ return entries;
161
+ }
162
+
163
+ /**
164
+ * Recursively find all SUMMARY*.md files under a phases directory.
165
+ * Matches both single-summary (SUMMARY.md) and per-plan (SUMMARY-45-01.md) patterns.
166
+ * @param {string} phasesDir
167
+ * @returns {string[]} absolute paths to SUMMARY*.md files
168
+ */
169
+ function findSummaryFiles(phasesDir) {
170
+ const results = [];
171
+ if (!fs.existsSync(phasesDir)) return results;
172
+
173
+ try {
174
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
175
+ for (const entry of entries) {
176
+ const fullPath = path.join(phasesDir, entry.name);
177
+ if (entry.isDirectory()) {
178
+ // Find all SUMMARY*.md files in this phase directory
179
+ try {
180
+ const phaseFiles = fs.readdirSync(fullPath);
181
+ for (const file of phaseFiles) {
182
+ if (/^SUMMARY.*\.md$/i.test(file)) {
183
+ results.push(path.join(fullPath, file));
184
+ }
185
+ }
186
+ } catch (_e) {
187
+ // Ignore read errors for individual phase dirs
188
+ }
189
+ // Recurse in case of nested dirs
190
+ results.push(...findSummaryFiles(fullPath));
191
+ }
192
+ }
193
+ } catch (_e) {
194
+ // Ignore permission errors
195
+ }
196
+ return results;
197
+ }
198
+
199
+ // --- Main ---
200
+
201
+ async function main() {
202
+ const args = process.argv.slice(2);
203
+
204
+ // Parse CLI arguments
205
+ const archivePath = args[0];
206
+ if (!archivePath) {
207
+ process.stderr.write(
208
+ 'Usage: node milestone-learnings.js <milestone-archive-path> [--project <name>]\n' +
209
+ 'Error: archive path is required\n'
210
+ );
211
+ process.exit(1);
212
+ }
213
+
214
+ // Parse --project flag
215
+ let projectName = null;
216
+ for (let i = 1; i < args.length; i++) {
217
+ if (args[i] === '--project' && args[i + 1]) {
218
+ projectName = args[i + 1];
219
+ i++;
220
+ }
221
+ }
222
+
223
+ // Default project name to basename of cwd
224
+ if (!projectName) {
225
+ projectName = path.basename(process.cwd());
226
+ }
227
+
228
+ const resolvedArchivePath = path.resolve(archivePath);
229
+
230
+ // Verify archive path exists
231
+ if (!fs.existsSync(resolvedArchivePath)) {
232
+ process.stderr.write(`Error: archive path does not exist: ${resolvedArchivePath}\n`);
233
+ process.exit(1);
234
+ }
235
+
236
+ // Learnings file path (can be overridden for testing)
237
+ const learningsOpts = process.env.PBR_LEARNINGS_FILE
238
+ ? { filePath: process.env.PBR_LEARNINGS_FILE }
239
+ : {};
240
+
241
+ const phasesDir = path.join(resolvedArchivePath, 'phases');
242
+ const summaryFiles = findSummaryFiles(phasesDir);
243
+
244
+ let created = 0;
245
+ let updated = 0;
246
+ let errors = 0;
247
+
248
+ for (const summaryPath of summaryFiles) {
249
+ try {
250
+ const content = fs.readFileSync(summaryPath, 'utf8');
251
+ const rawEntries = extractLearningsFromSummary(content, projectName);
252
+
253
+ for (const rawEntry of rawEntries) {
254
+ try {
255
+ const result = learningsIngest(rawEntry, learningsOpts);
256
+ if (result.action === 'created') {
257
+ created++;
258
+ } else {
259
+ updated++;
260
+ }
261
+ } catch (ingestErr) {
262
+ errors++;
263
+ process.stderr.write(`[milestone-learnings] Ingest error: ${ingestErr.message}\n`);
264
+ }
265
+ }
266
+ } catch (readErr) {
267
+ errors++;
268
+ process.stderr.write(`[milestone-learnings] Read error for ${summaryPath}: ${readErr.message}\n`);
269
+ }
270
+ }
271
+
272
+ const summary = `Learnings aggregated: ${created} new, ${updated} updated, ${errors} errors`;
273
+ process.stdout.write(summary + '\n');
274
+
275
+ try {
276
+ logHook('milestone-learnings', 'complete', 'aggregated', { created, updated, errors });
277
+ } catch (_e) {
278
+ // Non-fatal: logging failure must not break the script
279
+ }
280
+ }
281
+
282
+ // Run if called directly
283
+ if (require.main === module || process.argv[1] === __filename) {
284
+ main().catch(err => {
285
+ process.stderr.write(`[milestone-learnings] Fatal error: ${err.message}\n`);
286
+ process.exit(1);
287
+ });
288
+ }
289
+
290
+ module.exports = { extractLearningsFromSummary, findSummaryFiles, parseFrontmatter };
@@ -37,6 +37,9 @@
37
37
  * phase add <slug> [--after N] — Add a new phase directory (with renumbering)
38
38
  * phase remove <N> — Remove an empty phase directory (with renumbering)
39
39
  * phase list — List all phase directories with status
40
+ * learnings ingest <json-file> — Ingest a learning entry into global store
41
+ * learnings query [--tags X] [--min-confidence Y] [--stack S] [--type T] — Query learnings
42
+ * learnings check-thresholds — Check deferral trigger conditions
40
43
  *
41
44
  * Environment: PBR_PROJECT_ROOT — Override project root directory (used when hooks fire from subagent cwd)
42
45
  */
@@ -129,6 +132,12 @@ const {
129
132
  applyMigrations: _applyMigrations
130
133
  } = require('./lib/migrate');
131
134
 
135
+ const {
136
+ learningsIngest: _learningsIngest,
137
+ learningsQuery: _learningsQuery,
138
+ checkDeferralThresholds: _checkDeferralThresholds
139
+ } = require('./lib/learnings');
140
+
132
141
  // --- Local LLM imports (not extracted — separate module tree) ---
133
142
  const { resolveConfig, checkHealth } = require('./local-llm/health');
134
143
  const { classifyArtifact } = require('./local-llm/operations/classify-artifact');
@@ -669,10 +678,43 @@ async function main() {
669
678
  const force = args.includes('--force');
670
679
  const result = await migrate({ dryRun, force });
671
680
  output(result);
681
+ } else if (command === 'learnings') {
682
+ const subCmd = args[1];
683
+
684
+ if (subCmd === 'ingest') {
685
+ // learnings ingest <json-file-path>
686
+ const jsonFile = args[2];
687
+ if (!jsonFile) { error('Usage: learnings ingest <json-file>'); process.exit(1); }
688
+ const raw = fs.readFileSync(jsonFile, 'utf8');
689
+ const entry = JSON.parse(raw);
690
+ const result = _learningsIngest(entry);
691
+ output(result);
692
+
693
+ } else if (subCmd === 'query') {
694
+ // learnings query [--tags tag1,tag2] [--min-confidence low|medium|high] [--stack react] [--type tech-pattern]
695
+ const filters = {};
696
+ for (let i = 2; i < args.length; i++) {
697
+ if (args[i] === '--tags' && args[i + 1]) { filters.tags = args[++i].split(',').map(t => t.trim()); }
698
+ else if (args[i] === '--min-confidence' && args[i + 1]) { filters.minConfidence = args[++i]; }
699
+ else if (args[i] === '--stack' && args[i + 1]) { filters.stack = args[++i]; }
700
+ else if (args[i] === '--type' && args[i + 1]) { filters.type = args[++i]; }
701
+ }
702
+ const results = _learningsQuery(filters);
703
+ output(results);
704
+
705
+ } else if (subCmd === 'check-thresholds') {
706
+ // learnings check-thresholds — for progress-tracker to call
707
+ const triggered = _checkDeferralThresholds();
708
+ output(triggered);
709
+
710
+ } else {
711
+ error('Usage: learnings <ingest|query|check-thresholds>');
712
+ process.exit(1);
713
+ }
672
714
  } else if (command === 'validate-project') {
673
715
  output(validateProject());
674
716
  } else {
675
- error(`Unknown command: ${args.join(' ')}\nCommands: state load|check-progress|update|patch|advance-plan|record-metric, config validate|load-defaults|save-defaults|resolve-depth, validate-project, migrate [--dry-run] [--force], init execute-phase|plan-phase|quick|verify-work|resume|progress, plan-index, frontmatter, must-haves, phase-info, phase add|remove|list, roadmap update-status|update-plans, history append|load, todo list|get|add|done, event, llm health|status|classify|score-source|classify-error|summarize|metrics [--session <ISO>]|adjust-thresholds`);
717
+ error(`Unknown command: ${args.join(' ')}\nCommands: state load|check-progress|update|patch|advance-plan|record-metric, config validate|load-defaults|save-defaults|resolve-depth, validate-project, migrate [--dry-run] [--force], init execute-phase|plan-phase|quick|verify-work|resume|progress, plan-index, frontmatter, must-haves, phase-info, phase add|remove|list, roadmap update-status|update-plans, history append|load, todo list|get|add|done, event, llm health|status|classify|score-source|classify-error|summarize|metrics [--session <ISO>]|adjust-thresholds, learnings ingest|query|check-thresholds`);
676
718
  }
677
719
  } catch (e) {
678
720
  error(e.message);
@@ -262,6 +262,12 @@ function buildContext(planningDir, stateFile) {
262
262
  parts.push(`\n${hookHealth}`);
263
263
  }
264
264
 
265
+ // Check learnings deferral thresholds
266
+ const learningsThresholds = checkLearningsDeferrals(planningDir);
267
+ if (learningsThresholds.length > 0) {
268
+ parts.push(`\nLearnings deferral triggers ready:\n${learningsThresholds.join('\n')}`);
269
+ }
270
+
265
271
  parts.push('\n[PBR WORKFLOW REQUIRED — Route all work through PBR commands]\n- Fix a bug or small task → /pbr:quick\n- Plan a feature → /pbr:plan N\n- Build from a plan → /pbr:build N\n- Explore or research → /pbr:explore\n- Freeform request → /pbr:do\n- Do NOT write source code or spawn generic agents without an active PBR skill.\n- Use PBR agents (pbr:researcher, pbr:executor, etc.) not Explore/general-purpose.');
266
272
 
267
273
  return parts.join('\n');
@@ -396,7 +402,24 @@ function tryLaunchDashboard(port, _planningDir, projectDir) {
396
402
  probe.unref();
397
403
  }
398
404
 
405
+ /**
406
+ * Check learnings deferral thresholds and return notification strings.
407
+ * Wrapped in try/catch — threshold check must never break SessionStart.
408
+ * Equivalent to: node pbr-tools.js learnings check-thresholds
409
+ * @param {string} _planningDir — unused; thresholds check global learnings store
410
+ * @returns {string[]}
411
+ */
412
+ function checkLearningsDeferrals(_planningDir) {
413
+ try {
414
+ const { checkDeferralThresholds } = require('./lib/learnings');
415
+ const triggered = checkDeferralThresholds();
416
+ return triggered.map(t => ` - ${t.key}: ${t.trigger} met — consider implementing deferred feature`);
417
+ } catch (_e) {
418
+ return [];
419
+ }
420
+ }
421
+
399
422
  // Exported for testing
400
- module.exports = { getHookHealthSummary, FAILURE_DECISIONS, HOOK_HEALTH_MAX_ENTRIES, tryLaunchDashboard };
423
+ module.exports = { getHookHealthSummary, checkLearningsDeferrals, FAILURE_DECISIONS, HOOK_HEALTH_MAX_ENTRIES, tryLaunchDashboard };
401
424
 
402
425
  main().catch(() => {});
@@ -215,6 +215,25 @@ Spawn parallel Task() subagents for research. Each researcher writes to `.planni
215
215
 
216
216
  **CRITICAL: Create .planning/research/ directory NOW before spawning researchers. Do NOT skip this step.**
217
217
 
218
+ **Learnings injection (opt-in):** Before spawning researchers, check if global learnings exist:
219
+
220
+ ```bash
221
+ node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "stack,tech" 2>/dev/null
222
+ ```
223
+
224
+ If the command succeeds AND returns a non-empty JSON array:
225
+
226
+ - Write the results to a temp file:
227
+
228
+ ```bash
229
+ node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "stack,tech" > /tmp/pbr-learnings-$$.md
230
+ ```
231
+
232
+ - Note the temp file path as `{learnings_temp_path}`
233
+ - Add this file to the researcher's `files_to_read` block (see below)
234
+
235
+ If no learnings exist or the command fails: skip injection silently.
236
+
218
237
  **For each research topic, spawn a Task():**
219
238
 
220
239
  ```
@@ -236,13 +255,17 @@ For each researcher, construct the prompt by reading the template and filling in
236
255
  Read `skills/begin/templates/researcher-prompt.md.tmpl` for the prompt structure.
237
256
 
238
257
  **Prepend this block to the researcher prompt before sending:**
258
+
239
259
  ```
240
260
  <files_to_read>
241
261
  CRITICAL: Read these files BEFORE any other action:
242
262
  1. .planning/REQUIREMENTS.md — scoped requirements (if exists)
263
+ {if learnings_temp_path exists}2. {learnings_temp_path} — cross-project learnings (tech stack patterns from past PBR projects){/if}
243
264
  </files_to_read>
244
265
  ```
245
266
 
267
+ If `{learnings_temp_path}` was produced in the learnings injection step above, replace `{if...}{/if}` with the actual line. If no learnings were found, omit line 2 entirely.
268
+
246
269
  **Placeholders to fill:**
247
270
  - `{project name from questioning}` — project name gathered in Step 2
248
271
  - `{2-3 sentence description from questioning}` — project description from Step 2
@@ -616,6 +616,26 @@ Resume at: Task {N+1} (or re-execute checkpoint task with user's answer)
616
616
  Continue execution from the checkpoint. Skip completed tasks. Process the checkpoint resolution, then continue with remaining tasks. Write SUMMARY.md when done.
617
617
  ```
618
618
 
619
+ #### 6e-ii. CI Gate (after wave completion, conditional)
620
+
621
+ If `config.ci.gate_enabled` is `true` AND `config.git.branching` is not `none`:
622
+
623
+ 1. Push current commits: `git push`
624
+ 2. Wait 5 seconds for CI to trigger
625
+ 3. Check: `gh run list --branch $(git branch --show-current) --limit 1 --json status,conclusion,url`
626
+ 4. If in_progress: poll every 15 seconds up to `config.ci.wait_timeout_seconds`
627
+ 5. If failed/timed out: show warning box:
628
+
629
+ ```
630
+ ⚠ CI Status: {conclusion}
631
+ Run: {url}
632
+ Options: [Wait] [Continue anyway] [Abort]
633
+ ```
634
+
635
+ 6. Use AskUserQuestion to present options: Wait / Continue anyway / Abort
636
+ 7. If "Continue anyway": log deviation — `DEVIATION: CI gate bypassed for wave {N}`
637
+ 8. If "Abort": stop build, update STATE.md
638
+
619
639
  #### 6f. Update STATE.md
620
640
 
621
641
  After each wave completes (all plans in the wave are done, skipped, or aborted):
@@ -791,6 +811,31 @@ If `git.branching` is `phase`:
791
811
  - If "Yes, merge": complete the merge and delete the phase branch
792
812
  - If "No, keep" or "Other": leave the branch as-is and inform the user
793
813
 
814
+ **8d-ii. PR Creation (when branching enabled):**
815
+
816
+ If `config.git.branching` is `phase` or `milestone` AND phase verification passed:
817
+
818
+ 1. Push the phase branch: `git push -u origin {branch-name}`
819
+ 2. If `config.git.auto_pr` is `true`:
820
+ - Run: `gh pr create --title "feat({phase-scope}): {phase-slug}" --body "$(cat <<'EOF'
821
+ ## Phase {N}: {phase name}
822
+
823
+ **Goal**: {phase goal from ROADMAP.md}
824
+
825
+ ### Key Files
826
+ {key_files from SUMMARY.md, bulleted}
827
+
828
+ ### Verification
829
+ {pass/fail status from VERIFICATION.md}
830
+
831
+ ---
832
+ Generated by Plan-Build-Run
833
+ EOF
834
+ )"`
835
+ 3. If `config.git.auto_pr` is `false`:
836
+ - Use AskUserQuestion to ask: "Phase branch pushed. Create a PR?"
837
+ - Options: Yes (create PR as above) / No / Later (skip)
838
+
794
839
  **8e. Auto-advance / auto-continue (conditional):**
795
840
 
796
841
  **If `features.auto_advance` is `true` AND `mode` is `autonomous`:**
@@ -119,6 +119,19 @@ When a knowledge gap emerges during the conversation — you're unsure about a l
119
119
 
120
120
  Display to the user: `◐ Spawning researcher...`
121
121
 
122
+ **Learnings injection (opt-in):** Check for relevant tech stack learnings:
123
+
124
+ ```bash
125
+ node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "stack,tech" 2>/dev/null
126
+ ```
127
+
128
+ If non-empty JSON array returned:
129
+
130
+ - Write to temp file: `node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "stack,tech" > /tmp/pbr-learnings-$$.md`
131
+ - Note path as `{learnings_temp_path}`; add as item 3 in the researcher's `files_to_read` block below
132
+
133
+ If no learnings or command fails: omit the extra files_to_read entry.
134
+
122
135
  ```
123
136
  Task({
124
137
  subagent_type: "pbr:researcher",
@@ -126,6 +139,7 @@ Task({
126
139
  CRITICAL: Read these files BEFORE any other action:
127
140
  1. .planning/CONTEXT.md — locked decisions and constraints (if exists)
128
141
  2. .planning/STATE.md — current project state (if exists)
142
+ {if learnings_temp_path exists}3. {learnings_temp_path} — cross-project learnings (tech stack patterns from past PBR projects){/if}
129
143
  </files_to_read>
130
144
  <research_assignment>
131
145
  Topic: {specific research question}
@@ -139,6 +153,8 @@ Task({
139
153
  })
140
154
  ```
141
155
 
156
+ If `{learnings_temp_path}` was produced above, replace `{if...}{/if}` with the actual line. If no learnings were found, omit item 3 entirely.
157
+
142
158
  After the researcher completes, check for completion markers in the Task() output:
143
159
 
144
160
  - If `## RESEARCH COMPLETE` is present: proceed normally
@@ -417,6 +417,28 @@ Archive a completed milestone and prepare for the next one.
417
417
  - Key deliverables: {summary from Step 4}
418
418
  ```
419
419
 
420
+ 7d. **Aggregate learnings from milestone phases:**
421
+
422
+ **CRITICAL: Run learnings aggregation NOW. Do NOT skip this step.**
423
+
424
+ ```bash
425
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/milestone-learnings.js .planning/milestones/{version} --project {project-name-from-STATE.md}
426
+ ```
427
+
428
+ - If the script outputs an error, log it but do NOT abort milestone completion — learnings aggregation is advisory.
429
+ - Display the aggregation summary line to the user (e.g., "Learnings aggregated: 12 new, 3 updated, 0 errors").
430
+ - After aggregation, check for triggered deferral thresholds:
431
+
432
+ ```bash
433
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js learnings check-thresholds
434
+ ```
435
+
436
+ If any thresholds are triggered, display each as a notification:
437
+
438
+ ```
439
+ Note: Learnings threshold met — {key}: {trigger}. Consider implementing the deferred feature.
440
+ ```
441
+
420
442
  8. **Git tag:**
421
443
  ```bash
422
444
  git tag -a {version} -m "Milestone: {name}"
@@ -428,6 +450,38 @@ Archive a completed milestone and prepare for the next one.
428
450
  git commit -m "docs(planning): complete milestone {version}"
429
451
  ```
430
452
 
453
+ 9b. **Push milestone to remote:**
454
+
455
+ Use AskUserQuestion to ask the user how they want to publish the milestone:
456
+
457
+ ```
458
+ question: "How should this milestone be published to GitHub?"
459
+ header: "Publish"
460
+ options:
461
+ - label: "Push tag + commits" description: "Push the v{version} tag and any unpushed commits to origin"
462
+ - label: "Skip for now" description: "Keep everything local — push later manually"
463
+ ```
464
+
465
+ - If "Push tag + commits": run `git push origin main --follow-tags` to push both commits and the annotated tag in one command. Display success or error.
466
+ - If "Skip for now": display reminder: "Tag v{version} is local only. Push when ready: `git push origin main --follow-tags`"
467
+ - If "Other": follow user instructions (e.g., create a PR, push to a different branch, etc.)
468
+
469
+ ### Post-Completion Smoke Test
470
+
471
+ If `config.deployment.smoke_test_command` is set and non-empty:
472
+
473
+ 1. Run the command via Bash
474
+ 2. If exit code 0: display "Smoke test passed" with command output
475
+ 3. If exit code non-zero: display advisory warning:
476
+
477
+ ```
478
+ ⚠ Smoke test failed (exit code {N})
479
+ Command: {smoke_test_command}
480
+ Output: {first 20 lines of output}
481
+ ```
482
+
483
+ This is advisory only — the milestone is already archived. Surface it as a potential issue for the user to investigate.
484
+
431
485
  10. **Confirm** with branded output:
432
486
  ```
433
487
  ╔══════════════════════════════════════════════════════════════╗
@@ -350,6 +350,24 @@ If `--teams` is NOT set and `config.parallelization.use_teams` is false or unset
350
350
 
351
351
  #### Single-Planner Flow (default)
352
352
 
353
+ **Learnings injection (opt-in):** Check for planning and estimation learnings before spawning the planner:
354
+
355
+ ```bash
356
+ node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "estimation,planning,process" 2>/dev/null
357
+ ```
358
+
359
+ If non-empty JSON array returned:
360
+
361
+ - Write to temp file and note as `{learnings_temp_path}`:
362
+
363
+ ```bash
364
+ node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "estimation,planning,process" > /tmp/pbr-learnings-$$.md
365
+ ```
366
+
367
+ - Add as an additional `files_to_read` item in the planner prompt below
368
+
369
+ If no learnings or command fails: omit.
370
+
353
371
  Display to the user: `◐ Spawning planner...`
354
372
 
355
373
  Spawn the planner Task() with all context inlined:
@@ -370,6 +388,7 @@ After planner completes, check for completion markers: `## PLANNING COMPLETE`, `
370
388
  #### Planning Prompt Template
371
389
 
372
390
  Read `skills/plan/templates/planner-prompt.md.tmpl` and use it as the prompt template for spawning the planner agent. Fill in all placeholder blocks with phase-specific context:
391
+
373
392
  - `<phase_context>` - phase number, directory, goal, requirements, dependencies, success criteria
374
393
  - `<project_context>` - locked decisions, user constraints, deferred ideas, phase-specific decisions
375
394
  - `<prior_work>` - manifest table of preceding phase SUMMARY.md file paths with status and one-line exports (NOT full bodies)
@@ -378,15 +397,19 @@ Read `skills/plan/templates/planner-prompt.md.tmpl` and use it as the prompt tem
378
397
  - `<planning_instructions>` - phase-specific planning rules and output path
379
398
 
380
399
  **Prepend this block to the planner prompt before sending:**
400
+
381
401
  ```
382
402
  <files_to_read>
383
403
  CRITICAL: Read these files BEFORE any other action:
384
404
  1. .planning/CONTEXT.md — locked decisions and constraints (if exists)
385
405
  2. .planning/ROADMAP.md — phase goals, dependencies, and structure
386
406
  3. .planning/phases/{NN}-{slug}/RESEARCH.md — research findings (if exists)
407
+ {if learnings_temp_path exists}4. {learnings_temp_path} — cross-project learnings (estimation and planning patterns from past PBR projects){/if}
387
408
  </files_to_read>
388
409
  ```
389
410
 
411
+ If `{learnings_temp_path}` was produced in the learnings injection step above, replace `{if...}{/if}` with the actual line. If no learnings were found, omit item 4 entirely.
412
+
390
413
  Wait for the planner to complete.
391
414
 
392
415
  After the planner returns, read the plan files it created to extract counts. Display a completion summary:
@@ -0,0 +1,22 @@
1
+ ## Phase <%= phase_number %>: <%= phase_name %>
2
+
3
+ **Goal**: <%= phase_goal %>
4
+
5
+ ### Key Files Changed
6
+ <% key_files.forEach(f => { %>
7
+ - `<%= f %>`
8
+ <% }) %>
9
+
10
+ ### Verification
11
+ - Status: <%= verification_status %>
12
+ - Must-haves: <%= must_haves_passed %>/<%= must_haves_total %> passed
13
+
14
+ <% if (closes_issues && closes_issues.length > 0) { %>
15
+ ### Issues
16
+ <% closes_issues.forEach(n => { %>
17
+ Closes #<%= n %>
18
+ <% }) %>
19
+ <% } %>
20
+
21
+ ---
22
+ *Generated by [Plan-Build-Run](https://github.com/SienkLogic/plan-build-run)*