@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -72,7 +72,8 @@ function allocateId(cwd) {
72
72
 
73
73
  // ─── Directory & File Helpers ─────────────────────────────────────────────────
74
74
 
75
- const IDEA_STATES = ['pending', 'done', 'rejected', 'consolidated'];
75
+ const IDEA_STATUSES = ['pending', 'done', 'rejected', 'consolidated'];
76
+ const IDEA_STATES = IDEA_STATUSES;
76
77
 
77
78
  /**
78
79
  * Ensure ideas/{pending,done,rejected}/ directories exist.
@@ -208,6 +209,9 @@ function buildIdeaContent(frontmatter, body, notes, discussionLog, researchLog)
208
209
  let yaml = '---\n';
209
210
  yaml += `id: ${frontmatter.id}\n`;
210
211
  yaml += `title: "${frontmatter.title}"\n`;
212
+ if (frontmatter.status) {
213
+ yaml += `status: ${frontmatter.status}\n`;
214
+ }
211
215
  if (frontmatter.tags && frontmatter.tags.length > 0) {
212
216
  yaml += `tags: [${frontmatter.tags.map(t => `"${t}"`).join(', ')}]\n`;
213
217
  } else {
@@ -227,6 +231,22 @@ function buildIdeaContent(frontmatter, body, notes, discussionLog, researchLog)
227
231
  if (frontmatter.consolidated_into) {
228
232
  yaml += `consolidated_into: "${frontmatter.consolidated_into}"\n`;
229
233
  }
234
+ // Passthrough: preserve any unknown frontmatter fields (round-trip integrity)
235
+ const knownFields = new Set(['id', 'title', 'status', 'tags', 'created', 'updated', 'author', 'updated_by', 'consolidated_from', 'consolidated_into']);
236
+ for (const [key, value] of Object.entries(frontmatter)) {
237
+ if (knownFields.has(key)) continue;
238
+ if (value === null || value === undefined) continue;
239
+ if (Array.isArray(value)) {
240
+ yaml += `${key}: [${value.map(v => `"${v}"`).join(', ')}]\n`;
241
+ } else {
242
+ const sv = String(value);
243
+ if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
244
+ yaml += `${key}: "${sv}"\n`;
245
+ } else {
246
+ yaml += `${key}: ${sv}\n`;
247
+ }
248
+ }
249
+ }
230
250
  yaml += '---\n';
231
251
 
232
252
  // Body
@@ -270,11 +290,12 @@ function buildIdeaContent(frontmatter, body, notes, discussionLog, researchLog)
270
290
  }
271
291
 
272
292
  /**
273
- * Find an idea file by numeric ID or exact filename across all state directories.
293
+ * Find an idea file by numeric ID or exact filename.
294
+ * Scans flat ideas/ directory first (frontmatter status), then falls back to legacy subdirectories.
274
295
  *
275
296
  * @param {string} cwd - Working directory
276
297
  * @param {string} idOrFilename - Numeric ID (e.g., "42", "042") or filename
277
- * @returns {{ path: string, state: string, filename: string, frontmatter: object, body: string, notes: string, discussionLog: string, researchLog: string } | null}
298
+ * @returns {{ path: string, state: string, status: string, filename: string, frontmatter: object, body: string, notes: string, discussionLog: string, researchLog: string } | null}
278
299
  */
279
300
  function findIdeaFile(cwd, idOrFilename) {
280
301
  if (!idOrFilename) return null;
@@ -282,6 +303,24 @@ function findIdeaFile(cwd, idOrFilename) {
282
303
  const idStr = idOrFilename.replace(/^0+/, '') || '0';
283
304
  const paddedId = String(parseInt(idStr, 10)).padStart(3, '0');
284
305
 
306
+ // Flat-first scan: check ideas/ root directory
307
+ const flatDir = path.join(getPlanningRoot(cwd), 'ideas');
308
+ try {
309
+ const flatFiles = fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory());
310
+ for (const file of flatFiles) {
311
+ if (file.startsWith(paddedId + '-') || file === idOrFilename) {
312
+ const content = safeReadFile(path.join(flatDir, file));
313
+ if (!content) continue;
314
+ const { frontmatter, body, notes, discussionLog, researchLog } = parseIdeaFrontmatter(content);
315
+ const status = frontmatter.status || 'pending';
316
+ return { path: path.join(flatDir, file), state: status, status, filename: file, frontmatter, body, notes, discussionLog, researchLog };
317
+ }
318
+ }
319
+ } catch {
320
+ // Flat directory may not exist yet — continue to legacy scan
321
+ }
322
+
323
+ // Legacy subdirectory fallback
285
324
  for (const state of IDEA_STATES) {
286
325
  const dir = path.join(getPlanningRoot(cwd), 'ideas', state);
287
326
  let files;
@@ -293,18 +332,22 @@ function findIdeaFile(cwd, idOrFilename) {
293
332
 
294
333
  for (const file of files) {
295
334
  // Match by ID prefix
296
- if (file.startsWith(paddedId + '-')) {
335
+ if (file.startsWith(paddedId + '-') || file === idOrFilename) {
297
336
  const content = safeReadFile(path.join(dir, file));
298
337
  if (!content) continue;
299
338
  const { frontmatter, body, notes, discussionLog, researchLog } = parseIdeaFrontmatter(content);
300
- return { path: path.join(dir, file), state, filename: file, frontmatter, body, notes, discussionLog, researchLog };
301
- }
302
- // Match by exact filename
303
- if (file === idOrFilename) {
304
- const content = safeReadFile(path.join(dir, file));
305
- if (!content) continue;
306
- const { frontmatter, body, notes, discussionLog, researchLog } = parseIdeaFrontmatter(content);
307
- return { path: path.join(dir, file), state, filename: file, frontmatter, body, notes, discussionLog, researchLog };
339
+
340
+ // Legacy fallback warning
341
+ process.stderr.write(`[DGS] Warning: idea '${paddedId}' found in legacy ${state}/ directory. Run migration to flatten.\n`);
342
+
343
+ // Check frontmatter status vs directory disagreement
344
+ const fmStatus = frontmatter.status;
345
+ if (fmStatus && fmStatus !== state) {
346
+ process.stderr.write(`[DGS] Warning: idea '${paddedId}' frontmatter status '${fmStatus}' disagrees with directory '${state}'. Frontmatter wins.\n`);
347
+ }
348
+
349
+ const effectiveStatus = fmStatus || state;
350
+ return { path: path.join(dir, file), state: effectiveStatus, status: effectiveStatus, filename: file, frontmatter, body, notes, discussionLog, researchLog };
308
351
  }
309
352
  }
310
353
  }
@@ -329,14 +372,14 @@ function cmdIdeasCreate(cwd, title, body, tags, raw, author) {
329
372
  error('title required for ideas create');
330
373
  }
331
374
 
332
- ensureIdeasDirs(cwd);
375
+ const planRoot = getPlanningRoot(cwd);
376
+ fs.mkdirSync(path.join(planRoot, 'ideas'), { recursive: true });
333
377
 
334
378
  const id = allocateId(cwd);
335
379
  const slug = generateSlugInternal(title);
336
380
  const filename = `${id}-${slug}.md`;
337
- const planRoot = getPlanningRoot(cwd);
338
381
  const planRootRel = path.relative(cwd, planRoot) || '.';
339
- const filePath = path.join(planRoot, 'ideas', 'pending', filename);
382
+ const filePath = path.join(planRoot, 'ideas', filename);
340
383
  const now = new Date().toISOString();
341
384
 
342
385
  const parsedTags = tags
@@ -346,6 +389,7 @@ function cmdIdeasCreate(cwd, title, body, tags, raw, author) {
346
389
  const frontmatter = {
347
390
  id: parseInt(id, 10),
348
391
  title,
392
+ status: 'pending',
349
393
  tags: parsedTags,
350
394
  created: now,
351
395
  updated: now,
@@ -362,7 +406,7 @@ function cmdIdeasCreate(cwd, title, body, tags, raw, author) {
362
406
  const result = {
363
407
  id: parseInt(id, 10),
364
408
  filename,
365
- path: path.join(planRootRel, 'ideas', 'pending', filename),
409
+ path: path.join(planRootRel, 'ideas', filename),
366
410
  title,
367
411
  };
368
412
  output(result, raw);
@@ -491,7 +535,8 @@ function cmdIdeasAppendNote(cwd, idOrFilename, noteText, raw, author) {
491
535
  }
492
536
 
493
537
  /**
494
- * List ideas grouped by state with optional filtering.
538
+ * List ideas grouped by status with optional filtering.
539
+ * Scans flat ideas/ directory first (frontmatter status), then falls back to legacy subdirectories.
495
540
  *
496
541
  * @param {string} cwd - Working directory
497
542
  * @param {{ state: string|null, tag: string|null, include_orphan_check: boolean, include_consolidated: boolean, show_all: boolean }} options
@@ -502,7 +547,10 @@ function cmdIdeasList(cwd, options, raw) {
502
547
  const counts = { pending: 0, done: 0, rejected: 0, consolidated: 0, total: 0 };
503
548
  const orphanedIds = [];
504
549
 
505
- // Determine which states to iterate
550
+ // Track IDs seen in flat directory to avoid duplicates from legacy fallback
551
+ const seenIds = new Set();
552
+
553
+ // Determine which states to iterate (for legacy fallback and status filter)
506
554
  let statesToList;
507
555
  if (options.state) {
508
556
  statesToList = [options.state];
@@ -514,6 +562,64 @@ function cmdIdeasList(cwd, options, raw) {
514
562
  statesToList = ['pending', 'done', 'rejected']; // default: hide consolidated
515
563
  }
516
564
 
565
+ // Flat-first scan: check ideas/ root directory
566
+ const flatDir = path.join(getPlanningRoot(cwd), 'ideas');
567
+ try {
568
+ const flatFiles = fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory());
569
+ for (const file of flatFiles) {
570
+ const content = safeReadFile(path.join(flatDir, file));
571
+ if (!content) continue;
572
+ const { frontmatter, discussionLog, researchLog } = parseIdeaFrontmatter(content);
573
+ const status = frontmatter.status || 'pending';
574
+
575
+ // Apply status filter (using existing --state flag for backward compat)
576
+ if (options.state && status !== options.state) continue;
577
+ if (!options.state && options.include_consolidated && status !== 'consolidated') continue;
578
+ if (!options.state && !options.include_consolidated && !options.show_all && status === 'consolidated') continue;
579
+ if (!options.state && !options.show_all && !options.include_consolidated && !['pending', 'done', 'rejected'].includes(status)) continue;
580
+
581
+ // Tag filter (case-insensitive)
582
+ if (options.tag) {
583
+ const tagLower = options.tag.toLowerCase();
584
+ const tags = (frontmatter.tags || []).map(t => t.toLowerCase());
585
+ if (!tags.includes(tagLower)) continue;
586
+ }
587
+
588
+ // Author filter (case-insensitive substring match on name-only)
589
+ const authorName = extractNameFromAuthor(frontmatter.author || '');
590
+ if (options.author) {
591
+ const filterAuthor = options.author.toLowerCase();
592
+ if (!authorName.toLowerCase().includes(filterAuthor)) continue;
593
+ }
594
+
595
+ const ideaObj = {
596
+ id: frontmatter.id,
597
+ title: frontmatter.title || '',
598
+ tags: frontmatter.tags || [],
599
+ created: frontmatter.created || '',
600
+ updated: frontmatter.updated || '',
601
+ author: authorName,
602
+ state: status, // backward compat
603
+ status, // new preferred field
604
+ filename: file,
605
+ discussed: !!(discussionLog && discussionLog.trim().length > 0),
606
+ researched: !!(researchLog && researchLog.trim().length > 0),
607
+ };
608
+
609
+ if (status === 'consolidated' && frontmatter.consolidated_into) {
610
+ ideaObj.consolidated_into = frontmatter.consolidated_into;
611
+ }
612
+
613
+ ideas.push(ideaObj);
614
+ if (counts[status] !== undefined) counts[status]++;
615
+ counts.total++;
616
+ seenIds.add(frontmatter.id);
617
+ }
618
+ } catch {
619
+ // Flat directory may not exist — continue to legacy scan
620
+ }
621
+
622
+ // Legacy subdirectory fallback
517
623
  for (const state of statesToList) {
518
624
 
519
625
  const dir = path.join(getPlanningRoot(cwd), 'ideas', state);
@@ -530,6 +636,14 @@ function cmdIdeasList(cwd, options, raw) {
530
636
 
531
637
  const { frontmatter, discussionLog, researchLog } = parseIdeaFrontmatter(content);
532
638
 
639
+ // Skip if already found in flat directory
640
+ if (seenIds.has(frontmatter.id)) continue;
641
+
642
+ // Legacy fallback warning
643
+ process.stderr.write(`[DGS] Warning: idea '${file}' found in legacy ${state}/ directory. Run migration to flatten.\n`);
644
+
645
+ const effectiveStatus = frontmatter.status || state;
646
+
533
647
  // Tag filter (case-insensitive)
534
648
  if (options.tag) {
535
649
  const tagLower = options.tag.toLowerCase();
@@ -551,26 +665,28 @@ function cmdIdeasList(cwd, options, raw) {
551
665
  created: frontmatter.created || '',
552
666
  updated: frontmatter.updated || '',
553
667
  author: authorName,
554
- state,
668
+ state: effectiveStatus, // backward compat
669
+ status: effectiveStatus, // new preferred field
555
670
  filename: file,
556
671
  discussed: !!(discussionLog && discussionLog.trim().length > 0),
557
672
  researched: !!(researchLog && researchLog.trim().length > 0),
558
673
  };
559
674
 
560
- if (state === 'consolidated' && frontmatter.consolidated_into) {
675
+ if (effectiveStatus === 'consolidated' && frontmatter.consolidated_into) {
561
676
  ideaObj.consolidated_into = frontmatter.consolidated_into;
562
677
  }
563
678
 
564
679
  ideas.push(ideaObj);
565
680
 
566
- counts[state]++;
681
+ if (counts[effectiveStatus] !== undefined) counts[effectiveStatus]++;
567
682
  counts.total++;
683
+ seenIds.add(frontmatter.id);
568
684
  }
569
685
  }
570
686
 
571
687
  // Orphan check: scan specs for references to done idea IDs
572
688
  if (options.include_orphan_check) {
573
- const doneIdeas = ideas.filter(i => i.state === 'done');
689
+ const doneIdeas = ideas.filter(i => i.status === 'done');
574
690
  const specsDir = path.join(getPlanningRoot(cwd), 'specs');
575
691
 
576
692
  let specContent = '';
@@ -649,13 +765,8 @@ function cmdIdeasReject(cwd, idOrFilename, reason, raw, author) {
649
765
  const rebuilt = buildIdeaContent(fm, bd, updatedNotes, dl, rl);
650
766
  fs.writeFileSync(idea.path, rebuilt, 'utf-8');
651
767
 
652
- ensureIdeasDirs(cwd);
653
-
654
- // Git mv from pending to rejected
655
- const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
656
- const fromRel = path.join(planRootRel, 'ideas', 'pending', idea.filename);
657
- const toRel = path.join(planRootRel, 'ideas', 'rejected', idea.filename);
658
- execGit(cwd, ['mv', fromRel, toRel]);
768
+ // Set status to rejected via frontmatter edit
769
+ setIdeaStatus(cwd, idOrFilename, 'rejected');
659
770
 
660
771
  // Auto-commit
661
772
  execGit(cwd, ['add', '-A']);
@@ -714,13 +825,8 @@ function cmdIdeasRestore(cwd, idOrFilename, raw, author) {
714
825
  fs.writeFileSync(idea.path, buildIdeaContent(fm, bd, nt, dl, rl), 'utf-8');
715
826
  }
716
827
 
717
- ensureIdeasDirs(cwd);
718
-
719
- // Git mv to pending (keep rejection reason in file as history)
720
- const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
721
- const fromRel = path.join(planRootRel, 'ideas', idea.state, idea.filename);
722
- const toRel = path.join(planRootRel, 'ideas', 'pending', idea.filename);
723
- execGit(cwd, ['mv', fromRel, toRel]);
828
+ // Set status to pending via frontmatter edit
829
+ setIdeaStatus(cwd, idOrFilename, 'pending');
724
830
 
725
831
  // Auto-commit
726
832
  execGit(cwd, ['add', '-A']);
@@ -737,6 +843,48 @@ function cmdIdeasRestore(cwd, idOrFilename, raw, author) {
737
843
  output(result, raw);
738
844
  }
739
845
 
846
+ /**
847
+ * Set an idea's frontmatter status field (internal helper — throws on error).
848
+ *
849
+ * This is the foundation function that Phase 133's cmdSpecsFinalize will call
850
+ * instead of using git mv. It edits frontmatter in-place without moving the file.
851
+ *
852
+ * @param {string} cwd - Working directory
853
+ * @param {string} idOrFilename - Idea ID or filename
854
+ * @param {string} status - Target status (must be in IDEA_STATUSES)
855
+ * @param {string} [author] - Author string for updated_by
856
+ * @returns {{ id: number, filename: string, previous_status: string, status: string }}
857
+ * @throws {Error} If status is invalid, idea not found, or already in target status
858
+ */
859
+ function setIdeaStatus(cwd, idOrFilename, status, author) {
860
+ if (!IDEA_STATUSES.includes(status)) {
861
+ throw new Error(`Invalid status: ${status}. Allowed: ${IDEA_STATUSES.join(', ')}`);
862
+ }
863
+
864
+ const idea = findIdeaFile(cwd, idOrFilename);
865
+ if (!idea) {
866
+ throw new Error(`Idea not found: ${idOrFilename}`);
867
+ }
868
+
869
+ const previousStatus = idea.frontmatter.status || idea.state;
870
+
871
+ // Update frontmatter
872
+ idea.frontmatter.status = status;
873
+ idea.frontmatter.updated = new Date().toISOString();
874
+ if (author) {
875
+ idea.frontmatter.updated_by = author;
876
+ }
877
+
878
+ // Write back using buildIdeaContent (preserves all fields)
879
+ const content = buildIdeaContent(
880
+ idea.frontmatter, idea.body, idea.notes,
881
+ idea.discussionLog, idea.researchLog
882
+ );
883
+ fs.writeFileSync(idea.path, content, 'utf-8');
884
+
885
+ return { id: idea.frontmatter.id, filename: idea.filename, previous_status: previousStatus, status };
886
+ }
887
+
740
888
  /**
741
889
  * Move an idea to a target state (general-purpose state transition).
742
890
  *
@@ -753,49 +901,18 @@ function cmdIdeasMoveState(cwd, idOrFilename, targetState, raw, author) {
753
901
  if (!targetState) {
754
902
  error('target state required for ideas move-state');
755
903
  }
756
- if (!IDEA_STATES.includes(targetState)) {
757
- error(`invalid state: ${targetState}. Use ${IDEA_STATES.join(', ')}`);
758
- }
759
904
 
760
- const idea = findIdeaFile(cwd, idOrFilename);
761
- if (!idea) {
762
- error(`idea not found: ${idOrFilename}`);
763
- }
905
+ try {
906
+ const result = setIdeaStatus(cwd, idOrFilename, targetState, author);
764
907
 
765
- if (idea.state === targetState) {
766
- error(`idea ${idOrFilename} is already in ${targetState}`);
767
- }
908
+ // Git add + commit (replaces the old git mv + commit)
909
+ execGit(cwd, ['add', '-A']);
910
+ execGit(cwd, ['commit', '-m', `ideas: set #${result.id} status to ${targetState}`]);
768
911
 
769
- // Set updated_by if author provided
770
- if (author) {
771
- const currentContent = safeReadFile(idea.path) || '';
772
- const { frontmatter: fm, body: bd, notes: nt, discussionLog: dl, researchLog: rl } = parseIdeaFrontmatter(currentContent);
773
- fm.updated_by = author;
774
- fm.updated = new Date().toISOString();
775
- fs.writeFileSync(idea.path, buildIdeaContent(fm, bd, nt, dl, rl), 'utf-8');
912
+ output(result, raw);
913
+ } catch (err) {
914
+ error(err.message);
776
915
  }
777
-
778
- ensureIdeasDirs(cwd);
779
-
780
- // Git mv
781
- const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
782
- const fromRel = path.join(planRootRel, 'ideas', idea.state, idea.filename);
783
- const toRel = path.join(planRootRel, 'ideas', targetState, idea.filename);
784
- execGit(cwd, ['mv', fromRel, toRel]);
785
-
786
- // Auto-commit
787
- execGit(cwd, ['add', '-A']);
788
- const title = idea.frontmatter.title || idea.filename;
789
- const idNum = idea.frontmatter.id || idOrFilename;
790
- execGit(cwd, ['commit', '-m', `ideas: move #${idNum} - ${title} to ${targetState}`]);
791
-
792
- const result = {
793
- id: idea.frontmatter.id,
794
- filename: idea.filename,
795
- from: idea.state,
796
- to: targetState,
797
- };
798
- output(result, raw);
799
916
  }
800
917
 
801
918
  // ─── Section Entry Helpers ────────────────────────────────────────────────────
@@ -1061,8 +1178,6 @@ function cmdIdeasConsolidate(cwd, options, raw, author) {
1061
1178
  sourceIdeas.push(idea);
1062
1179
  }
1063
1180
 
1064
- ensureIdeasDirs(cwd);
1065
-
1066
1181
  // Collect tags: union of all source tags + provided tags (deduplicated)
1067
1182
  const allTags = new Set();
1068
1183
  for (const idea of sourceIdeas) {
@@ -1080,7 +1195,8 @@ function cmdIdeasConsolidate(cwd, options, raw, author) {
1080
1195
  const filename = `${newId}-${slug}.md`;
1081
1196
  const planRoot = getPlanningRoot(cwd);
1082
1197
  const planRootRel = path.relative(cwd, planRoot) || '.';
1083
- const filePath = path.join(planRoot, 'ideas', 'pending', filename);
1198
+ const filePath = path.join(planRoot, 'ideas', filename);
1199
+ fs.mkdirSync(path.join(planRoot, 'ideas'), { recursive: true });
1084
1200
  const now = new Date().toISOString();
1085
1201
  const dateStr = now.split('T')[0];
1086
1202
 
@@ -1088,6 +1204,7 @@ function cmdIdeasConsolidate(cwd, options, raw, author) {
1088
1204
  const frontmatter = {
1089
1205
  id: parseInt(newId, 10),
1090
1206
  title,
1207
+ status: 'pending',
1091
1208
  tags: [...allTags],
1092
1209
  created: now,
1093
1210
  updated: now,
@@ -1098,7 +1215,7 @@ function cmdIdeasConsolidate(cwd, options, raw, author) {
1098
1215
  }
1099
1216
  frontmatter.consolidated_from = sourceIdeas.map(i => String(i.frontmatter.id).padStart(3, '0'));
1100
1217
 
1101
- // Write new consolidated idea to pending/
1218
+ // Write new consolidated idea to flat ideas/
1102
1219
  const content = buildIdeaContent(frontmatter, body || '', '', discussion || '', research || '');
1103
1220
  fs.writeFileSync(filePath, content, 'utf-8');
1104
1221
 
@@ -1116,15 +1233,13 @@ function cmdIdeasConsolidate(cwd, options, raw, author) {
1116
1233
  // Append consolidation note
1117
1234
  appendConsolidationNote(idea.path, newId, title, dateStr);
1118
1235
 
1119
- // Git mv source from pending/ to consolidated/
1120
- const fromRel = path.join(planRootRel, 'ideas', 'pending', idea.filename);
1121
- const toRel = path.join(planRootRel, 'ideas', 'consolidated', idea.filename);
1122
- execGit(cwd, ['mv', fromRel, toRel]);
1236
+ // Set status to consolidated via frontmatter edit
1237
+ setIdeaStatus(cwd, String(idea.frontmatter.id), 'consolidated');
1123
1238
 
1124
1239
  movedFiles.push({
1125
1240
  id: idea.frontmatter.id,
1126
1241
  filename: idea.filename,
1127
- from: 'pending',
1242
+ from: idea.state,
1128
1243
  to: 'consolidated',
1129
1244
  });
1130
1245
  }
@@ -1138,7 +1253,7 @@ function cmdIdeasConsolidate(cwd, options, raw, author) {
1138
1253
  const result = {
1139
1254
  id: parseInt(newId, 10),
1140
1255
  filename,
1141
- path: path.join(planRootRel, 'ideas', 'pending', filename),
1256
+ path: path.join(planRootRel, 'ideas', filename),
1142
1257
  title,
1143
1258
  consolidated_from: frontmatter.consolidated_from,
1144
1259
  moved_files: movedFiles,
@@ -1350,16 +1465,14 @@ function cmdIdeasUndoConsolidation(cwd, idOrFilename, raw, author) {
1350
1465
  if (author) parsed.frontmatter.updated_by = author;
1351
1466
  fs.writeFileSync(sourceIdea.path, buildIdeaContent(parsed.frontmatter, parsed.body, parsed.notes, parsed.discussionLog, parsed.researchLog), 'utf-8');
1352
1467
 
1353
- // Git mv from consolidated/ to pending/
1354
- const fromRel = path.join(planRootRel, 'ideas', 'consolidated', sourceIdea.filename);
1355
- const toRel = path.join(planRootRel, 'ideas', 'pending', sourceIdea.filename);
1356
- execGit(cwd, ['mv', fromRel, toRel]);
1468
+ // Set status to pending via frontmatter edit
1469
+ setIdeaStatus(cwd, sourceId, 'pending');
1357
1470
 
1358
1471
  restoredIds.push(parseInt(sourceId, 10));
1359
1472
  }
1360
1473
 
1361
- // Delete the consolidated result idea
1362
- const ideaRel = path.join(planRootRel, 'ideas', idea.state, idea.filename);
1474
+ // Delete the consolidated result idea (flat directory — no state subdir)
1475
+ const ideaRel = path.join(planRootRel, 'ideas', idea.filename);
1363
1476
  execGit(cwd, ['rm', ideaRel]);
1364
1477
 
1365
1478
  // Atomic git commit
@@ -1380,7 +1493,9 @@ function cmdIdeasUndoConsolidation(cwd, idOrFilename, raw, author) {
1380
1493
  // ─── Exports ──────────────────────────────────────────────────────────────────
1381
1494
 
1382
1495
  module.exports = {
1496
+ IDEA_STATUSES,
1383
1497
  IDEA_STATES,
1498
+ setIdeaStatus,
1384
1499
  loadManifest,
1385
1500
  saveManifest,
1386
1501
  allocateId,