gsd-opencode 1.33.2 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-advisor-researcher.md +23 -0
- package/agents/gsd-ai-researcher.md +142 -0
- package/agents/gsd-code-fixer.md +523 -0
- package/agents/gsd-code-reviewer.md +361 -0
- package/agents/gsd-debugger.md +14 -1
- package/agents/gsd-domain-researcher.md +162 -0
- package/agents/gsd-eval-auditor.md +170 -0
- package/agents/gsd-eval-planner.md +161 -0
- package/agents/gsd-executor.md +70 -7
- package/agents/gsd-framework-selector.md +167 -0
- package/agents/gsd-intel-updater.md +320 -0
- package/agents/gsd-phase-researcher.md +26 -0
- package/agents/gsd-plan-checker.md +12 -0
- package/agents/gsd-planner.md +16 -6
- package/agents/gsd-project-researcher.md +23 -0
- package/agents/gsd-ui-researcher.md +23 -0
- package/agents/gsd-verifier.md +55 -1
- package/commands/gsd/gsd-add-backlog.md +1 -1
- package/commands/gsd/gsd-add-phase.md +1 -1
- package/commands/gsd/gsd-add-todo.md +1 -1
- package/commands/gsd/gsd-ai-integration-phase.md +36 -0
- package/commands/gsd/gsd-audit-fix.md +33 -0
- package/commands/gsd/gsd-autonomous.md +1 -0
- package/commands/gsd/gsd-check-todos.md +1 -1
- package/commands/gsd/gsd-code-review-fix.md +52 -0
- package/commands/gsd/gsd-code-review.md +55 -0
- package/commands/gsd/gsd-complete-milestone.md +1 -1
- package/commands/gsd/gsd-debug.md +1 -1
- package/commands/gsd/gsd-eval-review.md +32 -0
- package/commands/gsd/gsd-explore.md +27 -0
- package/commands/gsd/gsd-from-gsd2.md +45 -0
- package/commands/gsd/gsd-health.md +1 -1
- package/commands/gsd/gsd-import.md +36 -0
- package/commands/gsd/gsd-insert-phase.md +1 -1
- package/commands/gsd/gsd-intel.md +183 -0
- package/commands/gsd/gsd-manager.md +1 -1
- package/commands/gsd/gsd-next.md +2 -0
- package/commands/gsd/gsd-reapply-patches.md +58 -3
- package/commands/gsd/gsd-remove-phase.md +1 -1
- package/commands/gsd/gsd-review.md +4 -2
- package/commands/gsd/gsd-scan.md +26 -0
- package/commands/gsd/gsd-set-profile.md +1 -1
- package/commands/gsd/gsd-thread.md +1 -1
- package/commands/gsd/gsd-undo.md +34 -0
- package/commands/gsd/gsd-workstreams.md +6 -6
- package/get-shit-done/bin/gsd-tools.cjs +143 -5
- package/get-shit-done/bin/lib/commands.cjs +10 -2
- package/get-shit-done/bin/lib/config.cjs +71 -37
- package/get-shit-done/bin/lib/core.cjs +70 -8
- package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
- package/get-shit-done/bin/lib/init.cjs +20 -6
- package/get-shit-done/bin/lib/intel.cjs +660 -0
- package/get-shit-done/bin/lib/learnings.cjs +378 -0
- package/get-shit-done/bin/lib/milestone.cjs +25 -15
- package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
- package/get-shit-done/bin/lib/phase.cjs +148 -112
- package/get-shit-done/bin/lib/roadmap.cjs +12 -5
- package/get-shit-done/bin/lib/security.cjs +119 -0
- package/get-shit-done/bin/lib/state.cjs +283 -221
- package/get-shit-done/bin/lib/template.cjs +8 -4
- package/get-shit-done/bin/lib/verify.cjs +42 -5
- package/get-shit-done/references/ai-evals.md +156 -0
- package/get-shit-done/references/ai-frameworks.md +186 -0
- package/get-shit-done/references/common-bug-patterns.md +114 -0
- package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
- package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
- package/get-shit-done/references/gates.md +70 -0
- package/get-shit-done/references/ios-scaffold.md +123 -0
- package/get-shit-done/references/model-profile-resolution.md +6 -7
- package/get-shit-done/references/model-profiles.md +20 -14
- package/get-shit-done/references/planning-config.md +237 -0
- package/get-shit-done/references/thinking-models-debug.md +44 -0
- package/get-shit-done/references/thinking-models-execution.md +50 -0
- package/get-shit-done/references/thinking-models-planning.md +62 -0
- package/get-shit-done/references/thinking-models-research.md +50 -0
- package/get-shit-done/references/thinking-models-verification.md +55 -0
- package/get-shit-done/references/thinking-partner.md +96 -0
- package/get-shit-done/references/universal-anti-patterns.md +6 -1
- package/get-shit-done/references/verification-overrides.md +227 -0
- package/get-shit-done/templates/AI-SPEC.md +246 -0
- package/get-shit-done/workflows/add-tests.md +3 -0
- package/get-shit-done/workflows/add-todo.md +2 -0
- package/get-shit-done/workflows/ai-integration-phase.md +284 -0
- package/get-shit-done/workflows/audit-fix.md +154 -0
- package/get-shit-done/workflows/autonomous.md +33 -2
- package/get-shit-done/workflows/check-todos.md +2 -0
- package/get-shit-done/workflows/cleanup.md +2 -0
- package/get-shit-done/workflows/code-review-fix.md +497 -0
- package/get-shit-done/workflows/code-review.md +515 -0
- package/get-shit-done/workflows/complete-milestone.md +40 -15
- package/get-shit-done/workflows/diagnose-issues.md +1 -1
- package/get-shit-done/workflows/discovery-phase.md +3 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
- package/get-shit-done/workflows/discuss-phase.md +21 -7
- package/get-shit-done/workflows/do.md +2 -0
- package/get-shit-done/workflows/docs-update.md +2 -0
- package/get-shit-done/workflows/eval-review.md +155 -0
- package/get-shit-done/workflows/execute-phase.md +307 -57
- package/get-shit-done/workflows/execute-plan.md +64 -93
- package/get-shit-done/workflows/explore.md +136 -0
- package/get-shit-done/workflows/help.md +1 -1
- package/get-shit-done/workflows/import.md +273 -0
- package/get-shit-done/workflows/inbox.md +387 -0
- package/get-shit-done/workflows/manager.md +4 -10
- package/get-shit-done/workflows/new-milestone.md +3 -1
- package/get-shit-done/workflows/new-project.md +2 -0
- package/get-shit-done/workflows/new-workspace.md +2 -0
- package/get-shit-done/workflows/next.md +56 -0
- package/get-shit-done/workflows/note.md +2 -0
- package/get-shit-done/workflows/plan-phase.md +97 -17
- package/get-shit-done/workflows/plant-seed.md +3 -0
- package/get-shit-done/workflows/pr-branch.md +41 -13
- package/get-shit-done/workflows/profile-user.md +4 -2
- package/get-shit-done/workflows/quick.md +99 -4
- package/get-shit-done/workflows/remove-workspace.md +2 -0
- package/get-shit-done/workflows/review.md +53 -6
- package/get-shit-done/workflows/scan.md +98 -0
- package/get-shit-done/workflows/secure-phase.md +2 -0
- package/get-shit-done/workflows/settings.md +18 -3
- package/get-shit-done/workflows/ship.md +3 -0
- package/get-shit-done/workflows/ui-phase.md +10 -2
- package/get-shit-done/workflows/ui-review.md +2 -0
- package/get-shit-done/workflows/undo.md +314 -0
- package/get-shit-done/workflows/update.md +2 -0
- package/get-shit-done/workflows/validate-phase.md +2 -0
- package/get-shit-done/workflows/verify-phase.md +83 -0
- package/get-shit-done/workflows/verify-work.md +12 -1
- package/package.json +1 -1
- package/skills/gsd-code-review/SKILL.md +48 -0
- package/skills/gsd-code-review-fix/SKILL.md +44 -0
|
@@ -27,6 +27,16 @@ const WORKSTREAM_SESSION_ENV_KEYS = [
|
|
|
27
27
|
let cachedControllingTtyToken = null;
|
|
28
28
|
let didProbeControllingTtyToken = false;
|
|
29
29
|
|
|
30
|
+
// Track all .planning/.lock files held by this process so they can be removed
|
|
31
|
+
// on exit. process.on('exit') fires even on process.exit(1), unlike try/finally
|
|
32
|
+
// which is skipped when error() calls process.exit(1) inside a locked region (#1916).
|
|
33
|
+
const _heldPlanningLocks = new Set();
|
|
34
|
+
process.on('exit', () => {
|
|
35
|
+
for (const lockPath of _heldPlanningLocks) {
|
|
36
|
+
try { fs.unlinkSync(lockPath); } catch { /* already gone */ }
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
30
40
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
31
41
|
|
|
32
42
|
/** Normalize a relative path to always use forward slashes (cross-platform). */
|
|
@@ -229,6 +239,7 @@ const CONFIG_DEFAULTS = {
|
|
|
229
239
|
plan_checker: true,
|
|
230
240
|
verifier: true,
|
|
231
241
|
nyquist_validation: true,
|
|
242
|
+
ai_integration_phase: true,
|
|
232
243
|
parallelization: true,
|
|
233
244
|
brave_search: false,
|
|
234
245
|
firecrawl: false,
|
|
@@ -300,7 +311,7 @@ function loadConfig(cwd) {
|
|
|
300
311
|
// Extract top-level key names from dot-notation paths (e.g., 'workflow.research' → 'workflow')
|
|
301
312
|
...[...VALID_CONFIG_KEYS].map(k => k.split('.')[0]),
|
|
302
313
|
// Section containers that hold nested sub-keys
|
|
303
|
-
'git', 'workflow', 'planning', 'hooks',
|
|
314
|
+
'git', 'workflow', 'planning', 'hooks', 'features',
|
|
304
315
|
// Internal keys loadConfig reads but config-set doesn't expose
|
|
305
316
|
'model_overrides', 'agent_skills', 'context_window', 'resolve_model_ids',
|
|
306
317
|
// Deprecated keys (still accepted for migration, not in config-set)
|
|
@@ -400,7 +411,11 @@ function loadConfig(cwd) {
|
|
|
400
411
|
|
|
401
412
|
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
402
413
|
|
|
414
|
+
const _gitIgnoredCache = new Map();
|
|
415
|
+
|
|
403
416
|
function isGitIgnored(cwd, targetPath) {
|
|
417
|
+
const key = cwd + '::' + targetPath;
|
|
418
|
+
if (_gitIgnoredCache.has(key)) return _gitIgnoredCache.get(key);
|
|
404
419
|
try {
|
|
405
420
|
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
406
421
|
// Without it, git check-ignore returns "not ignored" for tracked files even when
|
|
@@ -412,8 +427,10 @@ function isGitIgnored(cwd, targetPath) {
|
|
|
412
427
|
cwd,
|
|
413
428
|
stdio: 'pipe',
|
|
414
429
|
});
|
|
430
|
+
_gitIgnoredCache.set(key, true);
|
|
415
431
|
return true;
|
|
416
432
|
} catch {
|
|
433
|
+
_gitIgnoredCache.set(key, false);
|
|
417
434
|
return false;
|
|
418
435
|
}
|
|
419
436
|
}
|
|
@@ -598,10 +615,15 @@ function withPlanningLock(cwd, fn) {
|
|
|
598
615
|
acquired: new Date().toISOString(),
|
|
599
616
|
}), { flag: 'wx' });
|
|
600
617
|
|
|
618
|
+
// Register for exit-time cleanup so process.exit(1) inside a locked region
|
|
619
|
+
// cannot leave a stale lock file (#1916).
|
|
620
|
+
_heldPlanningLocks.add(lockPath);
|
|
621
|
+
|
|
601
622
|
// Lock acquired — run the function
|
|
602
623
|
try {
|
|
603
624
|
return fn();
|
|
604
625
|
} finally {
|
|
626
|
+
_heldPlanningLocks.delete(lockPath);
|
|
605
627
|
try { fs.unlinkSync(lockPath); } catch { /* already released */ }
|
|
606
628
|
}
|
|
607
629
|
} catch (err) {
|
|
@@ -670,19 +692,23 @@ function planningRoot(cwd) {
|
|
|
670
692
|
}
|
|
671
693
|
|
|
672
694
|
/**
|
|
673
|
-
* Get common .planning file paths, workstream-aware.
|
|
674
|
-
*
|
|
675
|
-
*
|
|
695
|
+
* Get common .planning file paths, project-and-workstream-aware.
|
|
696
|
+
*
|
|
697
|
+
* All paths route through planningDir(cwd, ws), which honors the GSD_PROJECT
|
|
698
|
+
* env var and active workstream. This matches loadConfig() above (line 256),
|
|
699
|
+
* which has always read config.json via planningDir(cwd). Previously project
|
|
700
|
+
* and config were resolved against the unrouted .planning/ root, which broke
|
|
701
|
+
* `gsd-tools config-get` in multi-project layouts (the CRUD writers and the
|
|
702
|
+
* reader pointed at different files).
|
|
676
703
|
*/
|
|
677
704
|
function planningPaths(cwd, ws) {
|
|
678
705
|
const base = planningDir(cwd, ws);
|
|
679
|
-
const root = path.join(cwd, '.planning');
|
|
680
706
|
return {
|
|
681
707
|
planning: base,
|
|
682
708
|
state: path.join(base, 'STATE.md'),
|
|
683
709
|
roadmap: path.join(base, 'ROADMAP.md'),
|
|
684
|
-
project: path.join(
|
|
685
|
-
config: path.join(
|
|
710
|
+
project: path.join(base, 'PROJECT.md'),
|
|
711
|
+
config: path.join(base, 'config.json'),
|
|
686
712
|
phases: path.join(base, 'phases'),
|
|
687
713
|
requirements: path.join(base, 'REQUIREMENTS.md'),
|
|
688
714
|
};
|
|
@@ -879,7 +905,10 @@ function normalizePhaseName(phase) {
|
|
|
879
905
|
const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
880
906
|
if (match) {
|
|
881
907
|
const padded = match[1].padStart(2, '0');
|
|
882
|
-
|
|
908
|
+
// Preserve original case of letter suffix (#1962).
|
|
909
|
+
// Uppercasing causes directory/roadmap mismatches on case-sensitive filesystems
|
|
910
|
+
// (e.g., "16c" in ROADMAP.md → directory "16C-name" → progress can't match).
|
|
911
|
+
const letter = match[2] || '';
|
|
883
912
|
const decimal = match[3] || '';
|
|
884
913
|
return padded + letter + decimal;
|
|
885
914
|
}
|
|
@@ -1485,6 +1514,38 @@ function readSubdirectories(dirPath, sort = false) {
|
|
|
1485
1514
|
}
|
|
1486
1515
|
}
|
|
1487
1516
|
|
|
1517
|
+
// ─── Atomic file writes ───────────────────────────────────────────────────────
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* write a file atomically using write-to-temp-then-rename.
|
|
1521
|
+
*
|
|
1522
|
+
* On POSIX systems, `fs.renameSync` is atomic when the source and destination
|
|
1523
|
+
* are on the same filesystem. This prevents a process killed mid-write from
|
|
1524
|
+
* leaving a truncated file that is unparseable on next read.
|
|
1525
|
+
*
|
|
1526
|
+
* The temp file is placed alongside the target so it is guaranteed to be on
|
|
1527
|
+
* the same filesystem (required for rename atomicity). The PID is embedded in
|
|
1528
|
+
* the temp file name so concurrent writers use distinct paths.
|
|
1529
|
+
*
|
|
1530
|
+
* If `renameSync` fails (e.g. cross-device move), the function falls back to a
|
|
1531
|
+
* direct `writeFileSync` so callers always get a best-effort write.
|
|
1532
|
+
*
|
|
1533
|
+
* @param {string} filePath Absolute path to write.
|
|
1534
|
+
* @param {string|Buffer} content File content.
|
|
1535
|
+
* @param {string} [encoding='utf-8'] Encoding passed to writeFileSync.
|
|
1536
|
+
*/
|
|
1537
|
+
function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
|
|
1538
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
1539
|
+
try {
|
|
1540
|
+
fs.writeFileSync(tmpPath, content, encoding);
|
|
1541
|
+
fs.renameSync(tmpPath, filePath);
|
|
1542
|
+
} catch (renameErr) {
|
|
1543
|
+
// Clean up the temp file if rename failed, then fall back to direct write.
|
|
1544
|
+
try { fs.unlinkSync(tmpPath); } catch { /* already gone or never created */ }
|
|
1545
|
+
fs.writeFileSync(filePath, content, encoding);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1488
1549
|
module.exports = {
|
|
1489
1550
|
output,
|
|
1490
1551
|
error,
|
|
@@ -1530,4 +1591,5 @@ module.exports = {
|
|
|
1530
1591
|
readSubdirectories,
|
|
1531
1592
|
getAgentsDir,
|
|
1532
1593
|
checkAgentsInstalled,
|
|
1594
|
+
atomicWriteFileSync,
|
|
1533
1595
|
};
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gsd2-import — Reverse migration from GSD-2 (.gsd/) to GSD v1 (.planning/)
|
|
5
|
+
*
|
|
6
|
+
* Reads a GSD-2 project directory structure and produces a complete
|
|
7
|
+
* .planning/ artifact tree in GSD v1 format.
|
|
8
|
+
*
|
|
9
|
+
* GSD-2 hierarchy: Milestone → Slice → task
|
|
10
|
+
* GSD v1 hierarchy: Milestone (in ROADMAP.md) → Phase → Plan
|
|
11
|
+
*
|
|
12
|
+
* Mapping rules:
|
|
13
|
+
* - Slices are numbered sequentially across all milestones (01, 02, …)
|
|
14
|
+
* - Tasks within a slice become plans (01-01, 01-02, …)
|
|
15
|
+
* - Completed slices ([x] in ROADMAP) → [x] phases in ROADMAP.md
|
|
16
|
+
* - Tasks with a SUMMARY file → SUMMARY.md written
|
|
17
|
+
* - Slice RESEARCH.md → phase XX-RESEARCH.md
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function readOptional(filePath) {
|
|
26
|
+
try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function zeroPad(n, width = 2) {
|
|
30
|
+
return String(n).padStart(width, '0');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function slugify(title) {
|
|
34
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── GSD-2 Parser ───────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find the .gsd/ directory starting from a project root.
|
|
41
|
+
* Returns the absolute path or null if not found.
|
|
42
|
+
*/
|
|
43
|
+
function findGsd2Root(startPath) {
|
|
44
|
+
if (path.basename(startPath) === '.gsd' && fs.existsSync(startPath)) {
|
|
45
|
+
return startPath;
|
|
46
|
+
}
|
|
47
|
+
const candidate = path.join(startPath, '.gsd');
|
|
48
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse the ## Slices section from a GSD-2 milestone ROADMAP.md.
|
|
56
|
+
* Each slice entry looks like:
|
|
57
|
+
* - [x] **S01: Title** `risk:medium` `depends:[S00]`
|
|
58
|
+
*/
|
|
59
|
+
function parseSlicesFromRoadmap(content) {
|
|
60
|
+
const slices = [];
|
|
61
|
+
const sectionMatch = content.match(/## Slices\n([\s\S]*?)(?:\n## |\n# |$)/);
|
|
62
|
+
if (!sectionMatch) return slices;
|
|
63
|
+
|
|
64
|
+
for (const line of sectionMatch[1].split('\n')) {
|
|
65
|
+
const m = line.match(/^- \[([x ])\]\s+\*\*(\w+):\s*([^*]+)\*\*/);
|
|
66
|
+
if (!m) continue;
|
|
67
|
+
slices.push({ done: m[1] === 'x', id: m[2].trim(), title: m[3].trim() });
|
|
68
|
+
}
|
|
69
|
+
return slices;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse the milestone title from the first heading in a GSD-2 ROADMAP.md.
|
|
74
|
+
* Format: # M001: Title
|
|
75
|
+
*/
|
|
76
|
+
function parseMilestoneTitle(content) {
|
|
77
|
+
const m = content.match(/^# \w+:\s*(.+)/m);
|
|
78
|
+
return m ? m[1].trim() : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a task title from a GSD-2 T##-PLAN.md.
|
|
83
|
+
* Format: # T01: Title
|
|
84
|
+
*/
|
|
85
|
+
function parseTaskTitle(content, fallback) {
|
|
86
|
+
const m = content.match(/^# \w+:\s*(.+)/m);
|
|
87
|
+
return m ? m[1].trim() : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse the ## Description body from a GSD-2 task plan.
|
|
92
|
+
*/
|
|
93
|
+
function parseTaskDescription(content) {
|
|
94
|
+
const m = content.match(/## Description\n+([\s\S]+?)(?:\n## |\n# |$)/);
|
|
95
|
+
return m ? m[1].trim() : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse ## Must-Haves items from a GSD-2 task plan.
|
|
100
|
+
*/
|
|
101
|
+
function parseTaskMustHaves(content) {
|
|
102
|
+
const m = content.match(/## Must-Haves\n+([\s\S]+?)(?:\n## |\n# |$)/);
|
|
103
|
+
if (!m) return [];
|
|
104
|
+
return m[1].split('\n')
|
|
105
|
+
.map(l => l.match(/^- \[[ x]\]\s*(.+)/))
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map(match => match[1].trim());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* read all task plan files from a GSD-2 tasks/ directory.
|
|
112
|
+
*/
|
|
113
|
+
function readTasksDir(tasksDir) {
|
|
114
|
+
if (!fs.existsSync(tasksDir)) return [];
|
|
115
|
+
|
|
116
|
+
return fs.readdirSync(tasksDir)
|
|
117
|
+
.filter(f => f.endsWith('-PLAN.md'))
|
|
118
|
+
.sort()
|
|
119
|
+
.map(tf => {
|
|
120
|
+
const tid = tf.replace('-PLAN.md', '');
|
|
121
|
+
const plan = readOptional(path.join(tasksDir, tf));
|
|
122
|
+
const summary = readOptional(path.join(tasksDir, `${tid}-SUMMARY.md`));
|
|
123
|
+
return {
|
|
124
|
+
id: tid,
|
|
125
|
+
title: plan ? parseTaskTitle(plan, tid) : tid,
|
|
126
|
+
description: plan ? parseTaskDescription(plan) : '',
|
|
127
|
+
mustHaves: plan ? parseTaskMustHaves(plan) : [],
|
|
128
|
+
plan,
|
|
129
|
+
summary,
|
|
130
|
+
done: !!summary,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse a complete GSD-2 .gsd/ directory into a structured representation.
|
|
137
|
+
*/
|
|
138
|
+
function parseGsd2(gsdDir) {
|
|
139
|
+
const data = {
|
|
140
|
+
projectContent: readOptional(path.join(gsdDir, 'PROJECT.md')),
|
|
141
|
+
requirements: readOptional(path.join(gsdDir, 'REQUIREMENTS.md')),
|
|
142
|
+
milestones: [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const milestonesBase = path.join(gsdDir, 'milestones');
|
|
146
|
+
if (!fs.existsSync(milestonesBase)) return data;
|
|
147
|
+
|
|
148
|
+
const milestoneIds = fs.readdirSync(milestonesBase)
|
|
149
|
+
.filter(d => fs.statSync(path.join(milestonesBase, d)).isDirectory())
|
|
150
|
+
.sort();
|
|
151
|
+
|
|
152
|
+
for (const mid of milestoneIds) {
|
|
153
|
+
const mDir = path.join(milestonesBase, mid);
|
|
154
|
+
const roadmapContent = readOptional(path.join(mDir, `${mid}-ROADMAP.md`));
|
|
155
|
+
const slicesDir = path.join(mDir, 'slices');
|
|
156
|
+
|
|
157
|
+
const sliceInfos = roadmapContent ? parseSlicesFromRoadmap(roadmapContent) : [];
|
|
158
|
+
|
|
159
|
+
const slices = sliceInfos.map(info => {
|
|
160
|
+
const sDir = path.join(slicesDir, info.id);
|
|
161
|
+
const hasSDir = fs.existsSync(sDir);
|
|
162
|
+
return {
|
|
163
|
+
id: info.id,
|
|
164
|
+
title: info.title,
|
|
165
|
+
done: info.done,
|
|
166
|
+
plan: hasSDir ? readOptional(path.join(sDir, `${info.id}-PLAN.md`)) : null,
|
|
167
|
+
summary: hasSDir ? readOptional(path.join(sDir, `${info.id}-SUMMARY.md`)) : null,
|
|
168
|
+
research: hasSDir ? readOptional(path.join(sDir, `${info.id}-RESEARCH.md`)) : null,
|
|
169
|
+
context: hasSDir ? readOptional(path.join(sDir, `${info.id}-CONTEXT.md`)) : null,
|
|
170
|
+
tasks: hasSDir ? readTasksDir(path.join(sDir, 'tasks')) : [],
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
data.milestones.push({
|
|
175
|
+
id: mid,
|
|
176
|
+
title: roadmapContent ? (parseMilestoneTitle(roadmapContent) ?? mid) : mid,
|
|
177
|
+
research: readOptional(path.join(mDir, `${mid}-RESEARCH.md`)),
|
|
178
|
+
slices,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Artifact Builders ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a GSD v1 PLAN.md from a GSD-2 task.
|
|
189
|
+
*/
|
|
190
|
+
function buildPlanMd(task, phasePrefix, planPrefix, phaseSlug, milestoneTitle) {
|
|
191
|
+
const lines = [
|
|
192
|
+
'---',
|
|
193
|
+
`phase: "${phasePrefix}"`,
|
|
194
|
+
`plan: "${planPrefix}"`,
|
|
195
|
+
'type: "implementation"',
|
|
196
|
+
'---',
|
|
197
|
+
'',
|
|
198
|
+
'<objective>',
|
|
199
|
+
task.title,
|
|
200
|
+
'</objective>',
|
|
201
|
+
'',
|
|
202
|
+
'<context>',
|
|
203
|
+
`Phase: ${phasePrefix} (${phaseSlug}) — Milestone: ${milestoneTitle}`,
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
if (task.description) {
|
|
207
|
+
lines.push('', task.description);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
lines.push('</context>');
|
|
211
|
+
|
|
212
|
+
if (task.mustHaves.length > 0) {
|
|
213
|
+
lines.push('', '<must_haves>');
|
|
214
|
+
for (const mh of task.mustHaves) {
|
|
215
|
+
lines.push(`- ${mh}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push('</must_haves>');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return lines.join('\n') + '\n';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a GSD v1 SUMMARY.md from a GSD-2 task summary.
|
|
225
|
+
* Strips the GSD-2 frontmatter and preserves the body.
|
|
226
|
+
*/
|
|
227
|
+
function buildSummaryMd(task, phasePrefix, planPrefix) {
|
|
228
|
+
const raw = task.summary || '';
|
|
229
|
+
// Strip GSD-2 frontmatter block (--- ... ---) if present
|
|
230
|
+
const bodyMatch = raw.match(/^---[\s\S]*?---\n+([\s\S]*)$/);
|
|
231
|
+
const body = bodyMatch ? bodyMatch[1].trim() : raw.trim();
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
'---',
|
|
235
|
+
`phase: "${phasePrefix}"`,
|
|
236
|
+
`plan: "${planPrefix}"`,
|
|
237
|
+
'---',
|
|
238
|
+
'',
|
|
239
|
+
body || 'task completed (migrated from GSD-2).',
|
|
240
|
+
'',
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build a GSD v1 XX-CONTEXT.md from a GSD-2 slice.
|
|
246
|
+
*/
|
|
247
|
+
function buildContextMd(slice, phasePrefix) {
|
|
248
|
+
const lines = [
|
|
249
|
+
`# Phase ${phasePrefix} Context`,
|
|
250
|
+
'',
|
|
251
|
+
`Migrated from GSD-2 slice ${slice.id}: ${slice.title}`,
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const extra = slice.context || '';
|
|
255
|
+
if (extra.trim()) {
|
|
256
|
+
lines.push('', extra.trim());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return lines.join('\n') + '\n';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Build the GSD v1 ROADMAP.md with milestone-sectioned format.
|
|
264
|
+
*/
|
|
265
|
+
function buildRoadmapMd(milestones, phaseMap) {
|
|
266
|
+
const lines = ['# Roadmap', ''];
|
|
267
|
+
|
|
268
|
+
for (const milestone of milestones) {
|
|
269
|
+
lines.push(`## ${milestone.id}: ${milestone.title}`, '');
|
|
270
|
+
const mPhases = phaseMap.filter(p => p.milestoneId === milestone.id);
|
|
271
|
+
for (const { slice, phaseNum } of mPhases) {
|
|
272
|
+
const prefix = zeroPad(phaseNum);
|
|
273
|
+
const slug = slugify(slice.title);
|
|
274
|
+
const check = slice.done ? 'x' : ' ';
|
|
275
|
+
lines.push(`- [${check}] **Phase ${prefix}: ${slug}** — ${slice.title}`);
|
|
276
|
+
}
|
|
277
|
+
lines.push('');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return lines.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build the GSD v1 STATE.md reflecting the current position in the project.
|
|
285
|
+
*/
|
|
286
|
+
function buildStateMd(phaseMap) {
|
|
287
|
+
const currentEntry = phaseMap.find(p => !p.slice.done);
|
|
288
|
+
const totalPhases = phaseMap.length;
|
|
289
|
+
const donePhases = phaseMap.filter(p => p.slice.done).length;
|
|
290
|
+
const pct = totalPhases > 0 ? Math.round((donePhases / totalPhases) * 100) : 0;
|
|
291
|
+
|
|
292
|
+
const currentPhaseNum = currentEntry ? zeroPad(currentEntry.phaseNum) : zeroPad(totalPhases);
|
|
293
|
+
const currentSlug = currentEntry ? slugify(currentEntry.slice.title) : 'complete';
|
|
294
|
+
const status = currentEntry ? 'Ready to plan' : 'All phases complete';
|
|
295
|
+
|
|
296
|
+
const filled = Math.round(pct / 10);
|
|
297
|
+
const bar = `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}]`;
|
|
298
|
+
const today = new Date().toISOString().split('T')[0];
|
|
299
|
+
|
|
300
|
+
return [
|
|
301
|
+
'# Project State',
|
|
302
|
+
'',
|
|
303
|
+
'## Project Reference',
|
|
304
|
+
'',
|
|
305
|
+
'See: .planning/PROJECT.md',
|
|
306
|
+
'',
|
|
307
|
+
`**Current focus:** Phase ${currentPhaseNum} (${currentSlug})`,
|
|
308
|
+
'',
|
|
309
|
+
'## Current Position',
|
|
310
|
+
'',
|
|
311
|
+
`Phase: ${currentPhaseNum} of ${zeroPad(totalPhases)} (${currentSlug})`,
|
|
312
|
+
`Status: ${status}`,
|
|
313
|
+
`Last activity: ${today} — Migrated from GSD-2`,
|
|
314
|
+
'',
|
|
315
|
+
`Progress: ${bar} ${pct}%`,
|
|
316
|
+
'',
|
|
317
|
+
'## Accumulated Context',
|
|
318
|
+
'',
|
|
319
|
+
'### Decisions',
|
|
320
|
+
'',
|
|
321
|
+
'Migrated from GSD-2. Review PROJECT.md for key decisions.',
|
|
322
|
+
'',
|
|
323
|
+
'### Blockers/Concerns',
|
|
324
|
+
'',
|
|
325
|
+
'None.',
|
|
326
|
+
'',
|
|
327
|
+
'## Session Continuity',
|
|
328
|
+
'',
|
|
329
|
+
`Last session: ${today}`,
|
|
330
|
+
'Stopped at: Migration from GSD-2 completed',
|
|
331
|
+
'Resume file: None',
|
|
332
|
+
'',
|
|
333
|
+
].join('\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ─── Transformer ─────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Convert parsed GSD-2 data into a map of relative path → file content.
|
|
340
|
+
* All paths are relative to the .planning/ root.
|
|
341
|
+
*/
|
|
342
|
+
function buildPlanningArtifacts(gsd2Data) {
|
|
343
|
+
const artifacts = new Map();
|
|
344
|
+
|
|
345
|
+
// Passthrough files
|
|
346
|
+
artifacts.set('PROJECT.md', gsd2Data.projectContent || '# Project\n\n(Migrated from GSD-2)\n');
|
|
347
|
+
if (gsd2Data.requirements) {
|
|
348
|
+
artifacts.set('REQUIREMENTS.md', gsd2Data.requirements);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Minimal valid v1 config
|
|
352
|
+
artifacts.set('config.json', JSON.stringify({ version: 1 }, null, 2) + '\n');
|
|
353
|
+
|
|
354
|
+
// Build sequential phase map: flatten Milestones → Slices into numbered phases
|
|
355
|
+
const phaseMap = [];
|
|
356
|
+
let phaseNum = 1;
|
|
357
|
+
for (const milestone of gsd2Data.milestones) {
|
|
358
|
+
for (const slice of milestone.slices) {
|
|
359
|
+
phaseMap.push({ milestoneId: milestone.id, milestoneTitle: milestone.title, slice, phaseNum });
|
|
360
|
+
phaseNum++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
artifacts.set('ROADMAP.md', buildRoadmapMd(gsd2Data.milestones, phaseMap));
|
|
365
|
+
artifacts.set('STATE.md', buildStateMd(phaseMap));
|
|
366
|
+
|
|
367
|
+
for (const { slice, phaseNum, milestoneTitle } of phaseMap) {
|
|
368
|
+
const prefix = zeroPad(phaseNum);
|
|
369
|
+
const slug = slugify(slice.title);
|
|
370
|
+
const dir = `phases/${prefix}-${slug}`;
|
|
371
|
+
|
|
372
|
+
artifacts.set(`${dir}/${prefix}-CONTEXT.md`, buildContextMd(slice, prefix));
|
|
373
|
+
|
|
374
|
+
if (slice.research) {
|
|
375
|
+
artifacts.set(`${dir}/${prefix}-RESEARCH.md`, slice.research);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (let i = 0; i < slice.tasks.length; i++) {
|
|
379
|
+
const task = slice.tasks[i];
|
|
380
|
+
const planPrefix = zeroPad(i + 1);
|
|
381
|
+
|
|
382
|
+
artifacts.set(
|
|
383
|
+
`${dir}/${prefix}-${planPrefix}-PLAN.md`,
|
|
384
|
+
buildPlanMd(task, prefix, planPrefix, slug, milestoneTitle)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (task.done && task.summary) {
|
|
388
|
+
artifacts.set(
|
|
389
|
+
`${dir}/${prefix}-${planPrefix}-SUMMARY.md`,
|
|
390
|
+
buildSummaryMd(task, prefix, planPrefix)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return artifacts;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Preview ─────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Format a dry-run preview string for display before writing.
|
|
403
|
+
*/
|
|
404
|
+
function buildPreview(gsd2Data, artifacts) {
|
|
405
|
+
const lines = ['Preview — files that will be created in .planning/:'];
|
|
406
|
+
|
|
407
|
+
for (const rel of artifacts.keys()) {
|
|
408
|
+
lines.push(` ${rel}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const totalSlices = gsd2Data.milestones.reduce((s, m) => s + m.slices.length, 0);
|
|
412
|
+
const doneSlices = gsd2Data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
413
|
+
const allTasks = gsd2Data.milestones.flatMap(m => m.slices.flatMap(sl => sl.tasks));
|
|
414
|
+
const doneTasks = allTasks.filter(t => t.done).length;
|
|
415
|
+
|
|
416
|
+
lines.push('');
|
|
417
|
+
lines.push(`Milestones: ${gsd2Data.milestones.length}`);
|
|
418
|
+
lines.push(`Phases (slices): ${totalSlices} (${doneSlices} completed)`);
|
|
419
|
+
lines.push(`Plans (tasks): ${allTasks.length} (${doneTasks} completed)`);
|
|
420
|
+
lines.push('');
|
|
421
|
+
lines.push('Cannot migrate automatically:');
|
|
422
|
+
lines.push(' - GSD-2 cost/token ledger (no v1 equivalent)');
|
|
423
|
+
lines.push(' - GSD-2 database state (rebuilt from files on first /gsd-health)');
|
|
424
|
+
lines.push(' - VS Code extension state');
|
|
425
|
+
|
|
426
|
+
return lines.join('\n');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─── Writer ───────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* write all artifacts to the .planning/ directory.
|
|
433
|
+
*/
|
|
434
|
+
function writePlanningDir(artifacts, planningRoot) {
|
|
435
|
+
for (const [rel, content] of artifacts) {
|
|
436
|
+
const absPath = path.join(planningRoot, rel);
|
|
437
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
438
|
+
fs.writeFileSync(absPath, content, 'utf8');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Command Handler ──────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Entry point called from gsd-tools.cjs.
|
|
446
|
+
* Supports: --force, --dry-run, --path <dir>
|
|
447
|
+
*/
|
|
448
|
+
function cmdFromGsd2(args, cwd, raw) {
|
|
449
|
+
const { output, error } = require('./core.cjs');
|
|
450
|
+
|
|
451
|
+
const force = args.includes('--force');
|
|
452
|
+
const dryRun = args.includes('--dry-run');
|
|
453
|
+
|
|
454
|
+
const pathIdx = args.indexOf('--path');
|
|
455
|
+
const projectDir = pathIdx >= 0 && args[pathIdx + 1]
|
|
456
|
+
? path.resolve(cwd, args[pathIdx + 1])
|
|
457
|
+
: cwd;
|
|
458
|
+
|
|
459
|
+
const gsdDir = findGsd2Root(projectDir);
|
|
460
|
+
if (!gsdDir) {
|
|
461
|
+
return output({ success: false, error: `No .gsd/ directory found in ${projectDir}` }, raw);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const planningRoot = path.join(path.dirname(gsdDir), '.planning');
|
|
465
|
+
if (fs.existsSync(planningRoot) && !force) {
|
|
466
|
+
return output({
|
|
467
|
+
success: false,
|
|
468
|
+
error: `.planning/ already exists at ${planningRoot}. Pass --force to overwrite.`,
|
|
469
|
+
}, raw);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const gsd2Data = parseGsd2(gsdDir);
|
|
473
|
+
const artifacts = buildPlanningArtifacts(gsd2Data);
|
|
474
|
+
const preview = buildPreview(gsd2Data, artifacts);
|
|
475
|
+
|
|
476
|
+
if (dryRun) {
|
|
477
|
+
return output({ success: true, dryRun: true, preview }, raw);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
writePlanningDir(artifacts, planningRoot);
|
|
481
|
+
|
|
482
|
+
return output({
|
|
483
|
+
success: true,
|
|
484
|
+
planningDir: planningRoot,
|
|
485
|
+
filesWritten: artifacts.size,
|
|
486
|
+
milestones: gsd2Data.milestones.length,
|
|
487
|
+
preview,
|
|
488
|
+
}, raw);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
module.exports = {
|
|
492
|
+
findGsd2Root,
|
|
493
|
+
parseGsd2,
|
|
494
|
+
buildPlanningArtifacts,
|
|
495
|
+
buildPreview,
|
|
496
|
+
writePlanningDir,
|
|
497
|
+
cmdFromGsd2,
|
|
498
|
+
// Exported for unit tests
|
|
499
|
+
parseSlicesFromRoadmap,
|
|
500
|
+
parseMilestoneTitle,
|
|
501
|
+
parseTaskTitle,
|
|
502
|
+
parseTaskDescription,
|
|
503
|
+
parseTaskMustHaves,
|
|
504
|
+
buildPlanMd,
|
|
505
|
+
buildSummaryMd,
|
|
506
|
+
buildContextMd,
|
|
507
|
+
buildRoadmapMd,
|
|
508
|
+
buildStateMd,
|
|
509
|
+
slugify,
|
|
510
|
+
zeroPad,
|
|
511
|
+
};
|