@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.
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.js +6 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.d.ts +1 -0
- package/dist/commands/plan.js +298 -195
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/resume.js +6 -5
- package/dist/commands/resume.js.map +1 -1
- package/dist/commands/roadmap.js +2 -3
- package/dist/commands/roadmap.js.map +1 -1
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +33 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/triage.js +12 -16
- package/dist/commands/triage.js.map +1 -1
- package/dist/commands/vision.js +4 -3
- package/dist/commands/vision.js.map +1 -1
- package/dist/lib/github.d.ts +16 -3
- package/dist/lib/github.js +151 -75
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/learning.d.ts +13 -1
- package/dist/lib/learning.js +90 -17
- package/dist/lib/learning.js.map +1 -1
- package/dist/lib/logger.d.ts +1 -0
- package/dist/lib/logger.js +2 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/pipeline.js +116 -44
- package/dist/lib/pipeline.js.map +1 -1
- package/dist/lib/planning.d.ts +5 -0
- package/dist/lib/planning.js +14 -0
- package/dist/lib/planning.js.map +1 -1
- package/dist/lib/prompts.d.ts +5 -0
- package/dist/lib/prompts.js +18 -5
- package/dist/lib/prompts.js.map +1 -1
- package/dist/lib/rate-limit.d.ts +55 -0
- package/dist/lib/rate-limit.js +188 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/session.js +2 -1
- package/dist/lib/session.js.map +1 -1
- package/dist/lib/validation.d.ts +69 -0
- package/dist/lib/validation.js +280 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/lib/worktree.d.ts +16 -0
- package/dist/lib/worktree.js +99 -31
- package/dist/lib/worktree.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/reviewer.md +7 -0
package/dist/commands/plan.js
CHANGED
|
@@ -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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
25
|
-
let
|
|
26
|
-
if (options.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
log.
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
if (!seedDescription.trim()) {
|
|
98
|
+
log.error('A description is required.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
log.info(
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
105
|
-
fs.writeFileSync(
|
|
106
|
-
let
|
|
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
|
-
|
|
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(
|
|
210
|
+
fs.unlinkSync(planPromptFile);
|
|
113
211
|
}
|
|
114
212
|
catch { /* cleanup best-effort */ }
|
|
115
213
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 (
|
|
153
|
-
|
|
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
|
-
|
|
163
|
-
log.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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 (
|
|
356
|
+
for (let i = 0; i < selectedIssues.length; i++) {
|
|
357
|
+
const issue = selectedIssues[i];
|
|
251
358
|
try {
|
|
252
|
-
const
|
|
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,
|
|
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
|
-
|
|
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 ${
|
|
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
|