@ktpartners/dgs-platform 2.8.0 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +41 -13
- package/agents/dgs-plan-checker.md +29 -3
- package/agents/dgs-planner.md +10 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +2 -2
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +6 -6
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +2 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +1 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
- package/deliver-great-systems/bin/lib/commands.cjs +316 -31
- package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
- package/deliver-great-systems/bin/lib/config.cjs +39 -6
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +28 -11
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +306 -39
- package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
- package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +54 -29
- package/deliver-great-systems/bin/lib/phase.cjs +128 -2
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/projects.cjs +28 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
- package/deliver-great-systems/bin/lib/quick.cjs +584 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
- package/deliver-great-systems/bin/lib/repos.cjs +25 -1
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +142 -54
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +80 -1
- package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
- package/deliver-great-systems/templates/claude-md.md +16 -0
- package/deliver-great-systems/workflows/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/complete-milestone.md +197 -22
- package/deliver-great-systems/workflows/complete-quick.md +68 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/execute-phase.md +121 -32
- package/deliver-great-systems/workflows/execute-plan.md +12 -21
- package/deliver-great-systems/workflows/help.md +33 -29
- package/deliver-great-systems/workflows/init-product.md +2 -18
- package/deliver-great-systems/workflows/new-milestone.md +40 -24
- package/deliver-great-systems/workflows/new-project.md +22 -680
- package/deliver-great-systems/workflows/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +68 -0
- package/deliver-great-systems/workflows/quick.md +152 -23
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +8 -8
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +8 -8
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for quick.cjs -- Quick workflow lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Uses real git repos in temp directories, following the same pattern as worktrees.test.cjs.
|
|
5
|
+
* Functions that call output()/process.exit() are tested via subprocess (dgs-tools.cjs CLI).
|
|
6
|
+
* Pure functions (detectQuickMode, getActiveQuick, etc.) are tested directly with
|
|
7
|
+
* config.local.json manipulation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
const { resetPaths, initPaths } = require('./paths.cjs');
|
|
18
|
+
|
|
19
|
+
const DGS_TOOLS = path.resolve(__dirname, '..', 'dgs-tools.cjs');
|
|
20
|
+
|
|
21
|
+
// ─── Test Helpers ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const GIT_ENV = {
|
|
24
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
25
|
+
GIT_AUTHOR_EMAIL: 'test@test.com',
|
|
26
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
27
|
+
GIT_COMMITTER_EMAIL: 'test@test.com',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a minimal DGS environment for quick workflow tests.
|
|
32
|
+
*/
|
|
33
|
+
function createTestEnv() {
|
|
34
|
+
const tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(require('os').tmpdir(), 'dgs-quick-')));
|
|
35
|
+
const planDir = path.join(tmpDir, 'planning');
|
|
36
|
+
const codeDir = path.join(tmpDir, 'code-repo');
|
|
37
|
+
|
|
38
|
+
// Create planning root (git repo)
|
|
39
|
+
fs.mkdirSync(planDir, { recursive: true });
|
|
40
|
+
execSync('git init -b main', { cwd: planDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
41
|
+
execSync('git config user.email "test@test.com"', { cwd: planDir, stdio: 'pipe' });
|
|
42
|
+
execSync('git config user.name "Test"', { cwd: planDir, stdio: 'pipe' });
|
|
43
|
+
|
|
44
|
+
// Create code repo (git repo on main branch)
|
|
45
|
+
fs.mkdirSync(codeDir, { recursive: true });
|
|
46
|
+
execSync('git init -b main', { cwd: codeDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
47
|
+
execSync('git config user.email "test@test.com"', { cwd: codeDir, stdio: 'pipe' });
|
|
48
|
+
execSync('git config user.name "Test"', { cwd: codeDir, stdio: 'pipe' });
|
|
49
|
+
fs.writeFileSync(path.join(codeDir, '.gitkeep'), '');
|
|
50
|
+
execSync('git add .', { cwd: codeDir, stdio: 'pipe' });
|
|
51
|
+
execSync('git commit -m "initial"', { cwd: codeDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
52
|
+
|
|
53
|
+
// DGS config files
|
|
54
|
+
fs.writeFileSync(path.join(planDir, 'config.json'), JSON.stringify({
|
|
55
|
+
git: { base_branch: 'main' },
|
|
56
|
+
}, null, 2));
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(path.join(planDir, 'config.local.json'), JSON.stringify({
|
|
59
|
+
current_project: 'tp',
|
|
60
|
+
}, null, 2));
|
|
61
|
+
|
|
62
|
+
// v2 markers
|
|
63
|
+
fs.writeFileSync(path.join(planDir, 'PROJECTS.md'), '# Projects\n');
|
|
64
|
+
fs.writeFileSync(path.join(planDir, 'REPOS.md'),
|
|
65
|
+
'# Repos\n\n' +
|
|
66
|
+
'| Name | Path | GitHub URL | Description |\n' +
|
|
67
|
+
'|------|------|------------|-------------|\n' +
|
|
68
|
+
'| code-repo | ' + path.relative(planDir, codeDir) + ' | | Test repo |\n'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Project structure
|
|
72
|
+
fs.mkdirSync(path.join(planDir, 'projects', 'tp'), { recursive: true });
|
|
73
|
+
fs.writeFileSync(path.join(planDir, 'projects', 'tp', 'STATE.md'), '# State\nStatus: planning\n');
|
|
74
|
+
|
|
75
|
+
// Commit planning files
|
|
76
|
+
execSync('git add .', { cwd: planDir, stdio: 'pipe' });
|
|
77
|
+
execSync('git commit -m "setup"', { cwd: planDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
78
|
+
|
|
79
|
+
initPaths(planDir);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
tmpDir,
|
|
83
|
+
planDir,
|
|
84
|
+
codeDir,
|
|
85
|
+
cleanup: function() {
|
|
86
|
+
resetPaths();
|
|
87
|
+
// Clean up sibling worktree directories
|
|
88
|
+
try {
|
|
89
|
+
const parent = path.dirname(codeDir);
|
|
90
|
+
const entries = fs.readdirSync(parent);
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
if (e.startsWith('code-repo--')) {
|
|
93
|
+
fs.rmSync(path.join(parent, e), { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read config.local.json from planning dir.
|
|
104
|
+
*/
|
|
105
|
+
function readLocalConfig(planDir) {
|
|
106
|
+
return JSON.parse(fs.readFileSync(path.join(planDir, 'config.local.json'), 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write config.local.json to planning dir.
|
|
111
|
+
*/
|
|
112
|
+
function writeLocalConfig(planDir, data) {
|
|
113
|
+
fs.writeFileSync(path.join(planDir, 'config.local.json'), JSON.stringify(data, null, 2) + '\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Run dgs-tools command and return parsed JSON output.
|
|
118
|
+
*/
|
|
119
|
+
function runCmd(cwd, args) {
|
|
120
|
+
const result = execSync(
|
|
121
|
+
'node ' + JSON.stringify(DGS_TOOLS) + ' ' + args,
|
|
122
|
+
{ cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', env: { ...process.env, ...GIT_ENV } }
|
|
123
|
+
);
|
|
124
|
+
return JSON.parse(result.trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── detectQuickMode ─────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe('detectQuickMode', () => {
|
|
130
|
+
let env;
|
|
131
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
132
|
+
afterEach(() => { env.cleanup(); });
|
|
133
|
+
|
|
134
|
+
it('returns product mode when no active_context set', () => {
|
|
135
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
136
|
+
const result = detectQuickMode(env.planDir, false);
|
|
137
|
+
assert.equal(result.mode, 'product');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns product mode when forceMain is true regardless of active milestone', () => {
|
|
141
|
+
// Set up an active milestone worktree entry
|
|
142
|
+
const config = readLocalConfig(env.planDir);
|
|
143
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: {} } } } };
|
|
144
|
+
config.execution = { active_context: 'v1-0' };
|
|
145
|
+
writeLocalConfig(env.planDir, config);
|
|
146
|
+
|
|
147
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
148
|
+
const result = detectQuickMode(env.planDir, true);
|
|
149
|
+
assert.equal(result.mode, 'product');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('returns milestone-context when active_context points to milestone worktree', () => {
|
|
153
|
+
const config = readLocalConfig(env.planDir);
|
|
154
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: {} } } } };
|
|
155
|
+
config.execution = { active_context: 'v1-0' };
|
|
156
|
+
writeLocalConfig(env.planDir, config);
|
|
157
|
+
|
|
158
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
159
|
+
const result = detectQuickMode(env.planDir, false);
|
|
160
|
+
assert.equal(result.mode, 'milestone-context');
|
|
161
|
+
assert.equal(result.activeSlug, 'v1-0');
|
|
162
|
+
assert.equal(result.activeMilestone, 'v1-0');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns product mode when active_context points to a quick worktree', () => {
|
|
166
|
+
// Create a real quick worktree directory so it's not auto-cleared
|
|
167
|
+
const wtPath = path.join(env.tmpDir, 'code-repo--tp-fix-bug');
|
|
168
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
169
|
+
|
|
170
|
+
const config = readLocalConfig(env.planDir);
|
|
171
|
+
config.projects = { tp: { worktrees: { 'fix-bug': { type: 'quick', repos: { 'code-repo': wtPath } } } } };
|
|
172
|
+
config.execution = { active_context: 'fix-bug' };
|
|
173
|
+
writeLocalConfig(env.planDir, config);
|
|
174
|
+
|
|
175
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
176
|
+
const result = detectQuickMode(env.planDir, false);
|
|
177
|
+
assert.equal(result.mode, 'product');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── getActiveQuick ──────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
describe('getActiveQuick', () => {
|
|
184
|
+
let env;
|
|
185
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
186
|
+
afterEach(() => { env.cleanup(); });
|
|
187
|
+
|
|
188
|
+
it('returns null when no worktrees tracked', () => {
|
|
189
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
190
|
+
const result = getActiveQuick(env.planDir);
|
|
191
|
+
assert.equal(result, null);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns null when only milestone worktrees exist', () => {
|
|
195
|
+
const config = readLocalConfig(env.planDir);
|
|
196
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: {} } } } };
|
|
197
|
+
writeLocalConfig(env.planDir, config);
|
|
198
|
+
|
|
199
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
200
|
+
const result = getActiveQuick(env.planDir);
|
|
201
|
+
assert.equal(result, null);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns the active quick entry when directory exists', () => {
|
|
205
|
+
// Create a real directory for the quick worktree
|
|
206
|
+
const wtPath = path.join(env.tmpDir, 'code-repo--tp-fix-bug');
|
|
207
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
208
|
+
|
|
209
|
+
const config = readLocalConfig(env.planDir);
|
|
210
|
+
config.projects = { tp: { worktrees: { 'fix-bug': { type: 'quick', repos: { 'code-repo': wtPath } } } } };
|
|
211
|
+
writeLocalConfig(env.planDir, config);
|
|
212
|
+
|
|
213
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
214
|
+
const result = getActiveQuick(env.planDir);
|
|
215
|
+
assert.ok(result);
|
|
216
|
+
assert.equal(result.slug, 'fix-bug');
|
|
217
|
+
assert.equal(result.entry.type, 'quick');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('auto-clears stale entries where directory is missing', () => {
|
|
221
|
+
const config = readLocalConfig(env.planDir);
|
|
222
|
+
config.projects = { tp: { worktrees: { 'stale-fix': { type: 'quick', repos: { 'code-repo': '/nonexistent/path' } } } } };
|
|
223
|
+
config.execution = { active_context: 'stale-fix' };
|
|
224
|
+
writeLocalConfig(env.planDir, config);
|
|
225
|
+
|
|
226
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
227
|
+
const result = getActiveQuick(env.planDir);
|
|
228
|
+
assert.equal(result, null);
|
|
229
|
+
|
|
230
|
+
// Verify the stale entry was removed from config
|
|
231
|
+
const updatedConfig = readLocalConfig(env.planDir);
|
|
232
|
+
const worktrees = updatedConfig.projects && updatedConfig.projects.tp
|
|
233
|
+
&& updatedConfig.projects.tp.worktrees;
|
|
234
|
+
assert.ok(!worktrees || !worktrees['stale-fix'], 'Stale entry should be removed');
|
|
235
|
+
|
|
236
|
+
// Verify active_context was cleared
|
|
237
|
+
assert.ok(!updatedConfig.execution || !updatedConfig.execution.active_context,
|
|
238
|
+
'active_context should be cleared for stale quick');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ─── startProductQuick ───────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
describe('startProductQuick', () => {
|
|
245
|
+
let env;
|
|
246
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
247
|
+
afterEach(() => { env.cleanup(); });
|
|
248
|
+
|
|
249
|
+
it('returns guard error when active product-level quick exists', () => {
|
|
250
|
+
// Create a real directory for the quick worktree
|
|
251
|
+
const wtPath = path.join(env.tmpDir, 'code-repo--tp-existing');
|
|
252
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
253
|
+
|
|
254
|
+
const config = readLocalConfig(env.planDir);
|
|
255
|
+
config.projects = { tp: { worktrees: { 'existing': { type: 'quick', repos: { 'code-repo': wtPath } } } } };
|
|
256
|
+
writeLocalConfig(env.planDir, config);
|
|
257
|
+
|
|
258
|
+
const { startProductQuick } = require('./quick.cjs');
|
|
259
|
+
const result = startProductQuick(env.planDir, 'new task', null);
|
|
260
|
+
assert.equal(result.success, false);
|
|
261
|
+
assert.ok(result.error.includes('Quick worktree already active'));
|
|
262
|
+
assert.equal(result.activeSlug, 'existing');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('creates quick worktree when no active quick exists', () => {
|
|
266
|
+
const { startProductQuick } = require('./quick.cjs');
|
|
267
|
+
const result = startProductQuick(env.planDir, 'fix token bug', null);
|
|
268
|
+
assert.equal(result.success, true);
|
|
269
|
+
assert.equal(result.slug, 'fix-token-bug');
|
|
270
|
+
|
|
271
|
+
// Verify active_context was set
|
|
272
|
+
const config = readLocalConfig(env.planDir);
|
|
273
|
+
assert.equal(config.execution.active_context, 'fix-token-bug');
|
|
274
|
+
|
|
275
|
+
// Verify worktree entry exists
|
|
276
|
+
const entry = config.projects.tp.worktrees['fix-token-bug'];
|
|
277
|
+
assert.ok(entry, 'Worktree entry should exist');
|
|
278
|
+
assert.equal(entry.type, 'quick');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── quickComplete ───────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe('quickComplete', () => {
|
|
285
|
+
let env;
|
|
286
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
287
|
+
afterEach(() => { env.cleanup(); });
|
|
288
|
+
|
|
289
|
+
it('returns error when no active quick', () => {
|
|
290
|
+
const { quickComplete } = require('./quick.cjs');
|
|
291
|
+
const result = quickComplete(env.planDir);
|
|
292
|
+
assert.equal(result.success, false);
|
|
293
|
+
assert.ok(result.error.includes('No active product-level quick'));
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ─── quickAbandon ────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
describe('quickAbandon', () => {
|
|
300
|
+
let env;
|
|
301
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
302
|
+
afterEach(() => { env.cleanup(); });
|
|
303
|
+
|
|
304
|
+
it('returns error when confirmed is false', () => {
|
|
305
|
+
const { quickAbandon } = require('./quick.cjs');
|
|
306
|
+
const result = quickAbandon(env.planDir, false);
|
|
307
|
+
assert.equal(result.success, false);
|
|
308
|
+
assert.ok(result.error.includes('Abandon not confirmed'));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('returns error when no active quick', () => {
|
|
312
|
+
const { quickAbandon } = require('./quick.cjs');
|
|
313
|
+
const result = quickAbandon(env.planDir, true);
|
|
314
|
+
assert.equal(result.success, false);
|
|
315
|
+
assert.ok(result.error.includes('No active product-level quick'));
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── CLI routing ─────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe('CLI complete-quick routing', () => {
|
|
322
|
+
let env;
|
|
323
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
324
|
+
afterEach(() => { env.cleanup(); });
|
|
325
|
+
|
|
326
|
+
it('abandon-quick --confirmed returns error when no active quick', () => {
|
|
327
|
+
try {
|
|
328
|
+
runCmd(env.planDir, 'abandon-quick --confirmed');
|
|
329
|
+
assert.fail('Should have thrown');
|
|
330
|
+
} catch (err) {
|
|
331
|
+
assert.ok(err.stderr.includes('No active product-level quick'));
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ─── cmdQuickFinalize ────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create a minimal git repo with DGS config + quick task directory structure.
|
|
340
|
+
* Returns {repoDir, quickDir, taskDir, statePath} with task artifacts NOT yet written.
|
|
341
|
+
*/
|
|
342
|
+
function createFinalizeEnv(opts) {
|
|
343
|
+
opts = opts || {};
|
|
344
|
+
const commitDocs = opts.commitDocs !== false;
|
|
345
|
+
const tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(require('os').tmpdir(), 'dgs-qf-')));
|
|
346
|
+
const repoDir = tmpDir; // single-repo layout — planning root == repo
|
|
347
|
+
const quickDir = path.join(repoDir, 'quick');
|
|
348
|
+
const quickId = opts.quickId || '260405-abc';
|
|
349
|
+
const taskDir = path.join(quickDir, quickId + '-test-task');
|
|
350
|
+
const statePath = path.join(repoDir, 'projects', 'tp', 'STATE.md');
|
|
351
|
+
|
|
352
|
+
execSync('git init -b main', { cwd: repoDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
353
|
+
execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
|
|
354
|
+
execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'pipe' });
|
|
355
|
+
|
|
356
|
+
fs.mkdirSync(quickDir, { recursive: true });
|
|
357
|
+
if (!opts.skipTaskDir) {
|
|
358
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
359
|
+
}
|
|
360
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
361
|
+
|
|
362
|
+
// DGS config
|
|
363
|
+
fs.writeFileSync(
|
|
364
|
+
path.join(repoDir, 'config.json'),
|
|
365
|
+
JSON.stringify({ commit_docs: commitDocs }, null, 2)
|
|
366
|
+
);
|
|
367
|
+
// Pre-seed config.local.json with migration marker so migrateBranchingConfig
|
|
368
|
+
// doesn't write an untracked file during dispatcher startup (which would
|
|
369
|
+
// otherwise interfere with nothing-to-commit detection).
|
|
370
|
+
fs.writeFileSync(
|
|
371
|
+
path.join(repoDir, 'config.local.json'),
|
|
372
|
+
JSON.stringify({ branching_migration_done: true }, null, 2)
|
|
373
|
+
);
|
|
374
|
+
// Create an initial commit so HEAD exists
|
|
375
|
+
fs.writeFileSync(path.join(repoDir, '.gitkeep'), '');
|
|
376
|
+
execSync('git add .gitkeep config.json config.local.json', { cwd: repoDir, stdio: 'pipe' });
|
|
377
|
+
execSync('git commit -m "initial"', { cwd: repoDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
tmpDir, repoDir, quickDir, taskDir, statePath, quickId,
|
|
381
|
+
cleanup: function() { fs.rmSync(tmpDir, { recursive: true, force: true }); },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Run `dgs-tools quick finalize` as a subprocess and return { stdout, stderr, exitCode, parsed }.
|
|
387
|
+
* Does NOT throw on non-zero exit.
|
|
388
|
+
*/
|
|
389
|
+
function runFinalize(cwd, argsArr) {
|
|
390
|
+
const { spawnSync } = require('child_process');
|
|
391
|
+
const res = spawnSync(
|
|
392
|
+
'node',
|
|
393
|
+
[DGS_TOOLS, 'quick', 'finalize', ...argsArr],
|
|
394
|
+
{ cwd, encoding: 'utf-8', env: { ...process.env, ...GIT_ENV } }
|
|
395
|
+
);
|
|
396
|
+
let parsed = null;
|
|
397
|
+
try { parsed = JSON.parse((res.stdout || '').trim()); } catch { /* not JSON */ }
|
|
398
|
+
return { stdout: res.stdout || '', stderr: res.stderr || '', exitCode: res.status, parsed };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Count commits on HEAD of a repo.
|
|
403
|
+
*/
|
|
404
|
+
function countCommits(repoDir) {
|
|
405
|
+
const out = execSync('git rev-list --count HEAD', { cwd: repoDir, encoding: 'utf-8' }).trim();
|
|
406
|
+
return parseInt(out, 10) || 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
describe('cmdQuickFinalize', () => {
|
|
410
|
+
let env;
|
|
411
|
+
afterEach(() => { if (env) env.cleanup(); env = null; });
|
|
412
|
+
|
|
413
|
+
it('commits ALL artifacts when PLAN + SUMMARY + CONTEXT + VERIFICATION + STATE + HISTORY all exist', () => {
|
|
414
|
+
env = createFinalizeEnv();
|
|
415
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
416
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
417
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-CONTEXT.md'), '# context\n');
|
|
418
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-VERIFICATION.md'), '# verification\n');
|
|
419
|
+
fs.writeFileSync(path.join(env.quickDir, 'HISTORY.md'), '# history\n');
|
|
420
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
421
|
+
|
|
422
|
+
const res = runFinalize(env.repoDir, [
|
|
423
|
+
env.quickId,
|
|
424
|
+
'--quick-dir', env.quickDir,
|
|
425
|
+
'--state-path', env.statePath,
|
|
426
|
+
'--description', 'test all artifacts',
|
|
427
|
+
]);
|
|
428
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
429
|
+
assert.ok(res.parsed, 'expected parsed JSON, got: ' + res.stdout);
|
|
430
|
+
assert.equal(res.parsed.committed, true);
|
|
431
|
+
assert.equal(res.parsed.commit_reason, 'committed');
|
|
432
|
+
assert.equal(res.parsed.files_committed.length, 6, 'files: ' + JSON.stringify(res.parsed.files_committed));
|
|
433
|
+
|
|
434
|
+
// Verify commit message
|
|
435
|
+
const msg = execSync('git log -1 --format=%s', { cwd: env.repoDir, encoding: 'utf-8' }).trim();
|
|
436
|
+
assert.equal(msg, 'docs(quick-' + env.quickId + '): test all artifacts');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('commits minimal artifacts when only PLAN + SUMMARY + STATE exist', () => {
|
|
440
|
+
env = createFinalizeEnv();
|
|
441
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
442
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
443
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
444
|
+
// No CONTEXT, VERIFICATION, or HISTORY
|
|
445
|
+
|
|
446
|
+
const res = runFinalize(env.repoDir, [
|
|
447
|
+
env.quickId,
|
|
448
|
+
'--quick-dir', env.quickDir,
|
|
449
|
+
'--state-path', env.statePath,
|
|
450
|
+
'--description', 'minimal artifacts',
|
|
451
|
+
]);
|
|
452
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
453
|
+
assert.ok(res.parsed);
|
|
454
|
+
assert.equal(res.parsed.committed, true);
|
|
455
|
+
assert.equal(res.parsed.commit_reason, 'committed');
|
|
456
|
+
assert.equal(res.parsed.files_committed.length, 3);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('fast mode: commit message is `docs(quick-<id>): track fast task`', () => {
|
|
460
|
+
env = createFinalizeEnv({ skipTaskDir: true });
|
|
461
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
462
|
+
fs.writeFileSync(path.join(env.quickDir, 'HISTORY.md'), '# history\n');
|
|
463
|
+
|
|
464
|
+
const res = runFinalize(env.repoDir, [
|
|
465
|
+
env.quickId,
|
|
466
|
+
'--quick-dir', env.quickDir,
|
|
467
|
+
'--state-path', env.statePath,
|
|
468
|
+
'--fast',
|
|
469
|
+
]);
|
|
470
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
471
|
+
assert.ok(res.parsed);
|
|
472
|
+
assert.equal(res.parsed.committed, true);
|
|
473
|
+
// files_committed contains STATE + HISTORY only (no PLAN/SUMMARY in fast)
|
|
474
|
+
assert.equal(res.parsed.files_committed.length, 2);
|
|
475
|
+
|
|
476
|
+
const msg = execSync('git log -1 --format=%s', { cwd: env.repoDir, encoding: 'utf-8' }).trim();
|
|
477
|
+
assert.equal(msg, 'docs(quick-' + env.quickId + '): track fast task');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('fast mode with HISTORY.md: HISTORY.md is in files_committed', () => {
|
|
481
|
+
env = createFinalizeEnv({ skipTaskDir: true });
|
|
482
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
483
|
+
fs.writeFileSync(path.join(env.quickDir, 'HISTORY.md'), '# history row\n');
|
|
484
|
+
|
|
485
|
+
const res = runFinalize(env.repoDir, [
|
|
486
|
+
env.quickId,
|
|
487
|
+
'--quick-dir', env.quickDir,
|
|
488
|
+
'--state-path', env.statePath,
|
|
489
|
+
'--fast',
|
|
490
|
+
]);
|
|
491
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
492
|
+
assert.ok(res.parsed);
|
|
493
|
+
assert.equal(res.parsed.committed, true);
|
|
494
|
+
const hasHistory = res.parsed.files_committed.some(f => f.endsWith('HISTORY.md'));
|
|
495
|
+
assert.ok(hasHistory, 'HISTORY.md should be in files_committed: ' + JSON.stringify(res.parsed.files_committed));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('invalid quick_id (no matching task dir) exits with clear error', () => {
|
|
499
|
+
env = createFinalizeEnv({ skipTaskDir: true });
|
|
500
|
+
// task dir NOT created — non-fast mode should error
|
|
501
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
502
|
+
|
|
503
|
+
const res = runFinalize(env.repoDir, [
|
|
504
|
+
'NONEXISTENT',
|
|
505
|
+
'--quick-dir', env.quickDir,
|
|
506
|
+
'--state-path', env.statePath,
|
|
507
|
+
'--description', 'some desc',
|
|
508
|
+
]);
|
|
509
|
+
assert.notEqual(res.exitCode, 0);
|
|
510
|
+
assert.ok(
|
|
511
|
+
res.stderr.includes('task directory not found'),
|
|
512
|
+
'expected "task directory not found" in stderr, got: ' + res.stderr
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('missing --description in non-fast mode exits with clear error', () => {
|
|
517
|
+
env = createFinalizeEnv();
|
|
518
|
+
|
|
519
|
+
const res = runFinalize(env.repoDir, [
|
|
520
|
+
env.quickId,
|
|
521
|
+
'--quick-dir', env.quickDir,
|
|
522
|
+
'--state-path', env.statePath,
|
|
523
|
+
]);
|
|
524
|
+
assert.notEqual(res.exitCode, 0);
|
|
525
|
+
assert.ok(
|
|
526
|
+
res.stderr.includes('description required') || res.stderr.includes('--description'),
|
|
527
|
+
'expected description error in stderr, got: ' + res.stderr
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('missing --quick-dir in non-fast mode exits with clear error', () => {
|
|
532
|
+
env = createFinalizeEnv();
|
|
533
|
+
|
|
534
|
+
const res = runFinalize(env.repoDir, [
|
|
535
|
+
env.quickId,
|
|
536
|
+
'--state-path', env.statePath,
|
|
537
|
+
'--description', 'desc here',
|
|
538
|
+
]);
|
|
539
|
+
assert.notEqual(res.exitCode, 0);
|
|
540
|
+
assert.ok(
|
|
541
|
+
res.stderr.includes('quick-dir required') || res.stderr.includes('--quick-dir'),
|
|
542
|
+
'expected quick-dir error in stderr, got: ' + res.stderr
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('config.commit_docs=false skips commit (no git commit created)', () => {
|
|
547
|
+
env = createFinalizeEnv({ commitDocs: false });
|
|
548
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
549
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
550
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
551
|
+
|
|
552
|
+
const commitsBefore = countCommits(env.repoDir);
|
|
553
|
+
const res = runFinalize(env.repoDir, [
|
|
554
|
+
env.quickId,
|
|
555
|
+
'--quick-dir', env.quickDir,
|
|
556
|
+
'--state-path', env.statePath,
|
|
557
|
+
'--description', 'skip mode',
|
|
558
|
+
]);
|
|
559
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
560
|
+
assert.ok(res.parsed);
|
|
561
|
+
assert.equal(res.parsed.committed, false);
|
|
562
|
+
assert.equal(res.parsed.commit_reason, 'skipped_commit_docs_false');
|
|
563
|
+
assert.deepEqual(res.parsed.files_committed, []);
|
|
564
|
+
const commitsAfter = countCommits(env.repoDir);
|
|
565
|
+
assert.equal(commitsAfter, commitsBefore, 'no new commit should have been created');
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('nothing-to-commit: returns committed=false, commit_reason=nothing_to_commit', () => {
|
|
569
|
+
env = createFinalizeEnv();
|
|
570
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
571
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
572
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
573
|
+
|
|
574
|
+
// First call: succeeds and commits
|
|
575
|
+
const first = runFinalize(env.repoDir, [
|
|
576
|
+
env.quickId,
|
|
577
|
+
'--quick-dir', env.quickDir,
|
|
578
|
+
'--state-path', env.statePath,
|
|
579
|
+
'--description', 'first call',
|
|
580
|
+
]);
|
|
581
|
+
assert.equal(first.exitCode, 0, 'first call stderr: ' + first.stderr);
|
|
582
|
+
assert.equal(first.parsed.committed, true);
|
|
583
|
+
|
|
584
|
+
// Second call: same files, nothing new staged
|
|
585
|
+
const second = runFinalize(env.repoDir, [
|
|
586
|
+
env.quickId,
|
|
587
|
+
'--quick-dir', env.quickDir,
|
|
588
|
+
'--state-path', env.statePath,
|
|
589
|
+
'--description', 'second call',
|
|
590
|
+
]);
|
|
591
|
+
assert.equal(second.exitCode, 0, 'second call stderr: ' + second.stderr);
|
|
592
|
+
assert.ok(second.parsed);
|
|
593
|
+
assert.equal(second.parsed.committed, false);
|
|
594
|
+
assert.equal(second.parsed.commit_reason, 'nothing_to_commit');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
@@ -63,6 +63,7 @@ function parseReposMd(cwd) {
|
|
|
63
63
|
path: cells[2] || '',
|
|
64
64
|
url: cells[3] || '',
|
|
65
65
|
description: cells[4] || '',
|
|
66
|
+
setup: (cells[5] || '').trim(),
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -1106,6 +1107,7 @@ function cmdReposInitProduct(cwd, options, raw) {
|
|
|
1106
1107
|
}
|
|
1107
1108
|
fs.mkdirSync(path.join(getPlanningRoot(cwd), 'specs'), { recursive: true });
|
|
1108
1109
|
fs.mkdirSync(path.join(getPlanningRoot(cwd), 'docs', 'product'), { recursive: true });
|
|
1110
|
+
fs.mkdirSync(path.join(getPlanningRoot(cwd), 'quick'), { recursive: true });
|
|
1109
1111
|
error('Product already initialized. Use /dgs:progress to see status.');
|
|
1110
1112
|
}
|
|
1111
1113
|
|
|
@@ -1119,6 +1121,7 @@ function cmdReposInitProduct(cwd, options, raw) {
|
|
|
1119
1121
|
}
|
|
1120
1122
|
fs.mkdirSync(path.join(getPlanningRoot(cwd), 'specs'), { recursive: true });
|
|
1121
1123
|
fs.mkdirSync(path.join(getPlanningRoot(cwd), 'docs', 'product'), { recursive: true });
|
|
1124
|
+
fs.mkdirSync(path.join(getPlanningRoot(cwd), 'quick'), { recursive: true });
|
|
1122
1125
|
|
|
1123
1126
|
// Add .gitkeep files to empty directories so they survive git commit
|
|
1124
1127
|
const gitkeepPaths = [
|
|
@@ -1127,6 +1130,7 @@ function cmdReposInitProduct(cwd, options, raw) {
|
|
|
1127
1130
|
path.join(getPlanningRoot(cwd), 'ideas', 'done', '.gitkeep'),
|
|
1128
1131
|
path.join(getPlanningRoot(cwd), 'specs', '.gitkeep'),
|
|
1129
1132
|
path.join(getPlanningRoot(cwd), 'docs', 'product', '.gitkeep'),
|
|
1133
|
+
path.join(getPlanningRoot(cwd), 'quick', '.gitkeep'),
|
|
1130
1134
|
];
|
|
1131
1135
|
for (const gk of gitkeepPaths) {
|
|
1132
1136
|
if (!fs.existsSync(gk)) {
|
|
@@ -1150,6 +1154,25 @@ function cmdReposInitProduct(cwd, options, raw) {
|
|
|
1150
1154
|
const siblingRepos = discoverSiblingRepos(cwd);
|
|
1151
1155
|
const allRepos = [...discoveredRepos, ...siblingRepos];
|
|
1152
1156
|
|
|
1157
|
+
// Bootstrap empty code repos so git worktree creation works later.
|
|
1158
|
+
// git worktree add requires at least one commit (a HEAD to branch from).
|
|
1159
|
+
const bootstrappedRepos = [];
|
|
1160
|
+
for (const repo of allRepos) {
|
|
1161
|
+
const repoPath = path.resolve(cwd, repo.path);
|
|
1162
|
+
if (!fs.existsSync(path.join(repoPath, '.git'))) continue;
|
|
1163
|
+
const logResult = execGit(repoPath, ['rev-parse', 'HEAD']);
|
|
1164
|
+
if (logResult.exitCode !== 0) {
|
|
1165
|
+
// Empty repo — no commits yet
|
|
1166
|
+
const readmePath = path.join(repoPath, 'README.md');
|
|
1167
|
+
if (!fs.existsSync(readmePath)) {
|
|
1168
|
+
fs.writeFileSync(readmePath, `# ${repo.name}\n`, 'utf-8');
|
|
1169
|
+
}
|
|
1170
|
+
execGit(repoPath, ['add', 'README.md']);
|
|
1171
|
+
execGit(repoPath, ['commit', '-m', 'chore: initial commit']);
|
|
1172
|
+
bootstrappedRepos.push(repo.name);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1153
1176
|
// Derive product name
|
|
1154
1177
|
const productName = options.productName || path.basename(cwd);
|
|
1155
1178
|
|
|
@@ -1178,7 +1201,8 @@ function cmdReposInitProduct(cwd, options, raw) {
|
|
|
1178
1201
|
ideas_dirs_created: true,
|
|
1179
1202
|
specs_dir_created: true,
|
|
1180
1203
|
docs_dir_created: true,
|
|
1181
|
-
|
|
1204
|
+
bootstrapped_repos: bootstrappedRepos,
|
|
1205
|
+
files_created: ['config.json', 'config.local.json', 'REPOS.md', 'PROJECTS.md', 'ideas/', 'specs/', 'docs/', 'quick/', '.gitignore', 'review-keys.json'],
|
|
1182
1206
|
}, raw);
|
|
1183
1207
|
}
|
|
1184
1208
|
|