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.
Files changed (167) hide show
  1. package/README.md +66 -0
  2. package/dist/server/cli/headless/claude-invoker-process.js +1 -1
  3. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  4. package/dist/server/cli/headless/mcp-config.d.ts +1 -1
  5. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
  6. package/dist/server/cli/headless/mcp-config.js +4 -1
  7. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +1 -0
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/types.d.ts +4 -1
  12. package/dist/server/cli/headless/types.d.ts.map +1 -1
  13. package/dist/server/index.js +9 -1
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  16. package/dist/server/mcp/bouncer-integration.js +9 -1
  17. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  18. package/dist/server/mcp/security-analysis.d.ts +6 -0
  19. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  20. package/dist/server/mcp/security-analysis.js +16 -1
  21. package/dist/server/mcp/security-analysis.js.map +1 -1
  22. package/dist/server/mcp/security-patterns.d.ts +8 -0
  23. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  24. package/dist/server/mcp/security-patterns.js +47 -2
  25. package/dist/server/mcp/security-patterns.js.map +1 -1
  26. package/dist/server/services/deploy/ai-broker.d.ts +63 -0
  27. package/dist/server/services/deploy/ai-broker.d.ts.map +1 -0
  28. package/dist/server/services/deploy/ai-broker.js +360 -0
  29. package/dist/server/services/deploy/ai-broker.js.map +1 -0
  30. package/dist/server/services/deploy/board-execution-handler.d.ts +114 -0
  31. package/dist/server/services/deploy/board-execution-handler.d.ts.map +1 -0
  32. package/dist/server/services/deploy/board-execution-handler.js +621 -0
  33. package/dist/server/services/deploy/board-execution-handler.js.map +1 -0
  34. package/dist/server/services/deploy/credentials.d.ts +35 -0
  35. package/dist/server/services/deploy/credentials.d.ts.map +1 -0
  36. package/dist/server/services/deploy/credentials.js +177 -0
  37. package/dist/server/services/deploy/credentials.js.map +1 -0
  38. package/dist/server/services/deploy/deploy-ai-service.d.ts +107 -0
  39. package/dist/server/services/deploy/deploy-ai-service.d.ts.map +1 -0
  40. package/dist/server/services/deploy/deploy-ai-service.js +294 -0
  41. package/dist/server/services/deploy/deploy-ai-service.js.map +1 -0
  42. package/dist/server/services/deploy/headless-session-handler.d.ts +94 -0
  43. package/dist/server/services/deploy/headless-session-handler.d.ts.map +1 -0
  44. package/dist/server/services/deploy/headless-session-handler.js +274 -0
  45. package/dist/server/services/deploy/headless-session-handler.js.map +1 -0
  46. package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
  47. package/dist/server/services/file-explorer-ops.js +2 -6
  48. package/dist/server/services/file-explorer-ops.js.map +1 -1
  49. package/dist/server/services/plan/agent-loader.d.ts +10 -0
  50. package/dist/server/services/plan/agent-loader.d.ts.map +1 -0
  51. package/dist/server/services/plan/agent-loader.js +65 -0
  52. package/dist/server/services/plan/agent-loader.js.map +1 -0
  53. package/dist/server/services/plan/composer.js +1 -1
  54. package/dist/server/services/plan/dependency-resolver.d.ts +1 -1
  55. package/dist/server/services/plan/dependency-resolver.js +2 -2
  56. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  57. package/dist/server/services/plan/executor.d.ts +6 -2
  58. package/dist/server/services/plan/executor.d.ts.map +1 -1
  59. package/dist/server/services/plan/executor.js +21 -6
  60. package/dist/server/services/plan/executor.js.map +1 -1
  61. package/dist/server/services/plan/front-matter.d.ts +5 -0
  62. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  63. package/dist/server/services/plan/front-matter.js +19 -0
  64. package/dist/server/services/plan/front-matter.js.map +1 -1
  65. package/dist/server/services/plan/issue-prompt-builder.d.ts +1 -1
  66. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  67. package/dist/server/services/plan/issue-prompt-builder.js +1 -1
  68. package/dist/server/services/plan/issue-retry.d.ts +2 -0
  69. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  70. package/dist/server/services/plan/issue-retry.js +1 -0
  71. package/dist/server/services/plan/issue-retry.js.map +1 -1
  72. package/dist/server/services/plan/output-manager.d.ts +2 -2
  73. package/dist/server/services/plan/output-manager.js +2 -2
  74. package/dist/server/services/plan/parser-core.d.ts +1 -1
  75. package/dist/server/services/plan/parser-core.js +1 -1
  76. package/dist/server/services/plan/parser-core.js.map +1 -1
  77. package/dist/server/services/plan/parser-migration.d.ts +2 -2
  78. package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
  79. package/dist/server/services/plan/parser-migration.js +5 -5
  80. package/dist/server/services/plan/parser-migration.js.map +1 -1
  81. package/dist/server/services/plan/parser.d.ts.map +1 -1
  82. package/dist/server/services/plan/parser.js +4 -7
  83. package/dist/server/services/plan/parser.js.map +1 -1
  84. package/dist/server/services/plan/prompt-builder.d.ts +1 -1
  85. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  86. package/dist/server/services/plan/review-gate.d.ts +4 -0
  87. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  88. package/dist/server/services/plan/review-gate.js +89 -34
  89. package/dist/server/services/plan/review-gate.js.map +1 -1
  90. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  91. package/dist/server/services/plan/state-reconciler.js +21 -11
  92. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  93. package/dist/server/services/plan/types.d.ts +2 -2
  94. package/dist/server/services/plan/types.d.ts.map +1 -1
  95. package/dist/server/services/plan/watcher.js +1 -1
  96. package/dist/server/services/sentry.d.ts.map +1 -1
  97. package/dist/server/services/sentry.js +8 -4
  98. package/dist/server/services/sentry.js.map +1 -1
  99. package/dist/server/services/websocket/deploy-handlers.d.ts +14 -0
  100. package/dist/server/services/websocket/deploy-handlers.d.ts.map +1 -0
  101. package/dist/server/services/websocket/deploy-handlers.js +409 -0
  102. package/dist/server/services/websocket/deploy-handlers.js.map +1 -0
  103. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  104. package/dist/server/services/websocket/handler.js +4 -0
  105. package/dist/server/services/websocket/handler.js.map +1 -1
  106. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +11 -0
  107. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +1 -0
  108. package/dist/server/services/websocket/handlers/deploy-handlers.js +181 -0
  109. package/dist/server/services/websocket/handlers/deploy-handlers.js.map +1 -0
  110. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  111. package/dist/server/services/websocket/plan-board-handlers.js +54 -1
  112. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  113. package/dist/server/services/websocket/plan-helpers.d.ts +1 -1
  114. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
  115. package/dist/server/services/websocket/plan-helpers.js +3 -4
  116. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  117. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
  118. package/dist/server/services/websocket/plan-issue-handlers.js +5 -1
  119. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
  120. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
  121. package/dist/server/services/websocket/plan-sprint-handlers.js +3 -11
  122. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
  123. package/dist/server/services/websocket/types.d.ts +264 -2
  124. package/dist/server/services/websocket/types.d.ts.map +1 -1
  125. package/package.json +1 -1
  126. package/server/cli/headless/claude-invoker-process.ts +1 -1
  127. package/server/cli/headless/mcp-config.ts +4 -1
  128. package/server/cli/headless/runner.ts +1 -0
  129. package/server/cli/headless/types.ts +4 -1
  130. package/server/index.ts +9 -1
  131. package/server/mcp/bouncer-integration.ts +9 -1
  132. package/server/mcp/security-analysis.ts +19 -0
  133. package/server/mcp/security-patterns.ts +53 -2
  134. package/server/services/deploy/ai-broker.ts +512 -0
  135. package/server/services/deploy/board-execution-handler.ts +847 -0
  136. package/server/services/deploy/credentials.ts +200 -0
  137. package/server/services/deploy/deploy-ai-service.ts +401 -0
  138. package/server/services/deploy/headless-session-handler.ts +415 -0
  139. package/server/services/file-explorer-ops.ts +2 -6
  140. package/server/services/plan/agent-loader.ts +73 -0
  141. package/server/services/plan/agents/review-code.md +28 -0
  142. package/server/services/plan/agents/review-custom.md +27 -0
  143. package/server/services/plan/agents/review-quality.md +42 -0
  144. package/server/services/plan/composer.ts +1 -1
  145. package/server/services/plan/dependency-resolver.ts +2 -2
  146. package/server/services/plan/executor.ts +21 -6
  147. package/server/services/plan/front-matter.ts +23 -0
  148. package/server/services/plan/issue-prompt-builder.ts +2 -2
  149. package/server/services/plan/issue-retry.ts +3 -0
  150. package/server/services/plan/output-manager.ts +2 -2
  151. package/server/services/plan/parser-core.ts +2 -2
  152. package/server/services/plan/parser-migration.ts +5 -5
  153. package/server/services/plan/parser.ts +4 -5
  154. package/server/services/plan/prompt-builder.ts +1 -1
  155. package/server/services/plan/review-gate.ts +104 -33
  156. package/server/services/plan/state-reconciler.ts +21 -11
  157. package/server/services/plan/types.ts +3 -3
  158. package/server/services/plan/watcher.ts +1 -1
  159. package/server/services/sentry.ts +8 -4
  160. package/server/services/websocket/deploy-handlers.ts +544 -0
  161. package/server/services/websocket/handler.ts +5 -1
  162. package/server/services/websocket/handlers/deploy-handlers.ts +231 -0
  163. package/server/services/websocket/plan-board-handlers.ts +53 -1
  164. package/server/services/websocket/plan-helpers.ts +3 -4
  165. package/server/services/websocket/plan-issue-handlers.ts +6 -1
  166. package/server/services/websocket/plan-sprint-handlers.ts +3 -9
  167. 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 — accept agent's done status directly
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
- setFrontMatterField(join(pmDir, issuePath), 'status', newStatus);
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: done\`
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 || 'backlog'),
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 || 'backlog');
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 !== 'backlog') {
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/ (or legacy .plan/) directories.
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(issue: Issue, pmDir: string, outputPath: string, isCodeTask: boolean, reviewCriteria?: string): string {
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 a generic review prompt
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
- return `You are a reviewer. Review the work done for issue ${issue.id}: ${issue.title}.
187
- ${isCodeTask ? `\n## Files Modified\n${issue.filesToModify.map(f => `- ${f}`).join('\n')}` : `\n## Output File\n${outputPath}\n\n## Issue Spec\n${join(pmDir, issue.path)}`}
188
-
189
- ## Acceptance Criteria
190
- ${criteria || 'No specific criteria defined.'}
191
-
192
- ## Review Criteria
193
- ${reviewCriteria}
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
- ## Instructions
196
- 1. ${isCodeTask ? 'Read each modified file listed above' : 'Read the output file and issue spec at the paths above'}
197
- 2. Check if all acceptance criteria are met
198
- 3. Evaluate against the review criteria above
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
- Output EXACTLY one JSON object on its own line (no markdown fencing):
201
- {"passed": true, "checks": [{"name": "criteria_met", "passed": true, "details": "..."}]}
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
- Include checks for: criteria_met, review_criteria.`;
204
- }
237
+ // ── Hardcoded fallbacks (used when agent files are missing) ──────────
205
238
 
206
- if (isCodeTask) {
207
- return `You are a code reviewer. Review the work done for issue ${issue.id}: ${issue.title}.
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
- ${issue.filesToModify.map(f => `- ${f}`).join('\n')}
243
+ ${filesModified}
211
244
 
