@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.
Files changed (256) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +851 -0
  3. package/agents/dgs-codebase-cross-analyzer.md +183 -0
  4. package/agents/dgs-codebase-mapper.md +782 -0
  5. package/agents/dgs-codebase-synthesizer.md +156 -0
  6. package/agents/dgs-debugger.md +1256 -0
  7. package/agents/dgs-executor.md +550 -0
  8. package/agents/dgs-integration-checker.md +481 -0
  9. package/agents/dgs-nyquist-auditor.md +178 -0
  10. package/agents/dgs-phase-researcher.md +563 -0
  11. package/agents/dgs-phase-verifier.md +450 -0
  12. package/agents/dgs-plan-checker.md +708 -0
  13. package/agents/dgs-planner.md +1324 -0
  14. package/agents/dgs-project-researcher.md +631 -0
  15. package/agents/dgs-research-synthesizer.md +249 -0
  16. package/agents/dgs-roadmapper.md +652 -0
  17. package/agents/dgs-verifier.md +607 -0
  18. package/bin/install.js +2073 -0
  19. package/commands/dgs/add-doc.md +45 -0
  20. package/commands/dgs/add-idea.md +38 -0
  21. package/commands/dgs/add-phase.md +43 -0
  22. package/commands/dgs/add-repo.md +54 -0
  23. package/commands/dgs/add-tests.md +41 -0
  24. package/commands/dgs/add-todo.md +47 -0
  25. package/commands/dgs/approve-spec.md +38 -0
  26. package/commands/dgs/audit-milestone.md +36 -0
  27. package/commands/dgs/audit-phase.md +37 -0
  28. package/commands/dgs/cancel-job.md +23 -0
  29. package/commands/dgs/capture-principle.md +143 -0
  30. package/commands/dgs/check-todos.md +45 -0
  31. package/commands/dgs/cleanup.md +18 -0
  32. package/commands/dgs/complete-milestone.md +136 -0
  33. package/commands/dgs/complete-project.md +70 -0
  34. package/commands/dgs/consolidate-ideas.md +50 -0
  35. package/commands/dgs/create-milestone-job.md +37 -0
  36. package/commands/dgs/debug.md +164 -0
  37. package/commands/dgs/develop-idea.md +53 -0
  38. package/commands/dgs/discuss-idea.md +41 -0
  39. package/commands/dgs/discuss-phase.md +83 -0
  40. package/commands/dgs/execute-phase.md +41 -0
  41. package/commands/dgs/fast.md +38 -0
  42. package/commands/dgs/find-related-ideas.md +43 -0
  43. package/commands/dgs/health.md +28 -0
  44. package/commands/dgs/help.md +22 -0
  45. package/commands/dgs/import-spec.md +36 -0
  46. package/commands/dgs/init-product.md +28 -0
  47. package/commands/dgs/insert-phase.md +32 -0
  48. package/commands/dgs/join-discord.md +18 -0
  49. package/commands/dgs/list-docs.md +40 -0
  50. package/commands/dgs/list-ideas.md +42 -0
  51. package/commands/dgs/list-jobs.md +22 -0
  52. package/commands/dgs/list-phase-assumptions.md +46 -0
  53. package/commands/dgs/list-projects.md +57 -0
  54. package/commands/dgs/list-specs.md +40 -0
  55. package/commands/dgs/map-codebase.md +92 -0
  56. package/commands/dgs/new-milestone.md +44 -0
  57. package/commands/dgs/new-project.md +42 -0
  58. package/commands/dgs/node-repair.md +26 -0
  59. package/commands/dgs/overlap-check.md +20 -0
  60. package/commands/dgs/pause-work.md +38 -0
  61. package/commands/dgs/plan-milestone-gaps.md +34 -0
  62. package/commands/dgs/plan-phase.md +44 -0
  63. package/commands/dgs/progress.md +24 -0
  64. package/commands/dgs/quick.md +41 -0
  65. package/commands/dgs/reactivate-project.md +70 -0
  66. package/commands/dgs/reapply-patches.md +110 -0
  67. package/commands/dgs/refine-spec.md +38 -0
  68. package/commands/dgs/reject-idea.md +43 -0
  69. package/commands/dgs/remove-doc.md +44 -0
  70. package/commands/dgs/remove-phase.md +31 -0
  71. package/commands/dgs/remove-repo.md +69 -0
  72. package/commands/dgs/research-idea.md +43 -0
  73. package/commands/dgs/research-phase.md +189 -0
  74. package/commands/dgs/restore-idea.md +45 -0
  75. package/commands/dgs/resume-work.md +40 -0
  76. package/commands/dgs/rollback-job.md +24 -0
  77. package/commands/dgs/run-job.md +35 -0
  78. package/commands/dgs/search.md +40 -0
  79. package/commands/dgs/set-profile.md +34 -0
  80. package/commands/dgs/settings.md +38 -0
  81. package/commands/dgs/switch-project.md +58 -0
  82. package/commands/dgs/undo-consolidation.md +42 -0
  83. package/commands/dgs/update-idea.md +44 -0
  84. package/commands/dgs/update.md +37 -0
  85. package/commands/dgs/validate-phase.md +35 -0
  86. package/commands/dgs/verify-work.md +39 -0
  87. package/commands/dgs/write-spec.md +49 -0
  88. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
  89. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
  90. package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
  91. package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
  92. package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
  93. package/deliver-great-systems/bin/lib/commands.cjs +570 -0
  94. package/deliver-great-systems/bin/lib/config.cjs +417 -0
  95. package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
  96. package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
  97. package/deliver-great-systems/bin/lib/context.cjs +929 -0
  98. package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
  99. package/deliver-great-systems/bin/lib/core.cjs +744 -0
  100. package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
  101. package/deliver-great-systems/bin/lib/docs.cjs +919 -0
  102. package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
  103. package/deliver-great-systems/bin/lib/execution.cjs +705 -0
  104. package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
  105. package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
  106. package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
  107. package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
  108. package/deliver-great-systems/bin/lib/identity.cjs +125 -0
  109. package/deliver-great-systems/bin/lib/init.cjs +1114 -0
  110. package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
  111. package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
  112. package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
  113. package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
  114. package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
  115. package/deliver-great-systems/bin/lib/migration.cjs +352 -0
  116. package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
  117. package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
  118. package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
  119. package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
  120. package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
  121. package/deliver-great-systems/bin/lib/paths.cjs +144 -0
  122. package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
  123. package/deliver-great-systems/bin/lib/phase.cjs +910 -0
  124. package/deliver-great-systems/bin/lib/projects.cjs +691 -0
  125. package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
  126. package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
  127. package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
  128. package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
  129. package/deliver-great-systems/bin/lib/search.cjs +570 -0
  130. package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
  131. package/deliver-great-systems/bin/lib/state.cjs +893 -0
  132. package/deliver-great-systems/bin/lib/template.cjs +228 -0
  133. package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
  134. package/deliver-great-systems/bin/lib/verify.cjs +796 -0
  135. package/deliver-great-systems/references/checkpoints.md +776 -0
  136. package/deliver-great-systems/references/conflict-resolution.md +66 -0
  137. package/deliver-great-systems/references/context-tiers.md +166 -0
  138. package/deliver-great-systems/references/continuation-format.md +249 -0
  139. package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
  140. package/deliver-great-systems/references/git-integration.md +250 -0
  141. package/deliver-great-systems/references/git-planning-commit.md +40 -0
  142. package/deliver-great-systems/references/model-profile-resolution.md +36 -0
  143. package/deliver-great-systems/references/model-profiles.md +95 -0
  144. package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
  145. package/deliver-great-systems/references/planning-config.md +224 -0
  146. package/deliver-great-systems/references/questioning.md +162 -0
  147. package/deliver-great-systems/references/spec-review-loop.md +177 -0
  148. package/deliver-great-systems/references/tdd.md +265 -0
  149. package/deliver-great-systems/references/ui-brand.md +160 -0
  150. package/deliver-great-systems/references/verification-patterns.md +612 -0
  151. package/deliver-great-systems/templates/DEBUG.md +166 -0
  152. package/deliver-great-systems/templates/UAT.md +251 -0
  153. package/deliver-great-systems/templates/VALIDATION.md +95 -0
  154. package/deliver-great-systems/templates/claude-md.md +74 -0
  155. package/deliver-great-systems/templates/codebase/architecture.md +257 -0
  156. package/deliver-great-systems/templates/codebase/concerns.md +312 -0
  157. package/deliver-great-systems/templates/codebase/conventions.md +309 -0
  158. package/deliver-great-systems/templates/codebase/integrations.md +282 -0
  159. package/deliver-great-systems/templates/codebase/stack.md +188 -0
  160. package/deliver-great-systems/templates/codebase/structure.md +287 -0
  161. package/deliver-great-systems/templates/codebase/testing.md +482 -0
  162. package/deliver-great-systems/templates/config.json +38 -0
  163. package/deliver-great-systems/templates/context.md +354 -0
  164. package/deliver-great-systems/templates/continue-here.md +80 -0
  165. package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
  166. package/deliver-great-systems/templates/discovery.md +148 -0
  167. package/deliver-great-systems/templates/milestone-archive.md +125 -0
  168. package/deliver-great-systems/templates/milestone.md +117 -0
  169. package/deliver-great-systems/templates/phase-prompt.md +615 -0
  170. package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
  171. package/deliver-great-systems/templates/project.md +186 -0
  172. package/deliver-great-systems/templates/requirements.md +233 -0
  173. package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
  174. package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
  175. package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
  176. package/deliver-great-systems/templates/research-project/STACK.md +122 -0
  177. package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
  178. package/deliver-great-systems/templates/research.md +554 -0
  179. package/deliver-great-systems/templates/retrospective.md +54 -0
  180. package/deliver-great-systems/templates/roadmap.md +204 -0
  181. package/deliver-great-systems/templates/state.md +178 -0
  182. package/deliver-great-systems/templates/summary-complex.md +59 -0
  183. package/deliver-great-systems/templates/summary-minimal.md +41 -0
  184. package/deliver-great-systems/templates/summary-standard.md +48 -0
  185. package/deliver-great-systems/templates/summary.md +253 -0
  186. package/deliver-great-systems/templates/user-setup.md +313 -0
  187. package/deliver-great-systems/templates/verification-report.md +324 -0
  188. package/deliver-great-systems/workflows/add-doc.md +151 -0
  189. package/deliver-great-systems/workflows/add-idea.md +96 -0
  190. package/deliver-great-systems/workflows/add-phase.md +120 -0
  191. package/deliver-great-systems/workflows/add-tests.md +359 -0
  192. package/deliver-great-systems/workflows/add-todo.md +162 -0
  193. package/deliver-great-systems/workflows/approve-spec.md +194 -0
  194. package/deliver-great-systems/workflows/audit-milestone.md +364 -0
  195. package/deliver-great-systems/workflows/audit-phase.md +462 -0
  196. package/deliver-great-systems/workflows/cancel-job.md +108 -0
  197. package/deliver-great-systems/workflows/check-todos.md +181 -0
  198. package/deliver-great-systems/workflows/cleanup.md +247 -0
  199. package/deliver-great-systems/workflows/codereview.md +526 -0
  200. package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
  201. package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
  202. package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
  203. package/deliver-great-systems/workflows/develop-idea.md +544 -0
  204. package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
  205. package/deliver-great-systems/workflows/discovery-phase.md +301 -0
  206. package/deliver-great-systems/workflows/discuss-idea.md +263 -0
  207. package/deliver-great-systems/workflows/discuss-phase.md +733 -0
  208. package/deliver-great-systems/workflows/execute-phase.md +571 -0
  209. package/deliver-great-systems/workflows/execute-plan.md +592 -0
  210. package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
  211. package/deliver-great-systems/workflows/health.md +173 -0
  212. package/deliver-great-systems/workflows/help.md +997 -0
  213. package/deliver-great-systems/workflows/import-spec.md +381 -0
  214. package/deliver-great-systems/workflows/init-product.md +767 -0
  215. package/deliver-great-systems/workflows/insert-phase.md +138 -0
  216. package/deliver-great-systems/workflows/list-docs.md +119 -0
  217. package/deliver-great-systems/workflows/list-ideas.md +154 -0
  218. package/deliver-great-systems/workflows/list-jobs.md +89 -0
  219. package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
  220. package/deliver-great-systems/workflows/list-specs.md +101 -0
  221. package/deliver-great-systems/workflows/map-codebase.md +621 -0
  222. package/deliver-great-systems/workflows/new-milestone.md +591 -0
  223. package/deliver-great-systems/workflows/new-project.md +1113 -0
  224. package/deliver-great-systems/workflows/node-repair.md +94 -0
  225. package/deliver-great-systems/workflows/overlap-check.md +86 -0
  226. package/deliver-great-systems/workflows/pause-work.md +134 -0
  227. package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
  228. package/deliver-great-systems/workflows/plan-phase.md +698 -0
  229. package/deliver-great-systems/workflows/progress.md +386 -0
  230. package/deliver-great-systems/workflows/quick.md +845 -0
  231. package/deliver-great-systems/workflows/refine-spec.md +275 -0
  232. package/deliver-great-systems/workflows/reject-idea.md +109 -0
  233. package/deliver-great-systems/workflows/remove-doc.md +117 -0
  234. package/deliver-great-systems/workflows/remove-phase.md +163 -0
  235. package/deliver-great-systems/workflows/research-idea.md +325 -0
  236. package/deliver-great-systems/workflows/research-phase.md +81 -0
  237. package/deliver-great-systems/workflows/restore-idea.md +101 -0
  238. package/deliver-great-systems/workflows/resume-project.md +311 -0
  239. package/deliver-great-systems/workflows/rollback-job.md +130 -0
  240. package/deliver-great-systems/workflows/run-job.md +498 -0
  241. package/deliver-great-systems/workflows/search.md +130 -0
  242. package/deliver-great-systems/workflows/set-profile.md +83 -0
  243. package/deliver-great-systems/workflows/settings.md +470 -0
  244. package/deliver-great-systems/workflows/transition.md +563 -0
  245. package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
  246. package/deliver-great-systems/workflows/update-idea.md +157 -0
  247. package/deliver-great-systems/workflows/update.md +242 -0
  248. package/deliver-great-systems/workflows/validate-phase.md +177 -0
  249. package/deliver-great-systems/workflows/verify-phase.md +253 -0
  250. package/deliver-great-systems/workflows/verify-work.md +671 -0
  251. package/deliver-great-systems/workflows/write-spec.md +450 -0
  252. package/hooks/dist/dgs-check-update.js +62 -0
  253. package/hooks/dist/dgs-context-monitor.js +141 -0
  254. package/hooks/dist/dgs-statusline.js +115 -0
  255. package/package.json +60 -0
  256. 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
+ };