@ktpartners/dgs-platform 2.9.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -0,0 +1,739 @@
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
+ const { checkFourEyes } = require('./governance.cjs');
23
+ const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
24
+
25
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Read config.local.json safely.
29
+ * @param {string} cwd
30
+ * @returns {object}
31
+ */
32
+ function _readLocalConfig(cwd) {
33
+ const localPath = getLocalConfigPath(cwd);
34
+ try {
35
+ if (fs.existsSync(localPath)) {
36
+ return JSON.parse(fs.readFileSync(localPath, 'utf-8'));
37
+ }
38
+ } catch { /* ignore */ }
39
+ return {};
40
+ }
41
+
42
+ /**
43
+ * Write config.local.json atomically.
44
+ * @param {string} cwd
45
+ * @param {object} data
46
+ */
47
+ function _writeLocalConfig(cwd, data) {
48
+ const localPath = getLocalConfigPath(cwd);
49
+ const dir = path.dirname(localPath);
50
+ if (!fs.existsSync(dir)) {
51
+ fs.mkdirSync(dir, { recursive: true });
52
+ }
53
+ const tmpPath = localPath + '.tmp.' + process.pid;
54
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
55
+ fs.renameSync(tmpPath, localPath);
56
+ }
57
+
58
+ /**
59
+ * Generate a collision-resistant quick task ID: YYMMDD-xxx
60
+ * xxx = 2-second precision blocks since midnight, encoded as 3-char Base36.
61
+ * @param {Date} [date] - Date to use (defaults to now)
62
+ * @returns {string} e.g. '260414-v4y'
63
+ */
64
+ function generateQuickId(date) {
65
+ const now = date || new Date();
66
+ const yy = String(now.getFullYear()).slice(-2);
67
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
68
+ const dd = String(now.getDate()).padStart(2, '0');
69
+ const dateStr = yy + mm + dd;
70
+ const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
71
+ const timeBlocks = Math.floor(secondsSinceMidnight / 2);
72
+ const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
73
+ return dateStr + '-' + timeEncoded;
74
+ }
75
+
76
+ /**
77
+ * Sanitize a title into a valid slug for branch/worktree naming.
78
+ * @param {string} title
79
+ * @returns {string}
80
+ */
81
+ function _sanitizeSlug(title) {
82
+ if (!title) return '';
83
+ return title
84
+ .toLowerCase()
85
+ .replace(/[^a-z0-9]+/g, '-')
86
+ .replace(/^-+|-+$/g, '')
87
+ .slice(0, 40)
88
+ .replace(/-+$/, '');
89
+ }
90
+
91
+ /**
92
+ * Clear a stale quick entry from config.local.json.
93
+ * Called when getActiveQuick detects a quick whose worktree directory is missing.
94
+ * @param {string} cwd
95
+ * @param {string} project
96
+ * @param {string} slug
97
+ * @param {object} localConfig - Already loaded config.local.json object
98
+ */
99
+ function _clearStaleQuick(cwd, project, slug, localConfig) {
100
+ if (localConfig.projects && localConfig.projects[project]
101
+ && localConfig.projects[project].worktrees) {
102
+ delete localConfig.projects[project].worktrees[slug];
103
+ }
104
+ // Clear active_context if it points to this stale quick
105
+ if (localConfig.execution && localConfig.execution.active_context === slug) {
106
+ delete localConfig.execution.active_context;
107
+ }
108
+ _writeLocalConfig(cwd, localConfig);
109
+ process.stderr.write('Warning: Stale quick \'' + slug + '\' auto-cleared (worktree directory missing)\n');
110
+ }
111
+
112
+ // ─── Exported functions ───────────────────────────────────────────────────────
113
+
114
+ // Stale-defence symmetry: this function probes entry.repos paths with
115
+ // fs.existsSync the same way getActiveQuick (below) does for QUICK
116
+ // entries. Don't "simplify" the on-disk check away — its absence is
117
+ // exactly the bug fixed in 260507-pdp. Asymmetry: we do NOT auto-clear
118
+ // stale milestone entries here; milestone state is heavier and may
119
+ // carry inspectable context. The user clears manually via
120
+ // `dgs-tools worktrees remove <slug>`.
121
+ /**
122
+ * Detect whether a quick should be product-level or milestone-context.
123
+ *
124
+ * @param {string} cwd - Planning root
125
+ * @param {boolean} forceMain - True if --main flag was passed
126
+ * @returns {{ mode: 'product'|'milestone-context', activeSlug?: string, activeMilestone?: string }}
127
+ */
128
+ function detectQuickMode(cwd, forceMain) {
129
+ if (forceMain) return { mode: 'product' };
130
+
131
+ const config = loadConfig(cwd);
132
+ const localConfig = _readLocalConfig(cwd);
133
+ const activeContext = localConfig.execution && localConfig.execution.active_context;
134
+
135
+ if (!activeContext) return { mode: 'product' };
136
+
137
+ // Check if active context is a milestone worktree
138
+ const project = config.current_project;
139
+ if (!project) return { mode: 'product' };
140
+
141
+ const worktrees = (localConfig.projects && localConfig.projects[project]
142
+ && localConfig.projects[project].worktrees) || {};
143
+ const entry = worktrees[activeContext];
144
+
145
+ if (entry && entry.type === 'milestone') {
146
+ const repos = entry.repos || {};
147
+ const paths = Object.values(repos);
148
+ const anyExists = paths.length > 0 && paths.some(function(p) { return fs.existsSync(p); });
149
+ if (anyExists) {
150
+ return { mode: 'milestone-context', activeSlug: activeContext, activeMilestone: activeContext };
151
+ }
152
+ // Stale milestone entry — no on-disk worktree. Fall through to product
153
+ // mode so quicks land where the user expects. Don't auto-clear: milestone
154
+ // state is heavier than quick state and may carry context worth
155
+ // inspecting; the user can run `dgs-tools worktrees remove <slug>`.
156
+ }
157
+
158
+ // If active context is a quick or unknown, treat as product-level
159
+ return { mode: 'product' };
160
+ }
161
+
162
+ /**
163
+ * Get the currently active product-level quick, if any.
164
+ *
165
+ * @param {string} cwd - Planning root
166
+ * @returns {{ slug: string, entry: object }|null}
167
+ */
168
+ function getActiveQuick(cwd) {
169
+ const config = loadConfig(cwd);
170
+ const localConfig = _readLocalConfig(cwd);
171
+ const project = config.current_project;
172
+ if (!project) return null;
173
+
174
+ const worktrees = (localConfig.projects && localConfig.projects[project]
175
+ && localConfig.projects[project].worktrees) || {};
176
+
177
+ for (const [slug, entry] of Object.entries(worktrees)) {
178
+ if (entry.type === 'quick') {
179
+ // Verify directory still exists (stale detection)
180
+ const repos = entry.repos || {};
181
+ const paths = Object.values(repos);
182
+ const anyExists = paths.some(function(p) { return fs.existsSync(p); });
183
+ if (anyExists) {
184
+ return { slug: slug, entry: entry };
185
+ }
186
+ // Directory missing -- stale entry, auto-clear
187
+ _clearStaleQuick(cwd, project, slug, localConfig);
188
+ }
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Start a product-level quick: guard check, create worktree, set context.
196
+ *
197
+ * @param {string} cwd - Planning root
198
+ * @param {string} title - Quick task title (used for slug and branch)
199
+ * @param {string|null} mode - 'full', 'debug', or null (plain)
200
+ * @returns {{ success: boolean, slug?: string, error?: string, activeSlug?: string }}
201
+ */
202
+ function startProductQuick(cwd, title, mode) {
203
+ // Guard: one active product-level quick at a time
204
+ const active = getActiveQuick(cwd);
205
+ if (active) {
206
+ return {
207
+ success: false,
208
+ error: 'Quick worktree already active: \'' + active.slug + '\'. Complete it (`dgs:complete-quick`), abandon it (`dgs:abandon-quick`), or use `dgs:fast` for trivial fixes.',
209
+ activeSlug: active.slug,
210
+ };
211
+ }
212
+
213
+ const config = loadConfig(cwd);
214
+ const project = config.current_project;
215
+ if (!project) return { success: false, error: 'No current project set' };
216
+
217
+ // Generate quickId and sanitize title to slug with quickId prefix
218
+ const quickId = generateQuickId();
219
+ const descSlug = _sanitizeSlug(title);
220
+ if (!descSlug) return { success: false, error: 'Cannot create slug from title: ' + title };
221
+ const slug = quickId + '-' + descSlug;
222
+ // Match worktrees.cjs _sanitizeSlug: slice(0, 50) then strip trailing dashes.
223
+ // cmdWorktreesCreate re-sanitises whatever slug we pass, so we must pre-truncate
224
+ // here and use the canonical slug for the execSync arg, the read-back lookup,
225
+ // the active_context write, and the return value. Otherwise long descSlugs
226
+ // (40 chars) push the total past 50 and the read-back below misses, returning
227
+ // repos: {} and breaking the workflow's worktree-context injection (which
228
+ // causes the executor to commit to main of the registered repo instead of
229
+ // the quick/<slug> branch in the worktree). See quick task 260507-kq9.
230
+ const canonicalSlug = slug.slice(0, 50).replace(/-+$/, '');
231
+
232
+ // Create worktree via existing cmdWorktreesCreate
233
+ // Note: cmdWorktreesCreate calls output() which exits the process.
234
+ // We need to handle this differently -- call the underlying logic directly.
235
+ // Since cmdWorktreesCreate calls process.exit via output(), we invoke it
236
+ // through a subprocess to capture the result.
237
+ const { execSync } = require('child_process');
238
+ const dgsTools = path.resolve(__dirname, '..', 'dgs-tools.cjs');
239
+ const root = getPlanningRoot(cwd);
240
+
241
+ try {
242
+ const modeArgs = mode ? ' --mode ' + mode : '';
243
+ const result = execSync(
244
+ 'node ' + JSON.stringify(dgsTools) + ' worktrees create ' + JSON.stringify(canonicalSlug) + ' --type quick' + modeArgs,
245
+ { cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
246
+ );
247
+ // Parse output to verify creation
248
+ const parsed = JSON.parse(result.trim());
249
+ if (!parsed.created) {
250
+ return { success: false, error: 'Worktree creation returned unexpected result' };
251
+ }
252
+ } catch (err) {
253
+ const stderr = (err.stderr || '').toString().trim();
254
+ return { success: false, error: 'Failed to create quick worktree: ' + (stderr || err.message || String(err)) };
255
+ }
256
+
257
+ // Set mode in worktree config entry and set active_context
258
+ const localConfig = _readLocalConfig(cwd);
259
+ if (localConfig.projects && localConfig.projects[project]
260
+ && localConfig.projects[project].worktrees
261
+ && localConfig.projects[project].worktrees[canonicalSlug]) {
262
+ localConfig.projects[project].worktrees[canonicalSlug].mode = mode || null;
263
+ }
264
+
265
+ // Set active_context
266
+ if (!localConfig.execution) localConfig.execution = {};
267
+ localConfig.execution.active_context = canonicalSlug;
268
+
269
+ _writeLocalConfig(cwd, localConfig);
270
+
271
+ const finalConfig = _readLocalConfig(cwd);
272
+ const repos =
273
+ (finalConfig.projects &&
274
+ finalConfig.projects[project] &&
275
+ finalConfig.projects[project].worktrees &&
276
+ finalConfig.projects[project].worktrees[canonicalSlug] &&
277
+ finalConfig.projects[project].worktrees[canonicalSlug].repos) || {};
278
+ return { success: true, slug: canonicalSlug, repos: repos };
279
+ }
280
+
281
+ /**
282
+ * Complete the active product-level quick: rebase, merge, push, cleanup.
283
+ *
284
+ * @param {string} cwd - Planning root
285
+ * @returns {{ success: boolean, commitCount?: number, slug?: string, error?: string, manualInstructions?: string }}
286
+ */
287
+ function quickComplete(cwd, options) {
288
+ options = options || {};
289
+ const active = getActiveQuick(cwd);
290
+ if (!active) {
291
+ return { success: false, error: 'No active product-level quick to complete. If working in a milestone context, changes are part of the milestone.' };
292
+ }
293
+
294
+ const { slug, entry } = active;
295
+ const repos = entry.repos || {};
296
+ const repoNames = Object.keys(repos);
297
+
298
+ if (repoNames.length === 0) {
299
+ return { success: false, error: 'Quick \'' + slug + '\' has no repos tracked' };
300
+ }
301
+
302
+ // Count commits on quick branch vs base_branch for summary
303
+ let totalCommits = 0;
304
+ const config = loadConfig(cwd);
305
+ const baseBranch = config.base_branch || 'main';
306
+
307
+ // ── Four-Eyes Gate (GATE-02) ───────────────────────────────────────────────
308
+ const planRoot = getPlanningRoot(cwd);
309
+ const rawConfig = (() => {
310
+ try {
311
+ return JSON.parse(fs.readFileSync(path.join(planRoot, 'config.json'), 'utf-8'));
312
+ } catch { return {}; }
313
+ })();
314
+ const fourEyesMode = (rawConfig.workflow && rawConfig.workflow.four_eyes) || 'off';
315
+
316
+ if (fourEyesMode !== 'off') {
317
+ // Resolve current user identity
318
+ let currentUserStr = '';
319
+ try {
320
+ const identity = requireGitIdentity(cwd);
321
+ currentUserStr = formatAuthorString(identity);
322
+ } catch {
323
+ currentUserStr = '';
324
+ }
325
+
326
+ // Get contributors from quick task worktree commits
327
+ const quickContributors = [];
328
+ for (const repoName of repoNames) {
329
+ const worktreePath = repos[repoName];
330
+ const branchName = 'quick/' + slug;
331
+ try {
332
+ // Get unique commit authors from the quick branch
333
+ const logResult = execGit(worktreePath, ['log', '--format=%aN <%aE>', baseBranch + '..' + branchName]);
334
+ if (logResult.exitCode === 0 && logResult.stdout.trim()) {
335
+ const authors = logResult.stdout.trim().split('\n');
336
+ for (const author of authors) {
337
+ if (author.trim()) quickContributors.push(author.trim());
338
+ }
339
+ }
340
+ } catch { /* ignore — contributor detection is best-effort */ }
341
+ }
342
+
343
+ // Deduplicate contributors
344
+ const seen = new Map();
345
+ for (const c of quickContributors) {
346
+ const key = c.toLowerCase();
347
+ if (!seen.has(key)) seen.set(key, c);
348
+ }
349
+ const uniqueContribs = [...seen.values()];
350
+
351
+ // Run four-eyes check
352
+ const feResult = checkFourEyes(uniqueContribs, currentUserStr, fourEyesMode);
353
+
354
+ // Display contributor list (SHR-02 — contextual: "this task")
355
+ const contribNames = uniqueContribs.length > 0
356
+ ? uniqueContribs.join(', ')
357
+ : '(none detected)';
358
+
359
+ if (feResult.passed) {
360
+ process.stderr.write('Contributors: ' + contribNames + ' \u2014 \u2714 Four-eyes satisfied\n');
361
+ } else {
362
+ process.stderr.write('Contributors: ' + contribNames + '\n');
363
+
364
+ let displayName = currentUserStr;
365
+ try {
366
+ const identity = requireGitIdentity(cwd);
367
+ displayName = identity.name;
368
+ } catch { /* use full string */ }
369
+
370
+ if (fourEyesMode === 'warn') {
371
+ // Warn: display warning, proceed
372
+ process.stderr.write('\u26A0 You (' + displayName + ') contributed to this task. Completing anyway (warn mode).\n');
373
+ } else if (fourEyesMode === 'enforce') {
374
+ if (options.force) {
375
+ // Force: display override, proceed
376
+ process.stderr.write('\u26A0 Forced: you (' + displayName + ') are the only contributor. Override logged.\n');
377
+ } else {
378
+ // Block: return error
379
+ return {
380
+ success: false,
381
+ error: '\u2718 Blocked: you (' + displayName + ') are the only contributor. Use --force to override.',
382
+ };
383
+ }
384
+ }
385
+ }
386
+ }
387
+ // Off mode: no check, no output (GATE-03)
388
+
389
+ // Rebase and merge each repo
390
+ for (const repoName of repoNames) {
391
+ const worktreePath = repos[repoName];
392
+ const branchName = 'quick/' + slug;
393
+
394
+ // Count commits before merge
395
+ const countResult = execGit(worktreePath, ['rev-list', '--count', baseBranch + '..' + branchName]);
396
+ if (countResult.exitCode === 0) {
397
+ totalCommits += parseInt(countResult.stdout.trim(), 10) || 0;
398
+ }
399
+
400
+ const result = rebaseAndMerge(cwd, repoName, slug, { push: true });
401
+ if (!result.success) {
402
+ return {
403
+ success: false,
404
+ slug: slug,
405
+ error: result.error,
406
+ manualInstructions: result.manualInstructions,
407
+ };
408
+ }
409
+ }
410
+
411
+ // Cleanup: remove worktree and branch via subprocess
412
+ const { execSync } = require('child_process');
413
+ const dgsTools = path.resolve(__dirname, '..', 'dgs-tools.cjs');
414
+ const root = getPlanningRoot(cwd);
415
+
416
+ try {
417
+ execSync(
418
+ 'node ' + JSON.stringify(dgsTools) + ' worktrees remove ' + JSON.stringify(slug),
419
+ { cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
420
+ );
421
+ } catch (err) {
422
+ process.stderr.write('Warning: Worktree cleanup failed: ' + ((err.stderr || '').toString().trim() || err.message || String(err)) + '\n');
423
+ }
424
+
425
+ // Clear active_context and worktree entry (belt-and-suspenders -- remove should handle this)
426
+ const localConfig = _readLocalConfig(cwd);
427
+ if (localConfig.execution && localConfig.execution.active_context === slug) {
428
+ delete localConfig.execution.active_context;
429
+ }
430
+ const project = config.current_project;
431
+ if (project && localConfig.projects && localConfig.projects[project]
432
+ && localConfig.projects[project].worktrees) {
433
+ delete localConfig.projects[project].worktrees[slug];
434
+ }
435
+ _writeLocalConfig(cwd, localConfig);
436
+
437
+ return {
438
+ success: true,
439
+ slug: slug,
440
+ commitCount: totalCommits,
441
+ };
442
+ }
443
+
444
+ /**
445
+ * Abandon the active product-level quick: remove worktree without merging.
446
+ *
447
+ * @param {string} cwd - Planning root
448
+ * @param {boolean} confirmed - Must be true to proceed (workflows handle confirmation UI)
449
+ * @returns {{ success: boolean, slug?: string, error?: string }}
450
+ */
451
+ function quickAbandon(cwd, confirmed) {
452
+ if (!confirmed) {
453
+ return { success: false, error: 'Abandon not confirmed. Pass confirmed=true to proceed.' };
454
+ }
455
+
456
+ const active = getActiveQuick(cwd);
457
+ if (!active) {
458
+ return { success: false, error: 'No active product-level quick to abandon. If working in a milestone context, use complete-milestone instead.' };
459
+ }
460
+
461
+ const { slug } = active;
462
+
463
+ // Remove worktree and branch (without merging) via subprocess
464
+ const { execSync } = require('child_process');
465
+ const dgsTools = path.resolve(__dirname, '..', 'dgs-tools.cjs');
466
+ const root = getPlanningRoot(cwd);
467
+
468
+ try {
469
+ execSync(
470
+ 'node ' + JSON.stringify(dgsTools) + ' worktrees remove ' + JSON.stringify(slug),
471
+ { cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
472
+ );
473
+ } catch (err) {
474
+ process.stderr.write('Warning: Worktree removal failed: ' + ((err.stderr || '').toString().trim() || err.message || String(err)) + '\n');
475
+ }
476
+
477
+ // Clear active_context and worktree entry
478
+ const localConfig = _readLocalConfig(cwd);
479
+ const config = loadConfig(cwd);
480
+ if (localConfig.execution && localConfig.execution.active_context === slug) {
481
+ delete localConfig.execution.active_context;
482
+ }
483
+ const project = config.current_project;
484
+ if (project && localConfig.projects && localConfig.projects[project]
485
+ && localConfig.projects[project].worktrees) {
486
+ delete localConfig.projects[project].worktrees[slug];
487
+ }
488
+ _writeLocalConfig(cwd, localConfig);
489
+
490
+ return { success: true, slug: slug };
491
+ }
492
+
493
+ // ─── CLI command wrappers ─────────────────────────────────────────────────────
494
+
495
+ /**
496
+ * CLI handler for `dgs-tools complete-quick` (also `quick-complete` for backward compat).
497
+ * @param {string} cwd
498
+ */
499
+ function cmdQuickComplete(cwd, args) {
500
+ const force = args && args.includes('--force');
501
+ const result = quickComplete(cwd, { force });
502
+ if (!result.success) {
503
+ if (result.manualInstructions) {
504
+ process.stderr.write(result.manualInstructions + '\n');
505
+ }
506
+ error(result.error);
507
+ return;
508
+ }
509
+ output({
510
+ completed: true,
511
+ slug: result.slug,
512
+ commits: result.commitCount,
513
+ message: 'Quick \'' + result.slug + '\' merged to main (' + result.commitCount + ' commits). Worktree cleaned up. Pushed to origin.',
514
+ });
515
+ }
516
+
517
+ /**
518
+ * CLI handler for `dgs-tools abandon-quick` (also `quick-abandon` for backward compat).
519
+ * @param {string} cwd
520
+ * @param {string[]} args
521
+ */
522
+ function cmdQuickAbandon(cwd, args) {
523
+ const confirmed = args && args.includes('--confirmed');
524
+ const result = quickAbandon(cwd, confirmed);
525
+ if (!result.success) {
526
+ error(result.error);
527
+ return;
528
+ }
529
+ output({
530
+ abandoned: true,
531
+ slug: result.slug,
532
+ message: 'Quick \'' + result.slug + '\' abandoned. Worktree removed.',
533
+ });
534
+ }
535
+
536
+ /**
537
+ * CLI: `dgs-tools quick finalize <quick_id> [flags]`.
538
+ *
539
+ * Stages and commits all quick-task artifacts atomically in a single git commit:
540
+ * - {quickDir}/{quickId}-{slug}/{quickId}-PLAN.md (required in non-fast mode)
541
+ * - {quickDir}/{quickId}-{slug}/{quickId}-SUMMARY.md (required in non-fast mode)
542
+ * - {quickDir}/{quickId}-{slug}/{quickId}-CONTEXT.md (optional)
543
+ * - {quickDir}/{quickId}-{slug}/{quickId}-VERIFICATION.md (optional)
544
+ * - {quickDir}/HISTORY.md (optional)
545
+ * - {statePath} (optional)
546
+ *
547
+ * In --fast mode the task directory does NOT exist; only STATE.md and optional
548
+ * HISTORY.md are committed. Commit message: `docs(quick-${quickId}): track fast task`.
549
+ *
550
+ * Respects config.commit_docs (skip commit if false). Does NOT call cmdCommit
551
+ * (which process.exits via output()); uses execGit directly, mirroring the
552
+ * inline pattern used by cmdPhaseFinalize (phase.cjs ~line 947).
553
+ *
554
+ * @param {string} cwd - Planning root (used for config + git cwd unless options.repoCwd)
555
+ * @param {string} quickId - Quick task id (e.g., '260405-u6b')
556
+ * @param {object} options - { description, quickDir, statePath, push, repoCwd, fast }
557
+ * @param {boolean} raw - true to emit raw JSON, false for pretty output
558
+ */
559
+ // Collect the list of still-dirty paths in `gitCwd` after a commit. Purely
560
+ // informational — populates `result.dirty_after` so callers can detect
561
+ // verify-step side effects that leaked outside the staged file set. Never
562
+ // throws. Duplicated from commands.cjs/phase.cjs instead of extracted because
563
+ // the three call sites use slightly different cwd variables (gitCwd vs
564
+ // gitCwdReal vs cwd) and a shared helper would obscure that.
565
+ function collectDirtyAfter(gitCwd) {
566
+ const porcelain = execGit(gitCwd, ['status', '--porcelain']);
567
+ if (porcelain.exitCode !== 0) return [];
568
+ return (porcelain.stdout || '')
569
+ .split('\n')
570
+ .map(l => l.trim())
571
+ .filter(Boolean)
572
+ .map(l => l.replace(/^..\s+/, ''));
573
+ }
574
+
575
+ function cmdQuickFinalize(cwd, quickId, options, raw) {
576
+ options = options || {};
577
+
578
+ if (!quickId) {
579
+ error('quick_id required for quick finalize');
580
+ return;
581
+ }
582
+ if (!options.quickDir) {
583
+ error('--quick-dir required');
584
+ return;
585
+ }
586
+ if (!options.fast && !options.description) {
587
+ error('--description required (unless --fast)');
588
+ return;
589
+ }
590
+
591
+ const result = {
592
+ committed: false,
593
+ hash: null,
594
+ files_committed: [],
595
+ commit_reason: 'unknown',
596
+ };
597
+
598
+ const gitCwd = options.repoCwd || cwd;
599
+ // Resolve to real path so path.relative works when gitCwd and file paths
600
+ // cross symlink boundaries (e.g., /tmp vs /private/tmp on macOS).
601
+ let gitCwdReal = gitCwd;
602
+ try { gitCwdReal = fs.realpathSync(gitCwd); } catch { /* fallback to gitCwd */ }
603
+ const toRel = (absPath) => {
604
+ let p = absPath;
605
+ try { p = fs.realpathSync(absPath); } catch { /* use original */ }
606
+ return path.relative(gitCwdReal, p);
607
+ };
608
+ const filesToStage = [];
609
+
610
+ // In non-fast mode, locate the task directory and enumerate artifacts
611
+ if (!options.fast) {
612
+ if (!fs.existsSync(options.quickDir)) {
613
+ error('task directory not found for quick_id: ' + quickId + ' (quick-dir missing: ' + options.quickDir + ')');
614
+ return;
615
+ }
616
+ let taskDirName = null;
617
+ try {
618
+ const entries = fs.readdirSync(options.quickDir);
619
+ const prefix = quickId + '-';
620
+ for (const e of entries) {
621
+ if (e === prefix.replace(/-$/, '')) { continue; }
622
+ if (e.indexOf(prefix) === 0) {
623
+ const full = path.join(options.quickDir, e);
624
+ try {
625
+ if (fs.statSync(full).isDirectory()) {
626
+ taskDirName = e;
627
+ break;
628
+ }
629
+ } catch { /* ignore */ }
630
+ }
631
+ }
632
+ } catch { /* ignore */ }
633
+
634
+ if (!taskDirName) {
635
+ error('task directory not found for quick_id: ' + quickId);
636
+ return;
637
+ }
638
+
639
+ const taskDir = path.join(options.quickDir, taskDirName);
640
+ // Sweep all quick-task artifacts in taskDir, accepting both the flat
641
+ // ({quickId}-NAME.md) and numbered ({quickId}-NN-NAME.md) shapes. The
642
+ // numbered shape is what the dgs-planner template renders for quick
643
+ // tasks (see agents/dgs-planner.md). False-positive risk is negligible
644
+ // because quickId is a 6-char base36 timestamp + 3-char suffix.
645
+ const escapedId = quickId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
646
+ const artifactRe = new RegExp(
647
+ '^' + escapedId + '(-\\d+)?-(PLAN|SUMMARY|CONTEXT|VERIFICATION|CODEREVIEW|DEBUG-LOG|RESEARCH)\\.md$'
648
+ );
649
+ let taskEntries = [];
650
+ try { taskEntries = fs.readdirSync(taskDir); } catch { /* taskDir guard above already handled missing dir */ }
651
+ for (const entry of taskEntries) {
652
+ if (artifactRe.test(entry)) {
653
+ filesToStage.push(toRel(path.join(taskDir, entry)));
654
+ }
655
+ }
656
+ }
657
+
658
+ // STATE.md (optional — may not exist in some flows)
659
+ if (options.statePath && fs.existsSync(options.statePath)) {
660
+ filesToStage.push(toRel(options.statePath));
661
+ }
662
+
663
+ // HISTORY.md at quickDir root (optional — created by archival)
664
+ const historyPath = path.join(options.quickDir, 'HISTORY.md');
665
+ if (fs.existsSync(historyPath)) {
666
+ filesToStage.push(toRel(historyPath));
667
+ }
668
+
669
+ // Honor config.commit_docs — file writes already happened, just skip commit.
670
+ const config = loadConfig(cwd);
671
+ if (!config.commit_docs) {
672
+ result.committed = false;
673
+ result.commit_reason = 'skipped_commit_docs_false';
674
+ result.files_committed = [];
675
+ output(result, raw);
676
+ return;
677
+ }
678
+
679
+ // Stage + commit atomically (inline execGit — cmdCommit calls output/exit).
680
+ for (const f of filesToStage) {
681
+ execGit(gitCwdReal, ['add', f]);
682
+ }
683
+ const message = options.fast
684
+ ? 'docs(quick-' + quickId + '): track fast task'
685
+ : 'docs(quick-' + quickId + '): ' + options.description;
686
+ const commitResult = execGit(gitCwdReal, ['commit', '-m', message]);
687
+ if (commitResult.exitCode !== 0) {
688
+ const nothing =
689
+ (commitResult.stdout || '').includes('nothing to commit') ||
690
+ (commitResult.stderr || '').includes('nothing to commit') ||
691
+ (commitResult.stdout || '').includes('no changes added') ||
692
+ (commitResult.stderr || '').includes('no changes added');
693
+ result.committed = false;
694
+ result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
695
+ if (!nothing) result.commit_error = commitResult.stderr;
696
+ result.files_committed = [];
697
+ result.dirty_after = collectDirtyAfter(gitCwdReal);
698
+ output(result, raw);
699
+ return;
700
+ }
701
+ const hashResult = execGit(gitCwdReal, ['rev-parse', '--short', 'HEAD']);
702
+ result.committed = true;
703
+ result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
704
+ result.commit_reason = 'committed';
705
+ result.files_committed = filesToStage;
706
+ result.dirty_after = collectDirtyAfter(gitCwdReal);
707
+
708
+ // Optional push (same semantics as cmdPhaseFinalize)
709
+ if (options.push) {
710
+ const syncPush = config.sync_push || 'off';
711
+ if (syncPush === 'auto') {
712
+ try {
713
+ const { pushAll } = require('./sync.cjs');
714
+ const pushRes = pushAll(gitCwdReal, { force: true });
715
+ result.pushed = pushRes.ok;
716
+ result.push_result = pushRes;
717
+ } catch (err) {
718
+ result.pushed = false;
719
+ result.push_result = { ok: false, error: err.message };
720
+ }
721
+ } else if (syncPush === 'prompt') {
722
+ result.needs_push = true;
723
+ }
724
+ }
725
+
726
+ output(result, raw);
727
+ }
728
+
729
+ module.exports = {
730
+ generateQuickId,
731
+ detectQuickMode,
732
+ getActiveQuick,
733
+ startProductQuick,
734
+ quickComplete,
735
+ quickAbandon,
736
+ cmdQuickComplete,
737
+ cmdQuickAbandon,
738
+ cmdQuickFinalize,
739
+ };