212
245
  ## Acceptance Criteria
213
- ${criteria || 'No specific criteria defined.'}
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 code changes
218
- 3. Look for obvious bugs, security vulnerabilities, or code quality issues
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
- ${join(pmDir, issue.path)}
269
+ ${issueSpecPath}
234
270
 
235
271
  ## Acceptance Criteria
236
- ${criteria || 'No specific criteria defined.'}
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. Check if all acceptance criteria are met
242
- 4. Check for completeness and quality of the output
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 (auto-complete the epic).
114
+ * - All children done/cancelled → done
115
+ * - Any child in_progress/in_review → in_progress
116
+ * - Otherwise → null (no change)
115
117
  */
116
- function deriveEpicDone(epic: Issue, issueByPath: Map<string, Issue>): boolean {
117
- if (epic.children.length === 0) return false;
118
- if (epic.status === 'done' || epic.status === 'cancelled') return false;
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
- return epic.children.every(childPath => {
121
- const child = issueByPath.get(childPath);
122
- return child && (child.status === 'done' || child.status === 'cancelled');
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
- if (!deriveEpicDone(epic, issueByPath)) continue;
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', 'done');
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
- if (!deriveEpicDone(epic, issueByPath)) return null;
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/ (or legacy .plan/) directory for changes and broadcasts updates.
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
- // Hardcoded DSN for production - this is safe to expose (can only send, not read)
10
- // Override with SENTRY_DSN env var for development/testing
11
- const SENTRY_DSN = process.env.SENTRY_DSN || 'https://2a8d2493e3ee5a7beec30f4518a5e24c@o4510824844820480.ingest.us.sentry.io/4510824923594752'
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: SENTRY_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,