mstro-app 0.4.20 → 0.4.22
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/README.md +66 -0
- package/dist/server/cli/headless/claude-invoker-process.js +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +1 -1
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +4 -1
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +1 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +4 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/index.js +9 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +2 -2
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +20 -20
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts +6 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -1
- package/dist/server/mcp/security-analysis.js +16 -1
- package/dist/server/mcp/security-analysis.js.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts +8 -0
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +47 -2
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/deploy/ai-broker.d.ts +63 -0
- package/dist/server/services/deploy/ai-broker.d.ts.map +1 -0
- package/dist/server/services/deploy/ai-broker.js +360 -0
- package/dist/server/services/deploy/ai-broker.js.map +1 -0
- package/dist/server/services/deploy/board-execution-handler.d.ts +114 -0
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +1 -0
- package/dist/server/services/deploy/board-execution-handler.js +621 -0
- package/dist/server/services/deploy/board-execution-handler.js.map +1 -0
- package/dist/server/services/deploy/credentials.d.ts +35 -0
- package/dist/server/services/deploy/credentials.d.ts.map +1 -0
- package/dist/server/services/deploy/credentials.js +177 -0
- package/dist/server/services/deploy/credentials.js.map +1 -0
- package/dist/server/services/deploy/deploy-ai-service.d.ts +107 -0
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +1 -0
- package/dist/server/services/deploy/deploy-ai-service.js +294 -0
- package/dist/server/services/deploy/deploy-ai-service.js.map +1 -0
- package/dist/server/services/deploy/headless-session-handler.d.ts +94 -0
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +1 -0
- package/dist/server/services/deploy/headless-session-handler.js +274 -0
- package/dist/server/services/deploy/headless-session-handler.js.map +1 -0
- package/dist/server/services/pathUtils.d.ts.map +1 -1
- package/dist/server/services/pathUtils.js +33 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/plan/agent-loader.d.ts +10 -0
- package/dist/server/services/plan/agent-loader.d.ts.map +1 -0
- package/dist/server/services/plan/agent-loader.js +65 -0
- package/dist/server/services/plan/agent-loader.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +5 -1
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/dependency-resolver.d.ts +1 -1
- package/dist/server/services/plan/dependency-resolver.js +2 -2
- package/dist/server/services/plan/dependency-resolver.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +7 -3
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +27 -14
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +5 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +19 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +25 -0
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -0
- package/dist/server/services/plan/issue-retry.js +216 -0
- package/dist/server/services/plan/issue-retry.js.map +1 -0
- package/dist/server/services/plan/output-manager.d.ts +2 -2
- package/dist/server/services/plan/output-manager.js +2 -2
- package/dist/server/services/plan/parser-core.d.ts +1 -1
- package/dist/server/services/plan/parser-core.js +1 -1
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/parser-migration.d.ts +2 -2
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
- package/dist/server/services/plan/parser-migration.js +5 -5
- package/dist/server/services/plan/parser-migration.js.map +1 -1
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +4 -7
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/prompt-builder.d.ts +1 -1
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts +4 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +90 -35
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +21 -11
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -2
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/plan/watcher.js +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js +8 -4
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +14 -0
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/deploy-handlers.js +409 -0
- package/dist/server/services/websocket/deploy-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +12 -0
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +11 -0
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/handlers/deploy-handlers.js +180 -0
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +54 -1
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-helpers.js +3 -4
- package/dist/server/services/websocket/plan-helpers.js.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.js +5 -1
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -11
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +17 -21
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +264 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +1 -1
- package/server/cli/headless/headless-logger.ts +1 -1
- package/server/cli/headless/mcp-config.ts +4 -1
- package/server/cli/headless/runner.ts +1 -0
- package/server/cli/headless/types.ts +4 -1
- package/server/index.ts +9 -1
- package/server/mcp/bouncer-integration.ts +19 -17
- package/server/mcp/security-analysis.ts +19 -0
- package/server/mcp/security-patterns.ts +53 -2
- package/server/services/deploy/ai-broker.ts +512 -0
- package/server/services/deploy/board-execution-handler.ts +847 -0
- package/server/services/deploy/credentials.ts +200 -0
- package/server/services/deploy/deploy-ai-service.ts +401 -0
- package/server/services/deploy/headless-session-handler.ts +415 -0
- package/server/services/pathUtils.ts +35 -1
- package/server/services/plan/agent-loader.ts +73 -0
- package/server/services/plan/agents/review-code.md +28 -0
- package/server/services/plan/agents/review-custom.md +27 -0
- package/server/services/plan/agents/review-quality.md +42 -0
- package/server/services/plan/composer.ts +5 -1
- package/server/services/plan/dependency-resolver.ts +2 -2
- package/server/services/plan/executor.ts +27 -15
- package/server/services/plan/front-matter.ts +23 -0
- package/server/services/plan/issue-prompt-builder.ts +2 -2
- package/server/services/plan/issue-retry.ts +297 -0
- package/server/services/plan/output-manager.ts +2 -2
- package/server/services/plan/parser-core.ts +2 -2
- package/server/services/plan/parser-migration.ts +5 -5
- package/server/services/plan/parser.ts +4 -5
- package/server/services/plan/prompt-builder.ts +1 -1
- package/server/services/plan/review-gate.ts +105 -34
- package/server/services/plan/state-reconciler.ts +21 -11
- package/server/services/plan/types.ts +3 -3
- package/server/services/plan/watcher.ts +1 -1
- package/server/services/sentry.ts +8 -4
- package/server/services/websocket/deploy-handlers.ts +544 -0
- package/server/services/websocket/handler.ts +11 -1
- package/server/services/websocket/handlers/deploy-handlers.ts +230 -0
- package/server/services/websocket/plan-board-handlers.ts +53 -1
- package/server/services/websocket/plan-helpers.ts +3 -4
- package/server/services/websocket/plan-issue-handlers.ts +6 -1
- package/server/services/websocket/plan-sprint-handlers.ts +3 -9
- package/server/services/websocket/settings-handlers.ts +18 -22
- package/server/services/websocket/types.ts +333 -2
|
@@ -20,11 +20,11 @@ import { EventEmitter } from 'node:events';
|
|
|
20
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
|
|
23
|
-
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
24
23
|
import { ConfigInstaller } from './config-installer.js';
|
|
25
24
|
import { resolveReadyToWork } from './dependency-resolver.js';
|
|
26
|
-
import { replaceFrontMatterField, setFrontMatterField } from './front-matter.js';
|
|
25
|
+
import { checkAllAcceptanceCriteria, replaceFrontMatterField, setFrontMatterField } from './front-matter.js';
|
|
27
26
|
import { buildIssuePrompt } from './issue-prompt-builder.js';
|
|
27
|
+
import { runIssueWithRetry } from './issue-retry.js';
|
|
28
28
|
import { listExistingDocs, publishOutputs, resolveOutputPath } from './output-manager.js';
|
|
29
29
|
import { parseBoardDirectory, parsePlanDirectory, resolvePmDir } from './parser.js';
|
|
30
30
|
import { appendReviewFeedback, getReviewAttemptCount, MAX_REVIEW_ATTEMPTS, persistReviewResult, reviewIssue } from './review-gate.js';
|
|
@@ -62,13 +62,15 @@ export class PlanExecutor extends EventEmitter {
|
|
|
62
62
|
private epicScope: string | null = null;
|
|
63
63
|
/** Cached PM directory path — resolved once per start(). */
|
|
64
64
|
private pmDir: string | null = null;
|
|
65
|
-
/** Board directory path (e.g. /path/.pm/boards/BOARD-001). Used for outputs, reviews, progress. */
|
|
65
|
+
/** Board directory path (e.g. /path/.mstro/pm/boards/BOARD-001). Used for outputs, reviews, progress. */
|
|
66
66
|
private boardDir: string | null = null;
|
|
67
67
|
/** Board ID being executed (e.g. "BOARD-001") */
|
|
68
68
|
private boardId: string | null = null;
|
|
69
69
|
private configInstaller: ConfigInstaller;
|
|
70
70
|
/** Flag to prevent start() from clearing scope set by startBoard/startEpic */
|
|
71
71
|
private _scopeSetByCall = false;
|
|
72
|
+
/** Extra environment variables forwarded to HeadlessRunner child processes (e.g. API keys) */
|
|
73
|
+
private extraEnv?: Record<string, string>;
|
|
72
74
|
private metrics: ExecutionMetrics = {
|
|
73
75
|
issuesCompleted: 0,
|
|
74
76
|
issuesAttempted: 0,
|
|
@@ -77,9 +79,10 @@ export class PlanExecutor extends EventEmitter {
|
|
|
77
79
|
currentWaveIds: [],
|
|
78
80
|
};
|
|
79
81
|
|
|
80
|
-
constructor(workingDir: string) {
|
|
82
|
+
constructor(workingDir: string, options?: { extraEnv?: Record<string, string> }) {
|
|
81
83
|
super();
|
|
82
84
|
this.workingDir = workingDir;
|
|
85
|
+
this.extraEnv = options?.extraEnv;
|
|
83
86
|
this.configInstaller = new ConfigInstaller(workingDir);
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -223,7 +226,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
223
226
|
return completedCount;
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
/** Run a single issue via its own headless Claude Code instance. */
|
|
229
|
+
/** Run a single issue via its own headless Claude Code instance with retry logic. */
|
|
227
230
|
private async runSingleIssue(
|
|
228
231
|
issue: Issue,
|
|
229
232
|
pmDir: string | null,
|
|
@@ -240,21 +243,19 @@ export class PlanExecutor extends EventEmitter {
|
|
|
240
243
|
outputPath,
|
|
241
244
|
});
|
|
242
245
|
|
|
243
|
-
const
|
|
246
|
+
const boardLogDir = this.boardDir ? join(this.boardDir, 'logs') : undefined;
|
|
247
|
+
const result = await runWithFileLogger(`pm-issue-${issue.id}`, () => runIssueWithRetry({
|
|
244
248
|
workingDir: this.workingDir,
|
|
245
|
-
|
|
249
|
+
prompt,
|
|
246
250
|
stallWarningMs: ISSUE_STALL_WARNING_MS,
|
|
247
251
|
stallKillMs: ISSUE_STALL_KILL_MS,
|
|
248
252
|
stallHardCapMs: ISSUE_STALL_HARD_CAP_MS,
|
|
249
253
|
stallMaxExtensions: ISSUE_STALL_MAX_EXTENSIONS,
|
|
250
|
-
verbose: process.env.MSTRO_VERBOSE === '1',
|
|
251
254
|
outputCallback: (text: string) => {
|
|
252
255
|
this.emit('output', { issueId: issue.id, text });
|
|
253
256
|
},
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const boardLogDir = this.boardDir ? join(this.boardDir, 'logs') : undefined;
|
|
257
|
-
const result = await runWithFileLogger(`pm-issue-${issue.id}`, () => runner.run(), boardLogDir);
|
|
257
|
+
extraEnv: this.extraEnv,
|
|
258
|
+
}), boardLogDir);
|
|
258
259
|
|
|
259
260
|
if (!result.completed || result.error) {
|
|
260
261
|
this.emit('output', { issueId: waveLabel, text: `Issue ${issue.id}: ${result.error || 'did not complete'}` });
|
|
@@ -317,9 +318,10 @@ export class PlanExecutor extends EventEmitter {
|
|
|
317
318
|
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
318
319
|
const currentStatus = statusMatch?.[1] ?? 'unknown';
|
|
319
320
|
|
|
320
|
-
if (currentStatus === 'done') {
|
|
321
|
+
if (currentStatus === 'in_review' || currentStatus === 'done') {
|
|
321
322
|
if (issue.reviewGate === 'none') {
|
|
322
|
-
// Skip review gate —
|
|
323
|
+
// Skip review gate — mark done directly
|
|
324
|
+
this.updateIssueFrontMatter(issue.path, 'done');
|
|
323
325
|
this.metrics.issuesCompleted++;
|
|
324
326
|
this.emit('issueCompleted', issue);
|
|
325
327
|
completed++;
|
|
@@ -364,6 +366,8 @@ export class PlanExecutor extends EventEmitter {
|
|
|
364
366
|
onOutput: (text) => this.emit('output', { issueId: issue.id, text }),
|
|
365
367
|
logDir: this.boardDir ? join(this.boardDir, 'logs') : undefined,
|
|
366
368
|
reviewCriteria: this.getBoardReviewCriteria(),
|
|
369
|
+
boardDir: this.boardDir,
|
|
370
|
+
extraEnv: this.extraEnv,
|
|
367
371
|
});
|
|
368
372
|
persistReviewResult(reviewDir, issue, result);
|
|
369
373
|
|
|
@@ -545,7 +549,15 @@ export class PlanExecutor extends EventEmitter {
|
|
|
545
549
|
const pmDir = this.pmDir;
|
|
546
550
|
if (!pmDir) return;
|
|
547
551
|
try {
|
|
548
|
-
|
|
552
|
+
const fullPath = join(pmDir, issuePath);
|
|
553
|
+
setFrontMatterField(fullPath, 'status', newStatus);
|
|
554
|
+
|
|
555
|
+
// Check off all acceptance criteria when marking done
|
|
556
|
+
if (newStatus === 'done') {
|
|
557
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
558
|
+
const updated = checkAllAcceptanceCriteria(content);
|
|
559
|
+
if (updated !== content) writeFileSync(fullPath, updated, 'utf-8');
|
|
560
|
+
}
|
|
549
561
|
} catch { /* file may have been moved */ }
|
|
550
562
|
}
|
|
551
563
|
|
|
@@ -46,3 +46,26 @@ export function setFrontMatterField(filePath: string, field: string, value: stri
|
|
|
46
46
|
const updated = replaceFrontMatterField(content, field, value);
|
|
47
47
|
writeFileSync(filePath, updated, 'utf-8');
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check off all unchecked acceptance criteria checkboxes in a markdown string.
|
|
52
|
+
* Only modifies checkboxes within the "## Acceptance Criteria" section.
|
|
53
|
+
*/
|
|
54
|
+
export function checkAllAcceptanceCriteria(content: string): string {
|
|
55
|
+
const sectionStart = content.indexOf('## Acceptance Criteria');
|
|
56
|
+
if (sectionStart === -1) return content;
|
|
57
|
+
|
|
58
|
+
const afterHeader = content.indexOf('\n', sectionStart);
|
|
59
|
+
if (afterHeader === -1) return content;
|
|
60
|
+
|
|
61
|
+
const nextSection = content.slice(afterHeader).search(/\n## /);
|
|
62
|
+
const sectionEnd = nextSection !== -1 ? afterHeader + nextSection : content.length;
|
|
63
|
+
|
|
64
|
+
const before = content.slice(0, afterHeader);
|
|
65
|
+
const section = content.slice(afterHeader, sectionEnd);
|
|
66
|
+
const after = content.slice(sectionEnd);
|
|
67
|
+
|
|
68
|
+
const updatedSection = section.replace(/^([-*]\s+)\[ \]/gm, '$1[x]');
|
|
69
|
+
|
|
70
|
+
return before + updatedSection + after;
|
|
71
|
+
}
|
|
@@ -15,7 +15,7 @@ export interface IssuePromptOptions {
|
|
|
15
15
|
issue: Issue;
|
|
16
16
|
workingDir: string;
|
|
17
17
|
pmDir: string | null;
|
|
18
|
-
/** Board directory path (e.g. /path/.pm/boards/BOARD-001). */
|
|
18
|
+
/** Board directory path (e.g. /path/.mstro/pm/boards/BOARD-001). */
|
|
19
19
|
boardDir: string | null;
|
|
20
20
|
existingDocs: string[];
|
|
21
21
|
outputPath: string;
|
|
@@ -71,7 +71,7 @@ ${files}${predecessorSection}
|
|
|
71
71
|
1. Read the full issue spec at ${pmDir ? join(pmDir, issue.path) : issue.path}
|
|
72
72
|
2. Execute all acceptance criteria listed above
|
|
73
73
|
3. Write your output and results to **${outputPath}** — this is the handoff artifact for downstream issues
|
|
74
|
-
4. After writing output, update the issue front matter: change \`status: in_progress\` to \`status:
|
|
74
|
+
4. After writing output, update the issue front matter: change \`status: in_progress\` to \`status: in_review\`
|
|
75
75
|
|
|
76
76
|
## Rules
|
|
77
77
|
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Issue Retry — Retry loop for PM issue execution.
|
|
6
|
+
*
|
|
7
|
+
* Brings the same resilience as Chat view (improvisation-retry.ts) to PM agents:
|
|
8
|
+
* - Tool timeout checkpoint recovery (preserves completed tools, skips hung tool)
|
|
9
|
+
* - Signal crash recovery (preserves accumulated results across retries)
|
|
10
|
+
* - Premature completion handling (max_tokens / end_turn → resume with "continue")
|
|
11
|
+
*
|
|
12
|
+
* Unlike Chat's retry system, PM agents don't maintain session continuity across
|
|
13
|
+
* prompts — each issue is independent — so we skip inter-movement recovery and
|
|
14
|
+
* simplify the resume strategy.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { hlog } from '../../cli/headless/headless-logger.js';
|
|
18
|
+
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
19
|
+
import { assessPrematureCompletion } from '../../cli/headless/stall-assessor.js';
|
|
20
|
+
import type { ExecutionCheckpoint, SessionResult } from '../../cli/headless/types.js';
|
|
21
|
+
import {
|
|
22
|
+
buildResumeRetryPrompt,
|
|
23
|
+
buildRetryPrompt,
|
|
24
|
+
buildSignalCrashRecoveryPrompt,
|
|
25
|
+
} from '../../cli/prompt-builders.js';
|
|
26
|
+
|
|
27
|
+
/** Max retries per issue execution (tool timeout, signal crash, premature completion combined) */
|
|
28
|
+
const MAX_ISSUE_RETRIES = 3;
|
|
29
|
+
|
|
30
|
+
/** Max accumulated tool results to carry across retries */
|
|
31
|
+
const MAX_ACCUMULATED_RESULTS = 50;
|
|
32
|
+
|
|
33
|
+
/** Lightweight tool record for accumulation across retries */
|
|
34
|
+
interface ToolRecord {
|
|
35
|
+
toolName: string;
|
|
36
|
+
toolId: string;
|
|
37
|
+
toolInput: Record<string, unknown>;
|
|
38
|
+
result?: string;
|
|
39
|
+
isError?: boolean;
|
|
40
|
+
duration?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface IssueRetryState {
|
|
44
|
+
currentPrompt: string;
|
|
45
|
+
retryNumber: number;
|
|
46
|
+
checkpoint: ExecutionCheckpoint | null;
|
|
47
|
+
accumulatedToolResults: ToolRecord[];
|
|
48
|
+
timedOutTools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>;
|
|
49
|
+
/** Session ID from a prior run — enables --resume for premature completion */
|
|
50
|
+
lastSessionId: string | undefined;
|
|
51
|
+
bestResult: SessionResult | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IssueRunnerConfig {
|
|
55
|
+
workingDir: string;
|
|
56
|
+
/** Original enriched prompt for this issue */
|
|
57
|
+
prompt: string;
|
|
58
|
+
/** Stall detection timeouts (ms) */
|
|
59
|
+
stallWarningMs: number;
|
|
60
|
+
stallKillMs: number;
|
|
61
|
+
stallHardCapMs: number;
|
|
62
|
+
stallMaxExtensions: number;
|
|
63
|
+
/** Callback for streaming output to executor event bus */
|
|
64
|
+
outputCallback?: (text: string) => void;
|
|
65
|
+
/** Extra environment variables for spawned Claude processes (e.g. API keys) */
|
|
66
|
+
extraEnv?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute a PM issue with retry logic.
|
|
71
|
+
*
|
|
72
|
+
* This wraps HeadlessRunner.run() with the same retry strategies as Chat view:
|
|
73
|
+
* 1. Tool timeout → checkpoint recovery with accumulated results
|
|
74
|
+
* 2. Signal crash → fresh start with preserved tool results
|
|
75
|
+
* 3. Premature completion → resume session with "continue"
|
|
76
|
+
*/
|
|
77
|
+
export async function runIssueWithRetry(config: IssueRunnerConfig): Promise<SessionResult> {
|
|
78
|
+
const state: IssueRetryState = {
|
|
79
|
+
currentPrompt: config.prompt,
|
|
80
|
+
retryNumber: 0,
|
|
81
|
+
checkpoint: null,
|
|
82
|
+
accumulatedToolResults: [],
|
|
83
|
+
timedOutTools: [],
|
|
84
|
+
lastSessionId: undefined,
|
|
85
|
+
bestResult: null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let result: SessionResult | undefined;
|
|
89
|
+
|
|
90
|
+
while (state.retryNumber <= MAX_ISSUE_RETRIES) {
|
|
91
|
+
// Clear checkpoint from prior iteration
|
|
92
|
+
state.checkpoint = null;
|
|
93
|
+
|
|
94
|
+
// Determine resume strategy
|
|
95
|
+
const useResume = !!state.lastSessionId;
|
|
96
|
+
const resumeSessionId = state.lastSessionId;
|
|
97
|
+
state.lastSessionId = undefined;
|
|
98
|
+
|
|
99
|
+
const runner = new HeadlessRunner({
|
|
100
|
+
workingDir: config.workingDir,
|
|
101
|
+
directPrompt: state.currentPrompt,
|
|
102
|
+
stallWarningMs: config.stallWarningMs,
|
|
103
|
+
stallKillMs: config.stallKillMs,
|
|
104
|
+
stallHardCapMs: config.stallHardCapMs,
|
|
105
|
+
stallMaxExtensions: config.stallMaxExtensions,
|
|
106
|
+
verbose: true,
|
|
107
|
+
continueSession: useResume,
|
|
108
|
+
claudeSessionId: resumeSessionId,
|
|
109
|
+
outputCallback: config.outputCallback,
|
|
110
|
+
onToolTimeout: (cp: ExecutionCheckpoint) => {
|
|
111
|
+
state.checkpoint = cp;
|
|
112
|
+
},
|
|
113
|
+
extraEnv: config.extraEnv,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
result = await runner.run();
|
|
117
|
+
|
|
118
|
+
// Track best result for fallback selection
|
|
119
|
+
if (!state.bestResult || scoreResult(result) > scoreResult(state.bestResult)) {
|
|
120
|
+
state.bestResult = result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Evaluate retry strategies in priority order
|
|
124
|
+
if (tryToolTimeoutRetry(state, result, config)) continue;
|
|
125
|
+
if (trySignalCrashRetry(state, result, config)) continue;
|
|
126
|
+
if (await tryPrematureCompletionRetry(state, result, config)) continue;
|
|
127
|
+
|
|
128
|
+
// No retry needed — break out
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result ?? state.bestResult ?? {
|
|
133
|
+
completed: false,
|
|
134
|
+
needsHandoff: false,
|
|
135
|
+
totalTokens: 0,
|
|
136
|
+
sessionId: '',
|
|
137
|
+
error: 'No result produced after retries',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ========== Retry Strategies ==========
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Strategy 1: Tool timeout checkpoint recovery.
|
|
145
|
+
* When a tool times out, we have a checkpoint with all completed tools.
|
|
146
|
+
* Build a new prompt injecting those results and skip the hung resource.
|
|
147
|
+
*/
|
|
148
|
+
function tryToolTimeoutRetry(
|
|
149
|
+
state: IssueRetryState,
|
|
150
|
+
_result: SessionResult,
|
|
151
|
+
config: IssueRunnerConfig,
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!state.checkpoint || state.retryNumber >= MAX_ISSUE_RETRIES) return false;
|
|
154
|
+
|
|
155
|
+
const cp = state.checkpoint;
|
|
156
|
+
state.retryNumber++;
|
|
157
|
+
|
|
158
|
+
state.timedOutTools.push({
|
|
159
|
+
toolName: cp.hungTool.toolName,
|
|
160
|
+
input: cp.hungTool.input ?? {},
|
|
161
|
+
timeoutMs: cp.hungTool.timeoutMs,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const canResume = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
|
|
165
|
+
|
|
166
|
+
hlog(`[PM-RETRY] Tool timeout: ${cp.hungTool.toolName} after ${Math.round(cp.hungTool.timeoutMs / 1000)}s, ${cp.completedTools.length} tools completed, retry ${state.retryNumber}/${MAX_ISSUE_RETRIES} (${canResume ? 'resume' : 'fresh'})`);
|
|
167
|
+
|
|
168
|
+
if (canResume) {
|
|
169
|
+
state.lastSessionId = cp.claudeSessionId;
|
|
170
|
+
state.currentPrompt = buildResumeRetryPrompt(cp, state.timedOutTools);
|
|
171
|
+
} else {
|
|
172
|
+
state.currentPrompt = buildRetryPrompt(cp, config.prompt, state.timedOutTools);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
config.outputCallback?.(`\n[PM-RETRY] Auto-retry ${state.retryNumber}/${MAX_ISSUE_RETRIES}: ${canResume ? 'Resuming session' : 'Continuing'} with ${cp.completedTools.length} results, skipping failed ${cp.hungTool.toolName}.\n`);
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Strategy 2: Signal crash recovery.
|
|
182
|
+
* Process was killed by signal (SIGTERM/SIGKILL from stall watchdog or OS).
|
|
183
|
+
* Accumulate completed tools and retry with preserved context.
|
|
184
|
+
*/
|
|
185
|
+
function trySignalCrashRetry(
|
|
186
|
+
state: IssueRetryState,
|
|
187
|
+
result: SessionResult,
|
|
188
|
+
config: IssueRunnerConfig,
|
|
189
|
+
): boolean {
|
|
190
|
+
const isSignalCrash = !!result.signalName;
|
|
191
|
+
const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
|
|
192
|
+
if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= MAX_ISSUE_RETRIES) return false;
|
|
193
|
+
// Don't double-handle if a checkpoint was already captured (tool timeout takes priority)
|
|
194
|
+
if (state.checkpoint) return false;
|
|
195
|
+
|
|
196
|
+
accumulateToolResults(result, state);
|
|
197
|
+
state.retryNumber++;
|
|
198
|
+
|
|
199
|
+
const signalInfo = result.signalName || 'unknown signal';
|
|
200
|
+
const useResume = !!result.claudeSessionId && state.retryNumber === 1;
|
|
201
|
+
|
|
202
|
+
hlog(`[PM-RETRY] Signal crash: ${signalInfo}, ${state.accumulatedToolResults.length} tools preserved, retry ${state.retryNumber}/${MAX_ISSUE_RETRIES} (${useResume ? 'resume' : 'fresh'})`);
|
|
203
|
+
|
|
204
|
+
if (useResume) {
|
|
205
|
+
state.lastSessionId = result.claudeSessionId;
|
|
206
|
+
state.currentPrompt = buildSignalCrashRecoveryPrompt(config.prompt, true);
|
|
207
|
+
} else {
|
|
208
|
+
state.currentPrompt = buildSignalCrashRecoveryPrompt(
|
|
209
|
+
config.prompt,
|
|
210
|
+
false,
|
|
211
|
+
state.accumulatedToolResults,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
config.outputCallback?.(`\n[PM-RETRY] Signal recovery ${state.retryNumber}/${MAX_ISSUE_RETRIES}: ${useResume ? 'Resuming' : 'Restarting'} with ${state.accumulatedToolResults.length} preserved results.\n`);
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Check if an end_turn result is actually incomplete using Haiku assessment. */
|
|
221
|
+
async function isEndTurnIncomplete(result: SessionResult): Promise<boolean> {
|
|
222
|
+
if (!result.assistantResponse) return false;
|
|
223
|
+
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
224
|
+
try {
|
|
225
|
+
const verdict = await assessPrematureCompletion({
|
|
226
|
+
responseTail: result.assistantResponse.slice(-800),
|
|
227
|
+
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
228
|
+
hasThinking: !!result.thinkingOutput,
|
|
229
|
+
responseLength: result.assistantResponse.length,
|
|
230
|
+
}, claudeCmd, true);
|
|
231
|
+
|
|
232
|
+
hlog(`[PM-RETRY] Premature completion check: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
|
|
233
|
+
return verdict.isIncomplete;
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Strategy 3: Premature completion.
|
|
241
|
+
* Claude hit max_tokens or ended early without finishing work.
|
|
242
|
+
* Resume the session with "continue".
|
|
243
|
+
*/
|
|
244
|
+
async function tryPrematureCompletionRetry(
|
|
245
|
+
state: IssueRetryState,
|
|
246
|
+
result: SessionResult,
|
|
247
|
+
config: IssueRunnerConfig,
|
|
248
|
+
): Promise<boolean> {
|
|
249
|
+
if (!result.completed || result.signalName || state.retryNumber >= MAX_ISSUE_RETRIES) return false;
|
|
250
|
+
if (state.checkpoint) return false;
|
|
251
|
+
if (!result.claudeSessionId || !result.stopReason) return false;
|
|
252
|
+
|
|
253
|
+
const isMaxTokens = result.stopReason === 'max_tokens';
|
|
254
|
+
const isEndTurn = result.stopReason === 'end_turn';
|
|
255
|
+
if (!isMaxTokens && !isEndTurn) return false;
|
|
256
|
+
|
|
257
|
+
// max_tokens always continues; end_turn requires AI assessment
|
|
258
|
+
if (isEndTurn && !(await isEndTurnIncomplete(result))) return false;
|
|
259
|
+
|
|
260
|
+
state.retryNumber++;
|
|
261
|
+
state.lastSessionId = result.claudeSessionId;
|
|
262
|
+
state.currentPrompt = 'continue';
|
|
263
|
+
|
|
264
|
+
const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished';
|
|
265
|
+
hlog(`[PM-RETRY] Premature completion: ${reason}, resuming session, retry ${state.retryNumber}/${MAX_ISSUE_RETRIES}`);
|
|
266
|
+
config.outputCallback?.(`\n[PM-RETRY] ${reason} — resuming session (retry ${state.retryNumber}/${MAX_ISSUE_RETRIES}).\n`);
|
|
267
|
+
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ========== Helpers ==========
|
|
272
|
+
|
|
273
|
+
function accumulateToolResults(result: SessionResult, state: IssueRetryState): void {
|
|
274
|
+
if (!result.toolUseHistory) return;
|
|
275
|
+
for (const t of result.toolUseHistory) {
|
|
276
|
+
if (t.result !== undefined) {
|
|
277
|
+
state.accumulatedToolResults.push({
|
|
278
|
+
toolName: t.toolName,
|
|
279
|
+
toolId: t.toolId,
|
|
280
|
+
toolInput: t.toolInput,
|
|
281
|
+
result: t.result,
|
|
282
|
+
isError: t.isError,
|
|
283
|
+
duration: t.duration,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (state.accumulatedToolResults.length > MAX_ACCUMULATED_RESULTS) {
|
|
288
|
+
state.accumulatedToolResults = state.accumulatedToolResults.slice(-MAX_ACCUMULATED_RESULTS);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function scoreResult(r: SessionResult): number {
|
|
293
|
+
const toolCount = r.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
294
|
+
const responseLen = Math.min((r.assistantResponse?.length ?? 0) / 50, 100);
|
|
295
|
+
const hasThinking = r.thinkingOutput ? 20 : 0;
|
|
296
|
+
return toolCount * 10 + responseLen + hasThinking;
|
|
297
|
+
}
|
|
@@ -23,7 +23,7 @@ export function slugify(text: string): string {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Resolve the canonical output path for an issue.
|
|
26
|
-
* Uses sprint sandbox when available, otherwise global .pm/out/.
|
|
26
|
+
* Uses sprint sandbox when available, otherwise global .mstro/pm/out/.
|
|
27
27
|
*/
|
|
28
28
|
export function resolveOutputPath(issue: Issue, workingDir: string, sprintSandboxDir: string | null): string {
|
|
29
29
|
if (sprintSandboxDir) {
|
|
@@ -61,7 +61,7 @@ export interface PublishOutputsCallbacks {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Copy confirmed-done outputs from .pm/out/ to user-specified output_file paths.
|
|
64
|
+
* Copy confirmed-done outputs from .mstro/pm/out/ to user-specified output_file paths.
|
|
65
65
|
* Only copies for issues that completed successfully and have output_file set.
|
|
66
66
|
*/
|
|
67
67
|
export function publishOutputs(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* YAML front-matter parsing and entity parsers for PPS (.pm/) files.
|
|
5
|
+
* YAML front-matter parsing and entity parsers for PPS (.mstro/pm/) files.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
@@ -264,7 +264,7 @@ export function parseIssue(content: string, filePath: string): Issue {
|
|
|
264
264
|
id: String(fm.id || ''),
|
|
265
265
|
title: String(fm.title || ''),
|
|
266
266
|
type: (fm.type as Issue['type']) || 'issue',
|
|
267
|
-
status: String(fm.status || '
|
|
267
|
+
status: String(fm.status || 'todo'),
|
|
268
268
|
priority: String(fm.priority || 'P2'),
|
|
269
269
|
estimate: fm.estimate != null ? fm.estimate as number | string : null,
|
|
270
270
|
labels: toStringArray(fm.labels),
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Legacy → Board-centric migration for .pm/ directories.
|
|
5
|
+
* Legacy → Board-centric migration for .mstro/pm/ directories.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { parseFrontMatter } from './parser-core.js';
|
|
11
11
|
|
|
12
|
-
/** Check whether a .pm/ directory uses the legacy flat format (has backlog/ at root, no boards/). */
|
|
12
|
+
/** Check whether a .mstro/pm/ directory uses the legacy flat format (has backlog/ at root, no boards/). */
|
|
13
13
|
export function isLegacyFormat(pmDir: string): boolean {
|
|
14
14
|
return existsSync(join(pmDir, 'backlog')) && !existsSync(join(pmDir, 'boards'));
|
|
15
15
|
}
|
|
@@ -73,10 +73,10 @@ function cleanupMigratedIssues(boardBacklogDir: string): boolean {
|
|
|
73
73
|
const content = readFileIfExists(join(boardBacklogDir, f));
|
|
74
74
|
if (!content) continue;
|
|
75
75
|
const fm = parseFrontMatter(content).frontMatter;
|
|
76
|
-
const status = String(fm.status || '
|
|
76
|
+
const status = String(fm.status || 'todo');
|
|
77
77
|
if (status === 'done' || status === 'closed' || status === 'cancelled') {
|
|
78
78
|
rmSync(join(boardBacklogDir, f));
|
|
79
|
-
} else if (status !== '
|
|
79
|
+
} else if (status !== 'todo') {
|
|
80
80
|
hasActive = true;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -104,7 +104,7 @@ function writeBoardMetadata(pmDir: string, boardDir: string, boardId: string, sp
|
|
|
104
104
|
}, null, 2));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** Migrate a legacy flat .pm/ directory to the board-centric format. */
|
|
107
|
+
/** Migrate a legacy flat .mstro/pm/ directory to the board-centric format. */
|
|
108
108
|
export function migrateToBoards(pmDir: string): void {
|
|
109
109
|
const boardId = 'BOARD-001';
|
|
110
110
|
const boardDir = join(pmDir, 'boards', boardId);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* PPS Parser — Public API for reading .pm/
|
|
5
|
+
* PPS Parser — Public API for reading .mstro/pm/ directories.
|
|
6
6
|
*
|
|
7
7
|
* Entity parsing lives in parser-core.ts; migration in parser-migration.ts.
|
|
8
8
|
*/
|
|
@@ -37,10 +37,6 @@ export function isBoardCentricFormat(pmDir: string): boolean {
|
|
|
37
37
|
export function resolvePmDir(workingDir: string): string | null {
|
|
38
38
|
const mstroPmDir = join(workingDir, '.mstro', 'pm');
|
|
39
39
|
if (existsSync(mstroPmDir)) return mstroPmDir;
|
|
40
|
-
const legacyPmDir = join(workingDir, '.pm');
|
|
41
|
-
if (existsSync(legacyPmDir)) return legacyPmDir;
|
|
42
|
-
const legacyPlanDir = join(workingDir, '.plan');
|
|
43
|
-
if (existsSync(legacyPlanDir)) return legacyPlanDir;
|
|
44
40
|
return null;
|
|
45
41
|
}
|
|
46
42
|
|
|
@@ -125,6 +121,9 @@ export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullSt
|
|
|
125
121
|
issue.blockedBy = issue.blockedBy.map(bp => bp.startsWith('boards/') ? bp : `${boardPrefix}${bp}`);
|
|
126
122
|
issue.blocks = issue.blocks.map(bp => bp.startsWith('boards/') ? bp : `${boardPrefix}${bp}`);
|
|
127
123
|
if (issue.epic && !issue.epic.startsWith('boards/')) issue.epic = `${boardPrefix}${issue.epic}`;
|
|
124
|
+
if (issue.children.length > 0) {
|
|
125
|
+
issue.children = issue.children.map(cp => cp.startsWith('boards/') ? cp : `${boardPrefix}${cp}`);
|
|
126
|
+
}
|
|
128
127
|
return issue;
|
|
129
128
|
});
|
|
130
129
|
|
|
@@ -15,7 +15,7 @@ export interface CoordinatorPromptOptions {
|
|
|
15
15
|
issues: Issue[];
|
|
16
16
|
workingDir: string;
|
|
17
17
|
pmDir: string | null;
|
|
18
|
-
/** Board directory path when executing a board (e.g. /path/.pm/boards/BOARD-001). */
|
|
18
|
+
/** Board directory path when executing a board (e.g. /path/.mstro/pm/boards/BOARD-001). */
|
|
19
19
|
boardDir: string | null;
|
|
20
20
|
existingDocs: string[];
|
|
21
21
|
resolveOutputPath: (issue: Issue) => string;
|