gsd-opencode 1.22.1 → 1.33.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 (188) hide show
  1. package/agents/gsd-advisor-researcher.md +112 -0
  2. package/agents/gsd-assumptions-analyzer.md +110 -0
  3. package/agents/gsd-codebase-mapper.md +0 -2
  4. package/agents/gsd-debugger.md +117 -2
  5. package/agents/gsd-doc-verifier.md +207 -0
  6. package/agents/gsd-doc-writer.md +608 -0
  7. package/agents/gsd-executor.md +45 -4
  8. package/agents/gsd-integration-checker.md +0 -2
  9. package/agents/gsd-nyquist-auditor.md +0 -2
  10. package/agents/gsd-phase-researcher.md +191 -5
  11. package/agents/gsd-plan-checker.md +152 -5
  12. package/agents/gsd-planner.md +131 -157
  13. package/agents/gsd-project-researcher.md +28 -3
  14. package/agents/gsd-research-synthesizer.md +0 -2
  15. package/agents/gsd-roadmapper.md +29 -2
  16. package/agents/gsd-security-auditor.md +129 -0
  17. package/agents/gsd-ui-auditor.md +485 -0
  18. package/agents/gsd-ui-checker.md +305 -0
  19. package/agents/gsd-ui-researcher.md +368 -0
  20. package/agents/gsd-user-profiler.md +173 -0
  21. package/agents/gsd-verifier.md +207 -22
  22. package/commands/gsd/gsd-add-backlog.md +76 -0
  23. package/commands/gsd/gsd-analyze-dependencies.md +34 -0
  24. package/commands/gsd/gsd-audit-uat.md +24 -0
  25. package/commands/gsd/gsd-autonomous.md +45 -0
  26. package/commands/gsd/gsd-cleanup.md +5 -0
  27. package/commands/gsd/gsd-debug.md +29 -21
  28. package/commands/gsd/gsd-discuss-phase.md +15 -36
  29. package/commands/gsd/gsd-do.md +30 -0
  30. package/commands/gsd/gsd-docs-update.md +48 -0
  31. package/commands/gsd/gsd-execute-phase.md +24 -2
  32. package/commands/gsd/gsd-fast.md +30 -0
  33. package/commands/gsd/gsd-forensics.md +56 -0
  34. package/commands/gsd/gsd-help.md +2 -0
  35. package/commands/gsd/gsd-join-discord.md +2 -1
  36. package/commands/gsd/gsd-list-workspaces.md +19 -0
  37. package/commands/gsd/gsd-manager.md +40 -0
  38. package/commands/gsd/gsd-milestone-summary.md +51 -0
  39. package/commands/gsd/gsd-new-project.md +4 -0
  40. package/commands/gsd/gsd-new-workspace.md +44 -0
  41. package/commands/gsd/gsd-next.md +24 -0
  42. package/commands/gsd/gsd-note.md +34 -0
  43. package/commands/gsd/gsd-plan-phase.md +8 -1
  44. package/commands/gsd/gsd-plant-seed.md +28 -0
  45. package/commands/gsd/gsd-pr-branch.md +25 -0
  46. package/commands/gsd/gsd-profile-user.md +46 -0
  47. package/commands/gsd/gsd-quick.md +7 -3
  48. package/commands/gsd/gsd-reapply-patches.md +178 -45
  49. package/commands/gsd/gsd-remove-workspace.md +26 -0
  50. package/commands/gsd/gsd-research-phase.md +7 -12
  51. package/commands/gsd/gsd-review-backlog.md +62 -0
  52. package/commands/gsd/gsd-review.md +38 -0
  53. package/commands/gsd/gsd-secure-phase.md +35 -0
  54. package/commands/gsd/gsd-session-report.md +19 -0
  55. package/commands/gsd/gsd-set-profile.md +24 -23
  56. package/commands/gsd/gsd-ship.md +23 -0
  57. package/commands/gsd/gsd-stats.md +18 -0
  58. package/commands/gsd/gsd-thread.md +127 -0
  59. package/commands/gsd/gsd-ui-phase.md +34 -0
  60. package/commands/gsd/gsd-ui-review.md +32 -0
  61. package/commands/gsd/gsd-workstreams.md +71 -0
  62. package/get-shit-done/bin/gsd-tools.cjs +450 -90
  63. package/get-shit-done/bin/lib/commands.cjs +489 -24
  64. package/get-shit-done/bin/lib/config.cjs +329 -48
  65. package/get-shit-done/bin/lib/core.cjs +1143 -102
  66. package/get-shit-done/bin/lib/docs.cjs +267 -0
  67. package/get-shit-done/bin/lib/frontmatter.cjs +125 -43
  68. package/get-shit-done/bin/lib/init.cjs +918 -106
  69. package/get-shit-done/bin/lib/milestone.cjs +65 -33
  70. package/get-shit-done/bin/lib/model-profiles.cjs +70 -0
  71. package/get-shit-done/bin/lib/phase.cjs +434 -404
  72. package/get-shit-done/bin/lib/profile-output.cjs +1048 -0
  73. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  74. package/get-shit-done/bin/lib/roadmap.cjs +156 -101
  75. package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
  76. package/get-shit-done/bin/lib/security.cjs +384 -0
  77. package/get-shit-done/bin/lib/state.cjs +711 -79
  78. package/get-shit-done/bin/lib/template.cjs +2 -2
  79. package/get-shit-done/bin/lib/uat.cjs +282 -0
  80. package/get-shit-done/bin/lib/verify.cjs +254 -42
  81. package/get-shit-done/bin/lib/workstream.cjs +495 -0
  82. package/get-shit-done/references/agent-contracts.md +79 -0
  83. package/get-shit-done/references/artifact-types.md +113 -0
  84. package/get-shit-done/references/checkpoints.md +12 -10
  85. package/get-shit-done/references/context-budget.md +49 -0
  86. package/get-shit-done/references/continuation-format.md +15 -15
  87. package/get-shit-done/references/decimal-phase-calculation.md +2 -3
  88. package/get-shit-done/references/domain-probes.md +125 -0
  89. package/get-shit-done/references/gate-prompts.md +100 -0
  90. package/get-shit-done/references/git-integration.md +47 -0
  91. package/get-shit-done/references/model-profile-resolution.md +2 -0
  92. package/get-shit-done/references/model-profiles.md +62 -16
  93. package/get-shit-done/references/phase-argument-parsing.md +2 -2
  94. package/get-shit-done/references/planner-gap-closure.md +62 -0
  95. package/get-shit-done/references/planner-reviews.md +39 -0
  96. package/get-shit-done/references/planner-revision.md +87 -0
  97. package/get-shit-done/references/planning-config.md +18 -1
  98. package/get-shit-done/references/revision-loop.md +97 -0
  99. package/get-shit-done/references/ui-brand.md +2 -2
  100. package/get-shit-done/references/universal-anti-patterns.md +58 -0
  101. package/get-shit-done/references/user-profiling.md +681 -0
  102. package/get-shit-done/references/workstream-flag.md +111 -0
  103. package/get-shit-done/templates/SECURITY.md +61 -0
  104. package/get-shit-done/templates/UAT.md +21 -3
  105. package/get-shit-done/templates/UI-SPEC.md +100 -0
  106. package/get-shit-done/templates/VALIDATION.md +3 -3
  107. package/get-shit-done/templates/claude-md.md +145 -0
  108. package/get-shit-done/templates/config.json +14 -3
  109. package/get-shit-done/templates/context.md +61 -6
  110. package/get-shit-done/templates/debug-subagent-prompt.md +2 -6
  111. package/get-shit-done/templates/dev-preferences.md +21 -0
  112. package/get-shit-done/templates/discussion-log.md +63 -0
  113. package/get-shit-done/templates/phase-prompt.md +46 -5
  114. package/get-shit-done/templates/planner-subagent-prompt.md +2 -10
  115. package/get-shit-done/templates/project.md +2 -0
  116. package/get-shit-done/templates/state.md +2 -2
  117. package/get-shit-done/templates/user-profile.md +146 -0
  118. package/get-shit-done/workflows/add-phase.md +4 -4
  119. package/get-shit-done/workflows/add-tests.md +4 -4
  120. package/get-shit-done/workflows/add-todo.md +4 -4
  121. package/get-shit-done/workflows/analyze-dependencies.md +96 -0
  122. package/get-shit-done/workflows/audit-milestone.md +20 -16
  123. package/get-shit-done/workflows/audit-uat.md +109 -0
  124. package/get-shit-done/workflows/autonomous.md +1036 -0
  125. package/get-shit-done/workflows/check-todos.md +4 -4
  126. package/get-shit-done/workflows/cleanup.md +4 -4
  127. package/get-shit-done/workflows/complete-milestone.md +22 -10
  128. package/get-shit-done/workflows/diagnose-issues.md +21 -7
  129. package/get-shit-done/workflows/discovery-phase.md +2 -2
  130. package/get-shit-done/workflows/discuss-phase-assumptions.md +671 -0
  131. package/get-shit-done/workflows/discuss-phase-power.md +291 -0
  132. package/get-shit-done/workflows/discuss-phase.md +558 -47
  133. package/get-shit-done/workflows/do.md +104 -0
  134. package/get-shit-done/workflows/docs-update.md +1093 -0
  135. package/get-shit-done/workflows/execute-phase.md +741 -58
  136. package/get-shit-done/workflows/execute-plan.md +77 -12
  137. package/get-shit-done/workflows/fast.md +105 -0
  138. package/get-shit-done/workflows/forensics.md +265 -0
  139. package/get-shit-done/workflows/health.md +28 -6
  140. package/get-shit-done/workflows/help.md +127 -7
  141. package/get-shit-done/workflows/insert-phase.md +4 -4
  142. package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
  143. package/get-shit-done/workflows/list-workspaces.md +56 -0
  144. package/get-shit-done/workflows/manager.md +363 -0
  145. package/get-shit-done/workflows/map-codebase.md +83 -44
  146. package/get-shit-done/workflows/milestone-summary.md +223 -0
  147. package/get-shit-done/workflows/new-milestone.md +133 -25
  148. package/get-shit-done/workflows/new-project.md +216 -54
  149. package/get-shit-done/workflows/new-workspace.md +237 -0
  150. package/get-shit-done/workflows/next.md +97 -0
  151. package/get-shit-done/workflows/node-repair.md +92 -0
  152. package/get-shit-done/workflows/note.md +156 -0
  153. package/get-shit-done/workflows/pause-work.md +132 -15
  154. package/get-shit-done/workflows/plan-milestone-gaps.md +6 -7
  155. package/get-shit-done/workflows/plan-phase.md +513 -62
  156. package/get-shit-done/workflows/plant-seed.md +169 -0
  157. package/get-shit-done/workflows/pr-branch.md +129 -0
  158. package/get-shit-done/workflows/profile-user.md +450 -0
  159. package/get-shit-done/workflows/progress.md +154 -29
  160. package/get-shit-done/workflows/quick.md +285 -111
  161. package/get-shit-done/workflows/remove-phase.md +2 -2
  162. package/get-shit-done/workflows/remove-workspace.md +90 -0
  163. package/get-shit-done/workflows/research-phase.md +13 -9
  164. package/get-shit-done/workflows/resume-project.md +37 -18
  165. package/get-shit-done/workflows/review.md +281 -0
  166. package/get-shit-done/workflows/secure-phase.md +154 -0
  167. package/get-shit-done/workflows/session-report.md +146 -0
  168. package/get-shit-done/workflows/set-profile.md +2 -2
  169. package/get-shit-done/workflows/settings.md +91 -11
  170. package/get-shit-done/workflows/ship.md +237 -0
  171. package/get-shit-done/workflows/stats.md +60 -0
  172. package/get-shit-done/workflows/transition.md +150 -23
  173. package/get-shit-done/workflows/ui-phase.md +292 -0
  174. package/get-shit-done/workflows/ui-review.md +183 -0
  175. package/get-shit-done/workflows/update.md +262 -30
  176. package/get-shit-done/workflows/validate-phase.md +14 -17
  177. package/get-shit-done/workflows/verify-phase.md +143 -11
  178. package/get-shit-done/workflows/verify-work.md +141 -39
  179. package/package.json +1 -1
  180. package/skills/gsd-audit-milestone/SKILL.md +29 -0
  181. package/skills/gsd-cleanup/SKILL.md +19 -0
  182. package/skills/gsd-complete-milestone/SKILL.md +131 -0
  183. package/skills/gsd-discuss-phase/SKILL.md +54 -0
  184. package/skills/gsd-execute-phase/SKILL.md +49 -0
  185. package/skills/gsd-plan-phase/SKILL.md +37 -0
  186. package/skills/gsd-ui-phase/SKILL.md +24 -0
  187. package/skills/gsd-ui-review/SKILL.md +24 -0
  188. package/skills/gsd-verify-work/SKILL.md +30 -0
