mstro-app 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mstro.js +119 -40
- package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.js +140 -0
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
- package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +10 -804
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
- package/dist/server/cli/headless/haiku-assessments.js +281 -0
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
- package/dist/server/cli/headless/headless-logger.d.ts +3 -2
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +28 -5
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
- package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
- package/dist/server/cli/headless/native-timeout-detector.js +99 -0
- package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +65 -457
- package/dist/server/cli/headless/stall-assessor.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/cli/improvisation-attachments.d.ts +21 -0
- package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
- package/dist/server/cli/improvisation-attachments.js +116 -0
- package/dist/server/cli/improvisation-attachments.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +52 -0
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
- package/dist/server/cli/improvisation-retry.js +434 -0
- package/dist/server/cli/improvisation-retry.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +117 -1079
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +86 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -0
- package/dist/server/cli/improvisation-types.js +10 -0
- package/dist/server/cli/improvisation-types.js.map +1 -0
- package/dist/server/cli/prompt-builders.d.ts +68 -0
- package/dist/server/cli/prompt-builders.d.ts.map +1 -0
- package/dist/server/cli/prompt-builders.js +312 -0
- package/dist/server/cli/prompt-builders.js.map +1 -0
- package/dist/server/index.js +33 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-haiku.js +152 -0
- package/dist/server/mcp/bouncer-haiku.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +3 -4
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +50 -196
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts +38 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -0
- package/dist/server/mcp/security-analysis.js +183 -0
- package/dist/server/mcp/security-analysis.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +1 -1
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts +1 -25
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +55 -260
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/server-setup.d.ts +22 -0
- package/dist/server/server-setup.d.ts.map +1 -0
- package/dist/server/server-setup.js +101 -0
- package/dist/server/server-setup.js.map +1 -0
- package/dist/server/services/file-explorer-ops.d.ts +24 -0
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
- package/dist/server/services/file-explorer-ops.js +211 -0
- package/dist/server/services/file-explorer-ops.js.map +1 -0
- package/dist/server/services/files.d.ts +2 -85
- package/dist/server/services/files.d.ts.map +1 -1
- package/dist/server/services/files.js +7 -427
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +118 -32
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/config-installer.d.ts +25 -0
- package/dist/server/services/plan/config-installer.d.ts.map +1 -0
- package/dist/server/services/plan/config-installer.js +182 -0
- package/dist/server/services/plan/config-installer.js.map +1 -0
- package/dist/server/services/plan/dependency-resolver.d.ts +1 -1
- package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
- package/dist/server/services/plan/dependency-resolver.js +4 -1
- package/dist/server/services/plan/dependency-resolver.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +38 -74
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +274 -460
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +18 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -0
- package/dist/server/services/plan/front-matter.js +44 -0
- package/dist/server/services/plan/front-matter.js.map +1 -0
- package/dist/server/services/plan/output-manager.d.ts +22 -0
- package/dist/server/services/plan/output-manager.d.ts.map +1 -0
- package/dist/server/services/plan/output-manager.js +97 -0
- package/dist/server/services/plan/output-manager.js.map +1 -0
- package/dist/server/services/plan/parser-core.d.ts +20 -0
- package/dist/server/services/plan/parser-core.d.ts.map +1 -0
- package/dist/server/services/plan/parser-core.js +350 -0
- package/dist/server/services/plan/parser-core.js.map +1 -0
- package/dist/server/services/plan/parser-migration.d.ts +5 -0
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
- package/dist/server/services/plan/parser-migration.js +124 -0
- package/dist/server/services/plan/parser-migration.js.map +1 -0
- package/dist/server/services/plan/parser.d.ts +11 -3
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +184 -369
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/prompt-builder.d.ts +17 -0
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -0
- package/dist/server/services/plan/prompt-builder.js +137 -0
- package/dist/server/services/plan/prompt-builder.js.map +1 -0
- package/dist/server/services/plan/review-gate.d.ts +28 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -0
- package/dist/server/services/plan/review-gate.js +191 -0
- package/dist/server/services/plan/review-gate.js.map +1 -0
- package/dist/server/services/plan/state-reconciler.d.ts +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +59 -7
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +68 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts +24 -0
- package/dist/server/services/platform-credentials.d.ts.map +1 -0
- package/dist/server/services/platform-credentials.js +68 -0
- package/dist/server/services/platform-credentials.js.map +1 -0
- package/dist/server/services/platform.d.ts +1 -31
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +11 -109
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +7 -97
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +53 -266
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts +57 -0
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-utils.js +141 -0
- package/dist/server/services/terminal/pty-utils.js.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.js +153 -0
- package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-search-handlers.js +238 -0
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.js +3 -3
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.js +110 -0
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.js +123 -0
- package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +2 -31
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +35 -541
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-log-handlers.js +128 -0
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +13 -53
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-tag-handlers.js +76 -0
- package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-utils.d.ts +43 -0
- package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/git-utils.js +201 -0
- package/dist/server/services/websocket/git-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +37 -112
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.js +218 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +21 -462
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-helpers.js +199 -0
- package/dist/server/services/websocket/plan-helpers.js.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-complexity.js +262 -0
- package/dist/server/services/websocket/quality-complexity.js.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.js +140 -0
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +34 -346
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-linting.d.ts +9 -0
- package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-linting.js +178 -0
- package/dist/server/services/websocket/quality-linting.js.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.js +206 -0
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +3 -51
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +9 -651
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +23 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-tools.js +208 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -0
- package/dist/server/services/websocket/quality-types.d.ts +59 -0
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-types.js +101 -0
- package/dist/server/services/websocket/quality-types.js.map +1 -0
- package/dist/server/services/websocket/session-handlers.d.ts +3 -4
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +3 -378
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.d.ts +4 -0
- package/dist/server/services/websocket/session-history.d.ts.map +1 -0
- package/dist/server/services/websocket/session-history.js +208 -0
- package/dist/server/services/websocket/session-history.js.map +1 -0
- package/dist/server/services/websocket/session-initialization.d.ts +5 -0
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
- package/dist/server/services/websocket/session-initialization.js +163 -0
- package/dist/server/services/websocket/session-initialization.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +12 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -2
- package/server/cli/headless/claude-invoker-process.ts +204 -0
- package/server/cli/headless/claude-invoker-stall.ts +164 -0
- package/server/cli/headless/claude-invoker-stream.ts +353 -0
- package/server/cli/headless/claude-invoker-tools.ts +187 -0
- package/server/cli/headless/claude-invoker.ts +15 -1092
- package/server/cli/headless/haiku-assessments.ts +365 -0
- package/server/cli/headless/headless-logger.ts +26 -5
- package/server/cli/headless/native-timeout-detector.ts +117 -0
- package/server/cli/headless/stall-assessor.ts +65 -618
- package/server/cli/headless/types.ts +4 -1
- package/server/cli/improvisation-attachments.ts +148 -0
- package/server/cli/improvisation-retry.ts +602 -0
- package/server/cli/improvisation-session-manager.ts +140 -1349
- package/server/cli/improvisation-types.ts +98 -0
- package/server/cli/prompt-builders.ts +370 -0
- package/server/index.ts +35 -246
- package/server/mcp/bouncer-haiku.ts +182 -0
- package/server/mcp/bouncer-integration.ts +87 -248
- package/server/mcp/security-analysis.ts +217 -0
- package/server/mcp/security-audit.ts +1 -1
- package/server/mcp/security-patterns.ts +60 -283
- package/server/server-setup.ts +114 -0
- package/server/services/file-explorer-ops.ts +293 -0
- package/server/services/files.ts +20 -532
- package/server/services/plan/composer.ts +140 -35
- package/server/services/plan/config-installer.ts +187 -0
- package/server/services/plan/dependency-resolver.ts +4 -1
- package/server/services/plan/executor.ts +281 -488
- package/server/services/plan/front-matter.ts +48 -0
- package/server/services/plan/output-manager.ts +113 -0
- package/server/services/plan/parser-core.ts +406 -0
- package/server/services/plan/parser-migration.ts +128 -0
- package/server/services/plan/parser.ts +188 -394
- package/server/services/plan/prompt-builder.ts +161 -0
- package/server/services/plan/review-gate.ts +212 -0
- package/server/services/plan/state-reconciler.ts +68 -7
- package/server/services/plan/types.ts +101 -1
- package/server/services/platform-credentials.ts +83 -0
- package/server/services/platform.ts +16 -131
- package/server/services/terminal/pty-manager.ts +66 -313
- package/server/services/terminal/pty-utils.ts +176 -0
- package/server/services/websocket/file-definition-handlers.ts +165 -0
- package/server/services/websocket/file-explorer-handlers.ts +37 -452
- package/server/services/websocket/file-search-handlers.ts +291 -0
- package/server/services/websocket/file-utils.ts +3 -3
- package/server/services/websocket/git-branch-handlers.ts +130 -0
- package/server/services/websocket/git-diff-handlers.ts +140 -0
- package/server/services/websocket/git-handlers.ts +40 -625
- package/server/services/websocket/git-log-handlers.ts +149 -0
- package/server/services/websocket/git-pr-handlers.ts +17 -62
- package/server/services/websocket/git-tag-handlers.ts +91 -0
- package/server/services/websocket/git-utils.ts +230 -0
- package/server/services/websocket/handler.ts +39 -112
- package/server/services/websocket/plan-board-handlers.ts +277 -0
- package/server/services/websocket/plan-execution-handlers.ts +184 -0
- package/server/services/websocket/plan-handlers.ts +23 -544
- package/server/services/websocket/plan-helpers.ts +215 -0
- package/server/services/websocket/plan-issue-handlers.ts +204 -0
- package/server/services/websocket/plan-sprint-handlers.ts +252 -0
- package/server/services/websocket/quality-complexity.ts +294 -0
- package/server/services/websocket/quality-fix-agent.ts +181 -0
- package/server/services/websocket/quality-handlers.ts +36 -404
- package/server/services/websocket/quality-linting.ts +187 -0
- package/server/services/websocket/quality-review-agent.ts +246 -0
- package/server/services/websocket/quality-service.ts +11 -762
- package/server/services/websocket/quality-tools.ts +209 -0
- package/server/services/websocket/quality-types.ts +169 -0
- package/server/services/websocket/session-handlers.ts +5 -437
- package/server/services/websocket/session-history.ts +222 -0
- package/server/services/websocket/session-initialization.ts +209 -0
- package/server/services/websocket/types.ts +46 -2
|
@@ -4,19 +4,29 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Plan Executor — Wave-based execution with Claude Code Agent Teams.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Orchestrates the execution loop: picks ready issues, executes waves,
|
|
8
|
+
* runs AI review gate, reconciles state, and repeats.
|
|
9
|
+
*
|
|
10
|
+
* Implementation is split across focused modules:
|
|
11
|
+
* - config-installer.ts — teammate permissions + bouncer MCP install/uninstall
|
|
12
|
+
* - prompt-builder.ts — Agent Teams coordinator prompt construction
|
|
13
|
+
* - output-manager.ts — output path resolution, listing, publishing
|
|
14
|
+
* - review-gate.ts — AI-powered quality gate (review, parse, persist)
|
|
15
|
+
* - front-matter.ts — YAML front matter field editing utility
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import {
|
|
14
|
-
import { join
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
15
21
|
import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
|
|
16
22
|
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
17
|
-
import {
|
|
23
|
+
import { ConfigInstaller } from './config-installer.js';
|
|
18
24
|
import { resolveReadyToWork } from './dependency-resolver.js';
|
|
19
|
-
import {
|
|
25
|
+
import { replaceFrontMatterField, setFrontMatterField } from './front-matter.js';
|
|
26
|
+
import { listExistingDocs, publishOutputs, resolveOutputPath } from './output-manager.js';
|
|
27
|
+
import { parseBoardDirectory, parsePlanDirectory, resolvePmDir } from './parser.js';
|
|
28
|
+
import { buildCoordinatorPrompt } from './prompt-builder.js';
|
|
29
|
+
import { appendReviewFeedback, getReviewAttemptCount, MAX_REVIEW_ATTEMPTS, persistReviewResult, reviewIssue } from './review-gate.js';
|
|
20
30
|
import { reconcileState } from './state-reconciler.js';
|
|
21
31
|
import type { Issue } from './types.js';
|
|
22
32
|
|
|
@@ -25,6 +35,15 @@ export type ExecutionStatus = 'idle' | 'starting' | 'executing' | 'paused' | 'st
|
|
|
25
35
|
/** Max teammates per wave. Agent Teams docs recommend 3-5; beyond 5-6 returns diminish. */
|
|
26
36
|
const MAX_WAVE_SIZE = 5;
|
|
27
37
|
|
|
38
|
+
/** Stop after this many consecutive waves with zero completions. */
|
|
39
|
+
const MAX_CONSECUTIVE_EMPTY_WAVES = 3;
|
|
40
|
+
|
|
41
|
+
/** Wave execution stall timeouts (ms) */
|
|
42
|
+
const WAVE_STALL_WARNING_MS = 1_800_000; // 30 min — Agent Teams leads are silent while teammates work
|
|
43
|
+
const WAVE_STALL_KILL_MS = 3_600_000; // 60 min — waves run longer
|
|
44
|
+
const WAVE_STALL_HARD_CAP_MS = 7_200_000; // 2 hr hard cap
|
|
45
|
+
const WAVE_STALL_MAX_EXTENSIONS = 10;
|
|
46
|
+
|
|
28
47
|
export interface ExecutionMetrics {
|
|
29
48
|
issuesCompleted: number;
|
|
30
49
|
issuesAttempted: number;
|
|
@@ -40,6 +59,13 @@ export class PlanExecutor extends EventEmitter {
|
|
|
40
59
|
private shouldStop = false;
|
|
41
60
|
private shouldPause = false;
|
|
42
61
|
private epicScope: string | null = null;
|
|
62
|
+
/** Board directory path (e.g. /path/.pm/boards/BOARD-001). Used for outputs, reviews, progress. */
|
|
63
|
+
private boardDir: string | null = null;
|
|
64
|
+
/** Board ID being executed (e.g. "BOARD-001") */
|
|
65
|
+
private boardId: string | null = null;
|
|
66
|
+
private configInstaller: ConfigInstaller;
|
|
67
|
+
/** Flag to prevent start() from clearing scope set by startBoard/startEpic */
|
|
68
|
+
private _scopeSetByCall = false;
|
|
43
69
|
private metrics: ExecutionMetrics = {
|
|
44
70
|
issuesCompleted: 0,
|
|
45
71
|
issuesAttempted: 0,
|
|
@@ -51,18 +77,22 @@ export class PlanExecutor extends EventEmitter {
|
|
|
51
77
|
constructor(workingDir: string) {
|
|
52
78
|
super();
|
|
53
79
|
this.workingDir = workingDir;
|
|
80
|
+
this.configInstaller = new ConfigInstaller(workingDir);
|
|
54
81
|
}
|
|
55
82
|
|
|
56
|
-
getStatus(): ExecutionStatus {
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getMetrics(): ExecutionMetrics {
|
|
61
|
-
return { ...this.metrics };
|
|
62
|
-
}
|
|
83
|
+
getStatus(): ExecutionStatus { return this.status; }
|
|
84
|
+
getMetrics(): ExecutionMetrics { return { ...this.metrics }; }
|
|
63
85
|
|
|
64
86
|
async startEpic(epicPath: string): Promise<void> {
|
|
65
87
|
this.epicScope = epicPath;
|
|
88
|
+
this._scopeSetByCall = true;
|
|
89
|
+
return this.start();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Start execution, optionally scoped to a specific board. */
|
|
93
|
+
async startBoard(boardId: string): Promise<void> {
|
|
94
|
+
this.boardId = boardId;
|
|
95
|
+
this._scopeSetByCall = true;
|
|
66
96
|
return this.start();
|
|
67
97
|
}
|
|
68
98
|
|
|
@@ -71,44 +101,29 @@ export class PlanExecutor extends EventEmitter {
|
|
|
71
101
|
|
|
72
102
|
this.shouldStop = false;
|
|
73
103
|
this.shouldPause = false;
|
|
104
|
+
// Reset scoping from previous runs unless explicitly set by startBoard/startEpic
|
|
105
|
+
if (!this._scopeSetByCall) {
|
|
106
|
+
this.epicScope = null;
|
|
107
|
+
this.boardId = null;
|
|
108
|
+
}
|
|
109
|
+
this._scopeSetByCall = false;
|
|
74
110
|
this.status = 'starting';
|
|
75
111
|
this.emit('statusChanged', this.status);
|
|
76
112
|
|
|
77
113
|
const startTime = Date.now();
|
|
78
|
-
|
|
79
114
|
this.status = 'executing';
|
|
80
115
|
this.emit('statusChanged', this.status);
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
while (!this.shouldStop && !this.shouldPause) {
|
|
85
|
-
const readyIssues = this.pickReadyIssues();
|
|
86
|
-
if (readyIssues.length === 0) break;
|
|
87
|
-
|
|
88
|
-
// Cap wave size per Agent Teams best practices (3-5 teammates optimal).
|
|
89
|
-
// Remaining issues will be picked up in subsequent waves.
|
|
90
|
-
const waveIssues = readyIssues.slice(0, MAX_WAVE_SIZE);
|
|
91
|
-
|
|
92
|
-
const completedCount = await this.executeWave(waveIssues);
|
|
117
|
+
this.boardDir = this.resolveBoardDir();
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
consecutiveZeroCompletions = 0;
|
|
96
|
-
} else {
|
|
97
|
-
consecutiveZeroCompletions++;
|
|
98
|
-
// Stop after 3 consecutive waves with zero completions to avoid
|
|
99
|
-
// retrying the same failing issues indefinitely.
|
|
100
|
-
if (consecutiveZeroCompletions >= 3) {
|
|
101
|
-
this.metrics.totalDuration = Date.now() - startTime;
|
|
102
|
-
this.status = 'error';
|
|
103
|
-
this.emit('statusChanged', this.status);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
119
|
+
const stallResult = await this.runWaveLoop();
|
|
108
120
|
|
|
109
121
|
this.metrics.totalDuration = Date.now() - startTime;
|
|
110
122
|
|
|
111
|
-
if (
|
|
123
|
+
if (stallResult === 'stalled') {
|
|
124
|
+
this.status = 'error';
|
|
125
|
+
this.emit('error', `Execution stalled: ${MAX_CONSECUTIVE_EMPTY_WAVES} consecutive waves completed zero issues. Issues may be stuck in review or failing repeatedly.`);
|
|
126
|
+
} else if (this.shouldPause) {
|
|
112
127
|
this.status = 'paused';
|
|
113
128
|
} else if (this.shouldStop) {
|
|
114
129
|
this.status = 'idle';
|
|
@@ -118,21 +133,38 @@ export class PlanExecutor extends EventEmitter {
|
|
|
118
133
|
this.emit('statusChanged', this.status);
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
/** Run waves until done, paused, stopped, or stalled. Returns 'stalled' if zero-completion cap hit. */
|
|
137
|
+
private async runWaveLoop(): Promise<'done' | 'stalled'> {
|
|
138
|
+
let consecutiveZeroCompletions = 0;
|
|
139
|
+
|
|
140
|
+
while (!this.shouldStop && !this.shouldPause) {
|
|
141
|
+
const readyIssues = this.pickReadyIssues();
|
|
142
|
+
if (readyIssues.length === 0) break;
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
const completedCount = await this.executeWave(readyIssues.slice(0, MAX_WAVE_SIZE));
|
|
145
|
+
|
|
146
|
+
if (completedCount > 0) {
|
|
147
|
+
consecutiveZeroCompletions = 0;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
consecutiveZeroCompletions++;
|
|
151
|
+
if (consecutiveZeroCompletions >= MAX_CONSECUTIVE_EMPTY_WAVES) return 'stalled';
|
|
152
|
+
}
|
|
153
|
+
return 'done';
|
|
127
154
|
}
|
|
128
155
|
|
|
156
|
+
pause(): void { this.shouldPause = true; }
|
|
157
|
+
stop(): void { this.shouldStop = true; }
|
|
158
|
+
|
|
129
159
|
resume(): Promise<void> {
|
|
130
160
|
if (this.status !== 'paused') return Promise.resolve();
|
|
131
161
|
this.shouldPause = false;
|
|
162
|
+
// Preserve board/epic scope across resume by marking as a scoped call
|
|
163
|
+
this._scopeSetByCall = true;
|
|
132
164
|
return this.start();
|
|
133
165
|
}
|
|
134
166
|
|
|
135
|
-
// ── Wave execution
|
|
167
|
+
// ── Wave execution ───────────────────────────────────────────
|
|
136
168
|
|
|
137
169
|
private async executeWave(issues: Issue[]): Promise<number> {
|
|
138
170
|
const waveStart = Date.now();
|
|
@@ -142,25 +174,24 @@ export class PlanExecutor extends EventEmitter {
|
|
|
142
174
|
this.metrics.issuesAttempted += issues.length;
|
|
143
175
|
this.emit('waveStarted', { issueIds: waveIds });
|
|
144
176
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const outDir = join(pmDir, 'out');
|
|
149
|
-
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Pre-approve tools so teammates don't hit interactive permission prompts
|
|
153
|
-
this.installTeammatePermissions();
|
|
177
|
+
this.ensureOutputDirs();
|
|
178
|
+
this.configInstaller.installTeammatePermissions();
|
|
179
|
+
this.configInstaller.installBouncerForSubagents();
|
|
154
180
|
|
|
155
|
-
// Install bouncer .mcp.json so Agent Teams teammates discover it
|
|
156
|
-
this.installBouncerForSubagents();
|
|
157
|
-
|
|
158
|
-
// Mark all wave issues as in_progress
|
|
159
181
|
for (const issue of issues) {
|
|
160
182
|
this.updateIssueFrontMatter(issue.path, 'in_progress');
|
|
161
183
|
}
|
|
162
184
|
|
|
163
|
-
const
|
|
185
|
+
const existingDocs = listExistingDocs(this.workingDir, this.boardDir);
|
|
186
|
+
const pmDir = resolvePmDir(this.workingDir);
|
|
187
|
+
const prompt = buildCoordinatorPrompt({
|
|
188
|
+
issues,
|
|
189
|
+
workingDir: this.workingDir,
|
|
190
|
+
pmDir,
|
|
191
|
+
boardDir: this.boardDir,
|
|
192
|
+
existingDocs,
|
|
193
|
+
resolveOutputPath: (issue) => resolveOutputPath(issue, this.workingDir, this.boardDir),
|
|
194
|
+
});
|
|
164
195
|
|
|
165
196
|
let completedCount = 0;
|
|
166
197
|
|
|
@@ -168,20 +199,20 @@ export class PlanExecutor extends EventEmitter {
|
|
|
168
199
|
const runner = new HeadlessRunner({
|
|
169
200
|
workingDir: this.workingDir,
|
|
170
201
|
directPrompt: prompt,
|
|
171
|
-
stallWarningMs:
|
|
172
|
-
stallKillMs:
|
|
173
|
-
stallHardCapMs:
|
|
174
|
-
stallMaxExtensions:
|
|
202
|
+
stallWarningMs: WAVE_STALL_WARNING_MS,
|
|
203
|
+
stallKillMs: WAVE_STALL_KILL_MS,
|
|
204
|
+
stallHardCapMs: WAVE_STALL_HARD_CAP_MS,
|
|
205
|
+
stallMaxExtensions: WAVE_STALL_MAX_EXTENSIONS,
|
|
175
206
|
verbose: process.env.MSTRO_VERBOSE === '1',
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
},
|
|
207
|
+
disallowedTools: ['TeamCreate', 'TeamDelete', 'TaskCreate', 'TaskUpdate', 'TaskList'],
|
|
208
|
+
extraEnv: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' },
|
|
179
209
|
outputCallback: (text: string) => {
|
|
180
210
|
this.emit('output', { issueId: waveLabel, text });
|
|
181
211
|
},
|
|
182
212
|
});
|
|
183
213
|
|
|
184
|
-
const
|
|
214
|
+
const boardLogDir = this.boardDir ? join(this.boardDir, 'logs') : undefined;
|
|
215
|
+
const result = await runWithFileLogger('pm-execute-wave', () => runner.run(), boardLogDir);
|
|
185
216
|
|
|
186
217
|
if (!result.completed || result.error) {
|
|
187
218
|
this.emit('waveError', {
|
|
@@ -190,9 +221,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
190
221
|
});
|
|
191
222
|
}
|
|
192
223
|
|
|
193
|
-
|
|
194
|
-
completedCount = this.reconcileWaveResults(issues);
|
|
195
|
-
|
|
224
|
+
completedCount = await this.reconcileWaveResults(issues);
|
|
196
225
|
} catch (error) {
|
|
197
226
|
this.emit('waveError', {
|
|
198
227
|
issueIds: waveIds,
|
|
@@ -200,9 +229,8 @@ export class PlanExecutor extends EventEmitter {
|
|
|
200
229
|
});
|
|
201
230
|
this.revertIncompleteIssues(issues);
|
|
202
231
|
} finally {
|
|
203
|
-
|
|
204
|
-
this.
|
|
205
|
-
this.uninstallTeammatePermissions();
|
|
232
|
+
this.configInstaller.uninstallBouncerForSubagents();
|
|
233
|
+
this.configInstaller.uninstallTeammatePermissions();
|
|
206
234
|
}
|
|
207
235
|
|
|
208
236
|
this.finalizeWave(issues, waveStart, waveLabel);
|
|
@@ -212,12 +240,11 @@ export class PlanExecutor extends EventEmitter {
|
|
|
212
240
|
|
|
213
241
|
/**
|
|
214
242
|
* Post-wave operations wrapped individually so a failure in one
|
|
215
|
-
* (e.g. reconcileState hitting a concurrent write from PlanWatcher)
|
|
216
243
|
* doesn't prevent the others or kill the while loop in start().
|
|
217
244
|
*/
|
|
218
245
|
private finalizeWave(issues: Issue[], waveStart: number, waveLabel: string): void {
|
|
219
246
|
try {
|
|
220
|
-
reconcileState(this.workingDir);
|
|
247
|
+
reconcileState(this.workingDir, this.boardId ?? undefined);
|
|
221
248
|
this.emit('stateUpdated');
|
|
222
249
|
} catch (err) {
|
|
223
250
|
this.emit('output', {
|
|
@@ -227,7 +254,9 @@ export class PlanExecutor extends EventEmitter {
|
|
|
227
254
|
}
|
|
228
255
|
|
|
229
256
|
try {
|
|
230
|
-
|
|
257
|
+
publishOutputs(issues, this.workingDir, this.boardDir, {
|
|
258
|
+
onWarning: (issueId, text) => this.emit('output', { issueId, text: `Warning: ${text}` }),
|
|
259
|
+
});
|
|
231
260
|
} catch (err) {
|
|
232
261
|
this.emit('output', {
|
|
233
262
|
issueId: waveLabel,
|
|
@@ -245,13 +274,14 @@ export class PlanExecutor extends EventEmitter {
|
|
|
245
274
|
}
|
|
246
275
|
}
|
|
247
276
|
|
|
277
|
+
// ── Review gate orchestration ────────────────────────────────
|
|
278
|
+
|
|
248
279
|
/**
|
|
249
|
-
* After a wave, check each issue's status on disk.
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* (bug fixes, refactors) don't produce docs but are still valid completions.
|
|
280
|
+
* After a wave, check each issue's status on disk and run the AI review gate.
|
|
281
|
+
* Issues that agents marked as `done` are moved to `in_review`, reviewed,
|
|
282
|
+
* and either confirmed `done` (passed) or reverted to `todo` (failed).
|
|
253
283
|
*/
|
|
254
|
-
private reconcileWaveResults(issues: Issue[]): number {
|
|
284
|
+
private async reconcileWaveResults(issues: Issue[]): Promise<number> {
|
|
255
285
|
const pmDir = resolvePmDir(this.workingDir);
|
|
256
286
|
if (!pmDir) return 0;
|
|
257
287
|
|
|
@@ -265,11 +295,15 @@ export class PlanExecutor extends EventEmitter {
|
|
|
265
295
|
const currentStatus = statusMatch?.[1] ?? 'unknown';
|
|
266
296
|
|
|
267
297
|
if (currentStatus === 'done') {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
298
|
+
if (issue.reviewGate === 'none') {
|
|
299
|
+
// Skip review gate — accept agent's done status directly
|
|
300
|
+
this.metrics.issuesCompleted++;
|
|
301
|
+
this.emit('issueCompleted', issue);
|
|
302
|
+
completed++;
|
|
303
|
+
} else {
|
|
304
|
+
completed += await this.runReviewGate(issue, pmDir);
|
|
305
|
+
}
|
|
271
306
|
} else {
|
|
272
|
-
// Not done — revert to prior status
|
|
273
307
|
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
274
308
|
this.emit('issueError', {
|
|
275
309
|
issueId: issue.id,
|
|
@@ -284,454 +318,195 @@ export class PlanExecutor extends EventEmitter {
|
|
|
284
318
|
return completed;
|
|
285
319
|
}
|
|
286
320
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
this.
|
|
293
|
-
|
|
321
|
+
/** Run the review gate for a single issue that agents marked as done. Returns 1 if passed, 0 otherwise. */
|
|
322
|
+
private async runReviewGate(issue: Issue, pmDir: string): Promise<number> {
|
|
323
|
+
const reviewDir = this.boardDir ?? pmDir;
|
|
324
|
+
const attempts = getReviewAttemptCount(reviewDir, issue);
|
|
325
|
+
if (attempts >= MAX_REVIEW_ATTEMPTS) {
|
|
326
|
+
this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
327
|
+
this.emit('reviewProgress', { issueId: issue.id, status: 'max_attempts' });
|
|
328
|
+
this.emit('output', { issueId: issue.id, text: 'Review: max attempts reached, keeping in review' });
|
|
329
|
+
return 0;
|
|
294
330
|
}
|
|
295
|
-
if (fullState.state.paused) {
|
|
296
|
-
this.emit('error', 'Project is paused');
|
|
297
|
-
return [];
|
|
298
|
-
}
|
|
299
|
-
const readyIssues = resolveReadyToWork(fullState.issues, this.epicScope ?? undefined);
|
|
300
|
-
if (readyIssues.length === 0) {
|
|
301
|
-
this.emit('complete', this.epicScope ? 'All epic issues are done or blocked' : 'All work is done or blocked');
|
|
302
|
-
}
|
|
303
|
-
return readyIssues;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ── Prompt building ───────────────────────────────────────────
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Build the team lead prompt for a wave of issues.
|
|
310
|
-
* Uses Agent Teams for true parallel execution as separate processes —
|
|
311
|
-
* each teammate gets its own context window and sends idle notifications
|
|
312
|
-
* when done. The team is created implicitly by the first Agent(team_name=...) call.
|
|
313
|
-
*/
|
|
314
|
-
private buildCoordinatorPrompt(issues: Issue[]): string {
|
|
315
|
-
const pmDir = resolvePmDir(this.workingDir);
|
|
316
|
-
const outDir = pmDir ? join(pmDir, 'out') : join(this.workingDir, '.pm', 'out');
|
|
317
|
-
|
|
318
|
-
// Collect existing output docs that issues may need as input
|
|
319
|
-
const existingDocs = this.listExistingDocs();
|
|
320
|
-
|
|
321
|
-
const issueBlocks = issues.map(issue => {
|
|
322
|
-
const criteria = issue.acceptanceCriteria
|
|
323
|
-
.map(c => `- [${c.checked ? 'x' : ' '}] ${c.text}`)
|
|
324
|
-
.join('\n');
|
|
325
|
-
|
|
326
|
-
const files = issue.filesToModify.length > 0
|
|
327
|
-
? `\nFiles to modify:\n${issue.filesToModify.map(f => `- ${f}`).join('\n')}`
|
|
328
|
-
: '';
|
|
329
|
-
|
|
330
|
-
// Find predecessor output docs this issue should read
|
|
331
|
-
const predecessorDocs = issue.blockedBy
|
|
332
|
-
.map(bp => {
|
|
333
|
-
const blockerId = bp.replace(/^backlog\//, '').replace(/\.md$/, '');
|
|
334
|
-
return existingDocs.find(d => d.toLowerCase().includes(blockerId.toLowerCase()));
|
|
335
|
-
})
|
|
336
|
-
.filter(Boolean) as string[];
|
|
337
|
-
|
|
338
|
-
const predecessorSection = predecessorDocs.length > 0
|
|
339
|
-
? `\nPredecessor outputs to read:\n${predecessorDocs.map(d => `- ${d}`).join('\n')}`
|
|
340
|
-
: '';
|
|
341
|
-
|
|
342
|
-
return `### ${issue.id}: ${issue.title}
|
|
343
|
-
|
|
344
|
-
**Type**: ${issue.type} | **Priority**: ${issue.priority} | **Estimate**: ${issue.estimate ?? 'unestimated'}
|
|
345
|
-
|
|
346
|
-
**Description**:
|
|
347
|
-
${issue.description}
|
|
348
|
-
|
|
349
|
-
**Acceptance Criteria**:
|
|
350
|
-
${criteria || 'No specific criteria defined.'}
|
|
351
|
-
|
|
352
|
-
**Technical Notes**:
|
|
353
|
-
${issue.technicalNotes || 'None'}
|
|
354
|
-
${files}${predecessorSection}
|
|
355
|
-
|
|
356
|
-
**Output file**: ${this.resolveOutputPath(issue)}`;
|
|
357
|
-
}).join('\n\n---\n\n');
|
|
358
|
-
|
|
359
|
-
const teamName = `pm-wave-${Date.now()}`;
|
|
360
|
-
|
|
361
|
-
const teammateSpawns = issues.map(issue => {
|
|
362
|
-
const predecessorDocs = issue.blockedBy
|
|
363
|
-
.map(bp => {
|
|
364
|
-
const blockerId = bp.replace(/^backlog\//, '').replace(/\.md$/, '');
|
|
365
|
-
return existingDocs.find(d => d.toLowerCase().includes(blockerId.toLowerCase()));
|
|
366
|
-
})
|
|
367
|
-
.filter(Boolean) as string[];
|
|
368
|
-
|
|
369
|
-
const predInstr = predecessorDocs.length > 0
|
|
370
|
-
? `Read these predecessor output docs before starting: ${predecessorDocs.join(', ')}. `
|
|
371
|
-
: '';
|
|
372
|
-
|
|
373
|
-
const outputFile = this.resolveOutputPath(issue);
|
|
374
|
-
|
|
375
|
-
const fileOwnership = issue.filesToModify.length > 0
|
|
376
|
-
? `\n> FILE OWNERSHIP: You may ONLY modify these files: ${issue.filesToModify.join(', ')}. Do not touch files owned by other teammates.`
|
|
377
|
-
: '';
|
|
378
|
-
|
|
379
|
-
return `Spawn teammate **${issue.id.toLowerCase()}** using the **Agent** tool with \`team_name: "${teamName}"\` and \`name: "${issue.id.toLowerCase()}"\`:
|
|
380
|
-
> ${predInstr}Work on issue ${issue.id}: ${issue.title}.
|
|
381
|
-
> Read the full spec at ${pmDir ? join(pmDir, issue.path) : issue.path}.
|
|
382
|
-
> Execute all acceptance criteria.
|
|
383
|
-
> CRITICAL: Write ALL output/results to ${outputFile} — this is the handoff artifact for downstream issues.
|
|
384
|
-
> After writing output, update the issue front matter: change \`status: in_progress\` to \`status: done\`.
|
|
385
|
-
> Do not modify STATE.md. Do not work on anything outside this issue's scope.${fileOwnership}`;
|
|
386
|
-
}).join('\n\n');
|
|
387
|
-
|
|
388
|
-
return `You are the team lead coordinating ${issues.length} issue${issues.length > 1 ? 's' : ''} using Agent Teams.
|
|
389
|
-
|
|
390
|
-
## Project Directory
|
|
391
|
-
Working directory: ${this.workingDir}
|
|
392
|
-
Plan directory: ${pmDir || '.pm/'}
|
|
393
331
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
${issues.map(i => `- \`${i.id.toLowerCase()}\``).join('\n')}
|
|
415
|
-
|
|
416
|
-
While waiting:
|
|
417
|
-
- As each teammate goes idle, verify their output file exists on disk using the **Read** tool
|
|
418
|
-
- If a teammate has not gone idle after 15 minutes, send them a message using **SendMessage** with \`recipient: "{exact name from list above}"\` to check on their progress
|
|
419
|
-
- If a teammate does not respond within 5 more minutes after your SendMessage, assume they stalled: check their output file and issue status on disk. If the output exists and status is done, mark them complete. If not, update the issue status yourself based on whatever partial work exists on disk, then proceed.
|
|
420
|
-
- Do NOT proceed to Step 3 until all ${issues.length} teammates have either gone idle or been confirmed stalled and handled
|
|
421
|
-
|
|
422
|
-
WARNING: The #1 failure mode is exiting before all teammates finish. If you exit early, all teammate processes are killed and their work is permanently lost. When in doubt, keep waiting. Err on the side of waiting too long rather than exiting too early.
|
|
423
|
-
|
|
424
|
-
### Step 3: Verify outputs
|
|
425
|
-
|
|
426
|
-
Once every teammate has gone idle or been handled:
|
|
427
|
-
1. Verify each output file exists in ${outDir}/ using **Read** or **Glob**
|
|
428
|
-
2. Verify each issue's front matter status is \`done\`
|
|
429
|
-
3. If any teammate failed to write output or update status, do it yourself
|
|
430
|
-
4. Do NOT modify STATE.md — the orchestrator handles that
|
|
431
|
-
|
|
432
|
-
### Step 4: Clean up
|
|
433
|
-
|
|
434
|
-
After all outputs are verified, tell the team to shut down:
|
|
435
|
-
- Use **SendMessage** to send each remaining active teammate a shutdown message
|
|
436
|
-
- Then exit — the orchestrator will handle the next wave
|
|
437
|
-
|
|
438
|
-
## Critical Rules
|
|
439
|
-
|
|
440
|
-
- The team is created implicitly when you spawn the first teammate with \`team_name\`, and cleaned up when all teammates exit or the lead exits.
|
|
441
|
-
- You MUST wait for idle notifications from ALL ${issues.length} teammates before exiting. Exiting early kills all teammate processes and permanently loses their work.
|
|
442
|
-
- Each teammate MUST write its output to disk — research only in conversation is LOST.
|
|
443
|
-
- Each teammate MUST update the issue front matter status to \`done\`.
|
|
444
|
-
- One issue per teammate — no cross-issue work.
|
|
445
|
-
- NEVER send a SendMessage to a teammate name that is not in the exact list above — misaddressed messages are silently dropped.`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Revert issues that stayed in_progress after a failed wave.
|
|
450
|
-
*/
|
|
451
|
-
private revertIncompleteIssues(issues: Issue[]): void {
|
|
452
|
-
const pmDir = resolvePmDir(this.workingDir);
|
|
453
|
-
if (!pmDir) return;
|
|
454
|
-
for (const issue of issues) {
|
|
455
|
-
const fullPath = join(pmDir, issue.path);
|
|
456
|
-
try {
|
|
457
|
-
const content = readFileSync(fullPath, 'utf-8');
|
|
458
|
-
if (content.match(/^status:\s*in_progress$/m)) {
|
|
459
|
-
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
460
|
-
}
|
|
461
|
-
} catch { /* file may be gone */ }
|
|
332
|
+
this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
333
|
+
this.emit('reviewProgress', { issueId: issue.id, status: 'reviewing' });
|
|
334
|
+
|
|
335
|
+
const outputPath = resolveOutputPath(issue, this.workingDir, this.boardDir);
|
|
336
|
+
const result = await reviewIssue({
|
|
337
|
+
workingDir: this.workingDir,
|
|
338
|
+
issue,
|
|
339
|
+
pmDir,
|
|
340
|
+
outputPath,
|
|
341
|
+
onOutput: (text) => this.emit('output', { issueId: issue.id, text }),
|
|
342
|
+
logDir: this.boardDir ? join(this.boardDir, 'logs') : undefined,
|
|
343
|
+
});
|
|
344
|
+
persistReviewResult(reviewDir, issue, result);
|
|
345
|
+
|
|
346
|
+
if (result.passed) {
|
|
347
|
+
this.updateIssueFrontMatter(issue.path, 'done');
|
|
348
|
+
this.metrics.issuesCompleted++;
|
|
349
|
+
this.emit('reviewProgress', { issueId: issue.id, status: 'passed' });
|
|
350
|
+
this.emit('issueCompleted', issue);
|
|
351
|
+
return 1;
|
|
462
352
|
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// ── Teammate permissions ─────────────────────────────────────
|
|
466
353
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
354
|
+
this.updateIssueFrontMatter(issue.path, 'todo');
|
|
355
|
+
appendReviewFeedback(pmDir, issue, result);
|
|
356
|
+
this.emit('reviewProgress', { issueId: issue.id, status: 'failed' });
|
|
357
|
+
this.emit('issueError', {
|
|
358
|
+
issueId: issue.id,
|
|
359
|
+
error: `Review failed: ${result.checks.filter(c => !c.passed).map(c => c.name).join(', ')}`,
|
|
360
|
+
});
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
470
363
|
|
|
471
|
-
|
|
472
|
-
* Pre-approve tools in project .claude/settings.json so Agent Teams
|
|
473
|
-
* teammates can work without interactive permission prompts.
|
|
474
|
-
* Teammates are separate processes that inherit the lead's permission
|
|
475
|
-
* settings. Without pre-approved tools, they hit interactive prompts
|
|
476
|
-
* that can't be answered in headless/background mode (known bug #25254).
|
|
477
|
-
*/
|
|
478
|
-
private installTeammatePermissions(): void {
|
|
479
|
-
const claudeDir = join(this.workingDir, '.claude');
|
|
480
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
364
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
481
365
|
|
|
482
|
-
|
|
483
|
-
|
|
366
|
+
private pickReadyIssues(): Issue[] {
|
|
367
|
+
const pmDir = resolvePmDir(this.workingDir);
|
|
368
|
+
if (!pmDir) {
|
|
369
|
+
this.emit('error', 'No PM directory found');
|
|
370
|
+
return [];
|
|
484
371
|
}
|
|
485
372
|
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
'Edit',
|
|
491
|
-
'Write',
|
|
492
|
-
'Glob',
|
|
493
|
-
'Grep',
|
|
494
|
-
'WebFetch',
|
|
495
|
-
'WebSearch',
|
|
496
|
-
'Agent',
|
|
497
|
-
];
|
|
373
|
+
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
374
|
+
const issues = effectiveBoardId
|
|
375
|
+
? this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
376
|
+
: this.loadProjectIssues();
|
|
498
377
|
|
|
499
|
-
|
|
500
|
-
// Save existing settings
|
|
501
|
-
if (existsSync(settingsPath)) {
|
|
502
|
-
this.savedClaudeSettings = readFileSync(settingsPath, 'utf-8');
|
|
503
|
-
const existing = JSON.parse(this.savedClaudeSettings);
|
|
504
|
-
|
|
505
|
-
// Merge permissions into existing settings
|
|
506
|
-
if (!existing.permissions) existing.permissions = {};
|
|
507
|
-
if (!existing.permissions.allow) existing.permissions.allow = [];
|
|
508
|
-
|
|
509
|
-
for (const tool of requiredPermissions) {
|
|
510
|
-
if (!existing.permissions.allow.includes(tool)) {
|
|
511
|
-
existing.permissions.allow.push(tool);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
378
|
+
if (!issues) return [];
|
|
514
379
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}, null, 2));
|
|
380
|
+
const readyIssues = resolveReadyToWork(issues, this.epicScope ?? undefined);
|
|
381
|
+
if (readyIssues.length === 0) {
|
|
382
|
+
this.emit('complete', this.epicScope ? 'All epic issues are done or blocked' : 'All work is done or blocked');
|
|
383
|
+
if (effectiveBoardId) {
|
|
384
|
+
this.tryCompleteBoardIfDone(pmDir, effectiveBoardId, issues);
|
|
521
385
|
}
|
|
522
|
-
this.claudeSettingsInstalled = true;
|
|
523
|
-
} catch {
|
|
524
|
-
// Non-fatal — teammates may hit permission prompts
|
|
525
386
|
}
|
|
387
|
+
return readyIssues;
|
|
526
388
|
}
|
|
527
389
|
|
|
528
|
-
/**
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
try {
|
|
536
|
-
if (this.savedClaudeSettings !== null) {
|
|
537
|
-
writeFileSync(settingsPath, this.savedClaudeSettings);
|
|
538
|
-
} else {
|
|
539
|
-
unlinkSync(settingsPath);
|
|
540
|
-
}
|
|
541
|
-
} catch {
|
|
542
|
-
// Best effort
|
|
390
|
+
/** Load issues from a specific board, auto-activating draft boards. Returns null on error. */
|
|
391
|
+
private loadBoardIssues(pmDir: string, boardId: string): Issue[] | null {
|
|
392
|
+
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
393
|
+
if (!boardState) {
|
|
394
|
+
this.emit('error', `Board not found: ${boardId}`);
|
|
395
|
+
return null;
|
|
543
396
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
397
|
+
if (boardState.state.paused) {
|
|
398
|
+
this.emit('error', 'Board is paused');
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
if (boardState.board.status === 'draft') {
|
|
402
|
+
this.activateBoard(pmDir, boardId);
|
|
403
|
+
} else if (boardState.board.status !== 'active') {
|
|
404
|
+
this.emit('error', `Board ${boardId} is not active (status: ${boardState.board.status})`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
return boardState.issues;
|
|
547
408
|
}
|
|
548
409
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Write .mcp.json in the working directory so Agent Teams teammates
|
|
557
|
-
* (separate processes) auto-discover the bouncer MCP server.
|
|
558
|
-
* This is essential — teammates don't inherit --mcp-config or
|
|
559
|
-
* --permission-prompt-tool from the team lead. .mcp.json project-level
|
|
560
|
-
* discovery + global PreToolUse hooks are the two bouncer paths for teammates.
|
|
561
|
-
*
|
|
562
|
-
* Also generates ~/.mstro/mcp-config.json for the team lead (--mcp-config).
|
|
563
|
-
*/
|
|
564
|
-
private installBouncerForSubagents(): void {
|
|
565
|
-
const mcpJsonPath = join(this.workingDir, '.mcp.json');
|
|
566
|
-
|
|
567
|
-
// Generate the standard MCP config (for parent --mcp-config) and reuse for sub-agents
|
|
568
|
-
try {
|
|
569
|
-
const generatedPath = generateMcpConfig(this.workingDir);
|
|
570
|
-
if (!generatedPath) return;
|
|
571
|
-
|
|
572
|
-
const mcpConfig = readFileSync(generatedPath, 'utf-8');
|
|
573
|
-
|
|
574
|
-
// Save any existing .mcp.json
|
|
575
|
-
if (existsSync(mcpJsonPath)) {
|
|
576
|
-
this.savedMcpJson = readFileSync(mcpJsonPath, 'utf-8');
|
|
577
|
-
|
|
578
|
-
// Merge: add bouncer to existing config
|
|
579
|
-
const existing = JSON.parse(this.savedMcpJson);
|
|
580
|
-
const generated = JSON.parse(mcpConfig);
|
|
581
|
-
existing.mcpServers = {
|
|
582
|
-
...existing.mcpServers,
|
|
583
|
-
'mstro-bouncer': generated.mcpServers['mstro-bouncer'],
|
|
584
|
-
};
|
|
585
|
-
writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
|
|
586
|
-
} else {
|
|
587
|
-
writeFileSync(mcpJsonPath, mcpConfig);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
this.mcpJsonInstalled = true;
|
|
591
|
-
} catch {
|
|
592
|
-
// Non-fatal: parent has MCP via --mcp-config, teammates fall back to PreToolUse hooks
|
|
410
|
+
/** Load project-level issues (legacy or no boards). Returns null on error. */
|
|
411
|
+
private loadProjectIssues(): Issue[] | null {
|
|
412
|
+
const fullState = parsePlanDirectory(this.workingDir);
|
|
413
|
+
if (!fullState) {
|
|
414
|
+
this.emit('error', 'No PM directory found');
|
|
415
|
+
return null;
|
|
593
416
|
}
|
|
417
|
+
if (fullState.state.paused) {
|
|
418
|
+
this.emit('error', 'Project is paused');
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
return fullState.issues;
|
|
594
422
|
}
|
|
595
423
|
|
|
596
|
-
/**
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (!this.mcpJsonInstalled) return;
|
|
601
|
-
const mcpJsonPath = join(this.workingDir, '.mcp.json');
|
|
602
|
-
|
|
424
|
+
/** Activate a draft board by updating its status in board.md. */
|
|
425
|
+
private activateBoard(pmDir: string, boardId: string): void {
|
|
426
|
+
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
427
|
+
if (!existsSync(boardMdPath)) return;
|
|
603
428
|
try {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
} else {
|
|
608
|
-
// We created it — remove it
|
|
609
|
-
unlinkSync(mcpJsonPath);
|
|
610
|
-
}
|
|
611
|
-
} catch {
|
|
612
|
-
// Best effort cleanup
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
this.savedMcpJson = null;
|
|
616
|
-
this.mcpJsonInstalled = false;
|
|
429
|
+
const content = readFileSync(boardMdPath, 'utf-8');
|
|
430
|
+
writeFileSync(boardMdPath, replaceFrontMatterField(content, 'status', 'active'), 'utf-8');
|
|
431
|
+
} catch { /* non-fatal — pickReadyIssues will re-check */ }
|
|
617
432
|
}
|
|
618
433
|
|
|
619
|
-
|
|
434
|
+
/** Check if all issues in a board are done and mark board as completed. */
|
|
435
|
+
private tryCompleteBoardIfDone(pmDir: string, boardId: string, issues: Issue[]): void {
|
|
436
|
+
const allDone = issues.length > 0 && issues.every(i => i.status === 'done' || i.status === 'cancelled');
|
|
437
|
+
if (!allDone) return;
|
|
620
438
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
439
|
+
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
440
|
+
if (!existsSync(boardMdPath)) return;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
let content = readFileSync(boardMdPath, 'utf-8');
|
|
444
|
+
content = replaceFrontMatterField(content, 'status', 'completed');
|
|
445
|
+
content = replaceFrontMatterField(content, 'completed_at', `"${new Date().toISOString()}"`);
|
|
446
|
+
writeFileSync(boardMdPath, content, 'utf-8');
|
|
447
|
+
} catch { /* non-fatal */ }
|
|
630
448
|
}
|
|
631
449
|
|
|
632
|
-
|
|
633
|
-
* List existing execution output docs in .pm/out/.
|
|
634
|
-
* Single canonical location — no split-brain lookup.
|
|
635
|
-
*/
|
|
636
|
-
private listExistingDocs(): string[] {
|
|
450
|
+
private resolveActiveBoardId(): string | null {
|
|
637
451
|
const pmDir = resolvePmDir(this.workingDir);
|
|
638
|
-
if (!pmDir) return
|
|
639
|
-
const outDir = join(pmDir, 'out');
|
|
640
|
-
if (!existsSync(outDir)) return [];
|
|
641
|
-
|
|
452
|
+
if (!pmDir) return null;
|
|
642
453
|
try {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
454
|
+
const workspacePath = join(pmDir, 'workspace.json');
|
|
455
|
+
if (!existsSync(workspacePath)) return null;
|
|
456
|
+
const workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
|
|
457
|
+
return workspace.activeBoardId ?? null;
|
|
646
458
|
} catch {
|
|
647
|
-
return
|
|
459
|
+
return null;
|
|
648
460
|
}
|
|
649
461
|
}
|
|
650
462
|
|
|
651
|
-
|
|
652
|
-
* Copy confirmed-done outputs from .pm/out/ to user-specified output_file paths.
|
|
653
|
-
* Only copies for issues that completed successfully and have output_file set.
|
|
654
|
-
* Failures are non-fatal — the canonical artifact in .pm/out/ is always safe.
|
|
655
|
-
*/
|
|
656
|
-
private publishOutputs(issues: Issue[]): void {
|
|
463
|
+
private revertIncompleteIssues(issues: Issue[]): void {
|
|
657
464
|
const pmDir = resolvePmDir(this.workingDir);
|
|
658
465
|
if (!pmDir) return;
|
|
659
|
-
|
|
660
466
|
for (const issue of issues) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// Only publish for confirmed-done issues
|
|
670
|
-
try {
|
|
671
|
-
const content = readFileSync(join(pmDir, issue.path), 'utf-8');
|
|
672
|
-
if (!content.match(/^status:\s*done$/m)) return;
|
|
673
|
-
} catch { return; }
|
|
674
|
-
|
|
675
|
-
const srcPath = this.resolveOutputPath(issue);
|
|
676
|
-
if (!existsSync(srcPath)) return;
|
|
677
|
-
|
|
678
|
-
// Guard against path traversal — output_file must resolve within workingDir
|
|
679
|
-
const destPath = resolve(this.workingDir, issue.outputFile);
|
|
680
|
-
if (!destPath.startsWith(`${this.workingDir}/`) && destPath !== this.workingDir) {
|
|
681
|
-
this.emit('output', {
|
|
682
|
-
issueId: issue.id,
|
|
683
|
-
text: `Warning: output_file "${issue.outputFile}" escapes project directory — skipping`,
|
|
684
|
-
});
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
const destDir = join(destPath, '..');
|
|
690
|
-
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
691
|
-
copyFileSync(srcPath, destPath);
|
|
692
|
-
} catch {
|
|
693
|
-
this.emit('output', {
|
|
694
|
-
issueId: issue.id,
|
|
695
|
-
text: `Warning: could not copy output to ${issue.outputFile}`,
|
|
696
|
-
});
|
|
467
|
+
const fullPath = join(pmDir, issue.path);
|
|
468
|
+
try {
|
|
469
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
470
|
+
if (content.match(/^status:\s*in_progress$/m)) {
|
|
471
|
+
this.updateIssueFrontMatter(issue.path, issue.status);
|
|
472
|
+
}
|
|
473
|
+
} catch { /* file may be gone */ }
|
|
697
474
|
}
|
|
698
475
|
}
|
|
699
476
|
|
|
700
|
-
private slugify(text: string): string {
|
|
701
|
-
return text
|
|
702
|
-
.toLowerCase()
|
|
703
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
704
|
-
.replace(/^-+|-+$/g, '')
|
|
705
|
-
.slice(0, 60);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
477
|
private updateIssueFrontMatter(issuePath: string, newStatus: string): void {
|
|
709
478
|
const pmDir = resolvePmDir(this.workingDir);
|
|
710
479
|
if (!pmDir) return;
|
|
711
|
-
const fullPath = join(pmDir, issuePath);
|
|
712
480
|
try {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
481
|
+
setFrontMatterField(join(pmDir, issuePath), 'status', newStatus);
|
|
482
|
+
} catch { /* file may have been moved */ }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private ensureOutputDirs(): void {
|
|
486
|
+
if (this.boardDir) {
|
|
487
|
+
const boardOutDir = join(this.boardDir, 'out');
|
|
488
|
+
if (!existsSync(boardOutDir)) mkdirSync(boardOutDir, { recursive: true });
|
|
489
|
+
} else {
|
|
490
|
+
const pmDir = resolvePmDir(this.workingDir);
|
|
491
|
+
if (pmDir) {
|
|
492
|
+
const outDir = join(pmDir, 'out');
|
|
493
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
494
|
+
}
|
|
718
495
|
}
|
|
719
496
|
}
|
|
720
497
|
|
|
721
|
-
/**
|
|
722
|
-
* Append a progress log entry after a wave completes.
|
|
723
|
-
* Re-reads issue files from disk to determine which actually completed.
|
|
724
|
-
*/
|
|
725
498
|
private appendProgressEntry(issues: Issue[], waveStart: number): void {
|
|
726
499
|
const pmDir = resolvePmDir(this.workingDir);
|
|
727
500
|
if (!pmDir) return;
|
|
728
|
-
|
|
729
|
-
|
|
501
|
+
|
|
502
|
+
// Board-scoped progress log
|
|
503
|
+
const progressPath = this.boardDir
|
|
504
|
+
? join(this.boardDir, 'progress.md')
|
|
505
|
+
: join(pmDir, 'progress.md');
|
|
730
506
|
|
|
731
507
|
const durationMin = Math.round((Date.now() - waveStart) / 60_000);
|
|
732
508
|
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
733
509
|
|
|
734
|
-
// Re-read issue statuses from disk to get accurate completion count
|
|
735
510
|
const completed: string[] = [];
|
|
736
511
|
const failed: string[] = [];
|
|
737
512
|
for (const issue of issues) {
|
|
@@ -760,11 +535,29 @@ After all outputs are verified, tell the team to shut down:
|
|
|
760
535
|
}
|
|
761
536
|
lines.push('');
|
|
762
537
|
|
|
538
|
+
this.writeProgressLines(progressPath, lines);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private writeProgressLines(filePath: string, lines: string[]): void {
|
|
763
542
|
try {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
543
|
+
if (existsSync(filePath)) {
|
|
544
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
545
|
+
writeFileSync(filePath, `${existing.trimEnd()}\n${lines.join('\n')}`, 'utf-8');
|
|
546
|
+
} else {
|
|
547
|
+
writeFileSync(filePath, `# Board Progress\n${lines.join('\n')}`, 'utf-8');
|
|
548
|
+
}
|
|
549
|
+
} catch { /* non-fatal */ }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Resolve the active board's directory path for outputs, reviews, and progress. */
|
|
553
|
+
private resolveBoardDir(): string | null {
|
|
554
|
+
const pmDir = resolvePmDir(this.workingDir);
|
|
555
|
+
if (!pmDir) return null;
|
|
556
|
+
|
|
557
|
+
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
558
|
+
if (!effectiveBoardId) return null;
|
|
559
|
+
|
|
560
|
+
const boardDir = join(pmDir, 'boards', effectiveBoardId);
|
|
561
|
+
return existsSync(boardDir) ? boardDir : null;
|
|
769
562
|
}
|
|
770
563
|
}
|