@ktpartners/dgs-platform 2.9.0 → 3.3.0

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 (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -0,0 +1,790 @@
1
+ /**
2
+ * Worktrees -- git worktree lifecycle management
3
+ *
4
+ * Create, remove, list, setup, and prune worktrees for code repos.
5
+ * State tracked in config.local.json under projects.{project}.worktrees.
6
+ *
7
+ * Worktree paths are siblings to main checkout:
8
+ * {repoParent}/{repoName}--{projectSlug}-{slug}
9
+ *
10
+ * Branch naming:
11
+ * milestone/{slug} or quick/{slug}
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const crypto = require('crypto');
19
+ const { execSync } = require('child_process');
20
+ const { execGit, output, error, loadConfig } = require('./core.cjs');
21
+ const { getLocalConfigPath } = require('./config.cjs');
22
+ const { getPlanningRoot } = require('./paths.cjs');
23
+ const { parseReposMd } = require('./repos.cjs');
24
+ const { extractFrontmatter } = require('./frontmatter.cjs');
25
+
26
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Read config.local.json safely.
30
+ * @param {string} cwd
31
+ * @returns {object}
32
+ */
33
+ function _readLocalConfig(cwd) {
34
+ const localPath = getLocalConfigPath(cwd);
35
+ try {
36
+ if (fs.existsSync(localPath)) {
37
+ return JSON.parse(fs.readFileSync(localPath, 'utf-8'));
38
+ }
39
+ } catch { /* ignore */ }
40
+ return {};
41
+ }
42
+
43
+ /**
44
+ * Write config.local.json atomically.
45
+ * @param {string} cwd
46
+ * @param {object} data
47
+ */
48
+ function _writeLocalConfig(cwd, data) {
49
+ const localPath = getLocalConfigPath(cwd);
50
+ const dir = path.dirname(localPath);
51
+ if (!fs.existsSync(dir)) {
52
+ fs.mkdirSync(dir, { recursive: true });
53
+ }
54
+ const tmpPath = localPath + '.tmp.' + process.pid;
55
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
56
+ fs.renameSync(tmpPath, localPath);
57
+ }
58
+
59
+ /**
60
+ * Get worktree state for a slug.
61
+ * @param {string} cwd
62
+ * @param {string} project
63
+ * @param {string} slug
64
+ * @returns {object|null}
65
+ */
66
+ function _getWorktreeState(cwd, project, slug) {
67
+ const data = _readLocalConfig(cwd);
68
+ return (data.projects && data.projects[project]
69
+ && data.projects[project].worktrees
70
+ && data.projects[project].worktrees[slug]) || null;
71
+ }
72
+
73
+ /**
74
+ * Set worktree state for a slug.
75
+ * @param {string} cwd
76
+ * @param {string} project
77
+ * @param {string} slug
78
+ * @param {object} entry - { type, mode, setup_complete, repos: { repoName: absPath } }
79
+ */
80
+ function _setWorktreeState(cwd, project, slug, entry) {
81
+ const data = _readLocalConfig(cwd);
82
+ if (!data.projects) data.projects = {};
83
+ if (!data.projects[project]) data.projects[project] = {};
84
+ if (!data.projects[project].worktrees) data.projects[project].worktrees = {};
85
+
86
+ // Merge repos into existing entry if present (multi-repo iterative creation)
87
+ const existing = data.projects[project].worktrees[slug];
88
+ if (existing && existing.repos && entry.repos) {
89
+ entry.repos = { ...existing.repos, ...entry.repos };
90
+ }
91
+
92
+ data.projects[project].worktrees[slug] = entry;
93
+ _writeLocalConfig(cwd, data);
94
+ }
95
+
96
+ /**
97
+ * Remove worktree state for a slug.
98
+ * @param {string} cwd
99
+ * @param {string} project
100
+ * @param {string} slug
101
+ */
102
+ function _removeWorktreeState(cwd, project, slug) {
103
+ const data = _readLocalConfig(cwd);
104
+ if (data.projects && data.projects[project]
105
+ && data.projects[project].worktrees
106
+ && data.projects[project].worktrees[slug]) {
107
+ delete data.projects[project].worktrees[slug];
108
+ _writeLocalConfig(cwd, data);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Build worktree path as sibling to main checkout.
114
+ * Format: {repoParent}/{repoBaseName}--{projectSlug}-{slug}
115
+ * @param {string} repoAbsPath - Absolute path to the repo's main checkout
116
+ * @param {string} projectSlug
117
+ * @param {string} slug
118
+ * @returns {string}
119
+ */
120
+ function _buildWorktreePath(repoAbsPath, projectSlug, slug) {
121
+ return path.join(
122
+ path.dirname(repoAbsPath),
123
+ path.basename(repoAbsPath) + '--' + projectSlug + '-' + slug
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Build branch name from type and slug.
129
+ * @param {string} type - 'milestone' or 'quick'
130
+ * @param {string} slug
131
+ * @returns {string}
132
+ */
133
+ function _buildBranchName(type, slug) {
134
+ if (type === 'milestone') return 'milestone/' + slug;
135
+ if (type === 'quick') return 'quick/' + slug;
136
+ return slug;
137
+ }
138
+
139
+ /**
140
+ * Sanitize a string into a valid slug.
141
+ * @param {string} input
142
+ * @returns {string}
143
+ */
144
+ function _sanitizeSlug(input) {
145
+ if (!input) return '';
146
+ return input
147
+ .toLowerCase()
148
+ .replace(/[^a-z0-9-]/g, '-')
149
+ .replace(/-{2,}/g, '-')
150
+ .replace(/^-+|-+$/g, '')
151
+ .slice(0, 50)
152
+ .replace(/-+$/, '');
153
+ }
154
+
155
+ /**
156
+ * Detect collision and disambiguate path if needed.
157
+ * @param {string} targetPath
158
+ * @param {string} slug
159
+ * @returns {string}
160
+ */
161
+ function _detectCollision(targetPath, slug) {
162
+ if (!fs.existsSync(targetPath)) return targetPath;
163
+ // Append first 4 chars of a hash for disambiguation
164
+ const hash = crypto.createHash('sha256')
165
+ .update(slug + '-' + Date.now())
166
+ .digest('hex')
167
+ .slice(0, 4);
168
+ return targetPath + '-' + hash;
169
+ }
170
+
171
+ /**
172
+ * Resolve a relative repo path from REPOS.md to an absolute path.
173
+ * @param {string} cwd
174
+ * @param {string} repoRelPath
175
+ * @returns {string}
176
+ */
177
+ function _resolveRepoAbsPath(cwd, repoRelPath) {
178
+ const root = getPlanningRoot(cwd);
179
+ return path.resolve(root, repoRelPath);
180
+ }
181
+
182
+ /**
183
+ * Execute setup command in worktree directory.
184
+ * Setup script receives: $1=slug, $2=worktree_path (absolute).
185
+ * cwd is set to the worktree directory.
186
+ * @param {string} worktreePath - Absolute path to the worktree directory
187
+ * @param {string} setupCmd - Setup command from REPOS.md
188
+ * @param {string} slug - Worktree slug
189
+ * @returns {{ success: boolean, skipped: boolean, error?: string, stderr?: string }}
190
+ */
191
+ function _runSetupCommand(worktreePath, setupCmd, slug) {
192
+ if (!setupCmd) return { success: true, skipped: true };
193
+ try {
194
+ const cmd = setupCmd + ' ' + slug + ' "' + worktreePath + '"';
195
+ execSync(cmd, {
196
+ cwd: worktreePath,
197
+ stdio: ['pipe', 'pipe', 'pipe'],
198
+ encoding: 'utf-8',
199
+ timeout: 300000, // 5 minutes
200
+ });
201
+ return { success: true, skipped: false };
202
+ } catch (err) {
203
+ return {
204
+ success: false,
205
+ skipped: false,
206
+ error: err.message || String(err),
207
+ stderr: err.stderr || '',
208
+ };
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Find the main checkout path for a repo given its worktree path and branch.
214
+ * Derives from the worktree naming convention.
215
+ * @param {string} cwd
216
+ * @param {string} repoName
217
+ * @returns {string|null}
218
+ */
219
+ function _findMainCheckoutPath(cwd, repoName) {
220
+ const parsed = parseReposMd(cwd);
221
+ if (!parsed || !parsed.repos) return null;
222
+ const repo = parsed.repos.find(function(r) { return r.name === repoName; });
223
+ if (!repo || !repo.path) return null;
224
+ return _resolveRepoAbsPath(cwd, repo.path);
225
+ }
226
+
227
+ /**
228
+ * Read active milestone version from projects/{project}/STATE.md frontmatter.
229
+ * Returns null on any failure — callers must not throw.
230
+ * @param {string} cwd
231
+ * @param {string} project
232
+ * @returns {string|null}
233
+ */
234
+ function _readMilestoneVersion(cwd, project) {
235
+ try {
236
+ const statePath = path.join(cwd, 'projects', project, 'STATE.md');
237
+ if (!fs.existsSync(statePath)) return null;
238
+ const content = fs.readFileSync(statePath, 'utf-8');
239
+ const fm = extractFrontmatter(content);
240
+ const v = fm && fm.milestone;
241
+ if (typeof v === 'string' && /^v\d+(\.\d+)?$/.test(v)) return v;
242
+ return null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ // ─── Exported command functions ───────────────────────────────────────────────
249
+
250
+ /**
251
+ * Create worktrees for a project's repos.
252
+ * @param {string} cwd
253
+ * @param {string[]} args - [slug, --type milestone|quick, --mode full|debug|null, --repo name]
254
+ */
255
+ function cmdWorktreesCreate(cwd, args) {
256
+ // Parse args
257
+ const slug = _sanitizeSlug(args[0]);
258
+ if (!slug) error('Usage: worktrees create <slug> --type milestone|quick');
259
+
260
+ const typeIdx = args.indexOf('--type');
261
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : null;
262
+ if (!type || (type !== 'milestone' && type !== 'quick')) {
263
+ error('--type must be "milestone" or "quick"');
264
+ }
265
+
266
+ const modeIdx = args.indexOf('--mode');
267
+ const mode = modeIdx !== -1 ? args[modeIdx + 1] : null;
268
+
269
+ const repoIdx = args.indexOf('--repo');
270
+ const repoFilter = repoIdx !== -1 ? args[repoIdx + 1] : null;
271
+
272
+ // Load config
273
+ const config = loadConfig(cwd);
274
+ const project = config.current_project;
275
+ if (!project) error('No current project set. Use dgs-tools projects switch <name>');
276
+
277
+ const baseBranch = config.base_branch || 'main';
278
+
279
+ // Parse REPOS.md
280
+ const parsed = parseReposMd(cwd);
281
+ if (!parsed || !parsed.repos || parsed.repos.length === 0) {
282
+ error('No repos found in REPOS.md');
283
+ }
284
+
285
+ let repos = parsed.repos;
286
+ if (repoFilter) {
287
+ repos = repos.filter(function(r) { return r.name === repoFilter; });
288
+ if (repos.length === 0) error('Repo "' + repoFilter + '" not found in REPOS.md');
289
+ }
290
+
291
+ const createdRepos = {};
292
+
293
+ for (const repo of repos) {
294
+ const repoAbsPath = _resolveRepoAbsPath(cwd, repo.path);
295
+ let worktreePath = _buildWorktreePath(repoAbsPath, project, slug);
296
+ worktreePath = _detectCollision(worktreePath, slug);
297
+ const branchName = _buildBranchName(type, slug);
298
+
299
+ // Check existing state
300
+ const existingState = _getWorktreeState(cwd, project, slug);
301
+ if (existingState && existingState.repos && existingState.repos[repo.name]
302
+ && fs.existsSync(existingState.repos[repo.name])) {
303
+ process.stderr.write('Worktree for ' + repo.name + ' already exists at ' + existingState.repos[repo.name] + ', skipping.\n');
304
+ createdRepos[repo.name] = existingState.repos[repo.name];
305
+ continue;
306
+ }
307
+
308
+ // Verify main checkout is on base branch
309
+ const headResult = execGit(repoAbsPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
310
+ if (headResult.exitCode !== 0) {
311
+ error('Cannot determine branch for ' + repo.name + ': ' + headResult.stderr);
312
+ }
313
+ if (headResult.stdout !== baseBranch) {
314
+ error('Main checkout of ' + repo.name + ' is not on ' + baseBranch + ' (currently on ' + headResult.stdout + '). Cannot create worktree.');
315
+ }
316
+
317
+ // Check if branch already exists and is checked out elsewhere
318
+ const worktreeListResult = execGit(repoAbsPath, ['worktree', 'list', '--porcelain']);
319
+ if (worktreeListResult.exitCode === 0 && worktreeListResult.stdout.includes(branchName)) {
320
+ // Try pruning stale worktrees first
321
+ execGit(repoAbsPath, ['worktree', 'prune']);
322
+ }
323
+
324
+ // Create worktree
325
+ const createResult = execGit(repoAbsPath, ['worktree', 'add', '-b', branchName, worktreePath, baseBranch]);
326
+ if (createResult.exitCode !== 0) {
327
+ // Branch might already exist (e.g., from a previous partial creation)
328
+ // Try without -b (checkout existing branch)
329
+ const retryResult = execGit(repoAbsPath, ['worktree', 'add', worktreePath, branchName]);
330
+ if (retryResult.exitCode !== 0) {
331
+ error('Failed to create worktree for ' + repo.name + ': ' + createResult.stderr + ' | Retry: ' + retryResult.stderr);
332
+ }
333
+ }
334
+
335
+ // Run setup
336
+ const setupResult = _runSetupCommand(worktreePath, repo.setup, slug);
337
+ const setupComplete = setupResult.success;
338
+
339
+ if (!setupResult.success && !setupResult.skipped) {
340
+ process.stderr.write('Warning: Setup failed for ' + repo.name + ': ' + (setupResult.error || '') + '\n');
341
+ process.stderr.write('Re-run setup: dgs-tools worktrees setup ' + slug + '\n');
342
+ }
343
+
344
+ // Write state. milestone_version is captured from STATE.md frontmatter at
345
+ // creation time for milestone-type worktrees; quick worktrees and
346
+ // unresolvable STATE reads degrade cleanly to null.
347
+ const milestoneVersion = type === 'milestone' ? _readMilestoneVersion(cwd, project) : null;
348
+ _setWorktreeState(cwd, project, slug, {
349
+ type: type,
350
+ mode: mode,
351
+ setup_complete: setupComplete,
352
+ milestone_version: milestoneVersion,
353
+ repos: { [repo.name]: worktreePath },
354
+ });
355
+
356
+ createdRepos[repo.name] = worktreePath;
357
+ }
358
+
359
+ output({ created: true, slug: slug, type: type, repos: createdRepos });
360
+ }
361
+
362
+ /**
363
+ * Remove a worktree.
364
+ * @param {string} cwd
365
+ * @param {string[]} args - [slug]
366
+ */
367
+ function cmdWorktreesRemove(cwd, args) {
368
+ const slug = args[0];
369
+ if (!slug) error('Usage: worktrees remove <slug>');
370
+
371
+ const config = loadConfig(cwd);
372
+ const project = config.current_project;
373
+ if (!project) error('No current project set');
374
+
375
+ const state = _getWorktreeState(cwd, project, slug);
376
+ if (!state) error('No worktree tracked for slug \'' + slug + '\'');
377
+
378
+ const branchName = _buildBranchName(state.type || 'milestone', slug);
379
+
380
+ for (const repoName of Object.keys(state.repos)) {
381
+ const worktreePath = state.repos[repoName];
382
+ const mainCheckout = _findMainCheckoutPath(cwd, repoName);
383
+
384
+ if (!mainCheckout) {
385
+ process.stderr.write('Warning: Cannot find main checkout for ' + repoName + ', skipping git cleanup.\n');
386
+ // Still try to remove directory
387
+ if (fs.existsSync(worktreePath)) {
388
+ fs.rmSync(worktreePath, { recursive: true, force: true });
389
+ }
390
+ continue;
391
+ }
392
+
393
+ // Remove worktree via git
394
+ const removeResult = execGit(mainCheckout, ['worktree', 'remove', worktreePath]);
395
+ if (removeResult.exitCode !== 0) {
396
+ // Directory already gone or other issue — try prune
397
+ execGit(mainCheckout, ['worktree', 'prune']);
398
+ // Force remove directory if still exists
399
+ if (fs.existsSync(worktreePath)) {
400
+ fs.rmSync(worktreePath, { recursive: true, force: true });
401
+ execGit(mainCheckout, ['worktree', 'prune']);
402
+ }
403
+ }
404
+
405
+ // Delete branch (try -d first, then -D)
406
+ const delResult = execGit(mainCheckout, ['branch', '-d', branchName]);
407
+ if (delResult.exitCode !== 0) {
408
+ execGit(mainCheckout, ['branch', '-D', branchName]);
409
+ }
410
+ }
411
+
412
+ // Remove from config
413
+ _removeWorktreeState(cwd, project, slug);
414
+
415
+ // Clear active_context if it points to this slug
416
+ const localData = _readLocalConfig(cwd);
417
+ if (localData.execution && localData.execution.active_context === slug) {
418
+ localData.execution.active_context = null;
419
+ _writeLocalConfig(cwd, localData);
420
+ }
421
+
422
+ output({ removed: true, slug: slug });
423
+ }
424
+
425
+ /**
426
+ * List tracked worktrees.
427
+ * @param {string} cwd
428
+ * @param {string[]} args
429
+ */
430
+ function cmdWorktreesList(cwd, args) {
431
+ const config = loadConfig(cwd);
432
+ const project = config.current_project;
433
+ if (!project) error('No current project set');
434
+
435
+ const data = _readLocalConfig(cwd);
436
+ const worktrees = (data.projects && data.projects[project]
437
+ && data.projects[project].worktrees) || {};
438
+
439
+ const result = [];
440
+ for (const slug of Object.keys(worktrees)) {
441
+ const entry = worktrees[slug];
442
+ const repoStatus = {};
443
+ let stale = false;
444
+
445
+ if (entry.repos) {
446
+ for (const repoName of Object.keys(entry.repos)) {
447
+ const repoPath = entry.repos[repoName];
448
+ const exists = fs.existsSync(repoPath);
449
+ if (!exists) stale = true;
450
+ repoStatus[repoName] = { path: repoPath, exists: exists };
451
+ }
452
+ }
453
+
454
+ result.push({
455
+ slug: slug,
456
+ type: entry.type || 'milestone',
457
+ mode: entry.mode || null,
458
+ setup_complete: entry.setup_complete || false,
459
+ repos: repoStatus,
460
+ stale: stale,
461
+ });
462
+ }
463
+
464
+ output(result);
465
+ }
466
+
467
+ /**
468
+ * Re-run setup for a worktree.
469
+ * @param {string} cwd
470
+ * @param {string[]} args - [slug]
471
+ */
472
+ function cmdWorktreesSetup(cwd, args) {
473
+ const slug = args[0];
474
+ if (!slug) error('Usage: worktrees setup <slug>');
475
+
476
+ const config = loadConfig(cwd);
477
+ const project = config.current_project;
478
+ if (!project) error('No current project set');
479
+
480
+ const state = _getWorktreeState(cwd, project, slug);
481
+ if (!state) error('No worktree tracked for slug \'' + slug + '\'');
482
+
483
+ const parsed = parseReposMd(cwd);
484
+ if (!parsed || !parsed.repos) error('Cannot parse REPOS.md');
485
+
486
+ const results = {};
487
+ let allSuccess = true;
488
+
489
+ for (const repoName of Object.keys(state.repos)) {
490
+ const worktreePath = state.repos[repoName];
491
+ const repoEntry = parsed.repos.find(function(r) { return r.name === repoName; });
492
+
493
+ if (!repoEntry) {
494
+ results[repoName] = 'skipped';
495
+ continue;
496
+ }
497
+
498
+ if (!repoEntry.setup) {
499
+ results[repoName] = 'no_setup_command';
500
+ continue;
501
+ }
502
+
503
+ const setupResult = _runSetupCommand(worktreePath, repoEntry.setup, slug);
504
+ results[repoName] = setupResult.success ? 'success' : 'failed';
505
+ if (!setupResult.success) {
506
+ allSuccess = false;
507
+ process.stderr.write('Setup failed for ' + repoName + ': ' + (setupResult.error || '') + '\n');
508
+ }
509
+ }
510
+
511
+ // Update setup_complete flag
512
+ _setWorktreeState(cwd, project, slug, {
513
+ ...state,
514
+ setup_complete: allSuccess,
515
+ });
516
+
517
+ output({ setup_complete: allSuccess, slug: slug, results: results });
518
+ }
519
+
520
+ /**
521
+ * Prune orphaned worktree entries.
522
+ * @param {string} cwd
523
+ * @param {string[]} args
524
+ */
525
+ function cmdWorktreesPrune(cwd, args) {
526
+ const config = loadConfig(cwd);
527
+ const project = config.current_project;
528
+ if (!project) error('No current project set');
529
+
530
+ const data = _readLocalConfig(cwd);
531
+ const worktrees = (data.projects && data.projects[project]
532
+ && data.projects[project].worktrees) || {};
533
+
534
+ const pruned = [];
535
+
536
+ for (const slug of Object.keys(worktrees)) {
537
+ const entry = worktrees[slug];
538
+ if (!entry.repos) continue;
539
+
540
+ // Check if ALL repo directories are missing
541
+ const allMissing = Object.keys(entry.repos).every(function(repoName) {
542
+ return !fs.existsSync(entry.repos[repoName]);
543
+ });
544
+
545
+ if (!allMissing) continue;
546
+
547
+ const branchName = _buildBranchName(entry.type || 'milestone', slug);
548
+
549
+ // Clean up git metadata and branches
550
+ for (const repoName of Object.keys(entry.repos)) {
551
+ const mainCheckout = _findMainCheckoutPath(cwd, repoName);
552
+ if (mainCheckout) {
553
+ execGit(mainCheckout, ['worktree', 'prune']);
554
+ execGit(mainCheckout, ['branch', '-D', branchName]);
555
+ }
556
+ }
557
+
558
+ // Remove config entry
559
+ delete worktrees[slug];
560
+ pruned.push(slug);
561
+ }
562
+
563
+ if (pruned.length > 0) {
564
+ _writeLocalConfig(cwd, data);
565
+
566
+ // Clear active_context if it points to a pruned slug
567
+ const freshData = _readLocalConfig(cwd);
568
+ if (freshData.execution && pruned.includes(freshData.execution.active_context)) {
569
+ freshData.execution.active_context = null;
570
+ _writeLocalConfig(cwd, freshData);
571
+ }
572
+ }
573
+
574
+ output({ pruned: pruned, count: pruned.length });
575
+ }
576
+
577
+ // ─── Shared functions ─────────────────────────────────────────────────────────
578
+
579
+ /**
580
+ * Rebase a worktree branch onto base_branch and fast-forward merge.
581
+ * Used by complete-milestone and complete-quick for clean rebase-before-merge completion.
582
+ *
583
+ * Steps:
584
+ * 1. Pull base_branch in main checkout (fetch + pull)
585
+ * 2. Check if rebase is needed (idempotent -- skip if already rebased)
586
+ * 3. Rebase worktree branch onto base_branch in worktree directory
587
+ * 4. On conflict: git rebase --abort, return error with manual instructions
588
+ * 5. Fast-forward merge to base_branch in main checkout
589
+ * 6. Push to remote (always -- not gated by sync config)
590
+ *
591
+ * @param {string} cwd - Planning root for config resolution
592
+ * @param {string} repoName - Repo name from REPOS.md
593
+ * @param {string} slug - Worktree slug (e.g., 'v19-0')
594
+ * @param {object} [options] - { push: true }
595
+ * @returns {{ success: boolean, merged: boolean, skipped: boolean, conflicted: boolean, pushed: boolean, error?: string, manualInstructions?: string }}
596
+ */
597
+ function rebaseAndMerge(cwd, repoName, slug, options) {
598
+ options = options || {};
599
+ const shouldPush = options.push !== false; // default true
600
+
601
+ const config = loadConfig(cwd);
602
+ const project = config.current_project;
603
+ const baseBranch = config.base_branch || 'main';
604
+
605
+ if (!project) return { success: false, merged: false, skipped: false, conflicted: false, pushed: false, error: 'No current project set' };
606
+
607
+ const state = _getWorktreeState(cwd, project, slug);
608
+ if (!state) return { success: false, merged: false, skipped: false, conflicted: false, pushed: false, error: 'No worktree tracked for slug \'' + slug + '\'' };
609
+
610
+ const worktreePath = state.repos && state.repos[repoName];
611
+ if (!worktreePath) return { success: false, merged: false, skipped: false, conflicted: false, pushed: false, error: 'Repo \'' + repoName + '\' not in worktree \'' + slug + '\'' };
612
+
613
+ const mainCheckout = _findMainCheckoutPath(cwd, repoName);
614
+ if (!mainCheckout) return { success: false, merged: false, skipped: false, conflicted: false, pushed: false, error: 'Cannot find main checkout for \'' + repoName + '\'' };
615
+
616
+ const branchName = _buildBranchName(state.type || 'milestone', slug);
617
+
618
+ // Step 1: Pull base_branch in main checkout
619
+ const fetchResult = execGit(mainCheckout, ['fetch', 'origin']);
620
+ if (fetchResult.exitCode !== 0) {
621
+ process.stderr.write('Warning: fetch failed: ' + fetchResult.stderr + '\n');
622
+ // Continue anyway -- local base_branch may still be usable
623
+ }
624
+
625
+ // Ensure main checkout is on base_branch before pulling
626
+ const mainHead = execGit(mainCheckout, ['rev-parse', '--abbrev-ref', 'HEAD']);
627
+ if (mainHead.exitCode === 0 && mainHead.stdout.trim() === baseBranch) {
628
+ const pullResult = execGit(mainCheckout, ['pull', 'origin', baseBranch]);
629
+ if (pullResult.exitCode !== 0) {
630
+ process.stderr.write('Warning: pull failed: ' + pullResult.stderr + '\n');
631
+ // Continue anyway
632
+ }
633
+ }
634
+
635
+ // Step 2: Check if rebase is already done (idempotent re-run)
636
+ // If the milestone branch's merge-base with base_branch equals base_branch tip,
637
+ // the branch is already rebased onto base_branch.
638
+ const baseTip = execGit(mainCheckout, ['rev-parse', baseBranch]);
639
+ const mergeBase = execGit(mainCheckout, ['merge-base', baseBranch, branchName]);
640
+ let rebaseSkipped = false;
641
+ if (baseTip.exitCode === 0 && mergeBase.exitCode === 0
642
+ && baseTip.stdout.trim() === mergeBase.stdout.trim()) {
643
+ // Already rebased -- skip to merge
644
+ process.stderr.write('Branch ' + branchName + ' already rebased onto ' + baseBranch + '. Skipping rebase.\n');
645
+ rebaseSkipped = true;
646
+ } else {
647
+ // Step 3: Rebase worktree branch onto base_branch
648
+ // First ensure worktree is on the correct branch
649
+ const headCheck = execGit(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
650
+ if (headCheck.exitCode !== 0 || headCheck.stdout.trim() !== branchName) {
651
+ return {
652
+ success: false,
653
+ merged: false,
654
+ skipped: false,
655
+ conflicted: false,
656
+ pushed: false,
657
+ error: 'Worktree is not on expected branch ' + branchName + ' (on ' + (headCheck.stdout || 'unknown').trim() + ')',
658
+ };
659
+ }
660
+
661
+ const rebaseResult = execGit(worktreePath, ['rebase', baseBranch]);
662
+ if (rebaseResult.exitCode !== 0) {
663
+ // Rebase failed -- abort cleanly
664
+ execGit(worktreePath, ['rebase', '--abort']);
665
+
666
+ const instructions = [
667
+ 'Rebase conflict in ' + repoName + '. Manual resolution required:',
668
+ '',
669
+ ' cd ' + worktreePath,
670
+ ' git rebase ' + baseBranch,
671
+ ' # Resolve conflicts in each file',
672
+ ' git add <resolved-files>',
673
+ ' git rebase --continue',
674
+ ' # Repeat for each conflicting commit',
675
+ '',
676
+ 'Then re-run: /dgs:complete-milestone',
677
+ ].join('\n');
678
+
679
+ return {
680
+ success: false,
681
+ merged: false,
682
+ skipped: false,
683
+ conflicted: true,
684
+ pushed: false,
685
+ error: 'Rebase conflict in ' + repoName,
686
+ manualInstructions: instructions,
687
+ };
688
+ }
689
+ }
690
+
691
+ // Step 4: Fast-forward merge to base_branch in main checkout
692
+ // Ensure main checkout is on base_branch
693
+ const checkoutBase = execGit(mainCheckout, ['checkout', baseBranch]);
694
+ if (checkoutBase.exitCode !== 0) {
695
+ return {
696
+ success: false,
697
+ merged: false,
698
+ skipped: rebaseSkipped,
699
+ conflicted: false,
700
+ pushed: false,
701
+ error: 'Failed to checkout ' + baseBranch + ' in main checkout: ' + checkoutBase.stderr,
702
+ };
703
+ }
704
+
705
+ const mergeResult = execGit(mainCheckout, ['merge', '--ff-only', branchName]);
706
+ if (mergeResult.exitCode !== 0) {
707
+ return {
708
+ success: false,
709
+ merged: false,
710
+ skipped: rebaseSkipped,
711
+ conflicted: false,
712
+ pushed: false,
713
+ error: 'Fast-forward merge failed: ' + mergeResult.stderr,
714
+ };
715
+ }
716
+
717
+ // Step 5: Push to remote (always -- milestone completion is a definitive push point)
718
+ let pushed = false;
719
+ if (shouldPush) {
720
+ const pushResult = execGit(mainCheckout, ['push', 'origin', baseBranch]);
721
+ pushed = pushResult.exitCode === 0;
722
+ if (!pushed) {
723
+ process.stderr.write('Warning: push failed: ' + pushResult.stderr + '\n');
724
+ }
725
+ }
726
+
727
+ return {
728
+ success: true,
729
+ merged: true,
730
+ skipped: rebaseSkipped,
731
+ conflicted: false,
732
+ pushed: pushed,
733
+ };
734
+ }
735
+
736
+ /**
737
+ * Verify worktree health before execution.
738
+ * Checks: correct branch checked out, no uncommitted changes.
739
+ *
740
+ * @param {string} cwd - Planning root
741
+ * @param {string} slug - Worktree slug
742
+ * @returns {{ healthy: boolean, issues: string[] }}
743
+ */
744
+ function checkWorktreeHealth(cwd, slug) {
745
+ const config = loadConfig(cwd);
746
+ const project = config.current_project;
747
+ if (!project) return { healthy: false, issues: ['No current project set'] };
748
+
749
+ const state = _getWorktreeState(cwd, project, slug);
750
+ if (!state) return { healthy: false, issues: ['No worktree tracked for slug \'' + slug + '\''] };
751
+
752
+ const branchName = _buildBranchName(state.type || 'milestone', slug);
753
+ const issues = [];
754
+
755
+ for (const repoName of Object.keys(state.repos || {})) {
756
+ const worktreePath = state.repos[repoName];
757
+
758
+ // Check directory exists
759
+ if (!fs.existsSync(worktreePath)) {
760
+ issues.push(repoName + ': worktree directory missing at ' + worktreePath);
761
+ continue;
762
+ }
763
+
764
+ // Check correct branch
765
+ const headResult = execGit(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
766
+ if (headResult.exitCode !== 0) {
767
+ issues.push(repoName + ': cannot determine branch');
768
+ } else if (headResult.stdout.trim() !== branchName) {
769
+ issues.push(repoName + ': expected branch ' + branchName + ' but on ' + headResult.stdout.trim());
770
+ }
771
+
772
+ // Check no uncommitted changes
773
+ const statusResult = execGit(worktreePath, ['status', '--porcelain']);
774
+ if (statusResult.exitCode === 0 && statusResult.stdout.trim()) {
775
+ issues.push(repoName + ': has uncommitted changes');
776
+ }
777
+ }
778
+
779
+ return { healthy: issues.length === 0, issues: issues };
780
+ }
781
+
782
+ module.exports = {
783
+ cmdWorktreesCreate,
784
+ cmdWorktreesRemove,
785
+ cmdWorktreesList,
786
+ cmdWorktreesSetup,
787
+ cmdWorktreesPrune,
788
+ rebaseAndMerge,
789
+ checkWorktreeHealth,
790
+ };