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