@ktpartners/dgs-platform 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +38 -0
- package/README.md +851 -0
- package/agents/dgs-codebase-cross-analyzer.md +183 -0
- package/agents/dgs-codebase-mapper.md +782 -0
- package/agents/dgs-codebase-synthesizer.md +156 -0
- package/agents/dgs-debugger.md +1256 -0
- package/agents/dgs-executor.md +550 -0
- package/agents/dgs-integration-checker.md +481 -0
- package/agents/dgs-nyquist-auditor.md +178 -0
- package/agents/dgs-phase-researcher.md +563 -0
- package/agents/dgs-phase-verifier.md +450 -0
- package/agents/dgs-plan-checker.md +708 -0
- package/agents/dgs-planner.md +1324 -0
- package/agents/dgs-project-researcher.md +631 -0
- package/agents/dgs-research-synthesizer.md +249 -0
- package/agents/dgs-roadmapper.md +652 -0
- package/agents/dgs-verifier.md +607 -0
- package/bin/install.js +2073 -0
- package/commands/dgs/add-doc.md +45 -0
- package/commands/dgs/add-idea.md +38 -0
- package/commands/dgs/add-phase.md +43 -0
- package/commands/dgs/add-repo.md +54 -0
- package/commands/dgs/add-tests.md +41 -0
- package/commands/dgs/add-todo.md +47 -0
- package/commands/dgs/approve-spec.md +38 -0
- package/commands/dgs/audit-milestone.md +36 -0
- package/commands/dgs/audit-phase.md +37 -0
- package/commands/dgs/cancel-job.md +23 -0
- package/commands/dgs/capture-principle.md +143 -0
- package/commands/dgs/check-todos.md +45 -0
- package/commands/dgs/cleanup.md +18 -0
- package/commands/dgs/complete-milestone.md +136 -0
- package/commands/dgs/complete-project.md +70 -0
- package/commands/dgs/consolidate-ideas.md +50 -0
- package/commands/dgs/create-milestone-job.md +37 -0
- package/commands/dgs/debug.md +164 -0
- package/commands/dgs/develop-idea.md +53 -0
- package/commands/dgs/discuss-idea.md +41 -0
- package/commands/dgs/discuss-phase.md +83 -0
- package/commands/dgs/execute-phase.md +41 -0
- package/commands/dgs/fast.md +38 -0
- package/commands/dgs/find-related-ideas.md +43 -0
- package/commands/dgs/health.md +28 -0
- package/commands/dgs/help.md +22 -0
- package/commands/dgs/import-spec.md +36 -0
- package/commands/dgs/init-product.md +28 -0
- package/commands/dgs/insert-phase.md +32 -0
- package/commands/dgs/join-discord.md +18 -0
- package/commands/dgs/list-docs.md +40 -0
- package/commands/dgs/list-ideas.md +42 -0
- package/commands/dgs/list-jobs.md +22 -0
- package/commands/dgs/list-phase-assumptions.md +46 -0
- package/commands/dgs/list-projects.md +57 -0
- package/commands/dgs/list-specs.md +40 -0
- package/commands/dgs/map-codebase.md +92 -0
- package/commands/dgs/new-milestone.md +44 -0
- package/commands/dgs/new-project.md +42 -0
- package/commands/dgs/node-repair.md +26 -0
- package/commands/dgs/overlap-check.md +20 -0
- package/commands/dgs/pause-work.md +38 -0
- package/commands/dgs/plan-milestone-gaps.md +34 -0
- package/commands/dgs/plan-phase.md +44 -0
- package/commands/dgs/progress.md +24 -0
- package/commands/dgs/quick.md +41 -0
- package/commands/dgs/reactivate-project.md +70 -0
- package/commands/dgs/reapply-patches.md +110 -0
- package/commands/dgs/refine-spec.md +38 -0
- package/commands/dgs/reject-idea.md +43 -0
- package/commands/dgs/remove-doc.md +44 -0
- package/commands/dgs/remove-phase.md +31 -0
- package/commands/dgs/remove-repo.md +69 -0
- package/commands/dgs/research-idea.md +43 -0
- package/commands/dgs/research-phase.md +189 -0
- package/commands/dgs/restore-idea.md +45 -0
- package/commands/dgs/resume-work.md +40 -0
- package/commands/dgs/rollback-job.md +24 -0
- package/commands/dgs/run-job.md +35 -0
- package/commands/dgs/search.md +40 -0
- package/commands/dgs/set-profile.md +34 -0
- package/commands/dgs/settings.md +38 -0
- package/commands/dgs/switch-project.md +58 -0
- package/commands/dgs/undo-consolidation.md +42 -0
- package/commands/dgs/update-idea.md +44 -0
- package/commands/dgs/update.md +37 -0
- package/commands/dgs/validate-phase.md +35 -0
- package/commands/dgs/verify-work.md +39 -0
- package/commands/dgs/write-spec.md +49 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
- package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
- package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
- package/deliver-great-systems/bin/lib/commands.cjs +570 -0
- package/deliver-great-systems/bin/lib/config.cjs +417 -0
- package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
- package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
- package/deliver-great-systems/bin/lib/context.cjs +929 -0
- package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
- package/deliver-great-systems/bin/lib/core.cjs +744 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
- package/deliver-great-systems/bin/lib/docs.cjs +919 -0
- package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
- package/deliver-great-systems/bin/lib/execution.cjs +705 -0
- package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
- package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
- package/deliver-great-systems/bin/lib/identity.cjs +125 -0
- package/deliver-great-systems/bin/lib/init.cjs +1114 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
- package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
- package/deliver-great-systems/bin/lib/migration.cjs +352 -0
- package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
- package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
- package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
- package/deliver-great-systems/bin/lib/paths.cjs +144 -0
- package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
- package/deliver-great-systems/bin/lib/phase.cjs +910 -0
- package/deliver-great-systems/bin/lib/projects.cjs +691 -0
- package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
- package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
- package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
- package/deliver-great-systems/bin/lib/search.cjs +570 -0
- package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
- package/deliver-great-systems/bin/lib/state.cjs +893 -0
- package/deliver-great-systems/bin/lib/template.cjs +228 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
- package/deliver-great-systems/bin/lib/verify.cjs +796 -0
- package/deliver-great-systems/references/checkpoints.md +776 -0
- package/deliver-great-systems/references/conflict-resolution.md +66 -0
- package/deliver-great-systems/references/context-tiers.md +166 -0
- package/deliver-great-systems/references/continuation-format.md +249 -0
- package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
- package/deliver-great-systems/references/git-integration.md +250 -0
- package/deliver-great-systems/references/git-planning-commit.md +40 -0
- package/deliver-great-systems/references/model-profile-resolution.md +36 -0
- package/deliver-great-systems/references/model-profiles.md +95 -0
- package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
- package/deliver-great-systems/references/planning-config.md +224 -0
- package/deliver-great-systems/references/questioning.md +162 -0
- package/deliver-great-systems/references/spec-review-loop.md +177 -0
- package/deliver-great-systems/references/tdd.md +265 -0
- package/deliver-great-systems/references/ui-brand.md +160 -0
- package/deliver-great-systems/references/verification-patterns.md +612 -0
- package/deliver-great-systems/templates/DEBUG.md +166 -0
- package/deliver-great-systems/templates/UAT.md +251 -0
- package/deliver-great-systems/templates/VALIDATION.md +95 -0
- package/deliver-great-systems/templates/claude-md.md +74 -0
- package/deliver-great-systems/templates/codebase/architecture.md +257 -0
- package/deliver-great-systems/templates/codebase/concerns.md +312 -0
- package/deliver-great-systems/templates/codebase/conventions.md +309 -0
- package/deliver-great-systems/templates/codebase/integrations.md +282 -0
- package/deliver-great-systems/templates/codebase/stack.md +188 -0
- package/deliver-great-systems/templates/codebase/structure.md +287 -0
- package/deliver-great-systems/templates/codebase/testing.md +482 -0
- package/deliver-great-systems/templates/config.json +38 -0
- package/deliver-great-systems/templates/context.md +354 -0
- package/deliver-great-systems/templates/continue-here.md +80 -0
- package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
- package/deliver-great-systems/templates/discovery.md +148 -0
- package/deliver-great-systems/templates/milestone-archive.md +125 -0
- package/deliver-great-systems/templates/milestone.md +117 -0
- package/deliver-great-systems/templates/phase-prompt.md +615 -0
- package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
- package/deliver-great-systems/templates/project.md +186 -0
- package/deliver-great-systems/templates/requirements.md +233 -0
- package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
- package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
- package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
- package/deliver-great-systems/templates/research-project/STACK.md +122 -0
- package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
- package/deliver-great-systems/templates/research.md +554 -0
- package/deliver-great-systems/templates/retrospective.md +54 -0
- package/deliver-great-systems/templates/roadmap.md +204 -0
- package/deliver-great-systems/templates/state.md +178 -0
- package/deliver-great-systems/templates/summary-complex.md +59 -0
- package/deliver-great-systems/templates/summary-minimal.md +41 -0
- package/deliver-great-systems/templates/summary-standard.md +48 -0
- package/deliver-great-systems/templates/summary.md +253 -0
- package/deliver-great-systems/templates/user-setup.md +313 -0
- package/deliver-great-systems/templates/verification-report.md +324 -0
- package/deliver-great-systems/workflows/add-doc.md +151 -0
- package/deliver-great-systems/workflows/add-idea.md +96 -0
- package/deliver-great-systems/workflows/add-phase.md +120 -0
- package/deliver-great-systems/workflows/add-tests.md +359 -0
- package/deliver-great-systems/workflows/add-todo.md +162 -0
- package/deliver-great-systems/workflows/approve-spec.md +194 -0
- package/deliver-great-systems/workflows/audit-milestone.md +364 -0
- package/deliver-great-systems/workflows/audit-phase.md +462 -0
- package/deliver-great-systems/workflows/cancel-job.md +108 -0
- package/deliver-great-systems/workflows/check-todos.md +181 -0
- package/deliver-great-systems/workflows/cleanup.md +247 -0
- package/deliver-great-systems/workflows/codereview.md +526 -0
- package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
- package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
- package/deliver-great-systems/workflows/develop-idea.md +544 -0
- package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
- package/deliver-great-systems/workflows/discovery-phase.md +301 -0
- package/deliver-great-systems/workflows/discuss-idea.md +263 -0
- package/deliver-great-systems/workflows/discuss-phase.md +733 -0
- package/deliver-great-systems/workflows/execute-phase.md +571 -0
- package/deliver-great-systems/workflows/execute-plan.md +592 -0
- package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
- package/deliver-great-systems/workflows/health.md +173 -0
- package/deliver-great-systems/workflows/help.md +997 -0
- package/deliver-great-systems/workflows/import-spec.md +381 -0
- package/deliver-great-systems/workflows/init-product.md +767 -0
- package/deliver-great-systems/workflows/insert-phase.md +138 -0
- package/deliver-great-systems/workflows/list-docs.md +119 -0
- package/deliver-great-systems/workflows/list-ideas.md +154 -0
- package/deliver-great-systems/workflows/list-jobs.md +89 -0
- package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
- package/deliver-great-systems/workflows/list-specs.md +101 -0
- package/deliver-great-systems/workflows/map-codebase.md +621 -0
- package/deliver-great-systems/workflows/new-milestone.md +591 -0
- package/deliver-great-systems/workflows/new-project.md +1113 -0
- package/deliver-great-systems/workflows/node-repair.md +94 -0
- package/deliver-great-systems/workflows/overlap-check.md +86 -0
- package/deliver-great-systems/workflows/pause-work.md +134 -0
- package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
- package/deliver-great-systems/workflows/plan-phase.md +698 -0
- package/deliver-great-systems/workflows/progress.md +386 -0
- package/deliver-great-systems/workflows/quick.md +845 -0
- package/deliver-great-systems/workflows/refine-spec.md +275 -0
- package/deliver-great-systems/workflows/reject-idea.md +109 -0
- package/deliver-great-systems/workflows/remove-doc.md +117 -0
- package/deliver-great-systems/workflows/remove-phase.md +163 -0
- package/deliver-great-systems/workflows/research-idea.md +325 -0
- package/deliver-great-systems/workflows/research-phase.md +81 -0
- package/deliver-great-systems/workflows/restore-idea.md +101 -0
- package/deliver-great-systems/workflows/resume-project.md +311 -0
- package/deliver-great-systems/workflows/rollback-job.md +130 -0
- package/deliver-great-systems/workflows/run-job.md +498 -0
- package/deliver-great-systems/workflows/search.md +130 -0
- package/deliver-great-systems/workflows/set-profile.md +83 -0
- package/deliver-great-systems/workflows/settings.md +470 -0
- package/deliver-great-systems/workflows/transition.md +563 -0
- package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
- package/deliver-great-systems/workflows/update-idea.md +157 -0
- package/deliver-great-systems/workflows/update.md +242 -0
- package/deliver-great-systems/workflows/validate-phase.md +177 -0
- package/deliver-great-systems/workflows/verify-phase.md +253 -0
- package/deliver-great-systems/workflows/verify-work.md +671 -0
- package/deliver-great-systems/workflows/write-spec.md +450 -0
- package/hooks/dist/dgs-check-update.js +62 -0
- package/hooks/dist/dgs-context-monitor.js +141 -0
- package/hooks/dist/dgs-statusline.js +115 -0
- package/package.json +60 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,2015 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jobs — Job file parse, step update, file move, find, header update,
|
|
3
|
+
* step insertion, gap-fix sections, milestone job creation, and visibility/polish
|
|
4
|
+
*
|
|
5
|
+
* Provides the programmatic foundation for all job lifecycle operations.
|
|
6
|
+
* Parses markdown job files into structured JSON, updates individual step
|
|
7
|
+
* statuses, moves files between lifecycle directories, locates job files
|
|
8
|
+
* by version, updates header fields in-place, inserts new steps dynamically,
|
|
9
|
+
* manages gap-fix cycle sections (both milestone-level and phase-level),
|
|
10
|
+
* generates milestone job files from roadmap phase analysis, lists jobs,
|
|
11
|
+
* cancels jobs, runs health checks, previews dry runs, and generates
|
|
12
|
+
* job summaries.
|
|
13
|
+
*
|
|
14
|
+
* Exports: parseJobFile, updateJobStep, moveJobFile, findJobFile,
|
|
15
|
+
* updateJobHeader, insertJobSteps, buildGapFixSteps,
|
|
16
|
+
* buildPhaseGapFixSteps, insertGapFixSection,
|
|
17
|
+
* insertPhaseGapFixSection, updatePhaseFixCycleHeader,
|
|
18
|
+
* generateMilestoneSteps, buildJobFileContent, listJobs,
|
|
19
|
+
* cancelJob, recordStartShas, rollbackJob,
|
|
20
|
+
* healthCheck, dryRunPreview, generateJobSummary,
|
|
21
|
+
* cmdJobsParse, cmdJobsUpdateStep, cmdJobsMove,
|
|
22
|
+
* cmdJobsFindJob, cmdJobsUpdateHeader, cmdJobsInsertSteps,
|
|
23
|
+
* cmdJobsInsertGapFixSection, cmdJobsInsertPhaseGapFix,
|
|
24
|
+
* cmdJobsCreateMilestone, cmdJobsMilestonePreview,
|
|
25
|
+
* cmdJobsListJobs, cmdJobsCancelJob,
|
|
26
|
+
* cmdJobsRecordStartShas, cmdJobsRollback,
|
|
27
|
+
* cmdJobsHealthCheck, cmdJobsDryRun, cmdJobsGenerateSummary
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const { execSync } = require('child_process');
|
|
33
|
+
const { output, error, getProjectRoot } = require('./core.cjs');
|
|
34
|
+
const { getPlanningRoot } = require('./paths.cjs');
|
|
35
|
+
const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
|
|
36
|
+
|
|
37
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Known DGS command patterns for step validation */
|
|
40
|
+
const KNOWN_COMMANDS = new Set([
|
|
41
|
+
'plan-phase',
|
|
42
|
+
'execute-phase',
|
|
43
|
+
'verify-work',
|
|
44
|
+
'map-codebase',
|
|
45
|
+
'audit-milestone',
|
|
46
|
+
'audit-phase',
|
|
47
|
+
'complete-milestone',
|
|
48
|
+
'plan-milestone-gaps',
|
|
49
|
+
'discuss-phase',
|
|
50
|
+
'research-phase',
|
|
51
|
+
'verify-phase',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Required header fields */
|
|
55
|
+
const REQUIRED_FIELDS = ['Version', 'Created', 'Status', 'Check'];
|
|
56
|
+
|
|
57
|
+
/** Maximum error message length on step lines */
|
|
58
|
+
const MAX_ERROR_LENGTH = 120;
|
|
59
|
+
|
|
60
|
+
// ─── v2-aware Path Resolution ────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the project root directory for milestone/roadmap operations.
|
|
64
|
+
* Uses getProjectRoot (v2-aware) with a fallback to getPlanningRoot (v1).
|
|
65
|
+
* Returns an absolute path in all cases.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} cwd - Working directory
|
|
68
|
+
* @returns {string} Absolute path to project root
|
|
69
|
+
*/
|
|
70
|
+
function resolveProjectRoot(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const projRoot = getProjectRoot(cwd);
|
|
73
|
+
return path.isAbsolute(projRoot) ? projRoot : path.join(cwd, projRoot);
|
|
74
|
+
} catch {
|
|
75
|
+
return getPlanningRoot(cwd);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Checkbox marker to status mapping */
|
|
80
|
+
const MARKER_TO_STATUS = {
|
|
81
|
+
'x': 'completed',
|
|
82
|
+
'>': 'in-progress',
|
|
83
|
+
'!': 'failed',
|
|
84
|
+
' ': 'pending',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Status to checkbox marker mapping */
|
|
88
|
+
const STATUS_TO_MARKER = {
|
|
89
|
+
'completed': 'x',
|
|
90
|
+
'in-progress': '>',
|
|
91
|
+
'failed': '!',
|
|
92
|
+
'pending': ' ',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse header fields from job file content.
|
|
99
|
+
* @param {string} content - Raw file content
|
|
100
|
+
* @returns {Object} Parsed header fields { version, created, status, check }
|
|
101
|
+
* @throws {Error} If a required field is missing
|
|
102
|
+
*/
|
|
103
|
+
function parseHeader(content) {
|
|
104
|
+
const fields = {};
|
|
105
|
+
|
|
106
|
+
const versionMatch = content.match(/^\*\*Version:\*\*\s*(.+)$/m);
|
|
107
|
+
if (versionMatch) fields.version = versionMatch[1].trim();
|
|
108
|
+
|
|
109
|
+
const createdMatch = content.match(/^\*\*Created:\*\*\s*(.+)$/m);
|
|
110
|
+
if (createdMatch) fields.created = createdMatch[1].trim();
|
|
111
|
+
|
|
112
|
+
const statusMatch = content.match(/^\*\*Status:\*\*\s*(.+)$/m);
|
|
113
|
+
if (statusMatch) fields.status = statusMatch[1].trim();
|
|
114
|
+
|
|
115
|
+
const checkMatch = content.match(/^\*\*Check:\*\*\s*(.+)$/m);
|
|
116
|
+
if (checkMatch) {
|
|
117
|
+
const val = checkMatch[1].trim().toLowerCase();
|
|
118
|
+
fields.check = val === 'true';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Optional: Created_by field (not required for backward compat)
|
|
122
|
+
const createdByMatch = content.match(/^\*\*Created_by:\*\*\s*(.+)$/m);
|
|
123
|
+
if (createdByMatch) fields.created_by = createdByMatch[1].trim();
|
|
124
|
+
|
|
125
|
+
// Validate required fields
|
|
126
|
+
for (const field of REQUIRED_FIELDS) {
|
|
127
|
+
const key = field.toLowerCase();
|
|
128
|
+
if (fields[key] === undefined) {
|
|
129
|
+
throw new Error(`Missing required field: ${field}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return fields;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse a single step line into structured data.
|
|
138
|
+
* @param {string} line - The step line (e.g., `- [x] \`/dgs:plan-phase 41\` -- completed ...`)
|
|
139
|
+
* @param {number} index - Zero-based step index
|
|
140
|
+
* @returns {Object|null} Parsed step object or null if line is not a step
|
|
141
|
+
*/
|
|
142
|
+
function parseStepLine(line, index) {
|
|
143
|
+
// Match: - [marker] `/dgs:command args` [— annotation]
|
|
144
|
+
const stepRegex = /^-\s*\[([x >!])\]\s*`\/dgs:([^\s`]+)(?:\s+([^`]*))?`(?:\s*\u2014\s*(.+))?$/;
|
|
145
|
+
const match = line.match(stepRegex);
|
|
146
|
+
if (!match) return null;
|
|
147
|
+
|
|
148
|
+
const marker = match[1];
|
|
149
|
+
const command = match[2];
|
|
150
|
+
const args = (match[3] || '').trim();
|
|
151
|
+
const annotation = match[4] || null;
|
|
152
|
+
const raw = `/dgs:${command}${args ? ' ' + args : ''}`;
|
|
153
|
+
|
|
154
|
+
const status = MARKER_TO_STATUS[marker] || 'pending';
|
|
155
|
+
|
|
156
|
+
let timestamp = null;
|
|
157
|
+
let stepError = null;
|
|
158
|
+
|
|
159
|
+
if (annotation) {
|
|
160
|
+
// Parse annotation: "completed|failed|started <ISO timestamp>[: error message]"
|
|
161
|
+
const annoRegex = /^(?:completed|failed|started)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z)(?::\s*(.+))?$/;
|
|
162
|
+
const annoMatch = annotation.match(annoRegex);
|
|
163
|
+
if (annoMatch) {
|
|
164
|
+
timestamp = annoMatch[1];
|
|
165
|
+
stepError = annoMatch[2] || null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const step = {
|
|
170
|
+
index,
|
|
171
|
+
status,
|
|
172
|
+
command,
|
|
173
|
+
args,
|
|
174
|
+
raw,
|
|
175
|
+
timestamp,
|
|
176
|
+
error: stepError,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Add warning for unrecognized commands
|
|
180
|
+
if (!KNOWN_COMMANDS.has(command)) {
|
|
181
|
+
step.warning = `Unrecognized command: ${command}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return step;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a step line from structured data.
|
|
189
|
+
* @param {Object} step - Step object with command, args
|
|
190
|
+
* @param {string} status - New status
|
|
191
|
+
* @param {Object} [options] - { timestamp, error }
|
|
192
|
+
* @returns {string} Formatted step line
|
|
193
|
+
*/
|
|
194
|
+
function buildStepLine(step, status, options) {
|
|
195
|
+
const opts = options || {};
|
|
196
|
+
const marker = STATUS_TO_MARKER[status] || ' ';
|
|
197
|
+
let line = `- [${marker}] \`/dgs:${step.command}${step.args ? ' ' + step.args : ''}\``;
|
|
198
|
+
|
|
199
|
+
if (status === 'completed' && opts.timestamp) {
|
|
200
|
+
line += ` \u2014 completed ${opts.timestamp}`;
|
|
201
|
+
} else if (status === 'in-progress' && opts.timestamp) {
|
|
202
|
+
line += ` \u2014 started ${opts.timestamp}`;
|
|
203
|
+
} else if (status === 'failed' && opts.timestamp) {
|
|
204
|
+
let errorMsg = opts.error || '';
|
|
205
|
+
if (errorMsg.length > MAX_ERROR_LENGTH) {
|
|
206
|
+
errorMsg = errorMsg.substring(0, MAX_ERROR_LENGTH) + '...';
|
|
207
|
+
}
|
|
208
|
+
line += ` \u2014 failed ${opts.timestamp}`;
|
|
209
|
+
if (errorMsg) {
|
|
210
|
+
line += `: ${errorMsg}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return line;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Exported Functions ─────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse a job markdown file into structured JSON.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} filePath - Absolute path to the job file
|
|
223
|
+
* @returns {Object} Structured job data with steps and computed fields
|
|
224
|
+
* @throws {Error} On missing file, missing required fields, or malformed content
|
|
225
|
+
*/
|
|
226
|
+
function parseJobFile(filePath) {
|
|
227
|
+
if (!fs.existsSync(filePath)) {
|
|
228
|
+
throw new Error(`Job file not found: ${filePath}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
232
|
+
const header = parseHeader(content);
|
|
233
|
+
|
|
234
|
+
// Parse optional StartShas header (defaults to null for backwards compat)
|
|
235
|
+
const startShasMatch = content.match(/^\*\*StartShas:\*\*\s*(.+)$/m);
|
|
236
|
+
let startShas = null;
|
|
237
|
+
if (startShasMatch) {
|
|
238
|
+
try {
|
|
239
|
+
startShas = JSON.parse(startShasMatch[1].trim());
|
|
240
|
+
} catch {
|
|
241
|
+
// Malformed JSON — treat as absent
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Parse optional GapFixCycle header (defaults to 0 for backwards compat)
|
|
246
|
+
const gapFixMatch = content.match(/^\*\*GapFixCycle:\*\*\s*(\d+)$/m);
|
|
247
|
+
const gapFixCycle = gapFixMatch ? parseInt(gapFixMatch[1], 10) : 0;
|
|
248
|
+
|
|
249
|
+
// Parse optional PhaseFixCycle header (format: "N:phase_number")
|
|
250
|
+
const phaseFixMatch = content.match(/^\*\*PhaseFixCycle:\*\*\s*(\d+):(\S+)$/m);
|
|
251
|
+
const phaseFixCycle = phaseFixMatch ? parseInt(phaseFixMatch[1], 10) : 0;
|
|
252
|
+
const phaseFixCyclePhase = phaseFixMatch ? phaseFixMatch[2] : null;
|
|
253
|
+
|
|
254
|
+
// Parse steps from lines
|
|
255
|
+
const lines = content.split('\n');
|
|
256
|
+
const steps = [];
|
|
257
|
+
let stepIndex = 0;
|
|
258
|
+
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
const trimmed = line.trim();
|
|
261
|
+
if (trimmed.startsWith('- [')) {
|
|
262
|
+
const step = parseStepLine(trimmed, stepIndex);
|
|
263
|
+
if (step) {
|
|
264
|
+
steps.push(step);
|
|
265
|
+
stepIndex++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Compute aggregate fields
|
|
271
|
+
const stepCount = steps.length;
|
|
272
|
+
const completedCount = steps.filter(s => s.status === 'completed').length;
|
|
273
|
+
const failedCount = steps.filter(s => s.status === 'failed').length;
|
|
274
|
+
const inProgressCount = steps.filter(s => s.status === 'in-progress').length;
|
|
275
|
+
|
|
276
|
+
// nextStepIndex: first pending step, or null if all done/empty
|
|
277
|
+
const nextPending = steps.find(s => s.status === 'pending');
|
|
278
|
+
const nextStepIndex = nextPending ? nextPending.index : null;
|
|
279
|
+
|
|
280
|
+
// Progress: percentage of completed steps (100 if no steps)
|
|
281
|
+
const progress = stepCount === 0 ? 100 : Math.round((completedCount / stepCount) * 100);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
version: header.version,
|
|
285
|
+
created: header.created,
|
|
286
|
+
created_by: header.created_by || null,
|
|
287
|
+
status: header.status,
|
|
288
|
+
check: header.check,
|
|
289
|
+
startShas,
|
|
290
|
+
gapFixCycle,
|
|
291
|
+
phaseFixCycle,
|
|
292
|
+
phaseFixCyclePhase,
|
|
293
|
+
steps,
|
|
294
|
+
stepCount,
|
|
295
|
+
completedCount,
|
|
296
|
+
failedCount,
|
|
297
|
+
inProgressCount,
|
|
298
|
+
nextStepIndex,
|
|
299
|
+
progress,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Update a single step's status in-place in the job file.
|
|
305
|
+
*
|
|
306
|
+
* @param {string} filePath - Absolute path to the job file
|
|
307
|
+
* @param {number} stepIndex - Zero-based index of the step to update
|
|
308
|
+
* @param {string} status - New status: 'completed', 'failed', 'in-progress', 'pending'
|
|
309
|
+
* @param {Object} [options] - { timestamp: string, error: string }
|
|
310
|
+
* @throws {Error} On out-of-range step index
|
|
311
|
+
*/
|
|
312
|
+
function updateJobStep(filePath, stepIndex, status, options) {
|
|
313
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
314
|
+
const lines = content.split('\n');
|
|
315
|
+
|
|
316
|
+
// Find all step lines and their line indices
|
|
317
|
+
const stepLines = [];
|
|
318
|
+
for (let i = 0; i < lines.length; i++) {
|
|
319
|
+
const trimmed = lines[i].trim();
|
|
320
|
+
if (trimmed.startsWith('- [')) {
|
|
321
|
+
const step = parseStepLine(trimmed, stepLines.length);
|
|
322
|
+
if (step) {
|
|
323
|
+
stepLines.push({ lineIndex: i, step });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const stepCount = stepLines.length;
|
|
329
|
+
|
|
330
|
+
if (stepIndex < 0 || stepIndex >= stepCount) {
|
|
331
|
+
throw new Error(`Step index ${stepIndex} out of range (0-${stepCount - 1})`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const target = stepLines[stepIndex];
|
|
335
|
+
const newLine = buildStepLine(target.step, status, options);
|
|
336
|
+
lines[target.lineIndex] = newLine;
|
|
337
|
+
|
|
338
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Move a job file from its current location to a target directory.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} filePath - Absolute path to the job file
|
|
345
|
+
* @param {string} targetDir - Absolute path to the target directory
|
|
346
|
+
* @returns {{ moved: boolean, from: string, to: string, warning?: string }}
|
|
347
|
+
*/
|
|
348
|
+
function moveJobFile(filePath, targetDir) {
|
|
349
|
+
const fileName = path.basename(filePath);
|
|
350
|
+
const currentDir = path.dirname(filePath);
|
|
351
|
+
const destPath = path.join(targetDir, fileName);
|
|
352
|
+
|
|
353
|
+
// Check if file is already in the target directory
|
|
354
|
+
if (path.resolve(currentDir) === path.resolve(targetDir)) {
|
|
355
|
+
return {
|
|
356
|
+
moved: false,
|
|
357
|
+
from: filePath,
|
|
358
|
+
to: destPath,
|
|
359
|
+
warning: `File already in target directory: ${targetDir}`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Auto-create target directory
|
|
364
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
365
|
+
|
|
366
|
+
// Move file
|
|
367
|
+
fs.renameSync(filePath, destPath);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
moved: true,
|
|
371
|
+
from: filePath,
|
|
372
|
+
to: destPath,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Locate a job file by milestone version, checking directories in priority order.
|
|
378
|
+
*
|
|
379
|
+
* Search order: in-progress/ (resume case), pending/ (new job), completed/ (inspection).
|
|
380
|
+
*
|
|
381
|
+
* @param {string} cwd - Working directory
|
|
382
|
+
* @param {string} version - Milestone version (e.g., "v6.0" or "6.0")
|
|
383
|
+
* @returns {{ found: boolean, path?: string, directory?: string }}
|
|
384
|
+
*/
|
|
385
|
+
function findJobFile(cwd, version) {
|
|
386
|
+
// Normalize version: ensure 'v' prefix
|
|
387
|
+
const normalized = version.startsWith('v') ? version : 'v' + version;
|
|
388
|
+
const fileName = `milestone-${normalized}.md`;
|
|
389
|
+
const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
|
|
390
|
+
|
|
391
|
+
// Search order: in-progress first (resume case), then pending, then completed (inspection)
|
|
392
|
+
for (const dir of ['in-progress', 'pending', 'completed']) {
|
|
393
|
+
const filePath = path.join(jobsDir, dir, fileName);
|
|
394
|
+
if (fs.existsSync(filePath)) {
|
|
395
|
+
return { found: true, path: filePath, directory: dir };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Search project subdirectories: .planning/projects/*/jobs/
|
|
400
|
+
const projectsDir = path.join(getPlanningRoot(cwd), 'projects');
|
|
401
|
+
if (fs.existsSync(projectsDir)) {
|
|
402
|
+
const projects = fs.readdirSync(projectsDir).filter(d =>
|
|
403
|
+
fs.statSync(path.join(projectsDir, d)).isDirectory()
|
|
404
|
+
);
|
|
405
|
+
for (const project of projects) {
|
|
406
|
+
for (const dir of ['in-progress', 'pending', 'completed']) {
|
|
407
|
+
const filePath = path.join(projectsDir, project, 'jobs', dir, fileName);
|
|
408
|
+
if (fs.existsSync(filePath)) {
|
|
409
|
+
return { found: true, path: filePath, directory: dir };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { found: false };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Update a header field value in-place in a job file.
|
|
420
|
+
*
|
|
421
|
+
* Finds the line matching `**{Field}:** ...` and replaces the value portion.
|
|
422
|
+
* Does NOT alter any step lines.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} filePath - Absolute path to the job file
|
|
425
|
+
* @param {string} field - Header field name (e.g., "Status")
|
|
426
|
+
* @param {string} value - New value for the field
|
|
427
|
+
* @throws {Error} If the field is not found in the file
|
|
428
|
+
*/
|
|
429
|
+
function updateJobHeader(filePath, field, value) {
|
|
430
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
431
|
+
const lines = content.split('\n');
|
|
432
|
+
|
|
433
|
+
const fieldPattern = new RegExp(`^\\*\\*${field}:\\*\\*\\s+.*$`);
|
|
434
|
+
let found = false;
|
|
435
|
+
|
|
436
|
+
for (let i = 0; i < lines.length; i++) {
|
|
437
|
+
if (fieldPattern.test(lines[i])) {
|
|
438
|
+
lines[i] = `**${field}:** ${value}`;
|
|
439
|
+
found = true;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!found) {
|
|
445
|
+
throw new Error(`Field not found in job file: ${field}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Insert new step lines at a specific position in the job file.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} filePath - Absolute path to the job file
|
|
455
|
+
* @param {number} afterStepIndex - Insert after this step index (-1 = insert before all steps)
|
|
456
|
+
* @param {Array<{ command: string, args: string }>} newSteps - Steps to insert
|
|
457
|
+
* @returns {{ inserted: boolean, count: number, startIndex: number }}
|
|
458
|
+
*/
|
|
459
|
+
function insertJobSteps(filePath, afterStepIndex, newSteps) {
|
|
460
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
461
|
+
const lines = content.split('\n');
|
|
462
|
+
|
|
463
|
+
// Find all step lines and their line indices
|
|
464
|
+
const stepLines = [];
|
|
465
|
+
for (let i = 0; i < lines.length; i++) {
|
|
466
|
+
const trimmed = lines[i].trim();
|
|
467
|
+
if (trimmed.startsWith('- [')) {
|
|
468
|
+
const step = parseStepLine(trimmed, stepLines.length);
|
|
469
|
+
if (step) {
|
|
470
|
+
stepLines.push({ lineIndex: i, step });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Build the new step lines using buildStepLine
|
|
476
|
+
const newLines = newSteps.map(s => buildStepLine(s, 'pending'));
|
|
477
|
+
|
|
478
|
+
// Determine insertion line index
|
|
479
|
+
let insertAtLine;
|
|
480
|
+
if (afterStepIndex === -1) {
|
|
481
|
+
// Insert before the first step
|
|
482
|
+
if (stepLines.length > 0) {
|
|
483
|
+
insertAtLine = stepLines[0].lineIndex;
|
|
484
|
+
} else {
|
|
485
|
+
// No steps exist; insert at end of file
|
|
486
|
+
insertAtLine = lines.length;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
// Insert after the specified step's line
|
|
490
|
+
insertAtLine = stepLines[afterStepIndex].lineIndex + 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Splice in the new lines
|
|
494
|
+
lines.splice(insertAtLine, 0, ...newLines);
|
|
495
|
+
|
|
496
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
inserted: true,
|
|
500
|
+
count: newSteps.length,
|
|
501
|
+
startIndex: afterStepIndex + 1,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Build the step sequence for a gap-fix cycle.
|
|
507
|
+
*
|
|
508
|
+
* For each phase: plan-phase + execute-phase (no discuss-phase, no verify-work).
|
|
509
|
+
* Ends with audit-milestone for re-audit.
|
|
510
|
+
*
|
|
511
|
+
* @param {Array<{ number: string, name: string }>} newPhases - Gap-fix phases
|
|
512
|
+
* @param {string} version - Milestone version for re-audit step
|
|
513
|
+
* @returns {Array<{ command: string, args: string }>} Step sequence
|
|
514
|
+
*/
|
|
515
|
+
function buildGapFixSteps(newPhases, version) {
|
|
516
|
+
const steps = [];
|
|
517
|
+
for (const phase of newPhases) {
|
|
518
|
+
steps.push({ command: 'plan-phase', args: `${phase.number} --non-interactive` });
|
|
519
|
+
steps.push({ command: 'execute-phase', args: `${phase.number} --non-interactive` });
|
|
520
|
+
}
|
|
521
|
+
steps.push({ command: 'audit-milestone', args: version });
|
|
522
|
+
return steps;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Build the step sequence for a phase-level gap-fix cycle.
|
|
527
|
+
*
|
|
528
|
+
* Simpler than buildGapFixSteps (milestone-level): no plan-phase, no
|
|
529
|
+
* audit-milestone. Just execute existing fix plans and re-verify.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} phaseNumber - Phase number (e.g., "41")
|
|
532
|
+
* @returns {Array<{ command: string, args: string }>} Step sequence (2 steps)
|
|
533
|
+
*/
|
|
534
|
+
function buildPhaseGapFixSteps(phaseNumber) {
|
|
535
|
+
return [
|
|
536
|
+
{ command: 'execute-phase', args: `${phaseNumber} --gaps-only` },
|
|
537
|
+
{ command: 'audit-phase', args: `${phaseNumber} --rerun-failed` },
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Insert a gap-fix section into a job file.
|
|
543
|
+
*
|
|
544
|
+
* Inserts a section marker line followed by gap-fix step lines after the
|
|
545
|
+
* specified step index. Updates the GapFixCycle header field.
|
|
546
|
+
*
|
|
547
|
+
* @param {string} filePath - Absolute path to the job file
|
|
548
|
+
* @param {number} afterStepIndex - Insert after this step index (typically the audit step)
|
|
549
|
+
* @param {number} cycleNumber - Gap-fix cycle number (1, 2, 3, ...)
|
|
550
|
+
* @param {Array<{ number: string, name: string }>} newPhases - Gap-fix phases
|
|
551
|
+
* @param {string} version - Milestone version for re-audit step
|
|
552
|
+
* @returns {{ inserted: boolean, count: number, cycleNumber: number, sectionMarker: string }}
|
|
553
|
+
*/
|
|
554
|
+
function insertGapFixSection(filePath, afterStepIndex, cycleNumber, newPhases, version) {
|
|
555
|
+
// Build the section marker
|
|
556
|
+
const gapCount = newPhases.length;
|
|
557
|
+
let phaseNames = newPhases.map(p => p.name).join(', ');
|
|
558
|
+
if (phaseNames.length > 80) {
|
|
559
|
+
phaseNames = phaseNames.substring(0, 77) + '...';
|
|
560
|
+
}
|
|
561
|
+
const sectionMarker = `\n--- Gap-Fix Cycle ${cycleNumber} (${gapCount} gaps: ${phaseNames}) ---`;
|
|
562
|
+
|
|
563
|
+
// Build gap-fix steps
|
|
564
|
+
const gapSteps = buildGapFixSteps(newPhases, version);
|
|
565
|
+
|
|
566
|
+
// Insert the step lines using existing insertJobSteps
|
|
567
|
+
insertJobSteps(filePath, afterStepIndex, gapSteps);
|
|
568
|
+
|
|
569
|
+
// Now read the file again and insert the section marker line
|
|
570
|
+
// The section marker goes immediately before the first newly-inserted step line
|
|
571
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
572
|
+
const lines = content.split('\n');
|
|
573
|
+
|
|
574
|
+
// Find all step lines and their positions
|
|
575
|
+
const stepPositions = [];
|
|
576
|
+
for (let i = 0; i < lines.length; i++) {
|
|
577
|
+
const trimmed = lines[i].trim();
|
|
578
|
+
if (trimmed.startsWith('- [')) {
|
|
579
|
+
const step = parseStepLine(trimmed, stepPositions.length);
|
|
580
|
+
if (step) {
|
|
581
|
+
stepPositions.push({ lineIndex: i, step });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// The first inserted step is at step index afterStepIndex + 1
|
|
587
|
+
const firstInsertedStepIdx = afterStepIndex + 1;
|
|
588
|
+
if (firstInsertedStepIdx < stepPositions.length) {
|
|
589
|
+
const insertLineIdx = stepPositions[firstInsertedStepIdx].lineIndex;
|
|
590
|
+
lines.splice(insertLineIdx, 0, sectionMarker);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
594
|
+
|
|
595
|
+
// Update the GapFixCycle header field
|
|
596
|
+
const updatedContent = fs.readFileSync(filePath, 'utf-8');
|
|
597
|
+
const hasGapFixField = /^\*\*GapFixCycle:\*\*\s+.*$/m.test(updatedContent);
|
|
598
|
+
|
|
599
|
+
if (hasGapFixField) {
|
|
600
|
+
updateJobHeader(filePath, 'GapFixCycle', String(cycleNumber));
|
|
601
|
+
} else {
|
|
602
|
+
// Insert GapFixCycle line after the Check header line
|
|
603
|
+
const fileLines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
604
|
+
const checkLineIdx = fileLines.findIndex(l => /^\*\*Check:\*\*\s+/.test(l));
|
|
605
|
+
if (checkLineIdx !== -1) {
|
|
606
|
+
fileLines.splice(checkLineIdx + 1, 0, `**GapFixCycle:** ${cycleNumber}`);
|
|
607
|
+
fs.writeFileSync(filePath, fileLines.join('\n'), 'utf-8');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
inserted: true,
|
|
613
|
+
count: gapSteps.length,
|
|
614
|
+
cycleNumber,
|
|
615
|
+
sectionMarker,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* CLI wrapper for insertGapFixSection.
|
|
621
|
+
* Usage: dgs-tools.cjs jobs insert-gap-fix-section <file> <after-index> <cycle> <version> <phases-json>
|
|
622
|
+
*
|
|
623
|
+
* @param {string} cwd - Working directory
|
|
624
|
+
* @param {string} file - Path to job file
|
|
625
|
+
* @param {string} afterIndexStr - Step index to insert after (string, parsed to int)
|
|
626
|
+
* @param {string} cycleStr - Cycle number (string, parsed to int)
|
|
627
|
+
* @param {string} version - Milestone version
|
|
628
|
+
* @param {string} phasesJson - JSON string of phases array [{number, name}]
|
|
629
|
+
* @param {boolean} raw - Raw output mode
|
|
630
|
+
*/
|
|
631
|
+
function cmdJobsInsertGapFixSection(cwd, file, afterIndexStr, cycleStr, version, phasesJson, raw) {
|
|
632
|
+
if (!file || afterIndexStr === undefined || cycleStr === undefined || !version || !phasesJson) {
|
|
633
|
+
error('Usage: jobs insert-gap-fix-section <file> <after-index> <cycle> <version> <phases-json>');
|
|
634
|
+
}
|
|
635
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
636
|
+
const afterIndex = parseInt(afterIndexStr, 10);
|
|
637
|
+
if (isNaN(afterIndex)) {
|
|
638
|
+
error(`Invalid step index: ${afterIndexStr}`);
|
|
639
|
+
}
|
|
640
|
+
const cycle = parseInt(cycleStr, 10);
|
|
641
|
+
if (isNaN(cycle)) {
|
|
642
|
+
error(`Invalid cycle number: ${cycleStr}`);
|
|
643
|
+
}
|
|
644
|
+
let phases;
|
|
645
|
+
try {
|
|
646
|
+
phases = JSON.parse(phasesJson);
|
|
647
|
+
} catch (parseErr) {
|
|
648
|
+
error(`Invalid JSON for phases: ${parseErr.message}`);
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const result = insertGapFixSection(filePath, afterIndex, cycle, phases, version);
|
|
652
|
+
output(result, raw);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
error(err.message);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Update the PhaseFixCycle header in a job file.
|
|
660
|
+
*
|
|
661
|
+
* If the header exists, updates it in-place. If not, inserts it after
|
|
662
|
+
* the GapFixCycle header (if present) or after the Check header.
|
|
663
|
+
*
|
|
664
|
+
* @param {string} filePath - Absolute path to the job file
|
|
665
|
+
* @param {number} cycleNumber - Phase fix cycle number
|
|
666
|
+
* @param {string} phaseNumber - Phase number (e.g., "41")
|
|
667
|
+
*/
|
|
668
|
+
function updatePhaseFixCycleHeader(filePath, cycleNumber, phaseNumber) {
|
|
669
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
670
|
+
const headerPattern = /^\*\*PhaseFixCycle:\*\*\s+.*$/m;
|
|
671
|
+
|
|
672
|
+
if (headerPattern.test(content)) {
|
|
673
|
+
const updated = content.replace(headerPattern, `**PhaseFixCycle:** ${cycleNumber}:${phaseNumber}`);
|
|
674
|
+
fs.writeFileSync(filePath, updated, 'utf-8');
|
|
675
|
+
} else {
|
|
676
|
+
// Insert after GapFixCycle if it exists, else after Check line
|
|
677
|
+
const lines = content.split('\n');
|
|
678
|
+
const checkLineIdx = lines.findIndex(l => /^\*\*Check:\*\*\s+/.test(l));
|
|
679
|
+
if (checkLineIdx !== -1) {
|
|
680
|
+
const gapFixIdx = lines.findIndex(l => /^\*\*GapFixCycle:\*\*\s+/.test(l));
|
|
681
|
+
const insertIdx = gapFixIdx !== -1 ? gapFixIdx + 1 : checkLineIdx + 1;
|
|
682
|
+
lines.splice(insertIdx, 0, `**PhaseFixCycle:** ${cycleNumber}:${phaseNumber}`);
|
|
683
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Insert a phase-level gap-fix section into a job file.
|
|
690
|
+
*
|
|
691
|
+
* Inserts a section marker line followed by phase gap-fix step lines
|
|
692
|
+
* (execute-phase --gaps-only + audit-phase --rerun-failed) after the
|
|
693
|
+
* specified step index. Updates the PhaseFixCycle header field.
|
|
694
|
+
*
|
|
695
|
+
* @param {string} filePath - Absolute path to the job file
|
|
696
|
+
* @param {number} afterStepIndex - Insert after this step index (typically the audit-phase step)
|
|
697
|
+
* @param {number} cycleNumber - Phase fix cycle number (1, 2)
|
|
698
|
+
* @param {string} phaseNumber - Phase number (e.g., "41")
|
|
699
|
+
* @returns {{ inserted: boolean, count: number, cycleNumber: number, phaseNumber: string, sectionMarker: string }}
|
|
700
|
+
*/
|
|
701
|
+
function insertPhaseGapFixSection(filePath, afterStepIndex, cycleNumber, phaseNumber) {
|
|
702
|
+
const sectionMarker = `\n--- Phase ${phaseNumber} Gap-Fix Cycle ${cycleNumber}/2 ---`;
|
|
703
|
+
const gapSteps = buildPhaseGapFixSteps(phaseNumber);
|
|
704
|
+
|
|
705
|
+
// Insert steps using existing infrastructure
|
|
706
|
+
insertJobSteps(filePath, afterStepIndex, gapSteps);
|
|
707
|
+
|
|
708
|
+
// Insert section marker before the first inserted step (same technique as insertGapFixSection)
|
|
709
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
710
|
+
const lines = content.split('\n');
|
|
711
|
+
const stepPositions = [];
|
|
712
|
+
for (let i = 0; i < lines.length; i++) {
|
|
713
|
+
const trimmed = lines[i].trim();
|
|
714
|
+
if (trimmed.startsWith('- [')) {
|
|
715
|
+
const step = parseStepLine(trimmed, stepPositions.length);
|
|
716
|
+
if (step) stepPositions.push({ lineIndex: i, step });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const firstInsertedStepIdx = afterStepIndex + 1;
|
|
720
|
+
if (firstInsertedStepIdx < stepPositions.length) {
|
|
721
|
+
const insertLineIdx = stepPositions[firstInsertedStepIdx].lineIndex;
|
|
722
|
+
lines.splice(insertLineIdx, 0, sectionMarker);
|
|
723
|
+
}
|
|
724
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
725
|
+
|
|
726
|
+
// Update PhaseFixCycle header
|
|
727
|
+
updatePhaseFixCycleHeader(filePath, cycleNumber, phaseNumber);
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
inserted: true,
|
|
731
|
+
count: gapSteps.length,
|
|
732
|
+
cycleNumber,
|
|
733
|
+
phaseNumber,
|
|
734
|
+
sectionMarker,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* CLI wrapper for insertPhaseGapFixSection.
|
|
740
|
+
* Usage: dgs-tools.cjs jobs insert-phase-gap-fix <file> <after-index> <cycle> <phase-number>
|
|
741
|
+
*
|
|
742
|
+
* @param {string} cwd - Working directory
|
|
743
|
+
* @param {string} file - Path to job file
|
|
744
|
+
* @param {string} afterIndexStr - Step index to insert after (string, parsed to int)
|
|
745
|
+
* @param {string} cycleStr - Cycle number (string, parsed to int)
|
|
746
|
+
* @param {string} phaseNumber - Phase number (e.g., "41")
|
|
747
|
+
* @param {boolean} raw - Raw output mode
|
|
748
|
+
*/
|
|
749
|
+
function cmdJobsInsertPhaseGapFix(cwd, file, afterIndexStr, cycleStr, phaseNumber, raw) {
|
|
750
|
+
if (!file || afterIndexStr === undefined || cycleStr === undefined || !phaseNumber) {
|
|
751
|
+
error('Usage: jobs insert-phase-gap-fix <file> <after-index> <cycle> <phase-number>');
|
|
752
|
+
}
|
|
753
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
754
|
+
const afterIndex = parseInt(afterIndexStr, 10);
|
|
755
|
+
if (isNaN(afterIndex)) error(`Invalid step index: ${afterIndexStr}`);
|
|
756
|
+
const cycle = parseInt(cycleStr, 10);
|
|
757
|
+
if (isNaN(cycle)) error(`Invalid cycle number: ${cycleStr}`);
|
|
758
|
+
try {
|
|
759
|
+
const result = insertPhaseGapFixSection(filePath, afterIndex, cycle, phaseNumber);
|
|
760
|
+
output(result, raw);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
error(err.message);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ─── CLI Wrapper Functions ──────────────────────────────────────────────────
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* CLI wrapper for parseJobFile.
|
|
770
|
+
* Usage: dgs-tools.cjs jobs parse <file>
|
|
771
|
+
*
|
|
772
|
+
* @param {string} cwd - Working directory
|
|
773
|
+
* @param {string} file - Path to job file (relative to cwd or absolute)
|
|
774
|
+
* @param {boolean} raw - Raw output mode
|
|
775
|
+
*/
|
|
776
|
+
function cmdJobsParse(cwd, file, raw) {
|
|
777
|
+
if (!file) {
|
|
778
|
+
error('file path required. Usage: jobs parse <file>');
|
|
779
|
+
}
|
|
780
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
781
|
+
try {
|
|
782
|
+
const result = parseJobFile(filePath);
|
|
783
|
+
output(result, raw);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
error(err.message);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* CLI wrapper for updateJobStep.
|
|
791
|
+
* Usage: dgs-tools.cjs jobs update-step <file> <step-index> <status> [--timestamp T] [--error E]
|
|
792
|
+
*
|
|
793
|
+
* @param {string} cwd - Working directory
|
|
794
|
+
* @param {string} file - Path to job file
|
|
795
|
+
* @param {string} stepIndexStr - Step index as string
|
|
796
|
+
* @param {string} status - New status
|
|
797
|
+
* @param {Object} opts - { timestamp, error }
|
|
798
|
+
* @param {boolean} raw - Raw output mode
|
|
799
|
+
*/
|
|
800
|
+
function cmdJobsUpdateStep(cwd, file, stepIndexStr, status, opts, raw) {
|
|
801
|
+
if (!file || stepIndexStr === undefined || !status) {
|
|
802
|
+
error('Usage: jobs update-step <file> <step-index> <status> [--timestamp T] [--error E]');
|
|
803
|
+
}
|
|
804
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
805
|
+
const stepIndex = parseInt(stepIndexStr, 10);
|
|
806
|
+
if (isNaN(stepIndex)) {
|
|
807
|
+
error(`Invalid step index: ${stepIndexStr}`);
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
updateJobStep(filePath, stepIndex, status, opts);
|
|
811
|
+
output({ updated: true, step: stepIndex, status }, raw, `Step ${stepIndex} updated to ${status}`);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
error(err.message);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* CLI wrapper for moveJobFile.
|
|
819
|
+
* Usage: dgs-tools.cjs jobs move <file> <target-dir>
|
|
820
|
+
*
|
|
821
|
+
* @param {string} cwd - Working directory
|
|
822
|
+
* @param {string} file - Path to job file
|
|
823
|
+
* @param {string} targetDir - Target directory path
|
|
824
|
+
* @param {boolean} raw - Raw output mode
|
|
825
|
+
*/
|
|
826
|
+
function cmdJobsMove(cwd, file, targetDir, raw) {
|
|
827
|
+
if (!file || !targetDir) {
|
|
828
|
+
error('Usage: jobs move <file> <target-dir>');
|
|
829
|
+
}
|
|
830
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
831
|
+
const target = path.isAbsolute(targetDir) ? targetDir : path.join(cwd, targetDir);
|
|
832
|
+
try {
|
|
833
|
+
const result = moveJobFile(filePath, target);
|
|
834
|
+
const fileName = path.basename(filePath);
|
|
835
|
+
const dirName = path.basename(target);
|
|
836
|
+
output(result, raw, result.moved ? `${fileName} moved to ${dirName}` : result.warning);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
error(err.message);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* CLI wrapper for findJobFile.
|
|
844
|
+
* Usage: dgs-tools.cjs jobs find-job <version>
|
|
845
|
+
*
|
|
846
|
+
* @param {string} cwd - Working directory
|
|
847
|
+
* @param {string} version - Milestone version (e.g., "v6.0" or "6.0")
|
|
848
|
+
* @param {boolean} raw - Raw output mode
|
|
849
|
+
*/
|
|
850
|
+
function cmdJobsFindJob(cwd, version, raw) {
|
|
851
|
+
if (!version) {
|
|
852
|
+
error('version required. Usage: jobs find-job <version>');
|
|
853
|
+
}
|
|
854
|
+
const result = findJobFile(cwd, version);
|
|
855
|
+
output(result, raw);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* CLI wrapper for updateJobHeader.
|
|
860
|
+
* Usage: dgs-tools.cjs jobs update-header <file> <field> <value>
|
|
861
|
+
*
|
|
862
|
+
* @param {string} cwd - Working directory
|
|
863
|
+
* @param {string} file - Path to job file
|
|
864
|
+
* @param {string} field - Header field name
|
|
865
|
+
* @param {string} value - New value
|
|
866
|
+
* @param {boolean} raw - Raw output mode
|
|
867
|
+
*/
|
|
868
|
+
function cmdJobsUpdateHeader(cwd, file, field, value, raw) {
|
|
869
|
+
if (!file || !field || !value) {
|
|
870
|
+
error('Usage: jobs update-header <file> <field> <value>');
|
|
871
|
+
}
|
|
872
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
873
|
+
try {
|
|
874
|
+
updateJobHeader(filePath, field, value);
|
|
875
|
+
output({ updated: true, field, value }, raw, `${field} updated to ${value}`);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
error(err.message);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* CLI wrapper for insertJobSteps.
|
|
883
|
+
* Usage: dgs-tools.cjs jobs insert-steps <file> <after-index> <steps-json>
|
|
884
|
+
*
|
|
885
|
+
* @param {string} cwd - Working directory
|
|
886
|
+
* @param {string} file - Path to job file
|
|
887
|
+
* @param {string} afterIndexStr - Step index to insert after (string, parsed to int)
|
|
888
|
+
* @param {string} stepsJson - JSON string of steps array
|
|
889
|
+
* @param {boolean} raw - Raw output mode
|
|
890
|
+
*/
|
|
891
|
+
function cmdJobsInsertSteps(cwd, file, afterIndexStr, stepsJson, raw) {
|
|
892
|
+
if (!file || afterIndexStr === undefined || !stepsJson) {
|
|
893
|
+
error('Usage: jobs insert-steps <file> <after-index> <steps-json>');
|
|
894
|
+
}
|
|
895
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
896
|
+
const afterIndex = parseInt(afterIndexStr, 10);
|
|
897
|
+
if (isNaN(afterIndex)) {
|
|
898
|
+
error(`Invalid step index: ${afterIndexStr}`);
|
|
899
|
+
}
|
|
900
|
+
let steps;
|
|
901
|
+
try {
|
|
902
|
+
steps = JSON.parse(stepsJson);
|
|
903
|
+
} catch (parseErr) {
|
|
904
|
+
error(`Invalid JSON for steps: ${parseErr.message}`);
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
const result = insertJobSteps(filePath, afterIndex, steps);
|
|
908
|
+
output(result, raw, `${result.count} steps inserted at index ${result.startIndex}`);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
error(err.message);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ─── Milestone Step Generation ──────────────────────────────────────────────
|
|
915
|
+
|
|
916
|
+
/** Disk statuses that require map-codebase + plan-phase + execute-phase + audit-phase */
|
|
917
|
+
const NEEDS_PLANNING = new Set(['no_directory', 'empty', 'discussed', 'researched']);
|
|
918
|
+
|
|
919
|
+
/** Disk statuses that require execute-phase + audit-phase (already planned) */
|
|
920
|
+
const NEEDS_EXECUTION = new Set(['planned', 'partial']);
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Compare phase numbers numerically (handles decimals like 50.1).
|
|
924
|
+
* @param {string} a - Phase number
|
|
925
|
+
* @param {string} b - Phase number
|
|
926
|
+
* @returns {number} Comparison result
|
|
927
|
+
*/
|
|
928
|
+
function comparePhaseNumbers(a, b) {
|
|
929
|
+
return parseFloat(a) - parseFloat(b);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Generate the step sequence for a milestone job based on phase states.
|
|
934
|
+
*
|
|
935
|
+
* @param {Array} phases - Array of phase objects from roadmap analysis
|
|
936
|
+
* @param {Object} options - { check: boolean, version: string }
|
|
937
|
+
* @returns {Array<{ command: string, args: string }>} Ordered step sequence
|
|
938
|
+
*/
|
|
939
|
+
function generateMilestoneSteps(phases, options) {
|
|
940
|
+
const { check, version } = options;
|
|
941
|
+
|
|
942
|
+
// Sort phases by number (numeric, handles decimals)
|
|
943
|
+
const sorted = [...phases].sort((a, b) => comparePhaseNumbers(a.number, b.number));
|
|
944
|
+
|
|
945
|
+
const steps = [];
|
|
946
|
+
|
|
947
|
+
for (const phase of sorted) {
|
|
948
|
+
// Skip completed phases
|
|
949
|
+
if (phase.disk_status === 'complete' || phase.roadmap_complete === true) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const num = phase.number;
|
|
954
|
+
|
|
955
|
+
if (NEEDS_PLANNING.has(phase.disk_status)) {
|
|
956
|
+
steps.push({ command: 'map-codebase', args: `${num} --auto` });
|
|
957
|
+
steps.push({ command: 'plan-phase', args: `${num} --non-interactive` });
|
|
958
|
+
steps.push({ command: 'execute-phase', args: `${num} --non-interactive` });
|
|
959
|
+
steps.push({ command: 'audit-phase', args: `${num}` });
|
|
960
|
+
} else if (NEEDS_EXECUTION.has(phase.disk_status)) {
|
|
961
|
+
steps.push({ command: 'execute-phase', args: `${num} --non-interactive` });
|
|
962
|
+
steps.push({ command: 'audit-phase', args: `${num}` });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Append audit + complete steps if check is enabled
|
|
967
|
+
if (check) {
|
|
968
|
+
steps.push({ command: 'audit-milestone', args: version });
|
|
969
|
+
steps.push({ command: 'complete-milestone', args: version });
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return steps;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Build the markdown content for a milestone job file.
|
|
977
|
+
*
|
|
978
|
+
* @param {string} version - Milestone version (e.g., "v6.0")
|
|
979
|
+
* @param {boolean} check - Whether audit/complete steps are included
|
|
980
|
+
* @param {Array<{ command: string, args: string }>} steps - Step sequence
|
|
981
|
+
* @param {string|null} [createdBy=null] - Author string (e.g., "Name <email>"). Omits Created_by line when null/undefined.
|
|
982
|
+
* @returns {string} Complete markdown file content
|
|
983
|
+
*/
|
|
984
|
+
function buildJobFileContent(version, check, steps, createdBy) {
|
|
985
|
+
const created = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
986
|
+
|
|
987
|
+
let content = `# Milestone Job: ${version}\n\n`;
|
|
988
|
+
content += `**Version:** ${version}\n`;
|
|
989
|
+
content += `**Created:** ${created}\n`;
|
|
990
|
+
if (createdBy) {
|
|
991
|
+
content += `**Created_by:** ${createdBy}\n`;
|
|
992
|
+
}
|
|
993
|
+
content += `**Status:** pending\n`;
|
|
994
|
+
content += `**Check:** ${check}\n`;
|
|
995
|
+
content += `\n## Steps\n\n`;
|
|
996
|
+
|
|
997
|
+
for (const step of steps) {
|
|
998
|
+
content += `- [ ] \`/dgs:${step.command} ${step.args}\`\n`;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return content;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// ─── Milestone Phase Analysis ───────────────────────────────────────────────
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Analyze phases belonging to a specific milestone from ROADMAP.md.
|
|
1008
|
+
* Replicates the disk_status detection from roadmap.cjs but scoped to a
|
|
1009
|
+
* single milestone's phases, avoiding circular dependency with output().
|
|
1010
|
+
*
|
|
1011
|
+
* @param {string} cwd - Working directory
|
|
1012
|
+
* @param {string} version - Milestone version to analyze (e.g., "v6.0")
|
|
1013
|
+
* @returns {Array} Phase objects with { number, name, disk_status, roadmap_complete }
|
|
1014
|
+
* @throws {Error} If ROADMAP.md not found or version not found
|
|
1015
|
+
*/
|
|
1016
|
+
function analyzeMilestonePhases(cwd, version) {
|
|
1017
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
1018
|
+
const roadmapPath = path.join(projectRoot, 'ROADMAP.md');
|
|
1019
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
1020
|
+
throw new Error('ROADMAP.md not found at ' + roadmapPath);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1024
|
+
const phasesDir = path.join(projectRoot, 'phases');
|
|
1025
|
+
|
|
1026
|
+
// Find the milestone section: look for heading containing the version + "In Progress" or "SHIPPED"
|
|
1027
|
+
// Also match the milestone in the summary list to find its phase range
|
|
1028
|
+
const escapedVersion = version.replace(/\./g, '\\.');
|
|
1029
|
+
|
|
1030
|
+
// Fast path: explicit "Phases N-M" in milestone summary line
|
|
1031
|
+
const milestoneRangePattern = new RegExp(
|
|
1032
|
+
`${escapedVersion}\\s+[^\\n]*Phases\\s+(\\d+)\\s*-\\s*(\\d+)`,
|
|
1033
|
+
'i'
|
|
1034
|
+
);
|
|
1035
|
+
const rangeMatch = content.match(milestoneRangePattern);
|
|
1036
|
+
|
|
1037
|
+
let phaseStart, phaseEnd;
|
|
1038
|
+
|
|
1039
|
+
if (rangeMatch) {
|
|
1040
|
+
phaseStart = parseInt(rangeMatch[1], 10);
|
|
1041
|
+
phaseEnd = parseInt(rangeMatch[2], 10);
|
|
1042
|
+
} else {
|
|
1043
|
+
// Fallback: find milestone section heading containing the version, then
|
|
1044
|
+
// scan for Phase N: headers within that section to determine the range.
|
|
1045
|
+
const sectionHeadingPattern = new RegExp(
|
|
1046
|
+
`^(#{2,4})\\s+.*${escapedVersion}[^\\n]*$`,
|
|
1047
|
+
'im'
|
|
1048
|
+
);
|
|
1049
|
+
const sectionMatch = content.match(sectionHeadingPattern);
|
|
1050
|
+
if (!sectionMatch) {
|
|
1051
|
+
throw new Error(`Milestone ${version} not found in ROADMAP.md`);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const headingLevel = sectionMatch[1].length;
|
|
1055
|
+
const sectionStart = sectionMatch.index + sectionMatch[0].length;
|
|
1056
|
+
|
|
1057
|
+
// Find the end of this section (next heading at same or higher level)
|
|
1058
|
+
const endPattern = new RegExp(
|
|
1059
|
+
`^#{2,${headingLevel}}\\s+`,
|
|
1060
|
+
'im'
|
|
1061
|
+
);
|
|
1062
|
+
const restContent = content.slice(sectionStart);
|
|
1063
|
+
const endMatch = restContent.match(endPattern);
|
|
1064
|
+
const sectionContent = endMatch
|
|
1065
|
+
? restContent.slice(0, endMatch.index)
|
|
1066
|
+
: restContent;
|
|
1067
|
+
|
|
1068
|
+
// Collect Phase N: headers within this section
|
|
1069
|
+
const phaseHeaderPattern = /Phase\s+(\d+)\s*:/gi;
|
|
1070
|
+
const phaseNumbers = [];
|
|
1071
|
+
let phMatch;
|
|
1072
|
+
while ((phMatch = phaseHeaderPattern.exec(sectionContent)) !== null) {
|
|
1073
|
+
phaseNumbers.push(parseInt(phMatch[1], 10));
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// If section-based search found nothing (common when ROADMAP has peer-level
|
|
1077
|
+
// ## headings like ## v1.0, ## Overview, ## Phase Details), search the entire
|
|
1078
|
+
// ROADMAP for Phase N: headings. For single-milestone ROADMAPs all phases
|
|
1079
|
+
// belong to the milestone.
|
|
1080
|
+
if (phaseNumbers.length === 0) {
|
|
1081
|
+
const fullPhasePattern = new RegExp(
|
|
1082
|
+
`Phase\\s+(\\d+)`,
|
|
1083
|
+
'gi'
|
|
1084
|
+
);
|
|
1085
|
+
let fpMatch;
|
|
1086
|
+
while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
|
|
1087
|
+
phaseNumbers.push(parseInt(fpMatch[1], 10));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (phaseNumbers.length === 0) {
|
|
1092
|
+
throw new Error(`Milestone ${version} not found in ROADMAP.md`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
phaseStart = Math.min(...phaseNumbers);
|
|
1096
|
+
phaseEnd = Math.max(...phaseNumbers);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Extract all phase detail sections: ### Phase N: Name
|
|
1100
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi;
|
|
1101
|
+
const phases = [];
|
|
1102
|
+
let match;
|
|
1103
|
+
|
|
1104
|
+
while ((match = phasePattern.exec(content)) !== null) {
|
|
1105
|
+
const phaseNum = match[1];
|
|
1106
|
+
const phaseFloat = parseFloat(phaseNum);
|
|
1107
|
+
|
|
1108
|
+
// Only include phases in this milestone's range
|
|
1109
|
+
if (phaseFloat < phaseStart || phaseFloat > phaseEnd) continue;
|
|
1110
|
+
|
|
1111
|
+
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
1112
|
+
|
|
1113
|
+
// Determine disk status by examining the phases directory
|
|
1114
|
+
const padded = phaseNum.replace(/^(\d+)/, (m) => m.padStart(2, '0'));
|
|
1115
|
+
let diskStatus = 'no_directory';
|
|
1116
|
+
|
|
1117
|
+
try {
|
|
1118
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1119
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1120
|
+
const dirMatch = dirs.find(d => d.startsWith(padded + '-') || d === padded);
|
|
1121
|
+
|
|
1122
|
+
if (dirMatch) {
|
|
1123
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
|
|
1124
|
+
const planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
1125
|
+
const summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
1126
|
+
const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
1127
|
+
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
1128
|
+
|
|
1129
|
+
if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
|
|
1130
|
+
else if (summaryCount > 0) diskStatus = 'partial';
|
|
1131
|
+
else if (planCount > 0) diskStatus = 'planned';
|
|
1132
|
+
else if (hasResearch) diskStatus = 'researched';
|
|
1133
|
+
else if (hasContext) diskStatus = 'discussed';
|
|
1134
|
+
else diskStatus = 'empty';
|
|
1135
|
+
}
|
|
1136
|
+
} catch {}
|
|
1137
|
+
|
|
1138
|
+
// Check ROADMAP checkbox status
|
|
1139
|
+
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}`, 'i');
|
|
1140
|
+
const checkboxMatch = content.match(checkboxPattern);
|
|
1141
|
+
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
1142
|
+
|
|
1143
|
+
phases.push({
|
|
1144
|
+
number: phaseNum,
|
|
1145
|
+
name: phaseName,
|
|
1146
|
+
disk_status: diskStatus,
|
|
1147
|
+
roadmap_complete: roadmapComplete,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return phases;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Detect the active milestone version from ROADMAP.md.
|
|
1156
|
+
*
|
|
1157
|
+
* @param {string} content - ROADMAP.md content
|
|
1158
|
+
* @returns {string|null} Version string (e.g., "v6.0") or null if not found
|
|
1159
|
+
*/
|
|
1160
|
+
function detectActiveMilestone(content) {
|
|
1161
|
+
// Primary: heading with (In Progress)
|
|
1162
|
+
const match = content.match(/##\s+.*?(v\d+\.\d+).*?\((?:In|in)\s+[Pp]rogress\)/i);
|
|
1163
|
+
if (match) return match[1];
|
|
1164
|
+
// Fallback: milestone list item with (in progress)
|
|
1165
|
+
const listMatch = content.match(/-\s+.*?(v\d+\.\d+).*?\((?:in\s+progress)\)/i);
|
|
1166
|
+
return listMatch ? listMatch[1] : null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// ─── Milestone Job CLI Wrappers ─────────────────────────────────────────────
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Create a milestone job file from roadmap analysis.
|
|
1173
|
+
*
|
|
1174
|
+
* @param {string} cwd - Working directory
|
|
1175
|
+
* @param {string|null} version - Milestone version (null = auto-detect)
|
|
1176
|
+
* @param {boolean} check - Include audit/complete steps
|
|
1177
|
+
* @param {boolean} raw - Raw output mode
|
|
1178
|
+
* @returns {Object} Result JSON (when called directly, not via CLI)
|
|
1179
|
+
*/
|
|
1180
|
+
function cmdJobsCreateMilestone(cwd, version, check, raw) {
|
|
1181
|
+
const roadmapPath = path.join(resolveProjectRoot(cwd), 'ROADMAP.md');
|
|
1182
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
1183
|
+
throw new Error('ROADMAP.md not found at ' + roadmapPath);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1187
|
+
|
|
1188
|
+
// Resolve version
|
|
1189
|
+
let resolvedVersion = version;
|
|
1190
|
+
if (!resolvedVersion) {
|
|
1191
|
+
resolvedVersion = detectActiveMilestone(roadmapContent);
|
|
1192
|
+
if (!resolvedVersion) {
|
|
1193
|
+
throw new Error('No active milestone found in ROADMAP.md');
|
|
1194
|
+
}
|
|
1195
|
+
} else {
|
|
1196
|
+
// Validate the version exists in ROADMAP
|
|
1197
|
+
const escapedV = resolvedVersion.replace(/\./g, '\\.');
|
|
1198
|
+
const versionPattern = new RegExp(escapedV + '\\s+', 'i');
|
|
1199
|
+
if (!versionPattern.test(roadmapContent)) {
|
|
1200
|
+
throw new Error(`Milestone ${resolvedVersion} not found in ROADMAP.md`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Analyze phases for this milestone
|
|
1205
|
+
const phases = analyzeMilestonePhases(cwd, resolvedVersion);
|
|
1206
|
+
|
|
1207
|
+
// Generate steps
|
|
1208
|
+
const steps = generateMilestoneSteps(phases, { check, version: resolvedVersion });
|
|
1209
|
+
|
|
1210
|
+
// Resolve git identity for Created_by (best-effort; don't block creation)
|
|
1211
|
+
let createdBy = null;
|
|
1212
|
+
try {
|
|
1213
|
+
const identity = requireGitIdentity(cwd);
|
|
1214
|
+
createdBy = formatAuthorString(identity);
|
|
1215
|
+
} catch {
|
|
1216
|
+
// Identity resolution is best-effort for jobs; don't block creation
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Build file content
|
|
1220
|
+
const content = buildJobFileContent(resolvedVersion, check, steps, createdBy);
|
|
1221
|
+
|
|
1222
|
+
// Write to pending directory
|
|
1223
|
+
const pendingDir = path.join(getPlanningRoot(cwd), 'jobs', 'pending');
|
|
1224
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
1225
|
+
const fileName = `milestone-${resolvedVersion}.md`;
|
|
1226
|
+
const filePath = path.join(pendingDir, fileName);
|
|
1227
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
1228
|
+
|
|
1229
|
+
// Count phases that contribute at least one step
|
|
1230
|
+
const phasesWithSteps = phases.filter(p =>
|
|
1231
|
+
p.disk_status !== 'complete' && p.roadmap_complete !== true
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
const result = {
|
|
1235
|
+
created: true,
|
|
1236
|
+
version: resolvedVersion,
|
|
1237
|
+
file: path.join(path.relative(cwd, getPlanningRoot(cwd)) || '.', 'jobs', 'pending', fileName),
|
|
1238
|
+
step_count: steps.length,
|
|
1239
|
+
check,
|
|
1240
|
+
phase_count: phasesWithSteps.length,
|
|
1241
|
+
steps_preview: steps.map(s => `/dgs:${s.command} ${s.args}`),
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
return result;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Preview a milestone job without writing any file.
|
|
1249
|
+
*
|
|
1250
|
+
* @param {string} cwd - Working directory
|
|
1251
|
+
* @param {string|null} version - Milestone version (null = auto-detect)
|
|
1252
|
+
* @param {boolean} check - Include audit/complete steps
|
|
1253
|
+
* @param {boolean} raw - Raw output mode
|
|
1254
|
+
* @returns {Object} Preview JSON
|
|
1255
|
+
*/
|
|
1256
|
+
function cmdJobsMilestonePreview(cwd, version, check, raw) {
|
|
1257
|
+
const roadmapPath = path.join(resolveProjectRoot(cwd), 'ROADMAP.md');
|
|
1258
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
1259
|
+
throw new Error('ROADMAP.md not found at ' + roadmapPath);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1263
|
+
|
|
1264
|
+
// Resolve version
|
|
1265
|
+
let resolvedVersion = version;
|
|
1266
|
+
if (!resolvedVersion) {
|
|
1267
|
+
resolvedVersion = detectActiveMilestone(roadmapContent);
|
|
1268
|
+
if (!resolvedVersion) {
|
|
1269
|
+
throw new Error('No active milestone found in ROADMAP.md');
|
|
1270
|
+
}
|
|
1271
|
+
} else {
|
|
1272
|
+
const escapedV = resolvedVersion.replace(/\./g, '\\.');
|
|
1273
|
+
const versionPattern = new RegExp(escapedV + '\\s+', 'i');
|
|
1274
|
+
if (!versionPattern.test(roadmapContent)) {
|
|
1275
|
+
throw new Error(`Milestone ${resolvedVersion} not found in ROADMAP.md`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Analyze phases
|
|
1280
|
+
const phases = analyzeMilestonePhases(cwd, resolvedVersion);
|
|
1281
|
+
|
|
1282
|
+
// Generate steps
|
|
1283
|
+
const steps = generateMilestoneSteps(phases, { check, version: resolvedVersion });
|
|
1284
|
+
|
|
1285
|
+
// Build content (but do NOT write)
|
|
1286
|
+
const content = buildJobFileContent(resolvedVersion, check, steps);
|
|
1287
|
+
|
|
1288
|
+
// Count phases with steps
|
|
1289
|
+
const phasesWithSteps = phases.filter(p =>
|
|
1290
|
+
p.disk_status !== 'complete' && p.roadmap_complete !== true
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
return {
|
|
1294
|
+
preview: true,
|
|
1295
|
+
version: resolvedVersion,
|
|
1296
|
+
check,
|
|
1297
|
+
step_count: steps.length,
|
|
1298
|
+
phase_count: phasesWithSteps.length,
|
|
1299
|
+
steps_preview: steps.map(s => `/dgs:${s.command} ${s.args}`),
|
|
1300
|
+
content,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// ─── Visibility & Polish Functions ──────────────────────────────────────────
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* List all job files across pending/in-progress/completed, grouped by status.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {string} cwd - Working directory
|
|
1310
|
+
* @returns {{ in_progress: Array, pending: Array, completed: Array }}
|
|
1311
|
+
*/
|
|
1312
|
+
function listJobs(cwd) {
|
|
1313
|
+
const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
|
|
1314
|
+
const groups = { in_progress: [], pending: [], completed: [] };
|
|
1315
|
+
const dirMap = { 'in-progress': 'in_progress', 'pending': 'pending', 'completed': 'completed' };
|
|
1316
|
+
|
|
1317
|
+
for (const [dirName, groupKey] of Object.entries(dirMap)) {
|
|
1318
|
+
const dirPath = path.join(jobsDir, dirName);
|
|
1319
|
+
if (!fs.existsSync(dirPath)) {
|
|
1320
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let files;
|
|
1324
|
+
try {
|
|
1325
|
+
files = fs.readdirSync(dirPath).filter(f => f.startsWith('milestone-') && f.endsWith('.md'));
|
|
1326
|
+
} catch {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Sort by modification time, most recent first
|
|
1331
|
+
const fileStats = files.map(f => {
|
|
1332
|
+
const fullPath = path.join(dirPath, f);
|
|
1333
|
+
let mtime = 0;
|
|
1334
|
+
try { mtime = fs.statSync(fullPath).mtimeMs; } catch {}
|
|
1335
|
+
return { file: f, fullPath, mtime };
|
|
1336
|
+
});
|
|
1337
|
+
fileStats.sort((a, b) => b.mtime - a.mtime);
|
|
1338
|
+
|
|
1339
|
+
for (const { file, fullPath } of fileStats) {
|
|
1340
|
+
try {
|
|
1341
|
+
const parsed = parseJobFile(fullPath);
|
|
1342
|
+
groups[groupKey].push({
|
|
1343
|
+
version: parsed.version,
|
|
1344
|
+
status: parsed.status,
|
|
1345
|
+
check: parsed.check,
|
|
1346
|
+
created_by: parsed.created_by,
|
|
1347
|
+
progress: `${parsed.completedCount}/${parsed.stepCount}`,
|
|
1348
|
+
file: fullPath,
|
|
1349
|
+
});
|
|
1350
|
+
} catch {
|
|
1351
|
+
// Skip unparseable files
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return groups;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Cancel an in-progress job: reset [>] steps to [ ], set Status to pending, move to pending/.
|
|
1361
|
+
*
|
|
1362
|
+
* @param {string} cwd - Working directory
|
|
1363
|
+
* @param {string} version - Milestone version (e.g., "v6.0")
|
|
1364
|
+
* @returns {{ cancelled: boolean, version?: string, path?: string, steps_reset?: number, reason?: string }}
|
|
1365
|
+
*/
|
|
1366
|
+
function cancelJob(cwd, version) {
|
|
1367
|
+
const found = findJobFile(cwd, version);
|
|
1368
|
+
if (!found.found) {
|
|
1369
|
+
return { cancelled: false, reason: 'not_found' };
|
|
1370
|
+
}
|
|
1371
|
+
if (found.directory !== 'in-progress') {
|
|
1372
|
+
return { cancelled: false, reason: 'not_in_progress' };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const filePath = found.path;
|
|
1376
|
+
|
|
1377
|
+
// Read and find all in-progress steps
|
|
1378
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1379
|
+
const lines = content.split('\n');
|
|
1380
|
+
const stepLines = [];
|
|
1381
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1382
|
+
const trimmed = lines[i].trim();
|
|
1383
|
+
if (trimmed.startsWith('- [')) {
|
|
1384
|
+
const step = parseStepLine(trimmed, stepLines.length);
|
|
1385
|
+
if (step) {
|
|
1386
|
+
stepLines.push({ lineIndex: i, step });
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Reset in-progress steps to pending
|
|
1392
|
+
let stepsReset = 0;
|
|
1393
|
+
for (const { step } of stepLines) {
|
|
1394
|
+
if (step.status === 'in-progress') {
|
|
1395
|
+
updateJobStep(filePath, step.index, 'pending');
|
|
1396
|
+
stepsReset++;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Update Status header to pending
|
|
1401
|
+
updateJobHeader(filePath, 'Status', 'pending');
|
|
1402
|
+
|
|
1403
|
+
// Move to pending/
|
|
1404
|
+
const pendingDir = path.join(getPlanningRoot(cwd), 'jobs', 'pending');
|
|
1405
|
+
const moveResult = moveJobFile(filePath, pendingDir);
|
|
1406
|
+
|
|
1407
|
+
return {
|
|
1408
|
+
cancelled: true,
|
|
1409
|
+
version: version.startsWith('v') ? version : 'v' + version,
|
|
1410
|
+
path: moveResult.to,
|
|
1411
|
+
steps_reset: stepsReset,
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Validate jobs directory structure. Auto-create missing directories.
|
|
1417
|
+
*
|
|
1418
|
+
* @param {string} cwd - Working directory
|
|
1419
|
+
* @returns {{ healthy: boolean, directories: Array, job_count: number, issues?: Array }}
|
|
1420
|
+
*/
|
|
1421
|
+
function healthCheck(cwd) {
|
|
1422
|
+
const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
|
|
1423
|
+
const requiredDirs = ['pending', 'in-progress', 'completed'];
|
|
1424
|
+
const directories = [];
|
|
1425
|
+
const issues = [];
|
|
1426
|
+
let jobCount = 0;
|
|
1427
|
+
|
|
1428
|
+
for (const dirName of requiredDirs) {
|
|
1429
|
+
const dirPath = path.join(jobsDir, dirName);
|
|
1430
|
+
const exists = fs.existsSync(dirPath);
|
|
1431
|
+
let created = false;
|
|
1432
|
+
|
|
1433
|
+
if (!exists) {
|
|
1434
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1435
|
+
created = true;
|
|
1436
|
+
issues.push(`Missing directory auto-created: ${dirName}/`);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
directories.push({ name: dirName, exists: exists || created, created });
|
|
1440
|
+
|
|
1441
|
+
// Scan for job files
|
|
1442
|
+
try {
|
|
1443
|
+
const files = fs.readdirSync(dirPath).filter(f => f.startsWith('milestone-') && f.endsWith('.md'));
|
|
1444
|
+
jobCount += files.length;
|
|
1445
|
+
|
|
1446
|
+
// Validate each file
|
|
1447
|
+
for (const file of files) {
|
|
1448
|
+
const filePath = path.join(dirPath, file);
|
|
1449
|
+
try {
|
|
1450
|
+
parseJobFile(filePath);
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
issues.push(`Parse failure in ${dirName}/${file}: ${err.message}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
} catch {
|
|
1456
|
+
// Directory just created, no files
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return {
|
|
1461
|
+
healthy: issues.length === 0,
|
|
1462
|
+
directories,
|
|
1463
|
+
job_count: jobCount,
|
|
1464
|
+
issues,
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Preview a job's steps with status annotations and precondition warnings.
|
|
1470
|
+
*
|
|
1471
|
+
* @param {string} cwd - Working directory
|
|
1472
|
+
* @param {string} version - Milestone version
|
|
1473
|
+
* @returns {{ found: boolean, version?: string, status?: string, steps?: Array, resume_from?: number, warnings?: Array }}
|
|
1474
|
+
*/
|
|
1475
|
+
function dryRunPreview(cwd, version) {
|
|
1476
|
+
const found = findJobFile(cwd, version);
|
|
1477
|
+
if (!found.found) {
|
|
1478
|
+
return { found: false };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const parsed = parseJobFile(found.path);
|
|
1482
|
+
const steps = parsed.steps.map(step => {
|
|
1483
|
+
let marker;
|
|
1484
|
+
if (step.status === 'completed') marker = '[x]';
|
|
1485
|
+
else if (step.status === 'in-progress') marker = '[>]';
|
|
1486
|
+
else if (step.status === 'failed') marker = '[!]';
|
|
1487
|
+
else marker = '[ ]';
|
|
1488
|
+
|
|
1489
|
+
return {
|
|
1490
|
+
index: step.index,
|
|
1491
|
+
command: step.command,
|
|
1492
|
+
args: step.args,
|
|
1493
|
+
status: step.status,
|
|
1494
|
+
display: `${marker} /dgs:${step.command}${step.args ? ' ' + step.args : ''}`,
|
|
1495
|
+
};
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// Resume from first non-completed step
|
|
1499
|
+
const firstNonCompleted = parsed.steps.find(s => s.status !== 'completed');
|
|
1500
|
+
const resumeFrom = firstNonCompleted ? firstNonCompleted.index : null;
|
|
1501
|
+
|
|
1502
|
+
// Precondition warnings
|
|
1503
|
+
const planRoot = getPlanningRoot(cwd);
|
|
1504
|
+
const projectRootForHealth = resolveProjectRoot(cwd);
|
|
1505
|
+
const warnings = [];
|
|
1506
|
+
if (!fs.existsSync(planRoot)) {
|
|
1507
|
+
warnings.push('Planning root directory missing');
|
|
1508
|
+
}
|
|
1509
|
+
if (!fs.existsSync(path.join(projectRootForHealth, 'ROADMAP.md'))) {
|
|
1510
|
+
warnings.push('ROADMAP.md missing');
|
|
1511
|
+
}
|
|
1512
|
+
if (!fs.existsSync(path.join(projectRootForHealth, 'STATE.md'))) {
|
|
1513
|
+
warnings.push('STATE.md missing');
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
return {
|
|
1517
|
+
found: true,
|
|
1518
|
+
version: parsed.version,
|
|
1519
|
+
status: parsed.status,
|
|
1520
|
+
steps,
|
|
1521
|
+
resume_from: resumeFrom,
|
|
1522
|
+
warnings,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* Generate a markdown summary report for a job.
|
|
1528
|
+
*
|
|
1529
|
+
* @param {string} cwd - Working directory
|
|
1530
|
+
* @param {string} version - Milestone version
|
|
1531
|
+
* @returns {{ found: boolean, version?: string, content?: string }}
|
|
1532
|
+
*/
|
|
1533
|
+
function generateJobSummary(cwd, version) {
|
|
1534
|
+
const found = findJobFile(cwd, version);
|
|
1535
|
+
if (!found.found) {
|
|
1536
|
+
return { found: false };
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const parsed = parseJobFile(found.path);
|
|
1540
|
+
const normalizedVersion = parsed.version;
|
|
1541
|
+
|
|
1542
|
+
let md = `# Job Summary: milestone-${normalizedVersion}\n\n`;
|
|
1543
|
+
md += `## Overview\n\n`;
|
|
1544
|
+
md += `- **Version:** ${normalizedVersion}\n`;
|
|
1545
|
+
md += `- **Status:** ${parsed.status}\n`;
|
|
1546
|
+
if (parsed.created_by) {
|
|
1547
|
+
md += `- **Created_by:** ${parsed.created_by}\n`;
|
|
1548
|
+
}
|
|
1549
|
+
md += `- **Created:** ${parsed.created}\n`;
|
|
1550
|
+
md += `- **Total Steps:** ${parsed.stepCount}\n`;
|
|
1551
|
+
md += `- **Completed Steps:** ${parsed.completedCount}\n`;
|
|
1552
|
+
md += `- **Failed Steps:** ${parsed.failedCount}\n\n`;
|
|
1553
|
+
|
|
1554
|
+
// Per-step timing table
|
|
1555
|
+
md += `## Step Details\n\n`;
|
|
1556
|
+
md += `| # | Command | Started | Duration | Status |\n`;
|
|
1557
|
+
md += `|---|---------|---------|----------|--------|\n`;
|
|
1558
|
+
|
|
1559
|
+
for (let i = 0; i < parsed.steps.length; i++) {
|
|
1560
|
+
const step = parsed.steps[i];
|
|
1561
|
+
const cmd = `/dgs:${step.command}${step.args ? ' ' + step.args : ''}`;
|
|
1562
|
+
const started = step.timestamp || '-';
|
|
1563
|
+
|
|
1564
|
+
// Calculate duration from consecutive timestamps
|
|
1565
|
+
let duration = '-';
|
|
1566
|
+
if (step.timestamp && i + 1 < parsed.steps.length && parsed.steps[i + 1].timestamp) {
|
|
1567
|
+
const startMs = new Date(step.timestamp).getTime();
|
|
1568
|
+
const endMs = new Date(parsed.steps[i + 1].timestamp).getTime();
|
|
1569
|
+
if (!isNaN(startMs) && !isNaN(endMs) && endMs > startMs) {
|
|
1570
|
+
const diffSec = Math.round((endMs - startMs) / 1000);
|
|
1571
|
+
if (diffSec < 60) duration = `${diffSec}s`;
|
|
1572
|
+
else duration = `${Math.round(diffSec / 60)}m`;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
md += `| ${i} | ${cmd} | ${started} | ${duration} | ${step.status} |\n`;
|
|
1577
|
+
}
|
|
1578
|
+
md += `\n`;
|
|
1579
|
+
|
|
1580
|
+
// Errors section
|
|
1581
|
+
const failedSteps = parsed.steps.filter(s => s.status === 'failed');
|
|
1582
|
+
if (failedSteps.length > 0) {
|
|
1583
|
+
md += `## Errors\n\n`;
|
|
1584
|
+
for (const step of failedSteps) {
|
|
1585
|
+
const cmd = `/dgs:${step.command}${step.args ? ' ' + step.args : ''}`;
|
|
1586
|
+
md += `- **Step ${step.index}** (${cmd}): ${step.error || 'No error message recorded'}\n`;
|
|
1587
|
+
}
|
|
1588
|
+
md += `\n`;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// Auto-resolve audit section
|
|
1592
|
+
const autoSteps = parsed.steps.filter(s => s.args && (s.args.includes('--auto') || s.args.includes('--non-interactive')));
|
|
1593
|
+
if (autoSteps.length > 0) {
|
|
1594
|
+
md += `## Auto-Resolved Steps\n\n`;
|
|
1595
|
+
for (const step of autoSteps) {
|
|
1596
|
+
const cmd = `/dgs:${step.command}${step.args ? ' ' + step.args : ''}`;
|
|
1597
|
+
md += `- Step ${step.index}: ${cmd} (auto-resolved)\n`;
|
|
1598
|
+
}
|
|
1599
|
+
md += `\n`;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Human verification scanning — find UAT files with human_needed entries
|
|
1603
|
+
let humanVerifications = [];
|
|
1604
|
+
try {
|
|
1605
|
+
const milestonePhases = analyzeMilestonePhases(cwd, normalizedVersion);
|
|
1606
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
1607
|
+
const phasesDir = path.join(projectRoot, 'phases');
|
|
1608
|
+
|
|
1609
|
+
for (const phase of milestonePhases) {
|
|
1610
|
+
const phaseNum = phase.number;
|
|
1611
|
+
const padded = String(phaseNum).replace(/^(\d+)/, (m) => m.padStart(2, '0'));
|
|
1612
|
+
|
|
1613
|
+
// Find the phase directory on disk
|
|
1614
|
+
let phaseDir = null;
|
|
1615
|
+
try {
|
|
1616
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1617
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1618
|
+
phaseDir = dirs.find(d => d.startsWith(padded + '-') || d === padded);
|
|
1619
|
+
} catch {}
|
|
1620
|
+
|
|
1621
|
+
if (!phaseDir) continue;
|
|
1622
|
+
|
|
1623
|
+
// Look for UAT file
|
|
1624
|
+
const uatPath = path.join(phasesDir, phaseDir, `${padded}-UAT.md`);
|
|
1625
|
+
let uatContent = null;
|
|
1626
|
+
try {
|
|
1627
|
+
uatContent = fs.readFileSync(uatPath, 'utf-8');
|
|
1628
|
+
} catch {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Check for mode: auto-test in frontmatter (first ~10 lines)
|
|
1633
|
+
const headerLines = uatContent.split('\n').slice(0, 15).join('\n');
|
|
1634
|
+
if (!/mode:\s*auto-test/i.test(headerLines)) continue;
|
|
1635
|
+
|
|
1636
|
+
// Parse test blocks for human_needed entries
|
|
1637
|
+
const blocks = uatContent.split(/(?=^### \d+\.)/m);
|
|
1638
|
+
for (const block of blocks) {
|
|
1639
|
+
const headingMatch = block.match(/^### \d+\.\s+(.+)/);
|
|
1640
|
+
if (!headingMatch) continue;
|
|
1641
|
+
|
|
1642
|
+
if (!/^result:\s*human_needed\s*$/m.test(block)) continue;
|
|
1643
|
+
|
|
1644
|
+
const description = headingMatch[1].trim();
|
|
1645
|
+
const sourceMatch = block.match(/^source:\s*(.+)\s*$/m);
|
|
1646
|
+
const source = sourceMatch ? sourceMatch[1].trim() : '-';
|
|
1647
|
+
|
|
1648
|
+
humanVerifications.push({
|
|
1649
|
+
phase: phaseNum,
|
|
1650
|
+
description,
|
|
1651
|
+
source,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
} catch {
|
|
1656
|
+
// If milestone phase analysis fails (e.g., no ROADMAP.md), skip silently
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (humanVerifications.length > 0) {
|
|
1660
|
+
md += `## Outstanding Human Verifications\n\n`;
|
|
1661
|
+
md += `The following tests require manual verification:\n\n`;
|
|
1662
|
+
md += `| Phase | Test | Source |\n`;
|
|
1663
|
+
md += `|-------|------|--------|\n`;
|
|
1664
|
+
for (const hv of humanVerifications) {
|
|
1665
|
+
md += `| ${hv.phase} | ${hv.description} | ${hv.source} |\n`;
|
|
1666
|
+
}
|
|
1667
|
+
md += `\nTotal: ${humanVerifications.length} human verification(s) remaining.\n\n`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return {
|
|
1671
|
+
found: true,
|
|
1672
|
+
version: normalizedVersion,
|
|
1673
|
+
content: md,
|
|
1674
|
+
human_verification_count: humanVerifications.length,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// ─── SHA Recording & Rollback ───────────────────────────────────────────────
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Record starting commit SHAs for all registered code repos and the planning repo.
|
|
1682
|
+
* Writes a **StartShas:** header line into the job file.
|
|
1683
|
+
*
|
|
1684
|
+
* @param {string} cwd - Working directory
|
|
1685
|
+
* @param {string} jobFilePath - Absolute path to the job file
|
|
1686
|
+
* @returns {{ recorded: boolean, shas?: Object, reason?: string }}
|
|
1687
|
+
*/
|
|
1688
|
+
function recordStartShas(cwd, jobFilePath) {
|
|
1689
|
+
try {
|
|
1690
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
1691
|
+
const shas = {};
|
|
1692
|
+
|
|
1693
|
+
// Record planning repo SHA
|
|
1694
|
+
try {
|
|
1695
|
+
const planningSha = execSync('git rev-parse HEAD', { cwd: planningRoot, stdio: 'pipe' }).toString().trim();
|
|
1696
|
+
shas['_planning'] = planningSha;
|
|
1697
|
+
} catch (gitErr) {
|
|
1698
|
+
return { recorded: false, reason: `Failed to get planning repo SHA: ${gitErr.message}` };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// Check for REPOS.md (v2 multi-repo setup)
|
|
1702
|
+
const reposPath = path.join(planningRoot, 'REPOS.md');
|
|
1703
|
+
if (fs.existsSync(reposPath)) {
|
|
1704
|
+
const reposContent = fs.readFileSync(reposPath, 'utf-8');
|
|
1705
|
+
// Parse the markdown table: | Name | Path | ...
|
|
1706
|
+
const lines = reposContent.split('\n');
|
|
1707
|
+
for (const line of lines) {
|
|
1708
|
+
const match = line.match(/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/);
|
|
1709
|
+
if (!match) continue;
|
|
1710
|
+
const name = match[1].trim();
|
|
1711
|
+
const repoPath = match[2].trim();
|
|
1712
|
+
// Skip header row and separator
|
|
1713
|
+
if (name === 'Name' || name.startsWith('-')) continue;
|
|
1714
|
+
if (!repoPath || repoPath.startsWith('-')) continue;
|
|
1715
|
+
|
|
1716
|
+
// Resolve absolute path: paths in REPOS.md are relative to planning root's parent
|
|
1717
|
+
const absRepoPath = path.resolve(path.dirname(planningRoot), repoPath);
|
|
1718
|
+
if (!fs.existsSync(absRepoPath)) continue;
|
|
1719
|
+
|
|
1720
|
+
try {
|
|
1721
|
+
const sha = execSync('git rev-parse HEAD', { cwd: absRepoPath, stdio: 'pipe' }).toString().trim();
|
|
1722
|
+
shas[name] = sha;
|
|
1723
|
+
} catch {
|
|
1724
|
+
// Skip repos that aren't git repos or have issues
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Write StartShas into the job file (after header block, before ## Steps)
|
|
1730
|
+
const content = fs.readFileSync(jobFilePath, 'utf-8');
|
|
1731
|
+
|
|
1732
|
+
// Check if StartShas already exists
|
|
1733
|
+
if (/^\*\*StartShas:\*\*\s+/m.test(content)) {
|
|
1734
|
+
return { recorded: true, shas, already_existed: true };
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const shaJson = JSON.stringify(shas);
|
|
1738
|
+
const lines = content.split('\n');
|
|
1739
|
+
|
|
1740
|
+
// Find the ## Steps line and insert before it
|
|
1741
|
+
const stepsIdx = lines.findIndex(l => /^## Steps/.test(l));
|
|
1742
|
+
if (stepsIdx !== -1) {
|
|
1743
|
+
lines.splice(stepsIdx, 0, `**StartShas:** ${shaJson}`, '');
|
|
1744
|
+
} else {
|
|
1745
|
+
// Fallback: insert after Check header line
|
|
1746
|
+
const checkIdx = lines.findIndex(l => /^\*\*Check:\*\*\s+/.test(l));
|
|
1747
|
+
if (checkIdx !== -1) {
|
|
1748
|
+
lines.splice(checkIdx + 1, 0, `**StartShas:** ${shaJson}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
fs.writeFileSync(jobFilePath, lines.join('\n'), 'utf-8');
|
|
1753
|
+
return { recorded: true, shas };
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
return { recorded: false, reason: err.message };
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Roll back all code repo changes made by a milestone job.
|
|
1761
|
+
* Reads StartShas from the job file, resets CODE repos (not planning),
|
|
1762
|
+
* deletes SUMMARY.md files for executed phases, and marks job as rolled_back.
|
|
1763
|
+
*
|
|
1764
|
+
* @param {string} cwd - Working directory
|
|
1765
|
+
* @param {string} version - Milestone version (e.g., "v6.0")
|
|
1766
|
+
* @returns {{ rolledBack: boolean, version?: string, repos_reset?: Array, summaries_deleted?: Array, path?: string, reason?: string }}
|
|
1767
|
+
*/
|
|
1768
|
+
function rollbackJob(cwd, version) {
|
|
1769
|
+
const found = findJobFile(cwd, version);
|
|
1770
|
+
if (!found.found) {
|
|
1771
|
+
return { rolledBack: false, reason: 'not_found' };
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const filePath = found.path;
|
|
1775
|
+
const parsed = parseJobFile(filePath);
|
|
1776
|
+
|
|
1777
|
+
if (!parsed.startShas) {
|
|
1778
|
+
return { rolledBack: false, reason: 'no_start_shas' };
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
1782
|
+
const reposReset = [];
|
|
1783
|
+
const summariesDeleted = [];
|
|
1784
|
+
|
|
1785
|
+
// Reset code repos (all keys except _planning)
|
|
1786
|
+
for (const [repoName, sha] of Object.entries(parsed.startShas)) {
|
|
1787
|
+
if (repoName === '_planning') continue;
|
|
1788
|
+
|
|
1789
|
+
// Resolve repo path: check REPOS.md for the path
|
|
1790
|
+
let absRepoPath = null;
|
|
1791
|
+
const reposPath = path.join(planningRoot, 'REPOS.md');
|
|
1792
|
+
if (fs.existsSync(reposPath)) {
|
|
1793
|
+
const reposContent = fs.readFileSync(reposPath, 'utf-8');
|
|
1794
|
+
const repoLines = reposContent.split('\n');
|
|
1795
|
+
for (const line of repoLines) {
|
|
1796
|
+
const match = line.match(/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/);
|
|
1797
|
+
if (!match) continue;
|
|
1798
|
+
const name = match[1].trim();
|
|
1799
|
+
const repoPathStr = match[2].trim();
|
|
1800
|
+
if (name === repoName && repoPathStr && !repoPathStr.startsWith('-')) {
|
|
1801
|
+
absRepoPath = path.resolve(path.dirname(planningRoot), repoPathStr);
|
|
1802
|
+
break;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (!absRepoPath || !fs.existsSync(absRepoPath)) continue;
|
|
1808
|
+
|
|
1809
|
+
try {
|
|
1810
|
+
execSync(`git reset --hard ${sha}`, { cwd: absRepoPath, stdio: 'pipe' });
|
|
1811
|
+
reposReset.push({ name: repoName, sha, path: absRepoPath });
|
|
1812
|
+
} catch (gitErr) {
|
|
1813
|
+
// Log but continue with other repos
|
|
1814
|
+
reposReset.push({ name: repoName, sha, error: gitErr.message });
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// Identify executed phases from completed steps
|
|
1819
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
1820
|
+
const phasesDir = path.join(projectRoot, 'phases');
|
|
1821
|
+
|
|
1822
|
+
for (const step of parsed.steps) {
|
|
1823
|
+
if (step.status === 'completed' && step.command === 'execute-phase') {
|
|
1824
|
+
// Extract phase number from args (first token)
|
|
1825
|
+
const phaseNum = step.args.split(/\s+/)[0];
|
|
1826
|
+
if (!phaseNum) continue;
|
|
1827
|
+
|
|
1828
|
+
const padded = phaseNum.replace(/^(\d+)/, (m) => m.padStart(2, '0'));
|
|
1829
|
+
|
|
1830
|
+
try {
|
|
1831
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1832
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1833
|
+
const dirMatch = dirs.find(d => d.startsWith(padded + '-') || d === padded);
|
|
1834
|
+
|
|
1835
|
+
if (dirMatch) {
|
|
1836
|
+
const phaseDir = path.join(phasesDir, dirMatch);
|
|
1837
|
+
const phaseFiles = fs.readdirSync(phaseDir);
|
|
1838
|
+
const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
1839
|
+
|
|
1840
|
+
for (const sf of summaryFiles) {
|
|
1841
|
+
const sfPath = path.join(phaseDir, sf);
|
|
1842
|
+
try {
|
|
1843
|
+
fs.unlinkSync(sfPath);
|
|
1844
|
+
summariesDeleted.push(sfPath);
|
|
1845
|
+
} catch {
|
|
1846
|
+
// Skip files that can't be deleted
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
} catch {
|
|
1851
|
+
// Phase directory doesn't exist, skip
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// Update job status to rolled_back
|
|
1857
|
+
updateJobHeader(filePath, 'Status', 'rolled_back');
|
|
1858
|
+
|
|
1859
|
+
return {
|
|
1860
|
+
rolledBack: true,
|
|
1861
|
+
version: version.startsWith('v') ? version : 'v' + version,
|
|
1862
|
+
repos_reset: reposReset,
|
|
1863
|
+
summaries_deleted: summariesDeleted,
|
|
1864
|
+
path: filePath,
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* CLI wrapper for recordStartShas.
|
|
1870
|
+
* Usage: dgs-tools.cjs jobs record-start-shas <file>
|
|
1871
|
+
*/
|
|
1872
|
+
function cmdJobsRecordStartShas(cwd, file, raw) {
|
|
1873
|
+
if (!file) {
|
|
1874
|
+
error('file path required. Usage: jobs record-start-shas <file>');
|
|
1875
|
+
}
|
|
1876
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
1877
|
+
try {
|
|
1878
|
+
const result = recordStartShas(cwd, filePath);
|
|
1879
|
+
output(result, raw);
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
error(err.message);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* CLI wrapper for rollbackJob.
|
|
1887
|
+
* Usage: dgs-tools.cjs jobs rollback <version>
|
|
1888
|
+
*/
|
|
1889
|
+
function cmdJobsRollback(cwd, version, raw) {
|
|
1890
|
+
if (!version) {
|
|
1891
|
+
error('version required. Usage: jobs rollback <version>');
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
const result = rollbackJob(cwd, version);
|
|
1895
|
+
output(result, raw);
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
error(err.message);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// ─── Visibility & Polish CLI Wrappers ───────────────────────────────────────
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* CLI wrapper for listJobs.
|
|
1905
|
+
* Usage: dgs-tools.cjs jobs list-jobs
|
|
1906
|
+
*/
|
|
1907
|
+
function cmdJobsListJobs(cwd, raw) {
|
|
1908
|
+
try {
|
|
1909
|
+
const result = listJobs(cwd);
|
|
1910
|
+
output(result, raw);
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
error(err.message);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/**
|
|
1917
|
+
* CLI wrapper for cancelJob.
|
|
1918
|
+
* Usage: dgs-tools.cjs jobs cancel-job <version>
|
|
1919
|
+
*/
|
|
1920
|
+
function cmdJobsCancelJob(cwd, version, raw) {
|
|
1921
|
+
if (!version) {
|
|
1922
|
+
error('version required. Usage: jobs cancel-job <version>');
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
const result = cancelJob(cwd, version);
|
|
1926
|
+
output(result, raw);
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
error(err.message);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* CLI wrapper for healthCheck.
|
|
1934
|
+
* Usage: dgs-tools.cjs jobs health
|
|
1935
|
+
*/
|
|
1936
|
+
function cmdJobsHealthCheck(cwd, raw) {
|
|
1937
|
+
try {
|
|
1938
|
+
const result = healthCheck(cwd);
|
|
1939
|
+
output(result, raw);
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
error(err.message);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
/**
|
|
1946
|
+
* CLI wrapper for dryRunPreview.
|
|
1947
|
+
* Usage: dgs-tools.cjs jobs dry-run <version>
|
|
1948
|
+
*/
|
|
1949
|
+
function cmdJobsDryRun(cwd, version, raw) {
|
|
1950
|
+
if (!version) {
|
|
1951
|
+
error('version required. Usage: jobs dry-run <version>');
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
const result = dryRunPreview(cwd, version);
|
|
1955
|
+
output(result, raw);
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
error(err.message);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/**
|
|
1962
|
+
* CLI wrapper for generateJobSummary.
|
|
1963
|
+
* Usage: dgs-tools.cjs jobs generate-summary <version>
|
|
1964
|
+
*/
|
|
1965
|
+
function cmdJobsGenerateSummary(cwd, version, raw) {
|
|
1966
|
+
if (!version) {
|
|
1967
|
+
error('version required. Usage: jobs generate-summary <version>');
|
|
1968
|
+
}
|
|
1969
|
+
try {
|
|
1970
|
+
const result = generateJobSummary(cwd, version);
|
|
1971
|
+
output(result, raw);
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
error(err.message);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
module.exports = {
|
|
1978
|
+
parseJobFile,
|
|
1979
|
+
updateJobStep,
|
|
1980
|
+
moveJobFile,
|
|
1981
|
+
findJobFile,
|
|
1982
|
+
updateJobHeader,
|
|
1983
|
+
insertJobSteps,
|
|
1984
|
+
buildGapFixSteps,
|
|
1985
|
+
buildPhaseGapFixSteps,
|
|
1986
|
+
insertGapFixSection,
|
|
1987
|
+
insertPhaseGapFixSection,
|
|
1988
|
+
updatePhaseFixCycleHeader,
|
|
1989
|
+
generateMilestoneSteps,
|
|
1990
|
+
buildJobFileContent,
|
|
1991
|
+
listJobs,
|
|
1992
|
+
cancelJob,
|
|
1993
|
+
recordStartShas,
|
|
1994
|
+
rollbackJob,
|
|
1995
|
+
healthCheck,
|
|
1996
|
+
dryRunPreview,
|
|
1997
|
+
generateJobSummary,
|
|
1998
|
+
cmdJobsParse,
|
|
1999
|
+
cmdJobsUpdateStep,
|
|
2000
|
+
cmdJobsMove,
|
|
2001
|
+
cmdJobsFindJob,
|
|
2002
|
+
cmdJobsUpdateHeader,
|
|
2003
|
+
cmdJobsInsertSteps,
|
|
2004
|
+
cmdJobsInsertGapFixSection,
|
|
2005
|
+
cmdJobsInsertPhaseGapFix,
|
|
2006
|
+
cmdJobsCreateMilestone,
|
|
2007
|
+
cmdJobsMilestonePreview,
|
|
2008
|
+
cmdJobsListJobs,
|
|
2009
|
+
cmdJobsCancelJob,
|
|
2010
|
+
cmdJobsRecordStartShas,
|
|
2011
|
+
cmdJobsRollback,
|
|
2012
|
+
cmdJobsHealthCheck,
|
|
2013
|
+
cmdJobsDryRun,
|
|
2014
|
+
cmdJobsGenerateSummary,
|
|
2015
|
+
};
|