@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.
- package/CHANGELOG.md +197 -0
- package/README.md +34 -2
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +61 -3
- package/agents/dgs-planner.md +51 -8
- package/bin/install.js +44 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +4 -3
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +3 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +14 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +626 -46
- package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +80 -6
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +35 -14
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +357 -61
- package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
- package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +146 -3
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +65 -10
- package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
- package/deliver-great-systems/bin/lib/quick.cjs +739 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
- package/deliver-great-systems/bin/lib/repos.cjs +37 -13
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +147 -55
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +198 -7
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +27 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +2 -2
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +218 -24
- package/deliver-great-systems/workflows/complete-quick.md +106 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +209 -33
- package/deliver-great-systems/workflows/execute-plan.md +22 -22
- package/deliver-great-systems/workflows/help.md +53 -20
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +45 -167
- package/deliver-great-systems/workflows/new-milestone.md +140 -33
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +106 -0
- package/deliver-great-systems/workflows/quick.md +328 -26
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +77 -139
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +29 -43
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +11 -13
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- 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
|
+
};
|