@ktpartners/dgs-platform 2.8.0 → 3.0.4
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 +96 -0
- package/README.md +41 -13
- package/agents/dgs-plan-checker.md +29 -3
- package/agents/dgs-planner.md +10 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +2 -2
- 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/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 +6 -6
- 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 +2 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +1 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
- package/deliver-great-systems/bin/lib/commands.cjs +316 -31
- package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
- package/deliver-great-systems/bin/lib/config.cjs +39 -6
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +28 -11
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -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 +306 -39
- package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
- package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
- 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 +54 -29
- package/deliver-great-systems/bin/lib/phase.cjs +128 -2
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/projects.cjs +28 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
- package/deliver-great-systems/bin/lib/quick.cjs +584 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
- package/deliver-great-systems/bin/lib/repos.cjs +25 -1
- 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 +142 -54
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +80 -1
- package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
- package/deliver-great-systems/templates/claude-md.md +16 -0
- 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-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-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/complete-milestone.md +197 -22
- package/deliver-great-systems/workflows/complete-quick.md +68 -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/execute-phase.md +121 -32
- package/deliver-great-systems/workflows/execute-plan.md +12 -21
- package/deliver-great-systems/workflows/help.md +33 -29
- package/deliver-great-systems/workflows/init-product.md +2 -18
- package/deliver-great-systems/workflows/new-milestone.md +40 -24
- package/deliver-great-systems/workflows/new-project.md +22 -680
- 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 +68 -0
- package/deliver-great-systems/workflows/quick.md +152 -23
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +8 -8
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +8 -8
- 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 +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick -- Quick workflow lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Two flavors:
|
|
5
|
+
* - Product-level quick: ephemeral worktree off main with quick/{slug} branch.
|
|
6
|
+
* Full lifecycle: start, complete (rebase+merge+cleanup), abandon (discard).
|
|
7
|
+
* - Milestone-context quick: work in existing milestone worktree, no new worktree.
|
|
8
|
+
* No complete/abandon -- changes merge with the milestone.
|
|
9
|
+
*
|
|
10
|
+
* One active product-level quick at a time per product (planning root).
|
|
11
|
+
* Stale entries (directory missing) are auto-cleared.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { execGit, output, error, loadConfig } = require('./core.cjs');
|
|
19
|
+
const { getLocalConfigPath } = require('./config.cjs');
|
|
20
|
+
const { getPlanningRoot } = require('./paths.cjs');
|
|
21
|
+
const { cmdWorktreesCreate, cmdWorktreesRemove, rebaseAndMerge } = require('./worktrees.cjs');
|
|
22
|
+
|
|
23
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read config.local.json safely.
|
|
27
|
+
* @param {string} cwd
|
|
28
|
+
* @returns {object}
|
|
29
|
+
*/
|
|
30
|
+
function _readLocalConfig(cwd) {
|
|
31
|
+
const localPath = getLocalConfigPath(cwd);
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(localPath)) {
|
|
34
|
+
return JSON.parse(fs.readFileSync(localPath, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
} catch { /* ignore */ }
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write config.local.json atomically.
|
|
42
|
+
* @param {string} cwd
|
|
43
|
+
* @param {object} data
|
|
44
|
+
*/
|
|
45
|
+
function _writeLocalConfig(cwd, data) {
|
|
46
|
+
const localPath = getLocalConfigPath(cwd);
|
|
47
|
+
const dir = path.dirname(localPath);
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const tmpPath = localPath + '.tmp.' + process.pid;
|
|
52
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
53
|
+
fs.renameSync(tmpPath, localPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sanitize a title into a valid slug for branch/worktree naming.
|
|
58
|
+
* @param {string} title
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function _sanitizeSlug(title) {
|
|
62
|
+
if (!title) return '';
|
|
63
|
+
return title
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
66
|
+
.replace(/^-+|-+$/g, '')
|
|
67
|
+
.slice(0, 40)
|
|
68
|
+
.replace(/-+$/, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear a stale quick entry from config.local.json.
|
|
73
|
+
* Called when getActiveQuick detects a quick whose worktree directory is missing.
|
|
74
|
+
* @param {string} cwd
|
|
75
|
+
* @param {string} project
|
|
76
|
+
* @param {string} slug
|
|
77
|
+
* @param {object} localConfig - Already loaded config.local.json object
|
|
78
|
+
*/
|
|
79
|
+
function _clearStaleQuick(cwd, project, slug, localConfig) {
|
|
80
|
+
if (localConfig.projects && localConfig.projects[project]
|
|
81
|
+
&& localConfig.projects[project].worktrees) {
|
|
82
|
+
delete localConfig.projects[project].worktrees[slug];
|
|
83
|
+
}
|
|
84
|
+
// Clear active_context if it points to this stale quick
|
|
85
|
+
if (localConfig.execution && localConfig.execution.active_context === slug) {
|
|
86
|
+
delete localConfig.execution.active_context;
|
|
87
|
+
}
|
|
88
|
+
_writeLocalConfig(cwd, localConfig);
|
|
89
|
+
process.stderr.write('Warning: Stale quick \'' + slug + '\' auto-cleared (worktree directory missing)\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Exported functions ───────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect whether a quick should be product-level or milestone-context.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} cwd - Planning root
|
|
98
|
+
* @param {boolean} forceMain - True if --main flag was passed
|
|
99
|
+
* @returns {{ mode: 'product'|'milestone-context', activeSlug?: string, activeMilestone?: string }}
|
|
100
|
+
*/
|
|
101
|
+
function detectQuickMode(cwd, forceMain) {
|
|
102
|
+
if (forceMain) return { mode: 'product' };
|
|
103
|
+
|
|
104
|
+
const config = loadConfig(cwd);
|
|
105
|
+
const localConfig = _readLocalConfig(cwd);
|
|
106
|
+
const activeContext = localConfig.execution && localConfig.execution.active_context;
|
|
107
|
+
|
|
108
|
+
if (!activeContext) return { mode: 'product' };
|
|
109
|
+
|
|
110
|
+
// Check if active context is a milestone worktree
|
|
111
|
+
const project = config.current_project;
|
|
112
|
+
if (!project) return { mode: 'product' };
|
|
113
|
+
|
|
114
|
+
const worktrees = (localConfig.projects && localConfig.projects[project]
|
|
115
|
+
&& localConfig.projects[project].worktrees) || {};
|
|
116
|
+
const entry = worktrees[activeContext];
|
|
117
|
+
|
|
118
|
+
if (entry && entry.type === 'milestone') {
|
|
119
|
+
return { mode: 'milestone-context', activeSlug: activeContext, activeMilestone: activeContext };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If active context is a quick or unknown, treat as product-level
|
|
123
|
+
return { mode: 'product' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the currently active product-level quick, if any.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} cwd - Planning root
|
|
130
|
+
* @returns {{ slug: string, entry: object }|null}
|
|
131
|
+
*/
|
|
132
|
+
function getActiveQuick(cwd) {
|
|
133
|
+
const config = loadConfig(cwd);
|
|
134
|
+
const localConfig = _readLocalConfig(cwd);
|
|
135
|
+
const project = config.current_project;
|
|
136
|
+
if (!project) return null;
|
|
137
|
+
|
|
138
|
+
const worktrees = (localConfig.projects && localConfig.projects[project]
|
|
139
|
+
&& localConfig.projects[project].worktrees) || {};
|
|
140
|
+
|
|
141
|
+
for (const [slug, entry] of Object.entries(worktrees)) {
|
|
142
|
+
if (entry.type === 'quick') {
|
|
143
|
+
// Verify directory still exists (stale detection)
|
|
144
|
+
const repos = entry.repos || {};
|
|
145
|
+
const paths = Object.values(repos);
|
|
146
|
+
const anyExists = paths.some(function(p) { return fs.existsSync(p); });
|
|
147
|
+
if (anyExists) {
|
|
148
|
+
return { slug: slug, entry: entry };
|
|
149
|
+
}
|
|
150
|
+
// Directory missing -- stale entry, auto-clear
|
|
151
|
+
_clearStaleQuick(cwd, project, slug, localConfig);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Start a product-level quick: guard check, create worktree, set context.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} cwd - Planning root
|
|
162
|
+
* @param {string} title - Quick task title (used for slug and branch)
|
|
163
|
+
* @param {string|null} mode - 'full', 'debug', or null (plain)
|
|
164
|
+
* @returns {{ success: boolean, slug?: string, error?: string, activeSlug?: string }}
|
|
165
|
+
*/
|
|
166
|
+
function startProductQuick(cwd, title, mode) {
|
|
167
|
+
// Guard: one active product-level quick at a time
|
|
168
|
+
const active = getActiveQuick(cwd);
|
|
169
|
+
if (active) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: 'Quick worktree already active: \'' + active.slug + '\'. Complete it (`dgs:complete-quick`), abandon it (`dgs:abandon-quick`), or use `dgs:fast` for trivial fixes.',
|
|
173
|
+
activeSlug: active.slug,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const config = loadConfig(cwd);
|
|
178
|
+
const project = config.current_project;
|
|
179
|
+
if (!project) return { success: false, error: 'No current project set' };
|
|
180
|
+
|
|
181
|
+
// Sanitize title to slug
|
|
182
|
+
const slug = _sanitizeSlug(title);
|
|
183
|
+
if (!slug) return { success: false, error: 'Cannot create slug from title: ' + title };
|
|
184
|
+
|
|
185
|
+
// Create worktree via existing cmdWorktreesCreate
|
|
186
|
+
// Note: cmdWorktreesCreate calls output() which exits the process.
|
|
187
|
+
// We need to handle this differently -- call the underlying logic directly.
|
|
188
|
+
// Since cmdWorktreesCreate calls process.exit via output(), we invoke it
|
|
189
|
+
// through a subprocess to capture the result.
|
|
190
|
+
const { execSync } = require('child_process');
|
|
191
|
+
const dgsTools = path.resolve(__dirname, '..', 'dgs-tools.cjs');
|
|
192
|
+
const root = getPlanningRoot(cwd);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const modeArgs = mode ? ' --mode ' + mode : '';
|
|
196
|
+
const result = execSync(
|
|
197
|
+
'node ' + JSON.stringify(dgsTools) + ' worktrees create ' + JSON.stringify(slug) + ' --type quick' + modeArgs,
|
|
198
|
+
{ cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
|
|
199
|
+
);
|
|
200
|
+
// Parse output to verify creation
|
|
201
|
+
const parsed = JSON.parse(result.trim());
|
|
202
|
+
if (!parsed.created) {
|
|
203
|
+
return { success: false, error: 'Worktree creation returned unexpected result' };
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const stderr = (err.stderr || '').toString().trim();
|
|
207
|
+
return { success: false, error: 'Failed to create quick worktree: ' + (stderr || err.message || String(err)) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Set mode in worktree config entry and set active_context
|
|
211
|
+
const localConfig = _readLocalConfig(cwd);
|
|
212
|
+
if (localConfig.projects && localConfig.projects[project]
|
|
213
|
+
&& localConfig.projects[project].worktrees
|
|
214
|
+
&& localConfig.projects[project].worktrees[slug]) {
|
|
215
|
+
localConfig.projects[project].worktrees[slug].mode = mode || null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Set active_context
|
|
219
|
+
if (!localConfig.execution) localConfig.execution = {};
|
|
220
|
+
localConfig.execution.active_context = slug;
|
|
221
|
+
|
|
222
|
+
_writeLocalConfig(cwd, localConfig);
|
|
223
|
+
|
|
224
|
+
const finalConfig = _readLocalConfig(cwd);
|
|
225
|
+
const repos =
|
|
226
|
+
(finalConfig.projects &&
|
|
227
|
+
finalConfig.projects[project] &&
|
|
228
|
+
finalConfig.projects[project].worktrees &&
|
|
229
|
+
finalConfig.projects[project].worktrees[slug] &&
|
|
230
|
+
finalConfig.projects[project].worktrees[slug].repos) || {};
|
|
231
|
+
return { success: true, slug: slug, repos: repos };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Complete the active product-level quick: rebase, merge, push, cleanup.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} cwd - Planning root
|
|
238
|
+
* @returns {{ success: boolean, commitCount?: number, slug?: string, error?: string, manualInstructions?: string }}
|
|
239
|
+
*/
|
|
240
|
+
function quickComplete(cwd) {
|
|
241
|
+
const active = getActiveQuick(cwd);
|
|
242
|
+
if (!active) {
|
|
243
|
+
return { success: false, error: 'No active product-level quick to complete. If working in a milestone context, changes are part of the milestone.' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { slug, entry } = active;
|
|
247
|
+
const repos = entry.repos || {};
|
|
248
|
+
const repoNames = Object.keys(repos);
|
|
249
|
+
|
|
250
|
+
if (repoNames.length === 0) {
|
|
251
|
+
return { success: false, error: 'Quick \'' + slug + '\' has no repos tracked' };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Count commits on quick branch vs base_branch for summary
|
|
255
|
+
let totalCommits = 0;
|
|
256
|
+
const config = loadConfig(cwd);
|
|
257
|
+
const baseBranch = config.base_branch || 'main';
|
|
258
|
+
|
|
259
|
+
// Rebase and merge each repo
|
|
260
|
+
for (const repoName of repoNames) {
|
|
261
|
+
const worktreePath = repos[repoName];
|
|
262
|
+
const branchName = 'quick/' + slug;
|
|
263
|
+
|
|
264
|
+
// Count commits before merge
|
|
265
|
+
const countResult = execGit(worktreePath, ['rev-list', '--count', baseBranch + '..' + branchName]);
|
|
266
|
+
if (countResult.exitCode === 0) {
|
|
267
|
+
totalCommits += parseInt(countResult.stdout.trim(), 10) || 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const result = rebaseAndMerge(cwd, repoName, slug, { push: true });
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
slug: slug,
|
|
275
|
+
error: result.error,
|
|
276
|
+
manualInstructions: result.manualInstructions,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Cleanup: remove worktree and branch via subprocess
|
|
282
|
+
const { execSync } = require('child_process');
|
|
283
|
+
const dgsTools = path.resolve(__dirname, '..', 'dgs-tools.cjs');
|
|
284
|
+
const root = getPlanningRoot(cwd);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
execSync(
|
|
288
|
+
'node ' + JSON.stringify(dgsTools) + ' worktrees remove ' + JSON.stringify(slug),
|
|
289
|
+
{ cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
|
|
290
|
+
);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
process.stderr.write('Warning: Worktree cleanup failed: ' + ((err.stderr || '').toString().trim() || err.message || String(err)) + '\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Clear active_context and worktree entry (belt-and-suspenders -- remove should handle this)
|
|
296
|
+
const localConfig = _readLocalConfig(cwd);
|
|
297
|
+
if (localConfig.execution && localConfig.execution.active_context === slug) {
|
|
298
|
+
delete localConfig.execution.active_context;
|
|
299
|
+
}
|
|
300
|
+
const project = config.current_project;
|
|
301
|
+
if (project && localConfig.projects && localConfig.projects[project]
|
|
302
|
+
&& localConfig.projects[project].worktrees) {
|
|
303
|
+
delete localConfig.projects[project].worktrees[slug];
|
|
304
|
+
}
|
|
305
|
+
_writeLocalConfig(cwd, localConfig);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
slug: slug,
|
|
310
|
+
commitCount: totalCommits,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Abandon the active product-level quick: remove worktree without merging.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} cwd - Planning root
|
|
318
|
+
* @param {boolean} confirmed - Must be true to proceed (workflows handle confirmation UI)
|
|
319
|
+
* @returns {{ success: boolean, slug?: string, error?: string }}
|
|
320
|
+
*/
|
|
321
|
+
function quickAbandon(cwd, confirmed) {
|
|
322
|
+
if (!confirmed) {
|
|
323
|
+
return { success: false, error: 'Abandon not confirmed. Pass confirmed=true to proceed.' };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const active = getActiveQuick(cwd);
|
|
327
|
+
if (!active) {
|
|
328
|
+
return { success: false, error: 'No active product-level quick to abandon. If working in a milestone context, use complete-milestone instead.' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { slug } = active;
|
|
332
|
+
|
|
333
|
+
// Remove worktree and branch (without merging) via subprocess
|
|
334
|
+
const { execSync } = require('child_process');
|
|
335
|
+
const dgsTools = path.resolve(__dirname, '..', 'dgs-tools.cjs');
|
|
336
|
+
const root = getPlanningRoot(cwd);
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
execSync(
|
|
340
|
+
'node ' + JSON.stringify(dgsTools) + ' worktrees remove ' + JSON.stringify(slug),
|
|
341
|
+
{ cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
|
|
342
|
+
);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
process.stderr.write('Warning: Worktree removal failed: ' + ((err.stderr || '').toString().trim() || err.message || String(err)) + '\n');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Clear active_context and worktree entry
|
|
348
|
+
const localConfig = _readLocalConfig(cwd);
|
|
349
|
+
const config = loadConfig(cwd);
|
|
350
|
+
if (localConfig.execution && localConfig.execution.active_context === slug) {
|
|
351
|
+
delete localConfig.execution.active_context;
|
|
352
|
+
}
|
|
353
|
+
const project = config.current_project;
|
|
354
|
+
if (project && localConfig.projects && localConfig.projects[project]
|
|
355
|
+
&& localConfig.projects[project].worktrees) {
|
|
356
|
+
delete localConfig.projects[project].worktrees[slug];
|
|
357
|
+
}
|
|
358
|
+
_writeLocalConfig(cwd, localConfig);
|
|
359
|
+
|
|
360
|
+
return { success: true, slug: slug };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── CLI command wrappers ─────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* CLI handler for `dgs-tools complete-quick` (also `quick-complete` for backward compat).
|
|
367
|
+
* @param {string} cwd
|
|
368
|
+
*/
|
|
369
|
+
function cmdQuickComplete(cwd) {
|
|
370
|
+
const result = quickComplete(cwd);
|
|
371
|
+
if (!result.success) {
|
|
372
|
+
if (result.manualInstructions) {
|
|
373
|
+
process.stderr.write(result.manualInstructions + '\n');
|
|
374
|
+
}
|
|
375
|
+
error(result.error);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
output({
|
|
379
|
+
completed: true,
|
|
380
|
+
slug: result.slug,
|
|
381
|
+
commits: result.commitCount,
|
|
382
|
+
message: 'Quick \'' + result.slug + '\' merged to main (' + result.commitCount + ' commits). Worktree cleaned up. Pushed to origin.',
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* CLI handler for `dgs-tools abandon-quick` (also `quick-abandon` for backward compat).
|
|
388
|
+
* @param {string} cwd
|
|
389
|
+
* @param {string[]} args
|
|
390
|
+
*/
|
|
391
|
+
function cmdQuickAbandon(cwd, args) {
|
|
392
|
+
const confirmed = args && args.includes('--confirmed');
|
|
393
|
+
const result = quickAbandon(cwd, confirmed);
|
|
394
|
+
if (!result.success) {
|
|
395
|
+
error(result.error);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
output({
|
|
399
|
+
abandoned: true,
|
|
400
|
+
slug: result.slug,
|
|
401
|
+
message: 'Quick \'' + result.slug + '\' abandoned. Worktree removed.',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* CLI: `dgs-tools quick finalize <quick_id> [flags]`.
|
|
407
|
+
*
|
|
408
|
+
* Stages and commits all quick-task artifacts atomically in a single git commit:
|
|
409
|
+
* - {quickDir}/{quickId}-{slug}/{quickId}-PLAN.md (required in non-fast mode)
|
|
410
|
+
* - {quickDir}/{quickId}-{slug}/{quickId}-SUMMARY.md (required in non-fast mode)
|
|
411
|
+
* - {quickDir}/{quickId}-{slug}/{quickId}-CONTEXT.md (optional)
|
|
412
|
+
* - {quickDir}/{quickId}-{slug}/{quickId}-VERIFICATION.md (optional)
|
|
413
|
+
* - {quickDir}/HISTORY.md (optional)
|
|
414
|
+
* - {statePath} (optional)
|
|
415
|
+
*
|
|
416
|
+
* In --fast mode the task directory does NOT exist; only STATE.md and optional
|
|
417
|
+
* HISTORY.md are committed. Commit message: `docs(quick-${quickId}): track fast task`.
|
|
418
|
+
*
|
|
419
|
+
* Respects config.commit_docs (skip commit if false). Does NOT call cmdCommit
|
|
420
|
+
* (which process.exits via output()); uses execGit directly, mirroring the
|
|
421
|
+
* inline pattern used by cmdPhaseFinalize (phase.cjs ~line 947).
|
|
422
|
+
*
|
|
423
|
+
* @param {string} cwd - Planning root (used for config + git cwd unless options.repoCwd)
|
|
424
|
+
* @param {string} quickId - Quick task id (e.g., '260405-u6b')
|
|
425
|
+
* @param {object} options - { description, quickDir, statePath, push, repoCwd, fast }
|
|
426
|
+
* @param {boolean} raw - true to emit raw JSON, false for pretty output
|
|
427
|
+
*/
|
|
428
|
+
function cmdQuickFinalize(cwd, quickId, options, raw) {
|
|
429
|
+
options = options || {};
|
|
430
|
+
|
|
431
|
+
if (!quickId) {
|
|
432
|
+
error('quick_id required for quick finalize');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!options.quickDir) {
|
|
436
|
+
error('--quick-dir required');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (!options.fast && !options.description) {
|
|
440
|
+
error('--description required (unless --fast)');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const result = {
|
|
445
|
+
committed: false,
|
|
446
|
+
hash: null,
|
|
447
|
+
files_committed: [],
|
|
448
|
+
commit_reason: 'unknown',
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const gitCwd = options.repoCwd || cwd;
|
|
452
|
+
// Resolve to real path so path.relative works when gitCwd and file paths
|
|
453
|
+
// cross symlink boundaries (e.g., /tmp vs /private/tmp on macOS).
|
|
454
|
+
let gitCwdReal = gitCwd;
|
|
455
|
+
try { gitCwdReal = fs.realpathSync(gitCwd); } catch { /* fallback to gitCwd */ }
|
|
456
|
+
const toRel = (absPath) => {
|
|
457
|
+
let p = absPath;
|
|
458
|
+
try { p = fs.realpathSync(absPath); } catch { /* use original */ }
|
|
459
|
+
return path.relative(gitCwdReal, p);
|
|
460
|
+
};
|
|
461
|
+
const filesToStage = [];
|
|
462
|
+
|
|
463
|
+
// In non-fast mode, locate the task directory and enumerate artifacts
|
|
464
|
+
if (!options.fast) {
|
|
465
|
+
if (!fs.existsSync(options.quickDir)) {
|
|
466
|
+
error('task directory not found for quick_id: ' + quickId + ' (quick-dir missing: ' + options.quickDir + ')');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
let taskDirName = null;
|
|
470
|
+
try {
|
|
471
|
+
const entries = fs.readdirSync(options.quickDir);
|
|
472
|
+
const prefix = quickId + '-';
|
|
473
|
+
for (const e of entries) {
|
|
474
|
+
if (e === prefix.replace(/-$/, '')) { continue; }
|
|
475
|
+
if (e.indexOf(prefix) === 0) {
|
|
476
|
+
const full = path.join(options.quickDir, e);
|
|
477
|
+
try {
|
|
478
|
+
if (fs.statSync(full).isDirectory()) {
|
|
479
|
+
taskDirName = e;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
} catch { /* ignore */ }
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} catch { /* ignore */ }
|
|
486
|
+
|
|
487
|
+
if (!taskDirName) {
|
|
488
|
+
error('task directory not found for quick_id: ' + quickId);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const taskDir = path.join(options.quickDir, taskDirName);
|
|
493
|
+
const candidates = [
|
|
494
|
+
path.join(taskDir, quickId + '-PLAN.md'),
|
|
495
|
+
path.join(taskDir, quickId + '-SUMMARY.md'),
|
|
496
|
+
path.join(taskDir, quickId + '-CONTEXT.md'),
|
|
497
|
+
path.join(taskDir, quickId + '-VERIFICATION.md'),
|
|
498
|
+
];
|
|
499
|
+
for (const abs of candidates) {
|
|
500
|
+
if (fs.existsSync(abs)) {
|
|
501
|
+
filesToStage.push(toRel(abs));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// STATE.md (optional — may not exist in some flows)
|
|
507
|
+
if (options.statePath && fs.existsSync(options.statePath)) {
|
|
508
|
+
filesToStage.push(toRel(options.statePath));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// HISTORY.md at quickDir root (optional — created by archival)
|
|
512
|
+
const historyPath = path.join(options.quickDir, 'HISTORY.md');
|
|
513
|
+
if (fs.existsSync(historyPath)) {
|
|
514
|
+
filesToStage.push(toRel(historyPath));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Honor config.commit_docs — file writes already happened, just skip commit.
|
|
518
|
+
const config = loadConfig(cwd);
|
|
519
|
+
if (!config.commit_docs) {
|
|
520
|
+
result.committed = false;
|
|
521
|
+
result.commit_reason = 'skipped_commit_docs_false';
|
|
522
|
+
result.files_committed = [];
|
|
523
|
+
output(result, raw);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Stage + commit atomically (inline execGit — cmdCommit calls output/exit).
|
|
528
|
+
for (const f of filesToStage) {
|
|
529
|
+
execGit(gitCwdReal, ['add', f]);
|
|
530
|
+
}
|
|
531
|
+
const message = options.fast
|
|
532
|
+
? 'docs(quick-' + quickId + '): track fast task'
|
|
533
|
+
: 'docs(quick-' + quickId + '): ' + options.description;
|
|
534
|
+
const commitResult = execGit(gitCwdReal, ['commit', '-m', message]);
|
|
535
|
+
if (commitResult.exitCode !== 0) {
|
|
536
|
+
const nothing =
|
|
537
|
+
(commitResult.stdout || '').includes('nothing to commit') ||
|
|
538
|
+
(commitResult.stderr || '').includes('nothing to commit') ||
|
|
539
|
+
(commitResult.stdout || '').includes('no changes added') ||
|
|
540
|
+
(commitResult.stderr || '').includes('no changes added');
|
|
541
|
+
result.committed = false;
|
|
542
|
+
result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
|
|
543
|
+
if (!nothing) result.commit_error = commitResult.stderr;
|
|
544
|
+
result.files_committed = [];
|
|
545
|
+
output(result, raw);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const hashResult = execGit(gitCwdReal, ['rev-parse', '--short', 'HEAD']);
|
|
549
|
+
result.committed = true;
|
|
550
|
+
result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
551
|
+
result.commit_reason = 'committed';
|
|
552
|
+
result.files_committed = filesToStage;
|
|
553
|
+
|
|
554
|
+
// Optional push (same semantics as cmdPhaseFinalize)
|
|
555
|
+
if (options.push) {
|
|
556
|
+
const syncPush = config.sync_push || 'off';
|
|
557
|
+
if (syncPush === 'auto') {
|
|
558
|
+
try {
|
|
559
|
+
const { pushAll } = require('./sync.cjs');
|
|
560
|
+
const pushRes = pushAll(gitCwdReal, { force: true });
|
|
561
|
+
result.pushed = pushRes.ok;
|
|
562
|
+
result.push_result = pushRes;
|
|
563
|
+
} catch (err) {
|
|
564
|
+
result.pushed = false;
|
|
565
|
+
result.push_result = { ok: false, error: err.message };
|
|
566
|
+
}
|
|
567
|
+
} else if (syncPush === 'prompt') {
|
|
568
|
+
result.needs_push = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
output(result, raw);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
module.exports = {
|
|
576
|
+
detectQuickMode,
|
|
577
|
+
getActiveQuick,
|
|
578
|
+
startProductQuick,
|
|
579
|
+
quickComplete,
|
|
580
|
+
quickAbandon,
|
|
581
|
+
cmdQuickComplete,
|
|
582
|
+
cmdQuickAbandon,
|
|
583
|
+
cmdQuickFinalize,
|
|
584
|
+
};
|