code-as-plan 2.0.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/LICENSE +21 -0
  2. package/README.ja-JP.md +834 -0
  3. package/README.ko-KR.md +823 -0
  4. package/README.md +1006 -0
  5. package/README.pt-BR.md +452 -0
  6. package/README.zh-CN.md +800 -0
  7. package/agents/cap-brainstormer.md +154 -0
  8. package/agents/cap-debugger.md +221 -0
  9. package/agents/cap-prototyper.md +170 -0
  10. package/agents/cap-reviewer.md +230 -0
  11. package/agents/cap-tester.md +193 -0
  12. package/bin/install.js +5002 -0
  13. package/cap/bin/gsd-tools.cjs +1141 -0
  14. package/cap/bin/lib/arc-scanner.cjs +341 -0
  15. package/cap/bin/lib/cap-feature-map.cjs +506 -0
  16. package/cap/bin/lib/cap-session.cjs +191 -0
  17. package/cap/bin/lib/cap-stack-docs.cjs +598 -0
  18. package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
  19. package/cap/bin/lib/commands.cjs +959 -0
  20. package/cap/bin/lib/config.cjs +466 -0
  21. package/cap/bin/lib/convention-reader.cjs +180 -0
  22. package/cap/bin/lib/core.cjs +1230 -0
  23. package/cap/bin/lib/feature-aggregator.cjs +422 -0
  24. package/cap/bin/lib/frontmatter.cjs +336 -0
  25. package/cap/bin/lib/init.cjs +1442 -0
  26. package/cap/bin/lib/manifest-generator.cjs +381 -0
  27. package/cap/bin/lib/milestone.cjs +252 -0
  28. package/cap/bin/lib/model-profiles.cjs +68 -0
  29. package/cap/bin/lib/monorepo-context.cjs +224 -0
  30. package/cap/bin/lib/monorepo-migrator.cjs +507 -0
  31. package/cap/bin/lib/phase.cjs +888 -0
  32. package/cap/bin/lib/profile-output.cjs +952 -0
  33. package/cap/bin/lib/profile-pipeline.cjs +539 -0
  34. package/cap/bin/lib/roadmap.cjs +329 -0
  35. package/cap/bin/lib/security.cjs +382 -0
  36. package/cap/bin/lib/session-manager.cjs +290 -0
  37. package/cap/bin/lib/skeleton-generator.cjs +177 -0
  38. package/cap/bin/lib/state.cjs +1031 -0
  39. package/cap/bin/lib/template.cjs +222 -0
  40. package/cap/bin/lib/test-detector.cjs +61 -0
  41. package/cap/bin/lib/uat.cjs +282 -0
  42. package/cap/bin/lib/verify.cjs +888 -0
  43. package/cap/bin/lib/workspace-detector.cjs +369 -0
  44. package/cap/bin/lib/workstream.cjs +491 -0
  45. package/cap/commands/gsd/workstreams.md +63 -0
  46. package/cap/references/arc-standard.md +315 -0
  47. package/cap/references/cap-agent-architecture.md +102 -0
  48. package/cap/references/cap-gitignore-template +9 -0
  49. package/cap/references/cap-zero-deps.md +158 -0
  50. package/cap/references/checkpoints.md +778 -0
  51. package/cap/references/continuation-format.md +249 -0
  52. package/cap/references/decimal-phase-calculation.md +64 -0
  53. package/cap/references/feature-map-template.md +25 -0
  54. package/cap/references/git-integration.md +295 -0
  55. package/cap/references/git-planning-commit.md +38 -0
  56. package/cap/references/model-profile-resolution.md +36 -0
  57. package/cap/references/model-profiles.md +139 -0
  58. package/cap/references/phase-argument-parsing.md +61 -0
  59. package/cap/references/planning-config.md +202 -0
  60. package/cap/references/questioning.md +162 -0
  61. package/cap/references/session-template.json +8 -0
  62. package/cap/references/tdd.md +263 -0
  63. package/cap/references/ui-brand.md +160 -0
  64. package/cap/references/user-profiling.md +681 -0
  65. package/cap/references/verification-patterns.md +612 -0
  66. package/cap/references/workstream-flag.md +58 -0
  67. package/cap/templates/DEBUG.md +164 -0
  68. package/cap/templates/UAT.md +265 -0
  69. package/cap/templates/UI-SPEC.md +100 -0
  70. package/cap/templates/VALIDATION.md +76 -0
  71. package/cap/templates/claude-md.md +122 -0
  72. package/cap/templates/codebase/architecture.md +255 -0
  73. package/cap/templates/codebase/concerns.md +310 -0
  74. package/cap/templates/codebase/conventions.md +307 -0
  75. package/cap/templates/codebase/integrations.md +280 -0
  76. package/cap/templates/codebase/stack.md +186 -0
  77. package/cap/templates/codebase/structure.md +285 -0
  78. package/cap/templates/codebase/testing.md +480 -0
  79. package/cap/templates/config.json +44 -0
  80. package/cap/templates/context.md +352 -0
  81. package/cap/templates/continue-here.md +78 -0
  82. package/cap/templates/copilot-instructions.md +7 -0
  83. package/cap/templates/debug-subagent-prompt.md +91 -0
  84. package/cap/templates/dev-preferences.md +21 -0
  85. package/cap/templates/discovery.md +146 -0
  86. package/cap/templates/discussion-log.md +63 -0
  87. package/cap/templates/milestone-archive.md +123 -0
  88. package/cap/templates/milestone.md +115 -0
  89. package/cap/templates/phase-prompt.md +610 -0
  90. package/cap/templates/planner-subagent-prompt.md +117 -0
  91. package/cap/templates/project.md +186 -0
  92. package/cap/templates/requirements.md +231 -0
  93. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  94. package/cap/templates/research-project/FEATURES.md +147 -0
  95. package/cap/templates/research-project/PITFALLS.md +200 -0
  96. package/cap/templates/research-project/STACK.md +120 -0
  97. package/cap/templates/research-project/SUMMARY.md +170 -0
  98. package/cap/templates/research.md +552 -0
  99. package/cap/templates/retrospective.md +54 -0
  100. package/cap/templates/roadmap.md +202 -0
  101. package/cap/templates/state.md +176 -0
  102. package/cap/templates/summary-complex.md +59 -0
  103. package/cap/templates/summary-minimal.md +41 -0
  104. package/cap/templates/summary-standard.md +48 -0
  105. package/cap/templates/summary.md +248 -0
  106. package/cap/templates/user-profile.md +146 -0
  107. package/cap/templates/user-setup.md +311 -0
  108. package/cap/templates/verification-report.md +322 -0
  109. package/cap/workflows/add-phase.md +112 -0
  110. package/cap/workflows/add-tests.md +351 -0
  111. package/cap/workflows/add-todo.md +158 -0
  112. package/cap/workflows/audit-milestone.md +340 -0
  113. package/cap/workflows/audit-uat.md +109 -0
  114. package/cap/workflows/autonomous.md +891 -0
  115. package/cap/workflows/check-todos.md +177 -0
  116. package/cap/workflows/cleanup.md +152 -0
  117. package/cap/workflows/complete-milestone.md +767 -0
  118. package/cap/workflows/diagnose-issues.md +231 -0
  119. package/cap/workflows/discovery-phase.md +289 -0
  120. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  121. package/cap/workflows/discuss-phase.md +1049 -0
  122. package/cap/workflows/do.md +104 -0
  123. package/cap/workflows/execute-phase.md +846 -0
  124. package/cap/workflows/execute-plan.md +514 -0
  125. package/cap/workflows/fast.md +105 -0
  126. package/cap/workflows/forensics.md +265 -0
  127. package/cap/workflows/health.md +181 -0
  128. package/cap/workflows/help.md +660 -0
  129. package/cap/workflows/insert-phase.md +130 -0
  130. package/cap/workflows/list-phase-assumptions.md +178 -0
  131. package/cap/workflows/list-workspaces.md +56 -0
  132. package/cap/workflows/manager.md +362 -0
  133. package/cap/workflows/map-codebase.md +377 -0
  134. package/cap/workflows/milestone-summary.md +223 -0
  135. package/cap/workflows/new-milestone.md +486 -0
  136. package/cap/workflows/new-project.md +1250 -0
  137. package/cap/workflows/new-workspace.md +237 -0
  138. package/cap/workflows/next.md +97 -0
  139. package/cap/workflows/node-repair.md +92 -0
  140. package/cap/workflows/note.md +156 -0
  141. package/cap/workflows/pause-work.md +176 -0
  142. package/cap/workflows/plan-milestone-gaps.md +273 -0
  143. package/cap/workflows/plan-phase.md +859 -0
  144. package/cap/workflows/plant-seed.md +169 -0
  145. package/cap/workflows/pr-branch.md +129 -0
  146. package/cap/workflows/profile-user.md +450 -0
  147. package/cap/workflows/progress.md +507 -0
  148. package/cap/workflows/quick.md +757 -0
  149. package/cap/workflows/remove-phase.md +155 -0
  150. package/cap/workflows/remove-workspace.md +90 -0
  151. package/cap/workflows/research-phase.md +82 -0
  152. package/cap/workflows/resume-project.md +326 -0
  153. package/cap/workflows/review.md +228 -0
  154. package/cap/workflows/session-report.md +146 -0
  155. package/cap/workflows/settings.md +283 -0
  156. package/cap/workflows/ship.md +228 -0
  157. package/cap/workflows/stats.md +60 -0
  158. package/cap/workflows/transition.md +671 -0
  159. package/cap/workflows/ui-phase.md +302 -0
  160. package/cap/workflows/ui-review.md +165 -0
  161. package/cap/workflows/update.md +323 -0
  162. package/cap/workflows/validate-phase.md +174 -0
  163. package/cap/workflows/verify-phase.md +254 -0
  164. package/cap/workflows/verify-work.md +637 -0
  165. package/commands/cap/annotate.md +165 -0
  166. package/commands/cap/brainstorm.md +238 -0
  167. package/commands/cap/debug.md +297 -0
  168. package/commands/cap/init.md +262 -0
  169. package/commands/cap/iterate.md +234 -0
  170. package/commands/cap/prototype.md +281 -0
  171. package/commands/cap/refresh-docs.md +37 -0
  172. package/commands/cap/review.md +272 -0
  173. package/commands/cap/scan.md +249 -0
  174. package/commands/cap/start.md +234 -0
  175. package/commands/cap/status.md +189 -0
  176. package/commands/cap/test.md +250 -0
  177. package/hooks/dist/gsd-check-update.js +114 -0
  178. package/hooks/dist/gsd-context-monitor.js +156 -0
  179. package/hooks/dist/gsd-prompt-guard.js +96 -0
  180. package/hooks/dist/gsd-statusline.js +119 -0
  181. package/hooks/dist/gsd-workflow-guard.js +94 -0
  182. package/package.json +51 -0
  183. package/scripts/base64-scan.sh +262 -0
  184. package/scripts/build-hooks.js +82 -0
  185. package/scripts/cap-removal-checklist.md +202 -0
  186. package/scripts/prompt-injection-scan.sh +198 -0
  187. package/scripts/run-tests.cjs +29 -0
  188. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,506 @@
