@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +41 -13
  3. package/agents/dgs-plan-checker.md +29 -3
  4. package/agents/dgs-planner.md +10 -0
  5. package/commands/dgs/abandon-quick.md +28 -0
  6. package/commands/dgs/add-tests.md +2 -2
  7. package/commands/dgs/audit-milestone.md +2 -2
  8. package/commands/dgs/capture-principle.md +11 -11
  9. package/commands/dgs/cleanup.md +2 -2
  10. package/commands/dgs/complete-milestone.md +11 -11
  11. package/commands/dgs/complete-quick.md +28 -0
  12. package/commands/dgs/create-milestone-job.md +2 -2
  13. package/commands/dgs/debug.md +3 -3
  14. package/commands/dgs/develop-idea.md +1 -1
  15. package/commands/dgs/fast.md +3 -1
  16. package/commands/dgs/health.md +1 -1
  17. package/commands/dgs/map-codebase.md +6 -6
  18. package/commands/dgs/new-milestone.md +5 -5
  19. package/commands/dgs/new-project.md +6 -6
  20. package/commands/dgs/plan-milestone-gaps.md +1 -1
  21. package/commands/dgs/progress.md +3 -3
  22. package/commands/dgs/quick-abandon.md +8 -0
  23. package/commands/dgs/quick-complete.md +8 -0
  24. package/commands/dgs/quick.md +10 -3
  25. package/commands/dgs/research-idea.md +2 -2
  26. package/commands/dgs/research-phase.md +3 -3
  27. package/commands/dgs/switch-project.md +1 -1
  28. package/commands/dgs/write-spec.md +3 -3
  29. package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
  30. package/deliver-great-systems/bin/lib/commands.cjs +316 -31
  31. package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
  32. package/deliver-great-systems/bin/lib/config.cjs +39 -6
  33. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  34. package/deliver-great-systems/bin/lib/core.cjs +28 -11
  35. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  36. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  37. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  38. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  39. package/deliver-great-systems/bin/lib/init.cjs +306 -39
  40. package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
  41. package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
  42. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  43. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  44. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  45. package/deliver-great-systems/bin/lib/milestone.cjs +54 -29
  46. package/deliver-great-systems/bin/lib/phase.cjs +128 -2
  47. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  48. package/deliver-great-systems/bin/lib/projects.cjs +28 -8
  49. package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
  50. package/deliver-great-systems/bin/lib/quick.cjs +584 -0
  51. package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
  52. package/deliver-great-systems/bin/lib/repos.cjs +25 -1
  53. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  54. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  55. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  56. package/deliver-great-systems/bin/lib/state.cjs +142 -54
  57. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  58. package/deliver-great-systems/bin/lib/verify.cjs +80 -1
  59. package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
  60. package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
  61. package/deliver-great-systems/templates/claude-md.md +16 -0
  62. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  63. package/deliver-great-systems/workflows/add-idea.md +3 -3
  64. package/deliver-great-systems/workflows/add-tests.md +14 -0
  65. package/deliver-great-systems/workflows/add-todo.md +1 -0
  66. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  67. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  68. package/deliver-great-systems/workflows/cancel-job.md +1 -1
  69. package/deliver-great-systems/workflows/check-todos.md +2 -3
  70. package/deliver-great-systems/workflows/complete-milestone.md +197 -22
  71. package/deliver-great-systems/workflows/complete-quick.md +68 -0
  72. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  73. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  74. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  75. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  76. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  77. package/deliver-great-systems/workflows/execute-phase.md +121 -32
  78. package/deliver-great-systems/workflows/execute-plan.md +12 -21
  79. package/deliver-great-systems/workflows/help.md +33 -29
  80. package/deliver-great-systems/workflows/init-product.md +2 -18
  81. package/deliver-great-systems/workflows/new-milestone.md +40 -24
  82. package/deliver-great-systems/workflows/new-project.md +22 -680
  83. package/deliver-great-systems/workflows/progress-all.md +133 -0
  84. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  85. package/deliver-great-systems/workflows/quick-complete.md +68 -0
  86. package/deliver-great-systems/workflows/quick.md +152 -23
  87. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  88. package/deliver-great-systems/workflows/research-idea.md +8 -8
  89. package/deliver-great-systems/workflows/resume-project.md +2 -2
  90. package/deliver-great-systems/workflows/run-job.md +8 -8
  91. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  92. package/deliver-great-systems/workflows/verify-work.md +14 -0
  93. package/deliver-great-systems/workflows/write-spec.md +2 -2
  94. 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
+ };