mstro-app 0.4.52 → 0.5.1
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/LICENSE +129 -190
- package/PRIVACY.md +3 -3
- package/README.md +15 -6
- package/bin/commands/config.js +0 -1
- package/bin/mstro.js +1 -2
- package/bin/postinstall.js +0 -1
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +0 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.js +7 -3
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +0 -1
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-tools.js +0 -1
- package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +1 -2
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.js +0 -1
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +0 -1
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +0 -1
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.js +0 -1
- package/dist/server/cli/headless/native-timeout-detector.js.map +1 -1
- package/dist/server/cli/headless/output-utils.d.ts.map +1 -1
- package/dist/server/cli/headless/output-utils.js +0 -1
- package/dist/server/cli/headless/output-utils.js.map +1 -1
- package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
- package/dist/server/cli/headless/prompt-utils.js +0 -1
- package/dist/server/cli/headless/prompt-utils.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -1
- package/dist/server/cli/headless/resilient-runner.js +0 -1
- package/dist/server/cli/headless/resilient-runner.js.map +1 -1
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -1
- package/dist/server/cli/headless/retry-strategies.js +0 -1
- package/dist/server/cli/headless/retry-strategies.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +63 -68
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +9 -5
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +0 -1
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/headless/types.js +0 -1
- package/dist/server/cli/headless/types.js.map +1 -1
- package/dist/server/cli/improvisation-attachments.d.ts.map +1 -1
- package/dist/server/cli/improvisation-attachments.js +0 -1
- package/dist/server/cli/improvisation-attachments.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts +16 -0
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
- package/dist/server/cli/improvisation-history-store.js +51 -0
- package/dist/server/cli/improvisation-history-store.js.map +1 -0
- package/dist/server/cli/improvisation-movements.d.ts +31 -0
- package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
- package/dist/server/cli/improvisation-movements.js +92 -0
- package/dist/server/cli/improvisation-movements.js.map +1 -0
- package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
- package/dist/server/cli/improvisation-output-queue.js +39 -0
- package/dist/server/cli/improvisation-output-queue.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +21 -51
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +18 -434
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +53 -149
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js +0 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
- package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-best-result.js +60 -0
- package/dist/server/cli/retry/retry-best-result.js.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.js +67 -0
- package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.js +80 -0
- package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js +165 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.js +21 -0
- package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js +59 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.js +23 -0
- package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
- package/dist/server/cli/retry/retry-types.d.ts +30 -0
- package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-types.js +3 -0
- package/dist/server/cli/retry/retry-types.js.map +1 -0
- package/dist/server/index.js +21 -110
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +0 -1
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +0 -1
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +0 -1
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts.map +1 -1
- package/dist/server/mcp/security-analysis.js +0 -1
- package/dist/server/mcp/security-analysis.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js +0 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +0 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/mcp/server.js +0 -1
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/files.d.ts.map +1 -1
- package/dist/server/routes/files.js +0 -1
- package/dist/server/routes/files.js.map +1 -1
- package/dist/server/routes/improvise.d.ts.map +1 -1
- package/dist/server/routes/improvise.js +0 -1
- package/dist/server/routes/improvise.js.map +1 -1
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +0 -1
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/instances.d.ts.map +1 -1
- package/dist/server/routes/instances.js +0 -1
- package/dist/server/routes/instances.js.map +1 -1
- package/dist/server/routes/notifications.d.ts.map +1 -1
- package/dist/server/routes/notifications.js +0 -1
- package/dist/server/routes/notifications.js.map +1 -1
- package/dist/server/server-setup.d.ts +16 -1
- package/dist/server/server-setup.d.ts.map +1 -1
- package/dist/server/server-setup.js +107 -1
- package/dist/server/server-setup.js.map +1 -1
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +0 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/auth.d.ts.map +1 -1
- package/dist/server/services/auth.js +0 -1
- package/dist/server/services/auth.js.map +1 -1
- package/dist/server/services/client-id.d.ts.map +1 -1
- package/dist/server/services/client-id.js +0 -1
- package/dist/server/services/client-id.js.map +1 -1
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
- package/dist/server/services/file-explorer-ops.js +0 -1
- package/dist/server/services/file-explorer-ops.js.map +1 -1
- package/dist/server/services/files.d.ts.map +1 -1
- package/dist/server/services/files.js +0 -1
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/instances.d.ts.map +1 -1
- package/dist/server/services/instances.js +0 -1
- package/dist/server/services/instances.js.map +1 -1
- package/dist/server/services/pathUtils.d.ts.map +1 -1
- package/dist/server/services/pathUtils.js +0 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
- package/dist/server/services/plan/agent-loader.js +0 -1
- package/dist/server/services/plan/agent-loader.js.map +1 -1
- package/dist/server/services/plan/board-config.d.ts +21 -0
- package/dist/server/services/plan/board-config.d.ts.map +1 -0
- package/dist/server/services/plan/board-config.js +111 -0
- package/dist/server/services/plan/board-config.js.map +1 -0
- 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 +7 -6
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/config-installer.d.ts.map +1 -1
- package/dist/server/services/plan/config-installer.js +0 -1
- package/dist/server/services/plan/config-installer.js.map +1 -1
- package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
- package/dist/server/services/plan/dependency-resolver.js +0 -1
- package/dist/server/services/plan/dependency-resolver.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +48 -48
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +202 -458
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +0 -1
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -1
- package/dist/server/services/plan/issue-classification.js +0 -1
- package/dist/server/services/plan/issue-classification.js.map +1 -1
- package/dist/server/services/plan/issue-loader.d.ts +16 -0
- package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
- package/dist/server/services/plan/issue-loader.js +45 -0
- package/dist/server/services/plan/issue-loader.js.map +1 -0
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +0 -1
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +3 -1
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +2 -1
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/issue-writer.d.ts +34 -0
- package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
- package/dist/server/services/plan/issue-writer.js +109 -0
- package/dist/server/services/plan/issue-writer.js.map +1 -0
- package/dist/server/services/plan/output-manager.js +2 -2
- package/dist/server/services/plan/output-manager.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +0 -1
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
- package/dist/server/services/plan/parser-migration.js +0 -1
- package/dist/server/services/plan/parser-migration.js.map +1 -1
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +0 -1
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/progress-log.d.ts +11 -0
- package/dist/server/services/plan/progress-log.d.ts.map +1 -0
- package/dist/server/services/plan/progress-log.js +80 -0
- package/dist/server/services/plan/progress-log.js.map +1 -0
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/prompt-builder.js +48 -32
- package/dist/server/services/plan/prompt-builder.js.map +1 -1
- package/dist/server/services/plan/readiness-planner.d.ts +15 -0
- package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
- package/dist/server/services/plan/readiness-planner.js +40 -0
- package/dist/server/services/plan/readiness-planner.js.map +1 -0
- package/dist/server/services/plan/review-gate.d.ts +31 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +52 -3
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +0 -1
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/plan/types.js +0 -1
- package/dist/server/services/plan/types.js.map +1 -1
- package/dist/server/services/plan/watcher.d.ts.map +1 -1
- package/dist/server/services/plan/watcher.js +0 -1
- package/dist/server/services/plan/watcher.js.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +0 -1
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/platform-token-lifecycle.d.ts +70 -0
- package/dist/server/services/platform-token-lifecycle.d.ts.map +1 -0
- package/dist/server/services/platform-token-lifecycle.js +156 -0
- package/dist/server/services/platform-token-lifecycle.js.map +1 -0
- package/dist/server/services/platform.d.ts +25 -4
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +150 -92
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js +0 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +0 -1
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +0 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-utils.js +0 -1
- package/dist/server/services/terminal/pty-utils.js.map +1 -1
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
- package/dist/server/services/websocket/autocomplete.js +0 -1
- package/dist/server/services/websocket/autocomplete.js.map +1 -1
- package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-definition-handlers.js +0 -1
- package/dist/server/services/websocket/file-definition-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
- package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
- package/dist/server/services/websocket/file-download-handler.js +164 -0
- package/dist/server/services/websocket/file-download-handler.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 +0 -1
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.js +0 -1
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.d.ts +2 -3
- package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.js +4 -7
- package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +0 -1
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +0 -1
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-diff-handlers.js +0 -1
- package/dist/server/services/websocket/git-diff-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +58 -6
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -1
- package/dist/server/services/websocket/git-head-watcher.js +0 -1
- package/dist/server/services/websocket/git-head-watcher.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.js +0 -1
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +0 -1
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.js +0 -1
- package/dist/server/services/websocket/git-tag-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-utils.d.ts +18 -3
- package/dist/server/services/websocket/git-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/git-utils.js +58 -8
- package/dist/server/services/websocket/git-utils.js.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +258 -16
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +15 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler-context.js +0 -1
- package/dist/server/services/websocket/handler-context.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +7 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +76 -15
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js +0 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
- package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
- package/dist/server/services/websocket/msg-id-tracker.js +76 -0
- package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +0 -1
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -2
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +0 -1
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-helpers.js +0 -1
- package/dist/server/services/websocket/plan-helpers.js.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.js +0 -1
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js +0 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +0 -1
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-grading.d.ts +46 -0
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-grading.js +482 -0
- package/dist/server/services/websocket/quality-grading.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +15 -4
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-linting.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-linting.js +0 -1
- package/dist/server/services/websocket/quality-linting.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +14 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +28 -12
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +2 -3
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +3 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +53 -58
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +6 -3
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +18 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +0 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +48 -2
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +204 -66
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.d.ts.map +1 -1
- package/dist/server/services/websocket/session-history.js +0 -1
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts +2 -2
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +75 -18
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +29 -1
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +53 -5
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +0 -1
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +0 -1
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-watcher.js +0 -1
- package/dist/server/services/websocket/skill-watcher.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-broadcast.js +12 -0
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.js +106 -0
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.js +20 -0
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +2 -10
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.js +39 -4
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +17 -8
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +8 -7
- package/dist/server/services/websocket/types.js.map +1 -1
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +0 -1
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/paths.d.ts.map +1 -1
- package/dist/server/utils/paths.js +0 -1
- package/dist/server/utils/paths.js.map +1 -1
- package/dist/server/utils/port-manager.d.ts.map +1 -1
- package/dist/server/utils/port-manager.js +0 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/dist/server/utils/port.d.ts.map +1 -1
- package/dist/server/utils/port.js +0 -1
- package/dist/server/utils/port.js.map +1 -1
- package/package.json +2 -2
- package/server/README.md +1 -1
- package/server/cli/headless/claude-invoker-process.ts +0 -1
- package/server/cli/headless/claude-invoker-stall.ts +7 -3
- package/server/cli/headless/claude-invoker-stream.ts +0 -1
- package/server/cli/headless/claude-invoker-tools.ts +0 -1
- package/server/cli/headless/claude-invoker.ts +1 -2
- package/server/cli/headless/haiku-assessments.ts +0 -1
- package/server/cli/headless/headless-logger.ts +0 -1
- package/server/cli/headless/index.ts +0 -1
- package/server/cli/headless/native-timeout-detector.ts +0 -1
- package/server/cli/headless/output-utils.ts +0 -1
- package/server/cli/headless/prompt-utils.ts +0 -1
- package/server/cli/headless/resilient-runner.ts +0 -1
- package/server/cli/headless/retry-strategies.ts +0 -1
- package/server/cli/headless/runner.ts +67 -73
- package/server/cli/headless/stall-assessor.ts +9 -5
- package/server/cli/headless/tool-watchdog.ts +0 -1
- package/server/cli/headless/types.ts +1 -2
- package/server/cli/improvisation-attachments.ts +0 -1
- package/server/cli/improvisation-history-store.ts +61 -0
- package/server/cli/improvisation-movements.ts +119 -0
- package/server/cli/improvisation-output-queue.ts +41 -0
- package/server/cli/improvisation-retry.ts +25 -601
- package/server/cli/improvisation-session-manager.ts +74 -161
- package/server/cli/improvisation-types.ts +0 -1
- package/server/cli/retry/retry-best-result.ts +69 -0
- package/server/cli/retry/retry-context-loss.ts +86 -0
- package/server/cli/retry/retry-premature-completion.ts +112 -0
- package/server/cli/retry/retry-recovery-strategies.ts +246 -0
- package/server/cli/retry/retry-resume-strategy.ts +32 -0
- package/server/cli/retry/retry-runner-factory.ts +69 -0
- package/server/cli/retry/retry-tool-results.ts +30 -0
- package/server/cli/retry/retry-types.ts +31 -0
- package/server/index.ts +37 -124
- package/server/mcp/bouncer-cli.ts +0 -1
- package/server/mcp/bouncer-haiku.ts +0 -1
- package/server/mcp/bouncer-integration.ts +0 -1
- package/server/mcp/security-analysis.ts +0 -1
- package/server/mcp/security-audit.ts +0 -1
- package/server/mcp/security-patterns.ts +0 -1
- package/server/mcp/server.ts +0 -1
- package/server/routes/files.ts +0 -1
- package/server/routes/improvise.ts +0 -1
- package/server/routes/index.ts +0 -1
- package/server/routes/instances.ts +0 -1
- package/server/routes/notifications.ts +0 -1
- package/server/server-setup.ts +126 -2
- package/server/services/analytics.ts +0 -1
- package/server/services/auth.ts +0 -1
- package/server/services/client-id.ts +0 -1
- package/server/services/file-explorer-ops.ts +0 -1
- package/server/services/files.ts +0 -1
- package/server/services/instances.ts +0 -1
- package/server/services/pathUtils.ts +0 -1
- package/server/services/plan/agent-loader.ts +0 -1
- package/server/services/plan/agents/assess-stall.md +11 -4
- package/server/services/plan/agents/code-review.md +13 -11
- package/server/services/plan/board-config.ts +121 -0
- package/server/services/plan/composer.ts +7 -6
- package/server/services/plan/config-installer.ts +0 -1
- package/server/services/plan/dependency-resolver.ts +0 -1
- package/server/services/plan/executor.ts +259 -470
- package/server/services/plan/front-matter.ts +0 -1
- package/server/services/plan/issue-classification.ts +0 -1
- package/server/services/plan/issue-loader.ts +63 -0
- package/server/services/plan/issue-prompt-builder.ts +0 -1
- package/server/services/plan/issue-retry.ts +5 -2
- package/server/services/plan/issue-writer.ts +136 -0
- package/server/services/plan/output-manager.ts +2 -2
- package/server/services/plan/parser-core.ts +0 -1
- package/server/services/plan/parser-migration.ts +0 -1
- package/server/services/plan/parser.ts +0 -1
- package/server/services/plan/progress-log.ts +91 -0
- package/server/services/plan/prompt-builder.ts +73 -36
- package/server/services/plan/readiness-planner.ts +49 -0
- package/server/services/plan/review-gate.ts +102 -3
- package/server/services/plan/state-reconciler.ts +0 -1
- package/server/services/plan/types.ts +0 -1
- package/server/services/plan/watcher.ts +0 -1
- package/server/services/platform-credentials.ts +0 -1
- package/server/services/platform-token-lifecycle.ts +171 -0
- package/server/services/platform.ts +168 -105
- package/server/services/sentry.ts +0 -1
- package/server/services/settings.ts +0 -1
- package/server/services/terminal/pty-manager.ts +0 -1
- package/server/services/terminal/pty-utils.ts +0 -1
- package/server/services/websocket/autocomplete.ts +0 -1
- package/server/services/websocket/file-definition-handlers.ts +0 -1
- package/server/services/websocket/file-download-handler.ts +190 -0
- package/server/services/websocket/file-explorer-handlers.ts +0 -1
- package/server/services/websocket/file-search-handlers.ts +0 -1
- package/server/services/websocket/file-upload-handler.ts +6 -5
- package/server/services/websocket/file-utils.ts +0 -1
- package/server/services/websocket/git-branch-handlers.ts +0 -1
- package/server/services/websocket/git-diff-handlers.ts +0 -1
- package/server/services/websocket/git-handlers.ts +66 -10
- package/server/services/websocket/git-head-watcher.ts +0 -1
- package/server/services/websocket/git-log-handlers.ts +0 -1
- package/server/services/websocket/git-pr-handlers.ts +0 -1
- package/server/services/websocket/git-tag-handlers.ts +0 -1
- package/server/services/websocket/git-utils.ts +69 -9
- package/server/services/websocket/git-worktree-handlers.ts +289 -19
- package/server/services/websocket/handler-context.ts +15 -1
- package/server/services/websocket/handler.ts +79 -16
- package/server/services/websocket/index.ts +0 -1
- package/server/services/websocket/msg-id-tracker.ts +83 -0
- package/server/services/websocket/plan-board-handlers.ts +0 -1
- package/server/services/websocket/plan-execution-handlers.ts +6 -2
- package/server/services/websocket/plan-handlers.ts +0 -1
- package/server/services/websocket/plan-helpers.ts +0 -1
- package/server/services/websocket/plan-issue-handlers.ts +0 -1
- package/server/services/websocket/plan-sprint-handlers.ts +0 -1
- package/server/services/websocket/quality-complexity.ts +0 -1
- package/server/services/websocket/quality-grading.ts +611 -0
- package/server/services/websocket/quality-handlers.ts +16 -4
- package/server/services/websocket/quality-linting.ts +0 -1
- package/server/services/websocket/quality-persistence.ts +30 -8
- package/server/services/websocket/quality-review-agent.ts +2 -3
- package/server/services/websocket/quality-service.ts +54 -55
- package/server/services/websocket/quality-tools.ts +11 -3
- package/server/services/websocket/quality-types.ts +21 -3
- package/server/services/websocket/session-handlers.ts +213 -69
- package/server/services/websocket/session-history.ts +0 -1
- package/server/services/websocket/session-initialization.ts +83 -20
- package/server/services/websocket/session-registry.ts +61 -5
- package/server/services/websocket/settings-handlers.ts +0 -1
- package/server/services/websocket/skill-handlers.ts +0 -1
- package/server/services/websocket/skill-watcher.ts +0 -1
- package/server/services/websocket/tab-broadcast.ts +37 -0
- package/server/services/websocket/tab-event-buffer.ts +158 -0
- package/server/services/websocket/tab-event-replay.ts +41 -0
- package/server/services/websocket/tab-handlers.ts +2 -10
- package/server/services/websocket/terminal-handlers.ts +39 -3
- package/server/services/websocket/types.ts +19 -7
- package/server/utils/agent-manager.ts +0 -1
- package/server/utils/paths.ts +0 -1
- package/server/utils/port-manager.ts +0 -1
- package/server/utils/port.ts +0 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
-
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Plan Executor — Wave-based execution with parallel headless Claude Code instances.
|
|
@@ -9,41 +8,90 @@
|
|
|
9
8
|
* reconciles state, and repeats.
|
|
10
9
|
*
|
|
11
10
|
* Implementation is split across focused modules:
|
|
11
|
+
* - board-config.ts — board.md metadata reads, workspace.json active board resolution
|
|
12
12
|
* - config-installer.ts — tool permissions install/uninstall
|
|
13
13
|
* - issue-prompt-builder.ts — per-issue prompt construction
|
|
14
|
+
* - issue-writer.ts — issue front-matter updates, recovery, revert, cancellation notes
|
|
14
15
|
* - output-manager.ts — output path resolution, listing, publishing
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
16
|
+
* - progress-log.ts — progress.md writer + output dir creation
|
|
17
|
+
* - review-gate.ts — AI-powered quality gate (review, parse, persist, full pipeline)
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { EventEmitter } from 'node:events';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
21
|
+
import { readFile } from 'node:fs/promises';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
23
|
import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_MAX_PARALLEL_AGENTS,
|
|
26
|
+
getBoardMaxParallelAgents,
|
|
27
|
+
resolveActiveBoardId,
|
|
28
|
+
resolveBoardDir,
|
|
29
|
+
tryCompleteBoardIfDone,
|
|
30
|
+
} from './board-config.js';
|
|
24
31
|
import { ConfigInstaller } from './config-installer.js';
|
|
25
32
|
import { resolveReadyToWork } from './dependency-resolver.js';
|
|
26
|
-
import {
|
|
33
|
+
import { loadBoardIssues, loadProjectIssues } from './issue-loader.js';
|
|
27
34
|
import { buildIssuePrompt } from './issue-prompt-builder.js';
|
|
28
35
|
import { runIssueWithRetry } from './issue-retry.js';
|
|
36
|
+
import {
|
|
37
|
+
extractIssueStatus,
|
|
38
|
+
recoverStaleIssues,
|
|
39
|
+
revertIncompleteIssues,
|
|
40
|
+
updateIssueFrontMatter,
|
|
41
|
+
validateIssuePath,
|
|
42
|
+
type WarnFn,
|
|
43
|
+
} from './issue-writer.js';
|
|
29
44
|
import { listExistingDocs, publishOutputs, resolveOutputPath } from './output-manager.js';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
45
|
+
import { resolvePmDir } from './parser.js';
|
|
46
|
+
import { appendProgressEntry, ensureOutputDirs } from './progress-log.js';
|
|
47
|
+
import { buildCompletionReason, detectDeadState, hasBlockedIssues } from './readiness-planner.js';
|
|
48
|
+
import { runReviewPipeline } from './review-gate.js';
|
|
32
49
|
import { reconcileState } from './state-reconciler.js';
|
|
33
50
|
import type { Issue } from './types.js';
|
|
34
51
|
|
|
35
52
|
export type ExecutionStatus = 'idle' | 'starting' | 'executing' | 'paused' | 'stopping' | 'complete' | 'error';
|
|
36
53
|
|
|
37
|
-
/** Default max parallel agents when board doesn't specify. */
|
|
38
|
-
const DEFAULT_MAX_PARALLEL_AGENTS = 3;
|
|
39
|
-
|
|
40
54
|
/** Stop after this many consecutive waves with zero completions. */
|
|
41
55
|
const MAX_CONSECUTIVE_EMPTY_WAVES = 3;
|
|
42
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Render a tool-call event as a short single-line label for the verbose
|
|
59
|
+
* Output stream — `Read · package.json`, `Bash · npm test`, etc. The web UI
|
|
60
|
+
* splits the label on the first ` · ` to render the tool name as a badge.
|
|
61
|
+
*/
|
|
62
|
+
function formatToolCallLine(toolName: string, input: Record<string, unknown> | undefined): string {
|
|
63
|
+
const arg = pickPrimaryArg(toolName, input);
|
|
64
|
+
return arg ? `${toolName} · ${arg}` : toolName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pickPrimaryArg(toolName: string, input: Record<string, unknown> | undefined): string {
|
|
68
|
+
if (!input) return '';
|
|
69
|
+
// Map common Claude Code tools to their most informative single argument.
|
|
70
|
+
const candidates: Record<string, string[]> = {
|
|
71
|
+
Read: ['file_path'],
|
|
72
|
+
Write: ['file_path'],
|
|
73
|
+
Edit: ['file_path'],
|
|
74
|
+
Glob: ['pattern'],
|
|
75
|
+
Grep: ['pattern'],
|
|
76
|
+
Bash: ['command'],
|
|
77
|
+
WebFetch: ['url'],
|
|
78
|
+
WebSearch: ['query'],
|
|
79
|
+
Task: ['description'],
|
|
80
|
+
};
|
|
81
|
+
const keys = candidates[toolName] ?? ['command', 'file_path', 'path', 'pattern', 'url', 'query'];
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
const value = input[key];
|
|
84
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
85
|
+
return value.length > 120 ? `${value.slice(0, 117)}...` : value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
|
|
43
91
|
/** Per-issue stall timeouts (ms) — shorter than Agent Teams wave timeouts */
|
|
44
92
|
const ISSUE_STALL_WARNING_MS = 900_000; // 15 min
|
|
45
93
|
const ISSUE_STALL_KILL_MS = 1_800_000; // 30 min
|
|
46
|
-
const ISSUE_STALL_HARD_CAP_MS =
|
|
94
|
+
const ISSUE_STALL_HARD_CAP_MS = 14_400_000; // 4 hr backstop — only fires after stall signals flag the run
|
|
47
95
|
const ISSUE_STALL_MAX_EXTENSIONS = 10;
|
|
48
96
|
|
|
49
97
|
export interface ExecutionMetrics {
|
|
@@ -55,27 +103,44 @@ export interface ExecutionMetrics {
|
|
|
55
103
|
currentWaveIds: string[];
|
|
56
104
|
}
|
|
57
105
|
|
|
106
|
+
/** Scope options for a single run. Passed to start(), startBoard(), startEpic(). */
|
|
107
|
+
export interface StartOptions {
|
|
108
|
+
/** Epic file path — restricts execution to issues under that epic. */
|
|
109
|
+
epic?: string;
|
|
110
|
+
/** Board ID to execute (e.g. "BOARD-001"). */
|
|
111
|
+
board?: string;
|
|
112
|
+
/** Optional worktree directory for running agents. PM data is always read from workingDir. */
|
|
113
|
+
executionDir?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Immutable per-run context resolved when a run starts. Groups paths, scope,
|
|
118
|
+
* and environment so helper methods can read them in one place rather than
|
|
119
|
+
* reaching for scattered fields on the executor.
|
|
120
|
+
*/
|
|
121
|
+
interface PlanExecutionContext {
|
|
122
|
+
readonly workingDir: string;
|
|
123
|
+
readonly extraEnv?: Record<string, string>;
|
|
124
|
+
readonly epicScope: string | null;
|
|
125
|
+
readonly boardId: string | null;
|
|
126
|
+
readonly executionDir: string | null;
|
|
127
|
+
readonly pmDir: string | null;
|
|
128
|
+
readonly boardDir: string | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
58
131
|
export class PlanExecutor extends EventEmitter {
|
|
59
132
|
private status: ExecutionStatus = 'idle';
|
|
60
|
-
private workingDir: string;
|
|
133
|
+
private readonly workingDir: string;
|
|
134
|
+
private readonly extraEnv?: Record<string, string>;
|
|
61
135
|
private shouldStop = false;
|
|
62
136
|
private shouldPause = false;
|
|
63
137
|
/** AbortController for killing running HeadlessRunner processes on stop. */
|
|
64
138
|
private waveAbortController: AbortController | null = null;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
private boardDir: string | null = null;
|
|
70
|
-
/** Board ID being executed (e.g. "BOARD-001") */
|
|
71
|
-
private boardId: string | null = null;
|
|
139
|
+
/** Resolved context for the current/last run — rebuilt each runStart(). */
|
|
140
|
+
private context: PlanExecutionContext;
|
|
141
|
+
/** Options from the last run; replayed on resume() to preserve scope. */
|
|
142
|
+
private lastStartOptions: StartOptions = {};
|
|
72
143
|
private configInstaller: ConfigInstaller;
|
|
73
|
-
/** Flag to prevent start() from clearing scope set by startBoard/startEpic */
|
|
74
|
-
private _scopeSetByCall = false;
|
|
75
|
-
/** Extra environment variables forwarded to HeadlessRunner child processes (e.g. API keys) */
|
|
76
|
-
private extraEnv?: Record<string, string>;
|
|
77
|
-
/** Optional worktree directory for running AI agents. PM data is always read from workingDir. */
|
|
78
|
-
private executionDir: string | null = null;
|
|
79
144
|
private metrics: ExecutionMetrics = {
|
|
80
145
|
issuesCompleted: 0,
|
|
81
146
|
issuesAttempted: 0,
|
|
@@ -89,59 +154,60 @@ export class PlanExecutor extends EventEmitter {
|
|
|
89
154
|
this.workingDir = workingDir;
|
|
90
155
|
this.extraEnv = options?.extraEnv;
|
|
91
156
|
this.configInstaller = new ConfigInstaller(workingDir);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
private validateIssuePath(issuePath: string, baseDir: string): string {
|
|
95
|
-
const resolvedBase = resolve(baseDir);
|
|
96
|
-
const resolvedFull = resolve(resolvedBase, issuePath);
|
|
97
|
-
const rel = relative(resolvedBase, resolvedFull);
|
|
98
|
-
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
99
|
-
throw new Error(`Invalid issue path: path traversal detected in "${issuePath}"`);
|
|
100
|
-
}
|
|
101
|
-
return resolvedFull;
|
|
157
|
+
this.context = this.buildContext({});
|
|
102
158
|
}
|
|
103
159
|
|
|
104
160
|
getStatus(): ExecutionStatus { return this.status; }
|
|
105
161
|
getMetrics(): ExecutionMetrics { return { ...this.metrics }; }
|
|
106
162
|
|
|
107
|
-
|
|
108
|
-
this.
|
|
109
|
-
this._scopeSetByCall = true;
|
|
110
|
-
return this.start();
|
|
163
|
+
startEpic(epicPath: string): Promise<void> {
|
|
164
|
+
return this.runStart({ epic: epicPath });
|
|
111
165
|
}
|
|
112
166
|
|
|
113
167
|
/** Start execution, optionally scoped to a specific board. */
|
|
114
|
-
|
|
115
|
-
this.boardId
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
168
|
+
startBoard(boardId: string, executionDir?: string): Promise<void> {
|
|
169
|
+
return this.runStart({ board: boardId, executionDir });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
start(options: StartOptions = {}): Promise<void> {
|
|
173
|
+
return this.runStart(options);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pause(): void { this.shouldPause = true; }
|
|
177
|
+
|
|
178
|
+
stop(): void {
|
|
179
|
+
this.shouldStop = true;
|
|
180
|
+
this.status = 'stopping';
|
|
181
|
+
this.emit('statusChanged', this.status);
|
|
182
|
+
// Kill all running HeadlessRunner processes in the current wave
|
|
183
|
+
this.waveAbortController?.abort();
|
|
119
184
|
}
|
|
120
185
|
|
|
121
|
-
|
|
186
|
+
resume(): Promise<void> {
|
|
187
|
+
if (this.status !== 'paused') return Promise.resolve();
|
|
188
|
+
this.shouldPause = false;
|
|
189
|
+
// Replay the options from the previous run to preserve epic/board scope.
|
|
190
|
+
return this.runStart(this.lastStartOptions);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Run orchestration ────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
private async runStart(options: StartOptions): Promise<void> {
|
|
122
196
|
if (this.status === 'executing' || this.status === 'starting') return;
|
|
123
197
|
|
|
198
|
+
this.lastStartOptions = options;
|
|
124
199
|
this.shouldStop = false;
|
|
125
200
|
this.shouldPause = false;
|
|
126
|
-
// Reset scoping from previous runs unless explicitly set by startBoard/startEpic
|
|
127
|
-
if (!this._scopeSetByCall) {
|
|
128
|
-
this.epicScope = null;
|
|
129
|
-
this.boardId = null;
|
|
130
|
-
this.executionDir = null;
|
|
131
|
-
}
|
|
132
|
-
this._scopeSetByCall = false;
|
|
133
201
|
this.status = 'starting';
|
|
134
202
|
this.emit('statusChanged', this.status);
|
|
135
203
|
|
|
204
|
+
this.context = this.buildContext(options);
|
|
205
|
+
|
|
136
206
|
const startTime = Date.now();
|
|
137
207
|
this.status = 'executing';
|
|
138
208
|
this.emit('statusChanged', this.status);
|
|
139
209
|
|
|
140
|
-
this.
|
|
141
|
-
this.boardDir = this.resolveBoardDir();
|
|
142
|
-
|
|
143
|
-
await this.recoverStaleIssues();
|
|
144
|
-
|
|
210
|
+
await this.runStaleRecovery();
|
|
145
211
|
const stallResult = await this.runWaveLoop();
|
|
146
212
|
|
|
147
213
|
this.metrics.totalDuration = Date.now() - startTime;
|
|
@@ -163,10 +229,43 @@ export class PlanExecutor extends EventEmitter {
|
|
|
163
229
|
this.emit('statusChanged', this.status);
|
|
164
230
|
}
|
|
165
231
|
|
|
232
|
+
/** Build an immutable execution context from start options. */
|
|
233
|
+
private buildContext(options: StartOptions): PlanExecutionContext {
|
|
234
|
+
const pmDir = resolvePmDir(this.workingDir);
|
|
235
|
+
const boardId = options.board ?? null;
|
|
236
|
+
return {
|
|
237
|
+
workingDir: this.workingDir,
|
|
238
|
+
extraEnv: this.extraEnv,
|
|
239
|
+
epicScope: options.epic ?? null,
|
|
240
|
+
boardId,
|
|
241
|
+
executionDir: options.executionDir ?? null,
|
|
242
|
+
pmDir,
|
|
243
|
+
boardDir: resolveBoardDir(pmDir, boardId),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Warning / update helpers bound to the executor's event stream ──
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Forward module-emitted warnings as executor 'output' events so they flow
|
|
251
|
+
* through to the WebSocket broadcast like inline warnings always have.
|
|
252
|
+
*/
|
|
253
|
+
private emitWarn: WarnFn = (message, issueId) => {
|
|
254
|
+
this.emit('output', { issueId: issueId ?? 'system', text: message, boardId: this.context.boardId ?? null });
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
private async setIssueStatus(issuePath: string, newStatus: string): Promise<void> {
|
|
258
|
+
const { pmDir } = this.context;
|
|
259
|
+
if (!pmDir) return;
|
|
260
|
+
await updateIssueFrontMatter(pmDir, issuePath, newStatus, this.emitWarn);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Wave loop ────────────────────────────────────────────────
|
|
264
|
+
|
|
166
265
|
/** Run waves until done, paused, stopped, or stalled. */
|
|
167
266
|
private async runWaveLoop(): Promise<'done' | 'stalled' | 'dead'> {
|
|
168
267
|
let consecutiveZeroCompletions = 0;
|
|
169
|
-
const maxParallel = await this.
|
|
268
|
+
const maxParallel = await getBoardMaxParallelAgents(this.context.pmDir, this.effectiveBoardId(), this.emitWarn);
|
|
170
269
|
|
|
171
270
|
while (!this.shouldStop && !this.shouldPause) {
|
|
172
271
|
const readyIssues = await this.pickReadyIssues();
|
|
@@ -187,33 +286,15 @@ export class PlanExecutor extends EventEmitter {
|
|
|
187
286
|
return 'done';
|
|
188
287
|
}
|
|
189
288
|
|
|
190
|
-
private
|
|
191
|
-
|
|
192
|
-
if (!pmDir) return false;
|
|
193
|
-
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
194
|
-
const issues = effectiveBoardId
|
|
195
|
-
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
196
|
-
: this.loadProjectIssues();
|
|
197
|
-
if (!issues) return false;
|
|
198
|
-
const terminalStatuses = new Set(['done', 'cancelled']);
|
|
199
|
-
return issues.some(i => i.type !== 'epic' && !terminalStatuses.has(i.status) && i.status !== 'todo');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
pause(): void { this.shouldPause = true; }
|
|
203
|
-
stop(): void {
|
|
204
|
-
this.shouldStop = true;
|
|
205
|
-
this.status = 'stopping';
|
|
206
|
-
this.emit('statusChanged', this.status);
|
|
207
|
-
// Kill all running HeadlessRunner processes in the current wave
|
|
208
|
-
this.waveAbortController?.abort();
|
|
289
|
+
private effectiveBoardId(): string | null {
|
|
290
|
+
return this.context.boardId ?? resolveActiveBoardId(this.context.pmDir);
|
|
209
291
|
}
|
|
210
292
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return this.start();
|
|
293
|
+
private async hasDeadIssues(): Promise<boolean> {
|
|
294
|
+
const { pmDir } = this.context;
|
|
295
|
+
if (!pmDir) return false;
|
|
296
|
+
const { issues } = await this.loadScopedIssues(pmDir);
|
|
297
|
+
return issues ? hasBlockedIssues(issues) : false;
|
|
217
298
|
}
|
|
218
299
|
|
|
219
300
|
// ── Wave execution ───────────────────────────────────────────
|
|
@@ -229,15 +310,15 @@ export class PlanExecutor extends EventEmitter {
|
|
|
229
310
|
// Create abort controller for this wave — stop() will abort it
|
|
230
311
|
this.waveAbortController = new AbortController();
|
|
231
312
|
|
|
232
|
-
await this.
|
|
313
|
+
await ensureOutputDirs(this.context.pmDir, this.context.boardDir);
|
|
233
314
|
this.configInstaller.installPermissions();
|
|
234
315
|
|
|
235
316
|
for (const issue of issues) {
|
|
236
|
-
await this.
|
|
317
|
+
await this.setIssueStatus(issue.path, 'in_progress');
|
|
237
318
|
}
|
|
238
319
|
|
|
239
|
-
const existingDocs = listExistingDocs(this.workingDir, this.boardDir);
|
|
240
|
-
const pmDir = this.
|
|
320
|
+
const existingDocs = listExistingDocs(this.workingDir, this.context.boardDir);
|
|
321
|
+
const { pmDir } = this.context;
|
|
241
322
|
|
|
242
323
|
let completedCount = 0;
|
|
243
324
|
|
|
@@ -264,7 +345,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
264
345
|
issueIds: waveIds,
|
|
265
346
|
error: error instanceof Error ? error.message : String(error),
|
|
266
347
|
});
|
|
267
|
-
await
|
|
348
|
+
if (pmDir) await revertIncompleteIssues(pmDir, issues, this.emitWarn);
|
|
268
349
|
} finally {
|
|
269
350
|
this.configInstaller.uninstallPermissions();
|
|
270
351
|
}
|
|
@@ -283,18 +364,20 @@ export class PlanExecutor extends EventEmitter {
|
|
|
283
364
|
waveLabel: string,
|
|
284
365
|
abortSignal?: AbortSignal,
|
|
285
366
|
): Promise<void> {
|
|
286
|
-
const
|
|
287
|
-
const
|
|
367
|
+
const { executionDir, boardDir, workingDir } = this.context;
|
|
368
|
+
const effectiveDir = executionDir || workingDir;
|
|
369
|
+
const outputPath = resolveOutputPath(issue, workingDir, boardDir);
|
|
288
370
|
const prompt = buildIssuePrompt({
|
|
289
371
|
issue,
|
|
290
372
|
workingDir: effectiveDir,
|
|
291
373
|
pmDir,
|
|
292
|
-
boardDir
|
|
374
|
+
boardDir,
|
|
293
375
|
existingDocs,
|
|
294
376
|
outputPath,
|
|
295
377
|
});
|
|
296
378
|
|
|
297
|
-
const boardLogDir =
|
|
379
|
+
const boardLogDir = boardDir ? join(boardDir, 'logs') : undefined;
|
|
380
|
+
const emitOutput = (text: string) => this.emit('output', { issueId: issue.id, text });
|
|
298
381
|
const result = await runWithFileLogger(`pm-issue-${issue.id}`, () => runIssueWithRetry({
|
|
299
382
|
workingDir: effectiveDir,
|
|
300
383
|
prompt,
|
|
@@ -302,8 +385,16 @@ export class PlanExecutor extends EventEmitter {
|
|
|
302
385
|
stallKillMs: ISSUE_STALL_KILL_MS,
|
|
303
386
|
stallHardCapMs: ISSUE_STALL_HARD_CAP_MS,
|
|
304
387
|
stallMaxExtensions: ISSUE_STALL_MAX_EXTENSIONS,
|
|
305
|
-
outputCallback:
|
|
306
|
-
|
|
388
|
+
outputCallback: emitOutput,
|
|
389
|
+
// Thinking deltas: marker-prefix so the web UI can classify them as a
|
|
390
|
+
// separate verbose-only block type without needing a new message field.
|
|
391
|
+
thinkingCallback: (text: string) => emitOutput(`[[MSTRO_THINKING]]${text}`),
|
|
392
|
+
// Tool calls: emit a single line per completed tool with a short arg
|
|
393
|
+
// summary. The 'tool_complete' event fires after input has fully
|
|
394
|
+
// streamed in, so we can render the tool with its real arguments.
|
|
395
|
+
toolUseCallback: (event) => {
|
|
396
|
+
if (event.type !== 'tool_complete' || !event.toolName) return;
|
|
397
|
+
emitOutput(`[[MSTRO_TOOL]]${formatToolCallLine(event.toolName, event.completeInput)}`);
|
|
307
398
|
},
|
|
308
399
|
extraEnv: this.extraEnv,
|
|
309
400
|
abortSignal,
|
|
@@ -319,8 +410,10 @@ export class PlanExecutor extends EventEmitter {
|
|
|
319
410
|
* doesn't prevent the others or kill the while loop in start().
|
|
320
411
|
*/
|
|
321
412
|
private async finalizeWave(issues: Issue[], waveStart: number, waveLabel: string): Promise<void> {
|
|
413
|
+
const { pmDir, boardDir, boardId } = this.context;
|
|
414
|
+
|
|
322
415
|
try {
|
|
323
|
-
reconcileState(this.workingDir,
|
|
416
|
+
reconcileState(this.workingDir, boardId ?? undefined);
|
|
324
417
|
this.emit('stateUpdated');
|
|
325
418
|
} catch (err) {
|
|
326
419
|
this.emit('output', {
|
|
@@ -330,7 +423,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
330
423
|
}
|
|
331
424
|
|
|
332
425
|
try {
|
|
333
|
-
publishOutputs(issues, this.workingDir,
|
|
426
|
+
publishOutputs(issues, this.workingDir, boardDir, {
|
|
334
427
|
onWarning: (issueId, text) => this.emit('output', { issueId, text: `Warning: ${text}` }),
|
|
335
428
|
});
|
|
336
429
|
} catch (err) {
|
|
@@ -341,7 +434,7 @@ export class PlanExecutor extends EventEmitter {
|
|
|
341
434
|
}
|
|
342
435
|
|
|
343
436
|
try {
|
|
344
|
-
await
|
|
437
|
+
await appendProgressEntry(pmDir, boardDir, issues, waveStart, this.emitWarn);
|
|
345
438
|
} catch (err) {
|
|
346
439
|
this.emit('output', {
|
|
347
440
|
issueId: waveLabel,
|
|
@@ -358,30 +451,21 @@ export class PlanExecutor extends EventEmitter {
|
|
|
358
451
|
* and either confirmed `done` (passed) or reverted to `todo` (failed).
|
|
359
452
|
*/
|
|
360
453
|
private async reconcileWaveResults(issues: Issue[]): Promise<number> {
|
|
361
|
-
const pmDir = this.
|
|
454
|
+
const { pmDir } = this.context;
|
|
362
455
|
if (!pmDir) return 0;
|
|
363
456
|
|
|
364
457
|
let completed = 0;
|
|
365
458
|
|
|
366
459
|
for (const issue of issues) {
|
|
367
|
-
const fullPath =
|
|
460
|
+
const fullPath = validateIssuePath(issue.path, pmDir);
|
|
368
461
|
try {
|
|
369
462
|
const content = await readFile(fullPath, 'utf-8');
|
|
370
|
-
const
|
|
371
|
-
const currentStatus = statusMatch?.[1] ?? 'unknown';
|
|
463
|
+
const currentStatus = extractIssueStatus(content) ?? 'unknown';
|
|
372
464
|
|
|
373
465
|
if (currentStatus === 'in_review' || currentStatus === 'done') {
|
|
374
|
-
if (issue
|
|
375
|
-
// Skip review gate — mark done directly
|
|
376
|
-
await this.updateIssueFrontMatter(issue.path, 'done');
|
|
377
|
-
this.metrics.issuesCompleted++;
|
|
378
|
-
this.emit('issueCompleted', issue);
|
|
379
|
-
completed++;
|
|
380
|
-
} else {
|
|
381
|
-
completed += await this.runReviewGate(issue, pmDir);
|
|
382
|
-
}
|
|
466
|
+
if (await this.finalizeCompletedIssue(issue, pmDir)) completed++;
|
|
383
467
|
} else {
|
|
384
|
-
await this.
|
|
468
|
+
await this.setIssueStatus(issue.path, issue.status);
|
|
385
469
|
this.emit('issueError', {
|
|
386
470
|
issueId: issue.id,
|
|
387
471
|
error: 'Issue did not complete during wave execution',
|
|
@@ -395,56 +479,41 @@ export class PlanExecutor extends EventEmitter {
|
|
|
395
479
|
return completed;
|
|
396
480
|
}
|
|
397
481
|
|
|
398
|
-
/**
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.
|
|
406
|
-
this.emit('issueAbandoned', {
|
|
407
|
-
issueId: issue.id,
|
|
408
|
-
reason: `Review failed ${MAX_REVIEW_ATTEMPTS} times — cancelled to unblock dependents`,
|
|
409
|
-
attempts,
|
|
410
|
-
});
|
|
411
|
-
this.emit('output', { issueId: issue.id, text: `Review: max attempts reached, cancelling issue to unblock dependents` });
|
|
412
|
-
return 0;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
await this.updateIssueFrontMatter(issue.path, 'in_review');
|
|
416
|
-
this.emit('reviewProgress', { issueId: issue.id, status: 'reviewing' });
|
|
417
|
-
|
|
418
|
-
const outputPath = resolveOutputPath(issue, this.workingDir, this.boardDir);
|
|
419
|
-
const result = await reviewIssue({
|
|
420
|
-
workingDir: this.executionDir || this.workingDir,
|
|
421
|
-
issue,
|
|
422
|
-
pmDir,
|
|
423
|
-
outputPath,
|
|
424
|
-
onOutput: (text) => this.emit('output', { issueId: issue.id, text }),
|
|
425
|
-
logDir: this.boardDir ? join(this.boardDir, 'logs') : undefined,
|
|
426
|
-
reviewCriteria: await this.getBoardReviewCriteria(),
|
|
427
|
-
boardDir: this.boardDir,
|
|
428
|
-
extraEnv: this.extraEnv,
|
|
429
|
-
});
|
|
430
|
-
persistReviewResult(reviewDir, issue, result);
|
|
431
|
-
|
|
432
|
-
if (result.passed) {
|
|
433
|
-
await this.updateIssueFrontMatter(issue.path, 'done');
|
|
482
|
+
/**
|
|
483
|
+
* Finalize a single issue whose status reached `in_review`/`done`. Runs the
|
|
484
|
+
* review pipeline unless the issue opted out via `reviewGate: 'none'`.
|
|
485
|
+
* Returns true when the issue is confirmed done (counted toward completions).
|
|
486
|
+
*/
|
|
487
|
+
private async finalizeCompletedIssue(issue: Issue, pmDir: string): Promise<boolean> {
|
|
488
|
+
if (issue.reviewGate === 'none') {
|
|
489
|
+
await this.setIssueStatus(issue.path, 'done');
|
|
434
490
|
this.metrics.issuesCompleted++;
|
|
435
|
-
this.emit('reviewProgress', { issueId: issue.id, status: 'passed' });
|
|
436
491
|
this.emit('issueCompleted', issue);
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
await
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const passed = await runReviewPipeline(
|
|
496
|
+
{
|
|
497
|
+
issue,
|
|
498
|
+
pmDir,
|
|
499
|
+
workingDir: this.workingDir,
|
|
500
|
+
executionDir: this.context.executionDir,
|
|
501
|
+
boardDir: this.context.boardDir,
|
|
502
|
+
boardId: this.context.boardId,
|
|
503
|
+
extraEnv: this.extraEnv,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
setStatus: (path, status) => this.setIssueStatus(path, status),
|
|
507
|
+
onOutput: (issueId, text) => this.emit('output', { issueId, text }),
|
|
508
|
+
onReviewProgress: (issueId, status) => this.emit('reviewProgress', { issueId, status }),
|
|
509
|
+
onIssueAbandoned: (issueId, reason, attempts) => this.emit('issueAbandoned', { issueId, reason, attempts }),
|
|
510
|
+
onIssueCompleted: (completedIssue) => this.emit('issueCompleted', completedIssue),
|
|
511
|
+
onIssueError: (issueId, error) => this.emit('issueError', { issueId, error }),
|
|
512
|
+
warn: this.emitWarn,
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
if (passed) this.metrics.issuesCompleted++;
|
|
516
|
+
return passed;
|
|
448
517
|
}
|
|
449
518
|
|
|
450
519
|
// ── Recovery ─────────────────────────────────────────────────
|
|
@@ -455,28 +524,14 @@ export class PlanExecutor extends EventEmitter {
|
|
|
455
524
|
* these issues block the dependency graph and cause the executor to
|
|
456
525
|
* find zero ready issues, making "Implement" appear to do nothing.
|
|
457
526
|
*/
|
|
458
|
-
private async
|
|
459
|
-
const pmDir = this.
|
|
527
|
+
private async runStaleRecovery(): Promise<void> {
|
|
528
|
+
const { pmDir } = this.context;
|
|
460
529
|
if (!pmDir) return;
|
|
461
530
|
|
|
462
|
-
const
|
|
463
|
-
const issues = effectiveBoardId
|
|
464
|
-
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
465
|
-
: this.loadProjectIssues();
|
|
466
|
-
|
|
531
|
+
const { issues } = await this.loadScopedIssues(pmDir);
|
|
467
532
|
if (!issues) return;
|
|
468
533
|
|
|
469
|
-
const
|
|
470
|
-
const recovered: string[] = [];
|
|
471
|
-
|
|
472
|
-
for (const issue of issues) {
|
|
473
|
-
if (issue.type === 'epic') continue;
|
|
474
|
-
if (staleStatuses.has(issue.status)) {
|
|
475
|
-
await this.updateIssueFrontMatter(issue.path, 'todo');
|
|
476
|
-
recovered.push(`${issue.id} (${issue.status} → todo)`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
534
|
+
const recovered = await recoverStaleIssues(pmDir, issues, this.emitWarn);
|
|
480
535
|
if (recovered.length > 0) {
|
|
481
536
|
this.emit('output', {
|
|
482
537
|
issueId: 'recovery',
|
|
@@ -486,315 +541,49 @@ export class PlanExecutor extends EventEmitter {
|
|
|
486
541
|
}
|
|
487
542
|
}
|
|
488
543
|
|
|
489
|
-
// ──
|
|
490
|
-
|
|
491
|
-
/** Read the board's maxParallelAgents setting, falling back to default. */
|
|
492
|
-
private async getBoardMaxParallelAgents(): Promise<number> {
|
|
493
|
-
const pmDir = this.pmDir;
|
|
494
|
-
if (!pmDir) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
495
|
-
|
|
496
|
-
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
497
|
-
if (!effectiveBoardId) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
498
|
-
|
|
499
|
-
const boardMdPath = join(pmDir, 'boards', effectiveBoardId, 'board.md');
|
|
500
|
-
if (!existsSync(boardMdPath)) return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
501
|
-
|
|
502
|
-
try {
|
|
503
|
-
const content = await readFile(boardMdPath, 'utf-8');
|
|
504
|
-
const match = content.match(/^max_parallel_agents:\s*(\d+)/m);
|
|
505
|
-
return match ? Math.max(1, Math.min(Number(match[1]), 10)) : DEFAULT_MAX_PARALLEL_AGENTS;
|
|
506
|
-
} catch (err) {
|
|
507
|
-
this.emit('output', { issueId: 'system', text: `Warning: failed to read board max_parallel_agents: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
508
|
-
return DEFAULT_MAX_PARALLEL_AGENTS;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/** Read the board's custom review criteria, if set. */
|
|
513
|
-
private async getBoardReviewCriteria(): Promise<string | undefined> {
|
|
514
|
-
const pmDir = this.pmDir;
|
|
515
|
-
if (!pmDir) return undefined;
|
|
516
|
-
|
|
517
|
-
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
518
|
-
if (!effectiveBoardId) return undefined;
|
|
519
|
-
|
|
520
|
-
const boardMdPath = join(pmDir, 'boards', effectiveBoardId, 'board.md');
|
|
521
|
-
if (!existsSync(boardMdPath)) return undefined;
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
const content = await readFile(boardMdPath, 'utf-8');
|
|
525
|
-
const match = content.match(/^review_criteria:\s*"(.+)"/m);
|
|
526
|
-
if (!match) return undefined;
|
|
527
|
-
const raw = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
|
|
528
|
-
return raw || undefined;
|
|
529
|
-
} catch (err) {
|
|
530
|
-
this.emit('output', { issueId: 'system', text: `Warning: failed to read board review criteria: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
531
|
-
return undefined;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
544
|
+
// ── Issue loading & readiness ────────────────────────────────
|
|
534
545
|
|
|
535
546
|
private async pickReadyIssues(): Promise<Issue[]> {
|
|
536
|
-
const pmDir = this.
|
|
547
|
+
const { pmDir, epicScope } = this.context;
|
|
537
548
|
if (!pmDir) {
|
|
538
549
|
this.emit('error', 'No PM directory found');
|
|
539
550
|
return [];
|
|
540
551
|
}
|
|
541
552
|
|
|
542
|
-
const
|
|
543
|
-
const issues = effectiveBoardId
|
|
544
|
-
? await this.loadBoardIssues(pmDir, effectiveBoardId)
|
|
545
|
-
: this.loadProjectIssues();
|
|
546
|
-
|
|
553
|
+
const { issues, boardId } = await this.loadScopedIssues(pmDir);
|
|
547
554
|
if (!issues) return [];
|
|
548
555
|
|
|
549
|
-
const readyIssues = resolveReadyToWork(issues,
|
|
556
|
+
const readyIssues = resolveReadyToWork(issues, epicScope ?? undefined);
|
|
550
557
|
if (readyIssues.length === 0) {
|
|
551
|
-
const deadState =
|
|
558
|
+
const deadState = detectDeadState(issues);
|
|
552
559
|
if (deadState) {
|
|
553
560
|
this.emit('error', deadState);
|
|
554
561
|
} else {
|
|
555
|
-
this.emit('complete',
|
|
556
|
-
if (
|
|
557
|
-
await
|
|
562
|
+
this.emit('complete', buildCompletionReason(issues, epicScope));
|
|
563
|
+
if (boardId) {
|
|
564
|
+
await tryCompleteBoardIfDone(pmDir, boardId, issues, this.emitWarn);
|
|
558
565
|
}
|
|
559
566
|
}
|
|
560
567
|
}
|
|
561
568
|
return readyIssues;
|
|
562
569
|
}
|
|
563
570
|
|
|
564
|
-
/**
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
this.emit('error', `Board ${boardId} is not active (status: ${boardState.board.status})`);
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
return boardState.issues;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/** Load project-level issues (legacy or no boards). Returns null on error. */
|
|
585
|
-
private loadProjectIssues(): Issue[] | null {
|
|
586
|
-
const fullState = parsePlanDirectory(this.workingDir);
|
|
587
|
-
if (!fullState) {
|
|
588
|
-
this.emit('error', 'No PM directory found');
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
if (fullState.state.paused) {
|
|
592
|
-
this.emit('error', 'Project is paused');
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
|
-
return fullState.issues;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/** Activate a draft board by updating its status in board.md. */
|
|
599
|
-
private async activateBoard(pmDir: string, boardId: string): Promise<void> {
|
|
600
|
-
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
601
|
-
if (!existsSync(boardMdPath)) return;
|
|
602
|
-
try {
|
|
603
|
-
const content = await readFile(boardMdPath, 'utf-8');
|
|
604
|
-
await writeFile(boardMdPath, replaceFrontMatterField(content, 'status', 'active'), 'utf-8');
|
|
605
|
-
} catch (err) {
|
|
606
|
-
this.emit('output', { issueId: 'system', text: `Warning: failed to activate board ${boardId}: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/** Check if all issues in a board are done and mark board as completed. */
|
|
611
|
-
private async tryCompleteBoardIfDone(pmDir: string, boardId: string, issues: Issue[]): Promise<void> {
|
|
612
|
-
const allDone = issues.length > 0 && issues.every(i => i.status === 'done' || i.status === 'cancelled');
|
|
613
|
-
if (!allDone) return;
|
|
614
|
-
|
|
615
|
-
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
616
|
-
if (!existsSync(boardMdPath)) return;
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
let content = await readFile(boardMdPath, 'utf-8');
|
|
620
|
-
content = replaceFrontMatterField(content, 'status', 'completed');
|
|
621
|
-
content = replaceFrontMatterField(content, 'completed_at', `"${new Date().toISOString()}"`);
|
|
622
|
-
await writeFile(boardMdPath, content, 'utf-8');
|
|
623
|
-
} catch (err) {
|
|
624
|
-
this.emit('output', { issueId: 'system', text: `Warning: failed to mark board ${boardId} as completed: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
private resolveActiveBoardId(): string | null {
|
|
629
|
-
const pmDir = this.pmDir;
|
|
630
|
-
if (!pmDir) return null;
|
|
631
|
-
try {
|
|
632
|
-
const workspacePath = join(pmDir, 'workspace.json');
|
|
633
|
-
if (!existsSync(workspacePath)) return null;
|
|
634
|
-
const workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
|
|
635
|
-
return workspace.activeBoardId ?? null;
|
|
636
|
-
} catch {
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private buildCompletionReason(issues: Issue[]): string {
|
|
642
|
-
const nonEpic = issues.filter(i => i.type !== 'epic');
|
|
643
|
-
const done = nonEpic.filter(i => i.status === 'done' || i.status === 'cancelled').length;
|
|
644
|
-
const blocked = nonEpic.filter(i => i.status === 'todo').length;
|
|
645
|
-
if (done === nonEpic.length) return this.epicScope ? 'All epic issues are done' : 'All issues are done';
|
|
646
|
-
if (blocked > 0) return `${done}/${nonEpic.length} issues done, ${blocked} blocked by incomplete dependencies`;
|
|
647
|
-
return this.epicScope ? 'All epic issues are done or blocked' : 'All work is done or blocked';
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/** Detect issues stuck in non-terminal states with no path to completion. */
|
|
651
|
-
private detectDeadState(issues: Issue[]): string | null {
|
|
652
|
-
const nonEpic = issues.filter(i => i.type !== 'epic');
|
|
653
|
-
const terminalStatuses = new Set(['done', 'cancelled']);
|
|
654
|
-
const stuck = nonEpic.filter(i => !terminalStatuses.has(i.status) && i.status !== 'todo');
|
|
655
|
-
|
|
656
|
-
if (stuck.length === 0) return null;
|
|
657
|
-
|
|
658
|
-
const stuckIds = stuck.map(i => `${i.id} (${i.status})`).join(', ');
|
|
659
|
-
|
|
660
|
-
const issueByPath = new Map(issues.map(i => [i.path, i]));
|
|
661
|
-
const blockedByStuck = nonEpic.filter(i => {
|
|
662
|
-
if (i.status !== 'todo') return false;
|
|
663
|
-
return i.blockedBy.some(bp => {
|
|
664
|
-
const blocker = issueByPath.get(bp);
|
|
665
|
-
return blocker && !terminalStatuses.has(blocker.status);
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
const blockedIds = blockedByStuck.map(i => i.id).join(', ');
|
|
669
|
-
|
|
670
|
-
return `Board stuck: ${stuckIds} cannot progress${blockedIds ? `. Blocking: ${blockedIds}` : ''}`;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
private async revertIncompleteIssues(issues: Issue[]): Promise<void> {
|
|
674
|
-
const pmDir = this.pmDir;
|
|
675
|
-
if (!pmDir) return;
|
|
676
|
-
for (const issue of issues) {
|
|
677
|
-
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
678
|
-
try {
|
|
679
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
680
|
-
if (content.match(/^status:\s*in_progress$/m)) {
|
|
681
|
-
await this.updateIssueFrontMatter(issue.path, issue.status);
|
|
682
|
-
}
|
|
683
|
-
} catch (err) {
|
|
684
|
-
this.emit('output', { issueId: issue.id, text: `Warning: failed to revert issue status: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
private async appendCancellationNote(issue: Issue, pmDir: string, reason: string): Promise<void> {
|
|
690
|
-
const fullPath = this.validateIssuePath(issue.path, pmDir);
|
|
691
|
-
try {
|
|
692
|
-
let content = await readFile(fullPath, 'utf-8');
|
|
693
|
-
const entry = `- Cancelled (${new Date().toISOString().split('T')[0]}): ${reason}`;
|
|
694
|
-
if (content.includes('## Activity')) {
|
|
695
|
-
content = content.replace(/## Activity/, `## Activity\n${entry}`);
|
|
696
|
-
} else {
|
|
697
|
-
content += `\n\n## Activity\n${entry}`;
|
|
698
|
-
}
|
|
699
|
-
await writeFile(fullPath, content, 'utf-8');
|
|
700
|
-
} catch (err) {
|
|
701
|
-
this.emit('output', { issueId: issue.id, text: `Warning: failed to append cancellation note: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private async updateIssueFrontMatter(issuePath: string, newStatus: string): Promise<void> {
|
|
706
|
-
const pmDir = this.pmDir;
|
|
707
|
-
if (!pmDir) return;
|
|
708
|
-
try {
|
|
709
|
-
const fullPath = this.validateIssuePath(issuePath, pmDir);
|
|
710
|
-
await setFrontMatterFieldAsync(fullPath, 'status', newStatus);
|
|
711
|
-
|
|
712
|
-
if (newStatus === 'done') {
|
|
713
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
714
|
-
const updated = checkAllAcceptanceCriteria(content);
|
|
715
|
-
if (updated !== content) await writeFile(fullPath, updated, 'utf-8');
|
|
716
|
-
}
|
|
717
|
-
} catch (err) {
|
|
718
|
-
this.emit('output', { issueId: 'system', text: `Warning: failed to update issue front matter for ${issuePath}: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
private async ensureOutputDirs(): Promise<void> {
|
|
723
|
-
if (this.boardDir) {
|
|
724
|
-
const boardOutDir = join(this.boardDir, 'out');
|
|
725
|
-
if (!existsSync(boardOutDir)) await mkdir(boardOutDir, { recursive: true });
|
|
726
|
-
} else {
|
|
727
|
-
const pmDir = this.pmDir;
|
|
728
|
-
if (pmDir) {
|
|
729
|
-
const outDir = join(pmDir, 'out');
|
|
730
|
-
if (!existsSync(outDir)) await mkdir(outDir, { recursive: true });
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
private async appendProgressEntry(issues: Issue[], waveStart: number): Promise<void> {
|
|
736
|
-
const pmDir = this.pmDir;
|
|
737
|
-
if (!pmDir) return;
|
|
738
|
-
|
|
739
|
-
const progressPath = this.boardDir
|
|
740
|
-
? join(this.boardDir, 'progress.md')
|
|
741
|
-
: join(pmDir, 'progress.md');
|
|
742
|
-
|
|
743
|
-
const durationMin = Math.round((Date.now() - waveStart) / 60_000);
|
|
744
|
-
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
745
|
-
|
|
746
|
-
const completed: string[] = [];
|
|
747
|
-
const failed: string[] = [];
|
|
748
|
-
for (const issue of issues) {
|
|
749
|
-
try {
|
|
750
|
-
const content = await readFile(this.validateIssuePath(issue.path, pmDir), 'utf-8');
|
|
751
|
-
const statusMatch = content.match(/^status:\s*(\S+)/m);
|
|
752
|
-
if (statusMatch?.[1] === 'done') {
|
|
753
|
-
completed.push(issue.id);
|
|
754
|
-
} else {
|
|
755
|
-
failed.push(issue.id);
|
|
756
|
-
}
|
|
757
|
-
} catch {
|
|
758
|
-
failed.push(issue.id);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const lines = [
|
|
763
|
-
'',
|
|
764
|
-
`## ${timestamp} — Wave [${issues.map(i => i.id).join(', ')}]`,
|
|
765
|
-
'',
|
|
766
|
-
`- **Duration**: ${durationMin} min`,
|
|
767
|
-
`- **Completed**: ${completed.length}/${issues.length}${completed.length > 0 ? ` (${completed.join(', ')})` : ''}`,
|
|
768
|
-
];
|
|
769
|
-
if (failed.length > 0) {
|
|
770
|
-
lines.push(`- **Failed**: ${failed.join(', ')}`);
|
|
771
|
-
}
|
|
772
|
-
lines.push('');
|
|
773
|
-
|
|
774
|
-
await this.writeProgressLines(progressPath, lines);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
private async writeProgressLines(filePath: string, lines: string[]): Promise<void> {
|
|
778
|
-
try {
|
|
779
|
-
if (existsSync(filePath)) {
|
|
780
|
-
await appendFile(filePath, `\n${lines.join('\n')}`, 'utf-8');
|
|
781
|
-
} else {
|
|
782
|
-
await writeFile(filePath, `# Board Progress\n${lines.join('\n')}`, 'utf-8');
|
|
783
|
-
}
|
|
784
|
-
} catch (err) {
|
|
785
|
-
this.emit('output', { issueId: 'system', text: `Warning: failed to write progress log: ${err instanceof Error ? err.message : String(err)}`, boardId: this.boardId ?? null });
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/** Resolve the active board's directory path for outputs, reviews, and progress. */
|
|
790
|
-
private resolveBoardDir(): string | null {
|
|
791
|
-
const pmDir = this.pmDir;
|
|
792
|
-
if (!pmDir) return null;
|
|
793
|
-
|
|
794
|
-
const effectiveBoardId = this.boardId ?? this.resolveActiveBoardId();
|
|
795
|
-
if (!effectiveBoardId) return null;
|
|
796
|
-
|
|
797
|
-
const boardDir = join(pmDir, 'boards', effectiveBoardId);
|
|
798
|
-
return existsSync(boardDir) ? boardDir : null;
|
|
571
|
+
/**
|
|
572
|
+
* Load issues for the active execution scope. Returns the resolved boardId
|
|
573
|
+
* alongside the issues so callers can branch on board-specific logic without
|
|
574
|
+
* re-resolving the scope.
|
|
575
|
+
*/
|
|
576
|
+
private async loadScopedIssues(pmDir: string): Promise<{ issues: Issue[] | null; boardId: string | null }> {
|
|
577
|
+
const boardId = this.effectiveBoardId();
|
|
578
|
+
const issues = boardId
|
|
579
|
+
? await loadBoardIssues(pmDir, boardId, {
|
|
580
|
+
onError: msg => this.emit('error', msg),
|
|
581
|
+
warn: this.emitWarn,
|
|
582
|
+
})
|
|
583
|
+
: loadProjectIssues(this.workingDir, { onError: msg => this.emit('error', msg) });
|
|
584
|
+
return { issues, boardId };
|
|
799
585
|
}
|
|
800
586
|
}
|
|
587
|
+
|
|
588
|
+
// Re-export for backwards compatibility with modules that imported the constant from here.
|
|
589
|
+
export { DEFAULT_MAX_PARALLEL_AGENTS };
|