mstro-app 0.4.39 → 0.4.44
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/PRIVACY.md +1 -3
- 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 +163 -77
- 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/file-search-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.js +4 -0
- package/dist/server/services/websocket/file-search-handlers.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 +165 -77
- 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/file-search-handlers.ts +2 -0
- 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 { isAbsolute, join, relative, 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,16 @@ 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 resolvedBase = resolve(baseDir);
|
|
96
|
+
const resolvedFull = resolve(resolvedBase, issuePath);
|
|
97
|
+
const rel = relative(resolvedBase, resolvedFull);
|
|
98
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
99
|
+
throw new Error(`Invalid issue path: path traversal detected in "${issuePath}"`);
|
|
100
|
+
}
|
|
101
|
+
return resolvedFull;
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
getStatus(): ExecutionStatus { return this.status; }
|
|
94
105
|
getMetrics(): ExecutionMetrics { return { ...this.metrics }; }
|
|
95
106
|
|
|
@@ -129,33 +140,40 @@ export class PlanExecutor extends EventEmitter {
|
|
|
129
140
|
this.pmDir = resolvePmDir(this.workingDir);
|
|
130
141
|
this.boardDir = this.resolveBoardDir();
|
|
131
142
|
|
|
132
|
-
this.recoverStaleIssues();
|
|
143
|
+
await this.recoverStaleIssues();
|
|
133
144
|
|
|
134
145
|
const stallResult = await this.runWaveLoop();
|
|
135
146
|
|
|
136
147
|
this.metrics.totalDuration = Date.now() - startTime;
|
|
137
148
|
|
|
138
|
-
if (stallResult === 'stalled') {
|
|
149
|
+
if (stallResult === 'stalled' || stallResult === 'dead') {
|
|
139
150
|
this.status = 'error';
|
|
140
|
-
|
|
151
|
+
if (stallResult === 'stalled') {
|
|
152
|
+
this.emit('error', `Execution stalled: ${MAX_CONSECUTIVE_EMPTY_WAVES} consecutive waves completed zero issues. Issues may be stuck in review or failing repeatedly.`);
|
|
153
|
+
}
|
|
141
154
|
} else if (this.shouldPause) {
|
|
142
155
|
this.status = 'paused';
|
|
143
156
|
} else if (this.shouldStop) {
|
|
144
157
|
this.status = 'idle';
|
|
158
|
+
// Emit complete so clients can transition out of 'stopping' — metrics are broadcast by the handler.
|
|
159
|
+
this.emit('complete', 'Stopped by user');
|
|
145
160
|
} else {
|
|
146
161
|
this.status = 'complete';
|
|
147
162
|
}
|
|
148
163
|
this.emit('statusChanged', this.status);
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
/** Run waves until done, paused, stopped, or stalled.
|
|
152
|
-
private async runWaveLoop(): Promise<'done' | 'stalled'> {
|
|
166
|
+
/** Run waves until done, paused, stopped, or stalled. */
|
|
167
|
+
private async runWaveLoop(): Promise<'done' | 'stalled' | 'dead'> {
|
|
153
168
|
let consecutiveZeroCompletions = 0;
|
|
154
|
-
const maxParallel = this.getBoardMaxParallelAgents();
|
|
169
|
+
const maxParallel = await this.getBoardMaxParallelAgents();
|
|
155
170
|
|
|
156
171
|
while (!this.shouldStop && !this.shouldPause) {
|
|
157
|
-
const readyIssues = this.pickReadyIssues();
|
|
158
|
-
if (readyIssues.length === 0)
|
|
172
|
+
const readyIssues = await this.pickReadyIssues();
|
|
173
|
+
if (readyIssues.length === 0) {
|
|
174
|
+
// pickReadyIssues emits 'error' for dead state, 'complete' otherwise — check if dead
|
|
175
|
+
return await this.hasDeadIssues() ? 'dead' : 'done';
|
|
176
|
+
}
|
|
159
177
|
|
|
160
178
|
const completedCount = await this.executeWave(readyIssues.slice(0, maxParallel));
|
|
161
179
|
|
|
@@ -169,6 +187,18 @@ export class PlanExecutor extends EventEmitter {
|
|
|
169
187
|
return 'done';
|
|
170
188
|
}
|
|
171
189
|
|
|
190
|
+
private async hasDeadIssues(): Promise<boolean> {
|
|
191
|
+
const pmDir = this.pmDir;
|
|
192
|
+
if (!pmDir) return false;
|
|
193
|
+
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
194
|
+
const issues = effectiveBoardId
|
|
195
|
+
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
196
|
+
: this.loadProjectIssues();
|
|
197
|
+
if (!issues) return false;
|
|
198
|
+
const terminalStatuses = new Set(['done', 'cancelled']);
|
|
199
|
+
return issues.some(i => i.type !== 'epic' && !terminalStatuses.has(i.status) && i.status !== 'todo');
|
|
200
|
+
}
|
|
201
|
+
|
|
172
202
|
pause(): void { this.shouldPause = true; }
|
|
173
203
|
stop(): void {
|
|
174
204
|
this.shouldStop = true;
|
|
@@ -199,11 +229,11 @@ export class PlanExecutor extends EventEmitter {
|
|
|
199
229
|
// Create abort controller for this wave — stop() will abort it
|
|
200
230
|
this.waveAbortController = new AbortController();
|
|
201
231
|
|
|
202
|
-
this.ensureOutputDirs();
|
|
232
|
+
await this.ensureOutputDirs();
|
|
203
233
|
this.configInstaller.installPermissions();
|
|
204
234
|
|
|
205
235
|
for (const issue of issues) {
|
|
206
|
-
this.updateIssueFrontMatter(issue.path, 'in_progress');
|
|
236
|
+
await this.updateIssueFrontMatter(issue.path, 'in_progress');
|
|
207
237
|
}
|
|
208
238
|
|
|
209
239
|
const existingDocs = listExistingDocs(this.workingDir, this.boardDir);
|
|
@@ -234,13 +264,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
234
264
|
issueIds: waveIds,
|
|
235
265
|
error: error instanceof Error ? error.message : String(error),
|
|
236
266
|
});
|
|
237
|
-
this.revertIncompleteIssues(issues);
|
|
267
|
+
await this.revertIncompleteIssues(issues);
|
|
238
268
|
} finally {
|
|
239
269
|
this.configInstaller.uninstallPermissions();
|
|
240
270
|
}
|
|
241
271
|
|
|
242
272
|
this.waveAbortController = null;
|
|
243
|
-
this.finalizeWave(issues, waveStart, waveLabel);
|
|
273
|
+
await this.finalizeWave(issues, waveStart, waveLabel);
|
|
244
274
|
this.metrics.currentWaveIds = [];
|
|
245
275
|
return completedCount;
|
|
246
276
|
}
|
|
@@ -288,7 +318,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
288
318
|
* Post-wave operations wrapped individually so a failure in one
|
|
289
319
|
* doesn't prevent the others or kill the while loop in start().
|
|
290
320
|
*/
|
|
291
|
-
private finalizeWave(issues: Issue[], waveStart: number, waveLabel: string): void {
|
|
321
|
+
private async finalizeWave(issues: Issue[], waveStart: number, waveLabel: string): Promise<void> {
|
|
292
322
|
try {
|
|
293
323
|
reconcileState(this.workingDir, this.boardId ?? undefined);
|
|
294
324
|
this.emit('stateUpdated');
|
|
@@ -311,7 +341,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
try {
|
|
314
|
-
this.appendProgressEntry(issues, waveStart);
|
|
344
|
+
await this.appendProgressEntry(issues, waveStart);
|
|
315
345
|
} catch (err) {
|
|
316
346
|
this.emit('output', {
|
|
317
347
|
issueId: waveLabel,
|
|
@@ -334,16 +364,16 @@ export class PlanExecutor extends EventEmitter {
|
|
|
334
364
|
let completed = 0;
|
|
335
365
|
|
|
336
366
|
for (const issue of issues) {
|
|
337
|
-
const fullPath =
|
|
367
|
+
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
338
368
|
try {
|
|
339
|
-
const content =
|
|
369
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
340
370
|
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
341
371
|
const currentStatus = statusMatch?.[1] ?? 'unknown';
|
|
342
372
|
|
|
343
373
|
if (currentStatus === 'in_review' || currentStatus === 'done') {
|
|
344
374
|
if (issue.reviewGate === 'none') {
|
|
345
375
|
// Skip review gate — mark done directly
|
|
346
|
-
this.updateIssueFrontMatter(issue.path, 'done');
|
|
376
|
+
await this.updateIssueFrontMatter(issue.path, 'done');
|
|
347
377
|
this.metrics.issuesCompleted++;
|
|
348
378
|
this.emit('issueCompleted', issue);
|
|
349
379
|
completed++;
|
|
@@ -351,7 +381,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
351
381
|
completed += await this.runReviewGate(issue, pmDir);
|
|
352
382
|
}
|
|
353
383
|
} else {
|
|
354
|
-
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
384
|
+
await this.updateIssueFrontMatter(issue.path, issue.status);
|
|
355
385
|
this.emit('issueError', {
|
|
356
386
|
issueId: issue.id,
|
|
357
387
|
error: 'Issue did not complete during wave execution',
|
|
@@ -370,38 +400,44 @@ export class PlanExecutor extends EventEmitter {
|
|
|
370
400
|
const reviewDir = this.boardDir ?? pmDir;
|
|
371
401
|
const attempts = getReviewAttemptCount(reviewDir, issue);
|
|
372
402
|
if (attempts >= MAX_REVIEW_ATTEMPTS) {
|
|
373
|
-
this.updateIssueFrontMatter(issue.path, '
|
|
403
|
+
await this.updateIssueFrontMatter(issue.path, 'cancelled');
|
|
404
|
+
await this.appendCancellationNote(issue, pmDir, `Cancelled after ${MAX_REVIEW_ATTEMPTS} failed reviews — issue may need restructuring`);
|
|
374
405
|
this.emit('reviewProgress', { issueId: issue.id, status: 'max_attempts' });
|
|
375
|
-
this.emit('
|
|
406
|
+
this.emit('issueAbandoned', {
|
|
407
|
+
issueId: issue.id,
|
|
408
|
+
reason: `Review failed ${MAX_REVIEW_ATTEMPTS} times — cancelled to unblock dependents`,
|
|
409
|
+
attempts,
|
|
410
|
+
});
|
|
411
|
+
this.emit('output', { issueId: issue.id, text: `Review: max attempts reached, cancelling issue to unblock dependents` });
|
|
376
412
|
return 0;
|
|
377
413
|
}
|
|
378
414
|
|
|
379
|
-
this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
415
|
+
await this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
380
416
|
this.emit('reviewProgress', { issueId: issue.id, status: 'reviewing' });
|
|
381
417
|
|
|
382
418
|
const outputPath = resolveOutputPath(issue, this.workingDir, this.boardDir);
|
|
383
419
|
const result = await reviewIssue({
|
|
384
|
-
workingDir: this.workingDir,
|
|
420
|
+
workingDir: this.executionDir || this.workingDir,
|
|
385
421
|
issue,
|
|
386
422
|
pmDir,
|
|
387
423
|
outputPath,
|
|
388
424
|
onOutput: (text) => this.emit('output', { issueId: issue.id, text }),
|
|
389
425
|
logDir: this.boardDir ? join(this.boardDir, 'logs') : undefined,
|
|
390
|
-
reviewCriteria: this.getBoardReviewCriteria(),
|
|
426
|
+
reviewCriteria: await this.getBoardReviewCriteria(),
|
|
391
427
|
boardDir: this.boardDir,
|
|
392
428
|
extraEnv: this.extraEnv,
|
|
393
429
|
});
|
|
394
430
|
persistReviewResult(reviewDir, issue, result);
|
|
395
431
|
|
|
396
432
|
if (result.passed) {
|
|
397
|
-
this.updateIssueFrontMatter(issue.path, 'done');
|
|
433
|
+
await this.updateIssueFrontMatter(issue.path, 'done');
|
|
398
434
|
this.metrics.issuesCompleted++;
|
|
399
435
|
this.emit('reviewProgress', { issueId: issue.id, status: 'passed' });
|
|
400
436
|
this.emit('issueCompleted', issue);
|
|
401
437
|
return 1;
|
|
402
438
|
}
|
|
403
439
|
|
|
404
|
-
this.updateIssueFrontMatter(issue.path, 'todo');
|
|
440
|
+
await this.updateIssueFrontMatter(issue.path, 'todo');
|
|
405
441
|
appendReviewFeedback(pmDir, issue, result);
|
|
406
442
|
this.emit('reviewProgress', { issueId: issue.id, status: 'failed' });
|
|
407
443
|
this.emit('issueError', {
|
|
@@ -419,13 +455,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
419
455
|
* these issues block the dependency graph and cause the executor to
|
|
420
456
|
* find zero ready issues, making "Implement" appear to do nothing.
|
|
421
457
|
*/
|
|
422
|
-
private recoverStaleIssues(): void {
|
|
458
|
+
private async recoverStaleIssues(): Promise<void> {
|
|
423
459
|
const pmDir = this.pmDir;
|
|
424
460
|
if (!pmDir) return;
|
|
425
461
|
|
|
426
462
|
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
427
463
|
const issues = effectiveBoardId
|
|
428
|
-
? this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
464
|
+
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
429
465
|
: this.loadProjectIssues();
|
|
430
466
|
|
|
431
467
|
if (!issues) return;
|
|
@@ -436,7 +472,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
436
472
|
for (const issue of issues) {
|
|
437
473
|
if (issue.type === 'epic') continue;
|
|
438
474
|
if (staleStatuses.has(issue.status)) {
|
|
439
|
-
this.updateIssueFrontMatter(issue.path, 'todo');
|
|
475
|
+
await this.updateIssueFrontMatter(issue.path, 'todo');
|
|
440
476
|
recovered.push(`${issue.id} (${issue.status} → todo)`);
|
|
441
477
|
}
|
|
442
478
|
}
|
|
@@ -453,28 +489,28 @@ export class PlanExecutor extends EventEmitter {
|
|
|
453
489
|
// ── Helpers ──────────────────────────────────────────────────
|
|
454
490
|
|
|
455
491
|
/** Read the board's maxParallelAgents setting, falling back to default. */
|
|
456
|
-
private getBoardMaxParallelAgents(): number {
|
|
492
|
+
private async getBoardMaxParallelAgents(): Promise<number> {
|
|
457
493
|
const pmDir = this.pmDir;
|
|
458
494
|
if (!pmDir) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
459
495
|
|
|
460
496
|
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
461
497
|
if (!effectiveBoardId) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
462
498
|
|
|
463
|
-
// Read only board.md — avoids parsing STATE.md and all backlog issues just for one setting
|
|
464
499
|
const boardMdPath = join(pmDir, 'boards', effectiveBoardId, 'board.md');
|
|
465
500
|
if (!existsSync(boardMdPath)) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
466
501
|
|
|
467
502
|
try {
|
|
468
|
-
const content =
|
|
503
|
+
const content = await readFile(boardMdPath, 'utf-8');
|
|
469
504
|
const match = content.match(/^max_parallel_agents:\s*(\d+)/m);
|
|
470
505
|
return match ? Math.max(1, Math.min(Number(match[1]), 10)) : DEFAULT_MAX_PARALLEL_AGENTS;
|
|
471
|
-
} catch {
|
|
506
|
+
} catch (err) {
|
|
507
|
+
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
508
|
return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
473
509
|
}
|
|
474
510
|
}
|
|
475
511
|
|
|
476
512
|
/** Read the board's custom review criteria, if set. */
|
|
477
|
-
private getBoardReviewCriteria(): string | undefined {
|
|
513
|
+
private async getBoardReviewCriteria(): Promise<string | undefined> {
|
|
478
514
|
const pmDir = this.pmDir;
|
|
479
515
|
if (!pmDir) return undefined;
|
|
480
516
|
|
|
@@ -485,17 +521,18 @@ export class PlanExecutor extends EventEmitter {
|
|
|
485
521
|
if (!existsSync(boardMdPath)) return undefined;
|
|
486
522
|
|
|
487
523
|
try {
|
|
488
|
-
const content =
|
|
524
|
+
const content = await readFile(boardMdPath, 'utf-8');
|
|
489
525
|
const match = content.match(/^review_criteria:\s*"(.+)"/m);
|
|
490
526
|
if (!match) return undefined;
|
|
491
527
|
const raw = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
|
|
492
528
|
return raw || undefined;
|
|
493
|
-
} catch {
|
|
529
|
+
} catch (err) {
|
|
530
|
+
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
531
|
return undefined;
|
|
495
532
|
}
|
|
496
533
|
}
|
|
497
534
|
|
|
498
|
-
private pickReadyIssues(): Issue[] {
|
|
535
|
+
private async pickReadyIssues(): Promise<Issue[]> {
|
|
499
536
|
const pmDir = this.pmDir;
|
|
500
537
|
if (!pmDir) {
|
|
501
538
|
this.emit('error', 'No PM directory found');
|
|
@@ -504,23 +541,28 @@ export class PlanExecutor extends EventEmitter {
|
|
|
504
541
|
|
|
505
542
|
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
506
543
|
const issues = effectiveBoardId
|
|
507
|
-
? this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
544
|
+
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
508
545
|
: this.loadProjectIssues();
|
|
509
546
|
|
|
510
547
|
if (!issues) return [];
|
|
511
548
|
|
|
512
549
|
const readyIssues = resolveReadyToWork(issues, this.epicScope ?? undefined);
|
|
513
550
|
if (readyIssues.length === 0) {
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
this.
|
|
551
|
+
const deadState = this.detectDeadState(issues);
|
|
552
|
+
if (deadState) {
|
|
553
|
+
this.emit('error', deadState);
|
|
554
|
+
} else {
|
|
555
|
+
this.emit('complete', this.buildCompletionReason(issues));
|
|
556
|
+
if (effectiveBoardId) {
|
|
557
|
+
await this.tryCompleteBoardIfDone(pmDir, effectiveBoardId, issues);
|
|
558
|
+
}
|
|
517
559
|
}
|
|
518
560
|
}
|
|
519
561
|
return readyIssues;
|
|
520
562
|
}
|
|
521
563
|
|
|
522
564
|
/** Load issues from a specific board, auto-activating draft boards. Returns null on error. */
|
|
523
|
-
private loadBoardIssues(pmDir: string, boardId: string): Issue[] | null {
|
|
565
|
+
private async loadBoardIssues(pmDir: string, boardId: string): Promise<Issue[] | null> {
|
|
524
566
|
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
525
567
|
if (!boardState) {
|
|
526
568
|
this.emit('error', `Board not found: ${boardId}`);
|
|
@@ -531,7 +573,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
531
573
|
return null;
|
|
532
574
|
}
|
|
533
575
|
if (boardState.board.status === 'draft') {
|
|
534
|
-
this.activateBoard(pmDir, boardId);
|
|
576
|
+
await this.activateBoard(pmDir, boardId);
|
|
535
577
|
} else if (boardState.board.status !== 'active') {
|
|
536
578
|
this.emit('error', `Board ${boardId} is not active (status: ${boardState.board.status})`);
|
|
537
579
|
return null;
|
|
@@ -554,17 +596,19 @@ export class PlanExecutor extends EventEmitter {
|
|
|
554
596
|
}
|
|
555
597
|
|
|
556
598
|
/** Activate a draft board by updating its status in board.md. */
|
|
557
|
-
private activateBoard(pmDir: string, boardId: string): void {
|
|
599
|
+
private async activateBoard(pmDir: string, boardId: string): Promise<void> {
|
|
558
600
|
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
559
601
|
if (!existsSync(boardMdPath)) return;
|
|
560
602
|
try {
|
|
561
|
-
const content =
|
|
562
|
-
|
|
563
|
-
} catch {
|
|
603
|
+
const content = await readFile(boardMdPath, 'utf-8');
|
|
604
|
+
await writeFile(boardMdPath, replaceFrontMatterField(content, 'status', 'active'), 'utf-8');
|
|
605
|
+
} catch (err) {
|
|
606
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to activate board ${boardId}: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
607
|
+
}
|
|
564
608
|
}
|
|
565
609
|
|
|
566
610
|
/** Check if all issues in a board are done and mark board as completed. */
|
|
567
|
-
private tryCompleteBoardIfDone(pmDir: string, boardId: string, issues: Issue[]): void {
|
|
611
|
+
private async tryCompleteBoardIfDone(pmDir: string, boardId: string, issues: Issue[]): Promise<void> {
|
|
568
612
|
const allDone = issues.length > 0 && issues.every(i => i.status === 'done' || i.status === 'cancelled');
|
|
569
613
|
if (!allDone) return;
|
|
570
614
|
|
|
@@ -572,11 +616,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
572
616
|
if (!existsSync(boardMdPath)) return;
|
|
573
617
|
|
|
574
618
|
try {
|
|
575
|
-
let content =
|
|
619
|
+
let content = await readFile(boardMdPath, 'utf-8');
|
|
576
620
|
content = replaceFrontMatterField(content, 'status', 'completed');
|
|
577
621
|
content = replaceFrontMatterField(content, 'completed_at', `"${new Date().toISOString()}"`);
|
|
578
|
-
|
|
579
|
-
} catch {
|
|
622
|
+
await writeFile(boardMdPath, content, 'utf-8');
|
|
623
|
+
} catch (err) {
|
|
624
|
+
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 });
|
|
625
|
+
}
|
|
580
626
|
}
|
|
581
627
|
|
|
582
628
|
private resolveActiveBoardId(): string | null {
|
|
@@ -601,54 +647,95 @@ export class PlanExecutor extends EventEmitter {
|
|
|
601
647
|
return this.epicScope ? 'All epic issues are done or blocked' : 'All work is done or blocked';
|
|
602
648
|
}
|
|
603
649
|
|
|
604
|
-
|
|
650
|
+
/** Detect issues stuck in non-terminal states with no path to completion. */
|
|
651
|
+
private detectDeadState(issues: Issue[]): string | null {
|
|
652
|
+
const nonEpic = issues.filter(i => i.type !== 'epic');
|
|
653
|
+
const terminalStatuses = new Set(['done', 'cancelled']);
|
|
654
|
+
const stuck = nonEpic.filter(i => !terminalStatuses.has(i.status) && i.status !== 'todo');
|
|
655
|
+
|
|
656
|
+
if (stuck.length === 0) return null;
|
|
657
|
+
|
|
658
|
+
const stuckIds = stuck.map(i => `${i.id} (${i.status})`).join(', ');
|
|
659
|
+
|
|
660
|
+
const issueByPath = new Map(issues.map(i => [i.path, i]));
|
|
661
|
+
const blockedByStuck = nonEpic.filter(i => {
|
|
662
|
+
if (i.status !== 'todo') return false;
|
|
663
|
+
return i.blockedBy.some(bp => {
|
|
664
|
+
const blocker = issueByPath.get(bp);
|
|
665
|
+
return blocker && !terminalStatuses.has(blocker.status);
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
const blockedIds = blockedByStuck.map(i => i.id).join(', ');
|
|
669
|
+
|
|
670
|
+
return `Board stuck: ${stuckIds} cannot progress${blockedIds ? `. Blocking: ${blockedIds}` : ''}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private async revertIncompleteIssues(issues: Issue[]): Promise<void> {
|
|
605
674
|
const pmDir = this.pmDir;
|
|
606
675
|
if (!pmDir) return;
|
|
607
676
|
for (const issue of issues) {
|
|
608
|
-
const fullPath =
|
|
677
|
+
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
609
678
|
try {
|
|
610
|
-
const content =
|
|
679
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
611
680
|
if (content.match(/^status:\s*in_progress$/m)) {
|
|
612
|
-
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
681
|
+
await this.updateIssueFrontMatter(issue.path, issue.status);
|
|
613
682
|
}
|
|
614
|
-
} catch {
|
|
683
|
+
} catch (err) {
|
|
684
|
+
this.emit('output', { issueId: issue.id, text: `Warning: failed to revert issue status: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
685
|
+
}
|
|
615
686
|
}
|
|
616
687
|
}
|
|
617
688
|
|
|
618
|
-
private
|
|
689
|
+
private async appendCancellationNote(issue: Issue, pmDir: string, reason: string): Promise<void> {
|
|
690
|
+
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
691
|
+
try {
|
|
692
|
+
let content = await readFile(fullPath, 'utf-8');
|
|
693
|
+
const entry = `- Cancelled (${new Date().toISOString().split('T')[0]}): ${reason}`;
|
|
694
|
+
if (content.includes('## Activity')) {
|
|
695
|
+
content = content.replace(/## Activity/, `## Activity\n${entry}`);
|
|
696
|
+
} else {
|
|
697
|
+
content += `\n\n## Activity\n${entry}`;
|
|
698
|
+
}
|
|
699
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
700
|
+
} catch (err) {
|
|
701
|
+
this.emit('output', { issueId: issue.id, text: `Warning: failed to append cancellation note: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private async updateIssueFrontMatter(issuePath: string, newStatus: string): Promise<void> {
|
|
619
706
|
const pmDir = this.pmDir;
|
|
620
707
|
if (!pmDir) return;
|
|
621
708
|
try {
|
|
622
|
-
const fullPath =
|
|
623
|
-
|
|
709
|
+
const fullPath = this.validateIssuePath(issuePath, pmDir);
|
|
710
|
+
await setFrontMatterFieldAsync(fullPath, 'status', newStatus);
|
|
624
711
|
|
|
625
|
-
// Check off all acceptance criteria when marking done
|
|
626
712
|
if (newStatus === 'done') {
|
|
627
|
-
const content =
|
|
713
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
628
714
|
const updated = checkAllAcceptanceCriteria(content);
|
|
629
|
-
if (updated !== content)
|
|
715
|
+
if (updated !== content) await writeFile(fullPath, updated, 'utf-8');
|
|
630
716
|
}
|
|
631
|
-
} catch {
|
|
717
|
+
} catch (err) {
|
|
718
|
+
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 });
|
|
719
|
+
}
|
|
632
720
|
}
|
|
633
721
|
|
|
634
|
-
private ensureOutputDirs(): void {
|
|
722
|
+
private async ensureOutputDirs(): Promise<void> {
|
|
635
723
|
if (this.boardDir) {
|
|
636
724
|
const boardOutDir = join(this.boardDir, 'out');
|
|
637
|
-
if (!existsSync(boardOutDir))
|
|
725
|
+
if (!existsSync(boardOutDir)) await mkdir(boardOutDir, { recursive: true });
|
|
638
726
|
} else {
|
|
639
727
|
const pmDir = this.pmDir;
|
|
640
728
|
if (pmDir) {
|
|
641
729
|
const outDir = join(pmDir, 'out');
|
|
642
|
-
if (!existsSync(outDir))
|
|
730
|
+
if (!existsSync(outDir)) await mkdir(outDir, { recursive: true });
|
|
643
731
|
}
|
|
644
732
|
}
|
|
645
733
|
}
|
|
646
734
|
|
|
647
|
-
private appendProgressEntry(issues: Issue[], waveStart: number): void {
|
|
735
|
+
private async appendProgressEntry(issues: Issue[], waveStart: number): Promise<void> {
|
|
648
736
|
const pmDir = this.pmDir;
|
|
649
737
|
if (!pmDir) return;
|
|
650
738
|
|
|
651
|
-
// Board-scoped progress log
|
|
652
739
|
const progressPath = this.boardDir
|
|
653
740
|
? join(this.boardDir, 'progress.md')
|
|
654
741
|
: join(pmDir, 'progress.md');
|
|
@@ -660,7 +747,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
660
747
|
const failed: string[] = [];
|
|
661
748
|
for (const issue of issues) {
|
|
662
749
|
try {
|
|
663
|
-
const content =
|
|
750
|
+
const content = await readFile(this.validateIssuePath(issue.path, pmDir), 'utf-8');
|
|
664
751
|
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
665
752
|
if (statusMatch?.[1] === 'done') {
|
|
666
753
|
completed.push(issue.id);
|
|
@@ -684,18 +771,19 @@ export class PlanExecutor extends EventEmitter {
|
|
|
684
771
|
}
|
|
685
772
|
lines.push('');
|
|
686
773
|
|
|
687
|
-
this.writeProgressLines(progressPath, lines);
|
|
774
|
+
await this.writeProgressLines(progressPath, lines);
|
|
688
775
|
}
|
|
689
776
|
|
|
690
|
-
private writeProgressLines(filePath: string, lines: string[]): void {
|
|
777
|
+
private async writeProgressLines(filePath: string, lines: string[]): Promise<void> {
|
|
691
778
|
try {
|
|
692
779
|
if (existsSync(filePath)) {
|
|
693
|
-
|
|
694
|
-
writeFileSync(filePath, `${existing.trimEnd()}\n${lines.join('\n')}`, 'utf-8');
|
|
780
|
+
await appendFile(filePath, `\n${lines.join('\n')}`, 'utf-8');
|
|
695
781
|
} else {
|
|
696
|
-
|
|
782
|
+
await writeFile(filePath, `# Board Progress\n${lines.join('\n')}`, 'utf-8');
|
|
697
783
|
}
|
|
698
|
-
} catch {
|
|
784
|
+
} catch (err) {
|
|
785
|
+
this.emit('output', { issueId: 'system', text: `Warning: failed to write progress log: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
786
|
+
}
|
|
699
787
|
}
|
|
700
788
|
|
|
701
789
|
/** 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
|
|