@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
|
@@ -72,7 +72,8 @@ function allocateId(cwd) {
|
|
|
72
72
|
|
|
73
73
|
// ─── Directory & File Helpers ─────────────────────────────────────────────────
|
|
74
74
|
|
|
75
|
-
const
|
|
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
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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[
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
error(`idea not found: ${idOrFilename}`);
|
|
763
|
-
}
|
|
905
|
+
try {
|
|
906
|
+
const result = setIdeaStatus(cwd, idOrFilename, targetState, author);
|
|
764
907
|
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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',
|
|
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
|
|
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
|
-
//
|
|
1120
|
-
|
|
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:
|
|
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',
|
|
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
|
-
//
|
|
1354
|
-
|
|
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.
|
|
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,
|