@mytechtoday/augment-sdd 1.0.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 (60) hide show
  1. package/.eslintrc.json +20 -0
  2. package/out/commands/executeBeadsBatch.js +165 -0
  3. package/out/commands/executeBeadsBatch.js.map +1 -0
  4. package/out/commands/fullPipeline.js +129 -0
  5. package/out/commands/fullPipeline.js.map +1 -0
  6. package/out/commands/generateBeads.js +148 -0
  7. package/out/commands/generateBeads.js.map +1 -0
  8. package/out/commands/generateOpenSpec.js +241 -0
  9. package/out/commands/generateOpenSpec.js.map +1 -0
  10. package/out/dashboard/DashboardPanel.js +171 -0
  11. package/out/dashboard/DashboardPanel.js.map +1 -0
  12. package/out/extension.js +96 -0
  13. package/out/extension.js.map +1 -0
  14. package/out/parsers/parseBeadUpdates.js +28 -0
  15. package/out/parsers/parseBeadUpdates.js.map +1 -0
  16. package/out/parsers/parseTasksMarkdown.js +49 -0
  17. package/out/parsers/parseTasksMarkdown.js.map +1 -0
  18. package/out/test/integration/executeBeadsBatch.test.js +155 -0
  19. package/out/test/integration/executeBeadsBatch.test.js.map +1 -0
  20. package/out/test/integration/generateOpenSpec.test.js +154 -0
  21. package/out/test/integration/generateOpenSpec.test.js.map +1 -0
  22. package/out/test/runTest.js +47 -0
  23. package/out/test/runTest.js.map +1 -0
  24. package/out/test/suite/index.js +74 -0
  25. package/out/test/suite/index.js.map +1 -0
  26. package/out/test/unit/parseBeadUpdates.test.js +73 -0
  27. package/out/test/unit/parseBeadUpdates.test.js.map +1 -0
  28. package/out/test/unit/parseTasksMarkdown.test.js +69 -0
  29. package/out/test/unit/parseTasksMarkdown.test.js.map +1 -0
  30. package/out/test/unit/runCli.test.js +113 -0
  31. package/out/test/unit/runCli.test.js.map +1 -0
  32. package/out/utils/detectCli.js +30 -0
  33. package/out/utils/detectCli.js.map +1 -0
  34. package/out/utils/getConfig.js +60 -0
  35. package/out/utils/getConfig.js.map +1 -0
  36. package/out/utils/logger.js +30 -0
  37. package/out/utils/logger.js.map +1 -0
  38. package/out/utils/runCli.js +122 -0
  39. package/out/utils/runCli.js.map +1 -0
  40. package/package.json +111 -0
  41. package/src/commands/executeBeadsBatch.ts +153 -0
  42. package/src/commands/fullPipeline.ts +120 -0
  43. package/src/commands/generateBeads.ts +127 -0
  44. package/src/commands/generateOpenSpec.ts +227 -0
  45. package/src/dashboard/DashboardPanel.ts +168 -0
  46. package/src/extension.ts +77 -0
  47. package/src/parsers/parseBeadUpdates.ts +26 -0
  48. package/src/parsers/parseTasksMarkdown.ts +61 -0
  49. package/src/test/integration/executeBeadsBatch.test.ts +129 -0
  50. package/src/test/integration/generateOpenSpec.test.ts +129 -0
  51. package/src/test/runTest.ts +15 -0
  52. package/src/test/suite/index.ts +37 -0
  53. package/src/test/unit/parseBeadUpdates.test.ts +48 -0
  54. package/src/test/unit/parseTasksMarkdown.test.ts +41 -0
  55. package/src/test/unit/runCli.test.ts +109 -0
  56. package/src/utils/detectCli.ts +28 -0
  57. package/src/utils/getConfig.ts +25 -0
  58. package/src/utils/logger.ts +42 -0
  59. package/src/utils/runCli.ts +102 -0
  60. package/tsconfig.json +18 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * fullPipeline — Bead 8 command implementation.