1
+ // @gsd-context CAP v2.0 Feature Map reader/writer -- FEATURE-MAP.md is the single source of truth for all features, ACs, status, and dependencies.
2
+ // @gsd-decision Markdown format for Feature Map (not JSON/YAML) -- human-readable, diffable in git, editable in any text editor. Machine-readable via regex parsing of structured table rows.
3
+ // @gsd-decision Read and write are separate operations -- no in-memory mutation API. Read returns structured data, write takes structured data and serializes to markdown.
4
+ // @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path).
5
+ // @gsd-pattern Feature Map is the bridge between all CAP workflows. Brainstorm writes entries, scan updates status, status reads for dashboard.
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ const FEATURE_MAP_FILE = 'FEATURE-MAP.md';
13
+
14
+ // @gsd-todo(ref:AC-9) Feature state lifecycle: planned -> prototyped -> tested -> shipped
15
+ const VALID_STATES = ['planned', 'prototyped', 'tested', 'shipped'];
16
+ const STATE_TRANSITIONS = {
17
+ planned: ['prototyped'],
18
+ prototyped: ['tested'],
19
+ tested: ['shipped'],
20
+ shipped: [],
21
+ };
22
+
23
+ /**
24
+ * @typedef {Object} AcceptanceCriterion
25
+ * @property {string} id - AC identifier (e.g., "AC-1")
26
+ * @property {string} description - Imperative description text
27
+ * @property {'pending'|'implemented'|'tested'|'reviewed'} status - Current status
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} Feature
32
+ * @property {string} id - Feature ID (e.g., "F-001")
33
+ * @property {string} title - Feature title (verb+object format)
34
+ * @property {'planned'|'prototyped'|'tested'|'shipped'} state - Feature lifecycle state
35
+ * @property {AcceptanceCriterion[]} acs - Acceptance criteria
36
+ * @property {string[]} files - File references linked to this feature
37
+ * @property {string[]} dependencies - Feature IDs this depends on
38
+ * @property {Object<string,string>} metadata - Additional key-value metadata
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} FeatureMap
43
+ * @property {Feature[]} features - All features
44
+ * @property {string} lastScan - ISO timestamp of last scan
45
+ */
46
+
47
+ // @gsd-todo(ref:AC-7) Feature Map is a single Markdown file at the project root named FEATURE-MAP.md
48
+
49
+ // @gsd-todo(ref:AC-1) Generate empty FEATURE-MAP.md template with section headers (Features, Legend) and no feature entries
50
+ /**
51
+ * Generate the empty FEATURE-MAP.md template for /cap:init.
52
+ * @returns {string}
53
+ */
54
+ function generateTemplate() {
55
+ return `# Feature Map
56
+
57
+ > Single source of truth for feature identity, state, acceptance criteria, and relationships.
58
+ > Auto-enriched by \`@cap-feature\` tags and dependency analysis.
59
+
60
+ ## Features
61
+
62
+ <!-- No features yet. Run /cap:brainstorm or add features with addFeature(). -->
63
+
64
+ ## Legend
65
+
66
+ | State | Meaning |
67
+ |-------|---------|
68
+ | planned | Feature identified, not yet implemented |
69
+ | prototyped | Initial implementation exists |
70
+ | tested | Tests written and passing |
71
+ | shipped | Deployed / merged to main |
72
+
73
+ ---
74
+ *Last updated: ${new Date().toISOString()}*
75
+ `;
76
+ }
77
+
78
+ // @gsd-api readFeatureMap(projectRoot) -- Reads and parses FEATURE-MAP.md from project root.
79
+ // Returns: FeatureMap object with features and lastScan timestamp.
80
+ // @gsd-todo(ref:AC-10) Feature Map is the single source of truth for feature identity, state, ACs, and relationships
81
+ /**
82
+ * @param {string} projectRoot - Absolute path to project root
83
+ * @returns {FeatureMap}
84
+ */
85
+ function readFeatureMap(projectRoot) {
86
+ const filePath = path.join(projectRoot, FEATURE_MAP_FILE);
87
+ if (!fs.existsSync(filePath)) {
88
+ return { features: [], lastScan: null };
89
+ }
90
+
91
+ const content = fs.readFileSync(filePath, 'utf8');
92
+ return parseFeatureMapContent(content);
93
+ }
94
+
95
+ // @gsd-todo(ref:AC-8) Each feature entry contains: feature ID, title, state, ACs, and file references
96
+ // @gsd-todo(ref:AC-14) Feature Map scales to 80-120 features in a single file
97
+ /**
98
+ * Parse FEATURE-MAP.md content into structured data.
99
+ * @param {string} content - Raw markdown content
100
+ * @returns {FeatureMap}
101
+ */
102
+ function parseFeatureMapContent(content) {
103
+ const features = [];
104
+ const lines = content.split('\n');
105
+
106
+ // Match feature headers: ### F-001: Title text [state]
107
+ const featureHeaderRE = /^###\s+(F-\d{3}):\s+(.+?)\s+\[(\w+)\]\s*$/;
108
+ // Match AC rows: | AC-N | status | description |
109
+ const acRowRE = /^\|\s*(AC-\d+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|/;
110
+ // Match file refs: - `path/to/file`
111
+ const fileRefRE = /^-\s+`(.+?)`/;
112
+ // Match dependencies: **Depends on:** F-001, F-002
113
+ const depsRE = /^\*\*Depends on:\*\*\s*(.+)/;
114
+ // Match lastScan in footer
115
+ const lastScanRE = /^\*Last updated:\s*(.+?)\*$/;
116
+
117
+ let currentFeature = null;
118
+ let inAcTable = false;
119
+ let inFileRefs = false;
120
+ let lastScan = null;
121
+
122
+ for (const line of lines) {
123
+ const headerMatch = line.match(featureHeaderRE);
124
+ if (headerMatch) {
125
+ if (currentFeature) features.push(currentFeature);
126
+ currentFeature = {
127
+ id: headerMatch[1],
128
+ title: headerMatch[2],
129
+ state: headerMatch[3],
130
+ acs: [],
131
+ files: [],
132
+ dependencies: [],
133
+ metadata: {},
134
+ };
135
+ inAcTable = false;
136
+ inFileRefs = false;
137
+ continue;
138
+ }
139
+
140
+ if (!currentFeature) {
141
+ const scanMatch = line.match(lastScanRE);
142
+ if (scanMatch) lastScan = scanMatch[1].trim();
143
+ continue;
144
+ }
145
+
146
+ // Detect AC table start
147
+ if (line.startsWith('| AC') && line.includes('Status')) {
148
+ inAcTable = true;
149
+ inFileRefs = false;
150
+ continue;
151
+ }
152
+ // Skip table separator
153
+ if (line.match(/^\|[\s-]+\|/)) continue;
154
+
155
+ const acMatch = line.match(acRowRE);
156
+ if (acMatch && inAcTable) {
157
+ currentFeature.acs.push({
158
+ id: acMatch[1],
159
+ description: acMatch[3].trim(),
160
+ status: acMatch[2].toLowerCase(),
161
+ });
162
+ continue;
163
+ }
164
+
165
+ // File references section
166
+ if (line.startsWith('**Files:**')) {
167
+ inFileRefs = true;
168
+ inAcTable = false;
169
+ continue;
170
+ }
171
+
172
+ if (inFileRefs) {
173
+ const refMatch = line.match(fileRefRE);
174
+ if (refMatch) {
175
+ currentFeature.files.push(refMatch[1]);
176
+ continue;
177
+ } else if (line.trim() === '') {
178
+ inFileRefs = false;
179
+ }
180
+ }
181
+
182
+ // Dependencies
183
+ const depsMatch = line.match(depsRE);
184
+ if (depsMatch) {
185
+ currentFeature.dependencies = depsMatch[1].split(',').map(d => d.trim()).filter(Boolean);
186
+ continue;
187
+ }
188
+
189
+ const scanMatch = line.match(lastScanRE);
190
+ if (scanMatch) lastScan = scanMatch[1].trim();
191
+ }
192
+
193
+ if (currentFeature) features.push(currentFeature);
194
+
195
+ return { features, lastScan };
196
+ }
197
+
198
+ // @gsd-api writeFeatureMap(projectRoot, featureMap) -- Serializes FeatureMap to FEATURE-MAP.md.
199
+ // Side effect: overwrites FEATURE-MAP.md at project root.
200
+ /**
201
+ * @param {string} projectRoot - Absolute path to project root
202
+ * @param {FeatureMap} featureMap - Structured feature map data
203
+ */
204
+ function writeFeatureMap(projectRoot, featureMap) {
205
+ const filePath = path.join(projectRoot, FEATURE_MAP_FILE);
206
+ const content = serializeFeatureMap(featureMap);
207
+ fs.writeFileSync(filePath, content, 'utf8');
208
+ }
209
+
210
+ /**
211
+ * Serialize FeatureMap to markdown string.
212
+ * @param {FeatureMap} featureMap
213
+ * @returns {string}
214
+ */
215
+ function serializeFeatureMap(featureMap) {
216
+ const lines = [
217
+ '# Feature Map',
218
+ '',
219
+ '> Single source of truth for feature identity, state, acceptance criteria, and relationships.',
220
+ '> Auto-enriched by `@cap-feature` tags and dependency analysis.',
221
+ '',
222
+ '## Features',
223
+ '',
224
+ ];
225
+
226
+ for (const feature of featureMap.features) {
227
+ lines.push(`### ${feature.id}: ${feature.title} [${feature.state}]`);
228
+ lines.push('');
229
+
230
+ if (feature.dependencies.length > 0) {
231
+ lines.push(`**Depends on:** ${feature.dependencies.join(', ')}`);
232
+ lines.push('');
233
+ }
234
+
235
+ if (feature.acs.length > 0) {
236
+ lines.push('| AC | Status | Description |');
237
+ lines.push('|----|--------|-------------|');
238
+ for (const ac of feature.acs) {
239
+ lines.push(`| ${ac.id} | ${ac.status} | ${ac.description} |`);
240
+ }
241
+ lines.push('');
242
+ }
243
+
244
+ if (feature.files.length > 0) {
245
+ lines.push('**Files:**');
246
+ for (const file of feature.files) {
247
+ lines.push(`- \`${file}\``);
248
+ }
249
+ lines.push('');
250
+ }
251
+ }
252
+
253
+ if (featureMap.features.length === 0) {
254
+ lines.push('<!-- No features yet. Run /cap:brainstorm or add features with addFeature(). -->');
255
+ lines.push('');
256
+ }
257
+
258
+ lines.push('## Legend');
259
+ lines.push('');
260
+ lines.push('| State | Meaning |');
261
+ lines.push('|-------|---------|');
262
+ lines.push('| planned | Feature identified, not yet implemented |');
263
+ lines.push('| prototyped | Initial implementation exists |');
264
+ lines.push('| tested | Tests written and passing |');
265
+ lines.push('| shipped | Deployed / merged to main |');
266
+ lines.push('');
267
+ lines.push('---');
268
+ lines.push(`*Last updated: ${new Date().toISOString()}*`);
269
+ lines.push('');
270
+
271
+ return lines.join('\n');
272
+ }
273
+
274
+ // @gsd-api addFeature(projectRoot, feature) -- Add a new feature entry to FEATURE-MAP.md.
275
+ /**
276
+ * @param {string} projectRoot - Absolute path to project root
277
+ * @param {{ title: string, acs?: AcceptanceCriterion[], dependencies?: string[], metadata?: Object }} feature - Feature data (ID auto-generated)
278
+ * @returns {Feature} - The added feature with generated ID
279
+ */
280
+ function addFeature(projectRoot, feature) {
281
+ const featureMap = readFeatureMap(projectRoot);
282
+ const id = getNextFeatureId(featureMap.features);
283
+ const newFeature = {
284
+ id,
285
+ title: feature.title,
286
+ state: 'planned',
287
+ acs: feature.acs || [],
288
+ files: [],
289
+ dependencies: feature.dependencies || [],
290
+ metadata: feature.metadata || {},
291
+ };
292
+ featureMap.features.push(newFeature);
293
+ writeFeatureMap(projectRoot, featureMap);
294
+ return newFeature;
295
+ }
296
+
297
+ // @gsd-api updateFeatureState(projectRoot, featureId, newState) -- Transition feature state.
298
+ // @gsd-todo(ref:AC-9) Enforce valid state transitions: planned->prototyped->tested->shipped
299
+ /**
300
+ * @param {string} projectRoot - Absolute path to project root
301
+ * @param {string} featureId - Feature ID (e.g., "F-001")
302
+ * @param {string} newState - Target state
303
+ * @returns {boolean} - True if transition was valid and applied
304
+ */
305
+ function updateFeatureState(projectRoot, featureId, newState) {
306
+ if (!VALID_STATES.includes(newState)) return false;
307
+
308
+ const featureMap = readFeatureMap(projectRoot);
309
+ const feature = featureMap.features.find(f => f.id === featureId);
310
+ if (!feature) return false;
311
+
312
+ const allowed = STATE_TRANSITIONS[feature.state];
313
+ if (!allowed || !allowed.includes(newState)) return false;
314
+
315
+ feature.state = newState;
316
+ writeFeatureMap(projectRoot, featureMap);
317
+ return true;
318
+ }
319
+
320
+ // @gsd-api enrichFromTags(projectRoot, scanResults) -- Update file references from tag scan.
321
+ // @gsd-todo(ref:AC-12) Feature Map auto-enriched from @cap-feature tags found in source code
322
+ /**
323
+ * @param {string} projectRoot - Absolute path to project root
324
+ * @param {import('./cap-tag-scanner.cjs').CapTag[]} scanResults - Tags from cap-tag-scanner
325
+ * @returns {FeatureMap}
326
+ */
327
+ function enrichFromTags(projectRoot, scanResults) {
328
+ const featureMap = readFeatureMap(projectRoot);
329
+
330
+ for (const tag of scanResults) {
331
+ if (tag.type !== 'feature') continue;
332
+ const featureId = tag.metadata.feature;
333
+ if (!featureId) continue;
334
+
335
+ const feature = featureMap.features.find(f => f.id === featureId);
336
+ if (!feature) continue;
337
+
338
+ // Add file reference if not already present
339
+ if (!feature.files.includes(tag.file)) {
340
+ feature.files.push(tag.file);
341
+ }
342
+ }
343
+
344
+ writeFeatureMap(projectRoot, featureMap);
345
+ return featureMap;
346
+ }
347
+
348
+ // @gsd-api enrichFromDeps(projectRoot) -- Read package.json, detect imports, add dependency info to features.
349
+ // @gsd-todo(ref:AC-13) Feature Map auto-enriched from dependency graph analysis, env vars, package.json
350
+ /**
351
+ * @param {string} projectRoot - Absolute path to project root
352
+ * @returns {{ dependencies: string[], devDependencies: string[], envVars: string[] }}
353
+ */
354
+ function enrichFromDeps(projectRoot) {
355
+ const result = { dependencies: [], devDependencies: [], envVars: [] };
356
+
357
+ const pkgPath = path.join(projectRoot, 'package.json');
358
+ if (fs.existsSync(pkgPath)) {
359
+ try {
360
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
361
+ if (pkg.dependencies) result.dependencies = Object.keys(pkg.dependencies);
362
+ if (pkg.devDependencies) result.devDependencies = Object.keys(pkg.devDependencies);
363
+ } catch (_e) {
364
+ // Malformed package.json
365
+ }
366
+ }
367
+
368
+ // Scan for .env file to detect environment variables
369
+ const envPath = path.join(projectRoot, '.env');
370
+ if (fs.existsSync(envPath)) {
371
+ try {
372
+ const envContent = fs.readFileSync(envPath, 'utf8');
373
+ const envRE = /^([A-Z_][A-Z0-9_]*)=/gm;
374
+ let match;
375
+ while ((match = envRE.exec(envContent)) !== null) {
376
+ result.envVars.push(match[1]);
377
+ }
378
+ } catch (_e) {
379
+ // Ignore
380
+ }
381
+ }
382
+
383
+ return result;
384
+ }
385
+
386
+ // @gsd-api getNextFeatureId(features) -- Generate next F-NNN ID.
387
+ /**
388
+ * @param {Feature[]} features - Existing features
389
+ * @returns {string} - Next feature ID (e.g., "F-001")
390
+ */
391
+ function getNextFeatureId(features) {
392
+ if (!features || features.length === 0) return 'F-001';
393
+
394
+ let maxNum = 0;
395
+ for (const f of features) {
396
+ const match = f.id.match(/^F-(\d+)$/);
397
+ if (match) {
398
+ const num = parseInt(match[1], 10);
399
+ if (num > maxNum) maxNum = num;
400
+ }
401
+ }
402
+
403
+ return `F-${String(maxNum + 1).padStart(3, '0')}`;
404
+ }
405
+
406
+ // @gsd-api enrichFromScan(featureMap, tags) -- Updates Feature Map status from tag scan results.
407
+ // Returns: updated FeatureMap with AC statuses reflecting code annotations.
408
+ /**
409
+ * @param {FeatureMap} featureMap - Current feature map data
410
+ * @param {import('./cap-tag-scanner.cjs').CapTag[]} tags - Tags from cap-tag-scanner
411
+ * @returns {FeatureMap}
412
+ */
413
+ function enrichFromScan(featureMap, tags) {
414
+ for (const tag of tags) {
415
+ if (tag.type !== 'feature') continue;
416
+ const featureId = tag.metadata.feature;
417
+ if (!featureId) continue;
418
+
419
+ const feature = featureMap.features.find(f => f.id === featureId);
420
+ if (!feature) continue;
421
+
422
+ // Add file reference
423
+ if (!feature.files.includes(tag.file)) {
424
+ feature.files.push(tag.file);
425
+ }
426
+
427
+ // If AC reference in metadata, mark it as implemented
428
+ const acRef = tag.metadata.ac;
429
+ if (acRef) {
430
+ const ac = feature.acs.find(a => a.id === acRef);
431
+ if (ac && ac.status === 'pending') {
432
+ ac.status = 'implemented';
433
+ }
434
+ }
435
+ }
436
+
437
+ return featureMap;
438
+ }
439
+
440
+ // @gsd-api addFeatures(featureMap, newFeatures) -- Adds new features to an existing Feature Map (from brainstorm).
441
+ // @gsd-todo(ref:AC-11) Feature Map supports auto-derivation from brainstorm output
442
+ /**
443
+ * @param {FeatureMap} featureMap - Current feature map data
444
+ * @param {Feature[]} newFeatures - Features to add
445
+ * @returns {FeatureMap}
446
+ */
447
+ function addFeatures(featureMap, newFeatures) {
448
+ const existingIds = new Set(featureMap.features.map(f => f.id));
449
+ const existingTitles = new Set(featureMap.features.map(f => f.title.toLowerCase()));
450
+
451
+ for (const nf of newFeatures) {
452
+ // Skip duplicates by ID or title
453
+ if (existingIds.has(nf.id)) continue;
454
+ if (existingTitles.has(nf.title.toLowerCase())) continue;
455
+
456
+ featureMap.features.push(nf);
457
+ existingIds.add(nf.id);
458
+ existingTitles.add(nf.title.toLowerCase());
459
+ }
460
+
461
+ return featureMap;
462
+ }
463
+
464
+ // @gsd-api getStatus(featureMap) -- Computes aggregate project status from Feature Map.
465
+ /**
466
+ * @param {FeatureMap} featureMap
467
+ * @returns {{ totalFeatures: number, completedFeatures: number, totalACs: number, implementedACs: number, testedACs: number, reviewedACs: number }}
468
+ */
469
+ function getStatus(featureMap) {
470
+ let totalFeatures = featureMap.features.length;
471
+ let completedFeatures = featureMap.features.filter(f => f.state === 'shipped').length;
472
+ let totalACs = 0;
473
+ let implementedACs = 0;
474
+ let testedACs = 0;
475
+ let reviewedACs = 0;
476
+
477
+ for (const f of featureMap.features) {
478
+ totalACs += f.acs.length;
479
+ for (const ac of f.acs) {
480
+ if (ac.status === 'implemented') implementedACs++;
481
+ if (ac.status === 'tested') testedACs++;
482
+ if (ac.status === 'reviewed') reviewedACs++;
483
+ }
484
+ }
485
+
486
+ return { totalFeatures, completedFeatures, totalACs, implementedACs, testedACs, reviewedACs };
487
+ }
488
+
489
+ module.exports = {
490
+ FEATURE_MAP_FILE,
491
+ VALID_STATES,
492
+ STATE_TRANSITIONS,
493
+ generateTemplate,
494
+ readFeatureMap,
495
+ writeFeatureMap,
496
+ parseFeatureMapContent,
497
+ serializeFeatureMap,
498
+ addFeature,
499
+ updateFeatureState,
500
+ enrichFromTags,
501
+ enrichFromDeps,
502
+ getNextFeatureId,
503
+ enrichFromScan,
504
+ addFeatures,
505
+ getStatus,
506
+ };
@@ -0,0 +1,191 @@
1
+ // @gsd-context CAP v2.0 session manager -- manages .cap/SESSION.json for cross-conversation workflow state.
2
+ // @gsd-decision SESSION.json is ephemeral (gitignored) -- it tracks the current developer's workflow state, not project state. Project state lives in FEATURE-MAP.md.
3
+ // @gsd-decision JSON format (not markdown) -- session state is machine-consumed, not human-read. JSON is faster to parse and type-safe.
4
+ // @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path).
5
+ // @gsd-pattern All session reads/writes go through this module -- no direct fs.readFileSync of SESSION.json elsewhere.
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ // @gsd-decision Session schema is flat and extensible -- new workflow commands can add keys without schema migration.
13
+ // @gsd-todo(ref:AC-16) SESSION.json tracks ephemeral workflow state: active feature ID, current workflow step, session timestamps
14
+ /**
15
+ * @typedef {Object} CapSession
16
+ * @property {string} version - Session schema version (e.g., "2.0.0")
17
+ * @property {string|null} lastCommand - Last /cap: command executed
18
+ * @property {string|null} lastCommandTimestamp - ISO timestamp of last command
19
+ * @property {string|null} activeFeature - Currently focused feature ID
20
+ * @property {string|null} step - Current workflow step
21
+ * @property {string|null} startedAt - ISO timestamp of when session started
22
+ * @property {string|null} activeDebugSession - Active debug session ID
23
+ * @property {Object<string,string>} metadata - Extensible key-value metadata
24
+ */
25
+
26
+ const CAP_DIR = '.cap';
27
+ const SESSION_FILE = 'SESSION.json';
28
+
29
+ // @gsd-todo(ref:AC-3) .cap/.gitignore ignores SESSION.json (ephemeral state shall not be committed)
30
+ const GITIGNORE_CONTENT = `# CAP ephemeral state -- do not commit
31
+ SESSION.json
32
+ debug/
33
+ `;
34
+
35
+ // @gsd-api getDefaultSession() -- Returns a fresh default session object.
36
+ // @gsd-todo(ref:AC-2) SESSION.json with valid JSON structure: { active_feature: null, step: null, started_at: null }
37
+ /**
38
+ * @returns {CapSession}
39
+ */
40
+ function getDefaultSession() {
41
+ return {
42
+ version: '2.0.0',
43
+ lastCommand: null,
44
+ lastCommandTimestamp: null,
45
+ activeFeature: null,
46
+ step: null,
47
+ startedAt: null,
48
+ activeDebugSession: null,
49
+ metadata: {},
50
+ };
51
+ }
52
+
53
+ // @gsd-api loadSession(projectRoot) -- Loads .cap/SESSION.json. Returns default session if file missing or corrupt.
54
+ // @gsd-todo(ref:AC-19) SESSION.json is the only mutable session artifact
55
+ /**
56
+ * @param {string} projectRoot - Absolute path to project root
57
+ * @returns {CapSession}
58
+ */
59
+ function loadSession(projectRoot) {
60
+ const sessionPath = path.join(projectRoot, CAP_DIR, SESSION_FILE);
61
+ try {
62
+ if (!fs.existsSync(sessionPath)) return getDefaultSession();
63
+ const content = fs.readFileSync(sessionPath, 'utf8');
64
+ const parsed = JSON.parse(content);
65
+ // Merge with defaults to handle missing keys from older versions
66
+ return { ...getDefaultSession(), ...parsed };
67
+ } catch (_e) {
68
+ // Corrupt JSON -- return default
69
+ return getDefaultSession();
70
+ }
71
+ }
72
+
73
+ // @gsd-api saveSession(projectRoot, session) -- Writes .cap/SESSION.json. Creates .cap/ directory if needed.
74
+ // @gsd-todo(ref:AC-18) SESSION.json shall not be committed to version control (enforced by .cap/.gitignore)
75
+ /**
76
+ * @param {string} projectRoot - Absolute path to project root
77
+ * @param {CapSession} session - Session data to persist
78
+ */
79
+ function saveSession(projectRoot, session) {
80
+ const capDir = path.join(projectRoot, CAP_DIR);
81
+ if (!fs.existsSync(capDir)) {
82
+ fs.mkdirSync(capDir, { recursive: true });
83
+ }
84
+ const sessionPath = path.join(capDir, SESSION_FILE);
85
+ fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2) + '\n', 'utf8');
86
+ }
87
+
88
+ // @gsd-api updateSession(projectRoot, updates) -- Partial update to session (merge, not overwrite).
89
+ /**
90
+ * @param {string} projectRoot - Absolute path to project root
91
+ * @param {Partial<CapSession>} updates - Fields to merge into current session
92
+ * @returns {CapSession} - The updated session
93
+ */
94
+ function updateSession(projectRoot, updates) {
95
+ const session = loadSession(projectRoot);
96
+ // Shallow merge -- metadata gets replaced if present in updates
97
+ const updated = { ...session, ...updates };
98
+ saveSession(projectRoot, updated);
99
+ return updated;
100
+ }
101
+
102
+ // @gsd-api startSession(projectRoot, featureId, step) -- Set active feature and step with timestamp.
103
+ // @gsd-todo(ref:AC-17) SESSION.json connects to FEATURE-MAP.md only via feature IDs (loose coupling)
104
+ /**
105
+ * @param {string} projectRoot - Absolute path to project root
106
+ * @param {string} featureId - Feature ID to focus on (e.g., "F-001")
107
+ * @param {string} step - Current workflow step name
108
+ * @returns {CapSession}
109
+ */
110
+ function startSession(projectRoot, featureId, step) {
111
+ return updateSession(projectRoot, {
112
+ activeFeature: featureId,
113
+ step,
114
+ startedAt: new Date().toISOString(),
115
+ });
116
+ }
117
+
118
+ // @gsd-api updateStep(projectRoot, step) -- Update current workflow step.
119
+ /**
120
+ * @param {string} projectRoot - Absolute path to project root
121
+ * @param {string} step - New workflow step name
122
+ * @returns {CapSession}
123
+ */
124
+ function updateStep(projectRoot, step) {
125
+ return updateSession(projectRoot, { step });
126
+ }
127
+
128
+ // @gsd-api endSession(projectRoot) -- Clear active feature and step.
129
+ /**
130
+ * @param {string} projectRoot - Absolute path to project root
131
+ * @returns {CapSession}
132
+ */
133
+ function endSession(projectRoot) {
134
+ return updateSession(projectRoot, {
135
+ activeFeature: null,
136
+ step: null,
137
+ startedAt: null,
138
+ });
139
+ }
140
+
141
+ // @gsd-api isInitialized(projectRoot) -- Check if .cap/ exists.
142
+ /**
143
+ * @param {string} projectRoot - Absolute path to project root
144
+ * @returns {boolean}
145
+ */
146
+ function isInitialized(projectRoot) {
147
+ return fs.existsSync(path.join(projectRoot, CAP_DIR));
148
+ }
149
+
150
+ // @gsd-api initCapDirectory(projectRoot) -- Creates .cap/ directory structure and .gitignore. Idempotent.
151
+ // @gsd-todo(ref:AC-4) No prompts, questions, wizards, or configuration forms
152
+ // @gsd-todo(ref:AC-5) Completes in a single invocation with no follow-up steps
153
+ // @gsd-todo(ref:AC-6) Idempotent -- running on already-initialized project shall not overwrite existing content
154
+ /**
155
+ * @param {string} projectRoot - Absolute path to project root
156
+ */
157
+ function initCapDirectory(projectRoot) {
158
+ const capDir = path.join(projectRoot, CAP_DIR);
159
+ const stackDocsDir = path.join(capDir, 'stack-docs');
160
+ const debugDir = path.join(capDir, 'debug');
161
+ const gitignorePath = path.join(capDir, '.gitignore');
162
+ const sessionPath = path.join(capDir, SESSION_FILE);
163
+
164
+ // Create directories (idempotent via recursive:true)
165
+ fs.mkdirSync(capDir, { recursive: true });
166
+ fs.mkdirSync(stackDocsDir, { recursive: true });
167
+ fs.mkdirSync(debugDir, { recursive: true });
168
+
169
+ // Write .gitignore (always overwrite -- it's infrastructure, not user content)
170
+ fs.writeFileSync(gitignorePath, GITIGNORE_CONTENT, 'utf8');
171
+
172
+ // Write SESSION.json only if it doesn't exist (preserve existing session)
173
+ if (!fs.existsSync(sessionPath)) {
174
+ saveSession(projectRoot, getDefaultSession());
175
+ }
176
+ }
177
+
178
+ module.exports = {
179
+ CAP_DIR,
180
+ SESSION_FILE,
181
+ GITIGNORE_CONTENT,
182
+ loadSession,
183
+ saveSession,
184
+ updateSession,
185
+ getDefaultSession,
186
+ startSession,
187
+ updateStep,
188
+ endSession,
189
+ isInitialized,
190
+ initCapDirectory,
191
+ };