mstro-app 0.4.39 → 0.4.43
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/bin/commands/login.js +17 -7
- package/bin/commands/logout.js +14 -6
- package/bin/commands/status.js +9 -3
- package/bin/commands/whoami.js +10 -4
- package/bin/mstro.js +11 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts +1 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +2 -0
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
- package/dist/server/cli/headless/resilient-runner.js +234 -0
- package/dist/server/cli/headless/resilient-runner.js.map +1 -0
- package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
- package/dist/server/cli/headless/retry-strategies.js +262 -0
- package/dist/server/cli/headless/retry-strategies.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +5 -0
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +31 -4
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +1 -30
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +16 -3
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/prompt-builders.d.ts.map +1 -1
- package/dist/server/cli/prompt-builders.js +31 -13
- package/dist/server/cli/prompt-builders.js.map +1 -1
- package/dist/server/index.js +1 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +5 -4
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +1 -1
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +14 -8
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-patterns.js +1 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +19 -9
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +6 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +158 -76
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +1 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +6 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts +11 -0
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
- package/dist/server/services/plan/issue-classification.js +20 -0
- package/dist/server/services/plan/issue-classification.js.map +1 -0
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +7 -4
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +0 -5
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +12 -241
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +9 -6
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +11 -4
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +7 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +2 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +18 -7
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.js +90 -42
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +48 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +48 -1
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +74 -32
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +18 -18
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +52 -41
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/skill-watcher.js +85 -0
- package/dist/server/services/websocket/skill-watcher.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +2 -268
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +0 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-stream.ts +1 -0
- package/server/cli/headless/index.ts +2 -0
- package/server/cli/headless/resilient-runner.ts +354 -0
- package/server/cli/headless/retry-strategies.ts +330 -0
- package/server/cli/headless/stall-assessor.ts +5 -0
- package/server/cli/headless/tool-watchdog.ts +40 -4
- package/server/cli/improvisation-retry.ts +1 -32
- package/server/cli/improvisation-session-manager.ts +17 -3
- package/server/cli/prompt-builders.ts +33 -12
- package/server/index.ts +1 -9
- package/server/mcp/bouncer-cli.ts +5 -4
- package/server/mcp/bouncer-haiku.ts +1 -1
- package/server/mcp/bouncer-integration.ts +15 -8
- package/server/mcp/security-patterns.ts +1 -1
- package/server/services/plan/agents/code-review.md +109 -0
- package/server/services/plan/agents/commit-message.md +26 -0
- package/server/services/plan/agents/fix-quality.md +24 -0
- package/server/services/plan/agents/pr-description.md +28 -0
- package/server/services/plan/composer.ts +20 -9
- package/server/services/plan/executor.ts +160 -76
- package/server/services/plan/front-matter.ts +7 -0
- package/server/services/plan/issue-classification.ts +21 -0
- package/server/services/plan/issue-prompt-builder.ts +8 -4
- package/server/services/plan/issue-retry.ts +15 -330
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/review-gate.ts +9 -6
- package/server/services/plan/types.ts +3 -0
- package/server/services/platform-credentials.ts +10 -4
- package/server/services/terminal/pty-manager.ts +7 -1
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +18 -8
- package/server/services/websocket/plan-execution-handlers.ts +7 -7
- package/server/services/websocket/quality-fix-agent.ts +86 -44
- package/server/services/websocket/quality-handlers.ts +48 -7
- package/server/services/websocket/quality-persistence.ts +75 -1
- package/server/services/websocket/quality-review-agent.ts +70 -31
- package/server/services/websocket/quality-tools.ts +16 -14
- package/server/services/websocket/skill-handlers.ts +50 -40
- package/server/services/websocket/skill-watcher.ts +79 -0
- package/server/services/websocket/types.ts +0 -311
- package/dist/server/services/deploy/ai-broker.d.ts +0 -63
- package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
- package/dist/server/services/deploy/ai-broker.js +0 -360
- package/dist/server/services/deploy/ai-broker.js.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.js +0 -621
- package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
- package/dist/server/services/deploy/credentials.d.ts +0 -35
- package/dist/server/services/deploy/credentials.d.ts.map +0 -1
- package/dist/server/services/deploy/credentials.js +0 -177
- package/dist/server/services/deploy/credentials.js.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.js +0 -294
- package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.js +0 -266
- package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.js +0 -409
- package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
- package/server/cli/headless/RESEARCH.md +0 -627
- package/server/services/deploy/ai-broker.ts +0 -512
- package/server/services/deploy/board-execution-handler.ts +0 -847
- package/server/services/deploy/credentials.ts +0 -200
- package/server/services/deploy/deploy-ai-service.ts +0 -401
- package/server/services/deploy/headless-session-handler.ts +0 -414
- package/server/services/websocket/deploy-handlers.ts +0 -544
- package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { EventEmitter } from 'node:events';
|
|
20
|
-
import { existsSync,
|
|
21
|
-
import {
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { join, resolve } from 'node:path';
|
|
22
23
|
import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
|
|
23
24
|
import { ConfigInstaller } from './config-installer.js';
|
|
24
25
|
import { resolveReadyToWork } from './dependency-resolver.js';
|
|
25
|
-
import { checkAllAcceptanceCriteria, replaceFrontMatterField,
|
|
26
|
+
import { checkAllAcceptanceCriteria, replaceFrontMatterField, setFrontMatterFieldAsync } from './front-matter.js';
|
|
26
27
|
import { buildIssuePrompt } from './issue-prompt-builder.js';
|
|
27
28
|
import { runIssueWithRetry } from './issue-retry.js';
|
|
28
29
|
import { listExistingDocs, publishOutputs, resolveOutputPath } from './output-manager.js';
|
|
@@ -90,6 +91,14 @@ export class PlanExecutor extends EventEmitter {
|
|
|
90
91
|
this.configInstaller = new ConfigInstaller(workingDir);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
private validateIssuePath(issuePath: string, baseDir: string): string {
|
|
95
|
+
const fullPath = join(baseDir, issuePath);
|
|
96
|
+
if (!resolve(fullPath).startsWith(resolve(baseDir))) {
|
|
97
|
+
throw new Error(`Invalid issue path: path traversal detected in "${issuePath}"`);
|
|
98
|
+
}
|
|
99
|
+
return fullPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
getStatus(): ExecutionStatus { return this.status; }
|
|
94
103
|
getMetrics(): ExecutionMetrics { return { ...this.metrics }; }
|
|
95
104
|
|
|
@@ -129,15 +138,17 @@ export class PlanExecutor extends EventEmitter {
|
|
|
129
138
|
this.pmDir = resolvePmDir(this.workingDir);
|
|
130
139
|
this.boardDir = this.resolveBoardDir();
|
|
131
140
|
|
|
132
|
-
this.recoverStaleIssues();
|
|
141
|
+
await this.recoverStaleIssues();
|
|
133
142
|
|
|
134
143
|
const stallResult = await this.runWaveLoop();
|
|
135
144
|
|
|
136
145
|
this.metrics.totalDuration = Date.now() - startTime;
|
|
137
146
|
|
|
138
|
-
if (stallResult === 'stalled') {
|
|
147
|
+
if (stallResult === 'stalled' || stallResult === 'dead') {
|
|
139
148
|
this.status = 'error';
|
|
140
|
-
|
|
149
|
+
if (stallResult === 'stalled') {
|
|
150
|
+
this.emit('error', `Execution stalled: ${MAX_CONSECUTIVE_EMPTY_WAVES} consecutive waves completed zero issues. Issues may be stuck in review or failing repeatedly.`);
|
|
151
|
+
}
|
|
141
152
|
} else if (this.shouldPause) {
|
|
142
153
|
this.status = 'paused';
|
|
143
154
|
} else if (this.shouldStop) {
|
|
@@ -148,14 +159,17 @@ export class PlanExecutor extends EventEmitter {
|
|
|
148
159
|
this.emit('statusChanged', this.status);
|
|
149
160
|
}
|
|
150
161
|
|
|
151
|
-
/** Run waves until done, paused, stopped, or stalled.
|
|
152
|
-
private async runWaveLoop(): Promise<'done' | 'stalled'> {
|
|
162
|
+
/** Run waves until done, paused, stopped, or stalled. */
|
|
163
|
+
private async runWaveLoop(): Promise<'done' | 'stalled' | 'dead'> {
|
|
153
164
|
let consecutiveZeroCompletions = 0;
|
|
154
|
-
const maxParallel = this.getBoardMaxParallelAgents();
|
|
165
|
+
const maxParallel = await this.getBoardMaxParallelAgents();
|
|
155
166
|
|
|
156
167
|
while (!this.shouldStop && !this.shouldPause) {
|
|
157
|
-
const readyIssues = this.pickReadyIssues();
|
|
158
|
-
if (readyIssues.length === 0)
|
|
168
|
+
const readyIssues = await this.pickReadyIssues();
|
|
169
|
+
if (readyIssues.length === 0) {
|
|
170
|
+
// pickReadyIssues emits 'error' for dead state, 'complete' otherwise — check if dead
|
|
171
|
+
return await this.hasDeadIssues() ? 'dead' : 'done';
|
|
172
|
+
}
|
|
159
173
|
|
|
160
174
|
const completedCount = await this.executeWave(readyIssues.slice(0, maxParallel));
|
|
161
175
|
|
|
@@ -169,6 +183,18 @@ export class PlanExecutor extends EventEmitter {
|
|
|
169
183
|
return 'done';
|
|
170
184
|
}
|
|
171
185
|
|
|
186
|
+
private async hasDeadIssues(): Promise<boolean> {
|
|
187
|
+
const pmDir = this.pmDir;
|
|
188
|
+
if (!pmDir) return false;
|
|
189
|
+
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
190
|
+
const issues = effectiveBoardId
|
|
191
|
+
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
192
|
+
: this.loadProjectIssues();
|
|
193
|
+
if (!issues) return false;
|
|
194
|
+
const terminalStatuses = new Set(['done', 'cancelled']);
|
|
195
|
+
return issues.some(i => i.type !== 'epic' && !terminalStatuses.has(i.status) && i.status !== 'todo');
|
|
196
|
+
}
|
|
197
|
+
|
|
172
198
|
pause(): void { this.shouldPause = true; }
|
|
173
199
|
stop(): void {
|
|
174
200
|
this.shouldStop = true;
|
|
@@ -199,11 +225,11 @@ export class PlanExecutor extends EventEmitter {
|
|
|
199
225
|
// Create abort controller for this wave — stop() will abort it
|
|
200
226
|
this.waveAbortController = new AbortController();
|
|
201
227
|
|
|
202
|
-
this.ensureOutputDirs();
|
|
228
|
+
await this.ensureOutputDirs();
|
|
203
229
|
this.configInstaller.installPermissions();
|
|
204
230
|
|
|
205
231
|
for (const issue of issues) {
|
|
206
|
-
this.updateIssueFrontMatter(issue.path, 'in_progress');
|
|
232
|
+
await this.updateIssueFrontMatter(issue.path, 'in_progress');
|
|
207
233
|
}
|
|
208
234
|
|
|
209
235
|
const existingDocs = listExistingDocs(this.workingDir, this.boardDir);
|
|
@@ -234,13 +260,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
234
260
|
issueIds: waveIds,
|
|
235
261
|
error: error instanceof Error ? error.message : String(error),
|
|
236
262
|
});
|
|
237
|
-
this.revertIncompleteIssues(issues);
|
|
263
|
+
await this.revertIncompleteIssues(issues);
|
|
238
264
|
} finally {
|
|
239
265
|
this.configInstaller.uninstallPermissions();
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
this.waveAbortController = null;
|
|
243
|
-
this.finalizeWave(issues, waveStart, waveLabel);
|
|
269
|
+
await this.finalizeWave(issues, waveStart, waveLabel);
|
|
244
270
|
this.metrics.currentWaveIds = [];
|
|
245
271
|
return completedCount;
|
|
246
272
|
}
|
|
@@ -288,7 +314,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
288
314
|
* Post-wave operations wrapped individually so a failure in one
|
|
289
315
|
* doesn't prevent the others or kill the while loop in start().
|
|
290
316
|
*/
|
|
291
|
-
private finalizeWave(issues: Issue[], waveStart: number, waveLabel: string): void {
|
|
317
|
+
private async finalizeWave(issues: Issue[], waveStart: number, waveLabel: string): Promise<void> {
|
|
292
318
|
try {
|
|
293
319
|
reconcileState(this.workingDir, this.boardId ?? undefined);
|
|
294
320
|
this.emit('stateUpdated');
|
|
@@ -311,7 +337,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
311
337
|
}
|
|
312
338
|
|
|
313
339
|
try {
|
|
314
|
-
this.appendProgressEntry(issues, waveStart);
|
|
340
|
+
await this.appendProgressEntry(issues, waveStart);
|
|
315
341
|
} catch (err) {
|
|
316
342
|
this.emit('output', {
|
|
317
343
|
issueId: waveLabel,
|
|
@@ -334,16 +360,16 @@ export class PlanExecutor extends EventEmitter {
|
|
|
334
360
|
let completed = 0;
|
|
335
361
|
|
|
336
362
|
for (const issue of issues) {
|
|
337
|
-
const fullPath =
|
|
363
|
+
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
338
364
|
try {
|
|
339
|
-
const content =
|
|
365
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
340
366
|
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
341
367
|
const currentStatus = statusMatch?.[1] ?? 'unknown';
|
|
342
368
|
|
|
343
369
|
if (currentStatus === 'in_review' || currentStatus === 'done') {
|
|
344
370
|
if (issue.reviewGate === 'none') {
|
|
345
371
|
// Skip review gate — mark done directly
|
|
346
|
-
this.updateIssueFrontMatter(issue.path, 'done');
|
|
372
|
+
await this.updateIssueFrontMatter(issue.path, 'done');
|
|
347
373
|
this.metrics.issuesCompleted++;
|
|
348
374
|
this.emit('issueCompleted', issue);
|
|
349
375
|
completed++;
|
|
@@ -351,7 +377,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
351
377
|
completed += await this.runReviewGate(issue, pmDir);
|
|
352
378
|
}
|
|
353
379
|
} else {
|
|
354
|
-
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
380
|
+
await this.updateIssueFrontMatter(issue.path, issue.status);
|
|
355
381
|
this.emit('issueError', {
|
|
356
382
|
issueId: issue.id,
|
|
357
383
|
error: 'Issue did not complete during wave execution',
|
|
@@ -370,13 +396,19 @@ export class PlanExecutor extends EventEmitter {
|
|
|
370
396
|
const reviewDir = this.boardDir ?? pmDir;
|
|
371
397
|
const attempts = getReviewAttemptCount(reviewDir, issue);
|
|
372
398
|
if (attempts >= MAX_REVIEW_ATTEMPTS) {
|
|
373
|
-
this.updateIssueFrontMatter(issue.path, '
|
|
399
|
+
await this.updateIssueFrontMatter(issue.path, 'cancelled');
|
|
400
|
+
await this.appendCancellationNote(issue, pmDir, `Cancelled after ${MAX_REVIEW_ATTEMPTS} failed reviews — issue may need restructuring`);
|
|
374
401
|
this.emit('reviewProgress', { issueId: issue.id, status: 'max_attempts' });
|
|
375
|
-
this.emit('
|
|
402
|
+
this.emit('issueAbandoned', {
|
|
403
|
+
issueId: issue.id,
|
|
404
|
+
reason: `Review failed ${MAX_REVIEW_ATTEMPTS} times — cancelled to unblock dependents`,
|
|
405
|
+
attempts,
|
|
406
|
+
});
|
|
407
|
+
this.emit('output', { issueId: issue.id, text: `Review: max attempts reached, cancelling issue to unblock dependents` });
|
|
376
408
|
return 0;
|
|
377
409
|
}
|
|
378
410
|
|
|
379
|
-
this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
411
|
+
await this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
380
412
|
this.emit('reviewProgress', { issueId: issue.id, status: 'reviewing' });
|
|
381
413
|
|
|
382
414
|
const outputPath = resolveOutputPath(issue, this.workingDir, this.boardDir);
|
|
@@ -387,21 +419,21 @@ export class PlanExecutor extends EventEmitter {
|
|
|
387
419
|
outputPath,
|
|
388
420
|
onOutput: (text) => this.emit('output', { issueId: issue.id, text }),
|
|
389
421
|
logDir: this.boardDir ? join(this.boardDir, 'logs') : undefined,
|
|
390
|
-
reviewCriteria: this.getBoardReviewCriteria(),
|
|
422
|
+
reviewCriteria: await this.getBoardReviewCriteria(),
|
|
391
423
|
boardDir: this.boardDir,
|
|
392
424
|
extraEnv: this.extraEnv,
|
|
393
425
|
});
|
|
394
426
|
persistReviewResult(reviewDir, issue, result);
|
|
395
427
|
|
|
396
428
|
if (result.passed) {
|
|
397
|
-
this.updateIssueFrontMatter(issue.path, 'done');
|
|
429
|
+
await this.updateIssueFrontMatter(issue.path, 'done');
|
|
398
430
|
this.metrics.issuesCompleted++;
|
|
399
431
|
this.emit('reviewProgress', { issueId: issue.id, status: 'passed' });
|
|
400
432
|
this.emit('issueCompleted', issue);
|
|
401
433
|
return 1;
|
|
402
434
|
}
|
|
403
435
|
|
|
404
|
-
this.updateIssueFrontMatter(issue.path, 'todo');
|
|
436
|
+
await this.updateIssueFrontMatter(issue.path, 'todo');
|
|
405
437
|
appendReviewFeedback(pmDir, issue, result);
|
|
406
438
|
this.emit('reviewProgress', { issueId: issue.id, status: 'failed' });
|
|
407
439
|
this.emit('issueError', {
|
|
@@ -419,13 +451,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
419
451
|
* these issues block the dependency graph and cause the executor to
|
|
420
452
|
* find zero ready issues, making "Implement" appear to do nothing.
|
|
421
453
|
*/
|
|
422
|
-
private recoverStaleIssues(): void {
|
|
454
|
+
private async recoverStaleIssues(): Promise<void> {
|
|
423
455
|
const pmDir = this.pmDir;
|
|
424
456
|
if (!pmDir) return;
|
|
425
457
|
|
|
426
458
|
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
427
459
|
const issues = effectiveBoardId
|
|
428
|
-
? this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
460
|
+
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
429
461
|
: this.loadProjectIssues();
|
|
430
462
|
|
|
431
463
|
if (!issues) return;
|
|
@@ -436,7 +468,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
436
468
|
for (const issue of issues) {
|
|
437
469
|
if (issue.type === 'epic') continue;
|
|
438
470
|
if (staleStatuses.has(issue.status)) {
|
|
439
|
-
this.updateIssueFrontMatter(issue.path, 'todo');
|
|
471
|
+
await this.updateIssueFrontMatter(issue.path, 'todo');
|
|
440
472
|
recovered.push(`${issue.id} (${issue.status} → todo)`);
|
|
441
473
|
}
|
|
442
474
|
}
|
|
@@ -453,28 +485,28 @@ export class PlanExecutor extends EventEmitter {
|
|
|
453
485
|
// ── Helpers ──────────────────────────────────────────────────
|
|
454
486
|
|
|
455
487
|
/** Read the board's maxParallelAgents setting, falling back to default. */
|
|
456
|
-
private getBoardMaxParallelAgents(): number {
|
|
488
|
+
private async getBoardMaxParallelAgents(): Promise<number> {
|
|
457
489
|
const pmDir = this.pmDir;
|
|
458
490
|
if (!pmDir) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
459
491
|
|
|
460
492
|
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
461
493
|
if (!effectiveBoardId) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
462
494
|
|
|
463
|
-
// Read only board.md — avoids parsing STATE.md and all backlog issues just for one setting
|
|
464
495
|
const boardMdPath = join(pmDir, 'boards', effectiveBoardId, 'board.md');
|
|
465
496
|
if (!existsSync(boardMdPath)) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
466
497
|
|
|
467
498
|
try {
|
|
468
|
-
const content =
|
|
499
|
+
const content = await readFile(boardMdPath, 'utf-8');
|
|
469
500
|
const match = content.match(/^max_parallel_agents:\s*(\d+)/m);
|
|
470
501
|
return match ? Math.max(1, Math.min(Number(match[1]), 10)) : DEFAULT_MAX_PARALLEL_AGENTS;
|
|
471
|
-
} catch {
|
|
502
|
+
} catch (err) {
|
|
503
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to read board max_parallel_agents: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
472
504
|
return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
473
505
|
}
|
|
474
506
|
}
|
|
475
507
|
|
|
476
508
|
/** Read the board's custom review criteria, if set. */
|
|
477
|
-
private getBoardReviewCriteria(): string | undefined {
|
|
509
|
+
private async getBoardReviewCriteria(): Promise<string | undefined> {
|
|
478
510
|
const pmDir = this.pmDir;
|
|
479
511
|
if (!pmDir) return undefined;
|
|
480
512
|
|
|
@@ -485,17 +517,18 @@ export class PlanExecutor extends EventEmitter {
|
|
|
485
517
|
if (!existsSync(boardMdPath)) return undefined;
|
|
486
518
|
|
|
487
519
|
try {
|
|
488
|
-
const content =
|
|
520
|
+
const content = await readFile(boardMdPath, 'utf-8');
|
|
489
521
|
const match = content.match(/^review_criteria:\s*"(.+)"/m);
|
|
490
522
|
if (!match) return undefined;
|
|
491
523
|
const raw = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
|
|
492
524
|
return raw || undefined;
|
|
493
|
-
} catch {
|
|
525
|
+
} catch (err) {
|
|
526
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to read board review criteria: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
494
527
|
return undefined;
|
|
495
528
|
}
|
|
496
529
|
}
|
|
497
530
|
|
|
498
|
-
private pickReadyIssues(): Issue[] {
|
|
531
|
+
private async pickReadyIssues(): Promise<Issue[]> {
|
|
499
532
|
const pmDir = this.pmDir;
|
|
500
533
|
if (!pmDir) {
|
|
501
534
|
this.emit('error', 'No PM directory found');
|
|
@@ -504,23 +537,28 @@ export class PlanExecutor extends EventEmitter {
|
|
|
504
537
|
|
|
505
538
|
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
506
539
|
const issues = effectiveBoardId
|
|
507
|
-
? this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
540
|
+
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
508
541
|
: this.loadProjectIssues();
|
|
509
542
|
|
|
510
543
|
if (!issues) return [];
|
|
511
544
|
|
|
512
545
|
const readyIssues = resolveReadyToWork(issues, this.epicScope ?? undefined);
|
|
513
546
|
if (readyIssues.length === 0) {
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
this.
|
|
547
|
+
const deadState = this.detectDeadState(issues);
|
|
548
|
+
if (deadState) {
|
|
549
|
+
this.emit('error', deadState);
|
|
550
|
+
} else {
|
|
551
|
+
this.emit('complete', this.buildCompletionReason(issues));
|
|
552
|
+
if (effectiveBoardId) {
|
|
553
|
+
await this.tryCompleteBoardIfDone(pmDir, effectiveBoardId, issues);
|
|
554
|
+
}
|
|
517
555
|
}
|
|
518
556
|
}
|
|
519
557
|
return readyIssues;
|
|
520
558
|
}
|
|
521
559
|
|
|
522
560
|
/** Load issues from a specific board, auto-activating draft boards. Returns null on error. */
|
|
523
|
-
private loadBoardIssues(pmDir: string, boardId: string): Issue[] | null {
|
|
561
|
+
private async loadBoardIssues(pmDir: string, boardId: string): Promise<Issue[] | null> {
|
|
524
562
|
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
525
563
|
if (!boardState) {
|
|
526
564
|
this.emit('error', `Board not found: ${boardId}`);
|
|
@@ -531,7 +569,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
531
569
|
return null;
|
|
532
570
|
}
|
|
533
571
|
if (boardState.board.status === 'draft') {
|
|
534
|
-
this.activateBoard(pmDir, boardId);
|
|
572
|
+
await this.activateBoard(pmDir, boardId);
|
|
535
573
|
} else if (boardState.board.status !== 'active') {
|
|
536
574
|
this.emit('error', `Board ${boardId} is not active (status: ${boardState.board.status})`);
|
|
537
575
|
return null;
|
|
@@ -554,17 +592,19 @@ export class PlanExecutor extends EventEmitter {
|
|
|
554
592
|
}
|
|
555
593
|
|
|
556
594
|
/** Activate a draft board by updating its status in board.md. */
|
|
557
|
-
private activateBoard(pmDir: string, boardId: string): void {
|
|
595
|
+
private async activateBoard(pmDir: string, boardId: string): Promise<void> {
|
|
558
596
|
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
559
597
|
if (!existsSync(boardMdPath)) return;
|
|
560
598
|
try {
|
|
561
|
-
const content =
|
|
562
|
-
|
|
563
|
-
} catch {
|
|
599
|
+
const content = await readFile(boardMdPath, 'utf-8');
|
|
600
|
+
await writeFile(boardMdPath, replaceFrontMatterField(content, 'status', 'active'), 'utf-8');
|
|
601
|
+
} catch (err) {
|
|
602
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to activate board ${boardId}: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
603
|
+
}
|
|
564
604
|
}
|
|
565
605
|
|
|
566
606
|
/** Check if all issues in a board are done and mark board as completed. */
|
|
567
|
-
private tryCompleteBoardIfDone(pmDir: string, boardId: string, issues: Issue[]): void {
|
|
607
|
+
private async tryCompleteBoardIfDone(pmDir: string, boardId: string, issues: Issue[]): Promise<void> {
|
|
568
608
|
const allDone = issues.length > 0 && issues.every(i => i.status === 'done' || i.status === 'cancelled');
|
|
569
609
|
if (!allDone) return;
|
|
570
610
|
|
|
@@ -572,11 +612,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
572
612
|
if (!existsSync(boardMdPath)) return;
|
|
573
613
|
|
|
574
614
|
try {
|
|
575
|
-
let content =
|
|
615
|
+
let content = await readFile(boardMdPath, 'utf-8');
|
|
576
616
|
content = replaceFrontMatterField(content, 'status', 'completed');
|
|
577
617
|
content = replaceFrontMatterField(content, 'completed_at', `"${new Date().toISOString()}"`);
|
|
578
|
-
|
|
579
|
-
} catch {
|
|
618
|
+
await writeFile(boardMdPath, content, 'utf-8');
|
|
619
|
+
} catch (err) {
|
|
620
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to mark board ${boardId} as completed: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
621
|
+
}
|
|
580
622
|
}
|
|
581
623
|
|
|
582
624
|
private resolveActiveBoardId(): string | null {
|
|
@@ -601,54 +643,95 @@ export class PlanExecutor extends EventEmitter {
|
|
|
601
643
|
return this.epicScope ? 'All epic issues are done or blocked' : 'All work is done or blocked';
|
|
602
644
|
}
|
|
603
645
|
|
|
604
|
-
|
|
646
|
+
/** Detect issues stuck in non-terminal states with no path to completion. */
|
|
647
|
+
private detectDeadState(issues: Issue[]): string | null {
|
|
648
|
+
const nonEpic = issues.filter(i => i.type !== 'epic');
|
|
649
|
+
const terminalStatuses = new Set(['done', 'cancelled']);
|
|
650
|
+
const stuck = nonEpic.filter(i => !terminalStatuses.has(i.status) && i.status !== 'todo');
|
|
651
|
+
|
|
652
|
+
if (stuck.length === 0) return null;
|
|
653
|
+
|
|
654
|
+
const stuckIds = stuck.map(i => `${i.id} (${i.status})`).join(', ');
|
|
655
|
+
|
|
656
|
+
const issueByPath = new Map(issues.map(i => [i.path, i]));
|
|
657
|
+
const blockedByStuck = nonEpic.filter(i => {
|
|
658
|
+
if (i.status !== 'todo') return false;
|
|
659
|
+
return i.blockedBy.some(bp => {
|
|
660
|
+
const blocker = issueByPath.get(bp);
|
|
661
|
+
return blocker && !terminalStatuses.has(blocker.status);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
const blockedIds = blockedByStuck.map(i => i.id).join(', ');
|
|
665
|
+
|
|
666
|
+
return `Board stuck: ${stuckIds} cannot progress${blockedIds ? `. Blocking: ${blockedIds}` : ''}`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private async revertIncompleteIssues(issues: Issue[]): Promise<void> {
|
|
605
670
|
const pmDir = this.pmDir;
|
|
606
671
|
if (!pmDir) return;
|
|
607
672
|
for (const issue of issues) {
|
|
608
|
-
const fullPath =
|
|
673
|
+
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
609
674
|
try {
|
|
610
|
-
const content =
|
|
675
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
611
676
|
if (content.match(/^status:\s*in_progress$/m)) {
|
|
612
|
-
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
677
|
+
await this.updateIssueFrontMatter(issue.path, issue.status);
|
|
613
678
|
}
|
|
614
|
-
} catch {
|
|
679
|
+
} catch (err) {
|
|
680
|
+
this.emit('output', { issueId: issue.id, text: `Warning: failed to revert issue status: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
681
|
+
}
|
|
615
682
|
}
|
|
616
683
|
}
|
|
617
684
|
|
|
618
|
-
private
|
|
685
|
+
private async appendCancellationNote(issue: Issue, pmDir: string, reason: string): Promise<void> {
|
|
686
|
+
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
687
|
+
try {
|
|
688
|
+
let content = await readFile(fullPath, 'utf-8');
|
|
689
|
+
const entry = `- Cancelled (${new Date().toISOString().split('T')[0]}): ${reason}`;
|
|
690
|
+
if (content.includes('## Activity')) {
|
|
691
|
+
content = content.replace(/## Activity/, `## Activity\n${entry}`);
|
|
692
|
+
} else {
|
|
693
|
+
content += `\n\n## Activity\n${entry}`;
|
|
694
|
+
}
|
|
695
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
696
|
+
} catch (err) {
|
|
697
|
+
this.emit('output', { issueId: issue.id, text: `Warning: failed to append cancellation note: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private async updateIssueFrontMatter(issuePath: string, newStatus: string): Promise<void> {
|
|
619
702
|
const pmDir = this.pmDir;
|
|
620
703
|
if (!pmDir) return;
|
|
621
704
|
try {
|
|
622
|
-
const fullPath =
|
|
623
|
-
|
|
705
|
+
const fullPath = this.validateIssuePath(issuePath, pmDir);
|
|
706
|
+
await setFrontMatterFieldAsync(fullPath, 'status', newStatus);
|
|
624
707
|
|
|
625
|
-
// Check off all acceptance criteria when marking done
|
|
626
708
|
if (newStatus === 'done') {
|
|
627
|
-
const content =
|
|
709
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
628
710
|
const updated = checkAllAcceptanceCriteria(content);
|
|
629
|
-
if (updated !== content)
|
|
711
|
+
if (updated !== content) await writeFile(fullPath, updated, 'utf-8');
|
|
630
712
|
}
|
|
631
|
-
} catch {
|
|
713
|
+
} catch (err) {
|
|
714
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to update issue front matter for ${issuePath}: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
715
|
+
}
|
|
632
716
|
}
|
|
633
717
|
|
|
634
|
-
private ensureOutputDirs(): void {
|
|
718
|
+
private async ensureOutputDirs(): Promise<void> {
|
|
635
719
|
if (this.boardDir) {
|
|
636
720
|
const boardOutDir = join(this.boardDir, 'out');
|
|
637
|
-
if (!existsSync(boardOutDir))
|
|
721
|
+
if (!existsSync(boardOutDir)) await mkdir(boardOutDir, { recursive: true });
|
|
638
722
|
} else {
|
|
639
723
|
const pmDir = this.pmDir;
|
|
640
724
|
if (pmDir) {
|
|
641
725
|
const outDir = join(pmDir, 'out');
|
|
642
|
-
if (!existsSync(outDir))
|
|
726
|
+
if (!existsSync(outDir)) await mkdir(outDir, { recursive: true });
|
|
643
727
|
}
|
|
644
728
|
}
|
|
645
729
|
}
|
|
646
730
|
|
|
647
|
-
private appendProgressEntry(issues: Issue[], waveStart: number): void {
|
|
731
|
+
private async appendProgressEntry(issues: Issue[], waveStart: number): Promise<void> {
|
|
648
732
|
const pmDir = this.pmDir;
|
|
649
733
|
if (!pmDir) return;
|
|
650
734
|
|
|
651
|
-
// Board-scoped progress log
|
|
652
735
|
const progressPath = this.boardDir
|
|
653
736
|
? join(this.boardDir, 'progress.md')
|
|
654
737
|
: join(pmDir, 'progress.md');
|
|
@@ -660,7 +743,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
660
743
|
const failed: string[] = [];
|
|
661
744
|
for (const issue of issues) {
|
|
662
745
|
try {
|
|
663
|
-
const content =
|
|
746
|
+
const content = await readFile(this.validateIssuePath(issue.path, pmDir), 'utf-8');
|
|
664
747
|
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
665
748
|
if (statusMatch?.[1] === 'done') {
|
|
666
749
|
completed.push(issue.id);
|
|
@@ -684,18 +767,19 @@ export class PlanExecutor extends EventEmitter {
|
|
|
684
767
|
}
|
|
685
768
|
lines.push('');
|
|
686
769
|
|
|
687
|
-
this.writeProgressLines(progressPath, lines);
|
|
770
|
+
await this.writeProgressLines(progressPath, lines);
|
|
688
771
|
}
|
|
689
772
|
|
|
690
|
-
private writeProgressLines(filePath: string, lines: string[]): void {
|
|
773
|
+
private async writeProgressLines(filePath: string, lines: string[]): Promise<void> {
|
|
691
774
|
try {
|
|
692
775
|
if (existsSync(filePath)) {
|
|
693
|
-
|
|
694
|
-
writeFileSync(filePath, `${existing.trimEnd()}\n${lines.join('\n')}`, 'utf-8');
|
|
776
|
+
await appendFile(filePath, `\n${lines.join('\n')}`, 'utf-8');
|
|
695
777
|
} else {
|
|
696
|
-
|
|
778
|
+
await writeFile(filePath, `# Board Progress\n${lines.join('\n')}`, 'utf-8');
|
|
697
779
|
}
|
|
698
|
-
} catch {
|
|
780
|
+
} catch (err) {
|
|
781
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to write progress log: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
782
|
+
}
|
|
699
783
|
}
|
|
700
784
|
|
|
701
785
|
/** Resolve the active board's directory path for outputs, reviews, and progress. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Replace a field value in a raw YAML string (no --- delimiters).
|
|
@@ -47,6 +48,12 @@ export function setFrontMatterField(filePath: string, field: string, value: stri
|
|
|
47
48
|
writeFileSync(filePath, updated, 'utf-8');
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export async function setFrontMatterFieldAsync(filePath: string, field: string, value: string): Promise<void> {
|
|
52
|
+
const content = await readFile(filePath, 'utf-8');
|
|
53
|
+
const updated = replaceFrontMatterField(content, field, value);
|
|
54
|
+
await writeFile(filePath, updated, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* Check off all unchecked acceptance criteria checkboxes in a markdown string.
|
|
52
59
|
* Only modifies checkboxes within the "## Acceptance Criteria" section.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import type { Issue } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether an issue is a code task (modifies source files) or a
|
|
8
|
+
* document task (produces written output like research, design, plans).
|
|
9
|
+
*
|
|
10
|
+
* Uses the issue's `outputType` field when explicitly set. Falls back to
|
|
11
|
+
* inferring from `filesToModify` — entries prefixed with "Output:" are
|
|
12
|
+
* output path hints, not source files to edit.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveIsCodeTask(issue: Issue): boolean {
|
|
15
|
+
if (issue.outputType === 'code') return true;
|
|
16
|
+
if (issue.outputType === 'document') return false;
|
|
17
|
+
|
|
18
|
+
// auto: infer from filesToModify, filtering out Output:-prefixed entries
|
|
19
|
+
const codeFiles = issue.filesToModify.filter(f => !f.match(/^Output:/i));
|
|
20
|
+
return codeFiles.length > 0;
|
|
21
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
+
import { resolveIsCodeTask } from './issue-classification.js';
|
|
12
13
|
import type { Issue } from './types.js';
|
|
13
14
|
|
|
14
15
|
export interface IssuePromptOptions {
|
|
@@ -33,8 +34,11 @@ export function buildIssuePrompt(options: IssuePromptOptions): string {
|
|
|
33
34
|
.map(c => `- [${c.checked ? 'x' : ' '}] ${c.text}`)
|
|
34
35
|
.join('\n');
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
const isCode = resolveIsCodeTask(issue);
|
|
38
|
+
const codeFiles = isCode ? issue.filesToModify.filter(f => !f.match(/^Output:/i)) : [];
|
|
39
|
+
|
|
40
|
+
const files = codeFiles.length > 0
|
|
41
|
+
? `\n## Files to Modify\n${codeFiles.map(f => `- ${f}`).join('\n')}`
|
|
38
42
|
: '';
|
|
39
43
|
|
|
40
44
|
const predecessorDocs = resolvePredecessorDocs(issue, existingDocs);
|
|
@@ -69,7 +73,7 @@ ${files}${predecessorSection}
|
|
|
69
73
|
## Your Task
|
|
70
74
|
|
|
71
75
|
1. Read the full issue spec at ${pmDir ? join(pmDir, issue.path) : issue.path}
|
|
72
|
-
${
|
|
76
|
+
${isCode ? `2. **Implement the code changes** in the source files listed under "Files to Modify". You MUST edit or create the actual source code files — the acceptance criteria describe what the code must do, not what to document. Read each target file first, then make the changes using Edit or Write.
|
|
73
77
|
3. After implementation, write a brief summary of what you changed to **${outputPath}**${predecessorDocs.length > 0 ? ' — this is the handoff artifact for downstream issues' : ''}
|
|
74
78
|
4. After writing output, update the issue front matter: change \`status: in_progress\` to \`status: in_review\`` : `2. Execute all acceptance criteria listed above
|
|
75
79
|
3. Write your output and results to **${outputPath}** — this is the handoff artifact for downstream issues
|
|
@@ -79,7 +83,7 @@ ${issue.filesToModify.length > 0 ? `2. **Implement the code changes** in the sou
|
|
|
79
83
|
|
|
80
84
|
- Stay within this issue's scope. Do not modify files outside your assigned scope.
|
|
81
85
|
- The orchestrator manages STATE.md separately — do not edit STATE.md.
|
|
82
|
-
${
|
|
86
|
+
${isCode ? `- The output file is a summary of work done, NOT a substitute for implementation. You must modify the actual source code files listed in "Files to Modify". A review gate will verify the source files were changed.` : `- Write all significant output to ${outDir}/ so downstream issues can reference it.`}
|
|
83
87
|
- If you cannot complete the issue, leave status as \`in_progress\` and document what blocked you in the output file.`;
|
|
84
88
|
}
|
|
85
89
|
|