@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.
- package/.cursor/commands/create_intent_from_issue.md +28 -0
- package/.cursor/commands/create_pr.md +28 -0
- package/.cursor/commands/dashboard.md +39 -0
- package/.cursor/commands/deploy.md +152 -0
- package/.cursor/commands/drift_check.md +36 -0
- package/.cursor/commands/fix.md +39 -0
- package/.cursor/commands/generate_release_plan.md +31 -0
- package/.cursor/commands/generate_roadmap.md +38 -0
- package/.cursor/commands/help.md +37 -0
- package/.cursor/commands/init_project.md +26 -0
- package/.cursor/commands/kill.md +72 -0
- package/.cursor/commands/new_intent.md +68 -0
- package/.cursor/commands/pr.md +77 -0
- package/.cursor/commands/revert-plan.md +58 -0
- package/.cursor/commands/risk.md +64 -0
- package/.cursor/commands/rollback.md +43 -0
- package/.cursor/commands/scope_project.md +53 -0
- package/.cursor/commands/ship.md +345 -0
- package/.cursor/commands/status.md +71 -0
- package/.cursor/commands/suggest.md +44 -0
- package/.cursor/commands/test_shipit.md +197 -0
- package/.cursor/commands/verify.md +50 -0
- package/.cursor/rules/architect.mdc +84 -0
- package/.cursor/rules/assumption-extractor.mdc +95 -0
- package/.cursor/rules/docs.mdc +66 -0
- package/.cursor/rules/implementer.mdc +112 -0
- package/.cursor/rules/pm.mdc +136 -0
- package/.cursor/rules/qa.mdc +97 -0
- package/.cursor/rules/security.mdc +90 -0
- package/.cursor/rules/steward.mdc +99 -0
- package/.cursor/rules/test-runner.mdc +196 -0
- package/AGENTS.md +121 -0
- package/README.md +264 -0
- package/_system/architecture/CANON.md +159 -0
- package/_system/architecture/invariants.yml +87 -0
- package/_system/architecture/project-schema.json +98 -0
- package/_system/architecture/workflow-state-layout.md +68 -0
- package/_system/artifacts/SYSTEM_STATE.md +43 -0
- package/_system/artifacts/confidence-calibration.json +16 -0
- package/_system/artifacts/dependencies.md +46 -0
- package/_system/artifacts/framework-files-manifest.json +179 -0
- package/_system/artifacts/usage.json +1 -0
- package/_system/behaviors/DO_RELEASE.md +371 -0
- package/_system/behaviors/DO_RELEASE_AI.md +329 -0
- package/_system/behaviors/PREPARE_RELEASE.md +373 -0
- package/_system/behaviors/PREPARE_RELEASE_AI.md +234 -0
- package/_system/behaviors/WORK_ROOT_PLATFORM_ISSUES.md +140 -0
- package/_system/behaviors/WORK_TEST_PLAN_ISSUES.md +380 -0
- package/_system/do-not-repeat/abandoned-designs.md +18 -0
- package/_system/do-not-repeat/bad-patterns.md +19 -0
- package/_system/do-not-repeat/failed-experiments.md +18 -0
- package/_system/do-not-repeat/rejected-libraries.md +19 -0
- package/_system/drift/baselines.md +49 -0
- package/_system/drift/metrics.md +33 -0
- package/_system/golden-data/.gitkeep +0 -0
- package/_system/golden-data/README.md +47 -0
- package/_system/reports/mutation/mutation.html +492 -0
- package/_system/security/audit-allowlist.json +4 -0
- package/bin/create-shipit-app +29 -0
- package/bin/shipit +183 -0
- package/cli/src/commands/check.js +82 -0
- package/cli/src/commands/create.js +195 -0
- package/cli/src/commands/init.js +267 -0
- package/cli/src/commands/upgrade.js +196 -0
- package/cli/src/utils/config.js +27 -0
- package/cli/src/utils/file-copy.js +144 -0
- package/cli/src/utils/gitignore-merge.js +44 -0
- package/cli/src/utils/manifest.js +105 -0
- package/cli/src/utils/package-json-merge.js +163 -0
- package/cli/src/utils/project-json-merge.js +57 -0
- package/cli/src/utils/prompts.js +30 -0
- package/cli/src/utils/stack-detection.js +56 -0
- package/cli/src/utils/stack-files.js +364 -0
- package/cli/src/utils/upgrade-backup.js +159 -0
- package/cli/src/utils/version.js +64 -0
- package/dashboard-app/README.md +73 -0
- package/dashboard-app/eslint.config.js +23 -0
- package/dashboard-app/index.html +13 -0
- package/dashboard-app/package.json +30 -0
- package/dashboard-app/pnpm-lock.yaml +2721 -0
- package/dashboard-app/public/dashboard.json +66 -0
- package/dashboard-app/public/vite.svg +1 -0
- package/dashboard-app/src/App.css +141 -0
- package/dashboard-app/src/App.tsx +155 -0
- package/dashboard-app/src/assets/react.svg +1 -0
- package/dashboard-app/src/index.css +68 -0
- package/dashboard-app/src/main.tsx +10 -0
- package/dashboard-app/tsconfig.app.json +28 -0
- package/dashboard-app/tsconfig.json +4 -0
- package/dashboard-app/tsconfig.node.json +26 -0
- package/dashboard-app/vite.config.ts +7 -0
- package/package.json +116 -0
- package/scripts/README.md +70 -0
- package/scripts/audit-check.sh +125 -0
- package/scripts/calibration-report.sh +198 -0
- package/scripts/check-readiness.sh +155 -0
- package/scripts/collect-metrics.sh +116 -0
- package/scripts/command-manifest.yml +131 -0
- package/scripts/create-test-plan-issue.sh +110 -0
- package/scripts/dashboard-start.sh +16 -0
- package/scripts/deploy.sh +170 -0
- package/scripts/drift-check.sh +93 -0
- package/scripts/execute-rollback.sh +177 -0
- package/scripts/export-dashboard-json.js +208 -0
- package/scripts/fix-intents.sh +239 -0
- package/scripts/generate-dashboard.sh +136 -0
- package/scripts/generate-docs.sh +279 -0
- package/scripts/generate-project-context.sh +142 -0
- package/scripts/generate-release-plan.sh +443 -0
- package/scripts/generate-roadmap.sh +189 -0
- package/scripts/generate-system-state.sh +95 -0
- package/scripts/gh/create-intent-from-issue.sh +82 -0
- package/scripts/gh/create-issue-from-intent.sh +59 -0
- package/scripts/gh/create-pr.sh +41 -0
- package/scripts/gh/link-issue.sh +44 -0
- package/scripts/gh/on-ship-update-issue.sh +42 -0
- package/scripts/headless/README.md +8 -0
- package/scripts/headless/call-llm.js +109 -0
- package/scripts/headless/run-phase.sh +99 -0
- package/scripts/help.sh +271 -0
- package/scripts/init-project.sh +976 -0
- package/scripts/kill-intent.sh +125 -0
- package/scripts/lib/common.sh +29 -0
- package/scripts/lib/intent.sh +61 -0
- package/scripts/lib/progress.sh +57 -0
- package/scripts/lib/suggest-next.sh +131 -0
- package/scripts/lib/validate-intents.sh +240 -0
- package/scripts/lib/verify-outputs.sh +55 -0
- package/scripts/lib/workflow_state.sh +201 -0
- package/scripts/new-intent.sh +271 -0
- package/scripts/publish-npm.sh +28 -0
- package/scripts/scope-project.sh +380 -0
- package/scripts/setup-worktrees.sh +125 -0
- package/scripts/status.sh +278 -0
- package/scripts/suggest.sh +173 -0
- package/scripts/test-headless.sh +47 -0
- package/scripts/test-shipit.sh +52 -0
- package/scripts/test-workflow-state.sh +49 -0
- package/scripts/usage-report.sh +47 -0
- package/scripts/usage.sh +58 -0
- package/scripts/validate-cursor.sh +151 -0
- package/scripts/validate-project.sh +71 -0
- package/scripts/validate-vscode.sh +146 -0
- package/scripts/verify.sh +153 -0
- package/scripts/workflow-orchestrator.sh +97 -0
- package/scripts/workflow-templates/01_analysis.md.tpl +25 -0
- package/scripts/workflow-templates/02_plan.md.tpl +30 -0
- package/scripts/workflow-templates/03_implementation.md.tpl +25 -0
- package/scripts/workflow-templates/04_verification.md.tpl +29 -0
- package/scripts/workflow-templates/05_release_notes.md.tpl +16 -0
- package/scripts/workflow-templates/05_verification_legacy.md.tpl +6 -0
- package/scripts/workflow-templates/active.md.tpl +18 -0
- package/scripts/workflow-templates/phases.yml +39 -0
- package/stryker.conf.json +8 -0
- package/work/intent/templates/api-endpoint.md +124 -0
- package/work/intent/templates/bugfix.md +116 -0
- package/work/intent/templates/frontend-feature.md +115 -0
- package/work/intent/templates/generic.md +122 -0
- package/work/intent/templates/infra-change.md +121 -0
- 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"
|