@bradtaylorsf/alpha-loop 1.8.0 → 1.9.1

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 (48) hide show
  1. package/dist/cli.js +4 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/init.js +6 -5
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/plan.d.ts +1 -0
  6. package/dist/commands/plan.js +298 -195
  7. package/dist/commands/plan.js.map +1 -1
  8. package/dist/commands/resume.js +6 -5
  9. package/dist/commands/resume.js.map +1 -1
  10. package/dist/commands/roadmap.js +2 -3
  11. package/dist/commands/roadmap.js.map +1 -1
  12. package/dist/commands/run.d.ts +2 -0
  13. package/dist/commands/run.js +33 -2
  14. package/dist/commands/run.js.map +1 -1
  15. package/dist/commands/triage.js +12 -16
  16. package/dist/commands/triage.js.map +1 -1
  17. package/dist/commands/vision.js +4 -3
  18. package/dist/commands/vision.js.map +1 -1
  19. package/dist/lib/github.d.ts +16 -3
  20. package/dist/lib/github.js +151 -75
  21. package/dist/lib/github.js.map +1 -1
  22. package/dist/lib/learning.d.ts +13 -1
  23. package/dist/lib/learning.js +90 -17
  24. package/dist/lib/learning.js.map +1 -1
  25. package/dist/lib/logger.d.ts +1 -0
  26. package/dist/lib/logger.js +2 -0
  27. package/dist/lib/logger.js.map +1 -1
  28. package/dist/lib/pipeline.js +116 -44
  29. package/dist/lib/pipeline.js.map +1 -1
  30. package/dist/lib/planning.d.ts +5 -0
  31. package/dist/lib/planning.js +14 -0
  32. package/dist/lib/planning.js.map +1 -1
  33. package/dist/lib/prompts.d.ts +5 -0
  34. package/dist/lib/prompts.js +18 -5
  35. package/dist/lib/prompts.js.map +1 -1
  36. package/dist/lib/rate-limit.d.ts +55 -0
  37. package/dist/lib/rate-limit.js +188 -0
  38. package/dist/lib/rate-limit.js.map +1 -0
  39. package/dist/lib/session.js +2 -1
  40. package/dist/lib/session.js.map +1 -1
  41. package/dist/lib/validation.d.ts +69 -0
  42. package/dist/lib/validation.js +280 -0
  43. package/dist/lib/validation.js.map +1 -0
  44. package/dist/lib/worktree.d.ts +16 -0
  45. package/dist/lib/worktree.js +99 -31
  46. package/dist/lib/worktree.js.map +1 -1
  47. package/package.json +1 -1
  48. package/templates/agents/reviewer.md +7 -0
@@ -11,229 +11,337 @@ import { buildOneShotCommand } from '../lib/agent.js';
11
11
  import { buildPlanPrompt } from '../lib/prompts.js';
12
12
  import { exec } from '../lib/shell.js';
13
13
  import { log } from '../lib/logger.js';
