@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,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution — Multi-repo execution engine: preflight checks, per-repo commits,
|
|
3
|
+
* planning commit body, change detection
|
|
4
|
+
*
|
|
5
|
+
* This module owns all multi-repo execution concerns. It coordinates
|
|
6
|
+
* git operations across multiple repos for plan execution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execGit, safeReadFile, loadConfig, output, error } = require('./core.cjs');
|
|
12
|
+
const { getPlanningRoot } = require('./paths.cjs');
|
|
13
|
+
const { parseReposMd } = require('./repos.cjs');
|
|
14
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
15
|
+
const { scanProjectReposTags } = require('./projects.cjs');
|
|
16
|
+
|
|
17
|
+
// ─── Repo Path Resolution ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a repo name to its absolute path using REPOS.md.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} cwd - Product root
|
|
23
|
+
* @param {string} repoName - Repo name to resolve
|
|
24
|
+
* @param {Array} repos - Parsed repos array (optional, loads from REPOS.md if not provided)
|
|
25
|
+
* @returns {{ absPath: string, repo: Object } | null}
|
|
26
|
+
*/
|
|
27
|
+
function resolveRepoPath(cwd, repoName, repos) {
|
|
28
|
+
if (!repos) {
|
|
29
|
+
const parsed = parseReposMd(cwd);
|
|
30
|
+
if (!parsed) return null;
|
|
31
|
+
repos = parsed.repos;
|
|
32
|
+
}
|
|
33
|
+
const repo = repos.find(r => r.name === repoName);
|
|
34
|
+
if (!repo) return null;
|
|
35
|
+
const absPath = path.resolve(cwd, repo.path);
|
|
36
|
+
return { absPath, repo };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a repo-relative file path to its absolute path.
|
|
41
|
+
*
|
|
42
|
+
* Given a repo name and a path relative to that repo's root (e.g., 'src/index.ts'),
|
|
43
|
+
* resolves to the absolute file path on disk using REPOS.md lookup.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} cwd - Product root
|
|
46
|
+
* @param {string} repoName - Repo name to look up
|
|
47
|
+
* @param {string} relativePath - File path relative to repo root (e.g., 'src/index.ts')
|
|
48
|
+
* @param {Array} repos - Parsed repos array (optional, loads from REPOS.md if not provided)
|
|
49
|
+
* @returns {{ absFilePath: string, repoAbsPath: string, repo: Object } | null}
|
|
50
|
+
*/
|
|
51
|
+
function resolveRepoRelativePath(cwd, repoName, relativePath, repos) {
|
|
52
|
+
const resolved = resolveRepoPath(cwd, repoName, repos);
|
|
53
|
+
if (!resolved) return null;
|
|
54
|
+
const absFilePath = path.join(resolved.absPath, relativePath);
|
|
55
|
+
return { absFilePath, repoAbsPath: resolved.absPath, repo: resolved.repo };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Pre-flight Check ───────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pre-flight check: detect dirty repos, partial executions, and missing repos.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} cwd - Product root
|
|
64
|
+
* @param {string[]} repoNames - Repo names to check
|
|
65
|
+
* @param {string} [phaseDir] - Optional phase directory to check for partial SUMMARY.md
|
|
66
|
+
* @returns {{ passed: boolean, dirty_repos: Array, partial_execution: Object|null, missing_repos: string[] }}
|
|
67
|
+
*/
|
|
68
|
+
function preflightCheck(cwd, repoNames, phaseDir) {
|
|
69
|
+
const parsed = parseReposMd(cwd);
|
|
70
|
+
const repos = parsed ? parsed.repos : [];
|
|
71
|
+
const dirty_repos = [];
|
|
72
|
+
const missing_repos = [];
|
|
73
|
+
|
|
74
|
+
for (const name of repoNames) {
|
|
75
|
+
const resolved = resolveRepoPath(cwd, name, repos);
|
|
76
|
+
if (!resolved) {
|
|
77
|
+
missing_repos.push(name);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(resolved.absPath)) {
|
|
82
|
+
missing_repos.push(name);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const status = execGit(resolved.absPath, ['status', '--porcelain']);
|
|
87
|
+
if (status.exitCode === 0 && status.stdout.trim()) {
|
|
88
|
+
// Filter out untracked-only lines (starting with ??)
|
|
89
|
+
const tracked = status.stdout.split('\n').filter(l => l && !l.startsWith('??'));
|
|
90
|
+
if (tracked.length > 0) {
|
|
91
|
+
dirty_repos.push({ name, files: tracked });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check for partial execution in SUMMARY.md files
|
|
97
|
+
let partial_execution = null;
|
|
98
|
+
if (phaseDir) {
|
|
99
|
+
const dir = path.isAbsolute(phaseDir) ? phaseDir : path.join(cwd, phaseDir);
|
|
100
|
+
try {
|
|
101
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('-SUMMARY.md'));
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const content = safeReadFile(path.join(dir, file));
|
|
104
|
+
if (!content) continue;
|
|
105
|
+
const fm = extractFrontmatter(content);
|
|
106
|
+
if (fm.status === 'partial') {
|
|
107
|
+
partial_execution = { file, status: 'partial' };
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const passed = dirty_repos.length === 0 && missing_repos.length === 0 && partial_execution === null;
|
|
115
|
+
return { passed, dirty_repos, partial_execution, missing_repos };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Per-Repo Commit ────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Stage and commit specific files in a single repo.
|
|
122
|
+
* Internal helper used by both commitPerRepo and retryCommitPerRepo.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} absPath - Absolute path to the repo
|
|
125
|
+
* @param {string[]} files - Files to stage (relative to repo root)
|
|
126
|
+
* @param {string} commitMsg - Commit message
|
|
127
|
+
* @returns {{ status: string, sha: string|null, error?: string }}
|
|
128
|
+
*/
|
|
129
|
+
function _commitSingleRepo(absPath, files, commitMsg) {
|
|
130
|
+
try {
|
|
131
|
+
// Stage each file specifically (never git add .)
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
execGit(absPath, ['add', file]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if there's anything staged
|
|
137
|
+
const staged = execGit(absPath, ['diff', '--cached', '--name-only']);
|
|
138
|
+
if (staged.exitCode !== 0 || !staged.stdout.trim()) {
|
|
139
|
+
return { status: 'failed', sha: null, error: 'Nothing to commit after staging' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Commit
|
|
143
|
+
const commitResult = execGit(absPath, ['commit', '-m', commitMsg]);
|
|
144
|
+
|
|
145
|
+
if (commitResult.exitCode === 0) {
|
|
146
|
+
const shaResult = execGit(absPath, ['rev-parse', '--short', 'HEAD']);
|
|
147
|
+
return { status: 'complete', sha: shaResult.stdout.trim() };
|
|
148
|
+
} else {
|
|
149
|
+
return { status: 'failed', sha: null, error: commitResult.stderr };
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return { status: 'failed', sha: null, error: err.message };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Commit changes in each repo separately with project-prefixed messages.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} cwd - Product root
|
|
160
|
+
* @param {Array<{repoName: string, repoPath: string, files: string[]}>} repoChanges
|
|
161
|
+
* @param {{ type: string, project: string, phase: string, plan: string, description: string }} options
|
|
162
|
+
* @returns {Array<{repo: string, status: string, sha: string|null, error?: string}>}
|
|
163
|
+
*/
|
|
164
|
+
function commitPerRepo(cwd, repoChanges, options) {
|
|
165
|
+
const results = [];
|
|
166
|
+
|
|
167
|
+
// Sort by repoName alphabetically
|
|
168
|
+
const sorted = [...repoChanges].sort((a, b) => a.repoName.localeCompare(b.repoName));
|
|
169
|
+
|
|
170
|
+
for (const { repoName, repoPath, files } of sorted) {
|
|
171
|
+
const absPath = path.resolve(cwd, repoPath);
|
|
172
|
+
const msg = `${options.type}(${options.project}/${options.phase}-${options.plan}): ${options.description}`;
|
|
173
|
+
const result = _commitSingleRepo(absPath, files, msg);
|
|
174
|
+
results.push({ repo: repoName, ...result });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Retry commit for repos that failed in a prior run.
|
|
182
|
+
* Skips repos that already committed successfully (verified SHA),
|
|
183
|
+
* retries repos that failed or have stale SHAs.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} cwd - Product root
|
|
186
|
+
* @param {Array<{repoName: string, repoPath: string, files: string[]}>} repoChanges
|
|
187
|
+
* @param {{ type: string, project: string, phase: string, plan: string, description: string }} options
|
|
188
|
+
* @param {Array<{repo: string, status: string, sha: string|null, error?: string}>} priorResults
|
|
189
|
+
* @returns {Array<{repo: string, status: string, sha: string|null, error?: string}>}
|
|
190
|
+
*/
|
|
191
|
+
function retryCommitPerRepo(cwd, repoChanges, options, priorResults) {
|
|
192
|
+
const results = [];
|
|
193
|
+
const priorMap = new Map();
|
|
194
|
+
if (priorResults && priorResults.length > 0) {
|
|
195
|
+
for (const pr of priorResults) {
|
|
196
|
+
priorMap.set(pr.repo, pr);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Sort by repoName alphabetically
|
|
201
|
+
const sorted = [...repoChanges].sort((a, b) => a.repoName.localeCompare(b.repoName));
|
|
202
|
+
|
|
203
|
+
for (const { repoName, repoPath, files } of sorted) {
|
|
204
|
+
const absPath = path.resolve(cwd, repoPath);
|
|
205
|
+
const prior = priorMap.get(repoName);
|
|
206
|
+
|
|
207
|
+
// If prior result was complete with a SHA, verify it still exists
|
|
208
|
+
if (prior && prior.status === 'complete' && prior.sha) {
|
|
209
|
+
const verify = execGit(absPath, ['rev-parse', '--verify', prior.sha]);
|
|
210
|
+
if (verify.exitCode === 0) {
|
|
211
|
+
// SHA still exists — skip this repo, carry forward prior result
|
|
212
|
+
results.push({ repo: repoName, status: 'complete', sha: prior.sha });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// SHA no longer exists (rebased/force-pushed) — retry
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// No prior, prior was failed, or prior SHA is stale — commit normally
|
|
219
|
+
const msg = `${options.type}(${options.project}/${options.phase}-${options.plan}): ${options.description}`;
|
|
220
|
+
const result = _commitSingleRepo(absPath, files, msg);
|
|
221
|
+
results.push({ repo: repoName, ...result });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Manual Resolution Detection ────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Detect if a repo's expected changes have been manually resolved.
|
|
231
|
+
* Checks working tree cleanliness and file existence to determine
|
|
232
|
+
* if a user manually fixed and committed changes outside of DGS.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} cwd - Product root
|
|
235
|
+
* @param {string} repoName - Repo name
|
|
236
|
+
* @param {string[]} expectedFiles - Files expected to be committed
|
|
237
|
+
* @param {string} [priorSha] - SHA from a prior DGS commit attempt
|
|
238
|
+
* @returns {{ resolved: boolean, reason: string }}
|
|
239
|
+
*/
|
|
240
|
+
function detectManualResolution(cwd, repoName, expectedFiles, priorSha) {
|
|
241
|
+
const resolved = resolveRepoPath(cwd, repoName);
|
|
242
|
+
if (!resolved) {
|
|
243
|
+
return { resolved: false, reason: 'unresolved' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const absPath = resolved.absPath;
|
|
247
|
+
|
|
248
|
+
// Check if all expected files exist on disk
|
|
249
|
+
for (const file of expectedFiles) {
|
|
250
|
+
const filePath = path.join(absPath, file);
|
|
251
|
+
if (!fs.existsSync(filePath)) {
|
|
252
|
+
return { resolved: false, reason: 'unresolved' };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check if working tree is clean for expected files
|
|
257
|
+
const status = execGit(absPath, ['status', '--porcelain']);
|
|
258
|
+
if (status.exitCode === 0 && status.stdout.trim()) {
|
|
259
|
+
// Parse dirty files
|
|
260
|
+
const dirtyFiles = status.stdout
|
|
261
|
+
.split('\n')
|
|
262
|
+
.filter(l => l.trim())
|
|
263
|
+
.map(l => {
|
|
264
|
+
const match = l.match(/^[MADRCU?!]{1,2}\s+(.+)/);
|
|
265
|
+
return match ? match[1].trim() : null;
|
|
266
|
+
})
|
|
267
|
+
.filter(f => f);
|
|
268
|
+
|
|
269
|
+
// Check if any expected files are dirty
|
|
270
|
+
for (const file of expectedFiles) {
|
|
271
|
+
if (dirtyFiles.includes(file)) {
|
|
272
|
+
return { resolved: false, reason: 'unresolved' };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Working tree is clean for expected files. Check SHA if provided.
|
|
278
|
+
if (priorSha) {
|
|
279
|
+
const verify = execGit(absPath, ['rev-parse', '--verify', priorSha]);
|
|
280
|
+
if (verify.exitCode === 0) {
|
|
281
|
+
return { resolved: true, reason: 'sha_verified' };
|
|
282
|
+
}
|
|
283
|
+
// SHA doesn't exist but tree is clean and files present — manually resolved
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { resolved: true, reason: 'manually_resolved' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Planning Commit Body ───────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Build planning commit body with repo commit SHAs for traceability.
|
|
293
|
+
*
|
|
294
|
+
* @param {Array<{repo: string, status: string, sha: string|null}>} repoResults
|
|
295
|
+
* @returns {string}
|
|
296
|
+
*/
|
|
297
|
+
function buildPlanningCommitBody(repoResults) {
|
|
298
|
+
const completed = repoResults
|
|
299
|
+
.filter(r => r.status === 'complete')
|
|
300
|
+
.sort((a, b) => a.repo.localeCompare(b.repo));
|
|
301
|
+
|
|
302
|
+
if (completed.length === 0) return '';
|
|
303
|
+
|
|
304
|
+
return completed
|
|
305
|
+
.map(r => `Repo: ${r.repo} SHA: ${r.sha}`)
|
|
306
|
+
.join('\n');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Detect Repo Changes ───────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Detect changes in each repo by running git status.
|
|
313
|
+
* Groups changed files by repo, with paths relative to repo root.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} cwd - Product root
|
|
316
|
+
* @param {string[]} repoNames - Repo names to check
|
|
317
|
+
* @returns {Array<{repoName: string, repoPath: string, files: string[]}>}
|
|
318
|
+
*/
|
|
319
|
+
function detectRepoChanges(cwd, repoNames) {
|
|
320
|
+
const parsed = parseReposMd(cwd);
|
|
321
|
+
const repos = parsed ? parsed.repos : [];
|
|
322
|
+
const changes = [];
|
|
323
|
+
|
|
324
|
+
for (const name of repoNames) {
|
|
325
|
+
const resolved = resolveRepoPath(cwd, name, repos);
|
|
326
|
+
if (!resolved) continue;
|
|
327
|
+
if (!fs.existsSync(resolved.absPath)) continue;
|
|
328
|
+
|
|
329
|
+
const status = execGit(resolved.absPath, ['status', '--porcelain']);
|
|
330
|
+
if (status.exitCode !== 0 || !status.stdout.trim()) continue;
|
|
331
|
+
|
|
332
|
+
const files = status.stdout
|
|
333
|
+
.split('\n')
|
|
334
|
+
.filter(l => l.trim())
|
|
335
|
+
.map(l => {
|
|
336
|
+
// git status --porcelain format: XY filename (but execGit trims leading spaces)
|
|
337
|
+
// Use regex to strip 1-2 char status code + space prefix
|
|
338
|
+
// Handles: 'M file.js', 'D file.js', '?? file.js', 'AM file.js', 'R old -> new'
|
|
339
|
+
const match = l.match(/^[MADRCU?!]{1,2}\s+(.+)/);
|
|
340
|
+
if (match) return match[1].trim();
|
|
341
|
+
// Fallback: strip first word (status) and return rest
|
|
342
|
+
const parts = l.trim().split(/\s+/);
|
|
343
|
+
return parts.length > 1 ? parts.slice(1).join(' ') : l.trim();
|
|
344
|
+
})
|
|
345
|
+
.filter(f => f);
|
|
346
|
+
|
|
347
|
+
if (files.length > 0) {
|
|
348
|
+
changes.push({
|
|
349
|
+
repoName: name,
|
|
350
|
+
repoPath: resolved.repo.path,
|
|
351
|
+
files,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return changes;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── Branching ──────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Create branches in each repo for multi-repo execution.
|
|
363
|
+
* Respects branching_strategy config — if 'none', skips branch creation.
|
|
364
|
+
* Reuses existing branch if it already exists.
|
|
365
|
+
* Detects branch prefix collisions and warns about potential ambiguity.
|
|
366
|
+
*
|
|
367
|
+
* When baseBranch is provided, uses two-pass validation:
|
|
368
|
+
* Pass 1: Fetch and verify the base branch exists in ALL repos before any branch creation.
|
|
369
|
+
* Pass 2: Checkout base branch, then create/checkout the new branch in each repo.
|
|
370
|
+
* This ensures no partial state — either all repos are validated or none are modified.
|
|
371
|
+
*
|
|
372
|
+
* The product folder (cwd itself) is NOT affected — this function only operates on
|
|
373
|
+
* sibling code repos registered in REPOS.md.
|
|
374
|
+
*
|
|
375
|
+
* @param {string} cwd - Product root
|
|
376
|
+
* @param {string[]} repoNames - Repo names
|
|
377
|
+
* @param {string} branchName - Branch name (e.g., dgs/project/phase-slug)
|
|
378
|
+
* @param {{ branching_strategy?: string }} config - Config object
|
|
379
|
+
* @param {string|null} [baseBranch=null] - Base branch to checkout before creating new branch.
|
|
380
|
+
* When null/undefined, creates branch from wherever HEAD is (backwards-compatible behavior).
|
|
381
|
+
* @returns {{ created: boolean, reason?: string, error?: string, repo?: string, branches?: Array<{repo: string, branch: string, action: string}>, warnings?: Array }}
|
|
382
|
+
*/
|
|
383
|
+
function createRepoBranches(cwd, repoNames, branchName, config, baseBranch) {
|
|
384
|
+
if (config && config.branching_strategy === 'none') {
|
|
385
|
+
return { created: false, reason: 'branching_disabled' };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const parsed = parseReposMd(cwd);
|
|
389
|
+
const repos = parsed ? parsed.repos : [];
|
|
390
|
+
const branches = [];
|
|
391
|
+
const warnings = [];
|
|
392
|
+
|
|
393
|
+
// Extract project slug from branch name (format: dgs/<project-slug>/<phase-slug>)
|
|
394
|
+
const branchParts = branchName.split('/');
|
|
395
|
+
const projectSlug = branchParts.length >= 2 ? branchParts[1] : null;
|
|
396
|
+
|
|
397
|
+
// Resolve all repo paths upfront
|
|
398
|
+
const resolvedRepos = [];
|
|
399
|
+
for (const name of repoNames) {
|
|
400
|
+
const resolved = resolveRepoPath(cwd, name, repos);
|
|
401
|
+
if (!resolved) continue;
|
|
402
|
+
if (!fs.existsSync(resolved.absPath)) continue;
|
|
403
|
+
resolvedRepos.push({ name, resolved });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Pass 1: Validate base branch in all repos (when baseBranch provided) ──
|
|
407
|
+
if (baseBranch) {
|
|
408
|
+
for (const { name, resolved } of resolvedRepos) {
|
|
409
|
+
// Fetch the base branch from remote (non-fatal — branch may be local-only)
|
|
410
|
+
execGit(resolved.absPath, ['fetch', 'origin', baseBranch]);
|
|
411
|
+
|
|
412
|
+
// Verify base branch exists locally
|
|
413
|
+
const baseCheck = execGit(resolved.absPath, ['rev-parse', '--verify', baseBranch]);
|
|
414
|
+
if (baseCheck.exitCode !== 0) {
|
|
415
|
+
// Also check remote tracking branch
|
|
416
|
+
const remoteCheck = execGit(resolved.absPath, ['rev-parse', '--verify', `origin/${baseBranch}`]);
|
|
417
|
+
if (remoteCheck.exitCode !== 0) {
|
|
418
|
+
return {
|
|
419
|
+
created: false,
|
|
420
|
+
reason: 'base_branch_missing',
|
|
421
|
+
error: `Base branch '${baseBranch}' does not exist in repo '${name}'. ` +
|
|
422
|
+
`Checked both local and remote (origin/${baseBranch}). ` +
|
|
423
|
+
`Configure git.base_branch in .planning/config.json or create the branch first.`,
|
|
424
|
+
repo: name,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Pass 2: Create branches ───────────────────────────────────────────────
|
|
432
|
+
for (const { name, resolved } of resolvedRepos) {
|
|
433
|
+
// Check if branch already exists
|
|
434
|
+
const verify = execGit(resolved.absPath, ['rev-parse', '--verify', branchName]);
|
|
435
|
+
if (verify.exitCode === 0) {
|
|
436
|
+
// Branch exists — switch to it
|
|
437
|
+
execGit(resolved.absPath, ['checkout', branchName]);
|
|
438
|
+
branches.push({ repo: name, branch: branchName, action: 'reused' });
|
|
439
|
+
} else {
|
|
440
|
+
// Before creating, check for prefix collisions with existing branches
|
|
441
|
+
if (projectSlug) {
|
|
442
|
+
const listResult = execGit(resolved.absPath, ['branch', '--list', `dgs/${projectSlug}/*`]);
|
|
443
|
+
if (listResult.exitCode === 0 && listResult.stdout.trim()) {
|
|
444
|
+
const existingBranches = listResult.stdout
|
|
445
|
+
.split('\n')
|
|
446
|
+
.map(b => b.replace(/^\*?\s*/, '').trim())
|
|
447
|
+
.filter(b => b && b !== branchName);
|
|
448
|
+
if (existingBranches.length > 0) {
|
|
449
|
+
warnings.push({
|
|
450
|
+
type: 'prefix_collision',
|
|
451
|
+
repo: name,
|
|
452
|
+
existing_branches: existingBranches,
|
|
453
|
+
new_branch: branchName,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Checkout base branch first (when baseBranch provided)
|
|
460
|
+
if (baseBranch) {
|
|
461
|
+
const baseCheckout = execGit(resolved.absPath, ['checkout', baseBranch]);
|
|
462
|
+
if (baseCheckout.exitCode !== 0) {
|
|
463
|
+
branches.push({ repo: name, branch: branchName, action: 'failed', error: `Failed to checkout base branch '${baseBranch}': ${baseCheckout.stderr}` });
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Create and switch to new branch
|
|
469
|
+
const create = execGit(resolved.absPath, ['checkout', '-b', branchName]);
|
|
470
|
+
if (create.exitCode === 0) {
|
|
471
|
+
branches.push({ repo: name, branch: branchName, action: 'created' });
|
|
472
|
+
} else {
|
|
473
|
+
branches.push({ repo: name, branch: branchName, action: 'failed', error: create.stderr });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return { created: true, branches, warnings };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Report branches to merge at phase completion.
|
|
483
|
+
* Informational only — does not perform any merge.
|
|
484
|
+
*
|
|
485
|
+
* @param {string} cwd - Product root
|
|
486
|
+
* @param {string[]} repoNames - Repo names
|
|
487
|
+
* @param {string} branchName - Branch name to check for
|
|
488
|
+
* @returns {{ branches: Array<{repo: string, branch: string, exists: boolean}> }}
|
|
489
|
+
*/
|
|
490
|
+
function reportBranchesToMerge(cwd, repoNames, branchName) {
|
|
491
|
+
const parsed = parseReposMd(cwd);
|
|
492
|
+
const repos = parsed ? parsed.repos : [];
|
|
493
|
+
const branches = [];
|
|
494
|
+
|
|
495
|
+
for (const name of repoNames) {
|
|
496
|
+
const resolved = resolveRepoPath(cwd, name, repos);
|
|
497
|
+
if (!resolved) continue;
|
|
498
|
+
if (!fs.existsSync(resolved.absPath)) continue;
|
|
499
|
+
|
|
500
|
+
const verify = execGit(resolved.absPath, ['rev-parse', '--verify', branchName]);
|
|
501
|
+
if (verify.exitCode === 0) {
|
|
502
|
+
branches.push({ repo: name, branch: branchName, exists: true });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { branches };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ─── Repo Status in STATE.md ────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Update Repo Status section in STATE.md.
|
|
513
|
+
* Writes table with Repo, Branch, Last Commit, Touched By columns.
|
|
514
|
+
*
|
|
515
|
+
* @param {string} cwd - Product root
|
|
516
|
+
* @param {string|null} projectSlug - Project slug for v2, null for v1
|
|
517
|
+
* @param {Array<{repo: string, status: string, sha: string|null}>} repoResults - Commit results
|
|
518
|
+
*/
|
|
519
|
+
function updateRepoStatus(cwd, projectSlug, repoResults) {
|
|
520
|
+
// Determine STATE.md path
|
|
521
|
+
let statePath;
|
|
522
|
+
const planRoot = getPlanningRoot(cwd);
|
|
523
|
+
if (projectSlug) {
|
|
524
|
+
statePath = path.join(planRoot, projectSlug, 'STATE.md');
|
|
525
|
+
}
|
|
526
|
+
if (!statePath || !fs.existsSync(statePath)) {
|
|
527
|
+
statePath = path.join(planRoot, 'STATE.md');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let stateContent = safeReadFile(statePath) || '';
|
|
531
|
+
|
|
532
|
+
// Build Repo Status table
|
|
533
|
+
const parsed = parseReposMd(cwd);
|
|
534
|
+
const repos = parsed ? parsed.repos : [];
|
|
535
|
+
|
|
536
|
+
let table = '## Repo Status\n\n';
|
|
537
|
+
table += '| Repo | Branch | Last Commit | Touched By |\n';
|
|
538
|
+
table += '|------|--------|-------------|------------|\n';
|
|
539
|
+
|
|
540
|
+
for (const result of repoResults.filter(r => r.status === 'complete')) {
|
|
541
|
+
const resolved = resolveRepoPath(cwd, result.repo, repos);
|
|
542
|
+
let branch = '-';
|
|
543
|
+
if (resolved && fs.existsSync(resolved.absPath)) {
|
|
544
|
+
const branchResult = execGit(resolved.absPath, ['branch', '--show-current']);
|
|
545
|
+
if (branchResult.exitCode === 0 && branchResult.stdout.trim()) {
|
|
546
|
+
branch = branchResult.stdout.trim();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Derive "Touched By" from scanning active project plan tags
|
|
551
|
+
let touchedBy = '-';
|
|
552
|
+
try {
|
|
553
|
+
const projectFolders = getProjectSlugs(cwd);
|
|
554
|
+
const touchedByProjects = [];
|
|
555
|
+
for (const slug of projectFolders) {
|
|
556
|
+
const projectRepos = scanProjectReposTags(cwd, slug);
|
|
557
|
+
if (projectRepos.includes(result.repo)) {
|
|
558
|
+
touchedByProjects.push(slug);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (touchedByProjects.length > 0) {
|
|
562
|
+
touchedBy = touchedByProjects.join(', ');
|
|
563
|
+
}
|
|
564
|
+
} catch {}
|
|
565
|
+
|
|
566
|
+
table += `| ${result.repo} | ${branch} | ${result.sha || '-'} | ${touchedBy} |\n`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Replace existing ## Repo Status section or append
|
|
570
|
+
const repoStatusRegex = /## Repo Status[\s\S]*?(?=\n## |\n---|\Z)/;
|
|
571
|
+
if (repoStatusRegex.test(stateContent)) {
|
|
572
|
+
stateContent = stateContent.replace(repoStatusRegex, table.trim());
|
|
573
|
+
} else {
|
|
574
|
+
stateContent = stateContent.trimEnd() + '\n\n' + table;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
fs.writeFileSync(statePath, stateContent);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Get project slugs from .planning directory.
|
|
582
|
+
* Looks for subdirectories that contain STATE.md (project folders).
|
|
583
|
+
*
|
|
584
|
+
* @param {string} cwd - Product root
|
|
585
|
+
* @returns {string[]}
|
|
586
|
+
*/
|
|
587
|
+
function getProjectSlugs(cwd) {
|
|
588
|
+
const planningDir = getPlanningRoot(cwd);
|
|
589
|
+
const slugs = [];
|
|
590
|
+
try {
|
|
591
|
+
const entries = fs.readdirSync(planningDir, { withFileTypes: true });
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
if (!entry.isDirectory()) continue;
|
|
594
|
+
if (entry.name === 'phases' || entry.name === 'debug' || entry.name === 'codebase') continue;
|
|
595
|
+
if (entry.name.startsWith('.')) continue;
|
|
596
|
+
// Check if it has STATE.md (project folder indicator)
|
|
597
|
+
if (fs.existsSync(path.join(planningDir, entry.name, 'STATE.md'))) {
|
|
598
|
+
slugs.push(entry.name);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} catch {}
|
|
602
|
+
return slugs;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ─── CLI Command Functions ──────────────────────────────────────────────────
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* CLI: Run multi-repo commit workflow.
|
|
609
|
+
* Preflight dirty_repos are advisory warnings (proceed anyway).
|
|
610
|
+
* Preflight missing_repos are hard errors (block execution).
|
|
611
|
+
* Supports --retry with prior results to skip already-committed repos.
|
|
612
|
+
*
|
|
613
|
+
* @param {string} cwd - Working directory
|
|
614
|
+
* @param {Object} options - { type, project, phase, plan, description, repos, retry, priorResults, phaseDir }
|
|
615
|
+
* @param {boolean} raw - Raw output mode
|
|
616
|
+
*/
|
|
617
|
+
function cmdCommitMultiRepo(cwd, options, raw) {
|
|
618
|
+
const repoNames = options.repos;
|
|
619
|
+
if (!repoNames || repoNames.length === 0) {
|
|
620
|
+
error('No repos specified. Use --repos repo1,repo2');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const warnings = [];
|
|
624
|
+
|
|
625
|
+
// Run preflight
|
|
626
|
+
const preflight = preflightCheck(cwd, repoNames, options.phaseDir || null);
|
|
627
|
+
|
|
628
|
+
// Missing repos is a hard error — block execution
|
|
629
|
+
if (preflight.missing_repos && preflight.missing_repos.length > 0) {
|
|
630
|
+
output({
|
|
631
|
+
success: false,
|
|
632
|
+
preflight,
|
|
633
|
+
message: 'Pre-flight check failed: missing repos',
|
|
634
|
+
warnings,
|
|
635
|
+
}, raw);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Dirty repos are advisory — warn but proceed
|
|
640
|
+
if (preflight.dirty_repos && preflight.dirty_repos.length > 0) {
|
|
641
|
+
for (const dirty of preflight.dirty_repos) {
|
|
642
|
+
warnings.push({ type: 'dirty_repo', repo: dirty.name, files: dirty.files });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Partial execution is informational — include but do not block
|
|
647
|
+
if (preflight.partial_execution) {
|
|
648
|
+
warnings.push({ type: 'partial_execution', info: preflight.partial_execution });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Detect changes
|
|
652
|
+
const changes = detectRepoChanges(cwd, repoNames);
|
|
653
|
+
if (changes.length === 0) {
|
|
654
|
+
output({ success: true, message: 'No changes detected', commits: [], warnings }, raw);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Commit per repo (retry mode or normal)
|
|
659
|
+
let results;
|
|
660
|
+
if (options.retry && options.priorResults) {
|
|
661
|
+
results = retryCommitPerRepo(cwd, changes, options, options.priorResults);
|
|
662
|
+
} else {
|
|
663
|
+
results = commitPerRepo(cwd, changes, options);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Build planning commit body
|
|
667
|
+
const body = buildPlanningCommitBody(results);
|
|
668
|
+
|
|
669
|
+
output({
|
|
670
|
+
success: results.every(r => r.status === 'complete'),
|
|
671
|
+
commits: results,
|
|
672
|
+
planning_body: body,
|
|
673
|
+
warnings,
|
|
674
|
+
}, raw);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* CLI: Run pre-flight check.
|
|
679
|
+
*
|
|
680
|
+
* @param {string} cwd - Working directory
|
|
681
|
+
* @param {string[]} repoNames - Repo names to check
|
|
682
|
+
* @param {boolean} raw - Raw output mode
|
|
683
|
+
*/
|
|
684
|
+
function cmdCommitPreflight(cwd, repoNames, raw) {
|
|
685
|
+
const result = preflightCheck(cwd, repoNames);
|
|
686
|
+
output(result, raw);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
module.exports = {
|
|
692
|
+
resolveRepoPath,
|
|
693
|
+
resolveRepoRelativePath,
|
|
694
|
+
preflightCheck,
|
|
695
|
+
commitPerRepo,
|
|
696
|
+
retryCommitPerRepo,
|
|
697
|
+
detectManualResolution,
|
|
698
|
+
buildPlanningCommitBody,
|
|
699
|
+
detectRepoChanges,
|
|
700
|
+
createRepoBranches,
|
|
701
|
+
reportBranchesToMerge,
|
|
702
|
+
updateRepoStatus,
|
|
703
|
+
cmdCommitMultiRepo,
|
|
704
|
+
cmdCommitPreflight,
|
|
705
|
+
};
|