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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Front Matter Utilities — Read/write YAML front matter fields.
|
|
6
|
+
*
|
|
7
|
+
* All replacements are scoped to the --- delimiters to prevent
|
|
8
|
+
* markdown body corruption. Used across executor, plan-handlers,
|
|
9
|
+
* and state-reconciler.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Replace a field value in a raw YAML string (no --- delimiters).
|
|
16
|
+
* If the field does not exist, it is appended.
|
|
17
|
+
*/
|
|
18
|
+
export function replaceYamlField(yaml: string, field: string, value: string): string {
|
|
19
|
+
const regex = new RegExp(`^(${field}:\\s*).+$`, 'm');
|
|
20
|
+
if (regex.test(yaml)) {
|
|
21
|
+
return yaml.replace(regex, `$1${value}`);
|
|
22
|
+
}
|
|
23
|
+
return `${yaml}\n${field}: ${value}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Replace a YAML front matter field in a full markdown content string.
|
|
28
|
+
* Only modifies content between the first pair of --- delimiters.
|
|
29
|
+
* If the field does not exist in front matter, it is appended.
|
|
30
|
+
* Returns content unchanged if no front matter block is found.
|
|
31
|
+
*/
|
|
32
|
+
export function replaceFrontMatterField(content: string, field: string, value: string): string {
|
|
33
|
+
const fmMatch = content.match(/^(---\n)([\s\S]*?)(\n---)/);
|
|
34
|
+
if (!fmMatch) return content;
|
|
35
|
+
|
|
36
|
+
const yaml = replaceYamlField(fmMatch[2], field, value);
|
|
37
|
+
return `${fmMatch[1]}${yaml}${fmMatch[3]}${content.slice(fmMatch[0].length)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read a file, update a front matter field, and write it back.
|
|
42
|
+
* Convenience wrapper for single-field updates.
|
|
43
|
+
*/
|
|
44
|
+
export function setFrontMatterField(filePath: string, field: string, value: string): void {
|
|
45
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
46
|
+
const updated = replaceFrontMatterField(content, field, value);
|
|
47
|
+
writeFileSync(filePath, updated, 'utf-8');
|
|
48
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Output Manager — Resolves output paths, lists existing docs, and publishes outputs.
|
|
6
|
+
*
|
|
7
|
+
* Handles sprint-sandboxed and global output directories with fallback.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { defaultPmDir, resolvePmDir } from './parser.js';
|
|
13
|
+
import type { Issue } from './types.js';
|
|
14
|
+
|
|
15
|
+
/** Convert a title to a URL-friendly slug (max 60 chars). */
|
|
16
|
+
export function slugify(text: string): string {
|
|
17
|
+
return text
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '')
|
|
21
|
+
.slice(0, 60);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the canonical output path for an issue.
|
|
26
|
+
* Uses sprint sandbox when available, otherwise global .pm/out/.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveOutputPath(issue: Issue, workingDir: string, sprintSandboxDir: string | null): string {
|
|
29
|
+
if (sprintSandboxDir) {
|
|
30
|
+
return join(sprintSandboxDir, 'out', `${issue.id}-${slugify(issue.title)}.md`);
|
|
31
|
+
}
|
|
32
|
+
const pmDir = resolvePmDir(workingDir);
|
|
33
|
+
const outDir = pmDir ? join(pmDir, 'out') : join(defaultPmDir(workingDir), 'out');
|
|
34
|
+
return join(outDir, `${issue.id}-${slugify(issue.title)}.md`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* List existing execution output docs.
|
|
39
|
+
* Searches sprint sandbox first (higher priority), then global out/.
|
|
40
|
+
*/
|
|
41
|
+
export function listExistingDocs(workingDir: string, sprintSandboxDir: string | null): string[] {
|
|
42
|
+
const pmDir = resolvePmDir(workingDir);
|
|
43
|
+
if (!pmDir) return [];
|
|
44
|
+
|
|
45
|
+
const dirs: string[] = [];
|
|
46
|
+
if (sprintSandboxDir) dirs.push(join(sprintSandboxDir, 'out'));
|
|
47
|
+
dirs.push(join(pmDir, 'out'));
|
|
48
|
+
|
|
49
|
+
const docs: string[] = [];
|
|
50
|
+
for (const dir of dirs) {
|
|
51
|
+
if (!existsSync(dir)) continue;
|
|
52
|
+
try {
|
|
53
|
+
docs.push(...readdirSync(dir).filter(f => f.endsWith('.md')).map(f => join(dir, f)));
|
|
54
|
+
} catch { /* skip */ }
|
|
55
|
+
}
|
|
56
|
+
return docs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PublishOutputsCallbacks {
|
|
60
|
+
onWarning?: (issueId: string, text: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copy confirmed-done outputs from .pm/out/ to user-specified output_file paths.
|
|
65
|
+
* Only copies for issues that completed successfully and have output_file set.
|
|
66
|
+
*/
|
|
67
|
+
export function publishOutputs(
|
|
68
|
+
issues: Issue[],
|
|
69
|
+
workingDir: string,
|
|
70
|
+
sprintSandboxDir: string | null,
|
|
71
|
+
callbacks?: PublishOutputsCallbacks,
|
|
72
|
+
): void {
|
|
73
|
+
const pmDir = resolvePmDir(workingDir);
|
|
74
|
+
if (!pmDir) return;
|
|
75
|
+
|
|
76
|
+
for (const issue of issues) {
|
|
77
|
+
publishSingleOutput(issue, pmDir, workingDir, sprintSandboxDir, callbacks);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function publishSingleOutput(
|
|
82
|
+
issue: Issue,
|
|
83
|
+
pmDir: string,
|
|
84
|
+
workingDir: string,
|
|
85
|
+
sprintSandboxDir: string | null,
|
|
86
|
+
callbacks?: PublishOutputsCallbacks,
|
|
87
|
+
): void {
|
|
88
|
+
if (!issue.outputFile) return;
|
|
89
|
+
|
|
90
|
+
// Only publish for confirmed-done issues
|
|
91
|
+
try {
|
|
92
|
+
const content = readFileSync(join(pmDir, issue.path), 'utf-8');
|
|
93
|
+
if (!content.match(/^status:\s*done$/m)) return;
|
|
94
|
+
} catch { return; }
|
|
95
|
+
|
|
96
|
+
const srcPath = resolveOutputPath(issue, workingDir, sprintSandboxDir);
|
|
97
|
+
if (!existsSync(srcPath)) return;
|
|
98
|
+
|
|
99
|
+
// Guard against path traversal — output_file must resolve within workingDir
|
|
100
|
+
const destPath = resolve(workingDir, issue.outputFile);
|
|
101
|
+
if (!destPath.startsWith(`${workingDir}/`) && destPath !== workingDir) {
|
|
102
|
+
callbacks?.onWarning?.(issue.id, `output_file "${issue.outputFile}" escapes project directory — skipping`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const destDir = join(destPath, '..');
|
|
108
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
109
|
+
copyFileSync(srcPath, destPath);
|
|
110
|
+
} catch {
|
|
111
|
+
callbacks?.onWarning?.(issue.id, `could not copy output to ${issue.outputFile}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* YAML front-matter parsing and entity parsers for PPS (.pm/) files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AcceptanceCriterion,
|
|
10
|
+
Board,
|
|
11
|
+
BoardExecutionSummary,
|
|
12
|
+
Issue,
|
|
13
|
+
IssueSummary,
|
|
14
|
+
Milestone,
|
|
15
|
+
MilestoneEpicSummary,
|
|
16
|
+
ProjectConfig,
|
|
17
|
+
ProjectState,
|
|
18
|
+
Sprint,
|
|
19
|
+
SprintExecutionSummary,
|
|
20
|
+
SprintIssueSummary,
|
|
21
|
+
Team,
|
|
22
|
+
WorkflowStatus,
|
|
23
|
+
Workspace,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Front Matter Extraction
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export interface ParsedFile {
|
|
31
|
+
frontMatter: Record<string, unknown>;
|
|
32
|
+
body: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripQuotes(v: string): string {
|
|
36
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
37
|
+
return v.slice(1, -1);
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseYamlValue(v: string): unknown {
|
|
43
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
44
|
+
return v.slice(1, -1);
|
|
45
|
+
}
|
|
46
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
47
|
+
return v.slice(1, -1).split(',').map(s => stripQuotes(s.trim())).filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
if (v === 'true') return true;
|
|
50
|
+
if (v === 'false') return false;
|
|
51
|
+
if (v === 'null' || v === '~' || v === '') return null;
|
|
52
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function consumeIndentedList(lines: string[], startIdx: number): [string[], number] {
|
|
57
|
+
const items: string[] = [];
|
|
58
|
+
let i = startIdx;
|
|
59
|
+
while (i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1])) {
|
|
60
|
+
i++;
|
|
61
|
+
const item = lines[i].trim().replace(/^-\s+/, '');
|
|
62
|
+
items.push(stripQuotes(item));
|
|
63
|
+
}
|
|
64
|
+
return [items, i];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseFrontMatter(content: string): ParsedFile {
|
|
68
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
69
|
+
if (!match) {
|
|
70
|
+
return { frontMatter: {}, body: content };
|
|
71
|
+
}
|
|
72
|
+
const frontMatter: Record<string, unknown> = {};
|
|
73
|
+
const lines = match[1].split('\n');
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const trimmed = lines[i].trim();
|
|
77
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
78
|
+
const colonIdx = trimmed.indexOf(':');
|
|
79
|
+
if (colonIdx === -1) continue;
|
|
80
|
+
|
|
81
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
82
|
+
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
83
|
+
|
|
84
|
+
if (!rawValue) {
|
|
85
|
+
const [items, newIdx] = consumeIndentedList(lines, i);
|
|
86
|
+
i = newIdx;
|
|
87
|
+
frontMatter[key] = items.length > 0 ? items : null;
|
|
88
|
+
} else {
|
|
89
|
+
frontMatter[key] = parseYamlValue(rawValue);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { frontMatter, body: match[2] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Section Extraction
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export function extractSections(body: string): Map<string, string> {
|
|
101
|
+
const sections = new Map<string, string>();
|
|
102
|
+
const lines = body.split('\n');
|
|
103
|
+
let currentSection = '';
|
|
104
|
+
let currentContent: string[] = [];
|
|
105
|
+
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (line.startsWith('## ')) {
|
|
108
|
+
if (currentSection) {
|
|
109
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
110
|
+
}
|
|
111
|
+
currentSection = line.slice(3).trim();
|
|
112
|
+
currentContent = [];
|
|
113
|
+
} else {
|
|
114
|
+
currentContent.push(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (currentSection) {
|
|
118
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return sections;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseCheckboxes(content: string): AcceptanceCriterion[] {
|
|
125
|
+
const items: AcceptanceCriterion[] = [];
|
|
126
|
+
for (const line of content.split('\n')) {
|
|
127
|
+
const match = line.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
128
|
+
if (match) {
|
|
129
|
+
items.push({ text: match[2].trim(), checked: match[1] !== ' ' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return items;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseListItems(content: string): string[] {
|
|
136
|
+
const items: string[] = [];
|
|
137
|
+
for (const line of content.split('\n')) {
|
|
138
|
+
const match = line.match(/^[-*]\s+(.+)$/);
|
|
139
|
+
if (match) items.push(match[1].trim());
|
|
140
|
+
}
|
|
141
|
+
return items;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseIssueSummaries(content: string): IssueSummary[] {
|
|
145
|
+
const summaries: IssueSummary[] = [];
|
|
146
|
+
for (const line of content.split('\n')) {
|
|
147
|
+
const match = line.match(/\d+\.\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*\((\w+)\))?\s*$/);
|
|
148
|
+
if (match) {
|
|
149
|
+
summaries.push({ id: match[1], path: match[2], title: match[3].trim(), priority: match[4] || '' });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const match2 = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*[→→]\s*blocked by\s+\[([^\]]+)\])?\s*$/i);
|
|
153
|
+
if (match2) {
|
|
154
|
+
summaries.push({ id: match2[1], path: match2[2], title: match2[3].trim(), priority: '', blockedBy: match2[4] || undefined });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return summaries;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseCompletedSummaries(content: string): IssueSummary[] {
|
|
161
|
+
const summaries: IssueSummary[] = [];
|
|
162
|
+
for (const line of content.split('\n')) {
|
|
163
|
+
const match = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*✓)?\s*$/);
|
|
164
|
+
if (match) {
|
|
165
|
+
summaries.push({ id: match[1], path: match[2], title: match[3].trim(), priority: '' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return summaries;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Entity Parsers
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
function parseWorkflows(section: string | undefined): WorkflowStatus[] {
|
|
176
|
+
if (!section) return [];
|
|
177
|
+
const workflows: WorkflowStatus[] = [];
|
|
178
|
+
for (const line of section.split('\n')) {
|
|
179
|
+
const match = line.match(/\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|/);
|
|
180
|
+
if (match && match[1] !== 'Status') {
|
|
181
|
+
workflows.push({ status: match[1], category: match[2] as WorkflowStatus['category'], description: match[3].trim() });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return workflows;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseTeams(section: string | undefined): Team[] {
|
|
188
|
+
if (!section) return [];
|
|
189
|
+
const teams: Team[] = [];
|
|
190
|
+
for (const line of section.split('\n')) {
|
|
191
|
+
const match = line.match(/^[-*]\s+(\w+)(?:\s*[—–-]\s*(.+))?$/);
|
|
192
|
+
if (match) teams.push({ name: match[1], description: match[2]?.trim() });
|
|
193
|
+
}
|
|
194
|
+
return teams;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function toStringArray(val: unknown): string[] {
|
|
198
|
+
return Array.isArray(val) ? val.map(String) : [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function optionalString(val: unknown): string | null {
|
|
202
|
+
if (val == null) return null;
|
|
203
|
+
const s = String(val);
|
|
204
|
+
return s === '' ? null : s;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function optionalNumber(val: unknown): number | null {
|
|
208
|
+
return val != null ? Number(val) : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function parseProjectConfig(content: string): ProjectConfig {
|
|
212
|
+
const { frontMatter, body } = parseFrontMatter(content);
|
|
213
|
+
const sections = extractSections(body);
|
|
214
|
+
|
|
215
|
+
const idPrefixes: Record<string, string> = {};
|
|
216
|
+
const rawPrefixes = frontMatter.id_prefixes;
|
|
217
|
+
if (rawPrefixes && typeof rawPrefixes === 'object') {
|
|
218
|
+
Object.assign(idPrefixes, rawPrefixes);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
name: String(frontMatter.name || ''),
|
|
223
|
+
id: String(frontMatter.id || ''),
|
|
224
|
+
created: String(frontMatter.created || ''),
|
|
225
|
+
status: (frontMatter.status as ProjectConfig['status']) || 'active',
|
|
226
|
+
estimation: (frontMatter.estimation as ProjectConfig['estimation']) || 'none',
|
|
227
|
+
idPrefixes,
|
|
228
|
+
workflows: parseWorkflows(sections.get('Workflows')),
|
|
229
|
+
labels: (Array.isArray(frontMatter.labels) ? frontMatter.labels : []) as string[],
|
|
230
|
+
teams: parseTeams(sections.get('Teams')),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function parseProjectState(content: string): ProjectState {
|
|
235
|
+
const { frontMatter, body } = parseFrontMatter(content);
|
|
236
|
+
const sections = extractSections(body);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
project: String(frontMatter.project || ''),
|
|
240
|
+
currentSprint: (frontMatter.current_sprint as string) || null,
|
|
241
|
+
activeMilestone: (frontMatter.active_milestone as string) || null,
|
|
242
|
+
paused: frontMatter.paused === true,
|
|
243
|
+
lastSession: (frontMatter.last_session as string) || null,
|
|
244
|
+
readyToWork: parseIssueSummaries(sections.get('Ready to Work') || ''),
|
|
245
|
+
inProgress: parseIssueSummaries(sections.get('In Progress') || ''),
|
|
246
|
+
blocked: parseIssueSummaries(sections.get('Blocked') || ''),
|
|
247
|
+
recentlyCompleted: parseCompletedSummaries(sections.get('Recently Completed') || ''),
|
|
248
|
+
warnings: parseListItems(sections.get('Warnings') || ''),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function parseIssue(content: string, filePath: string): Issue {
|
|
253
|
+
const { frontMatter: fm, body } = parseFrontMatter(content);
|
|
254
|
+
const sections = extractSections(body);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
id: String(fm.id || ''),
|
|
258
|
+
title: String(fm.title || ''),
|
|
259
|
+
type: (fm.type as Issue['type']) || 'issue',
|
|
260
|
+
status: String(fm.status || 'backlog'),
|
|
261
|
+
priority: String(fm.priority || 'P2'),
|
|
262
|
+
estimate: fm.estimate != null ? fm.estimate as number | string : null,
|
|
263
|
+
labels: toStringArray(fm.labels),
|
|
264
|
+
epic: optionalString(fm.epic),
|
|
265
|
+
sprint: optionalString(fm.sprint),
|
|
266
|
+
milestone: optionalString(fm.milestone),
|
|
267
|
+
assigned: optionalString(fm.assigned),
|
|
268
|
+
created: String(fm.created || ''),
|
|
269
|
+
updated: optionalString(fm.updated),
|
|
270
|
+
due: optionalString(fm.due),
|
|
271
|
+
blockedBy: toStringArray(fm.blocked_by),
|
|
272
|
+
blocks: toStringArray(fm.blocks),
|
|
273
|
+
relatesTo: toStringArray(fm.relates_to),
|
|
274
|
+
children: toStringArray(fm.children),
|
|
275
|
+
progress: optionalString(fm.progress),
|
|
276
|
+
description: sections.get('Description') || '',
|
|
277
|
+
acceptanceCriteria: parseCheckboxes(sections.get('Acceptance Criteria') || ''),
|
|
278
|
+
technicalNotes: sections.get('Technical Notes') || null,
|
|
279
|
+
filesToModify: parseListItems(sections.get('Files to Modify') || ''),
|
|
280
|
+
activity: parseListItems(sections.get('Activity') || ''),
|
|
281
|
+
reviewGate: (['none', 'auto', 'required'].includes(String(fm.review_gate)) ? String(fm.review_gate) : 'auto') as Issue['reviewGate'],
|
|
282
|
+
outputFile: optionalString(fm.output_file),
|
|
283
|
+
body,
|
|
284
|
+
path: filePath,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseSprintIssues(section: string | undefined): SprintIssueSummary[] {
|
|
289
|
+
if (!section) return [];
|
|
290
|
+
const issues: SprintIssueSummary[] = [];
|
|
291
|
+
for (const line of section.split('\n')) {
|
|
292
|
+
const match = line.match(/\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|/);
|
|
293
|
+
if (match) {
|
|
294
|
+
issues.push({ id: match[1], path: match[2], title: match[3].trim(), points: /^\d+$/.test(match[4]) ? Number(match[4]) : match[4], status: match[5] });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return issues;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function parseSprint(content: string, filePath: string): Sprint {
|
|
301
|
+
const { frontMatter: fm, body } = parseFrontMatter(content);
|
|
302
|
+
const sections = extractSections(body);
|
|
303
|
+
|
|
304
|
+
let issues = parseSprintIssues(sections.get('Issues'));
|
|
305
|
+
if (issues.length === 0 && Array.isArray(fm.issues)) {
|
|
306
|
+
issues = (fm.issues as string[]).map(path => {
|
|
307
|
+
const id = path.replace(/^backlog\//, '').replace(/\.md$/, '');
|
|
308
|
+
return { id, path, title: '', points: null, status: '' };
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let executionSummary: SprintExecutionSummary | null = null;
|
|
313
|
+
if (fm.execution_summary && typeof fm.execution_summary === 'object') {
|
|
314
|
+
const es = fm.execution_summary as Record<string, unknown>;
|
|
315
|
+
executionSummary = {
|
|
316
|
+
totalIssues: Number(es.total_issues ?? 0),
|
|
317
|
+
completedIssues: Number(es.completed_issues ?? 0),
|
|
318
|
+
failedIssues: Number(es.failed_issues ?? 0),
|
|
319
|
+
totalDuration: Number(es.total_duration ?? 0),
|
|
320
|
+
waves: Number(es.waves ?? 0),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
id: String(fm.id || ''),
|
|
326
|
+
title: String(fm.title || ''),
|
|
327
|
+
status: (fm.status as Sprint['status']) || 'planned',
|
|
328
|
+
start: String(fm.start || fm.start_date || ''),
|
|
329
|
+
end: String(fm.end || fm.end_date || ''),
|
|
330
|
+
goal: String(fm.goal || sections.get('Goal') || sections.get('Sprint Goal') || ''),
|
|
331
|
+
capacity: optionalNumber(fm.capacity),
|
|
332
|
+
committed: optionalNumber(fm.committed),
|
|
333
|
+
completed: optionalNumber(fm.completed),
|
|
334
|
+
issues,
|
|
335
|
+
path: filePath,
|
|
336
|
+
completedAt: optionalString(fm.completed_at),
|
|
337
|
+
executionSummary,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function parseMilestone(content: string, filePath: string): Milestone {
|
|
342
|
+
const { frontMatter, body } = parseFrontMatter(content);
|
|
343
|
+
const sections = extractSections(body);
|
|
344
|
+
|
|
345
|
+
const epics: MilestoneEpicSummary[] = [];
|
|
346
|
+
const epicSection = sections.get('Epics');
|
|
347
|
+
if (epicSection) {
|
|
348
|
+
for (const line of epicSection.split('\n')) {
|
|
349
|
+
const match = line.match(/\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|/);
|
|
350
|
+
if (match) {
|
|
351
|
+
epics.push({ id: match[1], path: match[2], title: match[3].trim(), progress: match[4] });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
id: String(frontMatter.id || ''),
|
|
358
|
+
title: String(frontMatter.title || ''),
|
|
359
|
+
status: (frontMatter.status as Milestone['status']) || 'planned',
|
|
360
|
+
targetDate: (frontMatter.target_date as string) || null,
|
|
361
|
+
progress: (frontMatter.progress as string) || null,
|
|
362
|
+
definition: sections.get('Definition of Done') || '',
|
|
363
|
+
epics,
|
|
364
|
+
path: filePath,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function parseBoard(content: string, filePath: string): Board {
|
|
369
|
+
const { frontMatter: fm, body } = parseFrontMatter(content);
|
|
370
|
+
const sections = extractSections(body);
|
|
371
|
+
|
|
372
|
+
let executionSummary: BoardExecutionSummary | null = null;
|
|
373
|
+
if (fm.execution_summary && typeof fm.execution_summary === 'object') {
|
|
374
|
+
const es = fm.execution_summary as Record<string, unknown>;
|
|
375
|
+
executionSummary = {
|
|
376
|
+
totalIssues: Number(es.total_issues ?? 0),
|
|
377
|
+
completedIssues: Number(es.completed_issues ?? 0),
|
|
378
|
+
failedIssues: Number(es.failed_issues ?? 0),
|
|
379
|
+
totalDuration: Number(es.total_duration ?? 0),
|
|
380
|
+
waves: Number(es.waves ?? 0),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
id: String(fm.id || ''),
|
|
386
|
+
title: String(fm.title || ''),
|
|
387
|
+
status: (fm.status as Board['status']) || 'draft',
|
|
388
|
+
created: String(fm.created || ''),
|
|
389
|
+
completedAt: optionalString(fm.completed_at),
|
|
390
|
+
goal: String(fm.goal || sections.get('Goal') || ''),
|
|
391
|
+
executionSummary,
|
|
392
|
+
path: filePath,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function parseWorkspace(content: string): Workspace {
|
|
397
|
+
try {
|
|
398
|
+
const parsed = JSON.parse(content) as Record<string, unknown>;
|
|
399
|
+
return {
|
|
400
|
+
activeBoardId: typeof parsed.activeBoardId === 'string' ? parsed.activeBoardId : null,
|
|
401
|
+
boardOrder: Array.isArray(parsed.boardOrder) ? parsed.boardOrder.map(String) : [],
|
|
402
|
+
};
|
|
403
|
+
} catch {
|
|
404
|
+
return { activeBoardId: null, boardOrder: [] };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Legacy → Board-centric migration for .pm/ directories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { parseFrontMatter } from './parser-core.js';
|
|
11
|
+
|
|
12
|
+
/** Check whether a .pm/ directory uses the legacy flat format (has backlog/ at root, no boards/). */
|
|
13
|
+
export function isLegacyFormat(pmDir: string): boolean {
|
|
14
|
+
return existsSync(join(pmDir, 'backlog')) && !existsSync(join(pmDir, 'boards'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readFileIfExists(path: string): string | null {
|
|
18
|
+
try {
|
|
19
|
+
if (existsSync(path)) return readFileSync(path, 'utf-8');
|
|
20
|
+
} catch { /* skip */ }
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Move all files from a legacy directory into a board subdirectory and remove the source. */
|
|
25
|
+
function moveLegacyDir(srcDir: string, destDir: string): void {
|
|
26
|
+
if (!existsSync(srcDir)) return;
|
|
27
|
+
for (const file of readdirSync(srcDir)) {
|
|
28
|
+
renameSync(join(srcDir, file), join(destDir, file));
|
|
29
|
+
}
|
|
30
|
+
rmSync(srcDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Move a single file if it exists. */
|
|
34
|
+
function moveLegacyFile(src: string, dest: string): void {
|
|
35
|
+
if (existsSync(src)) renameSync(src, dest);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Copy review files from sprint sandbox directories into the board reviews dir. */
|
|
39
|
+
function copySprintReviews(sprintsDir: string, boardReviewsDir: string): void {
|
|
40
|
+
for (const entry of readdirSync(sprintsDir)) {
|
|
41
|
+
if (entry.endsWith('.md')) continue;
|
|
42
|
+
const reviewsDir = join(sprintsDir, entry, 'reviews');
|
|
43
|
+
if (!existsSync(reviewsDir)) continue;
|
|
44
|
+
for (const reviewFile of readdirSync(reviewsDir)) {
|
|
45
|
+
cpSync(join(reviewsDir, reviewFile), join(boardReviewsDir, reviewFile));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Find and return the goal from the active sprint .md file. */
|
|
51
|
+
function extractActiveSprintGoal(sprintsDir: string): string {
|
|
52
|
+
for (const entry of readdirSync(sprintsDir).filter(e => e.endsWith('.md'))) {
|
|
53
|
+
const content = readFileIfExists(join(sprintsDir, entry));
|
|
54
|
+
if (!content) continue;
|
|
55
|
+
const fm = parseFrontMatter(content).frontMatter;
|
|
56
|
+
if (fm.status === 'active') return String(fm.goal || '');
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Migrate active sprint data into the board and archive sprints dir. */
|
|
62
|
+
function migrateLegacySprints(sprintsDir: string, boardReviewsDir: string): string {
|
|
63
|
+
if (!existsSync(sprintsDir)) return '';
|
|
64
|
+
const goal = extractActiveSprintGoal(sprintsDir);
|
|
65
|
+
copySprintReviews(sprintsDir, boardReviewsDir);
|
|
66
|
+
return goal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Remove issues from the board backlog that are already tracked via paths in STATE.md */
|
|
70
|
+
function cleanupMigratedIssues(boardBacklogDir: string): boolean {
|
|
71
|
+
let hasActive = false;
|
|
72
|
+
for (const f of readdirSync(boardBacklogDir).filter(f => f.endsWith('.md'))) {
|
|
73
|
+
const content = readFileIfExists(join(boardBacklogDir, f));
|
|
74
|
+
if (!content) continue;
|
|
75
|
+
const fm = parseFrontMatter(content).frontMatter;
|
|
76
|
+
const status = String(fm.status || 'backlog');
|
|
77
|
+
if (status === 'done' || status === 'closed' || status === 'cancelled') {
|
|
78
|
+
rmSync(join(boardBacklogDir, f));
|
|
79
|
+
} else if (status !== 'backlog') {
|
|
80
|
+
hasActive = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return hasActive;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Write the board.md metadata file and optionally a STATE.md for active boards. */
|
|
87
|
+
function writeBoardMetadata(pmDir: string, boardDir: string, boardId: string, sprintGoal: string, hasActive: boolean): void {
|
|
88
|
+
const today = new Date().toISOString().split('T')[0];
|
|
89
|
+
const goal = sprintGoal || 'Migrated board';
|
|
90
|
+
writeFileSync(join(boardDir, 'board.md'), [
|
|
91
|
+
'---', `id: ${boardId}`, `title: Board 1`, `status: ${hasActive ? 'active' : 'draft'}`,
|
|
92
|
+
`created: ${today}`, `goal: ${goal}`, '---', '', `# Board 1`, '', `## Goal`, '', goal, '',
|
|
93
|
+
].join('\n'));
|
|
94
|
+
|
|
95
|
+
// Copy existing STATE.md into the board directory
|
|
96
|
+
const legacyState = readFileIfExists(join(pmDir, 'STATE.md'));
|
|
97
|
+
if (legacyState) {
|
|
98
|
+
writeFileSync(join(boardDir, 'STATE.md'), legacyState);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Write workspace.json pointing at the new board
|
|
102
|
+
writeFileSync(join(pmDir, 'workspace.json'), JSON.stringify({
|
|
103
|
+
activeBoardId: boardId, boardOrder: [boardId],
|
|
104
|
+
}, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Migrate a legacy flat .pm/ directory to the board-centric format. */
|
|
108
|
+
export function migrateToBoards(pmDir: string): void {
|
|
109
|
+
const boardId = 'BOARD-001';
|
|
110
|
+
const boardDir = join(pmDir, 'boards', boardId);
|
|
111
|
+
mkdirSync(join(boardDir, 'backlog'), { recursive: true });
|
|
112
|
+
mkdirSync(join(boardDir, 'reviews'), { recursive: true });
|
|
113
|
+
mkdirSync(join(boardDir, 'out'), { recursive: true });
|
|
114
|
+
|
|
115
|
+
moveLegacyDir(join(pmDir, 'backlog'), join(boardDir, 'backlog'));
|
|
116
|
+
moveLegacyFile(join(pmDir, 'progress.md'), join(boardDir, 'progress.md'));
|
|
117
|
+
|
|
118
|
+
const boardReviewsDir = join(boardDir, 'reviews');
|
|
119
|
+
const sprintGoal = migrateLegacySprints(join(pmDir, 'sprints'), boardReviewsDir);
|
|
120
|
+
const hasActive = cleanupMigratedIssues(join(boardDir, 'backlog'));
|
|
121
|
+
writeBoardMetadata(pmDir, boardDir, boardId, sprintGoal, hasActive);
|
|
122
|
+
|
|
123
|
+
// Remove migrated top-level items (STATE.md is now inside the board)
|
|
124
|
+
for (const f of ['STATE.md']) {
|
|
125
|
+
const p = join(pmDir, f);
|
|
126
|
+
if (existsSync(p)) rmSync(p);
|
|
127
|
+
}
|
|
128
|
+
}
|