mstro-app 0.4.3 → 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/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 -807
- 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/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.map +1 -1
- package/dist/server/services/plan/composer.js +2 -1
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- 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 +0 -8
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +50 -569
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts +2 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +2 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -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 +10 -119
- 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 -126
- 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 +6 -925
- 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 -1
- 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 -1096
- 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/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 +2 -1
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/parser-core.ts +406 -0
- package/server/services/plan/parser-migration.ts +128 -0
- package/server/services/plan/parser.ts +52 -620
- package/server/services/plan/review-gate.ts +4 -2
- package/server/services/plan/types.ts +2 -0
- package/server/services/platform-credentials.ts +83 -0
- package/server/services/platform.ts +15 -141
- 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 -126
- 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 +8 -1114
- 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 +17 -0
|
@@ -2,591 +2,38 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* PPS Parser —
|
|
5
|
+
* PPS Parser — Public API for reading .pm/ (or legacy .plan/) directories.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Entity parsing lives in parser-core.ts; migration in parser-migration.ts.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
+
import { parseBoard, parseIssue, parseMilestone, parseProjectConfig, parseProjectState, parseSprint, parseWorkspace } from './parser-core.js';
|
|
13
|
+
import { isLegacyFormat, migrateToBoards } from './parser-migration.js';
|
|
12
14
|
import type {
|
|
13
|
-
AcceptanceCriterion,
|
|
14
15
|
Board,
|
|
15
16
|
BoardArtifacts,
|
|
16
|
-
BoardExecutionSummary,
|
|
17
17
|
BoardFullState,
|
|
18
18
|
Issue,
|
|
19
|
-
IssueSummary,
|
|
20
19
|
Milestone,
|
|
21
|
-
MilestoneEpicSummary,
|
|
22
20
|
PlanFullState,
|
|
23
21
|
ProjectConfig,
|
|
24
22
|
ProjectState,
|
|
25
23
|
ReviewResult,
|
|
26
24
|
Sprint,
|
|
27
25
|
SprintArtifacts,
|
|
28
|
-
SprintExecutionSummary,
|
|
29
|
-
SprintIssueSummary,
|
|
30
|
-
Team,
|
|
31
|
-
WorkflowStatus,
|
|
32
26
|
Workspace,
|
|
33
27
|
} from './types.js';
|
|
34
28
|
|
|
35
29
|
// ============================================================================
|
|
36
|
-
//
|
|
30
|
+
// Directory Resolution
|
|
37
31
|
// ============================================================================
|
|
38
32
|
|
|
39
|
-
interface ParsedFile {
|
|
40
|
-
frontMatter: Record<string, unknown>;
|
|
41
|
-
body: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function stripQuotes(v: string): string {
|
|
45
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
46
|
-
return v.slice(1, -1);
|
|
47
|
-
}
|
|
48
|
-
return v;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseYamlValue(v: string): unknown {
|
|
52
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
53
|
-
return v.slice(1, -1);
|
|
54
|
-
}
|
|
55
|
-
if (v.startsWith('[') && v.endsWith(']')) {
|
|
56
|
-
return v.slice(1, -1).split(',').map(s => stripQuotes(s.trim())).filter(Boolean);
|
|
57
|
-
}
|
|
58
|
-
if (v === 'true') return true;
|
|
59
|
-
if (v === 'false') return false;
|
|
60
|
-
if (v === 'null' || v === '~' || v === '') return null;
|
|
61
|
-
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
62
|
-
return v;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Consume indented YAML list items starting after the current index. Returns [items, newIndex]. */
|
|
66
|
-
function consumeIndentedList(lines: string[], startIdx: number): [string[], number] {
|
|
67
|
-
const items: string[] = [];
|
|
68
|
-
let i = startIdx;
|
|
69
|
-
while (i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1])) {
|
|
70
|
-
i++;
|
|
71
|
-
const item = lines[i].trim().replace(/^-\s+/, '');
|
|
72
|
-
items.push(stripQuotes(item));
|
|
73
|
-
}
|
|
74
|
-
return [items, i];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function parseFrontMatter(content: string): ParsedFile {
|
|
78
|
-
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
79
|
-
if (!match) {
|
|
80
|
-
return { frontMatter: {}, body: content };
|
|
81
|
-
}
|
|
82
|
-
const frontMatter: Record<string, unknown> = {};
|
|
83
|
-
const lines = match[1].split('\n');
|
|
84
|
-
|
|
85
|
-
for (let i = 0; i < lines.length; i++) {
|
|
86
|
-
const trimmed = lines[i].trim();
|
|
87
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
88
|
-
const colonIdx = trimmed.indexOf(':');
|
|
89
|
-
if (colonIdx === -1) continue;
|
|
90
|
-
|
|
91
|
-
const key = trimmed.slice(0, colonIdx).trim();
|
|
92
|
-
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
93
|
-
|
|
94
|
-
if (!rawValue) {
|
|
95
|
-
const [items, newIdx] = consumeIndentedList(lines, i);
|
|
96
|
-
i = newIdx;
|
|
97
|
-
frontMatter[key] = items.length > 0 ? items : null;
|
|
98
|
-
} else {
|
|
99
|
-
frontMatter[key] = parseYamlValue(rawValue);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { frontMatter, body: match[2] };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ============================================================================
|
|
107
|
-
// Section Extraction
|
|
108
|
-
// ============================================================================
|
|
109
|
-
|
|
110
|
-
function extractSections(body: string): Map<string, string> {
|
|
111
|
-
const sections = new Map<string, string>();
|
|
112
|
-
const lines = body.split('\n');
|
|
113
|
-
let currentSection = '';
|
|
114
|
-
let currentContent: string[] = [];
|
|
115
|
-
|
|
116
|
-
for (const line of lines) {
|
|
117
|
-
if (line.startsWith('## ')) {
|
|
118
|
-
if (currentSection) {
|
|
119
|
-
sections.set(currentSection, currentContent.join('\n').trim());
|
|
120
|
-
}
|
|
121
|
-
currentSection = line.slice(3).trim();
|
|
122
|
-
currentContent = [];
|
|
123
|
-
} else {
|
|
124
|
-
currentContent.push(line);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (currentSection) {
|
|
128
|
-
sections.set(currentSection, currentContent.join('\n').trim());
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return sections;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function parseCheckboxes(content: string): AcceptanceCriterion[] {
|
|
135
|
-
const items: AcceptanceCriterion[] = [];
|
|
136
|
-
for (const line of content.split('\n')) {
|
|
137
|
-
const match = line.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
138
|
-
if (match) {
|
|
139
|
-
items.push({ text: match[2].trim(), checked: match[1] !== ' ' });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return items;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function parseListItems(content: string): string[] {
|
|
146
|
-
const items: string[] = [];
|
|
147
|
-
for (const line of content.split('\n')) {
|
|
148
|
-
const match = line.match(/^[-*]\s+(.+)$/);
|
|
149
|
-
if (match) items.push(match[1].trim());
|
|
150
|
-
}
|
|
151
|
-
return items;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function parseIssueSummaries(content: string): IssueSummary[] {
|
|
155
|
-
const summaries: IssueSummary[] = [];
|
|
156
|
-
for (const line of content.split('\n')) {
|
|
157
|
-
// Match: 1. [IS-003](backlog/IS-003.md) — Title (P1)
|
|
158
|
-
const match = line.match(/\d+\.\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*\((\w+)\))?\s*$/);
|
|
159
|
-
if (match) {
|
|
160
|
-
summaries.push({
|
|
161
|
-
id: match[1],
|
|
162
|
-
path: match[2],
|
|
163
|
-
title: match[3].trim(),
|
|
164
|
-
priority: match[4] || '',
|
|
165
|
-
});
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// Match: - [IS-001](backlog/IS-001.md) — Title
|
|
169
|
-
const match2 = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*[→→]\s*blocked by\s+\[([^\]]+)\])?\s*$/i);
|
|
170
|
-
if (match2) {
|
|
171
|
-
summaries.push({
|
|
172
|
-
id: match2[1],
|
|
173
|
-
path: match2[2],
|
|
174
|
-
title: match2[3].trim(),
|
|
175
|
-
priority: '',
|
|
176
|
-
blockedBy: match2[4] || undefined,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return summaries;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function parseCompletedSummaries(content: string): IssueSummary[] {
|
|
184
|
-
const summaries: IssueSummary[] = [];
|
|
185
|
-
for (const line of content.split('\n')) {
|
|
186
|
-
const match = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*✓)?\s*$/);
|
|
187
|
-
if (match) {
|
|
188
|
-
summaries.push({
|
|
189
|
-
id: match[1],
|
|
190
|
-
path: match[2],
|
|
191
|
-
title: match[3].trim(),
|
|
192
|
-
priority: '',
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return summaries;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ============================================================================
|
|
200
|
-
// Entity Parsers
|
|
201
|
-
// ============================================================================
|
|
202
|
-
|
|
203
|
-
function parseWorkflows(section: string | undefined): WorkflowStatus[] {
|
|
204
|
-
if (!section) return [];
|
|
205
|
-
const workflows: WorkflowStatus[] = [];
|
|
206
|
-
for (const line of section.split('\n')) {
|
|
207
|
-
const match = line.match(/\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|/);
|
|
208
|
-
if (match && match[1] !== 'Status') {
|
|
209
|
-
workflows.push({
|
|
210
|
-
status: match[1],
|
|
211
|
-
category: match[2] as WorkflowStatus['category'],
|
|
212
|
-
description: match[3].trim(),
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return workflows;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function parseTeams(section: string | undefined): Team[] {
|
|
220
|
-
if (!section) return [];
|
|
221
|
-
const teams: Team[] = [];
|
|
222
|
-
for (const line of section.split('\n')) {
|
|
223
|
-
const match = line.match(/^[-*]\s+(\w+)(?:\s*[—–-]\s*(.+))?$/);
|
|
224
|
-
if (match) teams.push({ name: match[1], description: match[2]?.trim() });
|
|
225
|
-
}
|
|
226
|
-
return teams;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function parseProjectConfig(content: string): ProjectConfig {
|
|
230
|
-
const { frontMatter, body } = parseFrontMatter(content);
|
|
231
|
-
const sections = extractSections(body);
|
|
232
|
-
|
|
233
|
-
const idPrefixes: Record<string, string> = {};
|
|
234
|
-
const rawPrefixes = frontMatter.id_prefixes;
|
|
235
|
-
if (rawPrefixes && typeof rawPrefixes === 'object') {
|
|
236
|
-
Object.assign(idPrefixes, rawPrefixes);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
name: String(frontMatter.name || ''),
|
|
241
|
-
id: String(frontMatter.id || ''),
|
|
242
|
-
created: String(frontMatter.created || ''),
|
|
243
|
-
status: (frontMatter.status as ProjectConfig['status']) || 'active',
|
|
244
|
-
estimation: (frontMatter.estimation as ProjectConfig['estimation']) || 'none',
|
|
245
|
-
idPrefixes,
|
|
246
|
-
workflows: parseWorkflows(sections.get('Workflows')),
|
|
247
|
-
labels: (Array.isArray(frontMatter.labels) ? frontMatter.labels : []) as string[],
|
|
248
|
-
teams: parseTeams(sections.get('Teams')),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function parseProjectState(content: string): ProjectState {
|
|
253
|
-
const { frontMatter, body } = parseFrontMatter(content);
|
|
254
|
-
const sections = extractSections(body);
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
project: String(frontMatter.project || ''),
|
|
258
|
-
currentSprint: (frontMatter.current_sprint as string) || null,
|
|
259
|
-
activeMilestone: (frontMatter.active_milestone as string) || null,
|
|
260
|
-
paused: frontMatter.paused === true,
|
|
261
|
-
lastSession: (frontMatter.last_session as string) || null,
|
|
262
|
-
readyToWork: parseIssueSummaries(sections.get('Ready to Work') || ''),
|
|
263
|
-
inProgress: parseIssueSummaries(sections.get('In Progress') || ''),
|
|
264
|
-
blocked: parseIssueSummaries(sections.get('Blocked') || ''),
|
|
265
|
-
recentlyCompleted: parseCompletedSummaries(sections.get('Recently Completed') || ''),
|
|
266
|
-
warnings: parseListItems(sections.get('Warnings') || ''),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function toStringArray(val: unknown): string[] {
|
|
271
|
-
return Array.isArray(val) ? val.map(String) : [];
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function optionalString(val: unknown): string | null {
|
|
275
|
-
if (val == null) return null;
|
|
276
|
-
const s = String(val);
|
|
277
|
-
return s === '' ? null : s;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function parseIssue(content: string, filePath: string): Issue {
|
|
281
|
-
const { frontMatter: fm, body } = parseFrontMatter(content);
|
|
282
|
-
const sections = extractSections(body);
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
id: String(fm.id || ''),
|
|
286
|
-
title: String(fm.title || ''),
|
|
287
|
-
type: (fm.type as Issue['type']) || 'issue',
|
|
288
|
-
status: String(fm.status || 'backlog'),
|
|
289
|
-
priority: String(fm.priority || 'P2'),
|
|
290
|
-
estimate: fm.estimate != null ? fm.estimate as number | string : null,
|
|
291
|
-
labels: toStringArray(fm.labels),
|
|
292
|
-
epic: optionalString(fm.epic),
|
|
293
|
-
sprint: optionalString(fm.sprint),
|
|
294
|
-
milestone: optionalString(fm.milestone),
|
|
295
|
-
assigned: optionalString(fm.assigned),
|
|
296
|
-
created: String(fm.created || ''),
|
|
297
|
-
updated: optionalString(fm.updated),
|
|
298
|
-
due: optionalString(fm.due),
|
|
299
|
-
blockedBy: toStringArray(fm.blocked_by),
|
|
300
|
-
blocks: toStringArray(fm.blocks),
|
|
301
|
-
relatesTo: toStringArray(fm.relates_to),
|
|
302
|
-
children: toStringArray(fm.children),
|
|
303
|
-
progress: optionalString(fm.progress),
|
|
304
|
-
description: sections.get('Description') || '',
|
|
305
|
-
acceptanceCriteria: parseCheckboxes(sections.get('Acceptance Criteria') || ''),
|
|
306
|
-
technicalNotes: sections.get('Technical Notes') || null,
|
|
307
|
-
filesToModify: parseListItems(sections.get('Files to Modify') || ''),
|
|
308
|
-
activity: parseListItems(sections.get('Activity') || ''),
|
|
309
|
-
reviewGate: (['none', 'auto', 'required'].includes(String(fm.review_gate)) ? String(fm.review_gate) : 'auto') as Issue['reviewGate'],
|
|
310
|
-
outputFile: optionalString(fm.output_file),
|
|
311
|
-
body,
|
|
312
|
-
path: filePath,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function parseSprintIssues(section: string | undefined): SprintIssueSummary[] {
|
|
317
|
-
if (!section) return [];
|
|
318
|
-
const issues: SprintIssueSummary[] = [];
|
|
319
|
-
for (const line of section.split('\n')) {
|
|
320
|
-
const match = line.match(/\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|/);
|
|
321
|
-
if (match) {
|
|
322
|
-
issues.push({
|
|
323
|
-
id: match[1],
|
|
324
|
-
path: match[2],
|
|
325
|
-
title: match[3].trim(),
|
|
326
|
-
points: /^\d+$/.test(match[4]) ? Number(match[4]) : match[4],
|
|
327
|
-
status: match[5],
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return issues;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function optionalNumber(val: unknown): number | null {
|
|
335
|
-
return val != null ? Number(val) : null;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function parseSprint(content: string, filePath: string): Sprint {
|
|
339
|
-
const { frontMatter: fm, body } = parseFrontMatter(content);
|
|
340
|
-
const sections = extractSections(body);
|
|
341
|
-
|
|
342
|
-
// Table-based parsing (markdown links in table rows)
|
|
343
|
-
let issues = parseSprintIssues(sections.get('Issues'));
|
|
344
|
-
|
|
345
|
-
// Fallback: front matter issues array (e.g., ["backlog/IS-001.md", ...])
|
|
346
|
-
if (issues.length === 0 && Array.isArray(fm.issues)) {
|
|
347
|
-
issues = (fm.issues as string[]).map(path => {
|
|
348
|
-
const id = path.replace(/^backlog\//, '').replace(/\.md$/, '');
|
|
349
|
-
return { id, path, title: '', points: null, status: '' };
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Parse execution_summary if present (JSON object in front matter)
|
|
354
|
-
let executionSummary: SprintExecutionSummary | null = null;
|
|
355
|
-
if (fm.execution_summary && typeof fm.execution_summary === 'object') {
|
|
356
|
-
const es = fm.execution_summary as Record<string, unknown>;
|
|
357
|
-
executionSummary = {
|
|
358
|
-
totalIssues: Number(es.total_issues ?? 0),
|
|
359
|
-
completedIssues: Number(es.completed_issues ?? 0),
|
|
360
|
-
failedIssues: Number(es.failed_issues ?? 0),
|
|
361
|
-
totalDuration: Number(es.total_duration ?? 0),
|
|
362
|
-
waves: Number(es.waves ?? 0),
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
id: String(fm.id || ''),
|
|
368
|
-
title: String(fm.title || ''),
|
|
369
|
-
status: (fm.status as Sprint['status']) || 'planned',
|
|
370
|
-
start: String(fm.start || fm.start_date || ''),
|
|
371
|
-
end: String(fm.end || fm.end_date || ''),
|
|
372
|
-
goal: String(fm.goal || sections.get('Goal') || sections.get('Sprint Goal') || ''),
|
|
373
|
-
capacity: optionalNumber(fm.capacity),
|
|
374
|
-
committed: optionalNumber(fm.committed),
|
|
375
|
-
completed: optionalNumber(fm.completed),
|
|
376
|
-
issues,
|
|
377
|
-
path: filePath,
|
|
378
|
-
completedAt: optionalString(fm.completed_at),
|
|
379
|
-
executionSummary,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function parseMilestone(content: string, filePath: string): Milestone {
|
|
384
|
-
const { frontMatter, body } = parseFrontMatter(content);
|
|
385
|
-
const sections = extractSections(body);
|
|
386
|
-
|
|
387
|
-
const epics: MilestoneEpicSummary[] = [];
|
|
388
|
-
const epicSection = sections.get('Epics');
|
|
389
|
-
if (epicSection) {
|
|
390
|
-
for (const line of epicSection.split('\n')) {
|
|
391
|
-
const match = line.match(/\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|/);
|
|
392
|
-
if (match) {
|
|
393
|
-
epics.push({
|
|
394
|
-
id: match[1],
|
|
395
|
-
path: match[2],
|
|
396
|
-
title: match[3].trim(),
|
|
397
|
-
progress: match[4],
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
id: String(frontMatter.id || ''),
|
|
405
|
-
title: String(frontMatter.title || ''),
|
|
406
|
-
status: (frontMatter.status as Milestone['status']) || 'planned',
|
|
407
|
-
targetDate: (frontMatter.target_date as string) || null,
|
|
408
|
-
progress: (frontMatter.progress as string) || null,
|
|
409
|
-
definition: sections.get('Definition of Done') || '',
|
|
410
|
-
epics,
|
|
411
|
-
path: filePath,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ============================================================================
|
|
416
|
-
// Board Parser
|
|
417
|
-
// ============================================================================
|
|
418
|
-
|
|
419
|
-
function parseBoard(content: string, filePath: string): Board {
|
|
420
|
-
const { frontMatter: fm, body } = parseFrontMatter(content);
|
|
421
|
-
const sections = extractSections(body);
|
|
422
|
-
|
|
423
|
-
let executionSummary: BoardExecutionSummary | null = null;
|
|
424
|
-
if (fm.execution_summary && typeof fm.execution_summary === 'object') {
|
|
425
|
-
const es = fm.execution_summary as Record<string, unknown>;
|
|
426
|
-
executionSummary = {
|
|
427
|
-
totalIssues: Number(es.total_issues ?? 0),
|
|
428
|
-
completedIssues: Number(es.completed_issues ?? 0),
|
|
429
|
-
failedIssues: Number(es.failed_issues ?? 0),
|
|
430
|
-
totalDuration: Number(es.total_duration ?? 0),
|
|
431
|
-
waves: Number(es.waves ?? 0),
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
id: String(fm.id || ''),
|
|
437
|
-
title: String(fm.title || ''),
|
|
438
|
-
status: (fm.status as Board['status']) || 'draft',
|
|
439
|
-
created: String(fm.created || ''),
|
|
440
|
-
completedAt: optionalString(fm.completed_at),
|
|
441
|
-
goal: String(fm.goal || sections.get('Goal') || ''),
|
|
442
|
-
executionSummary,
|
|
443
|
-
path: filePath,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function parseWorkspace(content: string): Workspace {
|
|
448
|
-
try {
|
|
449
|
-
const parsed = JSON.parse(content) as Record<string, unknown>;
|
|
450
|
-
return {
|
|
451
|
-
activeBoardId: typeof parsed.activeBoardId === 'string' ? parsed.activeBoardId : null,
|
|
452
|
-
boardOrder: Array.isArray(parsed.boardOrder) ? parsed.boardOrder.map(String) : [],
|
|
453
|
-
};
|
|
454
|
-
} catch {
|
|
455
|
-
return { activeBoardId: null, boardOrder: [] };
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/** Check whether a .pm/ directory uses the board-centric format (has boards/ subdirectory). */
|
|
460
33
|
export function isBoardCentricFormat(pmDir: string): boolean {
|
|
461
34
|
return existsSync(join(pmDir, 'boards'));
|
|
462
35
|
}
|
|
463
36
|
|
|
464
|
-
/** Check whether a .pm/ directory uses the legacy flat format (has backlog/ at root, no boards/). */
|
|
465
|
-
function isLegacyFormat(pmDir: string): boolean {
|
|
466
|
-
return existsSync(join(pmDir, 'backlog')) && !existsSync(join(pmDir, 'boards'));
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// ============================================================================
|
|
470
|
-
// Legacy → Board Migration
|
|
471
|
-
// ============================================================================
|
|
472
|
-
|
|
473
|
-
/** Move all files from a legacy directory into a board subdirectory and remove the source. */
|
|
474
|
-
function moveLegacyDir(srcDir: string, destDir: string): void {
|
|
475
|
-
if (!existsSync(srcDir)) return;
|
|
476
|
-
for (const file of readdirSync(srcDir)) {
|
|
477
|
-
renameSync(join(srcDir, file), join(destDir, file));
|
|
478
|
-
}
|
|
479
|
-
rmSync(srcDir, { recursive: true });
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** Move a single file if it exists. */
|
|
483
|
-
function moveLegacyFile(src: string, dest: string): void {
|
|
484
|
-
if (existsSync(src)) renameSync(src, dest);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/** Copy review files from sprint sandbox directories into the board reviews dir. */
|
|
488
|
-
function copySprintReviews(sprintsDir: string, boardReviewsDir: string): void {
|
|
489
|
-
for (const entry of readdirSync(sprintsDir)) {
|
|
490
|
-
if (entry.endsWith('.md')) continue;
|
|
491
|
-
const reviewsDir = join(sprintsDir, entry, 'reviews');
|
|
492
|
-
if (!existsSync(reviewsDir)) continue;
|
|
493
|
-
for (const reviewFile of readdirSync(reviewsDir)) {
|
|
494
|
-
cpSync(join(reviewsDir, reviewFile), join(boardReviewsDir, reviewFile));
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/** Find and return the goal from the active sprint .md file. */
|
|
500
|
-
function extractActiveSprintGoal(sprintsDir: string): string {
|
|
501
|
-
for (const entry of readdirSync(sprintsDir).filter(e => e.endsWith('.md'))) {
|
|
502
|
-
const content = readFileIfExists(join(sprintsDir, entry));
|
|
503
|
-
if (!content) continue;
|
|
504
|
-
const fm = parseFrontMatter(content).frontMatter;
|
|
505
|
-
if (fm.status === 'active') return String(fm.goal || '');
|
|
506
|
-
}
|
|
507
|
-
return '';
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/** Migrate sprint reviews and extract the active sprint's goal. */
|
|
511
|
-
function migrateLegacySprints(sprintsDir: string, boardReviewsDir: string): string {
|
|
512
|
-
if (!existsSync(sprintsDir)) return '';
|
|
513
|
-
copySprintReviews(sprintsDir, boardReviewsDir);
|
|
514
|
-
const goal = extractActiveSprintGoal(sprintsDir);
|
|
515
|
-
rmSync(sprintsDir, { recursive: true });
|
|
516
|
-
return goal;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/** Clean up migrated issues: remove sprint fields, detect active issues. */
|
|
520
|
-
function cleanupMigratedIssues(boardBacklogDir: string): boolean {
|
|
521
|
-
if (!existsSync(boardBacklogDir)) return false;
|
|
522
|
-
let hasActive = false;
|
|
523
|
-
|
|
524
|
-
for (const file of readdirSync(boardBacklogDir).filter(f => f.endsWith('.md'))) {
|
|
525
|
-
const content = readFileIfExists(join(boardBacklogDir, file));
|
|
526
|
-
if (!content) continue;
|
|
527
|
-
if (content.match(/^status:\s*(in_progress|in_review|todo)/m)) hasActive = true;
|
|
528
|
-
if (content.match(/^sprint:\s*.+$/m)) {
|
|
529
|
-
writeFileSync(join(boardBacklogDir, file), content.replace(/^sprint:\s*.+\n?/m, ''), 'utf-8');
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
return hasActive;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/** Write the board metadata files (board.md, workspace.json, STATE.md, progress.md). */
|
|
536
|
-
function writeBoardMetadata(pmDir: string, boardDir: string, boardId: string, sprintGoal: string, hasActive: boolean): void {
|
|
537
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
538
|
-
const boardMd = [
|
|
539
|
-
'---', `id: ${boardId}`, 'title: "Board 1"',
|
|
540
|
-
`status: ${hasActive ? 'active' : 'draft'}`, `created: "${today}"`,
|
|
541
|
-
'completed_at: null', `goal: "${sprintGoal.replace(/"/g, '\\"')}"`,
|
|
542
|
-
'---', '', '# Board 1', '',
|
|
543
|
-
sprintGoal ? `## Goal\n${sprintGoal}\n` : '',
|
|
544
|
-
].join('\n');
|
|
545
|
-
writeFileSync(join(boardDir, 'board.md'), boardMd, 'utf-8');
|
|
546
|
-
|
|
547
|
-
const workspace: Workspace = { activeBoardId: boardId, boardOrder: [boardId] };
|
|
548
|
-
writeFileSync(join(pmDir, 'workspace.json'), JSON.stringify(workspace, null, 2), 'utf-8');
|
|
549
|
-
|
|
550
|
-
if (!existsSync(join(boardDir, 'STATE.md'))) {
|
|
551
|
-
writeFileSync(join(boardDir, 'STATE.md'), [
|
|
552
|
-
'---', 'project: ../../project.md', 'board: board.md', 'paused: false', '---', '',
|
|
553
|
-
'# Board State', '', '## Ready to Work', '', '## In Progress', '',
|
|
554
|
-
'## Blocked', '', '## Recently Completed', '', '## Warnings', '',
|
|
555
|
-
].join('\n'), 'utf-8');
|
|
556
|
-
}
|
|
557
|
-
if (!existsSync(join(boardDir, 'progress.md'))) {
|
|
558
|
-
writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Migrate a legacy flat .pm/ directory to board-centric format.
|
|
564
|
-
* Creates BOARD-001 from the existing backlog, state, outputs, and reviews.
|
|
565
|
-
*/
|
|
566
|
-
function migrateToBoards(pmDir: string): void {
|
|
567
|
-
const boardId = 'BOARD-001';
|
|
568
|
-
const boardDir = join(pmDir, 'boards', boardId);
|
|
569
|
-
|
|
570
|
-
mkdirSync(boardDir, { recursive: true });
|
|
571
|
-
mkdirSync(join(boardDir, 'backlog'), { recursive: true });
|
|
572
|
-
mkdirSync(join(boardDir, 'out'), { recursive: true });
|
|
573
|
-
mkdirSync(join(boardDir, 'reviews'), { recursive: true });
|
|
574
|
-
|
|
575
|
-
moveLegacyDir(join(pmDir, 'backlog'), join(boardDir, 'backlog'));
|
|
576
|
-
moveLegacyFile(join(pmDir, 'STATE.md'), join(boardDir, 'STATE.md'));
|
|
577
|
-
moveLegacyDir(join(pmDir, 'out'), join(boardDir, 'out'));
|
|
578
|
-
moveLegacyFile(join(pmDir, 'progress.md'), join(boardDir, 'progress.md'));
|
|
579
|
-
|
|
580
|
-
const sprintGoal = migrateLegacySprints(join(pmDir, 'sprints'), join(boardDir, 'reviews'));
|
|
581
|
-
const hasActive = cleanupMigratedIssues(join(boardDir, 'backlog'));
|
|
582
|
-
writeBoardMetadata(pmDir, boardDir, boardId, sprintGoal, hasActive);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// ============================================================================
|
|
586
|
-
// Directory Parser
|
|
587
|
-
// ============================================================================
|
|
588
|
-
|
|
589
|
-
/** Resolve the PM directory — prefers .mstro/pm/, falls back to legacy .pm/ and .plan/ */
|
|
590
37
|
export function resolvePmDir(workingDir: string): string | null {
|
|
591
38
|
const mstroPmDir = join(workingDir, '.mstro', 'pm');
|
|
592
39
|
if (existsSync(mstroPmDir)) return mstroPmDir;
|
|
@@ -597,7 +44,6 @@ export function resolvePmDir(workingDir: string): string | null {
|
|
|
597
44
|
return null;
|
|
598
45
|
}
|
|
599
46
|
|
|
600
|
-
/** Default PM directory path for new projects */
|
|
601
47
|
export function defaultPmDir(workingDir: string): string {
|
|
602
48
|
return join(workingDir, '.mstro', 'pm');
|
|
603
49
|
}
|
|
@@ -606,6 +52,10 @@ export function planDirExists(workingDir: string): boolean {
|
|
|
606
52
|
return resolvePmDir(workingDir) !== null;
|
|
607
53
|
}
|
|
608
54
|
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// File Utilities
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
609
59
|
function readFileIfExists(path: string): string | null {
|
|
610
60
|
try {
|
|
611
61
|
if (existsSync(path)) return readFileSync(path, 'utf-8');
|
|
@@ -618,13 +68,30 @@ function readMdFilesInDir(dirPath: string): Array<{ name: string; content: strin
|
|
|
618
68
|
try {
|
|
619
69
|
return readdirSync(dirPath)
|
|
620
70
|
.filter(f => f.endsWith('.md'))
|
|
621
|
-
.map(name => {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
71
|
+
.map(name => ({ name, content: readFileSync(join(dirPath, name), 'utf-8') }));
|
|
72
|
+
} catch { return []; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function listDirFiles(dirPath: string, ext: string): string[] {
|
|
76
|
+
if (!existsSync(dirPath)) return [];
|
|
77
|
+
try {
|
|
78
|
+
return readdirSync(dirPath).filter(f => f.endsWith(ext));
|
|
625
79
|
} catch { return []; }
|
|
626
80
|
}
|
|
627
81
|
|
|
82
|
+
function readReviewResults(reviewsDir: string): ReviewResult[] {
|
|
83
|
+
const results: ReviewResult[] = [];
|
|
84
|
+
for (const f of listDirFiles(reviewsDir, '.json')) {
|
|
85
|
+
const content = readFileIfExists(join(reviewsDir, f));
|
|
86
|
+
if (content) results.push(JSON.parse(content) as ReviewResult);
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Defaults
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
628
95
|
const defaultProject: ProjectConfig = {
|
|
629
96
|
name: '', id: '', created: '', status: 'active', estimation: 'none',
|
|
630
97
|
idPrefixes: {}, workflows: [], labels: [], teams: [],
|
|
@@ -636,7 +103,10 @@ const defaultState: ProjectState = {
|
|
|
636
103
|
recentlyCompleted: [], warnings: [],
|
|
637
104
|
};
|
|
638
105
|
|
|
639
|
-
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Board & Plan Directory Parsing
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
640
110
|
export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullState | null {
|
|
641
111
|
const boardDir = join(pmDir, 'boards', boardId);
|
|
642
112
|
if (!existsSync(boardDir)) return null;
|
|
@@ -652,7 +122,6 @@ export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullSt
|
|
|
652
122
|
const boardPrefix = `boards/${boardId}/`;
|
|
653
123
|
const issues = issueFiles.map(f => {
|
|
654
124
|
const issue = parseIssue(f.content, `${boardPrefix}backlog/${f.name}`);
|
|
655
|
-
// Normalize blocked_by/blocks to full board-relative paths so dependency resolver matches
|
|
656
125
|
issue.blockedBy = issue.blockedBy.map(bp => bp.startsWith('boards/') ? bp : `${boardPrefix}${bp}`);
|
|
657
126
|
issue.blocks = issue.blocks.map(bp => bp.startsWith('boards/') ? bp : `${boardPrefix}${bp}`);
|
|
658
127
|
if (issue.epic && !issue.epic.startsWith('boards/')) issue.epic = `${boardPrefix}${issue.epic}`;
|
|
@@ -662,7 +131,6 @@ export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullSt
|
|
|
662
131
|
return { board, state, issues };
|
|
663
132
|
}
|
|
664
133
|
|
|
665
|
-
/** Parse all boards from the boards/ directory and resolve the active board. */
|
|
666
134
|
function parseBoardCentricState(planDir: string): { boards: Board[]; workspace: Workspace; activeBoard: BoardFullState | null } {
|
|
667
135
|
const workspaceContent = readFileIfExists(join(planDir, 'workspace.json'));
|
|
668
136
|
const workspace = workspaceContent ? parseWorkspace(workspaceContent) : { activeBoardId: null, boardOrder: [] };
|
|
@@ -710,11 +178,14 @@ export function parsePlanDirectory(workingDir: string): PlanFullState | null {
|
|
|
710
178
|
};
|
|
711
179
|
}
|
|
712
180
|
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Single Entity Parsers
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
713
185
|
export function parseSingleIssue(workingDir: string, issuePath: string): Issue | null {
|
|
714
186
|
const pmDir = resolvePmDir(workingDir);
|
|
715
187
|
if (!pmDir) return null;
|
|
716
|
-
const
|
|
717
|
-
const content = readFileIfExists(fullPath);
|
|
188
|
+
const content = readFileIfExists(join(pmDir, issuePath));
|
|
718
189
|
if (!content) return null;
|
|
719
190
|
return parseIssue(content, issuePath);
|
|
720
191
|
}
|
|
@@ -722,8 +193,7 @@ export function parseSingleIssue(workingDir: string, issuePath: string): Issue |
|
|
|
722
193
|
export function parseSingleSprint(workingDir: string, sprintPath: string): Sprint | null {
|
|
723
194
|
const pmDir = resolvePmDir(workingDir);
|
|
724
195
|
if (!pmDir) return null;
|
|
725
|
-
const
|
|
726
|
-
const content = readFileIfExists(fullPath);
|
|
196
|
+
const content = readFileIfExists(join(pmDir, sprintPath));
|
|
727
197
|
if (!content) return null;
|
|
728
198
|
return parseSprint(content, sprintPath);
|
|
729
199
|
}
|
|
@@ -731,13 +201,15 @@ export function parseSingleSprint(workingDir: string, sprintPath: string): Sprin
|
|
|
731
201
|
export function parseSingleMilestone(workingDir: string, milestonePath: string): Milestone | null {
|
|
732
202
|
const pmDir = resolvePmDir(workingDir);
|
|
733
203
|
if (!pmDir) return null;
|
|
734
|
-
const
|
|
735
|
-
const content = readFileIfExists(fullPath);
|
|
204
|
+
const content = readFileIfExists(join(pmDir, milestonePath));
|
|
736
205
|
if (!content) return null;
|
|
737
206
|
return parseMilestone(content, milestonePath);
|
|
738
207
|
}
|
|
739
208
|
|
|
740
|
-
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// ID Generation
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
741
213
|
export function getNextId(issues: Issue[], prefix: string): string {
|
|
742
214
|
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
743
215
|
const pattern = new RegExp(`^${escaped}-(\\d+)$`);
|
|
@@ -752,7 +224,6 @@ export function getNextId(issues: Issue[], prefix: string): string {
|
|
|
752
224
|
return `${prefix}-${String(max + 1).padStart(3, '0')}`;
|
|
753
225
|
}
|
|
754
226
|
|
|
755
|
-
/** Compute the next available board ID (e.g., "BOARD-003") */
|
|
756
227
|
export function getNextBoardId(boards: Board[]): string {
|
|
757
228
|
let max = 0;
|
|
758
229
|
for (const board of boards) {
|
|
@@ -765,7 +236,6 @@ export function getNextBoardId(boards: Board[]): string {
|
|
|
765
236
|
return `BOARD-${String(max + 1).padStart(3, '0')}`;
|
|
766
237
|
}
|
|
767
238
|
|
|
768
|
-
/** Compute the next available board number for display title (e.g., "Board 3") */
|
|
769
239
|
export function getNextBoardNumber(boards: Board[]): number {
|
|
770
240
|
let max = 0;
|
|
771
241
|
for (const board of boards) {
|
|
@@ -778,7 +248,6 @@ export function getNextBoardNumber(boards: Board[]): number {
|
|
|
778
248
|
return max + 1;
|
|
779
249
|
}
|
|
780
250
|
|
|
781
|
-
/** Parse board artifacts from boards/BOARD-N/ directory. */
|
|
782
251
|
export function parseBoardArtifacts(workingDir: string, boardId: string): BoardArtifacts | null {
|
|
783
252
|
const pmDir = resolvePmDir(workingDir);
|
|
784
253
|
if (!pmDir) return null;
|
|
@@ -787,29 +256,11 @@ export function parseBoardArtifacts(workingDir: string, boardId: string): BoardA
|
|
|
787
256
|
if (!existsSync(boardDir)) return null;
|
|
788
257
|
|
|
789
258
|
const progressLog = readFileIfExists(join(boardDir, 'progress.md')) ?? '';
|
|
259
|
+
const outputFiles = listDirFiles(join(boardDir, 'out'), '.md');
|
|
260
|
+
const reviewResults = readReviewResults(join(boardDir, 'reviews'));
|
|
261
|
+
const executionLogs = listDirFiles(join(boardDir, 'logs'), '.log').sort();
|
|
790
262
|
|
|
791
|
-
|
|
792
|
-
let outputFiles: string[] = [];
|
|
793
|
-
if (existsSync(outDir)) {
|
|
794
|
-
try {
|
|
795
|
-
outputFiles = readdirSync(outDir).filter(f => f.endsWith('.md'));
|
|
796
|
-
} catch { /* skip */ }
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const reviewsDir = join(boardDir, 'reviews');
|
|
800
|
-
const reviewResults: ReviewResult[] = [];
|
|
801
|
-
if (existsSync(reviewsDir)) {
|
|
802
|
-
try {
|
|
803
|
-
for (const f of readdirSync(reviewsDir).filter(f => f.endsWith('.json'))) {
|
|
804
|
-
const content = readFileIfExists(join(reviewsDir, f));
|
|
805
|
-
if (content) {
|
|
806
|
-
reviewResults.push(JSON.parse(content) as ReviewResult);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
} catch { /* skip */ }
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
return { boardId, progressLog, outputFiles, reviewResults };
|
|
263
|
+
return { boardId, progressLog, outputFiles, reviewResults, executionLogs };
|
|
813
264
|
}
|
|
814
265
|
|
|
815
266
|
/** @deprecated Use getNextBoardId — kept for migration compatibility */
|
|
@@ -834,27 +285,8 @@ export function parseSprintArtifacts(workingDir: string, sprintId: string): Spri
|
|
|
834
285
|
if (!existsSync(sandboxDir)) return null;
|
|
835
286
|
|
|
836
287
|
const progressLog = readFileIfExists(join(sandboxDir, 'progress.md')) ?? '';
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
let outputFiles: string[] = [];
|
|
840
|
-
if (existsSync(outDir)) {
|
|
841
|
-
try {
|
|
842
|
-
outputFiles = readdirSync(outDir).filter(f => f.endsWith('.md'));
|
|
843
|
-
} catch { /* skip */ }
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const reviewsDir = join(sandboxDir, 'reviews');
|
|
847
|
-
const reviewResults: ReviewResult[] = [];
|
|
848
|
-
if (existsSync(reviewsDir)) {
|
|
849
|
-
try {
|
|
850
|
-
for (const f of readdirSync(reviewsDir).filter(f => f.endsWith('.json'))) {
|
|
851
|
-
const content = readFileIfExists(join(reviewsDir, f));
|
|
852
|
-
if (content) {
|
|
853
|
-
reviewResults.push(JSON.parse(content) as ReviewResult);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
} catch { /* skip */ }
|
|
857
|
-
}
|
|
288
|
+
const outputFiles = listDirFiles(join(sandboxDir, 'out'), '.md');
|
|
289
|
+
const reviewResults = readReviewResults(join(sandboxDir, 'reviews'));
|
|
858
290
|
|
|
859
291
|
return { sprintId, progressLog, outputFiles, reviewResults };
|
|
860
292
|
}
|