@nlaprell/shipit 1.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 (160) hide show
  1. package/.cursor/commands/create_intent_from_issue.md +28 -0
  2. package/.cursor/commands/create_pr.md +28 -0
  3. package/.cursor/commands/dashboard.md +39 -0
  4. package/.cursor/commands/deploy.md +152 -0
  5. package/.cursor/commands/drift_check.md +36 -0
  6. package/.cursor/commands/fix.md +39 -0
  7. package/.cursor/commands/generate_release_plan.md +31 -0
  8. package/.cursor/commands/generate_roadmap.md +38 -0
  9. package/.cursor/commands/help.md +37 -0
  10. package/.cursor/commands/init_project.md +26 -0
  11. package/.cursor/commands/kill.md +72 -0
  12. package/.cursor/commands/new_intent.md +68 -0
  13. package/.cursor/commands/pr.md +77 -0
  14. package/.cursor/commands/revert-plan.md +58 -0
  15. package/.cursor/commands/risk.md +64 -0
  16. package/.cursor/commands/rollback.md +43 -0
  17. package/.cursor/commands/scope_project.md +53 -0
  18. package/.cursor/commands/ship.md +345 -0
  19. package/.cursor/commands/status.md +71 -0
  20. package/.cursor/commands/suggest.md +44 -0
  21. package/.cursor/commands/test_shipit.md +197 -0
  22. package/.cursor/commands/verify.md +50 -0
  23. package/.cursor/rules/architect.mdc +84 -0
  24. package/.cursor/rules/assumption-extractor.mdc +95 -0
  25. package/.cursor/rules/docs.mdc +66 -0
  26. package/.cursor/rules/implementer.mdc +112 -0
  27. package/.cursor/rules/pm.mdc +136 -0
  28. package/.cursor/rules/qa.mdc +97 -0
  29. package/.cursor/rules/security.mdc +90 -0
  30. package/.cursor/rules/steward.mdc +99 -0
  31. package/.cursor/rules/test-runner.mdc +196 -0
  32. package/AGENTS.md +121 -0
  33. package/README.md +264 -0
  34. package/_system/architecture/CANON.md +159 -0
  35. package/_system/architecture/invariants.yml +87 -0
  36. package/_system/architecture/project-schema.json +98 -0
  37. package/_system/architecture/workflow-state-layout.md +68 -0
  38. package/_system/artifacts/SYSTEM_STATE.md +43 -0
  39. package/_system/artifacts/confidence-calibration.json +16 -0
  40. package/_system/artifacts/dependencies.md +46 -0
  41. package/_system/artifacts/framework-files-manifest.json +179 -0
  42. package/_system/artifacts/usage.json +1 -0
  43. package/_system/behaviors/DO_RELEASE.md +371 -0
  44. package/_system/behaviors/DO_RELEASE_AI.md +329 -0
  45. package/_system/behaviors/PREPARE_RELEASE.md +373 -0
  46. package/_system/behaviors/PREPARE_RELEASE_AI.md +234 -0
  47. package/_system/behaviors/WORK_ROOT_PLATFORM_ISSUES.md +140 -0
  48. package/_system/behaviors/WORK_TEST_PLAN_ISSUES.md +380 -0
  49. package/_system/do-not-repeat/abandoned-designs.md +18 -0
  50. package/_system/do-not-repeat/bad-patterns.md +19 -0
  51. package/_system/do-not-repeat/failed-experiments.md +18 -0
  52. package/_system/do-not-repeat/rejected-libraries.md +19 -0
  53. package/_system/drift/baselines.md +49 -0
  54. package/_system/drift/metrics.md +33 -0
  55. package/_system/golden-data/.gitkeep +0 -0
  56. package/_system/golden-data/README.md +47 -0
  57. package/_system/reports/mutation/mutation.html +492 -0
  58. package/_system/security/audit-allowlist.json +4 -0
  59. package/bin/create-shipit-app +29 -0
  60. package/bin/shipit +183 -0
  61. package/cli/src/commands/check.js +82 -0
  62. package/cli/src/commands/create.js +195 -0
  63. package/cli/src/commands/init.js +267 -0
  64. package/cli/src/commands/upgrade.js +196 -0
  65. package/cli/src/utils/config.js +27 -0
  66. package/cli/src/utils/file-copy.js +144 -0
  67. package/cli/src/utils/gitignore-merge.js +44 -0
  68. package/cli/src/utils/manifest.js +105 -0
  69. package/cli/src/utils/package-json-merge.js +163 -0
  70. package/cli/src/utils/project-json-merge.js +57 -0
  71. package/cli/src/utils/prompts.js +30 -0
  72. package/cli/src/utils/stack-detection.js +56 -0
  73. package/cli/src/utils/stack-files.js +364 -0
  74. package/cli/src/utils/upgrade-backup.js +159 -0
  75. package/cli/src/utils/version.js +64 -0
  76. package/dashboard-app/README.md +73 -0
  77. package/dashboard-app/eslint.config.js +23 -0
  78. package/dashboard-app/index.html +13 -0
  79. package/dashboard-app/package.json +30 -0
  80. package/dashboard-app/pnpm-lock.yaml +2721 -0
  81. package/dashboard-app/public/dashboard.json +66 -0
  82. package/dashboard-app/public/vite.svg +1 -0
  83. package/dashboard-app/src/App.css +141 -0
  84. package/dashboard-app/src/App.tsx +155 -0
  85. package/dashboard-app/src/assets/react.svg +1 -0
  86. package/dashboard-app/src/index.css +68 -0
  87. package/dashboard-app/src/main.tsx +10 -0
  88. package/dashboard-app/tsconfig.app.json +28 -0
  89. package/dashboard-app/tsconfig.json +4 -0
  90. package/dashboard-app/tsconfig.node.json +26 -0
  91. package/dashboard-app/vite.config.ts +7 -0
  92. package/package.json +116 -0
  93. package/scripts/README.md +70 -0
  94. package/scripts/audit-check.sh +125 -0
  95. package/scripts/calibration-report.sh +198 -0
  96. package/scripts/check-readiness.sh +155 -0
  97. package/scripts/collect-metrics.sh +116 -0
  98. package/scripts/command-manifest.yml +131 -0
  99. package/scripts/create-test-plan-issue.sh +110 -0
  100. package/scripts/dashboard-start.sh +16 -0
  101. package/scripts/deploy.sh +170 -0
  102. package/scripts/drift-check.sh +93 -0
  103. package/scripts/execute-rollback.sh +177 -0
  104. package/scripts/export-dashboard-json.js +208 -0
  105. package/scripts/fix-intents.sh +239 -0
  106. package/scripts/generate-dashboard.sh +136 -0
  107. package/scripts/generate-docs.sh +279 -0
  108. package/scripts/generate-project-context.sh +142 -0
  109. package/scripts/generate-release-plan.sh +443 -0
  110. package/scripts/generate-roadmap.sh +189 -0
  111. package/scripts/generate-system-state.sh +95 -0
  112. package/scripts/gh/create-intent-from-issue.sh +82 -0
  113. package/scripts/gh/create-issue-from-intent.sh +59 -0
  114. package/scripts/gh/create-pr.sh +41 -0
  115. package/scripts/gh/link-issue.sh +44 -0
  116. package/scripts/gh/on-ship-update-issue.sh +42 -0
  117. package/scripts/headless/README.md +8 -0
  118. package/scripts/headless/call-llm.js +109 -0
  119. package/scripts/headless/run-phase.sh +99 -0
  120. package/scripts/help.sh +271 -0
  121. package/scripts/init-project.sh +976 -0
  122. package/scripts/kill-intent.sh +125 -0
  123. package/scripts/lib/common.sh +29 -0
  124. package/scripts/lib/intent.sh +61 -0
  125. package/scripts/lib/progress.sh +57 -0
  126. package/scripts/lib/suggest-next.sh +131 -0
  127. package/scripts/lib/validate-intents.sh +240 -0
  128. package/scripts/lib/verify-outputs.sh +55 -0
  129. package/scripts/lib/workflow_state.sh +201 -0
  130. package/scripts/new-intent.sh +271 -0
  131. package/scripts/publish-npm.sh +28 -0
  132. package/scripts/scope-project.sh +380 -0
  133. package/scripts/setup-worktrees.sh +125 -0
  134. package/scripts/status.sh +278 -0
  135. package/scripts/suggest.sh +173 -0
  136. package/scripts/test-headless.sh +47 -0
  137. package/scripts/test-shipit.sh +52 -0
  138. package/scripts/test-workflow-state.sh +49 -0
  139. package/scripts/usage-report.sh +47 -0
  140. package/scripts/usage.sh +58 -0
  141. package/scripts/validate-cursor.sh +151 -0
  142. package/scripts/validate-project.sh +71 -0
  143. package/scripts/validate-vscode.sh +146 -0
  144. package/scripts/verify.sh +153 -0
  145. package/scripts/workflow-orchestrator.sh +97 -0
  146. package/scripts/workflow-templates/01_analysis.md.tpl +25 -0
  147. package/scripts/workflow-templates/02_plan.md.tpl +30 -0
  148. package/scripts/workflow-templates/03_implementation.md.tpl +25 -0
  149. package/scripts/workflow-templates/04_verification.md.tpl +29 -0
  150. package/scripts/workflow-templates/05_release_notes.md.tpl +16 -0
  151. package/scripts/workflow-templates/05_verification_legacy.md.tpl +6 -0
  152. package/scripts/workflow-templates/active.md.tpl +18 -0
  153. package/scripts/workflow-templates/phases.yml +39 -0
  154. package/stryker.conf.json +8 -0
  155. package/work/intent/templates/api-endpoint.md +124 -0
  156. package/work/intent/templates/bugfix.md +116 -0
  157. package/work/intent/templates/frontend-feature.md +115 -0
  158. package/work/intent/templates/generic.md +122 -0
  159. package/work/intent/templates/infra-change.md +121 -0
  160. package/work/intent/templates/refactor.md +116 -0