3
+ *
4
+ * Steps (tasks 8.1-8.4):
5
+ * 8.1 Wire generateOpenSpec -> generateBeads -> executeBeadsBatch behind
6
+ * a VS Code progress notification.
7
+ * 8.2 Update progress indicator at each step boundary.
8
+ * 8.3 Halt pipeline on step failure; show step name, error, and offer
9
+ * Resume from step X quick-pick options.
10
+ * 8.4 Implement resume logic: skip steps before the selected resume point.
11
+ */
12
+ import * as vscode from 'vscode';
13
+ import { log } from '../utils/logger';
14
+ import { getConfig } from '../utils/getConfig';
15
+ import { DashboardPanel } from '../dashboard/DashboardPanel';
16
+ import { generateOpenSpec } from './generateOpenSpec';
17
+ import { generateBeads } from './generateBeads';
18
+ import { executeBeadsBatch } from './executeBeadsBatch';
19
+
20
+ /** Represents a single pipeline step. */
21
+ interface PipelineStep {
22
+ name: string;
23
+ label: string;
24
+ run: () => Promise<void>;
25
+ }
26
+
27
+ /** All pipeline steps in execution order. */
28
+ const PIPELINE_STEPS: PipelineStep[] = [
29
+ { name: 'generateOpenSpec', label: 'Generating OpenSpec…', run: generateOpenSpec },
30
+ { name: 'generateBeads', label: 'Generating Beads…', run: generateBeads },
31
+ { name: 'executeBeadsBatch', label: 'Executing Beads Batch…', run: executeBeadsBatch },
32
+ ];
33
+
34
+ /** Entry point registered as `augmentSdd.fullPipeline`. */
35
+ export async function fullPipeline(startFromIndex = 0): Promise<void> {
36
+ const steps = PIPELINE_STEPS.slice(startFromIndex);
37
+ const totalSteps = PIPELINE_STEPS.length;
38
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
39
+
40
+ await vscode.window.withProgress(
41
+ {
42
+ location: vscode.ProgressLocation.Notification,
43
+ title: 'Augment SDD: Full Pipeline',
44
+ cancellable: false,
45
+ },
46
+ async (progress) => {
47
+ for (const step of steps) {
48
+ const globalIndex = PIPELINE_STEPS.indexOf(step);
49
+ const stepNum = globalIndex + 1;
50
+ const pct = Math.round(((globalIndex) / totalSteps) * 100);
51
+ progress.report({
52
+ message: `Step ${stepNum}/${totalSteps}: ${step.label}`,
53
+ increment: pct === 0 ? 0 : Math.round(100 / totalSteps),
54
+ });
55
+ log(`fullPipeline: starting step ${stepNum}/${totalSteps} — ${step.name}`);
56
+
57
+ try {
58
+ await step.run();
59
+ log(`fullPipeline: step ${stepNum} completed — ${step.name}`);
60
+ // Open / refresh the dashboard after each successful step if the setting is enabled.
61
+ if (getConfig<boolean>('enableWebviewDashboard', true) && workspaceRoot) {
62
+ DashboardPanel.show(workspaceRoot);
63
+ }
64
+ } catch (err) {
65
+ const errMsg = err instanceof Error ? err.message : String(err);
66
+ log(`fullPipeline: step ${stepNum} FAILED — ${step.name}: ${errMsg}`);
67
+ await handleStepFailure(step.name, stepNum, errMsg);
68
+ return; // abort progress handler; resume handled separately
69
+ }
70
+ }
71
+
72
+ // All steps succeeded
73
+ progress.report({ message: 'Complete!', increment: 100 });
74
+ log('fullPipeline: all steps completed successfully');
75
+ vscode.window.showInformationMessage(
76
+ 'Augment SDD: Full pipeline complete — OpenSpec generated, beads created, and batch executed.'
77
+ );
78
+ }
79
+ );
80
+ }
81
+
82
+ /**
83
+ * 8.3: Show error notification with Resume from step X quick-pick options.
84
+ * Launches a new pipeline run from the selected resume point.
85
+ */
86
+ async function handleStepFailure(
87
+ stepName: string,
88
+ failedStepNum: number,
89
+ errMsg: string
90
+ ): Promise<void> {
91
+ // Build resume options for the failed step and all subsequent steps
92
+ const resumeOptions = PIPELINE_STEPS
93
+ .slice(failedStepNum - 1)
94
+ .map((s, i) => ({
95
+ label: `Resume from step ${failedStepNum + i}: ${s.name}`,
96
+ index: failedStepNum - 1 + i,
97
+ }));
98
+
99
+ const picked = await vscode.window.showErrorMessage(
100
+ `Augment SDD: Pipeline failed at step ${failedStepNum} (${stepName}): ${errMsg}`,
101
+ ...resumeOptions.map(o => o.label)
102
+ );
103
+
104
+ if (!picked) {
105
+ log('fullPipeline: user dismissed resume dialog — pipeline aborted');
106
+ return;
107
+ }
108
+
109
+ const selected = resumeOptions.find(o => o.label === picked);
110
+ if (selected !== undefined) {
111
+ log(`fullPipeline: user selected resume from index ${selected.index} (${PIPELINE_STEPS[selected.index].name})`);
112
+ // Run the resumed pipeline (non-awaited so the progress handler can close)
113
+ setImmediate(() => {
114
+ fullPipeline(selected.index).catch(e => {
115
+ log(`fullPipeline(resume): unexpected error — ${String(e)}`);
116
+ });
117
+ });
118
+ }
119
+ }
120
+
@@ -0,0 +1,127 @@
1
+ /**
2
+ * generateBeads — Bead 6 command implementation.
3
+ *
4
+ * Steps (tasks 6.1–6.5):
5
+ * 6.1 Glob openspec/changes/{id}/tasks.md and select the most recently modified.
6
+ * 6.2 Parse using parseTasksMarkdown (heading-based regex, design D3).
7
+ * 6.3 Execute bd create --title --description for each task.
8
+ * 6.4 Continue processing on individual bd create failures; log each one.
9
+ * 6.5 Run bd ready --json after all creates; display ready-bead count.
10
+ */
11
+ import * as vscode from 'vscode';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { runCli } from '../utils/runCli';
15
+ import { log, revealOutputChannel } from '../utils/logger';
16
+ import { parseTasksMarkdown } from '../parsers/parseTasksMarkdown';
17
+
18
+ /** Entry point registered as `augmentSdd.generateBeads`. */
19
+ export async function generateBeads(): Promise<void> {
20
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
21
+ if (!workspaceRoot) {
22
+ vscode.window.showErrorMessage('Augment SDD: No workspace folder open.');
23
+ return;
24
+ }
25
+
26
+ // 6.1 — Find the most recently modified tasks.md.
27
+ const tasksFile = findNewestTasksFile(workspaceRoot);
28
+ if (!tasksFile) {
29
+ vscode.window.showErrorMessage(
30
+ 'Augment SDD: No tasks.md found in openspec/changes/. ' +
31
+ 'Please run "Augment SDD: Generate OpenSpec" first.'
32
+ );
33
+ return;
34
+ }
35
+ log(`Using tasks.md: ${tasksFile}`);
36
+
37
+ // 6.2 — Parse headings into tasks.
38
+ const content = fs.readFileSync(tasksFile, 'utf8');
39
+ const tasks = parseTasksMarkdown(content);
40
+
41
+ if (tasks.length === 0) {
42
+ const snippet = content.slice(0, 300);
43
+ vscode.window.showErrorMessage(
44
+ `Augment SDD: No markdown headings found in tasks.md.\n\nFile begins:\n${snippet}`
45
+ );
46
+ return;
47
+ }
48
+ log(`Parsed ${tasks.length} task(s) from ${path.basename(path.dirname(tasksFile))}/tasks.md`);
49
+
50
+ // 6.3–6.4 — Create beads; continue on individual failures.
51
+ let created = 0;
52
+ for (const task of tasks) {
53
+ try {
54
+ // Shell-quote title and description so spaces are preserved correctly
55
+ // when spawn joins args into a shell command string (shell: true).
56
+ const quotedTitle = `"${task.title.replace(/"/g, '\\"')}"`;
57
+ const quotedDesc = `"${task.description.replace(/"/g, '\\"')}"`;
58
+ await runCli(
59
+ 'bd',
60
+ ['create', quotedTitle, '--description', quotedDesc],
61
+ workspaceRoot,
62
+ 30_000,
63
+ log
64
+ );
65
+ created++;
66
+ } catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ log(`bd create failed for "${task.title}": ${msg}`);
69
+ // Do NOT break — spec 6.4 requires continuing with remaining tasks.
70
+ }
71
+ }
72
+ log(`Created ${created}/${tasks.length} bead(s).`);
73
+
74
+ // 6.5 — Run bd ready --json and display count.
75
+ try {
76
+ const readyJson = await runCli('bd', ['ready', '--json'], workspaceRoot, 30_000, log);
77
+ let readyCount = 0;
78
+ try {
79
+ const parsed = JSON.parse(readyJson) as unknown[];
80
+ readyCount = Array.isArray(parsed) ? parsed.length : 0;
81
+ } catch {
82
+ log('Warning: could not parse bd ready --json output as JSON array');
83
+ }
84
+ vscode.window.showInformationMessage(
85
+ `Augment SDD: Created ${created}/${tasks.length} bead(s). ` +
86
+ `${readyCount} bead(s) ready to execute.`
87
+ );
88
+ } catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ log(`bd ready --json failed: ${msg}`);
91
+ const action = await vscode.window.showInformationMessage(
92
+ `Augment SDD: Created ${created}/${tasks.length} bead(s). ` +
93
+ `(Could not retrieve ready count.)`,
94
+ 'View Logs'
95
+ );
96
+ if (action === 'View Logs') { revealOutputChannel(); }
97
+ }
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Helper
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /**
105
+ * 6.1: Scan openspec/changes/{id}/tasks.md and return the path to the most
106
+ * recently modified file. Skips the 'archive' subdirectory.
107
+ * Returns undefined when no tasks.md is found.
108
+ */
109
+ function findNewestTasksFile(workspaceRoot: string): string | undefined {
110
+ const changesDir = path.join(workspaceRoot, 'openspec', 'changes');
111
+ if (!fs.existsSync(changesDir)) { return undefined; }
112
+
113
+ const candidates: Array<{ filePath: string; mtime: number }> = [];
114
+
115
+ for (const entry of fs.readdirSync(changesDir, { withFileTypes: true })) {
116
+ if (!entry.isDirectory() || entry.name === 'archive') { continue; }
117
+ const tasksPath = path.join(changesDir, entry.name, 'tasks.md');
118
+ if (fs.existsSync(tasksPath)) {
119
+ candidates.push({ filePath: tasksPath, mtime: fs.statSync(tasksPath).mtimeMs });
120
+ }
121
+ }
122
+
123
+ if (candidates.length === 0) { return undefined; }
124
+ candidates.sort((a, b) => b.mtime - a.mtime);
125
+ return candidates[0].filePath;
126
+ }
127
+
@@ -0,0 +1,227 @@
1
+ /**
2
+ * generateOpenSpec — Bead 5 command implementation.
3
+ *
4
+ * Steps (tasks 5.1–5.7):
5
+ * 5.1 File selection: file-picker dialog with fallback auto-detect from jiraFolder.
6
+ * 5.2 Run `openspec init --tools auggie --force`.
7
+ * 5.3 Retrieve proposal template via `openspec instructions proposal --json`;
8
+ * fallback to `spec` then `tasks`.
9
+ * 5.4 Build Auggie prompt: template + JIRA Markdown (truncate at 8 000 chars).
10
+ * 5.5 Execute `auggie --print --quiet <prompt>`; retry once on empty response.
11
+ * 5.6 Save Auggie output to `openspec/changes/<id>/proposal.md`.
12
+ * 5.7 Run `openspec validate`; surface result as info or error notification.
13
+ */
14
+ import * as vscode from 'vscode';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import { runCli } from '../utils/runCli';
18
+ import { getConfig } from '../utils/getConfig';
19
+ import { log, revealOutputChannel } from '../utils/logger';
20
+
21
+ /** Maximum JIRA content characters included in the Auggie prompt (risk D62). */
22
+ const MAX_JIRA_CHARS = 8_000;
23
+
24
+ /** Entry point registered as `augmentSdd.generateOpenSpec`. */
25
+ export async function generateOpenSpec(): Promise<void> {
26
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
27
+ if (!workspaceRoot) {
28
+ vscode.window.showErrorMessage('Augment SDD: No workspace folder open.');
29
+ return;
30
+ }
31
+
32
+ // 5.1 — Select the JIRA Markdown file.
33
+ const jiraFile = await selectJiraFile(workspaceRoot);
34
+ if (!jiraFile) {
35
+ vscode.window.showInformationMessage('Augment SDD: No file selected. Operation cancelled.');
36
+ return;
37
+ }
38
+ log(`JIRA file selected: ${jiraFile}`);
39
+
40
+ // 5.2 — openspec init.
41
+ try {
42
+ await runCli('openspec', ['init', '--tools', 'auggie', '--force'], workspaceRoot, 60_000, log);
43
+ } catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ log(`openspec init failed: ${msg}`);
46
+ const action = await vscode.window.showErrorMessage(
47
+ `Augment SDD: openspec init failed.\n${msg}`, 'View Logs'
48
+ );
49
+ if (action === 'View Logs') { revealOutputChannel(); }
50
+ return;
51
+ }
52
+
53
+ // Determine the change directory created by openspec init.
54
+ const changeId = findNewestChangeDir(workspaceRoot);
55
+ if (!changeId) {
56
+ vscode.window.showErrorMessage(
57
+ 'Augment SDD: Could not locate an openspec change directory after init.'
58
+ );
59
+ return;
60
+ }
61
+ const changeDir = path.join(workspaceRoot, 'openspec', 'changes', changeId);
62
+ log(`Using openspec change directory: ${changeId}`);
63
+
64
+ // 5.3 — Retrieve the proposal template (with fallbacks).
65
+ const template = await fetchProposalTemplate(workspaceRoot);
66
+
67
+ // 5.4 — Build the Auggie prompt.
68
+ const jiraRaw = fs.readFileSync(jiraFile, 'utf8');
69
+ let jiraContent = jiraRaw;
70
+ if (jiraRaw.length > MAX_JIRA_CHARS) {
71
+ jiraContent = jiraRaw.slice(0, MAX_JIRA_CHARS) + '\n[...truncated]';
72
+ log(`Warning: JIRA content truncated from ${jiraRaw.length} to ${MAX_JIRA_CHARS} chars`);
73
+ }
74
+ const prompt = template ? `${template}\n\n---\n\n${jiraContent}` : jiraContent;
75
+
76
+ // 5.5 — Run Auggie with one retry on empty output.
77
+ let auggieOutput: string;
78
+ try {
79
+ auggieOutput = await runAuggieWithRetry(workspaceRoot, prompt);
80
+ } catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ log(`Auggie execution failed: ${msg}`);
83
+ const action = await vscode.window.showErrorMessage(
84
+ `Augment SDD: Auggie failed.\n${msg}`, 'View Logs'
85
+ );
86
+ if (action === 'View Logs') { revealOutputChannel(); }
87
+ return;
88
+ }
89
+
90
+ // 5.6 — Save proposal.md.
91
+ const proposalPath = path.join(changeDir, 'proposal.md');
92
+ fs.mkdirSync(changeDir, { recursive: true });
93
+ fs.writeFileSync(proposalPath, auggieOutput, 'utf8');
94
+ log(`Saved proposal.md → ${proposalPath}`);
95
+
96
+ // 5.7 — openspec validate.
97
+ try {
98
+ await runCli('openspec', ['validate'], workspaceRoot, 30_000, log);
99
+ vscode.window.showInformationMessage(
100
+ `Augment SDD: OpenSpec generated and validated. Change: ${changeId}`
101
+ );
102
+ } catch (err) {
103
+ const msg = err instanceof Error ? err.message : String(err);
104
+ log(`openspec validate failed: ${msg}`);
105
+ const action = await vscode.window.showErrorMessage(
106
+ `Augment SDD: OpenSpec generated but validation failed.\n${msg}`, 'View Logs'
107
+ );
108
+ if (action === 'View Logs') { revealOutputChannel(); }
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Helpers
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * 5.1: Present a file-picker dialog filtered to Markdown files. If exactly one
118
+ * `.md` file exists in the configured `jiraFolder`, auto-select it; if multiple
119
+ * exist, show a Quick Pick; otherwise fall back to the OS open-file dialog.
120
+ */
121
+ async function selectJiraFile(workspaceRoot: string): Promise<string | undefined> {
122
+ const jiraFolder = getConfig<string>('jiraFolder', '.jira');
123
+ const jiraDir = path.join(workspaceRoot, jiraFolder);
124
+
125
+ if (fs.existsSync(jiraDir)) {
126
+ const mdFiles = fs.readdirSync(jiraDir)
127
+ .filter(f => f.toLowerCase().endsWith('.md'))
128
+ .map(f => path.join(jiraDir, f));
129
+
130
+ if (mdFiles.length === 1) {
131
+ log(`Auto-detected JIRA file: ${mdFiles[0]}`);
132
+ return mdFiles[0];
133
+ }
134
+
135
+ if (mdFiles.length > 1) {
136
+ const items = mdFiles.map(f => ({ label: path.basename(f), description: f }));
137
+ const pick = await vscode.window.showQuickPick(items, {
138
+ placeHolder: 'Select JIRA Markdown ticket file',
139
+ });
140
+ return pick?.description;
141
+ }
142
+ }
143
+
144
+ // Fall back to the OS file-picker dialog.
145
+ const uris = await vscode.window.showOpenDialog({
146
+ canSelectMany: false,
147
+ filters: { 'Markdown': ['md'] },
148
+ title: 'Select JIRA Ticket Markdown File',
149
+ });
150
+ return uris?.[0]?.fsPath;
151
+ }
152
+
153
+ /**
154
+ * Return the name of the most-recently-modified directory inside
155
+ * `openspec/changes/` (excluding the `archive` subdirectory).
156
+ * Returns `undefined` when no directories are found.
157
+ */
158
+ function findNewestChangeDir(workspaceRoot: string): string | undefined {
159
+ const changesDir = path.join(workspaceRoot, 'openspec', 'changes');
160
+ if (!fs.existsSync(changesDir)) { return undefined; }
161
+
162
+ const dirs = fs.readdirSync(changesDir, { withFileTypes: true })
163
+ .filter(e => e.isDirectory() && e.name !== 'archive')
164
+ .map(e => ({
165
+ name: e.name,
166
+ mtime: fs.statSync(path.join(changesDir, e.name)).mtimeMs,
167
+ }))
168
+ .sort((a, b) => b.mtime - a.mtime);
169
+
170
+ return dirs[0]?.name;
171
+ }
172
+
173
+ /**
174
+ * 5.3: Try `openspec instructions proposal --json`, then `spec`, then `tasks`.
175
+ * Returns the template string, or an empty string if all attempts fail.
176
+ */
177
+ async function fetchProposalTemplate(workspaceRoot: string): Promise<string> {
178
+ for (const type of ['proposal', 'spec', 'tasks']) {
179
+ try {
180
+ const raw = await runCli(
181
+ 'openspec', ['instructions', type, '--json'], workspaceRoot, 30_000, log
182
+ );
183
+ // The JSON may have a `template` or `content` field; fall back to raw text.
184
+ try {
185
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
186
+ const tmpl = (parsed['template'] ?? parsed['content'] ?? raw) as string;
187
+ log(`Fetched ${type} template (${String(tmpl).length} chars)`);
188
+ return String(tmpl);
189
+ } catch {
190
+ log(`Fetched ${type} template as raw text (${raw.length} chars)`);
191
+ return raw;
192
+ }
193
+ } catch {
194
+ log(`openspec instructions ${type} --json failed; trying next fallback`);
195
+ }
196
+ }
197
+ log('Warning: could not retrieve any proposal template; proceeding with JIRA content only');
198
+ return '';
199
+ }
200
+
201
+ /**
202
+ * 5.5: Run `auggie --print --quiet <prompt>`, retrying once with a reinforced
203
+ * suffix when the first attempt returns an empty string (design D6).
204
+ */
205
+ async function runAuggieWithRetry(workspaceRoot: string, prompt: string): Promise<string> {
206
+ const extraFlags = getConfig<string>('auggieExtraFlags', '');
207
+ const buildArgs = (p: string): string[] => {
208
+ const base = ['--print', '--quiet', p];
209
+ if (extraFlags) {
210
+ base.push(...extraFlags.split(/\s+/).filter(Boolean));
211
+ }
212
+ return base;
213
+ };
214
+
215
+ let output = await runCli('auggie', buildArgs(prompt), workspaceRoot, 300_000, log);
216
+ if (!output) {
217
+ log('Auggie returned empty output; retrying with reinforced prompt…');
218
+ const retryPrompt = prompt + '\nPlease provide the full output.';
219
+ output = await runCli('auggie', buildArgs(retryPrompt), workspaceRoot, 300_000, log);
220
+ if (!output) {
221
+ throw new Error('Auggie returned empty output after retry (max 2 attempts)');
222
+ }
223
+ }
224
+ return output;
225
+ }
226
+
227
+
@@ -0,0 +1,168 @@
1
+ /**
2
+ * DashboardPanel — Webview panel for the Augment SDD Dashboard (Bead 10 / bd-d47o).
3
+ *
4
+ * Renders a list of ready Beads fetched from `bd ready --json`.
5
+ * Provides "Run Next Batch" (triggers augmentSdd.executeBeadsBatch then refreshes)
6
+ * and "Refresh" (re-fetches and re-renders) buttons.
7
+ *
8
+ * The singleton is kept alive as long as the panel is visible.
9
+ * Callers: fullPipeline.ts (opens after each pipeline step when enabled).
10
+ */
11
+ import * as vscode from 'vscode';
12
+ import { runCli } from '../utils/runCli';
13
+ import { log } from '../utils/logger';
14
+
15
+ /** Shape returned by `bd ready --json`. */
16
+ interface ReadyBead {
17
+ id: string;
18
+ title: string;
19
+ description: string;
20
+ }
21
+
22
+ export class DashboardPanel {
23
+ public static readonly viewType = 'augmentSddDashboard';
24
+ private static _current: DashboardPanel | undefined;
25
+
26
+ private readonly _panel: vscode.WebviewPanel;
27
+ private _workspaceRoot: string;
28
+ private readonly _disposables: vscode.Disposable[] = [];
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Public API
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Open (or reveal + refresh) the Augment SDD Dashboard.
36
+ * Safe to call multiple times; only one panel is ever open.
37
+ */
38
+ public static show(workspaceRoot: string): void {
39
+ if (DashboardPanel._current) {
40
+ DashboardPanel._current._panel.reveal(vscode.ViewColumn.Two);
41
+ DashboardPanel._current._workspaceRoot = workspaceRoot;
42
+ DashboardPanel._current._refresh().catch(e =>
43
+ log(`DashboardPanel.refresh error: ${String(e)}`)
44
+ );
45
+ return;
46
+ }
47
+
48
+ const panel = vscode.window.createWebviewPanel(
49
+ DashboardPanel.viewType,
50
+ 'Augment SDD Dashboard',
51
+ vscode.ViewColumn.Two,
52
+ { enableScripts: true, retainContextWhenHidden: true }
53
+ );
54
+
55
+ DashboardPanel._current = new DashboardPanel(panel, workspaceRoot);
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Constructor
60
+ // ---------------------------------------------------------------------------
61
+
62
+ private constructor(panel: vscode.WebviewPanel, workspaceRoot: string) {
63
+ this._panel = panel;
64
+ this._workspaceRoot = workspaceRoot;
65
+
66
+ // Initial render
67
+ this._refresh().catch(e => log(`DashboardPanel initial render error: ${String(e)}`));
68
+
69
+ // Dispose when the user closes the panel
70
+ this._panel.onDidDispose(() => this._dispose(), null, this._disposables);
71
+
72
+ // Handle messages sent from the webview script
73
+ this._panel.webview.onDidReceiveMessage(
74
+ async (message: { command: string }) => {
75
+ if (message.command === 'runBatch') {
76
+ log('DashboardPanel: Run Next Batch clicked');
77
+ await vscode.commands.executeCommand('augmentSdd.executeBeadsBatch');
78
+ await this._refresh();
79
+ } else if (message.command === 'refresh') {
80
+ log('DashboardPanel: Refresh clicked');
81
+ await this._refresh();
82
+ }
83
+ },
84
+ null,
85
+ this._disposables
86
+ );
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Private helpers
91
+ // ---------------------------------------------------------------------------
92
+
93
+ private _dispose(): void {
94
+ DashboardPanel._current = undefined;
95
+ this._panel.dispose();
96
+ for (const d of this._disposables) { d.dispose(); }
97
+ this._disposables.length = 0;
98
+ }
99
+
100
+ /** Fetch beads and re-render the webview HTML. */
101
+ private async _refresh(): Promise<void> {
102
+ let beads: ReadyBead[] = [];
103
+ try {
104
+ const raw = await runCli('bd', ['ready', '--json'], this._workspaceRoot, 30_000, log);
105
+ beads = JSON.parse(raw) as ReadyBead[];
106
+ } catch (err) {
107
+ log(`DashboardPanel: failed to fetch ready beads — ${err instanceof Error ? err.message : String(err)}`);
108
+ }
109
+ this._panel.webview.html = this._buildHtml(beads);
110
+ }
111
+
112
+ private _buildHtml(beads: ReadyBead[]): string {
113
+ const cards = beads.length > 0
114
+ ? beads.map(b => `
115
+ <div class="card">
116
+ <span class="bead-id">${esc(b.id)}</span>
117
+ <div class="bead-title">${esc(b.title)}</div>
118
+ <div class="bead-desc">${esc(b.description)}</div>
119
+ </div>`).join('')
120
+ : '<p class="empty">No beads ready.</p>';
121
+
122
+ return `<!DOCTYPE html>
123
+ <html lang="en">
124
+ <head>
125
+ <meta charset="UTF-8">
126
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';">
127
+ <title>Augment SDD Dashboard</title>
128
+ <style>
129
+ body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; margin: 0; }
130
+ h1 { font-size: 1.1em; margin-bottom: 10px; }
131
+ .actions { margin-bottom: 14px; }
132
+ button { margin-right: 8px; padding: 5px 14px; cursor: pointer;
133
+ background: var(--vscode-button-background); color: var(--vscode-button-foreground);
134
+ border: none; border-radius: 2px; }
135
+ button:hover { background: var(--vscode-button-hoverBackground); }
136
+ .card { border: 1px solid var(--vscode-panel-border); border-radius: 4px;
137
+ padding: 10px 14px; margin-bottom: 8px; }
138
+ .bead-id { font-size: 0.8em; color: var(--vscode-descriptionForeground); }
139
+ .bead-title { font-weight: bold; margin: 3px 0; }
140
+ .bead-desc { font-size: 0.9em; }
141
+ .empty { color: var(--vscode-descriptionForeground); }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <h1>Augment SDD Dashboard</h1>
146
+ <div class="actions">
147
+ <button onclick="post('runBatch')">Run Next Batch</button>
148
+ <button onclick="post('refresh')">Refresh</button>
149
+ </div>
150
+ <div id="beads">${cards}</div>
151
+ <script>
152
+ const vscode = acquireVsCodeApi();
153
+ function post(cmd) { vscode.postMessage({ command: cmd }); }
154
+ </script>
155
+ </body>
156
+ </html>`;
157
+ }
158
+ }
159
+
160
+ /** Minimal HTML entity escaping to prevent XSS in bead data. */
161
+ function esc(text: string): string {
162
+ return text
163
+ .replace(/&/g, '&amp;')
164
+ .replace(/</g, '&lt;')
165
+ .replace(/>/g, '&gt;')
166
+ .replace(/"/g, '&quot;');
167
+ }
168
+