@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,730 @@
|
|
|
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 wtPath = path.join(env.tmpDir, 'code-repo--tp-v1-0');
|
|
154
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const config = readLocalConfig(env.planDir);
|
|
157
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: { 'code-repo': wtPath } } } } };
|
|
158
|
+
config.execution = { active_context: 'v1-0' };
|
|
159
|
+
writeLocalConfig(env.planDir, config);
|
|
160
|
+
|
|
161
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
162
|
+
const result = detectQuickMode(env.planDir, false);
|
|
163
|
+
assert.equal(result.mode, 'milestone-context');
|
|
164
|
+
assert.equal(result.activeSlug, 'v1-0');
|
|
165
|
+
assert.equal(result.activeMilestone, 'v1-0');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns product mode when milestone entry has no repos (stale)', () => {
|
|
169
|
+
const config = readLocalConfig(env.planDir);
|
|
170
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: {} } } } };
|
|
171
|
+
config.execution = { active_context: 'v1-0' };
|
|
172
|
+
writeLocalConfig(env.planDir, config);
|
|
173
|
+
|
|
174
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
175
|
+
const result = detectQuickMode(env.planDir, false);
|
|
176
|
+
assert.equal(result.mode, 'product');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns product mode when milestone entry repos point to non-existent path (stale)', () => {
|
|
180
|
+
// Regression test for 260507-pdp: a stale milestone entry left over from
|
|
181
|
+
// an interrupted /dgs:complete-milestone (or aborted `worktrees remove`)
|
|
182
|
+
// silently re-routed every subsequent /dgs:quick to milestone-context.
|
|
183
|
+
// Pre-fix this returned 'milestone-context' — post-fix it falls through.
|
|
184
|
+
const config = readLocalConfig(env.planDir);
|
|
185
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: { 'code-repo': '/nonexistent/path/v1-0' } } } } };
|
|
186
|
+
config.execution = { active_context: 'v1-0' };
|
|
187
|
+
writeLocalConfig(env.planDir, config);
|
|
188
|
+
|
|
189
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
190
|
+
const result = detectQuickMode(env.planDir, false);
|
|
191
|
+
assert.equal(result.mode, 'product');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does NOT auto-clear stale milestone entries from config', () => {
|
|
195
|
+
// Asymmetric to getActiveQuick by design: milestone state is heavier than
|
|
196
|
+
// quick state and may carry context worth inspecting; user clears manually
|
|
197
|
+
// via `dgs-tools worktrees remove <slug>`.
|
|
198
|
+
const config = readLocalConfig(env.planDir);
|
|
199
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: { 'code-repo': '/nonexistent/path/v1-0' } } } } };
|
|
200
|
+
config.execution = { active_context: 'v1-0' };
|
|
201
|
+
writeLocalConfig(env.planDir, config);
|
|
202
|
+
|
|
203
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
204
|
+
const result = detectQuickMode(env.planDir, false);
|
|
205
|
+
assert.equal(result.mode, 'product');
|
|
206
|
+
|
|
207
|
+
// Re-read config and assert the milestone entry is STILL present
|
|
208
|
+
const updatedConfig = readLocalConfig(env.planDir);
|
|
209
|
+
const entry = updatedConfig.projects && updatedConfig.projects.tp
|
|
210
|
+
&& updatedConfig.projects.tp.worktrees
|
|
211
|
+
&& updatedConfig.projects.tp.worktrees['v1-0'];
|
|
212
|
+
assert.ok(entry, 'Stale milestone entry should NOT be auto-cleared');
|
|
213
|
+
assert.equal(entry.type, 'milestone');
|
|
214
|
+
assert.equal(updatedConfig.execution.active_context, 'v1-0',
|
|
215
|
+
'active_context should also still point to the stale milestone');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns product mode when active_context points to a quick worktree', () => {
|
|
219
|
+
// Create a real quick worktree directory so it's not auto-cleared
|
|
220
|
+
const wtPath = path.join(env.tmpDir, 'code-repo--tp-fix-bug');
|
|
221
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
222
|
+
|
|
223
|
+
const config = readLocalConfig(env.planDir);
|
|
224
|
+
config.projects = { tp: { worktrees: { 'fix-bug': { type: 'quick', repos: { 'code-repo': wtPath } } } } };
|
|
225
|
+
config.execution = { active_context: 'fix-bug' };
|
|
226
|
+
writeLocalConfig(env.planDir, config);
|
|
227
|
+
|
|
228
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
229
|
+
const result = detectQuickMode(env.planDir, false);
|
|
230
|
+
assert.equal(result.mode, 'product');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ─── getActiveQuick ──────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe('getActiveQuick', () => {
|
|
237
|
+
let env;
|
|
238
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
239
|
+
afterEach(() => { env.cleanup(); });
|
|
240
|
+
|
|
241
|
+
it('returns null when no worktrees tracked', () => {
|
|
242
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
243
|
+
const result = getActiveQuick(env.planDir);
|
|
244
|
+
assert.equal(result, null);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('returns null when only milestone worktrees exist', () => {
|
|
248
|
+
const config = readLocalConfig(env.planDir);
|
|
249
|
+
config.projects = { tp: { worktrees: { 'v1-0': { type: 'milestone', repos: {} } } } };
|
|
250
|
+
writeLocalConfig(env.planDir, config);
|
|
251
|
+
|
|
252
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
253
|
+
const result = getActiveQuick(env.planDir);
|
|
254
|
+
assert.equal(result, null);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('returns the active quick entry when directory exists', () => {
|
|
258
|
+
// Create a real directory for the quick worktree
|
|
259
|
+
const wtPath = path.join(env.tmpDir, 'code-repo--tp-fix-bug');
|
|
260
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
261
|
+
|
|
262
|
+
const config = readLocalConfig(env.planDir);
|
|
263
|
+
config.projects = { tp: { worktrees: { 'fix-bug': { type: 'quick', repos: { 'code-repo': wtPath } } } } };
|
|
264
|
+
writeLocalConfig(env.planDir, config);
|
|
265
|
+
|
|
266
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
267
|
+
const result = getActiveQuick(env.planDir);
|
|
268
|
+
assert.ok(result);
|
|
269
|
+
assert.equal(result.slug, 'fix-bug');
|
|
270
|
+
assert.equal(result.entry.type, 'quick');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('auto-clears stale entries where directory is missing', () => {
|
|
274
|
+
const config = readLocalConfig(env.planDir);
|
|
275
|
+
config.projects = { tp: { worktrees: { 'stale-fix': { type: 'quick', repos: { 'code-repo': '/nonexistent/path' } } } } };
|
|
276
|
+
config.execution = { active_context: 'stale-fix' };
|
|
277
|
+
writeLocalConfig(env.planDir, config);
|
|
278
|
+
|
|
279
|
+
const { getActiveQuick } = require('./quick.cjs');
|
|
280
|
+
const result = getActiveQuick(env.planDir);
|
|
281
|
+
assert.equal(result, null);
|
|
282
|
+
|
|
283
|
+
// Verify the stale entry was removed from config
|
|
284
|
+
const updatedConfig = readLocalConfig(env.planDir);
|
|
285
|
+
const worktrees = updatedConfig.projects && updatedConfig.projects.tp
|
|
286
|
+
&& updatedConfig.projects.tp.worktrees;
|
|
287
|
+
assert.ok(!worktrees || !worktrees['stale-fix'], 'Stale entry should be removed');
|
|
288
|
+
|
|
289
|
+
// Verify active_context was cleared
|
|
290
|
+
assert.ok(!updatedConfig.execution || !updatedConfig.execution.active_context,
|
|
291
|
+
'active_context should be cleared for stale quick');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ─── startProductQuick ───────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
describe('startProductQuick', () => {
|
|
298
|
+
let env;
|
|
299
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
300
|
+
afterEach(() => { env.cleanup(); });
|
|
301
|
+
|
|
302
|
+
it('returns guard error when active product-level quick exists', () => {
|
|
303
|
+
// Create a real directory for the quick worktree
|
|
304
|
+
const wtPath = path.join(env.tmpDir, 'code-repo--tp-existing');
|
|
305
|
+
fs.mkdirSync(wtPath, { recursive: true });
|
|
306
|
+
|
|
307
|
+
const config = readLocalConfig(env.planDir);
|
|
308
|
+
config.projects = { tp: { worktrees: { 'existing': { type: 'quick', repos: { 'code-repo': wtPath } } } } };
|
|
309
|
+
writeLocalConfig(env.planDir, config);
|
|
310
|
+
|
|
311
|
+
const { startProductQuick } = require('./quick.cjs');
|
|
312
|
+
const result = startProductQuick(env.planDir, 'new task', null);
|
|
313
|
+
assert.equal(result.success, false);
|
|
314
|
+
assert.ok(result.error.includes('Quick worktree already active'));
|
|
315
|
+
assert.equal(result.activeSlug, 'existing');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('creates quick worktree when no active quick exists', () => {
|
|
319
|
+
const { startProductQuick } = require('./quick.cjs');
|
|
320
|
+
const result = startProductQuick(env.planDir, 'fix token bug', null);
|
|
321
|
+
assert.equal(result.success, true);
|
|
322
|
+
// Slug now includes quickId prefix: YYMMDD-xxx-fix-token-bug
|
|
323
|
+
assert.ok(/^\d{6}-[a-z0-9]{3}-fix-token-bug$/.test(result.slug),
|
|
324
|
+
'Slug should have quickId prefix: ' + result.slug);
|
|
325
|
+
|
|
326
|
+
// Verify active_context was set
|
|
327
|
+
const config = readLocalConfig(env.planDir);
|
|
328
|
+
assert.equal(config.execution.active_context, result.slug);
|
|
329
|
+
|
|
330
|
+
// Verify worktree entry exists
|
|
331
|
+
const entry = config.projects.tp.worktrees[result.slug];
|
|
332
|
+
assert.ok(entry, 'Worktree entry should exist');
|
|
333
|
+
assert.equal(entry.type, 'quick');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('returns canonical slug that matches worktrees[] lookup key for long descriptions (regression: slug truncation)', () => {
|
|
337
|
+
// Regression for 260507-kq9: a title whose descSlug fills the 40-char cap
|
|
338
|
+
// produces a 51-char raw slug (10-char quickId + '-' + 40-char descSlug).
|
|
339
|
+
// cmdWorktreesCreate re-sanitises with a 50-char cap, so the worktrees[]
|
|
340
|
+
// entry is keyed under the 50-char canonical slug. Pre-fix, startProductQuick
|
|
341
|
+
// read back at the 51-char key → undefined → returned `repos: {}`.
|
|
342
|
+
// The workflow then injected no <worktree_context> and the executor
|
|
343
|
+
// committed to main of the registered repo instead of the quick branch.
|
|
344
|
+
const { startProductQuick } = require('./quick.cjs');
|
|
345
|
+
const longTitle = 'fix slug truncation mismatch causing empty repos and main commits';
|
|
346
|
+
const result = startProductQuick(env.planDir, longTitle, null);
|
|
347
|
+
|
|
348
|
+
assert.equal(result.success, true,
|
|
349
|
+
'startProductQuick should succeed; got error: ' + (result.error || '<none>'));
|
|
350
|
+
assert.ok(result.slug.length <= 50,
|
|
351
|
+
'Returned slug must respect canonical 50-char cap; got ' + result.slug.length + ' chars: ' + result.slug);
|
|
352
|
+
assert.ok(Object.keys(result.repos).length > 0,
|
|
353
|
+
'Returned repos must be populated (regression: was {} when slug exceeded 50 chars). repos: ' + JSON.stringify(result.repos));
|
|
354
|
+
|
|
355
|
+
// The slug-as-returned must equal the slug-as-keyed in config.local.json.
|
|
356
|
+
const config = readLocalConfig(env.planDir);
|
|
357
|
+
assert.equal(config.execution.active_context, result.slug,
|
|
358
|
+
'active_context should equal returned slug; active_context=' + config.execution.active_context + ' slug=' + result.slug);
|
|
359
|
+
const entry = config.projects.tp.worktrees[result.slug];
|
|
360
|
+
assert.ok(entry,
|
|
361
|
+
'worktrees[result.slug] must exist; available keys: ' +
|
|
362
|
+
JSON.stringify(Object.keys(config.projects.tp.worktrees || {})));
|
|
363
|
+
assert.equal(entry.type, 'quick');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ─── quickComplete ───────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
describe('quickComplete', () => {
|
|
370
|
+
let env;
|
|
371
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
372
|
+
afterEach(() => { env.cleanup(); });
|
|
373
|
+
|
|
374
|
+
it('returns error when no active quick', () => {
|
|
375
|
+
const { quickComplete } = require('./quick.cjs');
|
|
376
|
+
const result = quickComplete(env.planDir);
|
|
377
|
+
assert.equal(result.success, false);
|
|
378
|
+
assert.ok(result.error.includes('No active product-level quick'));
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ─── quickAbandon ────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
describe('quickAbandon', () => {
|
|
385
|
+
let env;
|
|
386
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
387
|
+
afterEach(() => { env.cleanup(); });
|
|
388
|
+
|
|
389
|
+
it('returns error when confirmed is false', () => {
|
|
390
|
+
const { quickAbandon } = require('./quick.cjs');
|
|
391
|
+
const result = quickAbandon(env.planDir, false);
|
|
392
|
+
assert.equal(result.success, false);
|
|
393
|
+
assert.ok(result.error.includes('Abandon not confirmed'));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('returns error when no active quick', () => {
|
|
397
|
+
const { quickAbandon } = require('./quick.cjs');
|
|
398
|
+
const result = quickAbandon(env.planDir, true);
|
|
399
|
+
assert.equal(result.success, false);
|
|
400
|
+
assert.ok(result.error.includes('No active product-level quick'));
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// ─── CLI routing ─────────────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
describe('CLI complete-quick routing', () => {
|
|
407
|
+
let env;
|
|
408
|
+
beforeEach(() => { env = createTestEnv(); });
|
|
409
|
+
afterEach(() => { env.cleanup(); });
|
|
410
|
+
|
|
411
|
+
it('abandon-quick --confirmed returns error when no active quick', () => {
|
|
412
|
+
try {
|
|
413
|
+
runCmd(env.planDir, 'abandon-quick --confirmed');
|
|
414
|
+
assert.fail('Should have thrown');
|
|
415
|
+
} catch (err) {
|
|
416
|
+
assert.ok(err.stderr.includes('No active product-level quick'));
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ─── cmdQuickFinalize ────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Create a minimal git repo with DGS config + quick task directory structure.
|
|
425
|
+
* Returns {repoDir, quickDir, taskDir, statePath} with task artifacts NOT yet written.
|
|
426
|
+
*/
|
|
427
|
+
function createFinalizeEnv(opts) {
|
|
428
|
+
opts = opts || {};
|
|
429
|
+
const commitDocs = opts.commitDocs !== false;
|
|
430
|
+
const tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(require('os').tmpdir(), 'dgs-qf-')));
|
|
431
|
+
const repoDir = tmpDir; // single-repo layout — planning root == repo
|
|
432
|
+
const quickDir = path.join(repoDir, 'quick');
|
|
433
|
+
const quickId = opts.quickId || '260405-abc';
|
|
434
|
+
const taskDir = path.join(quickDir, quickId + '-test-task');
|
|
435
|
+
const statePath = path.join(repoDir, 'projects', 'tp', 'STATE.md');
|
|
436
|
+
|
|
437
|
+
execSync('git init -b main', { cwd: repoDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
438
|
+
execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
|
|
439
|
+
execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'pipe' });
|
|
440
|
+
|
|
441
|
+
fs.mkdirSync(quickDir, { recursive: true });
|
|
442
|
+
if (!opts.skipTaskDir) {
|
|
443
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
444
|
+
}
|
|
445
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
446
|
+
|
|
447
|
+
// DGS config
|
|
448
|
+
fs.writeFileSync(
|
|
449
|
+
path.join(repoDir, 'config.json'),
|
|
450
|
+
JSON.stringify({ commit_docs: commitDocs }, null, 2)
|
|
451
|
+
);
|
|
452
|
+
// Pre-seed config.local.json with migration marker so migrateBranchingConfig
|
|
453
|
+
// doesn't write an untracked file during dispatcher startup (which would
|
|
454
|
+
// otherwise interfere with nothing-to-commit detection).
|
|
455
|
+
fs.writeFileSync(
|
|
456
|
+
path.join(repoDir, 'config.local.json'),
|
|
457
|
+
JSON.stringify({ branching_migration_done: true }, null, 2)
|
|
458
|
+
);
|
|
459
|
+
// Create an initial commit so HEAD exists
|
|
460
|
+
fs.writeFileSync(path.join(repoDir, '.gitkeep'), '');
|
|
461
|
+
execSync('git add .gitkeep config.json config.local.json', { cwd: repoDir, stdio: 'pipe' });
|
|
462
|
+
execSync('git commit -m "initial"', { cwd: repoDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
tmpDir, repoDir, quickDir, taskDir, statePath, quickId,
|
|
466
|
+
cleanup: function() { fs.rmSync(tmpDir, { recursive: true, force: true }); },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Run `dgs-tools quick finalize` as a subprocess and return { stdout, stderr, exitCode, parsed }.
|
|
472
|
+
* Does NOT throw on non-zero exit.
|
|
473
|
+
*/
|
|
474
|
+
function runFinalize(cwd, argsArr) {
|
|
475
|
+
const { spawnSync } = require('child_process');
|
|
476
|
+
const res = spawnSync(
|
|
477
|
+
'node',
|
|
478
|
+
[DGS_TOOLS, 'quick', 'finalize', ...argsArr],
|
|
479
|
+
{ cwd, encoding: 'utf-8', env: { ...process.env, ...GIT_ENV } }
|
|
480
|
+
);
|
|
481
|
+
let parsed = null;
|
|
482
|
+
try { parsed = JSON.parse((res.stdout || '').trim()); } catch { /* not JSON */ }
|
|
483
|
+
return { stdout: res.stdout || '', stderr: res.stderr || '', exitCode: res.status, parsed };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Count commits on HEAD of a repo.
|
|
488
|
+
*/
|
|
489
|
+
function countCommits(repoDir) {
|
|
490
|
+
const out = execSync('git rev-list --count HEAD', { cwd: repoDir, encoding: 'utf-8' }).trim();
|
|
491
|
+
return parseInt(out, 10) || 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
describe('cmdQuickFinalize', () => {
|
|
495
|
+
let env;
|
|
496
|
+
afterEach(() => { if (env) env.cleanup(); env = null; });
|
|
497
|
+
|
|
498
|
+
it('commits ALL artifacts when PLAN + SUMMARY + CONTEXT + VERIFICATION + STATE + HISTORY all exist', () => {
|
|
499
|
+
env = createFinalizeEnv();
|
|
500
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
501
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
502
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-CONTEXT.md'), '# context\n');
|
|
503
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-VERIFICATION.md'), '# verification\n');
|
|
504
|
+
fs.writeFileSync(path.join(env.quickDir, 'HISTORY.md'), '# history\n');
|
|
505
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
506
|
+
|
|
507
|
+
const res = runFinalize(env.repoDir, [
|
|
508
|
+
env.quickId,
|
|
509
|
+
'--quick-dir', env.quickDir,
|
|
510
|
+
'--state-path', env.statePath,
|
|
511
|
+
'--description', 'test all artifacts',
|
|
512
|
+
]);
|
|
513
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
514
|
+
assert.ok(res.parsed, 'expected parsed JSON, got: ' + res.stdout);
|
|
515
|
+
assert.equal(res.parsed.committed, true);
|
|
516
|
+
assert.equal(res.parsed.commit_reason, 'committed');
|
|
517
|
+
assert.equal(res.parsed.files_committed.length, 6, 'files: ' + JSON.stringify(res.parsed.files_committed));
|
|
518
|
+
|
|
519
|
+
// Verify commit message
|
|
520
|
+
const msg = execSync('git log -1 --format=%s', { cwd: env.repoDir, encoding: 'utf-8' }).trim();
|
|
521
|
+
assert.equal(msg, 'docs(quick-' + env.quickId + '): test all artifacts');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('commits minimal artifacts when only PLAN + SUMMARY + STATE exist', () => {
|
|
525
|
+
env = createFinalizeEnv();
|
|
526
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
527
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
528
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
529
|
+
// No CONTEXT, VERIFICATION, or HISTORY
|
|
530
|
+
|
|
531
|
+
const res = runFinalize(env.repoDir, [
|
|
532
|
+
env.quickId,
|
|
533
|
+
'--quick-dir', env.quickDir,
|
|
534
|
+
'--state-path', env.statePath,
|
|
535
|
+
'--description', 'minimal artifacts',
|
|
536
|
+
]);
|
|
537
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
538
|
+
assert.ok(res.parsed);
|
|
539
|
+
assert.equal(res.parsed.committed, true);
|
|
540
|
+
assert.equal(res.parsed.commit_reason, 'committed');
|
|
541
|
+
assert.equal(res.parsed.files_committed.length, 3);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('fast mode: commit message is `docs(quick-<id>): track fast task`', () => {
|
|
545
|
+
env = createFinalizeEnv({ skipTaskDir: true });
|
|
546
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
547
|
+
fs.writeFileSync(path.join(env.quickDir, 'HISTORY.md'), '# history\n');
|
|
548
|
+
|
|
549
|
+
const res = runFinalize(env.repoDir, [
|
|
550
|
+
env.quickId,
|
|
551
|
+
'--quick-dir', env.quickDir,
|
|
552
|
+
'--state-path', env.statePath,
|
|
553
|
+
'--fast',
|
|
554
|
+
]);
|
|
555
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
556
|
+
assert.ok(res.parsed);
|
|
557
|
+
assert.equal(res.parsed.committed, true);
|
|
558
|
+
// files_committed contains STATE + HISTORY only (no PLAN/SUMMARY in fast)
|
|
559
|
+
assert.equal(res.parsed.files_committed.length, 2);
|
|
560
|
+
|
|
561
|
+
const msg = execSync('git log -1 --format=%s', { cwd: env.repoDir, encoding: 'utf-8' }).trim();
|
|
562
|
+
assert.equal(msg, 'docs(quick-' + env.quickId + '): track fast task');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('fast mode with HISTORY.md: HISTORY.md is in files_committed', () => {
|
|
566
|
+
env = createFinalizeEnv({ skipTaskDir: true });
|
|
567
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
568
|
+
fs.writeFileSync(path.join(env.quickDir, 'HISTORY.md'), '# history row\n');
|
|
569
|
+
|
|
570
|
+
const res = runFinalize(env.repoDir, [
|
|
571
|
+
env.quickId,
|
|
572
|
+
'--quick-dir', env.quickDir,
|
|
573
|
+
'--state-path', env.statePath,
|
|
574
|
+
'--fast',
|
|
575
|
+
]);
|
|
576
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
577
|
+
assert.ok(res.parsed);
|
|
578
|
+
assert.equal(res.parsed.committed, true);
|
|
579
|
+
const hasHistory = res.parsed.files_committed.some(f => f.endsWith('HISTORY.md'));
|
|
580
|
+
assert.ok(hasHistory, 'HISTORY.md should be in files_committed: ' + JSON.stringify(res.parsed.files_committed));
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('invalid quick_id (no matching task dir) exits with clear error', () => {
|
|
584
|
+
env = createFinalizeEnv({ skipTaskDir: true });
|
|
585
|
+
// task dir NOT created — non-fast mode should error
|
|
586
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
587
|
+
|
|
588
|
+
const res = runFinalize(env.repoDir, [
|
|
589
|
+
'NONEXISTENT',
|
|
590
|
+
'--quick-dir', env.quickDir,
|
|
591
|
+
'--state-path', env.statePath,
|
|
592
|
+
'--description', 'some desc',
|
|
593
|
+
]);
|
|
594
|
+
assert.notEqual(res.exitCode, 0);
|
|
595
|
+
assert.ok(
|
|
596
|
+
res.stderr.includes('task directory not found'),
|
|
597
|
+
'expected "task directory not found" in stderr, got: ' + res.stderr
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('missing --description in non-fast mode exits with clear error', () => {
|
|
602
|
+
env = createFinalizeEnv();
|
|
603
|
+
|
|
604
|
+
const res = runFinalize(env.repoDir, [
|
|
605
|
+
env.quickId,
|
|
606
|
+
'--quick-dir', env.quickDir,
|
|
607
|
+
'--state-path', env.statePath,
|
|
608
|
+
]);
|
|
609
|
+
assert.notEqual(res.exitCode, 0);
|
|
610
|
+
assert.ok(
|
|
611
|
+
res.stderr.includes('description required') || res.stderr.includes('--description'),
|
|
612
|
+
'expected description error in stderr, got: ' + res.stderr
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('missing --quick-dir in non-fast mode exits with clear error', () => {
|
|
617
|
+
env = createFinalizeEnv();
|
|
618
|
+
|
|
619
|
+
const res = runFinalize(env.repoDir, [
|
|
620
|
+
env.quickId,
|
|
621
|
+
'--state-path', env.statePath,
|
|
622
|
+
'--description', 'desc here',
|
|
623
|
+
]);
|
|
624
|
+
assert.notEqual(res.exitCode, 0);
|
|
625
|
+
assert.ok(
|
|
626
|
+
res.stderr.includes('quick-dir required') || res.stderr.includes('--quick-dir'),
|
|
627
|
+
'expected quick-dir error in stderr, got: ' + res.stderr
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('config.commit_docs=false skips commit (no git commit created)', () => {
|
|
632
|
+
env = createFinalizeEnv({ commitDocs: false });
|
|
633
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
634
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
635
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
636
|
+
|
|
637
|
+
const commitsBefore = countCommits(env.repoDir);
|
|
638
|
+
const res = runFinalize(env.repoDir, [
|
|
639
|
+
env.quickId,
|
|
640
|
+
'--quick-dir', env.quickDir,
|
|
641
|
+
'--state-path', env.statePath,
|
|
642
|
+
'--description', 'skip mode',
|
|
643
|
+
]);
|
|
644
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
645
|
+
assert.ok(res.parsed);
|
|
646
|
+
assert.equal(res.parsed.committed, false);
|
|
647
|
+
assert.equal(res.parsed.commit_reason, 'skipped_commit_docs_false');
|
|
648
|
+
assert.deepEqual(res.parsed.files_committed, []);
|
|
649
|
+
const commitsAfter = countCommits(env.repoDir);
|
|
650
|
+
assert.equal(commitsAfter, commitsBefore, 'no new commit should have been created');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('nothing-to-commit: returns committed=false, commit_reason=nothing_to_commit', () => {
|
|
654
|
+
env = createFinalizeEnv();
|
|
655
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
656
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-SUMMARY.md'), '# summary\n');
|
|
657
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
658
|
+
|
|
659
|
+
// First call: succeeds and commits
|
|
660
|
+
const first = runFinalize(env.repoDir, [
|
|
661
|
+
env.quickId,
|
|
662
|
+
'--quick-dir', env.quickDir,
|
|
663
|
+
'--state-path', env.statePath,
|
|
664
|
+
'--description', 'first call',
|
|
665
|
+
]);
|
|
666
|
+
assert.equal(first.exitCode, 0, 'first call stderr: ' + first.stderr);
|
|
667
|
+
assert.equal(first.parsed.committed, true);
|
|
668
|
+
|
|
669
|
+
// Second call: same files, nothing new staged
|
|
670
|
+
const second = runFinalize(env.repoDir, [
|
|
671
|
+
env.quickId,
|
|
672
|
+
'--quick-dir', env.quickDir,
|
|
673
|
+
'--state-path', env.statePath,
|
|
674
|
+
'--description', 'second call',
|
|
675
|
+
]);
|
|
676
|
+
assert.equal(second.exitCode, 0, 'second call stderr: ' + second.stderr);
|
|
677
|
+
assert.ok(second.parsed);
|
|
678
|
+
assert.equal(second.parsed.committed, false);
|
|
679
|
+
assert.equal(second.parsed.commit_reason, 'nothing_to_commit');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('commits both flat PLAN.md and numbered {quickId}-01-SUMMARY.md (regression: idea #18)', () => {
|
|
683
|
+
env = createFinalizeEnv();
|
|
684
|
+
// Planner writes flat PLAN, executor writes numbered SUMMARY (the 260410-ckl shape)
|
|
685
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-PLAN.md'), '# plan\n');
|
|
686
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-01-SUMMARY.md'), '# summary\n');
|
|
687
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
688
|
+
|
|
689
|
+
const res = runFinalize(env.repoDir, [
|
|
690
|
+
env.quickId,
|
|
691
|
+
'--quick-dir', env.quickDir,
|
|
692
|
+
'--state-path', env.statePath,
|
|
693
|
+
'--description', 'idea-18 regression',
|
|
694
|
+
]);
|
|
695
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
696
|
+
assert.ok(res.parsed, 'expected parsed JSON, got: ' + res.stdout);
|
|
697
|
+
assert.equal(res.parsed.committed, true);
|
|
698
|
+
assert.equal(res.parsed.commit_reason, 'committed');
|
|
699
|
+
// 3 files: flat PLAN + numbered SUMMARY + STATE
|
|
700
|
+
assert.equal(res.parsed.files_committed.length, 3, 'files: ' + JSON.stringify(res.parsed.files_committed));
|
|
701
|
+
|
|
702
|
+
// Both file basenames must appear in the actual commit
|
|
703
|
+
const committedFiles = execSync(
|
|
704
|
+
'git log -1 --name-only --format=',
|
|
705
|
+
{ cwd: env.repoDir, encoding: 'utf-8' }
|
|
706
|
+
).trim().split('\n').filter(Boolean);
|
|
707
|
+
const hasFlat = committedFiles.some(f => f.endsWith(env.quickId + '-PLAN.md'));
|
|
708
|
+
const hasNumberedSummary = committedFiles.some(f => f.endsWith(env.quickId + '-01-SUMMARY.md'));
|
|
709
|
+
assert.ok(hasFlat, 'flat PLAN.md should be in commit, got: ' + JSON.stringify(committedFiles));
|
|
710
|
+
assert.ok(hasNumberedSummary, 'numbered SUMMARY should be in commit, got: ' + JSON.stringify(committedFiles));
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('commits fully numbered shape ({quickId}-01-PLAN.md + {quickId}-01-SUMMARY.md)', () => {
|
|
714
|
+
env = createFinalizeEnv();
|
|
715
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-01-PLAN.md'), '# plan\n');
|
|
716
|
+
fs.writeFileSync(path.join(env.taskDir, env.quickId + '-01-SUMMARY.md'), '# summary\n');
|
|
717
|
+
fs.writeFileSync(env.statePath, '# state\n');
|
|
718
|
+
|
|
719
|
+
const res = runFinalize(env.repoDir, [
|
|
720
|
+
env.quickId,
|
|
721
|
+
'--quick-dir', env.quickDir,
|
|
722
|
+
'--state-path', env.statePath,
|
|
723
|
+
'--description', 'fully numbered shape',
|
|
724
|
+
]);
|
|
725
|
+
assert.equal(res.exitCode, 0, 'stderr: ' + res.stderr);
|
|
726
|
+
assert.ok(res.parsed);
|
|
727
|
+
assert.equal(res.parsed.committed, true);
|
|
728
|
+
assert.equal(res.parsed.files_committed.length, 3);
|
|
729
|
+
});
|
|
730
|
+
});
|