@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
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
-
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error, resolveProjectPath, getProjectRoot, isV2Install, getProjectFolders, getV2Hint } = require('./core.cjs');
|
|
8
|
+
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error, resolveProjectPath, getProjectRoot, isV2Install, getProjectFolders, getV2Hint, safeReadFile } = require('./core.cjs');
|
|
9
9
|
const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
|
|
10
10
|
const { getPlanningRoot, PROJECTS_DIR } = require('./paths.cjs');
|
|
11
11
|
const { parseReposMd, validateReposMdEager } = require('./repos.cjs');
|
|
12
12
|
const { getCadence, pullAll } = require('./sync.cjs');
|
|
13
|
+
const { detectQuickMode } = require('./quick.cjs');
|
|
14
|
+
const { listProjectsReadonly } = require('./projects.cjs');
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Safely resolve the current git author string.
|
|
@@ -221,9 +223,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
221
223
|
// Config flags
|
|
222
224
|
commit_docs: config.commit_docs,
|
|
223
225
|
parallelization: config.parallelization,
|
|
224
|
-
branching_strategy:
|
|
225
|
-
phase_branch_template: config.phase_branch_template,
|
|
226
|
-
milestone_branch_template: config.milestone_branch_template,
|
|
226
|
+
branching_strategy: 'milestone',
|
|
227
227
|
verifier_enabled: config.verifier,
|
|
228
228
|
base_branch: config.base_branch,
|
|
229
229
|
sync_push: config.sync_push,
|
|
@@ -246,25 +246,14 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
246
246
|
plan_count: phaseInfo?.plans?.length || 0,
|
|
247
247
|
incomplete_count: phaseInfo?.incomplete_plans?.length || 0,
|
|
248
248
|
|
|
249
|
-
// Branch name (
|
|
249
|
+
// Branch name (milestone worktree model — always uses milestone template)
|
|
250
250
|
branch_name: (() => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.replace('{phase}', phaseInfo.phase_number)
|
|
258
|
-
.replace('{slug}', phaseInfo.phase_slug || 'phase');
|
|
259
|
-
} else if (config.branching_strategy === 'milestone') {
|
|
260
|
-
template = config.milestone_branch_template;
|
|
261
|
-
resolved = resolveProjectInTemplate(template, ctx.current_project);
|
|
262
|
-
if (resolved.error) return resolved.error;
|
|
263
|
-
return resolved.value
|
|
264
|
-
.replace('{milestone}', milestone.version)
|
|
265
|
-
.replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
251
|
+
const template = 'dgs/{project}/{milestone}-{slug}';
|
|
252
|
+
const resolved = resolveProjectInTemplate(template, ctx.current_project);
|
|
253
|
+
if (resolved.error) return resolved.error;
|
|
254
|
+
return resolved.value
|
|
255
|
+
.replace('{milestone}', milestone.version)
|
|
256
|
+
.replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
|
|
268
257
|
})(),
|
|
269
258
|
|
|
270
259
|
// Milestone info
|
|
@@ -533,14 +522,27 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
533
522
|
const config = loadConfig(cwd);
|
|
534
523
|
const ctx = resolveProjectContext(cwd);
|
|
535
524
|
const now = new Date();
|
|
536
|
-
const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
|
|
537
525
|
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
538
526
|
|
|
527
|
+
// Parse --main flag out of description string
|
|
528
|
+
const forceMain = /\s*--main\s*/.test(description || '');
|
|
529
|
+
const cleanDescription = (description || '').replace(/\s*--main\s*/g, ' ').trim() || null;
|
|
530
|
+
const slug = cleanDescription ? generateSlugInternal(cleanDescription)?.substring(0, 40).replace(/-+$/, '') : null;
|
|
531
|
+
|
|
532
|
+
// Detect quick mode: product-level vs milestone-context
|
|
533
|
+
const quickMode = detectQuickMode(cwd, forceMain);
|
|
534
|
+
|
|
539
535
|
// Generate collision-resistant quick task ID: YYMMDD-xxx
|
|
540
536
|
// xxx = 2-second precision blocks since midnight, encoded as 3-char Base36 (lowercase)
|
|
541
537
|
// Range: 000 (00:00:00) to xbz (23:59:58), guaranteed 3 chars for any time of day.
|
|
542
538
|
// Provides ~2s uniqueness window per user — practically collision-free across a team.
|
|
543
|
-
|
|
539
|
+
// Branch path resolution by quickMode: product-level quicks (including --main
|
|
540
|
+
// override) land at the planning root; milestone-context quicks stay under the
|
|
541
|
+
// active project. This keeps product-level work out of project state.
|
|
542
|
+
const isProductMode = quickMode.mode === 'product';
|
|
543
|
+
const quickBase = isProductMode
|
|
544
|
+
? path.join(planRootRel, 'quick')
|
|
545
|
+
: (ctx.root ? path.join(ctx.root, 'quick') : path.join(planRootRel, 'quick'));
|
|
544
546
|
const yy = String(now.getFullYear()).slice(-2);
|
|
545
547
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
546
548
|
const dd = String(now.getDate()).padStart(2, '0');
|
|
@@ -550,6 +552,18 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
550
552
|
const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
|
|
551
553
|
const quickId = dateStr + '-' + timeEncoded;
|
|
552
554
|
|
|
555
|
+
// Auto-create product-level STATE.md on demand (matches project STATE.md
|
|
556
|
+
// auto-creation behavior elsewhere). Keep skeleton minimal — the /dgs:quick
|
|
557
|
+
// workflow adds ### Quick Tasks Completed on first use; we only need the
|
|
558
|
+
// ### Blockers/Concerns anchor so later inserts succeed.
|
|
559
|
+
if (isProductMode) {
|
|
560
|
+
const absoluteStatePath = path.resolve(cwd, planRootRel, 'STATE.md');
|
|
561
|
+
if (!fs.existsSync(absoluteStatePath)) {
|
|
562
|
+
const skeleton = '# Project State\n\n## Current Position\n\n(Product-level state — quick tasks tracked here.)\n\n## Accumulated Context\n\n### Blockers/Concerns\n\nNone yet.\n';
|
|
563
|
+
fs.writeFileSync(absoluteStatePath, skeleton, 'utf-8');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
553
567
|
const result = {
|
|
554
568
|
// Models
|
|
555
569
|
planner_model: resolveModelInternal(cwd, 'dgs-planner'),
|
|
@@ -567,7 +581,7 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
567
581
|
// Quick task info
|
|
568
582
|
quick_id: quickId,
|
|
569
583
|
slug: slug,
|
|
570
|
-
description:
|
|
584
|
+
description: cleanDescription,
|
|
571
585
|
|
|
572
586
|
// Timestamps
|
|
573
587
|
date: now.toISOString().split('T')[0],
|
|
@@ -576,9 +590,14 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
576
590
|
// Paths (project-qualified)
|
|
577
591
|
quick_dir: quickBase,
|
|
578
592
|
task_dir: slug ? path.join(quickBase, `${quickId}-${slug}`) : null,
|
|
579
|
-
state_path:
|
|
593
|
+
state_path: isProductMode
|
|
594
|
+
? path.join(planRootRel, 'STATE.md')
|
|
595
|
+
: (ctx.root ? path.join(ctx.root, 'STATE.md') : path.join(planRootRel, 'STATE.md')),
|
|
580
596
|
|
|
581
597
|
// File existence
|
|
598
|
+
// roadmap_exists stays project-scoped for both modes: the quick workflow
|
|
599
|
+
// only reads ROADMAP.md for project context (phase/milestone labels), and
|
|
600
|
+
// product-level quicks still run against the currently active project.
|
|
582
601
|
roadmap_exists: ctx.root ? pathExistsInternal(cwd, path.join(ctx.root, 'ROADMAP.md')) : pathExistsInternal(cwd, path.join(planRootRel, 'ROADMAP.md')),
|
|
583
602
|
planning_exists: pathExistsInternal(cwd, planRootRel),
|
|
584
603
|
|
|
@@ -844,17 +863,23 @@ function cmdInitTodos(cwd, area, raw, workflow) {
|
|
|
844
863
|
const now = new Date();
|
|
845
864
|
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
846
865
|
|
|
847
|
-
// List todos (
|
|
848
|
-
const
|
|
849
|
-
const
|
|
866
|
+
// List todos — flat directory (status in frontmatter), with legacy fallback
|
|
867
|
+
const todosBase = path.join(planRootRel, 'todos');
|
|
868
|
+
const todosDir = path.join(cwd, todosBase);
|
|
850
869
|
let count = 0;
|
|
851
870
|
const todos = [];
|
|
871
|
+
const seenFiles = new Set();
|
|
852
872
|
|
|
873
|
+
// Flat directory scan (new layout — status in frontmatter)
|
|
853
874
|
try {
|
|
854
|
-
const files = fs.readdirSync(
|
|
875
|
+
const files = fs.readdirSync(todosDir).filter(f => f.endsWith('.md'));
|
|
855
876
|
for (const file of files) {
|
|
856
877
|
try {
|
|
857
|
-
const content = fs.readFileSync(path.join(
|
|
878
|
+
const content = fs.readFileSync(path.join(todosDir, file), 'utf-8');
|
|
879
|
+
const statusMatch = content.match(/^status:\s*(.+)$/m);
|
|
880
|
+
const todoStatus = statusMatch ? statusMatch[1].trim() : 'pending';
|
|
881
|
+
if (todoStatus !== 'pending') continue; // Only show pending todos
|
|
882
|
+
|
|
858
883
|
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
859
884
|
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
860
885
|
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
@@ -863,19 +888,46 @@ function cmdInitTodos(cwd, area, raw, workflow) {
|
|
|
863
888
|
if (area && todoArea !== area) continue;
|
|
864
889
|
|
|
865
890
|
count++;
|
|
891
|
+
seenFiles.add(file);
|
|
866
892
|
todos.push({
|
|
867
893
|
file,
|
|
868
894
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
869
895
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
870
896
|
area: todoArea,
|
|
871
|
-
path:
|
|
897
|
+
path: todosBase + '/' + file,
|
|
898
|
+
});
|
|
899
|
+
} catch {}
|
|
900
|
+
}
|
|
901
|
+
} catch {}
|
|
902
|
+
|
|
903
|
+
// Legacy fallback: check todos/pending/
|
|
904
|
+
const legacyPendingDir = path.join(cwd, todosBase, 'pending');
|
|
905
|
+
try {
|
|
906
|
+
const files = fs.readdirSync(legacyPendingDir).filter(f => f.endsWith('.md'));
|
|
907
|
+
for (const file of files) {
|
|
908
|
+
if (seenFiles.has(file)) continue;
|
|
909
|
+
try {
|
|
910
|
+
const content = fs.readFileSync(path.join(legacyPendingDir, file), 'utf-8');
|
|
911
|
+
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
912
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
913
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
914
|
+
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
915
|
+
|
|
916
|
+
if (area && todoArea !== area) continue;
|
|
917
|
+
|
|
918
|
+
count++;
|
|
919
|
+
todos.push({
|
|
920
|
+
file,
|
|
921
|
+
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
922
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
923
|
+
area: todoArea,
|
|
924
|
+
path: todosBase + '/pending/' + file,
|
|
872
925
|
});
|
|
873
926
|
} catch {}
|
|
874
927
|
}
|
|
875
928
|
} catch {}
|
|
876
929
|
|
|
877
930
|
const completedBase = path.join(planRootRel, 'todos', 'completed');
|
|
878
|
-
const todosBase = path.join(planRootRel, 'todos');
|
|
879
931
|
|
|
880
932
|
const result = {
|
|
881
933
|
// Config
|
|
@@ -894,14 +946,14 @@ function cmdInitTodos(cwd, area, raw, workflow) {
|
|
|
894
946
|
todos,
|
|
895
947
|
area_filter: area || null,
|
|
896
948
|
|
|
897
|
-
// Paths (project-qualified)
|
|
898
|
-
pending_dir:
|
|
949
|
+
// Paths (project-qualified) — pending_dir now points to flat todos/
|
|
950
|
+
pending_dir: todosBase,
|
|
899
951
|
completed_dir: completedBase,
|
|
900
952
|
|
|
901
953
|
// File existence
|
|
902
954
|
planning_exists: pathExistsInternal(cwd, planRootRel),
|
|
903
955
|
todos_dir_exists: pathExistsInternal(cwd, todosBase),
|
|
904
|
-
pending_dir_exists: pathExistsInternal(cwd,
|
|
956
|
+
pending_dir_exists: pathExistsInternal(cwd, todosBase),
|
|
905
957
|
|
|
906
958
|
// Author
|
|
907
959
|
author: resolveAuthorSafe(cwd),
|
|
@@ -959,9 +1011,7 @@ function cmdInitMilestoneOp(cwd, raw, workflow) {
|
|
|
959
1011
|
// Config
|
|
960
1012
|
commit_docs: config.commit_docs,
|
|
961
1013
|
base_branch: config.base_branch,
|
|
962
|
-
branching_strategy:
|
|
963
|
-
phase_branch_template: config.phase_branch_template,
|
|
964
|
-
milestone_branch_template: config.milestone_branch_template,
|
|
1014
|
+
branching_strategy: 'milestone',
|
|
965
1015
|
sync_push: config.sync_push,
|
|
966
1016
|
sync_pull: config.sync_pull,
|
|
967
1017
|
cadence_pull: cadence.pull,
|
|
@@ -1236,6 +1286,222 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1236
1286
|
output(result, raw);
|
|
1237
1287
|
}
|
|
1238
1288
|
|
|
1289
|
+
// ─── cmdInitProgressAll — Product-level dashboard ────────────────────────────
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Parse the product-level STATE.md "Quick Tasks Completed" table and return
|
|
1293
|
+
* the most recent rows (up to `limit`). Returns [] if STATE.md or the table
|
|
1294
|
+
* is missing.
|
|
1295
|
+
*/
|
|
1296
|
+
function parseProductQuickTasks(planningRoot, limit) {
|
|
1297
|
+
const statePath = path.join(planningRoot, 'STATE.md');
|
|
1298
|
+
const content = safeReadFile(statePath);
|
|
1299
|
+
if (!content) return [];
|
|
1300
|
+
|
|
1301
|
+
const lines = content.split('\n');
|
|
1302
|
+
const rows = [];
|
|
1303
|
+
let inTable = false;
|
|
1304
|
+
let headerSeen = false;
|
|
1305
|
+
|
|
1306
|
+
for (const line of lines) {
|
|
1307
|
+
if (!inTable) {
|
|
1308
|
+
if (/^###\s+Quick Tasks Completed/i.test(line)) {
|
|
1309
|
+
inTable = true;
|
|
1310
|
+
}
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
// Inside the Quick Tasks section
|
|
1314
|
+
if (!line.startsWith('|')) {
|
|
1315
|
+
if (rows.length > 0 || headerSeen) break; // end of table
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
// It's a table row
|
|
1319
|
+
if (!headerSeen) {
|
|
1320
|
+
if (/^\|\s*#\s*\|\s*Description/i.test(line)) {
|
|
1321
|
+
headerSeen = true;
|
|
1322
|
+
}
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
// Skip separator row
|
|
1326
|
+
if (/^\|[\s-]+\|/.test(line)) continue;
|
|
1327
|
+
// Data row
|
|
1328
|
+
const cells = line.split('|').map(c => c.trim());
|
|
1329
|
+
// cells[0] is '' before first pipe; real cells are cells[1..n-1]
|
|
1330
|
+
if (cells.length < 6) continue;
|
|
1331
|
+
rows.push({
|
|
1332
|
+
id: cells[1],
|
|
1333
|
+
description: cells[2],
|
|
1334
|
+
date: cells[3],
|
|
1335
|
+
commit: cells[4],
|
|
1336
|
+
status: cells[5],
|
|
1337
|
+
directory: cells[6] || '',
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Keep order as in file; take last `limit` (most recent appended last)
|
|
1342
|
+
return rows.slice(-limit);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Parse the product-level MILESTONES.md for shipped milestone headers.
|
|
1347
|
+
* Returns at most `limit` milestones in file order (top = most recent).
|
|
1348
|
+
*/
|
|
1349
|
+
function parseProductShippedMilestones(planningRoot, limit) {
|
|
1350
|
+
const mPath = path.join(planningRoot, 'MILESTONES.md');
|
|
1351
|
+
const content = safeReadFile(mPath);
|
|
1352
|
+
if (!content) return [];
|
|
1353
|
+
|
|
1354
|
+
const regex = /^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:\s+(\d{4}-\d{2}-\d{2})\)/gm;
|
|
1355
|
+
const entries = [];
|
|
1356
|
+
let m;
|
|
1357
|
+
while ((m = regex.exec(content)) !== null) {
|
|
1358
|
+
entries.push({ version: m[1], name: m[2].trim(), shipped_date: m[3] });
|
|
1359
|
+
if (entries.length >= limit) break;
|
|
1360
|
+
}
|
|
1361
|
+
return entries;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Parse milestone info from an arbitrary ROADMAP.md file (project-scoped).
|
|
1366
|
+
* Uses the same regexes as core.getMilestoneInfo but reads any path.
|
|
1367
|
+
* Returns { milestone_version, milestone_name } or both nulls if none found.
|
|
1368
|
+
*/
|
|
1369
|
+
function parseProjectMilestone(roadmapPath) {
|
|
1370
|
+
const roadmap = safeReadFile(roadmapPath);
|
|
1371
|
+
if (!roadmap) return { milestone_version: null, milestone_name: null };
|
|
1372
|
+
|
|
1373
|
+
// 🚧 marker: "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
|
|
1374
|
+
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
|
|
1375
|
+
if (inProgressMatch) {
|
|
1376
|
+
return { milestone_version: 'v' + inProgressMatch[1], milestone_name: inProgressMatch[2].trim() };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Bullet list: "- v19.0 Git Worktrees -- Phases 124-129 (in progress)"
|
|
1380
|
+
const bulletMatch = roadmap.match(/^- v(\d+\.\d+)\s+(.+?)\s+--\s+Phases\s+\S+\s+\(in progress\)/m);
|
|
1381
|
+
if (bulletMatch) {
|
|
1382
|
+
return { milestone_version: 'v' + bulletMatch[1], milestone_name: bulletMatch[2].trim() };
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Heading format (strip shipped <details> blocks)
|
|
1386
|
+
const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
|
1387
|
+
const headingMatch = cleaned.match(/#{2,3}\s+v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
|
1388
|
+
if (headingMatch) {
|
|
1389
|
+
return { milestone_version: 'v' + headingMatch[1], milestone_name: headingMatch[2].trim() };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return { milestone_version: null, milestone_name: null };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Count .md files in a directory (non-recursive). Returns 0 on any error
|
|
1397
|
+
* (missing dir, permissions, etc).
|
|
1398
|
+
*/
|
|
1399
|
+
function countMdFiles(dir, excludePredicate) {
|
|
1400
|
+
try {
|
|
1401
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1402
|
+
let count = 0;
|
|
1403
|
+
for (const e of entries) {
|
|
1404
|
+
if (!e.isFile()) continue;
|
|
1405
|
+
if (!e.name.endsWith('.md')) continue;
|
|
1406
|
+
if (excludePredicate && excludePredicate(e.name)) continue;
|
|
1407
|
+
count++;
|
|
1408
|
+
}
|
|
1409
|
+
return count;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return 0;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Count .md files in a flat directory that have a specific frontmatter status.
|
|
1417
|
+
* Used for ideas/todos/jobs that use frontmatter-based status instead of subdirectories.
|
|
1418
|
+
*/
|
|
1419
|
+
function countMdFilesByStatus(dir, status) {
|
|
1420
|
+
try {
|
|
1421
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1422
|
+
let count = 0;
|
|
1423
|
+
for (const e of entries) {
|
|
1424
|
+
if (!e.isFile()) continue;
|
|
1425
|
+
if (!e.name.endsWith('.md')) continue;
|
|
1426
|
+
if (e.name === 'manifest.json') continue;
|
|
1427
|
+
try {
|
|
1428
|
+
const content = fs.readFileSync(path.join(dir, e.name), 'utf-8');
|
|
1429
|
+
const statusMatch = content.match(/^---\n[\s\S]*?^status:\s*(.+)/m);
|
|
1430
|
+
if (statusMatch && statusMatch[1].trim() === status) count++;
|
|
1431
|
+
} catch { /* skip unreadable files */ }
|
|
1432
|
+
}
|
|
1433
|
+
return count;
|
|
1434
|
+
} catch {
|
|
1435
|
+
return 0;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function cmdInitProgressAll(cwd, raw) {
|
|
1440
|
+
const config = loadConfig(cwd);
|
|
1441
|
+
const planningRoot = getPlanningRoot(cwd);
|
|
1442
|
+
|
|
1443
|
+
// listProjectsReadonly handles ghost projects internally via warnings
|
|
1444
|
+
const { projects: allProjects, warnings } = listProjectsReadonly(cwd);
|
|
1445
|
+
|
|
1446
|
+
const activeProjects = allProjects.filter(p => !p.status.toLowerCase().includes('completed'));
|
|
1447
|
+
const completedProjects = allProjects.filter(p => p.status.toLowerCase().includes('completed'));
|
|
1448
|
+
|
|
1449
|
+
// Enrich each active project with milestone info from its ROADMAP.md
|
|
1450
|
+
const enrichedProjects = activeProjects.map(p => {
|
|
1451
|
+
const roadmapPath = path.join(planningRoot, 'projects', p.name, 'ROADMAP.md');
|
|
1452
|
+
const { milestone_version, milestone_name } = parseProjectMilestone(roadmapPath);
|
|
1453
|
+
return {
|
|
1454
|
+
name: p.name,
|
|
1455
|
+
status: p.status,
|
|
1456
|
+
current_phase: p.current_phase,
|
|
1457
|
+
progress: p.progress,
|
|
1458
|
+
repos_touched: p.repos_touched,
|
|
1459
|
+
milestone_version,
|
|
1460
|
+
milestone_name,
|
|
1461
|
+
};
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// Product-level aggregates
|
|
1465
|
+
const quickTasksRecent = parseProductQuickTasks(planningRoot, 5);
|
|
1466
|
+
const shippedMilestonesRecent = parseProductShippedMilestones(planningRoot, 5);
|
|
1467
|
+
|
|
1468
|
+
const backlog = {
|
|
1469
|
+
todos: countMdFilesByStatus(path.join(planningRoot, 'todos'), 'pending'),
|
|
1470
|
+
ideas: countMdFilesByStatus(path.join(planningRoot, 'ideas'), 'pending'),
|
|
1471
|
+
specs: countMdFiles(path.join(planningRoot, 'specs')),
|
|
1472
|
+
debug_active: countMdFiles(path.join(planningRoot, 'debug'), name => name.includes('resolved')),
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
const result = {
|
|
1476
|
+
planner_model: resolveModelInternal(cwd, 'dgs-planner'),
|
|
1477
|
+
executor_model: resolveModelInternal(cwd, 'dgs-executor'),
|
|
1478
|
+
commit_docs: config.commit_docs,
|
|
1479
|
+
sync_push: config.sync_push,
|
|
1480
|
+
sync_pull: config.sync_pull,
|
|
1481
|
+
cadence_pull: getCadence('progress').pull,
|
|
1482
|
+
cadence_push: getCadence('progress').push,
|
|
1483
|
+
|
|
1484
|
+
product: {
|
|
1485
|
+
product_name: config.product_name || null,
|
|
1486
|
+
active_count: activeProjects.length,
|
|
1487
|
+
completed_count: completedProjects.length,
|
|
1488
|
+
quick_tasks_recent: quickTasksRecent,
|
|
1489
|
+
shipped_milestones_recent: shippedMilestonesRecent,
|
|
1490
|
+
backlog,
|
|
1491
|
+
},
|
|
1492
|
+
|
|
1493
|
+
projects: enrichedProjects,
|
|
1494
|
+
warnings,
|
|
1495
|
+
|
|
1496
|
+
author: resolveAuthorSafe(cwd),
|
|
1497
|
+
dgs_mode: isV2Install(cwd) ? 'v2' : 'v1',
|
|
1498
|
+
planning_root: path.relative(cwd, planningRoot) || '.',
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
applySyncPull(cwd, 'progress', result);
|
|
1502
|
+
output(result, raw);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1239
1505
|
module.exports = {
|
|
1240
1506
|
cmdInitExecutePhase,
|
|
1241
1507
|
cmdInitPlanPhase,
|
|
@@ -1250,5 +1516,6 @@ module.exports = {
|
|
|
1250
1516
|
cmdInitMilestoneOp,
|
|
1251
1517
|
cmdInitMapCodebase,
|
|
1252
1518
|
cmdInitProgress,
|
|
1519
|
+
cmdInitProgressAll,
|
|
1253
1520
|
applySyncPull,
|
|
1254
1521
|
};
|