@sienklogic/plan-build-run 2.53.0 → 2.55.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 +34 -0
- package/package.json +2 -2
- package/plugins/codex-pbr/agents/audit.md +223 -0
- package/plugins/codex-pbr/agents/codebase-mapper.md +196 -0
- package/plugins/codex-pbr/agents/debugger.md +245 -0
- package/plugins/codex-pbr/agents/dev-sync.md +142 -0
- package/plugins/codex-pbr/agents/executor.md +429 -0
- package/plugins/codex-pbr/agents/general.md +131 -0
- package/plugins/codex-pbr/agents/integration-checker.md +178 -0
- package/plugins/codex-pbr/agents/plan-checker.md +253 -0
- package/plugins/codex-pbr/agents/planner.md +343 -0
- package/plugins/codex-pbr/agents/researcher.md +253 -0
- package/plugins/codex-pbr/agents/synthesizer.md +183 -0
- package/plugins/codex-pbr/agents/verifier.md +352 -0
- package/plugins/codex-pbr/commands/audit.md +5 -0
- package/plugins/codex-pbr/commands/begin.md +5 -0
- package/plugins/codex-pbr/commands/build.md +5 -0
- package/plugins/codex-pbr/commands/config.md +5 -0
- package/plugins/codex-pbr/commands/continue.md +5 -0
- package/plugins/codex-pbr/commands/dashboard.md +5 -0
- package/plugins/codex-pbr/commands/debug.md +5 -0
- package/plugins/codex-pbr/commands/discuss.md +5 -0
- package/plugins/codex-pbr/commands/do.md +5 -0
- package/plugins/codex-pbr/commands/explore.md +5 -0
- package/plugins/codex-pbr/commands/health.md +5 -0
- package/plugins/codex-pbr/commands/help.md +5 -0
- package/plugins/codex-pbr/commands/import.md +5 -0
- package/plugins/codex-pbr/commands/milestone.md +5 -0
- package/plugins/codex-pbr/commands/note.md +5 -0
- package/plugins/codex-pbr/commands/pause.md +5 -0
- package/plugins/codex-pbr/commands/plan.md +5 -0
- package/plugins/codex-pbr/commands/quick.md +5 -0
- package/plugins/codex-pbr/commands/resume.md +5 -0
- package/plugins/codex-pbr/commands/review.md +5 -0
- package/plugins/codex-pbr/commands/scan.md +5 -0
- package/plugins/codex-pbr/commands/setup.md +5 -0
- package/plugins/codex-pbr/commands/status.md +5 -0
- package/plugins/codex-pbr/commands/statusline.md +5 -0
- package/plugins/codex-pbr/commands/test.md +5 -0
- package/plugins/codex-pbr/commands/todo.md +5 -0
- package/plugins/codex-pbr/commands/undo.md +5 -0
- package/plugins/codex-pbr/references/agent-contracts.md +324 -0
- package/plugins/codex-pbr/references/agent-teams.md +54 -0
- package/plugins/codex-pbr/references/common-bug-patterns.md +13 -0
- package/plugins/codex-pbr/references/config-reference.md +552 -0
- package/plugins/codex-pbr/references/continuation-format.md +212 -0
- package/plugins/codex-pbr/references/deviation-rules.md +112 -0
- package/plugins/codex-pbr/references/git-integration.md +256 -0
- package/plugins/codex-pbr/references/integration-patterns.md +117 -0
- package/plugins/codex-pbr/references/model-profiles.md +99 -0
- package/plugins/codex-pbr/references/model-selection.md +31 -0
- package/plugins/codex-pbr/references/pbr-tools-cli.md +400 -0
- package/plugins/codex-pbr/references/plan-authoring.md +246 -0
- package/plugins/codex-pbr/references/plan-format.md +313 -0
- package/plugins/codex-pbr/references/questioning.md +235 -0
- package/plugins/codex-pbr/references/reading-verification.md +127 -0
- package/plugins/codex-pbr/references/signal-files.md +41 -0
- package/plugins/codex-pbr/references/stub-patterns.md +160 -0
- package/plugins/codex-pbr/references/ui-formatting.md +444 -0
- package/plugins/codex-pbr/references/wave-execution.md +95 -0
- package/plugins/codex-pbr/skills/audit/SKILL.md +346 -0
- package/plugins/codex-pbr/skills/begin/SKILL.md +800 -0
- package/plugins/codex-pbr/skills/build/SKILL.md +958 -0
- package/plugins/codex-pbr/skills/config/SKILL.md +267 -0
- package/plugins/codex-pbr/skills/continue/SKILL.md +172 -0
- package/plugins/codex-pbr/skills/dashboard/SKILL.md +44 -0
- package/plugins/codex-pbr/skills/debug/SKILL.md +530 -0
- package/plugins/codex-pbr/skills/discuss/SKILL.md +355 -0
- package/plugins/codex-pbr/skills/do/SKILL.md +68 -0
- package/plugins/codex-pbr/skills/explore/SKILL.md +407 -0
- package/plugins/codex-pbr/skills/health/SKILL.md +300 -0
- package/plugins/codex-pbr/skills/help/SKILL.md +229 -0
- package/plugins/codex-pbr/skills/import/SKILL.md +538 -0
- package/plugins/codex-pbr/skills/milestone/SKILL.md +620 -0
- package/plugins/codex-pbr/skills/note/SKILL.md +215 -0
- package/plugins/codex-pbr/skills/pause/SKILL.md +258 -0
- package/plugins/codex-pbr/skills/plan/SKILL.md +650 -0
- package/plugins/codex-pbr/skills/quick/SKILL.md +417 -0
- package/plugins/codex-pbr/skills/resume/SKILL.md +403 -0
- package/plugins/codex-pbr/skills/review/SKILL.md +669 -0
- package/plugins/codex-pbr/skills/scan/SKILL.md +325 -0
- package/plugins/codex-pbr/skills/setup/SKILL.md +169 -0
- package/plugins/codex-pbr/skills/shared/commit-planning-docs.md +35 -0
- package/plugins/codex-pbr/skills/shared/config-loading.md +102 -0
- package/plugins/codex-pbr/skills/shared/context-budget.md +77 -0
- package/plugins/codex-pbr/skills/shared/context-loader-task.md +86 -0
- package/plugins/codex-pbr/skills/shared/digest-select.md +79 -0
- package/plugins/codex-pbr/skills/shared/domain-probes.md +125 -0
- package/plugins/codex-pbr/skills/shared/error-reporting.md +59 -0
- package/plugins/codex-pbr/skills/shared/gate-prompts.md +388 -0
- package/plugins/codex-pbr/skills/shared/phase-argument-parsing.md +45 -0
- package/plugins/codex-pbr/skills/shared/revision-loop.md +81 -0
- package/plugins/codex-pbr/skills/shared/state-update.md +169 -0
- package/plugins/codex-pbr/skills/shared/universal-anti-patterns.md +43 -0
- package/plugins/codex-pbr/skills/status/SKILL.md +449 -0
- package/plugins/codex-pbr/skills/statusline/SKILL.md +149 -0
- package/plugins/codex-pbr/skills/test/SKILL.md +210 -0
- package/plugins/codex-pbr/skills/todo/SKILL.md +281 -0
- package/plugins/codex-pbr/skills/undo/SKILL.md +172 -0
- package/plugins/codex-pbr/templates/CONTEXT.md.tmpl +52 -0
- package/plugins/codex-pbr/templates/INTEGRATION-REPORT.md.tmpl +167 -0
- package/plugins/codex-pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -0
- package/plugins/codex-pbr/templates/ROADMAP.md.tmpl +47 -0
- package/plugins/codex-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
- package/plugins/codex-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
- package/plugins/codex-pbr/templates/SUMMARY.md.tmpl +81 -0
- package/plugins/codex-pbr/templates/VERIFICATION-DETAIL.md.tmpl +117 -0
- package/plugins/codex-pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -0
- package/plugins/codex-pbr/templates/codebase/CONCERNS.md.tmpl +93 -0
- package/plugins/codex-pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -0
- package/plugins/codex-pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -0
- package/plugins/codex-pbr/templates/codebase/STACK.md.tmpl +78 -0
- package/plugins/codex-pbr/templates/codebase/STRUCTURE.md.tmpl +80 -0
- package/plugins/codex-pbr/templates/codebase/TESTING.md.tmpl +107 -0
- package/plugins/codex-pbr/templates/continue-here.md.tmpl +73 -0
- package/plugins/codex-pbr/templates/pr-body.md.tmpl +22 -0
- package/plugins/codex-pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -0
- package/plugins/codex-pbr/templates/research/ARCHITECTURE.md.tmpl +124 -0
- package/plugins/codex-pbr/templates/research/STACK.md.tmpl +71 -0
- package/plugins/codex-pbr/templates/research/SUMMARY.md.tmpl +112 -0
- package/plugins/codex-pbr/templates/research-outputs/phase-research.md.tmpl +81 -0
- package/plugins/codex-pbr/templates/research-outputs/project-research.md.tmpl +99 -0
- package/plugins/codex-pbr/templates/research-outputs/synthesis.md.tmpl +36 -0
- package/plugins/copilot-pbr/commands/setup.md +1 -1
- package/plugins/copilot-pbr/commands/undo.md +5 -0
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/skills/begin/SKILL.md +170 -17
- package/plugins/copilot-pbr/skills/build/SKILL.md +73 -8
- package/plugins/copilot-pbr/skills/plan/SKILL.md +67 -17
- package/plugins/copilot-pbr/skills/review/SKILL.md +12 -1
- package/plugins/copilot-pbr/skills/setup/SKILL.md +66 -214
- package/plugins/copilot-pbr/skills/shared/context-budget.md +27 -0
- package/plugins/copilot-pbr/skills/status/SKILL.md +44 -2
- package/plugins/copilot-pbr/skills/undo/SKILL.md +172 -0
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/commands/setup.md +1 -1
- package/plugins/cursor-pbr/commands/undo.md +5 -0
- package/plugins/cursor-pbr/skills/begin/SKILL.md +170 -17
- package/plugins/cursor-pbr/skills/build/SKILL.md +73 -8
- package/plugins/cursor-pbr/skills/plan/SKILL.md +67 -17
- package/plugins/cursor-pbr/skills/review/SKILL.md +12 -1
- package/plugins/cursor-pbr/skills/setup/SKILL.md +66 -214
- package/plugins/cursor-pbr/skills/shared/context-budget.md +27 -0
- package/plugins/cursor-pbr/skills/status/SKILL.md +44 -2
- package/plugins/cursor-pbr/skills/undo/SKILL.md +173 -0
- package/plugins/jules-pbr/AGENTS.md +600 -0
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/commands/setup.md +1 -1
- package/plugins/pbr/commands/undo.md +5 -0
- package/plugins/pbr/scripts/config-schema.json +5 -1
- package/plugins/pbr/scripts/lib/alternatives.js +203 -0
- package/plugins/pbr/scripts/lib/preview.js +174 -0
- package/plugins/pbr/scripts/lib/skill-section.js +99 -0
- package/plugins/pbr/scripts/lib/step-verify.js +149 -0
- package/plugins/pbr/scripts/pbr-tools.js +122 -2
- package/plugins/pbr/scripts/validate-commit.js +2 -2
- package/plugins/pbr/skills/begin/SKILL.md +170 -17
- package/plugins/pbr/skills/begin/templates/config.json.tmpl +5 -1
- package/plugins/pbr/skills/build/SKILL.md +73 -8
- package/plugins/pbr/skills/plan/SKILL.md +67 -17
- package/plugins/pbr/skills/review/SKILL.md +12 -1
- package/plugins/pbr/skills/setup/SKILL.md +66 -214
- package/plugins/pbr/skills/shared/context-budget.md +27 -0
- package/plugins/pbr/skills/status/SKILL.md +44 -2
- package/plugins/pbr/skills/undo/SKILL.md +174 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* alternatives.js — Conversational error recovery helpers for PBR skills.
|
|
5
|
+
*
|
|
6
|
+
* Provides structured JSON responses for three error scenarios:
|
|
7
|
+
* phaseAlternatives(slug, planningDir) — phase-not-found recovery
|
|
8
|
+
* prerequisiteAlternatives(phase, planningDir) — missing-prereq recovery
|
|
9
|
+
* configAlternatives(field, value, planningDir) — config-invalid recovery
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Known config fields and their valid values
|
|
16
|
+
const KNOWN_CONFIG_FIELDS = {
|
|
17
|
+
'depth': ['quick', 'standard', 'deep'],
|
|
18
|
+
'git.branching': ['phase', 'main', 'off'],
|
|
19
|
+
'gates.confirm_plan': [true, false, 'true', 'false'],
|
|
20
|
+
'gates.confirm_execute': [true, false, 'true', 'false'],
|
|
21
|
+
'gates.confirm_review': [true, false, 'true', 'false'],
|
|
22
|
+
'gates.confirm_milestone': [true, false, 'true', 'false'],
|
|
23
|
+
'parallelism': ['off', 'wave', 'full'],
|
|
24
|
+
'models.default': ['haiku', 'sonnet', 'inherit'],
|
|
25
|
+
'models.planner': ['haiku', 'sonnet', 'inherit'],
|
|
26
|
+
'models.executor': ['haiku', 'sonnet', 'inherit'],
|
|
27
|
+
'models.verifier': ['haiku', 'sonnet', 'inherit']
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Score similarity between two strings using character overlap.
|
|
32
|
+
* Returns a value between 0 and 1 (higher = more similar).
|
|
33
|
+
*
|
|
34
|
+
* @param {string} a - First string
|
|
35
|
+
* @param {string} b - Second string
|
|
36
|
+
* @returns {number} Similarity score between 0 and 1
|
|
37
|
+
*/
|
|
38
|
+
function scoreSlug(a, b) {
|
|
39
|
+
if (!a || !b) return 0;
|
|
40
|
+
const lowerA = a.toLowerCase();
|
|
41
|
+
const lowerB = b.toLowerCase();
|
|
42
|
+
// Count shared characters (by frequency)
|
|
43
|
+
const freqA = {};
|
|
44
|
+
for (const ch of lowerA) freqA[ch] = (freqA[ch] || 0) + 1;
|
|
45
|
+
let shared = 0;
|
|
46
|
+
const freqB = {};
|
|
47
|
+
for (const ch of lowerB) freqB[ch] = (freqB[ch] || 0) + 1;
|
|
48
|
+
for (const ch of Object.keys(freqA)) {
|
|
49
|
+
if (freqB[ch]) shared += Math.min(freqA[ch], freqB[ch]);
|
|
50
|
+
}
|
|
51
|
+
// Also boost score when one string contains the other as substring
|
|
52
|
+
let substringBonus = 0;
|
|
53
|
+
if (lowerB.includes(lowerA) || lowerA.includes(lowerB)) substringBonus = 0.2;
|
|
54
|
+
const score = shared / Math.max(lowerA.length, lowerB.length) + substringBonus;
|
|
55
|
+
return Math.min(score, 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate alternatives for a phase-not-found error.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} slug - The unknown phase slug
|
|
62
|
+
* @param {string} planningDir - Path to .planning directory
|
|
63
|
+
* @returns {{ error_type: string, slug: string, available: string[], suggestions: string[] }}
|
|
64
|
+
*/
|
|
65
|
+
function phaseAlternatives(slug, planningDir) {
|
|
66
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
67
|
+
let available = [];
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (fs.existsSync(phasesDir)) {
|
|
71
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
72
|
+
available = entries
|
|
73
|
+
.filter(e => e.isDirectory())
|
|
74
|
+
.map(e => e.name);
|
|
75
|
+
}
|
|
76
|
+
} catch (_e) {
|
|
77
|
+
// If we can't read, return empty
|
|
78
|
+
available = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let suggestions = [];
|
|
82
|
+
if (slug && slug.length > 0 && available.length > 0) {
|
|
83
|
+
const scored = available
|
|
84
|
+
.map(name => ({ name, score: scoreSlug(slug, name) }))
|
|
85
|
+
.filter(s => s.score > 0.3)
|
|
86
|
+
.sort((a, b) => b.score - a.score)
|
|
87
|
+
.slice(0, 3)
|
|
88
|
+
.map(s => s.name);
|
|
89
|
+
suggestions = scored;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
error_type: 'phase-not-found',
|
|
94
|
+
slug,
|
|
95
|
+
available,
|
|
96
|
+
suggestions
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate alternatives for a missing-prerequisite error.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} phase - The phase slug to check
|
|
104
|
+
* @param {string} planningDir - Path to .planning directory
|
|
105
|
+
* @returns {{ error_type: string, phase: string, existing_summaries: string[], missing_summaries: string[], suggested_action: string }}
|
|
106
|
+
*/
|
|
107
|
+
function prerequisiteAlternatives(phase, planningDir) {
|
|
108
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
109
|
+
|
|
110
|
+
// Find the phase directory (exact match or prefix match)
|
|
111
|
+
let phaseDir = null;
|
|
112
|
+
try {
|
|
113
|
+
if (fs.existsSync(phasesDir)) {
|
|
114
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
115
|
+
const match = entries.find(e => e.isDirectory() && (e.name === phase || e.name.endsWith('-' + phase) || e.name === phase));
|
|
116
|
+
if (match) {
|
|
117
|
+
phaseDir = path.join(phasesDir, match.name);
|
|
118
|
+
} else {
|
|
119
|
+
// Try exact slug match
|
|
120
|
+
const exact = path.join(phasesDir, phase);
|
|
121
|
+
if (fs.existsSync(exact)) phaseDir = exact;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (_e) { /* best effort */ }
|
|
125
|
+
|
|
126
|
+
const existing_summaries = [];
|
|
127
|
+
const missing_summaries = [];
|
|
128
|
+
|
|
129
|
+
if (phaseDir && fs.existsSync(phaseDir)) {
|
|
130
|
+
try {
|
|
131
|
+
const files = fs.readdirSync(phaseDir);
|
|
132
|
+
const planFiles = files.filter(f => /^PLAN-\d+\.md$/i.test(f));
|
|
133
|
+
|
|
134
|
+
for (const planFile of planFiles) {
|
|
135
|
+
// Extract plan ID from filename (e.g. PLAN-01.md → look for SUMMARY-{phase-num}-01.md)
|
|
136
|
+
const match = planFile.match(/^PLAN-(\d+)\.md$/i);
|
|
137
|
+
if (!match) continue;
|
|
138
|
+
const planNum = match[1];
|
|
139
|
+
|
|
140
|
+
// Check for any SUMMARY file matching this plan number
|
|
141
|
+
const summaryFiles = files.filter(f => {
|
|
142
|
+
const sm = f.match(/^SUMMARY[-.](.*?)\.md$/i);
|
|
143
|
+
if (!sm) return false;
|
|
144
|
+
return sm[1].endsWith('-' + planNum) || sm[1] === planNum;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (summaryFiles.length > 0) {
|
|
148
|
+
existing_summaries.push(summaryFiles[0]);
|
|
149
|
+
} else {
|
|
150
|
+
// Infer the expected plan ID from directory name prefix + plan number
|
|
151
|
+
const dirMatch = (phaseDir.split(path.sep).pop() || '').match(/^(\d+)-/);
|
|
152
|
+
const phaseNum = dirMatch ? dirMatch[1] : '';
|
|
153
|
+
const expectedId = phaseNum ? `${phaseNum}-${planNum}` : planNum;
|
|
154
|
+
missing_summaries.push(`SUMMARY-${expectedId}.md`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (_e) { /* best effort */ }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
error_type: 'missing-prereq',
|
|
162
|
+
phase,
|
|
163
|
+
existing_summaries,
|
|
164
|
+
missing_summaries,
|
|
165
|
+
suggested_action: `Run /pbr:build ${phase} to generate missing summaries`
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate alternatives for a config-invalid error.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} field - The config field name
|
|
173
|
+
* @param {string} value - The current invalid value
|
|
174
|
+
* @param {string} _planningDir - Path to .planning directory (unused, for API consistency)
|
|
175
|
+
* @returns {{ error_type: string, field: string, current_value: string, valid_values: Array, suggested_fix: string }}
|
|
176
|
+
*/
|
|
177
|
+
function configAlternatives(field, value, _planningDir) {
|
|
178
|
+
const knownValues = KNOWN_CONFIG_FIELDS[field];
|
|
179
|
+
|
|
180
|
+
if (knownValues !== undefined) {
|
|
181
|
+
// Convert to string representations for JSON output
|
|
182
|
+
const validStrings = knownValues
|
|
183
|
+
.filter(v => typeof v === 'string')
|
|
184
|
+
.filter((v, i, arr) => arr.indexOf(v) === i); // dedupe
|
|
185
|
+
return {
|
|
186
|
+
error_type: 'config-invalid',
|
|
187
|
+
field,
|
|
188
|
+
current_value: value,
|
|
189
|
+
valid_values: validStrings.length > 0 ? validStrings : knownValues.filter(v => typeof v !== 'boolean'),
|
|
190
|
+
suggested_fix: `Set ${field} to one of: ${validStrings.join(', ')}`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
error_type: 'config-invalid',
|
|
196
|
+
field,
|
|
197
|
+
current_value: value,
|
|
198
|
+
valid_values: [],
|
|
199
|
+
suggested_fix: 'Remove this field from config.json or check spelling'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { phaseAlternatives, prerequisiteAlternatives, configAlternatives };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/preview.js — Dry-run preview for /pbr:build and /pbr:plan.
|
|
5
|
+
*
|
|
6
|
+
* Reads PLAN.md frontmatter from a phase directory, aggregates file lists,
|
|
7
|
+
* counts task tags, and builds a structured preview object without executing
|
|
8
|
+
* any agents or making any state changes.
|
|
9
|
+
*
|
|
10
|
+
* Exports: buildPreview(phaseSlug, options, planningDir, pluginRoot)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { parseYamlFrontmatter, findFiles } = require('./core');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Group an array of plan objects by their wave number.
|
|
19
|
+
* Returns an array of { wave, plans, parallel } sorted ascending by wave.
|
|
20
|
+
*
|
|
21
|
+
* parallel = true when the wave contains more than one plan.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array<object>} plans
|
|
24
|
+
* @returns {Array<{wave: number, plans: Array<object>, parallel: boolean}>}
|
|
25
|
+
*/
|
|
26
|
+
function groupByWave(plans) {
|
|
27
|
+
const waveMap = new Map();
|
|
28
|
+
for (const plan of plans) {
|
|
29
|
+
const waveNum = typeof plan.wave === 'number' ? plan.wave : 1;
|
|
30
|
+
if (!waveMap.has(waveNum)) {
|
|
31
|
+
waveMap.set(waveNum, []);
|
|
32
|
+
}
|
|
33
|
+
waveMap.get(waveNum).push(plan);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Array.from(waveMap.entries())
|
|
37
|
+
.sort(([a], [b]) => a - b)
|
|
38
|
+
.map(([wave, wavePlans]) => ({
|
|
39
|
+
wave,
|
|
40
|
+
plans: wavePlans,
|
|
41
|
+
parallel: wavePlans.length > 1
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a preview object for a phase without executing any agents.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} phaseSlug - Phase slug (partial match, e.g. "advanced-orchestrator-features" or "56-advanced-...")
|
|
49
|
+
* @param {object} _options - Reserved for future options (currently unused)
|
|
50
|
+
* @param {string} planningDir - Absolute path to the .planning/ directory
|
|
51
|
+
* @param {string} _pluginRoot - Plugin root (passed for API consistency, unused here)
|
|
52
|
+
* @returns {object} Preview data or { error: string }
|
|
53
|
+
*/
|
|
54
|
+
function buildPreview(phaseSlug, _options, planningDir, _pluginRoot) {
|
|
55
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
56
|
+
|
|
57
|
+
// Find the phase directory
|
|
58
|
+
let phaseDir = null;
|
|
59
|
+
let phaseDirName = null;
|
|
60
|
+
try {
|
|
61
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry.isDirectory()) continue;
|
|
64
|
+
if (entry.name === phaseSlug || entry.name.endsWith('-' + phaseSlug) || entry.name.includes(phaseSlug)) {
|
|
65
|
+
phaseDir = path.join(phasesDir, entry.name);
|
|
66
|
+
phaseDirName = entry.name;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (_e) {
|
|
71
|
+
return { error: `Phase not found: ${phaseSlug}` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!phaseDir) {
|
|
75
|
+
return { error: `Phase not found: ${phaseSlug}` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Find all PLAN-*.md files
|
|
79
|
+
const planFiles = findFiles(phaseDir, /^PLAN.*\.md$/i);
|
|
80
|
+
|
|
81
|
+
if (planFiles.length === 0) {
|
|
82
|
+
return {
|
|
83
|
+
phase: phaseDirName,
|
|
84
|
+
plans: [],
|
|
85
|
+
waves: [],
|
|
86
|
+
files_affected: [],
|
|
87
|
+
agent_count: 0,
|
|
88
|
+
critical_path: [],
|
|
89
|
+
dependency_chain: []
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse each plan file
|
|
94
|
+
const plans = [];
|
|
95
|
+
for (const filename of planFiles) {
|
|
96
|
+
const filePath = path.join(phaseDir, filename);
|
|
97
|
+
let content = '';
|
|
98
|
+
try {
|
|
99
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
100
|
+
} catch (_e) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fm = parseYamlFrontmatter(content);
|
|
105
|
+
|
|
106
|
+
// Count task tags by matching "task id=" occurrences
|
|
107
|
+
const taskMatches = content.match(/task id=/g);
|
|
108
|
+
const taskCount = taskMatches ? taskMatches.length : 0;
|
|
109
|
+
|
|
110
|
+
// Normalize wave to a number
|
|
111
|
+
const wave = typeof fm.wave === 'number' ? fm.wave : (parseInt(fm.wave, 10) || 1);
|
|
112
|
+
|
|
113
|
+
// Normalize depends_on to an array
|
|
114
|
+
let dependsOn = fm.depends_on;
|
|
115
|
+
if (!dependsOn) {
|
|
116
|
+
dependsOn = [];
|
|
117
|
+
} else if (!Array.isArray(dependsOn)) {
|
|
118
|
+
dependsOn = [dependsOn];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Normalize files_modified to an array
|
|
122
|
+
let filesModified = fm.files_modified;
|
|
123
|
+
if (!filesModified) {
|
|
124
|
+
filesModified = [];
|
|
125
|
+
} else if (!Array.isArray(filesModified)) {
|
|
126
|
+
filesModified = [filesModified];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
plans.push({
|
|
130
|
+
id: fm.plan || filename.replace(/\.md$/i, ''),
|
|
131
|
+
wave,
|
|
132
|
+
depends_on: dependsOn,
|
|
133
|
+
files_modified: filesModified,
|
|
134
|
+
task_count: taskCount
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Group plans by wave
|
|
139
|
+
const waves = groupByWave(plans);
|
|
140
|
+
|
|
141
|
+
// Aggregate files_affected: union of all files_modified, deduplicated and sorted
|
|
142
|
+
const filesSet = new Set();
|
|
143
|
+
for (const plan of plans) {
|
|
144
|
+
for (const f of plan.files_modified) {
|
|
145
|
+
filesSet.add(f);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const files_affected = Array.from(filesSet).sort();
|
|
149
|
+
|
|
150
|
+
// Sum agent_count
|
|
151
|
+
const agent_count = plans.reduce((sum, p) => sum + p.task_count, 0);
|
|
152
|
+
|
|
153
|
+
// Critical path: first plan ID from each wave in order
|
|
154
|
+
const critical_path = waves.map(w => w.plans[0].id);
|
|
155
|
+
|
|
156
|
+
// Dependency chain: [{id, wave, depends_on}] for all plans
|
|
157
|
+
const dependency_chain = plans.map(p => ({
|
|
158
|
+
id: p.id,
|
|
159
|
+
wave: p.wave,
|
|
160
|
+
depends_on: p.depends_on
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
phase: phaseDirName,
|
|
165
|
+
plans,
|
|
166
|
+
waves,
|
|
167
|
+
files_affected,
|
|
168
|
+
agent_count,
|
|
169
|
+
critical_path,
|
|
170
|
+
dependency_chain
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { buildPreview, groupByWave };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* skill-section.js — Targeted SKILL.md section extraction for Plan-Build-Run.
|
|
5
|
+
*
|
|
6
|
+
* Enables surgical extraction of specific sections from skill files (SKILL.md),
|
|
7
|
+
* reducing token usage by fetching only the needed section on demand.
|
|
8
|
+
*
|
|
9
|
+
* Exported functions:
|
|
10
|
+
* skillSection(skillName, sectionQuery, pluginRoot) — Main entry point
|
|
11
|
+
* resolveSkillPath(skillName, pluginRoot) — Resolve skill name to file path
|
|
12
|
+
* listAvailableSkills(pluginRoot) — List all available skill names
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { extractSection, listHeadings } = require('./reference');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a skill name to its SKILL.md file path.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} skillName - Skill name (e.g., "build", "plan")
|
|
23
|
+
* @param {string} pluginRoot - Plugin root directory
|
|
24
|
+
* @returns {string | null} - Full path to SKILL.md, or null if not found
|
|
25
|
+
*/
|
|
26
|
+
function resolveSkillPath(skillName, pluginRoot) {
|
|
27
|
+
const skillPath = path.join(pluginRoot, 'skills', skillName, 'SKILL.md');
|
|
28
|
+
if (!fs.existsSync(skillPath)) return null;
|
|
29
|
+
return skillPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* List all available skill names from the skills/ directory.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} pluginRoot - Plugin root directory
|
|
36
|
+
* @returns {string[]} - Array of skill names (directory names)
|
|
37
|
+
*/
|
|
38
|
+
function listAvailableSkills(pluginRoot) {
|
|
39
|
+
const skillsDir = path.join(pluginRoot, 'skills');
|
|
40
|
+
try {
|
|
41
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
42
|
+
.filter(e => e.isDirectory())
|
|
43
|
+
.map(e => e.name);
|
|
44
|
+
} catch (_e) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract a specific section from a skill's SKILL.md.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} skillName - Skill name (e.g., "build", "plan")
|
|
53
|
+
* @param {string} sectionQuery - Section heading query (fuzzy matched)
|
|
54
|
+
* @param {string} pluginRoot - Plugin root directory
|
|
55
|
+
* @returns {object} - { skill, section, heading, content, char_count } or { error, available? }
|
|
56
|
+
*/
|
|
57
|
+
function skillSection(skillName, sectionQuery, pluginRoot) {
|
|
58
|
+
// Validate section query
|
|
59
|
+
if (!sectionQuery || !sectionQuery.trim()) {
|
|
60
|
+
return { error: 'Section query required' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve skill path
|
|
64
|
+
const skillPath = resolveSkillPath(skillName, pluginRoot);
|
|
65
|
+
if (!skillPath) {
|
|
66
|
+
return {
|
|
67
|
+
error: `Skill not found: ${skillName}`,
|
|
68
|
+
available: listAvailableSkills(pluginRoot)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Read skill content
|
|
73
|
+
let content;
|
|
74
|
+
try {
|
|
75
|
+
content = fs.readFileSync(skillPath, 'utf8');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { error: `Cannot read skill file: ${e.message}` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Normalize query: replace hyphens with spaces for better fuzzy matching
|
|
81
|
+
// e.g., "step-3" → "step 3" to match "Step 3: Automated Verification"
|
|
82
|
+
const normalizedQuery = sectionQuery.replace(/-/g, ' ');
|
|
83
|
+
|
|
84
|
+
// Extract the requested section (try normalized first, then original)
|
|
85
|
+
let result = extractSection(content, normalizedQuery);
|
|
86
|
+
if (!result && normalizedQuery !== sectionQuery) {
|
|
87
|
+
result = extractSection(content, sectionQuery);
|
|
88
|
+
}
|
|
89
|
+
if (!result) {
|
|
90
|
+
return {
|
|
91
|
+
error: `Section '${sectionQuery}' not found in skill '${skillName}'`,
|
|
92
|
+
available: listHeadings(content)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { skill: skillName, section: sectionQuery, ...result };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { skillSection, resolveSkillPath, listAvailableSkills };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/step-verify.js — Per-step completion checklist verifier for Plan-Build-Run.
|
|
5
|
+
*
|
|
6
|
+
* Provides stepVerify(skill, step, checklist, context) which maps checklist item
|
|
7
|
+
* strings to filesystem predicates using keyword matching and returns a structured
|
|
8
|
+
* pass/fail result.
|
|
9
|
+
*
|
|
10
|
+
* Usage (CLI via pbr-tools.js):
|
|
11
|
+
* node ${CLAUDE_PLUGIN_ROOT}/scripts/pbr-tools.js step-verify build step-6f '["STATE.md updated","SUMMARY.md exists"]'
|
|
12
|
+
*
|
|
13
|
+
* Returns: { skill, step, passed: string[], failed: string[], all_passed: boolean }
|
|
14
|
+
* On invalid checklist: { error: 'Invalid checklist JSON' }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Match a single checklist item string to a filesystem predicate.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} item - Checklist item string (e.g. "STATE.md updated")
|
|
25
|
+
* @param {object} context - { planningDir, phaseSlug, planId }
|
|
26
|
+
* @returns {{ passed: boolean, reason: string }}
|
|
27
|
+
*/
|
|
28
|
+
function matchPredicate(item, context) {
|
|
29
|
+
const lower = item.toLowerCase();
|
|
30
|
+
const { planningDir, phaseSlug, planId } = context;
|
|
31
|
+
|
|
32
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
33
|
+
const phaseDir = phaseSlug ? path.join(phasesDir, phaseSlug) : null;
|
|
34
|
+
|
|
35
|
+
// SUMMARY.md exists: check phaseDir for SUMMARY-{planId}.md or SUMMARY.md
|
|
36
|
+
if (lower.includes('summary') && lower.includes('exist')) {
|
|
37
|
+
if (!phaseDir) {
|
|
38
|
+
return { passed: false, reason: 'phaseSlug not provided in context' };
|
|
39
|
+
}
|
|
40
|
+
const summaryNamedPath = planId
|
|
41
|
+
? path.join(phaseDir, `SUMMARY-${planId}.md`)
|
|
42
|
+
: null;
|
|
43
|
+
const summaryGenericPath = path.join(phaseDir, 'SUMMARY.md');
|
|
44
|
+
const exists =
|
|
45
|
+
(summaryNamedPath && fs.existsSync(summaryNamedPath)) ||
|
|
46
|
+
fs.existsSync(summaryGenericPath);
|
|
47
|
+
return {
|
|
48
|
+
passed: exists,
|
|
49
|
+
reason: exists ? 'SUMMARY file found' : 'No SUMMARY file in phase dir'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// STATE.md updated or exists: check planningDir/STATE.md
|
|
54
|
+
if (lower.includes('state') && (lower.includes('update') || lower.includes('exist'))) {
|
|
55
|
+
const statePath = path.join(planningDir, 'STATE.md');
|
|
56
|
+
const exists = fs.existsSync(statePath);
|
|
57
|
+
return {
|
|
58
|
+
passed: exists,
|
|
59
|
+
reason: exists ? 'STATE.md found' : 'STATE.md not found in planningDir'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// PLAN.md exists: check phaseDir has at least one PLAN*.md file
|
|
64
|
+
if (lower.includes('plan') && lower.includes('exist')) {
|
|
65
|
+
if (!phaseDir) {
|
|
66
|
+
return { passed: false, reason: 'phaseSlug not provided in context' };
|
|
67
|
+
}
|
|
68
|
+
let planFiles = [];
|
|
69
|
+
try {
|
|
70
|
+
planFiles = fs.readdirSync(phaseDir).filter(f => /^PLAN.*\.md$/i.test(f));
|
|
71
|
+
} catch (_e) {
|
|
72
|
+
return { passed: false, reason: 'Phase directory not accessible' };
|
|
73
|
+
}
|
|
74
|
+
const exists = planFiles.length > 0;
|
|
75
|
+
return {
|
|
76
|
+
passed: exists,
|
|
77
|
+
reason: exists ? `Found: ${planFiles.join(', ')}` : 'No PLAN*.md files in phase dir'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ROADMAP.md updated: check planningDir/ROADMAP.md
|
|
82
|
+
if (lower.includes('roadmap') && lower.includes('update')) {
|
|
83
|
+
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
|
84
|
+
const exists = fs.existsSync(roadmapPath);
|
|
85
|
+
return {
|
|
86
|
+
passed: exists,
|
|
87
|
+
reason: exists ? 'ROADMAP.md found' : 'ROADMAP.md not found in planningDir'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// commit made: spawn 'git log --oneline -1'; pass if stdout non-empty
|
|
92
|
+
if (lower.includes('commit')) {
|
|
93
|
+
const result = spawnSync('git', ['log', '--oneline', '-1'], {
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
timeout: 5000
|
|
96
|
+
});
|
|
97
|
+
const output = (result.stdout || '').trim();
|
|
98
|
+
const passed = output.length > 0;
|
|
99
|
+
return {
|
|
100
|
+
passed,
|
|
101
|
+
reason: passed ? `Last commit: ${output}` : 'git log returned no output'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// No predicate matched
|
|
106
|
+
return {
|
|
107
|
+
passed: false,
|
|
108
|
+
reason: 'No predicate matched'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Verify a list of checklist items for a given skill step.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} skill - Skill name (e.g. 'build')
|
|
116
|
+
* @param {string} step - Step label (e.g. 'step-6f')
|
|
117
|
+
* @param {Array<string>|*} checklist - Array of checklist item strings
|
|
118
|
+
* @param {object} context - { planningDir, phaseSlug, planId }
|
|
119
|
+
* @returns {{ skill, step, passed: string[], failed: string[], all_passed: boolean }
|
|
120
|
+
* | { error: string }}
|
|
121
|
+
*/
|
|
122
|
+
function stepVerify(skill, step, checklist, context) {
|
|
123
|
+
if (!Array.isArray(checklist)) {
|
|
124
|
+
return { error: 'Invalid checklist JSON' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const passed = [];
|
|
128
|
+
const failed = [];
|
|
129
|
+
|
|
130
|
+
for (const item of checklist) {
|
|
131
|
+
const { passed: itemPassed } = matchPredicate(item, context || {});
|
|
132
|
+
if (itemPassed) {
|
|
133
|
+
passed.push(item);
|
|
134
|
+
} else {
|
|
135
|
+
failed.push(item);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
skill,
|
|
141
|
+
step,
|
|
142
|
+
passed,
|
|
143
|
+
failed,
|
|
144
|
+
all_passed: failed.length === 0
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// matchPredicate exported for unit testing of individual predicate branches
|
|
149
|
+
module.exports = { stepVerify, matchPredicate };
|