@@ -4,10 +4,76 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, planningPaths, withPlanningLock, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches } = require('./core.cjs');
8
+
9
+ /**
10
+ * Search for a phase header (and its section) within the given content string.
11
+ * Returns a result object if found (either a full match or a malformed_roadmap
12
+ * checklist-only match), or null if the phase is not present at all.
13
+ */
14
+ function searchPhaseInContent(content, escapedPhase, phaseNum) {
15
+ // Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
16
+ const phasePattern = new RegExp(
17
+ `#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
18
+ 'i'
19
+ );
20
+ const headerMatch = content.match(phasePattern);
21
+
22
+ if (!headerMatch) {
23
+ // Fallback: check if phase exists in summary list but missing detail section
24
+ const checklistPattern = new RegExp(
25
+ `-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
26
+ 'i'
27
+ );
28
+ const checklistMatch = content.match(checklistPattern);
29
+
30
+ if (checklistMatch) {
31
+ return {
32
+ found: false,
33
+ phase_number: phaseNum,
34
+ phase_name: checklistMatch[1].trim(),
35
+ error: 'malformed_roadmap',
36
+ message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.`
37
+ };
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ const phaseName = headerMatch[1].trim();
44
+ const headerIndex = headerMatch.index;
45
+
46
+ // Find the end of this section (next ## or ### phase header, or end of file)
47
+ const restOfContent = content.slice(headerIndex);
48
+ const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
49
+ const sectionEnd = nextHeaderMatch
50
+ ? headerIndex + nextHeaderMatch.index
51
+ : content.length;
52
+
53
+ const section = content.slice(headerIndex, sectionEnd).trim();
54
+
55
+ // Extract goal if present (supports both **Goal:** and **Goal**: formats)
56
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
57
+ const goal = goalMatch ? goalMatch[1].trim() : null;
58
+
59
+ // Extract success criteria as structured array
60
+ const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
61
+ const success_criteria = criteriaMatch
62
+ ? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
63
+ : [];
64
+
65
+ return {
66
+ found: true,
67
+ phase_number: phaseNum,
68
+ phase_name: phaseName,
69
+ goal,
70
+ success_criteria,
71
+ section,
72
+ };
73
+ }
8
74
 
9
75
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
10
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
76
+ const roadmapPath = planningPaths(cwd).roadmap;
11
77
 
12
78
  if (!fs.existsSync(roadmapPath)) {
13
79
  output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
@@ -15,91 +81,48 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
15
81
  }
16
82
 
17
83
  try {
18
- const content = fs.readFileSync(roadmapPath, 'utf-8');
84
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
85
+ const milestoneContent = extractCurrentMilestone(rawContent, cwd);
19
86
 
20
87
  // Escape special regex chars in phase number, handle decimal
21
88
  const escapedPhase = escapeRegex(phaseNum);
22
89
 
23
- // Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
24
- const phasePattern = new RegExp(
25
- `#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
26
- 'i'
27
- );
28
- const headerMatch = content.match(phasePattern);
29
-
30
- if (!headerMatch) {
31
- // Fallback: check if phase exists in summary list but missing detail section
32
- const checklistPattern = new RegExp(
33
- `-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
34
- 'i'
35
- );
36
- const checklistMatch = content.match(checklistPattern);
37
-
38
- if (checklistMatch) {
39
- // Phase exists in summary but missing detail section - malformed ROADMAP
40
- output({
41
- found: false,
42
- phase_number: phaseNum,
43
- phase_name: checklistMatch[1].trim(),
44
- error: 'malformed_roadmap',
45
- message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.`
46
- }, raw, '');
47
- return;
48
- }
90
+ // Search the current milestone slice first, then fall back to full roadmap.
91
+ // A malformed_roadmap result (checklist-only) from the milestone should not
92
+ // block finding a full header match in the wider roadmap content.
93
+ const fullContent = stripShippedMilestones(rawContent);
94
+ const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
95
+ const result = (milestoneResult && !milestoneResult.error)
96
+ ? milestoneResult
97
+ : searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
49
98
 
99
+ if (!result) {
50
100
  output({ found: false, phase_number: phaseNum }, raw, '');
51
101
  return;
52
102
  }
53
103
 
54
- const phaseName = headerMatch[1].trim();
55
- const headerIndex = headerMatch.index;
56
-
57
- // Find the end of this section (next ## or ### phase header, or end of file)
58
- const restOfContent = content.slice(headerIndex);
59
- const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
60
- const sectionEnd = nextHeaderMatch
61
- ? headerIndex + nextHeaderMatch.index
62
- : content.length;
63
-
64
- const section = content.slice(headerIndex, sectionEnd).trim();
65
-
66
- // Extract goal if present
67
- const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
68
- const goal = goalMatch ? goalMatch[1].trim() : null;
69
-
70
- // Extract success criteria as structured array
71
- const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
72
- const success_criteria = criteriaMatch
73
- ? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
74
- : [];
104
+ if (result.error) {
105
+ output(result, raw, '');
106
+ return;
107
+ }
75
108
 
76
- output(
77
- {
78
- found: true,
79
- phase_number: phaseNum,
80
- phase_name: phaseName,
81
- goal,
82
- success_criteria,
83
- section,
84
- },
85
- raw,
86
- section
87
- );
109
+ output(result, raw, result.section);
88
110
  } catch (e) {
89
111
  error('Failed to read ROADMAP.md: ' + e.message);
90
112
  }
