@ktpartners/dgs-platform 3.0.4 → 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 +115 -0
- package/README.md +8 -1
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +32 -0
- package/agents/dgs-planner.md +41 -8
- package/bin/install.js +44 -0
- package/commands/dgs/audit-milestone.md +2 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/research-idea.md +1 -0
- package/commands/dgs/switch-project.md +13 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
- 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 +311 -16
- package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +41 -0
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/core.cjs +7 -3
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- 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/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/init.cjs +56 -27
- package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
- package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
- package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
- 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 +18 -1
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +38 -3
- package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
- package/deliver-great-systems/bin/lib/quick.cjs +178 -23
- package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
- package/deliver-great-systems/bin/lib/repos.cjs +12 -12
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/state.cjs +7 -3
- 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/verify.cjs +118 -6
- 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 +27 -1
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -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 +11 -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/add-phase.md +5 -0
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +26 -7
- package/deliver-great-systems/workflows/complete-quick.md +40 -2
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +89 -2
- package/deliver-great-systems/workflows/execute-plan.md +10 -1
- package/deliver-great-systems/workflows/help.md +51 -18
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +46 -152
- package/deliver-great-systems/workflows/new-milestone.md +115 -14
- 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/quick-complete.md +40 -2
- package/deliver-great-systems/workflows/quick.md +183 -10
- package/deliver-great-systems/workflows/research-idea.md +80 -142
- package/deliver-great-systems/workflows/run-job.md +21 -35
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/write-spec.md +9 -11
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -817,8 +817,9 @@ function phaseCompleteInternal(cwd, phaseNum) {
|
|
|
817
817
|
const reqPath = path.join(cwd, projRoot, 'REQUIREMENTS.md');
|
|
818
818
|
if (fs.existsSync(reqPath)) {
|
|
819
819
|
// Extract Requirements line from roadmap for this phase
|
|
820
|
+
// Match both **Requirements:** (colon inside bold) and **Requirements**: (colon outside bold)
|
|
820
821
|
const reqMatch = roadmapContent.match(
|
|
821
|
-
new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements
|
|
822
|
+
new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements(?::\\*\\*|\\*\\*:)\\s*([^\\n]+)`, 'i')
|
|
822
823
|
);
|
|
823
824
|
|
|
824
825
|
if (reqMatch) {
|
|
@@ -944,6 +945,20 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
944
945
|
*
|
|
945
946
|
* Does NOT call cmdCommit (which exits). Uses execGit directly.
|
|
946
947
|
*/
|
|
948
|
+
// Collect the list of still-dirty paths in `gitCwd` after a commit. Purely
|
|
949
|
+
// informational — populates `result.dirty_after`. Duplicated from
|
|
950
|
+
// commands.cjs/quick.cjs instead of extracted because the three call sites
|
|
951
|
+
// use slightly different cwd variables and a shared module would obscure that.
|
|
952
|
+
function collectDirtyAfter(gitCwd) {
|
|
953
|
+
const porcelain = execGit(gitCwd, ['status', '--porcelain']);
|
|
954
|
+
if (porcelain.exitCode !== 0) return [];
|
|
955
|
+
return (porcelain.stdout || '')
|
|
956
|
+
.split('\n')
|
|
957
|
+
.map(l => l.trim())
|
|
958
|
+
.filter(Boolean)
|
|
959
|
+
.map(l => l.replace(/^..\s+/, ''));
|
|
960
|
+
}
|
|
961
|
+
|
|
947
962
|
function cmdPhaseFinalize(cwd, phaseNum, options, raw) {
|
|
948
963
|
if (!phaseNum) {
|
|
949
964
|
error('phase number required for phase finalize');
|
|
@@ -993,6 +1008,7 @@ function cmdPhaseFinalize(cwd, phaseNum, options, raw) {
|
|
|
993
1008
|
result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
|
|
994
1009
|
if (!nothing) result.commit_error = commitResult.stderr;
|
|
995
1010
|
result.files_committed = [];
|
|
1011
|
+
result.dirty_after = collectDirtyAfter(cwd);
|
|
996
1012
|
output(result, raw);
|
|
997
1013
|
return;
|
|
998
1014
|
}
|
|
@@ -1001,6 +1017,7 @@ function cmdPhaseFinalize(cwd, phaseNum, options, raw) {
|
|
|
1001
1017
|
result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
1002
1018
|
result.commit_reason = 'committed';
|
|
1003
1019
|
result.files_committed = filesToStage;
|
|
1020
|
+
result.dirty_after = collectDirtyAfter(cwd);
|
|
1004
1021
|
|
|
1005
1022
|
// Optional push (same semantics as cmdCommit)
|
|
1006
1023
|
if (options && options.push) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// deliver-great-systems/bin/lib/plan-number-validity.test.cjs
|
|
2
|
+
// REL-04 regression test scaffold — initially RED. Turns GREEN after plan 02
|
|
3
|
+
// adds Dimension 10 (plan-number validity) to agents/dgs-plan-checker.md.
|
|
4
|
+
//
|
|
5
|
+
// Strategy: this is an agent-prompt assertion, not a CLI test. Tests read
|
|
6
|
+
// agents/dgs-plan-checker.md and assert the dimension exists with the required
|
|
7
|
+
// remediation text + soft-fail marker + named-follow-up-phase reference.
|
|
8
|
+
|
|
9
|
+
const test = require('node:test');
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const repoRoot = path.resolve(__dirname, '../../..');
|
|
15
|
+
const checkerPath = path.join(repoRoot, 'agents/dgs-plan-checker.md');
|
|
16
|
+
|
|
17
|
+
test('REL-04: dgs-plan-checker has Dimension 10 (plan-number validity)', () => {
|
|
18
|
+
const content = fs.readFileSync(checkerPath, 'utf-8');
|
|
19
|
+
assert.match(content, /## Dimension 10: Plan Number Validity/, 'Dimension 10 must exist');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('REL-04: Dimension 10 enumerates rejected plan-number forms', () => {
|
|
23
|
+
const content = fs.readFileSync(checkerPath, 'utf-8');
|
|
24
|
+
for (const form of ['plan: 00', 'plan: 0', 'positive integer']) {
|
|
25
|
+
assert.ok(content.includes(form), `Dimension 10 must mention "${form}"`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('REL-04: Dimension 10 includes exact rename remediation command', () => {
|
|
30
|
+
const content = fs.readFileSync(checkerPath, 'utf-8');
|
|
31
|
+
assert.match(content, /git mv .*-00-PLAN\.md.*-01-PLAN\.md/, 'remediation must include git mv command');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('REL-04: Dimension 10 is soft-fail (warning, not block) on initial rollout', () => {
|
|
35
|
+
const content = fs.readFileSync(checkerPath, 'utf-8');
|
|
36
|
+
const dim10Match = content.match(/## Dimension 10[\s\S]+?(?=## Dimension|$)/);
|
|
37
|
+
assert.ok(dim10Match, 'Dimension 10 block must exist');
|
|
38
|
+
const dim10Text = dim10Match[0];
|
|
39
|
+
assert.ok(/soft-fail|warning|warn/i.test(dim10Text), 'Dimension 10 must mark itself as soft-fail/warning');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('REL-04: Dimension 10 references the named follow-up phase for hard-reject promotion', () => {
|
|
43
|
+
const content = fs.readFileSync(checkerPath, 'utf-8');
|
|
44
|
+
assert.ok(/follow-up phase|hard-reject promotion/i.test(content),
|
|
45
|
+
'Dimension 10 must reference hard-reject deferral to a named follow-up phase');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// REL-04 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.
|
|
@@ -171,8 +171,14 @@ function scanProjectReposTags(cwd, slug) {
|
|
|
171
171
|
* Enumerate all project subfolders and build a { projects, warnings } result
|
|
172
172
|
* WITHOUT writing PROJECTS.md. Pure read function — no side effects.
|
|
173
173
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
174
|
+
* Thin-skeleton projects (PROJECT.md present, STATE.md absent) yield a placeholder
|
|
175
|
+
* entry with status "No milestone yet" and emit no warning — this is the post-thin-skeleton
|
|
176
|
+
* contract where /dgs:new-project writes only PROJECT.md and STATE.md is created later
|
|
177
|
+
* by /dgs:new-milestone.
|
|
178
|
+
*
|
|
179
|
+
* Ghost-project guard (defensive): a directory with neither PROJECT.md nor STATE.md is
|
|
180
|
+
* normally filtered out by getProjectFolders before reaching here. If a future caller
|
|
181
|
+
* passes such a slug directly, the legacy warning is preserved and the entry is omitted.
|
|
176
182
|
*
|
|
177
183
|
* @param {string} cwd - Working directory (product root)
|
|
178
184
|
* @returns {{ projects: Array, warnings: string[] }}
|
|
@@ -185,7 +191,22 @@ function listProjectsReadonly(cwd) {
|
|
|
185
191
|
for (const slug of slugs) {
|
|
186
192
|
const state = readProjectState(cwd, slug);
|
|
187
193
|
if (!state) {
|
|
188
|
-
|
|
194
|
+
// No STATE.md (or unreadable): if PROJECT.md is present, treat as thin-skeleton
|
|
195
|
+
// and emit a placeholder row. Only warn when neither marker is present (defensive
|
|
196
|
+
// guard for direct callers that bypass getProjectFolders).
|
|
197
|
+
const projectMdPath = path.join(getProjectDir(cwd, slug), 'PROJECT.md');
|
|
198
|
+
if (fs.existsSync(projectMdPath)) {
|
|
199
|
+
projects.push({
|
|
200
|
+
name: slug,
|
|
201
|
+
status: 'No milestone yet',
|
|
202
|
+
repos_touched: '',
|
|
203
|
+
current_phase: '',
|
|
204
|
+
completed_date: '',
|
|
205
|
+
progress: '',
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
warnings.push(`Ghost project: ${slug} (STATE.md missing or unreadable)`);
|
|
209
|
+
}
|
|
189
210
|
continue;
|
|
190
211
|
}
|
|
191
212
|
|
|
@@ -671,6 +692,20 @@ function cmdProjectsReactivate(cwd, options, raw) {
|
|
|
671
692
|
const regenResult = regenerateProjectsMd(cwd);
|
|
672
693
|
const remainingActive = regenResult.projects.filter(p => !p.status.toLowerCase().includes('completed'));
|
|
673
694
|
|
|
695
|
+
// Commit STATE.md and PROJECTS.md so reactivation is persisted
|
|
696
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
697
|
+
const projectDir = getProjectDir(cwd, slug);
|
|
698
|
+
const statePath = path.join(projectDir, 'STATE.md');
|
|
699
|
+
const projectsPath = path.join(planningRoot, 'PROJECTS.md');
|
|
700
|
+
const filesToCommit = [statePath, projectsPath].filter(f => fs.existsSync(f));
|
|
701
|
+
try {
|
|
702
|
+
const { execSync } = require('child_process');
|
|
703
|
+
for (const f of filesToCommit) {
|
|
704
|
+
execSync(`git add "${f}"`, { cwd: planningRoot, stdio: 'pipe' });
|
|
705
|
+
}
|
|
706
|
+
execSync(`git commit -m "docs: reactivate project ${slug}" --allow-empty`, { cwd: planningRoot, stdio: 'pipe' });
|
|
707
|
+
} catch { /* commit may fail if nothing changed — safe to ignore */ }
|
|
708
|
+
|
|
674
709
|
output({ reactivated: true, slug, set_as_current: !!opts.set_current, remaining_active: remainingActive.length }, raw);
|
|
675
710
|
}
|
|
676
711
|
|
|
@@ -11,22 +11,57 @@ const os = require('os');
|
|
|
11
11
|
|
|
12
12
|
const { createTempDir, cleanupDir, createFixture , initGitRepo } = require('./test-helpers.cjs');
|
|
13
13
|
const { resetPaths } = require('./paths.cjs');
|
|
14
|
-
const { getProjectRoot } = require('./core.cjs');
|
|
14
|
+
const { getProjectRoot, loadConfig } = require('./core.cjs');
|
|
15
15
|
|
|
16
16
|
// Helper: create projects/ directory structure
|
|
17
17
|
function setupPlanning(cwd) {
|
|
18
18
|
fs.mkdirSync(path.join(cwd, 'projects'), { recursive: true });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// Helper: create a project subfolder
|
|
21
|
+
// Helper: create a fully-scaffolded project subfolder under projects/<slug>/
|
|
22
|
+
// (writes PROJECT.md as the canonical project marker plus optional STATE.md for milestone state)
|
|
22
23
|
function createProjectManually(cwd, slug, stateContent) {
|
|
23
24
|
const projDir = path.join(cwd, 'projects', slug);
|
|
24
25
|
fs.mkdirSync(projDir, { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(projDir, 'PROJECT.md'), `# Project: ${slug}\n`);
|
|
25
27
|
if (stateContent) {
|
|
26
28
|
fs.writeFileSync(path.join(projDir, 'STATE.md'), stateContent);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// Helper: create a thin-skeleton project (PROJECT.md only, no STATE.md) — mirrors what
|
|
33
|
+
// /dgs:new-project writes before /dgs:new-milestone creates STATE.md.
|
|
34
|
+
function createThinSkeletonProject(cwd, slug) {
|
|
35
|
+
const projDir = path.join(cwd, 'projects', slug);
|
|
36
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(projDir, 'PROJECT.md'), `# Project: ${slug}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Capture stdout helper for cmdProjects* CLI calls (mirrors commands.test.cjs:48-70).
|
|
41
|
+
function captureStdout(fn) {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
44
|
+
const origExit = process.exit;
|
|
45
|
+
let exitCode = null;
|
|
46
|
+
process.stdout.write = (data) => { chunks.push(String(data)); return true; };
|
|
47
|
+
process.exit = (code) => {
|
|
48
|
+
exitCode = code == null ? 0 : code;
|
|
49
|
+
throw new Error('__EXIT__');
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
fn();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e && e.message !== '__EXIT__') throw e;
|
|
55
|
+
} finally {
|
|
56
|
+
process.stdout.write = origWrite;
|
|
57
|
+
process.exit = origExit;
|
|
58
|
+
}
|
|
59
|
+
const stdout = chunks.join('');
|
|
60
|
+
let json = null;
|
|
61
|
+
try { json = JSON.parse(stdout); } catch { /* not JSON */ }
|
|
62
|
+
return { stdout, exitCode, json };
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
// Helper: create plan file with <repos> tags under projects/<slug>/phases/
|
|
31
66
|
function createPlanFile(cwd, slug, phaseDir, planName, content) {
|
|
32
67
|
const dir = path.join(cwd, 'projects', slug, 'phases', phaseDir);
|
|
@@ -44,6 +79,7 @@ const {
|
|
|
44
79
|
reactivateProject,
|
|
45
80
|
parseProjectsMd,
|
|
46
81
|
checkSlugPrefixCollision,
|
|
82
|
+
cmdProjectsSwitch,
|
|
47
83
|
} = require('./projects.cjs');
|
|
48
84
|
|
|
49
85
|
// ─── createProjectSubfolder ─────────────────────────────────────────────────
|
|
@@ -322,6 +358,17 @@ describe('regenerateProjectsMd', () => {
|
|
|
322
358
|
const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
|
|
323
359
|
assert.ok(content.startsWith('# Projects'));
|
|
324
360
|
});
|
|
361
|
+
|
|
362
|
+
it('writes thin-skeleton placeholder row into Active table', () => {
|
|
363
|
+
createThinSkeletonProject(tmpDir, 'word-gen');
|
|
364
|
+
regenerateProjectsMd(tmpDir);
|
|
365
|
+
const content = fs.readFileSync(path.join(tmpDir, 'PROJECTS.md'), 'utf-8');
|
|
366
|
+
const [activeSection, completedSection] = content.split('## Completed');
|
|
367
|
+
// Active row should match: | word-gen | No milestone yet | | |
|
|
368
|
+
assert.match(activeSection, /\|\s*word-gen\s*\|\s*No milestone yet\s*\|\s*\|\s*\|/);
|
|
369
|
+
// word-gen must NOT appear in the Completed section
|
|
370
|
+
assert.ok(!completedSection.includes('word-gen'), 'thin-skeleton project must not appear in Completed section');
|
|
371
|
+
});
|
|
325
372
|
});
|
|
326
373
|
|
|
327
374
|
// ─── listProjectsReadonly ───────────────────────────────────────────────────
|
|
@@ -407,6 +454,35 @@ describe('listProjectsReadonly', () => {
|
|
|
407
454
|
assert.deepStrictEqual(result.projects, []);
|
|
408
455
|
assert.ok(Array.isArray(result.warnings));
|
|
409
456
|
});
|
|
457
|
+
|
|
458
|
+
it('returns placeholder entry with status "No milestone yet" for thin-skeleton (PROJECT.md only) and emits no warning for that slug', () => {
|
|
459
|
+
createThinSkeletonProject(tmpDir, 'word-gen');
|
|
460
|
+
const result = listProjectsReadonly(tmpDir);
|
|
461
|
+
assert.strictEqual(result.projects.length, 1);
|
|
462
|
+
assert.deepStrictEqual(result.projects[0], {
|
|
463
|
+
name: 'word-gen',
|
|
464
|
+
status: 'No milestone yet',
|
|
465
|
+
repos_touched: '',
|
|
466
|
+
current_phase: '',
|
|
467
|
+
completed_date: '',
|
|
468
|
+
progress: '',
|
|
469
|
+
});
|
|
470
|
+
assert.ok(Array.isArray(result.warnings));
|
|
471
|
+
assert.ok(!result.warnings.some(w => w.includes('word-gen')), 'no warning should mention word-gen');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('preserves ghost-warning code path when neither PROJECT.md nor STATE.md exists', () => {
|
|
475
|
+
// Genuine ghost: no marker files at all — getProjectFolders filters it out, so it
|
|
476
|
+
// never reaches listProjectsReadonly's loop. Real project alongside to keep suite non-empty.
|
|
477
|
+
fs.mkdirSync(path.join(tmpDir, 'projects', 'ghost'), { recursive: true });
|
|
478
|
+
createProjectManually(tmpDir, 'real-project', '# State\n\nStatus: Active\n');
|
|
479
|
+
|
|
480
|
+
const result = listProjectsReadonly(tmpDir);
|
|
481
|
+
assert.ok(Array.isArray(result.warnings));
|
|
482
|
+
assert.ok(!result.projects.some(p => p.name === 'ghost'), 'ghost dir should be omitted from projects array');
|
|
483
|
+
// The warnings array remains a defensive code path; legacy callers may still trip it.
|
|
484
|
+
// No assertion on warnings content here — discovery layer (getProjectFolders) handles ghost exclusion.
|
|
485
|
+
});
|
|
410
486
|
});
|
|
411
487
|
|
|
412
488
|
// ─── completeProject ────────────────────────────────────────────────────────
|
|
@@ -943,6 +1019,7 @@ describe('cmdProjectsSwitch guards', () => {
|
|
|
943
1019
|
'config.json': JSON.stringify({ current_project: 'finished-proj' }),
|
|
944
1020
|
'PROJECTS.md': '# Projects\n\n## Active\n\n| Project | Status | Repos Touched | Current Phase |\n|---------|--------|---------------|---------------|\n\n## Completed\n\n| Project | Completed | Duration |\n|---------|-----------|----------|\n| finished-proj | 2026-02-20 | |\n',
|
|
945
1021
|
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
1022
|
+
'projects/finished-proj/PROJECT.md': '# Project: Finished Proj\n',
|
|
946
1023
|
'projects/finished-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
|
|
947
1024
|
});
|
|
948
1025
|
|
|
@@ -956,3 +1033,36 @@ describe('cmdProjectsSwitch guards', () => {
|
|
|
956
1033
|
}
|
|
957
1034
|
});
|
|
958
1035
|
});
|
|
1036
|
+
|
|
1037
|
+
// ─── cmdProjectsSwitch thin-skeleton ────────────────────────────────────────
|
|
1038
|
+
|
|
1039
|
+
describe('cmdProjectsSwitch thin-skeleton', () => {
|
|
1040
|
+
let tmpDir;
|
|
1041
|
+
|
|
1042
|
+
beforeEach(() => {
|
|
1043
|
+
tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir);
|
|
1044
|
+
setupPlanning(tmpDir);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
1048
|
+
|
|
1049
|
+
it('cmdProjectsSwitch succeeds on a thin-skeleton slug and writes current_project to config.json', () => {
|
|
1050
|
+
// v2 prerequisites: config.json, PROJECTS.md, REPOS.md at planning root,
|
|
1051
|
+
// plus the thin-skeleton project under projects/<slug>/PROJECT.md
|
|
1052
|
+
fs.writeFileSync(path.join(tmpDir, 'config.json'), '{}');
|
|
1053
|
+
fs.writeFileSync(path.join(tmpDir, 'PROJECTS.md'), '# Projects\n');
|
|
1054
|
+
fs.writeFileSync(path.join(tmpDir, 'REPOS.md'), '# Repos\n');
|
|
1055
|
+
createThinSkeletonProject(tmpDir, 'word-gen');
|
|
1056
|
+
|
|
1057
|
+
const { exitCode } = captureStdout(() => {
|
|
1058
|
+
cmdProjectsSwitch(tmpDir, 'word-gen', true);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// exitCode 0 (or unset null = success path that did not throw) signals success
|
|
1062
|
+
assert.ok(exitCode === 0 || exitCode === null, `expected success exit, got ${exitCode}`);
|
|
1063
|
+
|
|
1064
|
+
// config.json must now contain current_project: word-gen
|
|
1065
|
+
const config = loadConfig(tmpDir);
|
|
1066
|
+
assert.strictEqual(config.current_project, 'word-gen');
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
@@ -19,6 +19,8 @@ const { execGit, output, error, loadConfig } = require('./core.cjs');
|
|
|
19
19
|
const { getLocalConfigPath } = require('./config.cjs');
|
|
20
20
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
21
21
|
const { cmdWorktreesCreate, cmdWorktreesRemove, rebaseAndMerge } = require('./worktrees.cjs');
|
|
22
|
+
const { checkFourEyes } = require('./governance.cjs');
|
|
23
|
+
const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
|
|
22
24
|
|
|
23
25
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
24
26
|
|
|
@@ -53,6 +55,24 @@ function _writeLocalConfig(cwd, data) {
|
|
|
53
55
|
fs.renameSync(tmpPath, localPath);
|
|
54
56
|
}
|
|
55
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
|
+
|
|
56
76
|
/**
|
|
57
77
|
* Sanitize a title into a valid slug for branch/worktree naming.
|
|
58
78
|
* @param {string} title
|
|
@@ -91,6 +111,13 @@ function _clearStaleQuick(cwd, project, slug, localConfig) {
|
|
|
91
111
|
|
|
92
112
|
// ─── Exported functions ───────────────────────────────────────────────────────
|
|
93
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>`.
|
|
94
121
|
/**
|
|
95
122
|
* Detect whether a quick should be product-level or milestone-context.
|
|
96
123
|
*
|
|
@@ -116,7 +143,16 @@ function detectQuickMode(cwd, forceMain) {
|
|
|
116
143
|
const entry = worktrees[activeContext];
|
|
117
144
|
|
|
118
145
|
if (entry && entry.type === 'milestone') {
|
|
119
|
-
|
|
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>`.
|
|
120
156
|
}
|
|
121
157
|
|
|
122
158
|
// If active context is a quick or unknown, treat as product-level
|
|
@@ -178,9 +214,20 @@ function startProductQuick(cwd, title, mode) {
|
|
|
178
214
|
const project = config.current_project;
|
|
179
215
|
if (!project) return { success: false, error: 'No current project set' };
|
|
180
216
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
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(/-+$/, '');
|
|
184
231
|
|
|
185
232
|
// Create worktree via existing cmdWorktreesCreate
|
|
186
233
|
// Note: cmdWorktreesCreate calls output() which exits the process.
|
|
@@ -194,7 +241,7 @@ function startProductQuick(cwd, title, mode) {
|
|
|
194
241
|
try {
|
|
195
242
|
const modeArgs = mode ? ' --mode ' + mode : '';
|
|
196
243
|
const result = execSync(
|
|
197
|
-
'node ' + JSON.stringify(dgsTools) + ' worktrees create ' + JSON.stringify(
|
|
244
|
+
'node ' + JSON.stringify(dgsTools) + ' worktrees create ' + JSON.stringify(canonicalSlug) + ' --type quick' + modeArgs,
|
|
198
245
|
{ cwd: root, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 60000 }
|
|
199
246
|
);
|
|
200
247
|
// Parse output to verify creation
|
|
@@ -211,13 +258,13 @@ function startProductQuick(cwd, title, mode) {
|
|
|
211
258
|
const localConfig = _readLocalConfig(cwd);
|
|
212
259
|
if (localConfig.projects && localConfig.projects[project]
|
|
213
260
|
&& localConfig.projects[project].worktrees
|
|
214
|
-
&& localConfig.projects[project].worktrees[
|
|
215
|
-
localConfig.projects[project].worktrees[
|
|
261
|
+
&& localConfig.projects[project].worktrees[canonicalSlug]) {
|
|
262
|
+
localConfig.projects[project].worktrees[canonicalSlug].mode = mode || null;
|
|
216
263
|
}
|
|
217
264
|
|
|
218
265
|
// Set active_context
|
|
219
266
|
if (!localConfig.execution) localConfig.execution = {};
|
|
220
|
-
localConfig.execution.active_context =
|
|
267
|
+
localConfig.execution.active_context = canonicalSlug;
|
|
221
268
|
|
|
222
269
|
_writeLocalConfig(cwd, localConfig);
|
|
223
270
|
|
|
@@ -226,9 +273,9 @@ function startProductQuick(cwd, title, mode) {
|
|
|
226
273
|
(finalConfig.projects &&
|
|
227
274
|
finalConfig.projects[project] &&
|
|
228
275
|
finalConfig.projects[project].worktrees &&
|
|
229
|
-
finalConfig.projects[project].worktrees[
|
|
230
|
-
finalConfig.projects[project].worktrees[
|
|
231
|
-
return { success: true, slug:
|
|
276
|
+
finalConfig.projects[project].worktrees[canonicalSlug] &&
|
|
277
|
+
finalConfig.projects[project].worktrees[canonicalSlug].repos) || {};
|
|
278
|
+
return { success: true, slug: canonicalSlug, repos: repos };
|
|
232
279
|
}
|
|
233
280
|
|
|
234
281
|
/**
|
|
@@ -237,7 +284,8 @@ function startProductQuick(cwd, title, mode) {
|
|
|
237
284
|
* @param {string} cwd - Planning root
|
|
238
285
|
* @returns {{ success: boolean, commitCount?: number, slug?: string, error?: string, manualInstructions?: string }}
|
|
239
286
|
*/
|
|
240
|
-
function quickComplete(cwd) {
|
|
287
|
+
function quickComplete(cwd, options) {
|
|
288
|
+
options = options || {};
|
|
241
289
|
const active = getActiveQuick(cwd);
|
|
242
290
|
if (!active) {
|
|
243
291
|
return { success: false, error: 'No active product-level quick to complete. If working in a milestone context, changes are part of the milestone.' };
|
|
@@ -256,6 +304,88 @@ function quickComplete(cwd) {
|
|
|
256
304
|
const config = loadConfig(cwd);
|
|
257
305
|
const baseBranch = config.base_branch || 'main';
|
|
258
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
|
+
|
|
259
389
|
// Rebase and merge each repo
|
|
260
390
|
for (const repoName of repoNames) {
|
|
261
391
|
const worktreePath = repos[repoName];
|
|
@@ -366,8 +496,9 @@ function quickAbandon(cwd, confirmed) {
|
|
|
366
496
|
* CLI handler for `dgs-tools complete-quick` (also `quick-complete` for backward compat).
|
|
367
497
|
* @param {string} cwd
|
|
368
498
|
*/
|
|
369
|
-
function cmdQuickComplete(cwd) {
|
|
370
|
-
const
|
|
499
|
+
function cmdQuickComplete(cwd, args) {
|
|
500
|
+
const force = args && args.includes('--force');
|
|
501
|
+
const result = quickComplete(cwd, { force });
|
|
371
502
|
if (!result.success) {
|
|
372
503
|
if (result.manualInstructions) {
|
|
373
504
|
process.stderr.write(result.manualInstructions + '\n');
|
|
@@ -425,6 +556,22 @@ function cmdQuickAbandon(cwd, args) {
|
|
|
425
556
|
* @param {object} options - { description, quickDir, statePath, push, repoCwd, fast }
|
|
426
557
|
* @param {boolean} raw - true to emit raw JSON, false for pretty output
|
|
427
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
|
+
|
|
428
575
|
function cmdQuickFinalize(cwd, quickId, options, raw) {
|
|
429
576
|
options = options || {};
|
|
430
577
|
|
|
@@ -490,15 +637,20 @@ function cmdQuickFinalize(cwd, quickId, options, raw) {
|
|
|
490
637
|
}
|
|
491
638
|
|
|
492
639
|
const taskDir = path.join(options.quickDir, taskDirName);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
];
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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)));
|
|
502
654
|
}
|
|
503
655
|
}
|
|
504
656
|
}
|
|
@@ -542,6 +694,7 @@ function cmdQuickFinalize(cwd, quickId, options, raw) {
|
|
|
542
694
|
result.commit_reason = nothing ? 'nothing_to_commit' : 'commit_failed';
|
|
543
695
|
if (!nothing) result.commit_error = commitResult.stderr;
|
|
544
696
|
result.files_committed = [];
|
|
697
|
+
result.dirty_after = collectDirtyAfter(gitCwdReal);
|
|
545
698
|
output(result, raw);
|
|
546
699
|
return;
|
|
547
700
|
}
|
|
@@ -550,6 +703,7 @@ function cmdQuickFinalize(cwd, quickId, options, raw) {
|
|
|
550
703
|
result.hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
551
704
|
result.commit_reason = 'committed';
|
|
552
705
|
result.files_committed = filesToStage;
|
|
706
|
+
result.dirty_after = collectDirtyAfter(gitCwdReal);
|
|
553
707
|
|
|
554
708
|
// Optional push (same semantics as cmdPhaseFinalize)
|
|
555
709
|
if (options.push) {
|
|
@@ -573,6 +727,7 @@ function cmdQuickFinalize(cwd, quickId, options, raw) {
|
|
|
573
727
|
}
|
|
574
728
|
|
|
575
729
|
module.exports = {
|
|
730
|
+
generateQuickId,
|
|
576
731
|
detectQuickMode,
|
|
577
732
|
getActiveQuick,
|
|
578
733
|
startProductQuick,
|