@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +2 -2
  3. package/plugins/codex-pbr/agents/audit.md +223 -0
  4. package/plugins/codex-pbr/agents/codebase-mapper.md +196 -0
  5. package/plugins/codex-pbr/agents/debugger.md +245 -0
  6. package/plugins/codex-pbr/agents/dev-sync.md +142 -0
  7. package/plugins/codex-pbr/agents/executor.md +429 -0
  8. package/plugins/codex-pbr/agents/general.md +131 -0
  9. package/plugins/codex-pbr/agents/integration-checker.md +178 -0
  10. package/plugins/codex-pbr/agents/plan-checker.md +253 -0
  11. package/plugins/codex-pbr/agents/planner.md +343 -0
  12. package/plugins/codex-pbr/agents/researcher.md +253 -0
  13. package/plugins/codex-pbr/agents/synthesizer.md +183 -0
  14. package/plugins/codex-pbr/agents/verifier.md +352 -0
  15. package/plugins/codex-pbr/commands/audit.md +5 -0
  16. package/plugins/codex-pbr/commands/begin.md +5 -0
  17. package/plugins/codex-pbr/commands/build.md +5 -0
  18. package/plugins/codex-pbr/commands/config.md +5 -0
  19. package/plugins/codex-pbr/commands/continue.md +5 -0
  20. package/plugins/codex-pbr/commands/dashboard.md +5 -0
  21. package/plugins/codex-pbr/commands/debug.md +5 -0
  22. package/plugins/codex-pbr/commands/discuss.md +5 -0
  23. package/plugins/codex-pbr/commands/do.md +5 -0
  24. package/plugins/codex-pbr/commands/explore.md +5 -0
  25. package/plugins/codex-pbr/commands/health.md +5 -0
  26. package/plugins/codex-pbr/commands/help.md +5 -0
  27. package/plugins/codex-pbr/commands/import.md +5 -0
  28. package/plugins/codex-pbr/commands/milestone.md +5 -0
  29. package/plugins/codex-pbr/commands/note.md +5 -0
  30. package/plugins/codex-pbr/commands/pause.md +5 -0
  31. package/plugins/codex-pbr/commands/plan.md +5 -0
  32. package/plugins/codex-pbr/commands/quick.md +5 -0
  33. package/plugins/codex-pbr/commands/resume.md +5 -0
  34. package/plugins/codex-pbr/commands/review.md +5 -0
  35. package/plugins/codex-pbr/commands/scan.md +5 -0
  36. package/plugins/codex-pbr/commands/setup.md +5 -0
  37. package/plugins/codex-pbr/commands/status.md +5 -0
  38. package/plugins/codex-pbr/commands/statusline.md +5 -0
  39. package/plugins/codex-pbr/commands/test.md +5 -0
  40. package/plugins/codex-pbr/commands/todo.md +5 -0
  41. package/plugins/codex-pbr/commands/undo.md +5 -0
  42. package/plugins/codex-pbr/references/agent-contracts.md +324 -0
  43. package/plugins/codex-pbr/references/agent-teams.md +54 -0
  44. package/plugins/codex-pbr/references/common-bug-patterns.md +13 -0
  45. package/plugins/codex-pbr/references/config-reference.md +552 -0
  46. package/plugins/codex-pbr/references/continuation-format.md +212 -0
  47. package/plugins/codex-pbr/references/deviation-rules.md +112 -0
  48. package/plugins/codex-pbr/references/git-integration.md +256 -0
  49. package/plugins/codex-pbr/references/integration-patterns.md +117 -0
  50. package/plugins/codex-pbr/references/model-profiles.md +99 -0
  51. package/plugins/codex-pbr/references/model-selection.md +31 -0
  52. package/plugins/codex-pbr/references/pbr-tools-cli.md +400 -0
  53. package/plugins/codex-pbr/references/plan-authoring.md +246 -0
  54. package/plugins/codex-pbr/references/plan-format.md +313 -0
  55. package/plugins/codex-pbr/references/questioning.md +235 -0
  56. package/plugins/codex-pbr/references/reading-verification.md +127 -0
  57. package/plugins/codex-pbr/references/signal-files.md +41 -0
  58. package/plugins/codex-pbr/references/stub-patterns.md +160 -0
  59. package/plugins/codex-pbr/references/ui-formatting.md +444 -0
  60. package/plugins/codex-pbr/references/wave-execution.md +95 -0
  61. package/plugins/codex-pbr/skills/audit/SKILL.md +346 -0
  62. package/plugins/codex-pbr/skills/begin/SKILL.md +800 -0
  63. package/plugins/codex-pbr/skills/build/SKILL.md +958 -0
  64. package/plugins/codex-pbr/skills/config/SKILL.md +267 -0
  65. package/plugins/codex-pbr/skills/continue/SKILL.md +172 -0
  66. package/plugins/codex-pbr/skills/dashboard/SKILL.md +44 -0
  67. package/plugins/codex-pbr/skills/debug/SKILL.md +530 -0
  68. package/plugins/codex-pbr/skills/discuss/SKILL.md +355 -0
  69. package/plugins/codex-pbr/skills/do/SKILL.md +68 -0
  70. package/plugins/codex-pbr/skills/explore/SKILL.md +407 -0
  71. package/plugins/codex-pbr/skills/health/SKILL.md +300 -0
  72. package/plugins/codex-pbr/skills/help/SKILL.md +229 -0
  73. package/plugins/codex-pbr/skills/import/SKILL.md +538 -0
  74. package/plugins/codex-pbr/skills/milestone/SKILL.md +620 -0
  75. package/plugins/codex-pbr/skills/note/SKILL.md +215 -0
  76. package/plugins/codex-pbr/skills/pause/SKILL.md +258 -0
  77. package/plugins/codex-pbr/skills/plan/SKILL.md +650 -0
  78. package/plugins/codex-pbr/skills/quick/SKILL.md +417 -0
  79. package/plugins/codex-pbr/skills/resume/SKILL.md +403 -0
  80. package/plugins/codex-pbr/skills/review/SKILL.md +669 -0
  81. package/plugins/codex-pbr/skills/scan/SKILL.md +325 -0
  82. package/plugins/codex-pbr/skills/setup/SKILL.md +169 -0
  83. package/plugins/codex-pbr/skills/shared/commit-planning-docs.md +35 -0
  84. package/plugins/codex-pbr/skills/shared/config-loading.md +102 -0
  85. package/plugins/codex-pbr/skills/shared/context-budget.md +77 -0
  86. package/plugins/codex-pbr/skills/shared/context-loader-task.md +86 -0
  87. package/plugins/codex-pbr/skills/shared/digest-select.md +79 -0
  88. package/plugins/codex-pbr/skills/shared/domain-probes.md +125 -0
  89. package/plugins/codex-pbr/skills/shared/error-reporting.md +59 -0
  90. package/plugins/codex-pbr/skills/shared/gate-prompts.md +388 -0
  91. package/plugins/codex-pbr/skills/shared/phase-argument-parsing.md +45 -0
  92. package/plugins/codex-pbr/skills/shared/revision-loop.md +81 -0
  93. package/plugins/codex-pbr/skills/shared/state-update.md +169 -0
  94. package/plugins/codex-pbr/skills/shared/universal-anti-patterns.md +43 -0
  95. package/plugins/codex-pbr/skills/status/SKILL.md +449 -0
  96. package/plugins/codex-pbr/skills/statusline/SKILL.md +149 -0
  97. package/plugins/codex-pbr/skills/test/SKILL.md +210 -0
  98. package/plugins/codex-pbr/skills/todo/SKILL.md +281 -0
  99. package/plugins/codex-pbr/skills/undo/SKILL.md +172 -0
  100. package/plugins/codex-pbr/templates/CONTEXT.md.tmpl +52 -0
  101. package/plugins/codex-pbr/templates/INTEGRATION-REPORT.md.tmpl +167 -0
  102. package/plugins/codex-pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -0
  103. package/plugins/codex-pbr/templates/ROADMAP.md.tmpl +47 -0
  104. package/plugins/codex-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  105. package/plugins/codex-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  106. package/plugins/codex-pbr/templates/SUMMARY.md.tmpl +81 -0
  107. package/plugins/codex-pbr/templates/VERIFICATION-DETAIL.md.tmpl +117 -0
  108. package/plugins/codex-pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -0
  109. package/plugins/codex-pbr/templates/codebase/CONCERNS.md.tmpl +93 -0
  110. package/plugins/codex-pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -0
  111. package/plugins/codex-pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -0
  112. package/plugins/codex-pbr/templates/codebase/STACK.md.tmpl +78 -0
  113. package/plugins/codex-pbr/templates/codebase/STRUCTURE.md.tmpl +80 -0
  114. package/plugins/codex-pbr/templates/codebase/TESTING.md.tmpl +107 -0
  115. package/plugins/codex-pbr/templates/continue-here.md.tmpl +73 -0
  116. package/plugins/codex-pbr/templates/pr-body.md.tmpl +22 -0
  117. package/plugins/codex-pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -0
  118. package/plugins/codex-pbr/templates/research/ARCHITECTURE.md.tmpl +124 -0
  119. package/plugins/codex-pbr/templates/research/STACK.md.tmpl +71 -0
  120. package/plugins/codex-pbr/templates/research/SUMMARY.md.tmpl +112 -0
  121. package/plugins/codex-pbr/templates/research-outputs/phase-research.md.tmpl +81 -0
  122. package/plugins/codex-pbr/templates/research-outputs/project-research.md.tmpl +99 -0
  123. package/plugins/codex-pbr/templates/research-outputs/synthesis.md.tmpl +36 -0
  124. package/plugins/copilot-pbr/commands/setup.md +1 -1
  125. package/plugins/copilot-pbr/commands/undo.md +5 -0
  126. package/plugins/copilot-pbr/plugin.json +1 -1
  127. package/plugins/copilot-pbr/skills/begin/SKILL.md +170 -17
  128. package/plugins/copilot-pbr/skills/build/SKILL.md +73 -8
  129. package/plugins/copilot-pbr/skills/plan/SKILL.md +67 -17
  130. package/plugins/copilot-pbr/skills/review/SKILL.md +12 -1
  131. package/plugins/copilot-pbr/skills/setup/SKILL.md +66 -214
  132. package/plugins/copilot-pbr/skills/shared/context-budget.md +27 -0
  133. package/plugins/copilot-pbr/skills/status/SKILL.md +44 -2
  134. package/plugins/copilot-pbr/skills/undo/SKILL.md +172 -0
  135. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  136. package/plugins/cursor-pbr/commands/setup.md +1 -1
  137. package/plugins/cursor-pbr/commands/undo.md +5 -0
  138. package/plugins/cursor-pbr/skills/begin/SKILL.md +170 -17
  139. package/plugins/cursor-pbr/skills/build/SKILL.md +73 -8
  140. package/plugins/cursor-pbr/skills/plan/SKILL.md +67 -17
  141. package/plugins/cursor-pbr/skills/review/SKILL.md +12 -1
  142. package/plugins/cursor-pbr/skills/setup/SKILL.md +66 -214
  143. package/plugins/cursor-pbr/skills/shared/context-budget.md +27 -0
  144. package/plugins/cursor-pbr/skills/status/SKILL.md +44 -2
  145. package/plugins/cursor-pbr/skills/undo/SKILL.md +173 -0
  146. package/plugins/jules-pbr/AGENTS.md +600 -0
  147. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  148. package/plugins/pbr/commands/setup.md +1 -1
  149. package/plugins/pbr/commands/undo.md +5 -0
  150. package/plugins/pbr/scripts/config-schema.json +5 -1
  151. package/plugins/pbr/scripts/lib/alternatives.js +203 -0
  152. package/plugins/pbr/scripts/lib/preview.js +174 -0
  153. package/plugins/pbr/scripts/lib/skill-section.js +99 -0
  154. package/plugins/pbr/scripts/lib/step-verify.js +149 -0
  155. package/plugins/pbr/scripts/pbr-tools.js +122 -2
  156. package/plugins/pbr/scripts/validate-commit.js +2 -2
  157. package/plugins/pbr/skills/begin/SKILL.md +170 -17
  158. package/plugins/pbr/skills/begin/templates/config.json.tmpl +5 -1
  159. package/plugins/pbr/skills/build/SKILL.md +73 -8
  160. package/plugins/pbr/skills/plan/SKILL.md +67 -17
  161. package/plugins/pbr/skills/review/SKILL.md +12 -1
  162. package/plugins/pbr/skills/setup/SKILL.md +66 -214
  163. package/plugins/pbr/skills/shared/context-budget.md +27 -0
  164. package/plugins/pbr/skills/status/SKILL.md +44 -2
  165. 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 };