14
- import { extractJsonFromResponse, formatIssueTable, readSeedFiles, savePlanDraft, buildPlanningContext, } from '../lib/planning.js';
15
- import { createMilestone, createIssue, addIssueToProject, listOpenIssues, } from '../lib/github.js';
14
+ import { getRateLimitStatus } from '../lib/rate-limit.js';
15
+ import { extractJsonFromResponse, formatIssueTable, readSeedFiles, savePlanDraft, loadPlanDraft, buildPlanningContext, } from '../lib/planning.js';
16
+ import { createMilestone, createIssue, addIssueToProject, listOpenIssues, listMilestones, listLabels, createLabel, } from '../lib/github.js';
16
17
  export async function planCommand(options) {
17
18
  // ── TTY check ──────────────────────────────────────────────────────────────
18
- if (!process.stdin.isTTY && !options.yes) {
19
+ if (!process.stdin.isTTY && !options.yes && !options.resume) {
19
20
  log.info('The plan command requires an interactive terminal. Use --seed with --yes for non-interactive mode.');
20
21
  return;
21
22
  }
22
23
  const projectDir = process.cwd();
23
24
  const config = loadConfig({ dryRun: options.dryRun });
24
- // ── Seed description ───────────────────────────────────────────────────────
25
- let seedDescription;
26
- if (options.seed) {
27
- const seedPath = path.resolve(projectDir, options.seed);
28
- try {
29
- seedDescription = fs.readFileSync(seedPath, 'utf-8').trim();
30
- log.info(`Read seed from ${options.seed}`);
25
+ let draft;
26
+ let selectedIssues;
27
+ if (options.resume) {
28
+ // ── Resume from saved draft ───────────────────────────────────────────────
29
+ const saved = loadPlanDraft(projectDir);
30
+ if (!saved) {
31
+ log.error('No saved plan found at .alpha-loop/plan.json — nothing to resume.');
32
+ return;
31
33
  }
32
- catch {
33
- log.error(`Could not read seed file: ${options.seed}`);
34
+ draft = saved;
35
+ log.success(`Resumed plan: ${draft.milestones.length} milestone(s), ${draft.issues.length} issue(s)`);
36
+ console.log('');
37
+ console.log(formatIssueTable(draft.issues, draft.milestones));
38
+ console.log('');
39
+ if (options.dryRun) {
40
+ log.dry('Dry run — no GitHub resources will be created.');
34
41
  return;
35
42
  }
36
- }
37
- else if (options.yes) {
38
- log.error('--yes requires --seed <file> to provide a project description.');
39
- return;
43
+ if (options.yes) {
44
+ selectedIssues = draft.issues.filter((i) => i.selected);
45
+ log.info(`--yes: selecting ${selectedIssues.length} issue(s)`);
46
+ }
47
+ else {
48
+ const issueChoices = draft.issues.map((issue) => ({
49
+ name: `[${issue.priority}/${issue.complexity}] ${issue.title}`,
50
+ value: issue.id,
51
+ checked: issue.selected,
52
+ }));
53
+ const selectedIds = await checkbox({
54
+ message: 'Select issues to create:',
55
+ choices: issueChoices,
56
+ });
57
+ for (const issue of draft.issues) {
58
+ issue.selected = selectedIds.includes(issue.id);
59
+ }
60
+ selectedIssues = draft.issues.filter((i) => i.selected);
61
+ if (selectedIssues.length === 0) {
62
+ log.info('No issues selected.');
63
+ return;
64
+ }
65
+ const proceedConfirm = await confirm({
66
+ message: `Create ${selectedIssues.length} issue(s) on GitHub?`,
67
+ });
68
+ if (!proceedConfirm) {
69
+ log.info('Cancelled.');
70
+ return;
71
+ }
72
+ }
40
73
  }
41
74
  else {
42
- seedDescription = await input({
43
- message: 'Describe what you want to build:',
44
- });
45
- if (!seedDescription.trim()) {
46
- log.error('A description is required.');
75
+ // ── Normal flow: generate plan from seed ──────────────────────────────────
76
+ // ── Seed description ─────────────────────────────────────────────────────
77
+ let seedDescription;
78
+ if (options.seed) {
79
+ const seedPath = path.resolve(projectDir, options.seed);
80
+ try {
81
+ seedDescription = fs.readFileSync(seedPath, 'utf-8').trim();
82
+ log.info(`Read seed from ${options.seed}`);
83
+ }
84
+ catch {
85
+ log.error(`Could not read seed file: ${options.seed}`);
86
+ return;
87
+ }
88
+ }
89
+ else if (options.yes) {
90
+ log.error('--yes requires --seed <file> to provide a project description.');
47
91
  return;
48
92
  }
49
- }
50
- // ── Seed source selection ──────────────────────────────────────────────────
51
- const seedSources = options.yes
52
- ? (() => {
53
- log.info('--yes: selecting all seed sources (codebase, specs, issues)');
54
- return ['codebase', 'specs', 'issues'];
55
- })()
56
- : await checkbox({
57
- message: 'Select seed sources to include:',
58
- choices: [
59
- { name: 'Codebase scan (project context)', value: 'codebase' },
60
- { name: 'Spec files (glob pattern)', value: 'specs' },
61
- { name: 'Existing issues (avoid duplicates)', value: 'issues' },
62
- ],
63
- });
64
- // ── Gather seed data ──────────────────────────────────────────────────────
65
- let seedFiles = [];
66
- let existingIssues = [];
67
- if (seedSources.includes('specs')) {
68
- const globPattern = options.yes
69
- ? 'docs/**/*.md'
70
- : await input({
71
- message: 'Glob pattern for spec files (e.g. docs/**/*.md):',
93
+ else {
94
+ seedDescription = await input({
95
+ message: 'Describe what you want to build:',
72
96
  });
73
- if (globPattern.trim()) {
74
- seedFiles = readSeedFiles([globPattern.trim()], projectDir);
75
- log.info(`Found ${seedFiles.length} spec file(s)`);
97
+ if (!seedDescription.trim()) {
98
+ log.error('A description is required.');
99
+ return;
100
+ }
76
101
  }
77
- }
78
- if (seedSources.includes('issues')) {
79
- const issues = listOpenIssues(config.repo);
80
- existingIssues = issues.map((i) => ({ number: i.number, title: i.title }));
81
- log.info(`Loaded ${existingIssues.length} existing issue(s)`);
82
- }
83
- // ── Vision ─────────────────────────────────────────────────────────────────
84
- const contextDir = path.join(projectDir, '.alpha-loop');
85
- const visionFile = path.join(contextDir, 'vision.md');
86
- let visionContext = null;
87
- let projectContext = null;
88
- if (seedSources.includes('codebase')) {
89
- const ctx = buildPlanningContext(config);
90
- visionContext = ctx.visionContext;
91
- projectContext = ctx.projectContext;
92
- // Merge any existing issues from context if not already loaded
93
- if (!seedSources.includes('issues') && ctx.existingIssues.length > 0) {
94
- existingIssues = ctx.existingIssues.map((i) => ({ number: i.number, title: i.title }));
102
+ // ── Seed source selection ────────────────────────────────────────────────
103
+ const seedSources = options.yes
104
+ ? (() => {
105
+ log.info('--yes: selecting all seed sources (codebase, specs, issues)');
106
+ return ['codebase', 'specs', 'issues'];
107
+ })()
108
+ : await checkbox({
109
+ message: 'Select seed sources to include:',
110
+ choices: [
111
+ { name: 'Codebase scan (project context)', value: 'codebase' },
112
+ { name: 'Spec files (glob pattern)', value: 'specs' },
113
+ { name: 'Existing issues (avoid duplicates)', value: 'issues' },
114
+ ],
115
+ });
116
+ // ── Gather seed data ─────────────────────────────────────────────────────
117
+ let seedFiles = [];
118
+ let existingIssues = [];
119
+ if (seedSources.includes('specs')) {
120
+ const globPattern = options.yes
121
+ ? 'docs/**/*.md'
122
+ : await input({
123
+ message: 'Glob pattern for spec files (e.g. docs/**/*.md):',
124
+ });
125
+ if (globPattern.trim()) {
126
+ seedFiles = readSeedFiles([globPattern.trim()], projectDir);
127
+ log.info(`Found ${seedFiles.length} spec file(s)`);
128
+ }
95
129
  }
96
- }
97
- if (!fs.existsSync(visionFile) && (options.vision !== false || options.yes)) {
98
- if (options.yes)
99
- log.info('--yes: auto-generating vision document');
100
- log.step('Generating project vision...');
101
- const visionPrompt = `Based on this project description, generate a concise project vision document (under 500 words) in markdown.\n\nDescription: ${seedDescription}\n${projectContext ? `\nTechnical context:\n${projectContext}` : ''}`;
130
+ if (seedSources.includes('issues')) {
131
+ const issues = listOpenIssues(config.repo);
132
+ existingIssues = issues.map((i) => ({ number: i.number, title: i.title }));
133
+ log.info(`Loaded ${existingIssues.length} existing issue(s)`);
134
+ }
135
+ // ── Vision ───────────────────────────────────────────────────────────────
136
+ const contextDir = path.join(projectDir, '.alpha-loop');
137
+ const visionFile = path.join(contextDir, 'vision.md');
138
+ let visionContext = null;
139
+ let projectContext = null;
140
+ if (seedSources.includes('codebase')) {
141
+ const ctx = buildPlanningContext(config);
142
+ visionContext = ctx.visionContext;
143
+ projectContext = ctx.projectContext;
144
+ // Merge any existing issues from context if not already loaded
145
+ if (!seedSources.includes('issues') && ctx.existingIssues.length > 0) {
146
+ existingIssues = ctx.existingIssues.map((i) => ({ number: i.number, title: i.title }));
147
+ }
148
+ }
149
+ if (!fs.existsSync(visionFile) && (options.vision !== false || options.yes)) {
150
+ if (options.yes)
151
+ log.info('--yes: auto-generating vision document');
152
+ log.step('Generating project vision...');
153
+ const visionPrompt = `Based on this project description, generate a concise project vision document (under 500 words) in markdown.\n\nDescription: ${seedDescription}\n${projectContext ? `\nTechnical context:\n${projectContext}` : ''}`;
154
+ const safeModel = assertSafeShellArg(config.model, 'model');
155
+ const agentCmd = buildOneShotCommand(config.agent, safeModel);
156
+ const visionPromptFile = path.join(os.tmpdir(), `alpha-loop-prompt-${Date.now()}`);
157
+ fs.writeFileSync(visionPromptFile, visionPrompt, 'utf-8');
158
+ let visionResult;
159
+ try {
160
+ visionResult = exec(`${agentCmd} < "${visionPromptFile}" 2>/dev/null`, { cwd: projectDir, timeout: 5 * 60 * 1000 });
161
+ }
162
+ finally {
163
+ try {
164
+ fs.unlinkSync(visionPromptFile);
165
+ }
166
+ catch { /* cleanup best-effort */ }
167
+ }
168
+ if (visionResult.exitCode === 0 && visionResult.stdout) {
169
+ fs.mkdirSync(contextDir, { recursive: true });
170
+ fs.writeFileSync(visionFile, visionResult.stdout + '\n');
171
+ visionContext = visionResult.stdout;
172
+ log.success('Vision saved to .alpha-loop/vision.md');
173
+ }
174
+ else {
175
+ log.warn('Vision generation failed — continuing without vision');
176
+ }
177
+ }
178
+ else if (fs.existsSync(visionFile) && !visionContext) {
179
+ visionContext = fs.readFileSync(visionFile, 'utf-8');
180
+ }
181
+ // ── Fetch existing milestones ───────────────────────────────────────────
182
+ log.step('Fetching existing milestones...');
183
+ const existingMilestonesList = listMilestones(config.repo);
184
+ log.info(`Found ${existingMilestonesList.length} existing milestone(s)`);
185
+ const existingMilestonesForPrompt = existingMilestonesList.map((m) => ({
186
+ title: m.title,
187
+ description: m.description,
188
+ openIssues: m.openIssues,
189
+ }));
190
+ // ── AI plan generation ───────────────────────────────────────────────────
191
+ log.step('Generating project plan via AI agent...');
192
+ const planPrompt = buildPlanPrompt({
193
+ seedDescription,
194
+ seedFiles: seedFiles.length > 0 ? seedFiles : undefined,
195
+ visionContext,
196
+ projectContext,
197
+ existingIssues: existingIssues.length > 0 ? existingIssues : undefined,
198
+ existingMilestones: existingMilestonesForPrompt.length > 0 ? existingMilestonesForPrompt : undefined,
199
+ });
102
200
  const safeModel = assertSafeShellArg(config.model, 'model');
103
201
  const agentCmd = buildOneShotCommand(config.agent, safeModel);
104
- const visionPromptFile = path.join(os.tmpdir(), `alpha-loop-prompt-${Date.now()}`);
105
- fs.writeFileSync(visionPromptFile, visionPrompt, 'utf-8');
106
- let visionResult;
202
+ const planPromptFile = path.join(os.tmpdir(), `alpha-loop-prompt-${Date.now()}`);
203
+ fs.writeFileSync(planPromptFile, planPrompt, 'utf-8');
204
+ let planResult;
107
205
  try {
108
- visionResult = exec(`${agentCmd} < "${visionPromptFile}" 2>/dev/null`, { cwd: projectDir, timeout: 5 * 60 * 1000 });
206
+ planResult = exec(`${agentCmd} < "${planPromptFile}" 2>/dev/null`, { cwd: projectDir, timeout: 10 * 60 * 1000 });
109
207
  }
110
208
  finally {
111
209
  try {
112
- fs.unlinkSync(visionPromptFile);
210
+ fs.unlinkSync(planPromptFile);
113
211
  }
114
212
  catch { /* cleanup best-effort */ }
115
213
  }
116
- if (visionResult.exitCode === 0 && visionResult.stdout) {
117
- fs.mkdirSync(contextDir, { recursive: true });
118
- fs.writeFileSync(visionFile, visionResult.stdout + '\n');
119
- visionContext = visionResult.stdout;
120
- log.success('Vision saved to .alpha-loop/vision.md');
214
+ if (planResult.exitCode !== 0 || !planResult.stdout.trim()) {
215
+ log.error('Agent failed to generate a plan. Check agent configuration and try again.');
216
+ if (planResult.stderr)
217
+ log.error(planResult.stderr.slice(0, 500));
218
+ return;
219
+ }
220
+ try {
221
+ draft = extractJsonFromResponse(planResult.stdout);
222
+ }
223
+ catch (err) {
224
+ log.error(`Failed to parse plan JSON: ${err.message}`);
225
+ log.error(`Agent response (first 500 chars): ${planResult.stdout.slice(0, 500)}`);
226
+ return;
227
+ }
228
+ // ── Display plan ─────────────────────────────────────────────────────────
229
+ console.log('');
230
+ console.log(formatIssueTable(draft.issues, draft.milestones));
231
+ console.log('');
232
+ log.info(`Plan: ${draft.milestones.length} milestone(s), ${draft.issues.length} issue(s)`);
233
+ // ── Dry run exit ─────────────────────────────────────────────────────────
234
+ if (options.dryRun) {
235
+ log.dry('Dry run — no GitHub resources will be created.');
236
+ return;
237
+ }
238
+ // ── Review UX ────────────────────────────────────────────────────────────
239
+ if (options.yes) {
240
+ selectedIssues = draft.issues;
241
+ log.info(`--yes: selecting all ${selectedIssues.length} issue(s)`);
242
+ log.info('--yes: skipping body editing');
243
+ log.info(`--yes: creating ${draft.milestones.length} milestone(s) and ${selectedIssues.length} issue(s)`);
121
244
  }
122
245
  else {
123
- log.warn('Vision generation failed continuing without vision');
246
+ const issueChoices = draft.issues.map((issue) => ({
247
+ name: `[${issue.priority}/${issue.complexity}] ${issue.title}`,
248
+ value: issue.id,
249
+ checked: issue.selected,
250
+ }));
251
+ const selectedIds = await checkbox({
252
+ message: 'Select issues to create:',
253
+ choices: issueChoices,
254
+ });
255
+ // Update selected flags
256
+ for (const issue of draft.issues) {
257
+ issue.selected = selectedIds.includes(issue.id);
258
+ }
259
+ selectedIssues = draft.issues.filter((i) => i.selected);
260
+ // Offer to edit individual issue bodies
261
+ const wantsEdit = await confirm({
262
+ message: 'Edit any issue bodies before creating?',
263
+ default: false,
264
+ });
265
+ if (wantsEdit) {
266
+ for (const issue of selectedIssues) {
267
+ const edited = await editor({
268
+ message: `Edit body for: ${issue.title}`,
269
+ default: issue.body,
270
+ });
271
+ issue.body = edited;
272
+ }
273
+ }
274
+ const proceedConfirm = await confirm({
275
+ message: `Create ${draft.milestones.length} milestone(s) and ${selectedIssues.length} issue(s) on GitHub?`,
276
+ });
277
+ if (!proceedConfirm) {
278
+ log.info('Cancelled.');
279
+ return;
280
+ }
124
281
  }
282
+ // ── Save draft for recovery ──────────────────────────────────────────────
283
+ savePlanDraft(draft, projectDir);
284
+ log.info('Plan saved to .alpha-loop/plan.json');
125
285
  }
126
- else if (fs.existsSync(visionFile) && !visionContext) {
127
- visionContext = fs.readFileSync(visionFile, 'utf-8');
128
- }
129
- // ── AI plan generation ─────────────────────────────────────────────────────
130
- log.step('Generating project plan via AI agent...');
131
- const planPrompt = buildPlanPrompt({
132
- seedDescription,
133
- seedFiles: seedFiles.length > 0 ? seedFiles : undefined,
134
- visionContext,
135
- projectContext,
136
- existingIssues: existingIssues.length > 0 ? existingIssues : undefined,
137
- });
138
- const safeModel = assertSafeShellArg(config.model, 'model');
139
- const agentCmd = buildOneShotCommand(config.agent, safeModel);
140
- const planPromptFile = path.join(os.tmpdir(), `alpha-loop-prompt-${Date.now()}`);
141
- fs.writeFileSync(planPromptFile, planPrompt, 'utf-8');
142
- let planResult;
143
- try {
144
- planResult = exec(`${agentCmd} < "${planPromptFile}" 2>/dev/null`, { cwd: projectDir, timeout: 10 * 60 * 1000 });
145
- }
146
- finally {
147
- try {
148
- fs.unlinkSync(planPromptFile);
286
+ // ── GitHub execution (shared by normal + resume paths) ────────────────────
287
+ const failures = [];
288
+ // Fetch existing milestones for reuse
289
+ const existingMilestonesList = options.resume ? listMilestones(config.repo) : (
290
+ // In the normal flow we already fetched these above, but the variable
291
+ // is scoped inside the else block. Re-fetch here (cheap API call).
292
+ listMilestones(config.repo));
293
+ const existingMilestoneMap = new Map(existingMilestonesList.map((m) => [m.title.toLowerCase(), m]));
294
+ // ── Ensure labels exist ─────────────────────────────────────────────────
295
+ const allLabels = new Set();
296
+ for (const issue of selectedIssues) {
297
+ for (const label of issue.labels) {
298
+ allLabels.add(label);
149
299
  }
150
- catch { /* cleanup best-effort */ }
151
300
  }
152
- if (planResult.exitCode !== 0 || !planResult.stdout.trim()) {
153
- log.error('Agent failed to generate a plan. Check agent configuration and try again.');
154
- if (planResult.stderr)
155
- log.error(planResult.stderr.slice(0, 500));
156
- return;
157
- }
158
- let draft;
159
- try {
160
- draft = extractJsonFromResponse(planResult.stdout);
301
+ if (config.labelReady) {
302
+ allLabels.add(config.labelReady);
161
303
  }
162
- catch (err) {
163
- log.error(`Failed to parse plan JSON: ${err.message}`);
164
- log.error(`Agent response (first 500 chars): ${planResult.stdout.slice(0, 500)}`);
165
- return;
166
- }
167
- // ── Display plan ───────────────────────────────────────────────────────────
168
- console.log('');
169
- console.log(formatIssueTable(draft.issues, draft.milestones));
170
- console.log('');
171
- log.info(`Plan: ${draft.milestones.length} milestone(s), ${draft.issues.length} issue(s)`);
172
- // ── Dry run exit ───────────────────────────────────────────────────────────
173
- if (options.dryRun) {
174
- log.dry('Dry run — no GitHub resources will be created.');
175
- return;
304
+ if (allLabels.size > 0) {
305
+ log.step('Checking labels...');
306
+ const existingLabels = new Set(listLabels(config.repo).map((l) => l.toLowerCase()));
307
+ const missingLabels = [...allLabels].filter((l) => !existingLabels.has(l.toLowerCase()));
308
+ if (missingLabels.length > 0) {
309
+ log.info(`Creating ${missingLabels.length} missing label(s): ${missingLabels.join(', ')}`);
310
+ for (const label of missingLabels) {
311
+ if (!createLabel(config.repo, label)) {
312
+ failures.push(`Label "${label}": creation failed`);
313
+ }
314
+ }
315
+ }
176
316
  }
177
- // ── Review UX ──────────────────────────────────────────────────────────────
178
- let selectedIssues;
179
- if (options.yes) {
180
- selectedIssues = draft.issues;
181
- log.info(`--yes: selecting all ${selectedIssues.length} issue(s)`);
182
- log.info('--yes: skipping body editing');
183
- log.info(`--yes: creating ${draft.milestones.length} milestone(s) and ${selectedIssues.length} issue(s)`);
317
+ // Pre-flight budget check (only count new milestones)
318
+ const callsPerIssue = config.project > 0 ? 2 : 1; // createIssue + optional addToProject
319
+ const newMilestoneCount = draft.milestones.filter((ms) => !existingMilestoneMap.has(ms.title.toLowerCase())).length;
320
+ const estimatedCost = newMilestoneCount + (selectedIssues.length * callsPerIssue);
321
+ const budget = getRateLimitStatus();
322
+ if (estimatedCost > budget.remaining) {
323
+ const resetDate = new Date(budget.resetAt * 1000);
324
+ log.rate(`Budget warning: need ~${estimatedCost} calls but only ${budget.remaining}/${budget.limit} remaining. Resets at ${resetDate.toLocaleTimeString()}`);
325
+ log.rate('Proceeding with adaptive throttling — mutations may be delayed');
184
326
  }
185
327
  else {
186
- const issueChoices = draft.issues.map((issue) => ({
187
- name: `[${issue.priority}/${issue.complexity}] ${issue.title}`,
188
- value: issue.id,
189
- checked: issue.selected,
190
- }));
191
- const selectedIds = await checkbox({
192
- message: 'Select issues to create:',
193
- choices: issueChoices,
194
- });
195
- // Update selected flags
196
- for (const issue of draft.issues) {
197
- issue.selected = selectedIds.includes(issue.id);
198
- }
199
- selectedIssues = draft.issues.filter((i) => i.selected);
200
- // Offer to edit individual issue bodies
201
- const wantsEdit = await confirm({
202
- message: 'Edit any issue bodies before creating?',
203
- default: false,
204
- });
205
- if (wantsEdit) {
206
- for (const issue of selectedIssues) {
207
- const edited = await editor({
208
- message: `Edit body for: ${issue.title}`,
209
- default: issue.body,
210
- });
211
- issue.body = edited;
212
- }
213
- }
214
- const proceedConfirm = await confirm({
215
- message: `Create ${draft.milestones.length} milestone(s) and ${selectedIssues.length} issue(s) on GitHub?`,
216
- });
217
- if (!proceedConfirm) {
218
- log.info('Cancelled.');
219
- return;
220
- }
328
+ log.rate(`Budget OK: ~${estimatedCost} calls needed, ${budget.remaining}/${budget.limit} remaining`);
221
329
  }
222
- // ── Save draft for recovery ────────────────────────────────────────────────
223
- savePlanDraft(draft, projectDir);
224
- log.info('Plan saved to .alpha-loop/plan.json');
225
- // ── GitHub execution ───────────────────────────────────────────────────────
226
- const failures = [];
227
- const totalOps = draft.milestones.length + selectedIssues.length;
228
- const needsDelay = totalOps > 10;
229
- // Create milestones
230
- const milestoneMap = new Map();
231
- for (const ms of draft.milestones) {
330
+ // Create milestones (reuse existing ones by title match)
331
+ const availableMilestones = new Set();
332
+ for (let i = 0; i < draft.milestones.length; i++) {
333
+ const ms = draft.milestones[i];
334
+ const existing = existingMilestoneMap.get(ms.title.toLowerCase());
335
+ if (existing) {
336
+ availableMilestones.add(ms.title);
337
+ log.success(`Reusing existing milestone ${i + 1}/${draft.milestones.length}: ${ms.title} (#${existing.number})`);
338
+ continue;
339
+ }
232
340
  try {
233
341
  const msNum = createMilestone(config.repo, ms.title, ms.description, ms.dueOn ?? undefined);
234
342
  if (msNum > 0) {
235
- milestoneMap.set(ms.title, msNum);
236
- log.success(`Created milestone: ${ms.title}`);
343
+ availableMilestones.add(ms.title);
344
+ log.success(`Created milestone ${i + 1}/${draft.milestones.length}: ${ms.title}`);
237
345
  }
238
346
  else {
239
347
  failures.push(`Milestone "${ms.title}": creation returned 0`);
@@ -242,22 +350,22 @@ export async function planCommand(options) {
242
350
  catch (err) {
243
351
  failures.push(`Milestone "${ms.title}": ${err.message}`);
244
352
  }
245
- if (needsDelay)
246
- await delay(100);
247
353
  }
248
354
  // Create issues
249
355
  const createdIssues = [];
250
- for (const issue of selectedIssues) {
356
+ for (let i = 0; i < selectedIssues.length; i++) {
357
+ const issue = selectedIssues[i];
251
358
  try {
252
- const milestoneNum = milestoneMap.get(issue.milestone);
359
+ const milestoneTitle = availableMilestones.has(issue.milestone) ? issue.milestone : undefined;
253
360
  const labels = [...issue.labels];
254
361
  if (config.labelReady && !labels.includes(config.labelReady)) {
255
362
  labels.push(config.labelReady);
256
363
  }
257
- const issueNum = createIssue(config.repo, issue.title, issue.body, labels, milestoneNum);
364
+ const issueNum = createIssue(config.repo, issue.title, issue.body, labels, milestoneTitle);
258
365
  if (issueNum > 0) {
259
366
  createdIssues.push({ num: issueNum, title: issue.title });
260
- log.success(`Created issue #${issueNum}: ${issue.title}`);
367
+ const rateSt = getRateLimitStatus();
368
+ log.success(`Created issue ${i + 1}/${selectedIssues.length} #${issueNum}: ${issue.title} [rate: ${rateSt.remaining}/${rateSt.limit}]`);
261
369
  // Add to project board if configured
262
370
  if (config.project > 0) {
263
371
  try {
@@ -275,12 +383,10 @@ export async function planCommand(options) {
275
383
  catch (err) {
276
384
  failures.push(`Issue "${issue.title}": ${err.message}`);
277
385
  }
278
- if (needsDelay)
279
- await delay(100);
280
386
  }
281
387
  // ── Summary ────────────────────────────────────────────────────────────────
282
388
  console.log('');
283
- log.success(`Created ${milestoneMap.size} milestone(s) and ${createdIssues.length} issue(s)`);
389
+ log.success(`Created ${availableMilestones.size} milestone(s) and ${createdIssues.length} issue(s)`);
284
390
  for (const ci of createdIssues) {
285
391
  console.log(` https://github.com/${config.repo}/issues/${ci.num} ${ci.title}`);
286
392
  }
@@ -292,7 +398,4 @@ export async function planCommand(options) {
292
398
  }
293
399
  }
294
400
  }
295
- function delay(ms) {
296
- return new Promise((resolve) => setTimeout(resolve, ms));
297
- }
298
401
  //# sourceMappingURL=plan.js.map