@ktpartners/dgs-platform 3.3.0 → 3.4.1

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -1
  3. package/bin/install.js +1 -1
  4. package/commands/dgs/abandon-milestone.md +28 -0
  5. package/commands/dgs/new-milestone.md +3 -1
  6. package/deliver-great-systems/bin/dgs-tools.cjs +22 -4
  7. package/deliver-great-systems/bin/lib/context.cjs +45 -15
  8. package/deliver-great-systems/bin/lib/core.cjs +2 -6
  9. package/deliver-great-systems/bin/lib/docs.cjs +95 -41
  10. package/deliver-great-systems/bin/lib/docs.test.cjs +49 -6
  11. package/deliver-great-systems/bin/lib/init.cjs +60 -13
  12. package/deliver-great-systems/bin/lib/init.test.cjs +61 -3
  13. package/deliver-great-systems/bin/lib/milestone.cjs +470 -2
  14. package/deliver-great-systems/bin/lib/milestone.test.cjs +653 -0
  15. package/deliver-great-systems/bin/lib/search.cjs +5 -16
  16. package/deliver-great-systems/bin/lib/state.cjs +152 -1
  17. package/deliver-great-systems/bin/lib/sync.cjs +2 -6
  18. package/deliver-great-systems/bin/lib/verify.cjs +2 -1
  19. package/deliver-great-systems/bin/lib/worktrees.cjs +182 -1
  20. package/deliver-great-systems/bin/lib/worktrees.test.cjs +409 -0
  21. package/deliver-great-systems/templates/claude-md.md +2 -0
  22. package/deliver-great-systems/templates/state.md +16 -0
  23. package/deliver-great-systems/workflows/abandon-milestone.md +120 -0
  24. package/deliver-great-systems/workflows/complete-milestone.md +58 -4
  25. package/deliver-great-systems/workflows/create-milestone-job.md +15 -0
  26. package/deliver-great-systems/workflows/help.md +7 -0
  27. package/deliver-great-systems/workflows/new-milestone.md +69 -0
  28. package/deliver-great-systems/workflows/progress.md +5 -1
  29. package/deliver-great-systems/workflows/run-job.md +23 -1
  30. package/hooks/dist/dgs-enforce-discipline.js +34 -1
  31. package/package.json +1 -1
@@ -12,6 +12,7 @@ const { parseReposMd, validateReposMdEager } = require('./repos.cjs');
12
12
  const { getCadence, pullAll } = require('./sync.cjs');
13
13
  const { detectQuickMode, generateQuickId, getActiveQuick } = require('./quick.cjs');
14
14
  const { listProjectsReadonly } = require('./projects.cjs');
15
+ const { resolveMilestoneSlug } = require('./worktrees.cjs');
15
16
 
