mstro-app 0.4.21 → 0.4.24
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/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.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +9 -1
- 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/file-explorer-ops.d.ts.map +1 -1
- package/dist/server/services/file-explorer-ops.js +2 -6
- package/dist/server/services/file-explorer-ops.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.js +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 +6 -2
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +21 -6
- 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 +2 -0
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +1 -0
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- 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 +89 -34
- 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 +4 -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 +181 -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/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/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 +9 -1
- 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/file-explorer-ops.ts +2 -6
- 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 +1 -1
- package/server/services/plan/dependency-resolver.ts +2 -2
- package/server/services/plan/executor.ts +21 -6
- 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 +3 -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 +104 -33
- 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 +5 -1
- package/server/services/websocket/handlers/deploy-handlers.ts +231 -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/types.ts +333 -2
|
@@ -22,7 +22,7 @@ import { join } from 'node:path';
|
|
|
22
22
|
import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
|
|
23
23
|
import { ConfigInstaller } from './config-installer.js';
|
|
24
24
|
import { resolveReadyToWork } from './dependency-resolver.js';
|
|
25
|
-
import { replaceFrontMatterField, setFrontMatterField } from './front-matter.js';
|
|
25
|
+
import { checkAllAcceptanceCriteria, replaceFrontMatterField, setFrontMatterField } from './front-matter.js';
|
|
26
26
|
import { buildIssuePrompt } from './issue-prompt-builder.js';
|
|
27
27
|
import { runIssueWithRetry } from './issue-retry.js';
|
|
28
28
|
import { listExistingDocs, publishOutputs, resolveOutputPath } from './output-manager.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
|
|
|
@@ -251,6 +254,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
251
254
|
outputCallback: (text: string) => {
|
|
252
255
|
this.emit('output', { issueId: issue.id, text });
|
|
253
256
|
},
|
|
257
|
+
extraEnv: this.extraEnv,
|
|
254
258
|
}), boardLogDir);
|
|
255
259
|
|
|
256
260
|
if (!result.completed || result.error) {
|
|
@@ -314,9 +318,10 @@ export class PlanExecutor extends EventEmitter {
|
|
|
314
318
|
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
315
319
|
const currentStatus = statusMatch?.[1] ?? 'unknown';
|
|
316
320
|
|
|
317
|
-
if (currentStatus === 'done') {
|
|
321
|
+
if (currentStatus === 'in_review' || currentStatus === 'done') {
|
|
318
322
|
if (issue.reviewGate === 'none') {
|
|
319
|
-
// Skip review gate —
|
|
323
|
+
// Skip review gate — mark done directly
|
|
324
|
+
this.updateIssueFrontMatter(issue.path, 'done');
|
|
320
325
|
this.metrics.issuesCompleted++;
|
|
321
326
|
this.emit('issueCompleted', issue);
|
|
322
327
|
completed++;
|
|
@@ -361,6 +366,8 @@ export class PlanExecutor extends EventEmitter {
|
|
|
361
366
|
onOutput: (text) => this.emit('output', { issueId: issue.id, text }),
|
|
362
367
|
logDir: this.boardDir ? join(this.boardDir, 'logs') : undefined,
|
|
363
368
|
reviewCriteria: this.getBoardReviewCriteria(),
|
|
369
|
+
boardDir: this.boardDir,
|
|
370
|
+
extraEnv: this.extraEnv,
|
|
364
371
|
});
|
|
365
372
|
persistReviewResult(reviewDir, issue, result);
|
|
366
373
|
|
|
@@ -542,7 +549,15 @@ export class PlanExecutor extends EventEmitter {
|
|
|
542
549
|
const pmDir = this.pmDir;
|
|
543
550
|
if (!pmDir) return;
|
|
544
551
|
try {
|
|
545
|
-
|
|
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
|
+
}
|
|
546
561
|
} catch { /* file may have been moved */ }
|
|
547
562
|
}
|
|
548
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
|
|
|
@@ -62,6 +62,8 @@ export interface IssueRunnerConfig {
|
|
|
62
62
|
stallMaxExtensions: number;
|
|
63
63
|
/** Callback for streaming output to executor event bus */
|
|
64
64
|
outputCallback?: (text: string) => void;
|
|
65
|
+
/** Extra environment variables for spawned Claude processes (e.g. API keys) */
|
|
66
|
+
extraEnv?: Record<string, string>;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -108,6 +110,7 @@ export async function runIssueWithRetry(config: IssueRunnerConfig): Promise<Sess
|
|
|
108
110
|
onToolTimeout: (cp: ExecutionCheckpoint) => {
|
|
109
111
|
state.checkpoint = cp;
|
|
110
112
|
},
|
|
113
|
+
extraEnv: config.extraEnv,
|
|
111
114
|
});
|
|
112
115
|
|
|
113
116
|
result = await runner.run();
|
|
@@ -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;
|
|
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
|
|
15
15
|
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
16
|
+
import { loadAgentPrompt } from './agent-loader.js';
|
|
16
17
|
import type { Issue, ReviewCheck, ReviewResult } from './types.js';
|
|
17
18
|
|
|
18
19
|
/** Max review attempts per issue per sprint before giving up */
|
|
@@ -33,6 +34,10 @@ export interface ReviewIssueOptions {
|
|
|
33
34
|
logDir?: string;
|
|
34
35
|
/** Custom board-level review criteria — replaces default review instructions when set */
|
|
35
36
|
reviewCriteria?: string;
|
|
37
|
+
/** Board directory for agent prompt override resolution (e.g., .mstro/pm/boards/BOARD-001) */
|
|
38
|
+
boardDir?: string | null;
|
|
39
|
+
/** Extra environment variables for spawned Claude processes (e.g. API keys) */
|
|
40
|
+
extraEnv?: Record<string, string>;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/**
|
|
@@ -40,12 +45,12 @@ export interface ReviewIssueOptions {
|
|
|
40
45
|
* Returns auto-pass on infrastructure failures to avoid blocking execution.
|
|
41
46
|
*/
|
|
42
47
|
export async function reviewIssue(options: ReviewIssueOptions): Promise<ReviewResult> {
|
|
43
|
-
const { workingDir, issue, pmDir, outputPath, onOutput, logDir, reviewCriteria } = options;
|
|
48
|
+
const { workingDir, issue, pmDir, outputPath, onOutput, logDir, reviewCriteria, boardDir } = options;
|
|
44
49
|
const isCodeTask = issue.filesToModify.length > 0;
|
|
45
50
|
const issueType: ReviewResult['issueType'] = isCodeTask ? 'code' : 'non-code';
|
|
46
51
|
|
|
47
52
|
try {
|
|
48
|
-
const prompt = buildReviewPrompt(issue, pmDir, outputPath, isCodeTask, reviewCriteria);
|
|
53
|
+
const prompt = buildReviewPrompt(issue, pmDir, outputPath, isCodeTask, reviewCriteria, boardDir);
|
|
49
54
|
|
|
50
55
|
const runner = new HeadlessRunner({
|
|
51
56
|
workingDir,
|
|
@@ -55,6 +60,7 @@ export async function reviewIssue(options: ReviewIssueOptions): Promise<ReviewRe
|
|
|
55
60
|
stallHardCapMs: REVIEW_STALL_HARD_CAP_MS,
|
|
56
61
|
verbose: true,
|
|
57
62
|
outputCallback: onOutput ? (text: string) => onOutput(`Review: ${text}`) : undefined,
|
|
63
|
+
extraEnv: options.extraEnv,
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
const result = await runWithFileLogger('pm-review', () => runner.run(), logDir);
|
|
@@ -175,74 +181,139 @@ export function autoPassResult(issueId: string, issueType: ReviewResult['issueTy
|
|
|
175
181
|
|
|
176
182
|
// ── Private helpers ─────────────────────────────────────────
|
|
177
183
|
|
|
178
|
-
function buildReviewPrompt(
|
|
184
|
+
function buildReviewPrompt(
|
|
185
|
+
issue: Issue,
|
|
186
|
+
pmDir: string,
|
|
187
|
+
outputPath: string,
|
|
188
|
+
isCodeTask: boolean,
|
|
189
|
+
reviewCriteria?: string,
|
|
190
|
+
boardDir?: string | null,
|
|
191
|
+
): string {
|
|
179
192
|
const criteria = issue.acceptanceCriteria
|
|
180
193
|
.map(c => `- [${c.checked ? 'x' : ' '}] ${c.text}`)
|
|
181
194
|
.join('\n');
|
|
195
|
+
const criteriaStr = criteria || 'No specific criteria defined.';
|
|
196
|
+
const filesModified = issue.filesToModify.map(f => `- ${f}`).join('\n');
|
|
197
|
+
const issueSpecPath = join(pmDir, issue.path);
|
|
182
198
|
|
|
183
|
-
// When custom review criteria are set, use
|
|
184
|
-
// that applies the user's criteria instead of assuming code review.
|
|
199
|
+
// When custom review criteria are set, use the review-custom agent.
|
|
185
200
|
if (reviewCriteria) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
const contextSection = isCodeTask
|
|
202
|
+
? `\n## Files Modified\n${filesModified}`
|
|
203
|
+
: `\n## Output File\n${outputPath}\n\n## Issue Spec\n${issueSpecPath}`;
|
|
204
|
+
const readInstruction = isCodeTask
|
|
205
|
+
? 'Read each modified file listed above'
|
|
206
|
+
: 'Read the output file and issue spec at the paths above';
|
|
207
|
+
|
|
208
|
+
return loadAgentPrompt('review-custom', {
|
|
209
|
+
issue_id: issue.id,
|
|
210
|
+
issue_title: issue.title,
|
|
211
|
+
context_section: contextSection,
|
|
212
|
+
acceptance_criteria: criteriaStr,
|
|
213
|
+
review_criteria: reviewCriteria,
|
|
214
|
+
read_instruction: readInstruction,
|
|
215
|
+
}, boardDir) ?? buildCustomFallback(issue, contextSection, criteriaStr, reviewCriteria, readInstruction);
|
|
216
|
+
}
|
|
194
217
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
if (isCodeTask) {
|
|
219
|
+
return loadAgentPrompt('review-code', {
|
|
220
|
+
issue_id: issue.id,
|
|
221
|
+
issue_title: issue.title,
|
|
222
|
+
files_modified: filesModified,
|
|
223
|
+
acceptance_criteria: criteriaStr,
|
|
224
|
+
output_path: outputPath,
|
|
225
|
+
}, boardDir) ?? buildCodeFallback(issue, filesModified, criteriaStr, outputPath);
|
|
226
|
+
}
|
|
199
227
|
|
|
200
|
-
|
|
201
|
-
|
|
228
|
+
return loadAgentPrompt('review-quality', {
|
|
229
|
+
issue_id: issue.id,
|
|
230
|
+
issue_title: issue.title,
|
|
231
|
+
output_path: outputPath,
|
|
232
|
+
issue_spec_path: issueSpecPath,
|
|
233
|
+
acceptance_criteria: criteriaStr,
|
|
234
|
+
}, boardDir) ?? buildQualityFallback(issue, outputPath, issueSpecPath, criteriaStr);
|
|
235
|
+
}
|
|
202
236
|
|
|
203
|
-
|
|
204
|
-
}
|
|
237
|
+
// ── Hardcoded fallbacks (used when agent files are missing) ──────────
|
|
205
238
|
|
|
206
|
-
|
|
207
|
-
|
|
239
|
+
function buildCodeFallback(issue: Issue, filesModified: string, criteria: string, outputPath: string): string {
|
|
240
|
+
return `You are a reviewer. Review the work done for issue ${issue.id}: ${issue.title}.
|
|
208
241
|
|
|
209
242
|
## Files Modified
|
|
210
|
-
${
|
|
243
|
+
${filesModified}
|
|
211
244
|
|
|
212
245
|
## Acceptance Criteria
|
|
213
|
-
${criteria
|
|
246
|
+
${criteria}
|
|
214
247
|
|
|
215
248
|
## Instructions
|
|
216
249
|
1. Read each modified file listed above
|
|
217
|
-
2. Check if all acceptance criteria are met by the
|
|
218
|
-
3.
|
|
250
|
+
2. Check if all acceptance criteria are met by the changes
|
|
251
|
+
3. Evaluate the quality of the changes:
|
|
252
|
+
- For source code files: look for obvious bugs, security vulnerabilities, or code quality issues
|
|
253
|
+
- For content files (markdown, docs, config, copy): check for accuracy, completeness, and appropriate structure
|
|
219
254
|
4. Check if the output artifact exists at: ${outputPath}
|
|
220
255
|
|
|
221
256
|
Output EXACTLY one JSON object on its own line (no markdown fencing):
|
|
222
257
|
{"passed": true, "checks": [{"name": "criteria_met", "passed": true, "details": "..."}]}
|
|
223
258
|
|
|
224
259
|
Include checks for: criteria_met, code_quality, no_obvious_bugs.`;
|
|
225
|
-
|
|
260
|
+
}
|
|
226
261
|
|
|
262
|
+
function buildQualityFallback(issue: Issue, outputPath: string, issueSpecPath: string, criteria: string): string {
|
|
227
263
|
return `You are a quality reviewer. Review the work done for issue ${issue.id}: ${issue.title}.
|
|
228
264
|
|
|
229
265
|
## Output File
|
|
230
266
|
${outputPath}
|
|
231
267
|
|
|
232
268
|
## Issue Spec
|
|
233
|
-
${
|
|
269
|
+
${issueSpecPath}
|
|
234
270
|
|
|
235
271
|
## Acceptance Criteria
|
|
236
|
-
${criteria
|
|
272
|
+
${criteria}
|
|
237
273
|
|
|
238
274
|
## Instructions
|
|
239
275
|
1. Read the output file at the path above
|
|
240
|
-
2. Read the full issue spec
|
|
241
|
-
3.
|
|
242
|
-
|
|
276
|
+
2. Read the full issue spec to understand the original requirements and intent
|
|
277
|
+
3. Evaluate the output against ALL of the following dimensions:
|
|
278
|
+
|
|
279
|
+
### Acceptance Criteria
|
|
280
|
+
- Are all acceptance criteria met? Check each one individually.
|
|
281
|
+
|
|
282
|
+
### Content Quality
|
|
283
|
+
- Is the content accurate, well-reasoned, and free of factual errors?
|
|
284
|
+
- Is it written clearly with appropriate structure and organization?
|
|
285
|
+
- Does it have sufficient depth and detail for its purpose?
|
|
286
|
+
- Is the tone and style appropriate for the intended audience?
|
|
287
|
+
|
|
288
|
+
### Completeness
|
|
289
|
+
- Does the output fully address what was requested in the issue spec?
|
|
290
|
+
- Are there obvious gaps, missing sections, or incomplete thoughts?
|
|
291
|
+
- If the issue requested specific deliverables (e.g., a plan, analysis, document), are all deliverables present?
|
|
243
292
|
|
|
244
293
|
Output EXACTLY one JSON object on its own line (no markdown fencing):
|
|
245
294
|
{"passed": true, "checks": [{"name": "criteria_met", "passed": true, "details": "..."}]}
|
|
246
295
|
|
|
247
296
|
Include checks for: criteria_met, output_quality, completeness.`;
|
|
248
297
|
}
|
|
298
|
+
|
|
299
|
+
function buildCustomFallback(issue: Issue, contextSection: string, criteria: string, reviewCriteria: string, readInstruction: string): string {
|
|
300
|
+
return `You are a reviewer. Review the work done for issue ${issue.id}: ${issue.title}.
|
|
301
|
+
${contextSection}
|
|
302
|
+
|
|
303
|
+
## Acceptance Criteria
|
|
304
|
+
${criteria}
|
|
305
|
+
|
|
306
|
+
## Review Criteria
|
|
307
|
+
${reviewCriteria}
|
|
308
|
+
|
|
309
|
+
## Instructions
|
|
310
|
+
1. ${readInstruction}
|
|
311
|
+
2. Check if all acceptance criteria are met — evaluate each criterion individually
|
|
312
|
+
3. Evaluate thoroughly against the review criteria above
|
|
313
|
+
4. Consider the overall quality of the work: does it fully address the issue's intent, is it well-structured, and is it ready to ship?
|
|
314
|
+
|
|
315
|
+
Output EXACTLY one JSON object on its own line (no markdown fencing):
|
|
316
|
+
{"passed": true, "checks": [{"name": "criteria_met", "passed": true, "details": "..."}]}
|
|
317
|
+
|
|
318
|
+
Include checks for: criteria_met, review_criteria.`;
|
|
319
|
+
}
|
|
@@ -111,27 +111,36 @@ function buildStateMarkdown(
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Derive epic status from its children's actual statuses.
|
|
114
|
-
* All children done/cancelled → done
|
|
114
|
+
* - All children done/cancelled → done
|
|
115
|
+
* - Any child in_progress/in_review → in_progress
|
|
116
|
+
* - Otherwise → null (no change)
|
|
115
117
|
*/
|
|
116
|
-
function
|
|
117
|
-
if (epic.children.length === 0) return
|
|
118
|
-
if (epic.status === 'done' || epic.status === 'cancelled') return
|
|
118
|
+
function deriveEpicStatus(epic: Issue, issueByPath: Map<string, Issue>): string | null {
|
|
119
|
+
if (epic.children.length === 0) return null;
|
|
120
|
+
if (epic.status === 'done' || epic.status === 'cancelled') return null;
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const childStatuses = epic.children.map(cp => issueByPath.get(cp)?.status).filter(Boolean) as string[];
|
|
123
|
+
if (childStatuses.length === 0) return null;
|
|
124
|
+
|
|
125
|
+
const allFinished = childStatuses.every(s => s === 'done' || s === 'cancelled');
|
|
126
|
+
if (allFinished) return 'done';
|
|
127
|
+
|
|
128
|
+
const anyStarted = childStatuses.some(s => s === 'in_progress' || s === 'in_review');
|
|
129
|
+
if (anyStarted && epic.status !== 'in_progress') return 'in_progress';
|
|
130
|
+
|
|
131
|
+
return null;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
function reconcileEpicStatuses(pmDir: string, issues: Issue[], issueByPath: Map<string, Issue>): void {
|
|
127
135
|
const epics = issues.filter(i => i.type === 'epic');
|
|
128
136
|
for (const epic of epics) {
|
|
129
|
-
|
|
137
|
+
const derived = deriveEpicStatus(epic, issueByPath);
|
|
138
|
+
if (!derived) continue;
|
|
130
139
|
|
|
131
140
|
const epicPath = join(pmDir, epic.path);
|
|
132
141
|
try {
|
|
133
142
|
let content = readFileSync(epicPath, 'utf-8');
|
|
134
|
-
content = replaceFrontMatterField(content, 'status',
|
|
143
|
+
content = replaceFrontMatterField(content, 'status', derived);
|
|
135
144
|
writeFileSync(epicPath, content, 'utf-8');
|
|
136
145
|
} catch {
|
|
137
146
|
// Epic file may be missing or unwritable
|
|
@@ -206,7 +215,8 @@ export function tryCompleteParentEpic(workingDir: string, updatedIssue: Issue):
|
|
|
206
215
|
if (!epic) return null;
|
|
207
216
|
|
|
208
217
|
const issueByPath = new Map(issues.map(i => [i.path, i]));
|
|
209
|
-
|
|
218
|
+
const derived = deriveEpicStatus(epic, issueByPath);
|
|
219
|
+
if (derived !== 'done') return null;
|
|
210
220
|
|
|
211
221
|
const epicFullPath = join(pmDir, epic.path);
|
|
212
222
|
try {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Plan Types — Project Plan Spec (PPS) data structures
|
|
6
6
|
*
|
|
7
|
-
* These types represent the parsed contents of .pm/ directory files.
|
|
7
|
+
* These types represent the parsed contents of .mstro/pm/ directory files.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
@@ -25,7 +25,7 @@ export interface ProjectConfig {
|
|
|
25
25
|
|
|
26
26
|
export interface WorkflowStatus {
|
|
27
27
|
status: string;
|
|
28
|
-
category: 'unstarted' | 'started' | 'completed' | 'cancelled';
|
|
28
|
+
category: 'ready' | 'unstarted' | 'started' | 'completed' | 'cancelled';
|
|
29
29
|
description: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -98,7 +98,7 @@ export interface Issue {
|
|
|
98
98
|
outputFile: string | null;
|
|
99
99
|
// Full markdown body
|
|
100
100
|
body: string;
|
|
101
|
-
// File path relative to .pm/
|
|
101
|
+
// File path relative to .mstro/pm/
|
|
102
102
|
path: string;
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Plan Watcher — Watches .pm/
|
|
5
|
+
* Plan Watcher — Watches .mstro/pm/ directory for changes and broadcasts updates.
|
|
6
6
|
*
|
|
7
7
|
* Uses fs.watch with debouncing to batch rapid changes.
|
|
8
8
|
*/
|
|
@@ -6,9 +6,12 @@ import { homedir } from 'node:os'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import * as Sentry from '@sentry/node'
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
9
|
+
// Sentry DSN lives on the platform server. The CLI sends envelopes
|
|
10
|
+
// to the server's /sentry-tunnel endpoint which proxies to Sentry.
|
|
11
|
+
// A placeholder DSN is needed so the Sentry SDK initializes its
|
|
12
|
+
// transport — the real DSN is injected server-side before forwarding.
|
|
13
|
+
const SENTRY_TUNNEL_DSN = 'https://tunnel@sentry.io/0'
|
|
14
|
+
const PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
|
|
12
15
|
|
|
13
16
|
const CONFIG_FILE = join(homedir(), '.mstro', 'config.json')
|
|
14
17
|
|
|
@@ -51,7 +54,8 @@ export function initSentry(): void {
|
|
|
51
54
|
initialized = true
|
|
52
55
|
|
|
53
56
|
Sentry.init({
|
|
54
|
-
dsn:
|
|
57
|
+
dsn: SENTRY_TUNNEL_DSN,
|
|
58
|
+
tunnel: `${PLATFORM_URL}/sentry-tunnel`,
|
|
55
59
|
environment: process.env.NODE_ENV || 'development',
|
|
56
60
|
release: `mstro-cli@${process.env.npm_package_version || '0.0.0'}`,
|
|
57
61
|
tracesSampleRate: 0.1,
|