@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.
- package/CHANGELOG.md +26 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/agents/executor.agent.md +13 -0
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/references/config-reference.md +22 -0
- package/plugins/copilot-pbr/references/git-integration.md +30 -0
- package/plugins/copilot-pbr/references/plan-format.md +4 -0
- package/plugins/copilot-pbr/skills/begin/SKILL.md +22 -0
- package/plugins/copilot-pbr/skills/build/SKILL.md +45 -0
- package/plugins/copilot-pbr/skills/explore/SKILL.md +17 -0
- package/plugins/copilot-pbr/skills/milestone/SKILL.md +54 -0
- package/plugins/copilot-pbr/templates/pr-body.md.tmpl +22 -0
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/agents/executor.md +13 -0
- package/plugins/cursor-pbr/references/config-reference.md +22 -0
- package/plugins/cursor-pbr/references/git-integration.md +30 -0
- package/plugins/cursor-pbr/references/plan-format.md +4 -0
- package/plugins/cursor-pbr/skills/begin/SKILL.md +22 -0
- package/plugins/cursor-pbr/skills/build/SKILL.md +45 -0
- package/plugins/cursor-pbr/skills/explore/SKILL.md +17 -0
- package/plugins/cursor-pbr/skills/milestone/SKILL.md +54 -0
- package/plugins/cursor-pbr/templates/pr-body.md.tmpl +22 -0
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/agents/executor.md +13 -0
- package/plugins/pbr/references/config-reference.md +22 -0
- package/plugins/pbr/references/git-integration.md +30 -0
- package/plugins/pbr/references/plan-format.md +4 -0
- package/plugins/pbr/scripts/lib/learnings.js +312 -0
- package/plugins/pbr/scripts/milestone-learnings.js +290 -0
- package/plugins/pbr/scripts/pbr-tools.js +43 -1
- package/plugins/pbr/scripts/progress-tracker.js +24 -1
- package/plugins/pbr/skills/begin/SKILL.md +23 -0
- package/plugins/pbr/skills/build/SKILL.md +45 -0
- package/plugins/pbr/skills/explore/SKILL.md +16 -0
- package/plugins/pbr/skills/milestone/SKILL.md +54 -0
- package/plugins/pbr/skills/plan/SKILL.md +23 -0
- 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)*
|