91
113
  }
92
114
 
93
115
  function cmdRoadmapAnalyze(cwd, raw) {
94
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
116
+ const roadmapPath = planningPaths(cwd).roadmap;
95
117
 
96
118
  if (!fs.existsSync(roadmapPath)) {
97
119
  output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw);
98
120
  return;
99
121
  }
100
122
 
101
- const content = fs.readFileSync(roadmapPath, 'utf-8');
102
- const phasesDir = path.join(cwd, '.planning', 'phases');
123
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
124
+ const content = extractCurrentMilestone(rawContent, cwd);
125
+ const phasesDir = planningPaths(cwd).phases;
103
126
 
104
127
  // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
105
128
  const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
@@ -117,10 +140,10 @@ function cmdRoadmapAnalyze(cwd, raw) {
117
140
  const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
118
141
  const section = content.slice(sectionStart, sectionEnd);
119
142
 
120
- const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
143
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
121
144
  const goal = goalMatch ? goalMatch[1].trim() : null;
122
145
 
123
- const dependsMatch = section.match(/\*\*Depends on:\*\*\s*([^\n]+)/i);
146
+ const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
124
147
  const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
125
148
 
126
149
  // Check completion on disk
@@ -134,7 +157,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
134
157
  try {
135
158
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
136
159
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
137
- const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
160
+ const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
138
161
 
139
162
  if (dirMatch) {
140
163
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
@@ -150,13 +173,20 @@ function cmdRoadmapAnalyze(cwd, raw) {
150
173
  else if (hasContext) diskStatus = 'discussed';
151
174
  else diskStatus = 'empty';
152
175
  }
153
- } catch {}
176
+ } catch { /* intentionally empty */ }
154
177
 
155
178
  // Check ROADMAP checkbox status
156
- const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
179
+ const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
157
180
  const checkboxMatch = content.match(checkboxPattern);
158
181
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
159
182
 
183
+ // If roadmap marks phase complete, trust that over disk file structure.
184
+ // Phases completed before GSD tracking (or via external tools) may lack
185
+ // the standard PLAN/SUMMARY pairs but are still done.
186
+ if (roadmapComplete && diskStatus !== 'complete') {
187
+ diskStatus = 'complete';
188
+ }
189
+
160
190
  phases.push({
161
191
  number: phaseNum,
162
192
  name: phaseName,
@@ -173,7 +203,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
173
203
 
174
204
  // Extract milestone info
175
205
  const milestones = [];
176
- const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
206
+ const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi;
177
207
  let mMatch;
178
208
  while ((mMatch = milestonePattern.exec(content)) !== null) {
179
209
  milestones.push({
@@ -222,7 +252,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
222
252
  error('phase number required for roadmap update-plan-progress');
223
253
  }
224
254
 
225
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
255
+ const roadmapPath = planningPaths(cwd).roadmap;
226
256
 
227
257
  const phaseInfo = findPhaseInternal(cwd, phaseNum);
228
258
  if (!phaseInfo) {
@@ -246,41 +276,66 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
246
276
  return;
247
277
  }
248
278
 
249
- let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
250
- const phaseEscaped = escapeRegex(phaseNum);
279
+ // Wrap entire read-modify-write in lock to prevent concurrent corruption
280
+ withPlanningLock(cwd, () => {
281
+ let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
282
+ const phaseEscaped = escapeRegex(phaseNum);
251
283
 
252
- // Progress table row: update Plans column (summaries/plans) and Status column
253
- const tablePattern = new RegExp(
254
- `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
255
- 'i'
256
- );
257
- const dateField = isComplete ? ` ${today} ` : ' ';
258
- roadmapContent = roadmapContent.replace(
259
- tablePattern,
260
- `$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4`
261
- );
284
+ // Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables)
285
+ const tableRowPattern = new RegExp(
286
+ `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
287
+ 'im'
288
+ );
289
+ const dateField = isComplete ? ` ${today} ` : ' ';
290
+ roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
291
+ const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
292
+ if (cells.length === 5) {
293
+ // 5-col: Phase | Milestone | Plans | Status | Completed
294
+ cells[2] = ` ${summaryCount}/${planCount} `;
295
+ cells[3] = ` ${status.padEnd(11)}`;
296
+ cells[4] = dateField;
297
+ } else if (cells.length === 4) {
298
+ // 4-col: Phase | Plans | Status | Completed
299
+ cells[1] = ` ${summaryCount}/${planCount} `;
300
+ cells[2] = ` ${status.padEnd(11)}`;
301
+ cells[3] = dateField;
302
+ }
303
+ return '|' + cells.join('|') + '|';
304
+ });
262
305
 
263
- // Update plan count in phase detail section
264
- const planCountPattern = new RegExp(
265
- `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
266
- 'i'
267
- );
268
- const planCountText = isComplete
269
- ? `${summaryCount}/${planCount} plans complete`
270
- : `${summaryCount}/${planCount} plans executed`;
271
- roadmapContent = roadmapContent.replace(planCountPattern, `$1${planCountText}`);
272
-
273
- // If complete: check checkbox
274
- if (isComplete) {
275
- const checkboxPattern = new RegExp(
276
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
306
+ // Update plan count in phase detail section
307
+ const planCountPattern = new RegExp(
308
+ `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
277
309
  'i'
278
310
  );
279
- roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
280
- }
311
+ const planCountText = isComplete
312
+ ? `${summaryCount}/${planCount} plans complete`
313
+ : `${summaryCount}/${planCount} plans executed`;
314
+ roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
315
+
316
+ // If complete: check checkbox
317
+ if (isComplete) {
318
+ const checkboxPattern = new RegExp(
319
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
320
+ 'i'
321
+ );
322
+ roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
323
+ }
281
324
 
282
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
325
+ // Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md", "- [ ] 50-01:", or "- [ ] **50-01**")
326
+ for (const summaryFile of phaseInfo.summaries) {
327
+ const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
328
+ if (!planId) continue;
329
+ const planEscaped = escapeRegex(planId);
330
+ const planCheckboxPattern = new RegExp(
331
+ `(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
332
+ 'i'
333
+ );
334
+ roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
335
+ }
283
336
 
337
+ fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
338
+ });
284
339
  output({
285
340
  updated: true,
286
341
  phase: phaseNum,
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Schema Drift Detection — Detects schema-relevant file changes and verifies
3
+ * that the appropriate database push command was executed during a phase.
4
+ *
5
+ * Prevents false-positive verification when schema files change but no push
6
+ * occurs — TypeScript types come from config, not the live database, so
7
+ * build/types pass on a broken state.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ // ─── ORM Patterns ────────────────────────────────────────────────────────────
13
+ //
14
+ // Each entry maps a glob-like pattern to an ORM name. Patterns use forward
15
+ // slashes internally — Windows backslash paths are normalized before matching.
16
+
17
+ const SCHEMA_PATTERNS = [
18
+ // Payload CMS
19
+ { pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
20
+ { pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
21
+
22
+ // Prisma
23
+ { pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' },
24
+ { pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' },
25
+
26
+ // Drizzle
27
+ { pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' },
28
+ { pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' },
29
+ { pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' },
30
+
31
+ // Supabase
32
+ { pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' },
33
+
34
+ // TypeORM
35
+ { pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' },
36
+ { pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' },
37
+ ];
38
+
39
+ // ─── Push Commands & Evidence Patterns ───────────────────────────────────────
40
+ //
41
+ // For each ORM, the push command that agents should run, plus regex patterns
42
+ // that indicate the push was actually executed (matched against execution logs,
43
+ // SUMMARY.md content, and git commit messages).
44
+
45
+ const ORM_INFO = {
46
+ payload: {
47
+ pushCommand: 'npx payload migrate',
48
+ envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate',
49
+ interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress',
50
+ evidencePatterns: [
51
+ /payload\s+migrate/i,
52
+ /PAYLOAD_MIGRATING/,
53
+ ],
54
+ },
55
+ prisma: {
56
+ pushCommand: 'npx prisma db push',
57
+ envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)',
58
+ interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass',
59
+ evidencePatterns: [
60
+ /prisma\s+db\s+push/i,
61
+ /prisma\s+migrate\s+deploy/i,
62
+ /prisma\s+migrate\s+dev/i,
63
+ ],
64
+ },
65
+ drizzle: {
66
+ pushCommand: 'npx drizzle-kit push',
67
+ envHint: 'npx drizzle-kit push',
68
+ interactiveWarning: null,
69
+ evidencePatterns: [
70
+ /drizzle-kit\s+push/i,
71
+ /drizzle-kit\s+migrate/i,
72
+ ],
73
+ },
74
+ supabase: {
75
+ pushCommand: 'supabase db push',
76
+ envHint: 'supabase db push',
77
+ interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set',
78
+ evidencePatterns: [
79
+ /supabase\s+db\s+push/i,
80
+ /supabase\s+migration\s+up/i,
81
+ ],
82
+ },
83
+ typeorm: {
84
+ pushCommand: 'npx typeorm migration:run',
85
+ envHint: 'npx typeorm migration:run -d src/data-source.ts',
86
+ interactiveWarning: null,
87
+ evidencePatterns: [
88
+ /typeorm\s+migration:run/i,
89
+ /typeorm\s+schema:sync/i,
90
+ ],
91
+ },
92
+ };
93
+
94
+ // ─── Public API ──────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Detect schema-relevant files in a list of file paths.
98
+ *
99
+ * @param {string[]} files - List of file paths (relative to project root)
100
+ * @returns {{ detected: boolean, matches: string[], orms: string[] }}
101
+ */
102
+ function detectSchemaFiles(files) {
103
+ const matches = [];
104
+ const orms = new Set();
105
+
106
+ for (const rawFile of files) {
107
+ // Normalize Windows backslash paths
108
+ const file = rawFile.replace(/\\/g, '/');
109
+
110
+ for (const { pattern, orm } of SCHEMA_PATTERNS) {
111
+ if (pattern.test(file)) {
112
+ matches.push(rawFile);
113
+ orms.add(orm);
114
+ break; // One match per file is enough
115
+ }
116
+ }
117
+ }
118
+
119
+ return {
120
+ detected: matches.length > 0,
121
+ matches,
122
+ orms: Array.from(orms),
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Get ORM-specific push command info.
128
+ *
129
+ * @param {string} ormName - ORM identifier (payload, prisma, drizzle, supabase, typeorm)
130
+ * @returns {{ pushCommand: string, envHint: string, interactiveWarning: string|null, evidencePatterns: RegExp[] } | null}
131
+ */
132
+ function detectSchemaOrm(ormName) {
133
+ return ORM_INFO[ormName] || null;
134
+ }
135
+
136
+ /**
137
+ * Check for schema drift: schema files changed but no push evidence found.
138
+ *
139
+ * @param {string[]} changedFiles - Files changed during the phase
140
+ * @param {string} executionLog - Combined text from SUMMARY.md, commit messages, and execution logs
141
+ * @param {{ skipCheck?: boolean }} [options] - Options
142
+ * @returns {{ driftDetected: boolean, blocking: boolean, schemaFiles: string[], orms: string[], unpushedOrms: string[], message: string, skipped?: boolean }}
143
+ */
144
+ function checkSchemaDrift(changedFiles, executionLog, options = {}) {
145
+ const { skipCheck = false } = options;
146
+
147
+ const detection = detectSchemaFiles(changedFiles);
148
+
149
+ if (!detection.detected) {
150
+ return {
151
+ driftDetected: false,
152
+ blocking: false,
153
+ schemaFiles: [],
154
+ orms: [],
155
+ unpushedOrms: [],
156
+ message: '',
157
+ };
158
+ }
159
+
160
+ // Check which ORMs have push evidence in the execution log
161
+ const pushedOrms = new Set();
162
+ const unpushedOrms = [];
163
+
164
+ for (const orm of detection.orms) {
165
+ const info = ORM_INFO[orm];
166
+ if (!info) continue;
167
+
168
+ const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog));
169
+ if (hasPushEvidence) {
170
+ pushedOrms.add(orm);
171
+ } else {
172
+ unpushedOrms.push(orm);
173
+ }
174
+ }
175
+
176
+ const driftDetected = unpushedOrms.length > 0;
177
+
178
+ if (!driftDetected) {
179
+ return {
180
+ driftDetected: false,
181
+ blocking: false,
182
+ schemaFiles: detection.matches,
183
+ orms: detection.orms,
184
+ unpushedOrms: [],
185
+ message: '',
186
+ };
187
+ }
188
+
189
+ // Build actionable message
190
+ const pushCommands = unpushedOrms
191
+ .map(orm => {
192
+ const info = ORM_INFO[orm];
193
+ return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null;
194
+ })
195
+ .filter(Boolean)
196
+ .join('\n');
197
+
198
+ const message = [
199
+ 'Schema drift detected: schema-relevant files changed but no database push was executed.',
200
+ '',
201
+ `Schema files changed: ${detection.matches.join(', ')}`,
202
+ `ORMs requiring push: ${unpushedOrms.join(', ')}`,
203
+ '',
204
+ 'Required push commands:',
205
+ pushCommands,
206
+ '',
207
+ 'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.',
208
+ ].join('\n');
209
+
210
+ if (skipCheck) {
211
+ return {
212
+ driftDetected: true,
213
+ blocking: false,
214
+ skipped: true,
215
+ schemaFiles: detection.matches,
216
+ orms: detection.orms,
217
+ unpushedOrms,
218
+ message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).',
219
+ };
220
+ }
221
+
222
+ return {
223
+ driftDetected: true,
224
+ blocking: true,
225
+ schemaFiles: detection.matches,
226
+ orms: detection.orms,
227
+ unpushedOrms,
228
+ message,
229
+ };
230
+ }
231
+
232
+ module.exports = {
233
+ SCHEMA_PATTERNS,
234
+ ORM_INFO,
235
+ detectSchemaFiles,
236
+ detectSchemaOrm,
237
+ checkSchemaDrift,
238
+ };