16
17
  /**
17
18
  * Safely resolve the current git author string.
@@ -88,6 +89,54 @@ function applySyncPull(cwd, workflowName, result) {
88
89
  result.needs_pull = false;
89
90
  }
90
91
 
92
+ // ─── Brownfield detection ───────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Detect whether the directory tree rooted at cwd contains any source
96
+ * files in a small set of common languages, ignoring node_modules and .git,
97
+ * with a maximum recursion depth of 3 (cwd itself is depth 0).
98
+ *
99
+ * Returns true on first match (early termination).
100
+ * Returns false on any error (e.g. permission denied on top-level dir).
101
+ *
102
+ * Pure Node — replaces the prior `find ... | grep ... | head -5` shell
103
+ * pipeline that does not exist on Windows.
104
+ *
105
+ * @param {string} cwd - Directory to scan.
106
+ * @returns {boolean}
107
+ */
108
+ function hasCodeFiles(cwd) {
109
+ const CODE_EXTENSIONS = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']);
110
+ const SKIP_DIRS = new Set(['node_modules', '.git']);
111
+ const MAX_DEPTH = 3;
112
+
113
+ function walk(dir, depth) {
114
+ if (depth > MAX_DEPTH) return false;
115
+ let entries;
116
+ try {
117
+ entries = fs.readdirSync(dir, { withFileTypes: true });
118
+ } catch {
119
+ return false;
120
+ }
121
+ for (const entry of entries) {
122
+ if (entry.isDirectory()) {
123
+ if (SKIP_DIRS.has(entry.name)) continue;
124
+ if (walk(path.join(dir, entry.name), depth + 1)) return true;
125
+ } else if (entry.isFile()) {
126
+ const ext = path.extname(entry.name).toLowerCase();
127
+ if (CODE_EXTENSIONS.has(ext)) return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+
133
+ try {
134
+ return walk(cwd, 0);
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
91
140
  // ─── v2 Project Context Resolution ──────────────────────────────────────────
92
141
 
93
142
  /**
@@ -204,6 +253,10 @@ function cmdInitExecutePhase(cwd, phase, raw) {
204
253
  : null;
205
254
  const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
206
255
 
256
+ // Multi-project determination for the structural milestone slug's project
257
+ // prefix: only prefix with <project> when more than one ACTIVE project exists.
258
+ const multiProject = listProjectsReadonly(cwd).projects.length > 1;
259
+
207
260
  // Resolve {project} in a branch template — errors if template uses {project} but current_project is not set
208
261
  function resolveProjectInTemplate(template, currentProject) {
209
262
  if (template.includes('{project}')) {
@@ -253,13 +306,13 @@ function cmdInitExecutePhase(cwd, phase, raw) {
253
306
  if (resolved.error) return resolved.error;
254
307
  return resolved.value
255
308
  .replace('{milestone}', milestone.version)
256
- .replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
309
+ .replace('{slug}', resolveMilestoneSlug(cwd, ctx.current_project, { version: milestone.version, name: milestone.name, multiProject }));
257
310
  })(),
258
311
 
259
312
  // Milestone info
260
313
  milestone_version: milestone.version,
261
314
  milestone_name: milestone.name,
262
- milestone_slug: generateSlugInternal(milestone.name),
315
+ milestone_slug: resolveMilestoneSlug(cwd, ctx.current_project, { version: milestone.version, name: milestone.name, multiProject }),
263
316
 
264
317
  // File existence
265
318
  state_exists: ctx.root ? pathExistsInternal(cwd, path.join(ctx.root, 'STATE.md')) : false,
@@ -411,17 +464,9 @@ function cmdInitNewProject(cwd, slugArg, raw) {
411
464
  const braveKeyFile = path.join(homedir, '.dgs', 'brave_api_key');
412
465
  const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
413
466
 
414
- // Detect existing code
415
- let hasCode = false;
467
+ // Detect existing code (pure-Node walk, Windows-safe)
468
+ const hasCode = hasCodeFiles(cwd);
416
469
  let hasPackageFile = false;
417
- try {
418
- const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
419
- cwd,
420
- encoding: 'utf-8',
421
- stdio: ['pipe', 'pipe', 'pipe'],
422
- });
423
- hasCode = files.trim().length > 0;
424
- } catch {}
425
470
 
426
471
  hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
427
472
  pathExistsInternal(cwd, 'requirements.txt') ||
@@ -1005,6 +1050,7 @@ function cmdInitMilestoneOp(cwd, raw, workflow) {
1005
1050
  const milestone = getMilestoneInfo(cwd);
1006
1051
  const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
1007
1052
  const cadence = getCadence(workflow || 'complete-milestone');
1053
+ const multiProject = listProjectsReadonly(cwd).projects.length > 1;
1008
1054
 
1009
1055
  // Count phases (project-qualified)
1010
1056
  let phaseCount = 0;
@@ -1049,7 +1095,7 @@ function cmdInitMilestoneOp(cwd, raw, workflow) {
1049
1095
  // Current milestone
1050
1096
  milestone_version: milestone.version,
1051
1097
  milestone_name: milestone.name,
1052
- milestone_slug: generateSlugInternal(milestone.name),
1098
+ milestone_slug: resolveMilestoneSlug(cwd, ctx.current_project, { version: milestone.version, name: milestone.name, multiProject }),
1053
1099
 
1054
1100
  // Phase counts
1055
1101
  phase_count: phaseCount,
@@ -1547,4 +1593,5 @@ module.exports = {
1547
1593
  cmdInitProgress,
1548
1594
  cmdInitProgressAll,
1549
1595
  applySyncPull,
1596
+ hasCodeFiles,
1550
1597
  };
@@ -1177,7 +1177,10 @@ describe('branch_name with {project} resolution', () => {
1177
1177
  });
1178
1178
  try {
1179
1179
  const result = runInit(fixture.cwd, 'execute-phase 3');
1180
- assert.equal(result.branch_name, 'dgs/myapp/v1.0-milestone');
1180
+ // Structural slug now embeds the version segment (v1.0 -> v1-0) so a
1181
+ // placeholder/default 'milestone' name no longer collapses to a bare
1182
+ // milestone/milestone branch (branch-name collision fix).
1183
+ assert.equal(result.branch_name, 'dgs/myapp/v1.0-v1-0-milestone');
1181
1184
  } finally {
1182
1185
  fixture.cleanup();
1183
1186
  }
@@ -1196,7 +1199,7 @@ describe('branch_name with {project} resolution', () => {
1196
1199
  });
1197
1200
  try {
1198
1201
  const result = runInit(fixture.cwd, 'execute-phase 1');
1199
- assert.equal(result.branch_name, 'dgs/checkout/v1.0-milestone');
1202
+ assert.equal(result.branch_name, 'dgs/checkout/v1.0-v1-0-milestone');
1200
1203
  } finally {
1201
1204
  fixture.cleanup();
1202
1205
  }
@@ -1216,7 +1219,7 @@ describe('branch_name with {project} resolution', () => {
1216
1219
  });
1217
1220
  try {
1218
1221
  const result = runInit(fixture.cwd, 'execute-phase 3');
1219
- assert.equal(result.branch_name, 'dgs/myapp/v1.0-milestone');
1222
+ assert.equal(result.branch_name, 'dgs/myapp/v1.0-v1-0-milestone');
1220
1223
  } finally {
1221
1224
  fixture.cleanup();
1222
1225
  }
@@ -1240,6 +1243,61 @@ describe('branch_name with {project} resolution', () => {
1240
1243
  });
1241
1244
  });
1242
1245
 
1246
+ // ─── milestone_slug resolution (stamped / legacy fallback) ───────────────────
1247
+
1248
+ describe('milestone_slug resolution via resolveMilestoneSlug', () => {
1249
+ // ROADMAP carries an in-progress marker so version+name are deterministic:
1250
+ // 'v25.0' + 'Cool Thing' -> composed slug 'v25-0-cool-thing';
1251
+ // old bare-name slug 'cool-thing'.
1252
+ const roadmap = '# Roadmap\n\n- 🚧 **v25.0 Cool Thing** — Phases 1-2 (in progress)\n';
1253
+
1254
+ function fixtureWithWorktrees(worktrees) {
1255
+ return createFixture({
1256
+ 'config.json': JSON.stringify({ current_project: 'test-project' }),
1257
+ 'config.local.json': JSON.stringify({
1258
+ current_project: 'test-project',
1259
+ projects: { 'test-project': { worktrees } },
1260
+ }),
1261
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
1262
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
1263
+ 'projects/test-project/STATE.md': '# State',
1264
+ 'projects/test-project/ROADMAP.md': roadmap,
1265
+ 'projects/test-project/REQUIREMENTS.md': '# Requirements',
1266
+ 'projects/test-project/PROJECT.md': '# Project',
1267
+ });
1268
+ }
1269
+
1270
+ it('returns the stamped NEW composed slug when an entry exists under it', () => {
1271
+ const fixture = fixtureWithWorktrees({ 'v25-0-cool-thing': { type: 'milestone', milestone_slug: 'v25-0-cool-thing', slug_formula: 'v2' } });
1272
+ try {
1273
+ const result = runInit(fixture.cwd, 'milestone-op');
1274
+ assert.equal(result.milestone_slug, 'v25-0-cool-thing');
1275
+ } finally {
1276
+ fixture.cleanup();
1277
+ }
1278
+ });
1279
+
1280
+ it('LEGACY FALLBACK: returns the OLD bare-name slug when only an old-style entry exists (no orphaning)', () => {
1281
+ const fixture = fixtureWithWorktrees({ 'cool-thing': { type: 'milestone' } });
1282
+ try {
1283
+ const result = runInit(fixture.cwd, 'milestone-op');
1284
+ assert.equal(result.milestone_slug, 'cool-thing', 'pre-upgrade in-flight milestone must still resolve');
1285
+ } finally {
1286
+ fixture.cleanup();
1287
+ }
1288
+ });
1289
+
1290
+ it('returns the NEW composed slug when neither entry exists (fresh)', () => {
1291
+ const fixture = fixtureWithWorktrees({});
1292
+ try {
1293
+ const result = runInit(fixture.cwd, 'milestone-op');
1294
+ assert.equal(result.milestone_slug, 'v25-0-cool-thing');
1295
+ } finally {
1296
+ fixture.cleanup();
1297
+ }
1298
+ });
1299
+ });
1300
+
1243
1301
  // ─── Backward Compatibility ──────────────────────────────────────────────────
1244
1302
 
1245
1303
  // ─── Sync Pull: needs_pull field tests ────────────────────────────────────────