@@ -0,0 +1,443 @@
1
+ #!/bin/bash
2
+
3
+ # Generate Release Plan from Intents
4
+ # Produces work/release/plan.md with ordered intents by release/priority/dependencies
5
+
6
+ set -euo pipefail
7
+
8
+ error_exit() {
9
+ echo "ERROR: $1" >&2
10
+ exit "${2:-1}"
11
+ }
12
+
13
+ INTENT_DIR="work/intent"
14
+ RELEASE_DIR="work/release"
15
+ PLAN_FILE="$RELEASE_DIR/plan.md"
16
+
17
+ if [ ! -d "$INTENT_DIR" ]; then
18
+ error_exit "Intent directory not found: $INTENT_DIR" 1
19
+ fi
20
+
21
+ if ! command -v node >/dev/null 2>&1; then
22
+ error_exit "node is required to generate release plan" 1
23
+ fi
24
+
25
+ mkdir -p "$RELEASE_DIR"
26
+
27
+ # Run validation and show warnings
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ if [ -f "$SCRIPT_DIR/lib/validate-intents.sh" ]; then
30
+ source "$SCRIPT_DIR/lib/validate-intents.sh" || true
31
+ echo "Validating intents..."
32
+
33
+ # NOTE: validate_all_intents returns the number of issues found (non-zero when warnings exist).
34
+ # We want to surface warnings without failing the release plan generation.
35
+ set +e
36
+ validation_output=$(validate_all_intents 2>&1)
37
+ validation_exit=$?
38
+ set -e
39
+
40
+ if [ $validation_exit -ne 0 ] || [ -n "$validation_output" ]; then
41
+ echo ""
42
+ echo "⚠️ Validation warnings:"
43
+ echo "$validation_output" | while IFS= read -r issue; do
44
+ [ -z "$issue" ] && continue
45
+ issue_type=$(echo "$issue" | cut -d'|' -f1)
46
+ intent_id=$(echo "$issue" | cut -d'|' -f2)
47
+ message=$(echo "$issue" | cut -d'|' -f3)
48
+ echo " • $intent_id: $message"
49
+ done
50
+ echo ""
51
+ echo "💡 Run './scripts/fix-intents.sh' to auto-fix some issues"
52
+ echo ""
53
+ else
54
+ echo "✓ No validation issues found"
55
+ echo ""
56
+ fi
57
+ fi
58
+
59
+ INTENT_DIR="$INTENT_DIR" PLAN_FILE="$PLAN_FILE" node <<'NODE'
60
+ const fs = require('fs');
61
+ const path = require('path');
62
+
63
+ const intentDir = process.env.INTENT_DIR;
64
+ const planFile = process.env.PLAN_FILE;
65
+
66
+ const collectIntentFiles = (dir) => {
67
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
68
+ const files = [];
69
+ for (const entry of entries) {
70
+ const fullPath = path.join(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ files.push(...collectIntentFiles(fullPath));
73
+ } else if (entry.isFile()) {
74
+ files.push(fullPath);
75
+ }
76
+ }
77
+ return files;
78
+ };
79
+
80
+ const intentFiles = collectIntentFiles(intentDir)
81
+ .filter((file) => file.endsWith('.md') && path.basename(file) !== '_TEMPLATE.md');
82
+
83
+ const priorities = ['p0', 'p1', 'p2', 'p3'];
84
+ const efforts = ['s', 'm', 'l'];
85
+
86
+ const priorityToRelease = {
87
+ p0: 'R1',
88
+ p1: 'R1',
89
+ p2: 'R2',
90
+ p3: 'R3'
91
+ };
92
+
93
+ const parseSectionValue = (lines, header) => {
94
+ const headerLine = `## ${header}`;
95
+ const idx = lines.findIndex((line) => line.trim() === headerLine);
96
+ if (idx === -1) return '';
97
+ for (let i = idx + 1; i < lines.length; i++) {
98
+ const line = lines[i].trim();
99
+ if (!line) continue;
100
+ if (line.startsWith('## ')) break;
101
+ return line;
102
+ }
103
+ return '';
104
+ };
105
+
106
+ const normalizeReleaseTarget = (value) => {
107
+ const match = String(value).match(/R\d+/i);
108
+ return match ? match[0].toUpperCase() : '';
109
+ };
110
+
111
+ const parseReleaseTarget = (lines) => {
112
+ const headerLine = '## Release Target';
113
+ const idx = lines.findIndex((line) => line.trim().startsWith(headerLine));
114
+ if (idx === -1) return '';
115
+
116
+ // Handle inline "## Release Target: R1"
117
+ const inline = normalizeReleaseTarget(lines[idx]);
118
+ if (inline) return inline;
119
+
120
+ for (let i = idx + 1; i < lines.length; i++) {
121
+ const raw = lines[i].trim();
122
+ if (!raw) continue;
123
+ if (raw.startsWith('## ')) break;
124
+ const line = raw.replace(/^[-*]\s*/, '');
125
+ // Skip template option lines like "R1 | R2 | R3 | R4"
126
+ if (/\bR1\b\s*\|\s*\bR2\b/i.test(line)) continue;
127
+ const normalized = normalizeReleaseTarget(line);
128
+ if (normalized) return normalized;
129
+ }
130
+ return '';
131
+ };
132
+
133
+ const parseDependencies = (lines) => {
134
+ const headerLine = '## Dependencies';
135
+ const idx = lines.findIndex((line) => line.trim() === headerLine);
136
+ if (idx === -1) return [];
137
+ const deps = [];
138
+ for (let i = idx + 1; i < lines.length; i++) {
139
+ const line = lines[i].trim();
140
+ if (!line) continue;
141
+ if (line.startsWith('## ')) break;
142
+ if (line.startsWith('- ')) {
143
+ const depLine = line.replace(/^- /, '').trim();
144
+ // Extract just the intent ID (e.g., "F-002" from "F-002 (description)")
145
+ // Skip lines starting with "None", placeholders like "[...]", or "(none)"
146
+ if (!depLine || depLine.toLowerCase().startsWith('none') || depLine.startsWith('[') || depLine === '(none)') {
147
+ continue;
148
+ }
149
+ // Extract ID: first word that looks like F-XXX, B-XXX, T-XXX, etc.
150
+ const idMatch = depLine.match(/^([A-Z]-\d+)/i);
151
+ if (idMatch) {
152
+ deps.push(idMatch[1].toUpperCase());
153
+ }
154
+ }
155
+ }
156
+ return deps.filter(Boolean);
157
+ };
158
+
159
+ const intents = intentFiles.map((file) => {
160
+ const content = fs.readFileSync(file, 'utf8');
161
+ const lines = content.split('\n');
162
+ const titleLine = lines.find((line) => line.startsWith('# ')) || '';
163
+ const title = titleLine.replace(/^#\s*/, '').trim();
164
+ const id = path.basename(file, '.md');
165
+
166
+ const status = (parseSectionValue(lines, 'Status') || 'planned').toLowerCase();
167
+ const priority = (parseSectionValue(lines, 'Priority') || 'p2').toLowerCase();
168
+ const effort = (parseSectionValue(lines, 'Effort') || 'm').toLowerCase();
169
+ const releaseTarget =
170
+ parseReleaseTarget(lines) ||
171
+ normalizeReleaseTarget(lines.find((line) => /release target:/i.test(line)) || '');
172
+ const dependencies = parseDependencies(lines);
173
+
174
+ return {
175
+ id,
176
+ title,
177
+ status,
178
+ priority: priorities.includes(priority) ? priority : 'p2',
179
+ effort: efforts.includes(effort) ? effort : 'm',
180
+ releaseTarget,
181
+ dependencies
182
+ };
183
+ });
184
+
185
+ const activeStatuses = new Set(['planned', 'active', 'blocked', 'validating']);
186
+ const plannedIntents = intents.filter((i) => activeStatuses.has(i.status));
187
+ const excludedIntents = intents.filter((i) => !activeStatuses.has(i.status));
188
+
189
+ const releaseIndex = (release) => {
190
+ const match = String(release).match(/^R(\d+)$/i);
191
+ if (!match) return 0;
192
+ return Number(match[1]);
193
+ };
194
+
195
+ const defaultRelease = (intent) => {
196
+ if (intent.releaseTarget) return intent.releaseTarget.toUpperCase();
197
+ return priorityToRelease[intent.priority] || 'R2';
198
+ };
199
+
200
+ const intentMap = new Map(plannedIntents.map((i) => [i.id, i]));
201
+ const missingDeps = new Map();
202
+
203
+ // First pass: Track missing dependencies for ALL intents (before release assignment)
204
+ for (const intent of plannedIntents) {
205
+ for (const dep of intent.dependencies) {
206
+ if (!intentMap.has(dep)) {
207
+ const list = missingDeps.get(intent.id) || [];
208
+ list.push(dep);
209
+ missingDeps.set(intent.id, Array.from(new Set(list)));
210
+ }
211
+ }
212
+ }
213
+
214
+ const releases = new Map();
215
+ const explicitReleaseTargets = new Set();
216
+ plannedIntents.forEach((intent) => {
217
+ const explicit = intent.releaseTarget ? intent.releaseTarget.toUpperCase() : '';
218
+ if (explicit) {
219
+ releases.set(intent.id, explicit);
220
+ explicitReleaseTargets.add(intent.id);
221
+ } else {
222
+ releases.set(intent.id, defaultRelease(intent));
223
+ }
224
+ });
225
+
226
+ // Second pass: Adjust releases based on dependencies (for non-explicit targets)
227
+ let changed = true;
228
+ while (changed) {
229
+ changed = false;
230
+ for (const intent of plannedIntents) {
231
+ if (explicitReleaseTargets.has(intent.id)) {
232
+ continue;
233
+ }
234
+ let current = releaseIndex(releases.get(intent.id));
235
+ for (const dep of intent.dependencies) {
236
+ if (!intentMap.has(dep)) {
237
+ continue; // Already tracked in missingDeps
238
+ }
239
+ const depRelease = releaseIndex(releases.get(dep));
240
+ if (depRelease > current) {
241
+ current = depRelease;
242
+ }
243
+ }
244
+ const updated = `R${Math.max(1, current)}`;
245
+ if (releases.get(intent.id) !== updated) {
246
+ releases.set(intent.id, updated);
247
+ changed = true;
248
+ }
249
+ }
250
+ }
251
+
252
+ // Third pass: If an intent has an explicit release target, ensure dependencies are in same or earlier release
253
+ // When both have explicit targets, dependencies must come before dependents
254
+ // FIX: Respect explicit targets - if dependency is later, move dependency (not dependent) to match
255
+ for (const intent of plannedIntents) {
256
+ if (!explicitReleaseTargets.has(intent.id)) continue;
257
+ const targetRelease = releaseIndex(releases.get(intent.id));
258
+ for (const dep of intent.dependencies) {
259
+ if (!intentMap.has(dep)) continue; // Already tracked in missingDeps
260
+ const depRelease = releaseIndex(releases.get(dep));
261
+ if (explicitReleaseTargets.has(dep)) {
262
+ // Both have explicit targets: dependency must be in same or earlier release
263
+ if (depRelease > targetRelease) {
264
+ // Dependency is later than dependent - move dependency to match dependent's release
265
+ // (Dependencies must ship before dependents, so dependency moves earlier)
266
+ releases.set(dep, releases.get(intent.id));
267
+ changed = true;
268
+ }
269
+ } else {
270
+ // Dependency has no explicit target - move it to same or earlier release
271
+ if (depRelease > targetRelease) {
272
+ releases.set(dep, `R${Math.max(1, targetRelease)}`);
273
+ changed = true;
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ // Final pass: Re-adjust non-explicit targets if we moved explicit ones
280
+ if (changed) {
281
+ changed = true;
282
+ while (changed) {
283
+ changed = false;
284
+ for (const intent of plannedIntents) {
285
+ if (explicitReleaseTargets.has(intent.id)) continue;
286
+ let current = releaseIndex(releases.get(intent.id));
287
+ for (const dep of intent.dependencies) {
288
+ if (!intentMap.has(dep)) continue;
289
+ const depRelease = releaseIndex(releases.get(dep));
290
+ if (depRelease > current) {
291
+ current = depRelease;
292
+ }
293
+ }
294
+ const updated = `R${Math.max(1, current)}`;
295
+ if (releases.get(intent.id) !== updated) {
296
+ releases.set(intent.id, updated);
297
+ changed = true;
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ const releaseBuckets = new Map();
304
+ for (const intent of plannedIntents) {
305
+ const release = releases.get(intent.id) || 'R2';
306
+ const list = releaseBuckets.get(release) || [];
307
+ list.push(intent);
308
+ releaseBuckets.set(release, list);
309
+ }
310
+
311
+ const priorityRank = (priority) => priorities.indexOf(priority);
312
+ const effortRank = (effort) => efforts.indexOf(effort);
313
+
314
+ const topoSort = (items) => {
315
+ const ids = new Set(items.map((i) => i.id));
316
+ const incoming = new Map();
317
+ const outgoing = new Map();
318
+
319
+ items.forEach((item) => {
320
+ incoming.set(item.id, new Set());
321
+ outgoing.set(item.id, new Set());
322
+ });
323
+
324
+ items.forEach((item) => {
325
+ item.dependencies.forEach((dep) => {
326
+ if (!ids.has(dep)) return;
327
+ incoming.get(item.id).add(dep);
328
+ outgoing.get(dep).add(item.id);
329
+ });
330
+ });
331
+
332
+ const queue = [];
333
+ for (const [id, deps] of incoming.entries()) {
334
+ if (deps.size === 0) queue.push(id);
335
+ }
336
+
337
+ const sortQueue = () => {
338
+ queue.sort((a, b) => {
339
+ const ia = items.find((i) => i.id === a);
340
+ const ib = items.find((i) => i.id === b);
341
+ const pr = priorityRank(ia.priority) - priorityRank(ib.priority);
342
+ if (pr !== 0) return pr;
343
+ const er = effortRank(ia.effort) - effortRank(ib.effort);
344
+ if (er !== 0) return er;
345
+ return ia.id.localeCompare(ib.id);
346
+ });
347
+ };
348
+
349
+ sortQueue();
350
+ const ordered = [];
351
+ while (queue.length) {
352
+ const id = queue.shift();
353
+ ordered.push(id);
354
+ for (const dep of outgoing.get(id)) {
355
+ incoming.get(dep).delete(id);
356
+ if (incoming.get(dep).size === 0) {
357
+ queue.push(dep);
358
+ sortQueue();
359
+ }
360
+ }
361
+ }
362
+
363
+ const remaining = items
364
+ .map((i) => i.id)
365
+ .filter((id) => !ordered.includes(id));
366
+
367
+ return { ordered, remaining };
368
+ };
369
+
370
+ const baseReleases = ['R1', 'R2', 'R3', 'R4'];
371
+ const releaseOrder = Array.from(new Set([...baseReleases, ...releaseBuckets.keys()]))
372
+ .sort((a, b) => releaseIndex(a) - releaseIndex(b));
373
+
374
+ const now = new Date().toISOString();
375
+ let output = `# Release Plan\n\n**Generated:** ${now}\n\n`;
376
+ output += `## Summary\n\n`;
377
+ output += `- Total intents: ${intents.length}\n`;
378
+ output += `- Planned intents: ${plannedIntents.length}\n`;
379
+ output += `- Releases: ${releaseOrder.length}\n\n`;
380
+
381
+ releaseOrder.forEach((release) => {
382
+ const intentsInRelease = releaseBuckets.get(release) || [];
383
+ const { ordered, remaining } = topoSort(intentsInRelease);
384
+ output += `## ${release}\n\n`;
385
+ if (ordered.length === 0) {
386
+ output += `(No intents planned for ${release}.)\n\n`;
387
+ return;
388
+ }
389
+ output += `### Intent Order\n\n`;
390
+ ordered.forEach((id, index) => {
391
+ const intent = intentsInRelease.find((i) => i.id === id);
392
+ output += `${index + 1}. **${intent.id}:** ${intent.title} (priority ${intent.priority}, effort ${intent.effort}, status ${intent.status})\n`;
393
+ });
394
+ if (remaining.length) {
395
+ output += `\n### Dependency Cycles / Unordered\n\n`;
396
+ remaining.forEach((id) => {
397
+ const intent = intentsInRelease.find((i) => i.id === id);
398
+ output += `- **${intent.id}:** ${intent.title}\n`;
399
+ });
400
+ }
401
+ output += `\n`;
402
+ });
403
+
404
+ if (missingDeps.size) {
405
+ output += `## Missing Dependencies\n\n`;
406
+ for (const [id, deps] of missingDeps.entries()) {
407
+ output += `- **${id}:** ${deps.join(', ')}\n`;
408
+ }
409
+ output += `\n`;
410
+ }
411
+
412
+ if (excludedIntents.length) {
413
+ output += `## Excluded (Already Shipped or Killed)\n\n`;
414
+ excludedIntents.forEach((intent) => {
415
+ output += `- **${intent.id}:** ${intent.title} (status ${intent.status})\n`;
416
+ });
417
+ output += `\n`;
418
+ }
419
+
420
+ fs.writeFileSync(planFile, output, 'utf8');
421
+ console.log(`✓ Generated release plan: ${planFile}`);
422
+ NODE
423
+
424
+ # Verify output and show summary
425
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
426
+ if [ -f "$SCRIPT_DIR/lib/verify-outputs.sh" ]; then
427
+ source "$SCRIPT_DIR/lib/verify-outputs.sh"
428
+ echo ""
429
+ verify_file_exists "$PLAN_FILE" "work/release/plan.md"
430
+
431
+ # Count intents in plan
432
+ if [ -f "$PLAN_FILE" ]; then
433
+ intent_count=$(grep -c "^[0-9]\+\. \*\*" "$PLAN_FILE" 2>/dev/null || echo "0")
434
+ release_count=$(grep -c "^## R[0-9]" "$PLAN_FILE" 2>/dev/null || echo "0")
435
+ echo -e "${GREEN}✓${NC} Release plan contains $intent_count intent(s) across $release_count release(s)"
436
+ fi
437
+
438
+ # Show next-step suggestions
439
+ if [ -f "$SCRIPT_DIR/lib/suggest-next.sh" ]; then
440
+ source "$SCRIPT_DIR/lib/suggest-next.sh"
441
+ display_suggestions "release-plan"
442
+ fi
443
+ fi
@@ -0,0 +1,189 @@
1
+ #!/bin/bash
2
+
3
+ # Generate Project Roadmap from Intents
4
+ # Creates roadmap files and dependency visualization
5
+
6
+ set -euo pipefail
7
+
8
+ error_exit() {
9
+ echo "ERROR: $1" >&2
10
+ exit "${2:-1}"
11
+ }
12
+
13
+ # Colors
14
+ GREEN='\033[0;32m'
15
+ YELLOW='\033[1;33m'
16
+ BLUE='\033[0;34m'
17
+ NC='\033[0m'
18
+
19
+ INTENT_DIR="work/intent"
20
+ ROADMAP_DIR="work/roadmap"
21
+
22
+ if [ ! -d "$INTENT_DIR" ]; then
23
+ error_exit "Intent directory not found: $INTENT_DIR" 1
24
+ fi
25
+
26
+ echo -e "${BLUE}Generating project roadmap...${NC}"
27
+
28
+ # Initialize roadmap files
29
+ mkdir -p "$ROADMAP_DIR"
30
+
31
+ # Extract intents and categorize
32
+ NOW_INTENTS=()
33
+ NEXT_INTENTS=()
34
+ LATER_INTENTS=()
35
+
36
+ while IFS= read -r intent_file; do
37
+ [ -f "$intent_file" ] || continue
38
+
39
+ INTENT_ID=$(basename "$intent_file" .md)
40
+
41
+ # Skip template
42
+ if [ "$INTENT_ID" = "_TEMPLATE" ]; then
43
+ continue
44
+ fi
45
+
46
+ # Extract status (next non-empty line after header)
47
+ STATUS=$(awk '
48
+ $0 ~ /^## Status/ {found=1; next}
49
+ found && $0 ~ /^## / {exit}
50
+ found && $0 ~ /[^[:space:]]/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0); print tolower($0); exit}
51
+ ' "$intent_file")
52
+ [ -n "$STATUS" ] || STATUS="planned"
53
+
54
+ # Extract dependencies (lines between header and next header)
55
+ # Skip "None", "(none)", placeholder text in brackets, and empty lines
56
+ DEPENDENCIES=$(awk '
57
+ $0 ~ /^## Dependencies/ {found=1; next}
58
+ found && $0 ~ /^## / {exit}
59
+ found && $0 ~ /^[[:space:]]*- / {
60
+ line=$0; sub(/^[[:space:]]*-[[:space:]]*/,"",line); gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
61
+ # Skip empty, "(none)", lines starting with "None", or placeholder brackets
62
+ if (line == "" || line == "(none)" || tolower(line) ~ /^none/ || line ~ /^\[.*\]$/) next
63
+ print line
64
+ }
65
+ ' "$intent_file")
66
+
67
+ # Simple categorization (can be enhanced)
68
+ if [ "$STATUS" = "active" ] || [ "$STATUS" = "planned" ]; then
69
+ if [ -z "$DEPENDENCIES" ]; then
70
+ NOW_INTENTS+=("$INTENT_ID")
71
+ else
72
+ NEXT_INTENTS+=("$INTENT_ID")
73
+ fi
74
+ elif [ "$STATUS" = "blocked" ]; then
75
+ NEXT_INTENTS+=("$INTENT_ID")
76
+ else
77
+ LATER_INTENTS+=("$INTENT_ID")
78
+ fi
79
+ done < <(find "$INTENT_DIR" -type f -name "*.md" ! -name "_TEMPLATE.md" 2>/dev/null)
80
+
81
+ # Generate roadmap files
82
+ generate_roadmap_file() {
83
+ local file="$1"
84
+ local title="$2"
85
+ shift 2
86
+ local intents=()
87
+ if [ $# -gt 0 ]; then
88
+ intents=("$@")
89
+ fi
90
+
91
+ cat > "$file" << EOF || error_exit "Failed to create $file"
92
+ # $title
93
+
94
+ **Generated:** $(date -u +"%Y-%m-%dT%H:%M:%SZ")
95
+
96
+ ## Intents
97
+
98
+ EOF
99
+
100
+ if [ ${#intents[@]} -eq 0 ]; then
101
+ echo "(No intents yet. Add intents as they're planned.)" >> "$file"
102
+ else
103
+ for intent_id in "${intents[@]}"; do
104
+ INTENT_FILE=$(find "$INTENT_DIR" -type f -name "${intent_id}.md" -print -quit 2>/dev/null || true)
105
+ if [ -f "$INTENT_FILE" ]; then
106
+ TITLE=$(grep "^# " "$INTENT_FILE" | head -1 | sed 's/^# //' || echo "$intent_id")
107
+ echo "- **$intent_id:** $TITLE" >> "$file"
108
+ fi
109
+ done
110
+ fi
111
+
112
+ echo "" >> "$file"
113
+ echo "## Dependencies" >> "$file"
114
+ echo "" >> "$file"
115
+ echo "(Dependencies will be shown here)" >> "$file"
116
+ }
117
+
118
+ # Handle empty arrays safely (macOS bash + set -u workaround)
119
+ generate_roadmap_file "$ROADMAP_DIR/now.md" "Now (Current Sprint)" ${NOW_INTENTS[@]+"${NOW_INTENTS[@]}"}
120
+ generate_roadmap_file "$ROADMAP_DIR/next.md" "Next (Upcoming)" ${NEXT_INTENTS[@]+"${NEXT_INTENTS[@]}"}
121
+ generate_roadmap_file "$ROADMAP_DIR/later.md" "Later (Backlog)" ${LATER_INTENTS[@]+"${LATER_INTENTS[@]}"}
122
+
123
+ # Verify outputs and show summary
124
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
125
+ if [ -f "$SCRIPT_DIR/lib/verify-outputs.sh" ]; then
126
+ source "$SCRIPT_DIR/lib/verify-outputs.sh"
127
+ echo ""
128
+ verify_file_exists "$ROADMAP_DIR/now.md" "Updated work/roadmap/now.md"
129
+ verify_file_exists "$ROADMAP_DIR/next.md" "Updated work/roadmap/next.md"
130
+ verify_file_exists "$ROADMAP_DIR/later.md" "Updated work/roadmap/later.md"
131
+
132
+ # Count intents in each roadmap
133
+ now_count=$(grep -c "^\*\*" "$ROADMAP_DIR/now.md" 2>/dev/null || echo "0")
134
+ next_count=$(grep -c "^\*\*" "$ROADMAP_DIR/next.md" 2>/dev/null || echo "0")
135
+ later_count=$(grep -c "^\*\*" "$ROADMAP_DIR/later.md" 2>/dev/null || echo "0")
136
+ echo -e "${GREEN}✓${NC} Roadmap: $now_count in Now, $next_count in Next, $later_count in Later"
137
+
138
+ # Show next-step suggestions
139
+ if [ -f "$SCRIPT_DIR/lib/suggest-next.sh" ]; then
140
+ source "$SCRIPT_DIR/lib/suggest-next.sh"
141
+ display_suggestions "roadmap"
142
+ fi
143
+ else
144
+ echo -e "${GREEN}✓ Generated roadmap files${NC}"
145
+ fi
146
+
147
+ # Generate dependency graph
148
+ mkdir -p _system/artifacts
149
+ DEPENDENCY_FILE="_system/artifacts/dependencies.md"
150
+ cat > "$DEPENDENCY_FILE" << EOF || error_exit "Failed to create dependency file"
151
+ # Feature Dependency Graph
152
+
153
+ **Generated:** $(date -u +"%Y-%m-%dT%H:%M:%SZ")
154
+
155
+ ## Dependency Graph
156
+
157
+ EOF
158
+
159
+ while IFS= read -r intent_file; do
160
+ [ -f "$intent_file" ] || continue
161
+
162
+ INTENT_ID=$(basename "$intent_file" .md)
163
+ [ "$INTENT_ID" = "_TEMPLATE" ] && continue
164
+
165
+ TITLE=$(grep "^# " "$intent_file" | head -1 | sed 's/^# //' || echo "$intent_id")
166
+ DEPENDENCIES=$(awk '
167
+ $0 ~ /^## Dependencies/ {found=1; next}
168
+ found && $0 ~ /^## / {exit}
169
+ found && $0 ~ /^[[:space:]]*- / {line=$0; sub(/^[[:space:]]*-[[:space:]]*/,"",line); gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); if (line != "" && line != "(none)") print line}
170
+ ' "$intent_file")
171
+
172
+ echo "### $INTENT_ID: $TITLE" >> "$DEPENDENCY_FILE"
173
+ if [ -n "$DEPENDENCIES" ]; then
174
+ echo "$DEPENDENCIES" | while read -r dep; do
175
+ echo " └─> $dep" >> "$DEPENDENCY_FILE"
176
+ done
177
+ else
178
+ echo " (no dependencies)" >> "$DEPENDENCY_FILE"
179
+ fi
180
+ echo "" >> "$DEPENDENCY_FILE"
181
+ done < <(find "$INTENT_DIR" -type f -name "*.md" ! -name "_TEMPLATE.md" 2>/dev/null)
182
+
183
+ echo -e "${GREEN}✓ Generated dependency graph: $DEPENDENCY_FILE${NC}"
184
+ echo ""
185
+ echo -e "${YELLOW}Roadmap generated:${NC}"
186
+ echo " - $ROADMAP_DIR/now.md (${#NOW_INTENTS[@]} intents)"
187
+ echo " - $ROADMAP_DIR/next.md (${#NEXT_INTENTS[@]} intents)"
188
+ echo " - $ROADMAP_DIR/later.md (${#LATER_INTENTS[@]} intents)"
189
+ echo " - $DEPENDENCY_FILE"