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