@ionivetech/mugiwara 0.6.0 → 0.6.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/commands/mugiwara-onboard.md +15 -0
- package/.opencode/mugiwara-helpers.mjs +24 -0
- package/.opencode/plugins/mugiwara.mjs +16 -7
- package/AGENTS.md +1 -1
- package/README.md +118 -63
- package/content/agents/luffy-orchestrator.md +14 -4
- package/content/agents/onboarding-guide.md +24 -45
- package/content/agents/zoro-execution.md +4 -0
- package/content/skills/mugiwara-backend/SKILL.md +1 -1
- package/content/skills/mugiwara-brainstorm/SKILL.md +7 -0
- package/content/skills/mugiwara-checkpoint/SKILL.md +1 -1
- package/content/skills/mugiwara-claim-audit/SKILL.md +1 -1
- package/content/skills/mugiwara-execution/SKILL.md +44 -44
- package/content/skills/mugiwara-execution/references/dispatch.md +42 -0
- package/content/skills/mugiwara-frontend/SKILL.md +1 -1
- package/content/skills/mugiwara-healing/SKILL.md +1 -1
- package/content/skills/mugiwara-orchestration/SKILL.md +50 -49
- package/content/skills/mugiwara-orchestration/references/check-ins.md +34 -0
- package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +12 -11
- package/content/skills/mugiwara-planning/SKILL.md +6 -0
- package/content/skills/mugiwara-pr/SKILL.md +18 -8
- package/content/skills/mugiwara-quality/SKILL.md +7 -0
- package/content/skills/mugiwara-ship/SKILL.md +11 -9
- package/content/skills/mugiwara-sunset/SKILL.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +7 -0
- package/content/skills/mugiwara-workflow/SKILL.md +4 -2
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +7 -6
- package/content/skills/using-mugiwara/SKILL.md +11 -1
- package/dist/mugiwara.js +186 -13
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.ts +0 -0
- package/hooks/session-start.ts +0 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/evidence.sh +16 -1
- package/scripts/gate-selftest.ts +52 -1
- package/scripts/initiative.ts +34 -20
- package/scripts/lane.sh +4 -2
- package/scripts/mission-report.sh +152 -29
- package/scripts/onboard.ts +3 -29
- package/scripts/release-notes.ts +152 -75
- package/scripts/savepoint.sh +67 -15
- package/scripts/validate-content.ts +20 -0
- package/src/cli.ts +20 -3
- package/src/installer.ts +37 -1
- package/src/mission.ts +108 -1
- package/src/targets/claude.ts +29 -8
- package/src/targets/opencode.ts +12 -8
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
# scripts/mission-report.sh — generate aggregate mission report from state.json
|
|
3
3
|
# + per-mission wave artifacts. Usage: mission-report.sh <mission>
|
|
4
4
|
# Output: .mugiwara/reports/YYYY-MM-DD-<mission>.md — one-file summary of all waves.
|
|
5
|
+
# Enriched: mission header, token budget (WARN/STOP flag), tasks (state or plan
|
|
6
|
+
# doc fallback), gate/quality excerpt from 06-closure.md, evidence file list.
|
|
7
|
+
# Every section degrades to n/a when state.json or results/ artifacts are absent.
|
|
5
8
|
set -u
|
|
6
9
|
|
|
7
10
|
MISSION="${1:-}"
|
|
@@ -17,8 +20,6 @@ RESULTS_DIR="$MUGIWARA_DIR/results/$MISSION"
|
|
|
17
20
|
REVIEW_DIR="$MUGIWARA_DIR/review"
|
|
18
21
|
ISSUES_DIR="$MUGIWARA_DIR/issues"
|
|
19
22
|
|
|
20
|
-
[ -f "$STATE_FILE" ] || { echo "mission-report: $STATE_FILE not found" >&2; exit 1; }
|
|
21
|
-
|
|
22
23
|
export STATE_FILE REPORT_DIR RESULTS_DIR REVIEW_DIR ISSUES_DIR MISSION
|
|
23
24
|
node << 'NODE'
|
|
24
25
|
const fs = require('fs');
|
|
@@ -29,33 +30,49 @@ const reportDir = process.env.REPORT_DIR || ".mugiwara/reports";
|
|
|
29
30
|
const resultsDir = process.env.RESULTS_DIR || ".mugiwara/results/unknown";
|
|
30
31
|
const reviewDir = process.env.REVIEW_DIR || ".mugiwara/review";
|
|
31
32
|
const issuesDir = process.env.ISSUES_DIR || ".mugiwara/issues";
|
|
33
|
+
const mugiDir = path.dirname(stateFile);
|
|
32
34
|
const mission = process.env.MISSION || "unknown";
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// state.json is optional — degrade gracefully, never hard-exit
|
|
39
|
+
let s = null;
|
|
40
|
+
if (fs.existsSync(stateFile)) {
|
|
41
|
+
try {
|
|
42
|
+
s = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error("mission-report: warning: " + stateFile + " unreadable — degraded report");
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
console.error("mission-report: warning: " + stateFile + " not found — degraded report");
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
const val = (obj, key, dflt) => {
|
|
51
|
+
if (!s || !obj) return dflt;
|
|
52
|
+
const v = obj[key];
|
|
53
|
+
return (v === undefined || v === null || v === "") ? dflt : v;
|
|
54
|
+
};
|
|
41
55
|
|
|
42
56
|
const now = new Date().toISOString().slice(0, 10);
|
|
43
57
|
const reportFile = path.join(reportDir, now + "-" + mission + ".md");
|
|
44
|
-
const lane = s
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
58
|
+
const lane = val(s, 'lane', 'n/a');
|
|
59
|
+
const laneReason = val(s, 'lane_reason', 'n/a');
|
|
60
|
+
const mode = val(s, 'mode', 'n/a');
|
|
61
|
+
const actor = val(s, 'actor', 'n/a');
|
|
62
|
+
const branch = val(s, 'branch', 'n/a');
|
|
63
|
+
const wave = val(s, 'wave', 'n/a');
|
|
64
|
+
const filesTouched = val(s, 'files_touched', 0);
|
|
65
|
+
const locDelta = val(s, 'loc_delta', 0);
|
|
66
|
+
const sensitive = s ? (s.sensitive_paths || []) : [];
|
|
67
|
+
const tasks = s ? (s.tasks || {}) : {};
|
|
68
|
+
const blockers = val(s, 'blockers_open', 0);
|
|
69
|
+
const healCycle = val(s, 'heal_cycle', 1);
|
|
70
|
+
const tokens = Number(val(s, 'tokens_est', 0)) || 0;
|
|
71
|
+
const tokensSource = val(s, 'tokens_source', 'n/a');
|
|
72
|
+
const budget = Number(val(s, 'budget', 0)) || 0;
|
|
73
|
+
const budgetStatus = val(s, 'budget_status', 'n/a');
|
|
74
|
+
const evidence = s ? (s.evidence || []) : [];
|
|
75
|
+
const updated = val(s, 'updated_at', now);
|
|
59
76
|
|
|
60
77
|
// --- wave artifact scan: results/<mission>/NN-*.md with verdict sniffing ---
|
|
61
78
|
const WAVE_LABEL = {
|
|
@@ -117,26 +134,133 @@ if (fs.existsSync(issuesDir)) {
|
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
// --- tasks: state.json counts, plan-doc fallback (savepoint-style counting) ---
|
|
138
|
+
function findPlan(mDir, m) {
|
|
139
|
+
const plansDir = path.join(mDir, 'plans');
|
|
140
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
141
|
+
const direct = path.join(plansDir, m + '.md');
|
|
142
|
+
if (fs.existsSync(direct)) return direct;
|
|
143
|
+
const matches = fs.readdirSync(plansDir).filter(f => f.endsWith('-' + m + '.md'));
|
|
144
|
+
return matches.length ? path.join(plansDir, matches.sort()[0]) : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let tasksDone = tasks.done || 0;
|
|
148
|
+
let tasksTotal = tasks.total || 0;
|
|
149
|
+
let tasksSource = "state.json";
|
|
150
|
+
if (tasksTotal === 0) {
|
|
151
|
+
const plan = findPlan(mugiDir, mission);
|
|
152
|
+
if (plan) {
|
|
153
|
+
const lines = fs.readFileSync(plan, 'utf8').split(/\r?\n/);
|
|
154
|
+
const done = lines.filter(l => l.includes('[x]')).length;
|
|
155
|
+
const total = done + lines.filter(l => l.includes('[ ]')).length;
|
|
156
|
+
if (total > 0) {
|
|
157
|
+
tasksDone = done;
|
|
158
|
+
tasksTotal = total;
|
|
159
|
+
tasksSource = "plan doc";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (tasksDone === 0 && tasksTotal === 0) { tasksDone = 'n/a'; tasksTotal = 'n/a'; }
|
|
164
|
+
|
|
165
|
+
// --- token budget flag: WARN/STOP line when status is not ok ---
|
|
166
|
+
let budgetFlag = "";
|
|
167
|
+
if (budgetStatus === "warn" || budgetStatus === "stop") {
|
|
168
|
+
const warnAt = Math.floor(budget * 3 / 2);
|
|
169
|
+
const stopAt = budget * 3;
|
|
170
|
+
if (budgetStatus === "stop") {
|
|
171
|
+
budgetFlag = "🛑 STOP: tokens " + tokens.toLocaleString() + " ≥ 3× budget " + budget.toLocaleString() + " (stop at " + stopAt.toLocaleString() + ") — halt; escalate to Luffy";
|
|
172
|
+
} else {
|
|
173
|
+
budgetFlag = "⚠ WARN: tokens " + tokens.toLocaleString() + " ≥ 1.5× budget " + budget.toLocaleString() + " (warn at " + warnAt.toLocaleString() + ") — checkpoint before continuing";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- gate/quality excerpt: headings + body from 06-closure.md, as-is ---
|
|
178
|
+
function sectionExcerpt(file, heading) {
|
|
179
|
+
if (!fs.existsSync(file)) return null;
|
|
180
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
181
|
+
let collecting = false;
|
|
182
|
+
let content = 0;
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const l of lines) {
|
|
185
|
+
if (/^##\s/.test(l)) {
|
|
186
|
+
if (collecting) break;
|
|
187
|
+
if (l.trim() === heading) { collecting = true; out.push(l); continue; }
|
|
188
|
+
} else if (collecting) {
|
|
189
|
+
if (l.trim()) {
|
|
190
|
+
out.push(l);
|
|
191
|
+
content++;
|
|
192
|
+
if (content >= 20) break; // cap excerpt
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return collecting ? out : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const closureFile = path.join(resultsDir, "06-closure.md");
|
|
200
|
+
const gateSec = sectionExcerpt(closureFile, "## Gate verdicts");
|
|
201
|
+
const testsSec = sectionExcerpt(closureFile, "## Tests");
|
|
202
|
+
|
|
203
|
+
// --- evidence file list: all .md artifacts in results/<mission>/ ---
|
|
204
|
+
let evidenceFiles = [];
|
|
205
|
+
if (fs.existsSync(resultsDir)) {
|
|
206
|
+
evidenceFiles = fs.readdirSync(resultsDir).filter(f => f.endsWith('.md')).sort();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- assemble report ---
|
|
120
210
|
let report = "# Mission: " + mission + " . " + now + "\n\n";
|
|
121
|
-
|
|
211
|
+
|
|
212
|
+
report += "## Mission header\n\n| Field | Value |\n|-------|-------|\n";
|
|
213
|
+
report += "| Mission | " + mission + " |\n";
|
|
214
|
+
report += "| Branch | " + branch + " |\n";
|
|
215
|
+
report += "| Lane | " + lane + " |\n";
|
|
216
|
+
report += "| Lane reason | " + laneReason + " |\n";
|
|
217
|
+
report += "| Mode | " + mode + " |\n";
|
|
218
|
+
report += "| Wave | " + wave + " |\n";
|
|
219
|
+
report += "| Actor | " + actor + " |\n\n";
|
|
220
|
+
|
|
221
|
+
report += "## Token budget\n\n| Field | Value |\n|-------|-------|\n";
|
|
222
|
+
report += "| Tokens (est) | " + tokens.toLocaleString() + " |\n";
|
|
223
|
+
report += "| Tokens source | " + tokensSource + " |\n";
|
|
224
|
+
report += "| Budget | " + budget.toLocaleString() + " |\n";
|
|
225
|
+
report += "| Budget status | " + budgetStatus + " |\n";
|
|
226
|
+
if (budgetFlag) report += "\n" + budgetFlag;
|
|
227
|
+
report += "\n\n";
|
|
228
|
+
|
|
229
|
+
report += "## Tasks\n\n| Field | Value |\n|-------|-------|\n";
|
|
230
|
+
report += "| Done | " + tasksDone + " |\n";
|
|
231
|
+
report += "| Total | " + tasksTotal + " |\n";
|
|
232
|
+
report += "| Source | " + tasksSource + " |\n\n";
|
|
122
233
|
|
|
123
234
|
report += "## What changed\n\n";
|
|
124
235
|
report += filesTouched + " files, +" + locDelta + " LOC";
|
|
125
236
|
if (sensitive.length) report += "\nSensitive paths: " + sensitive.join(", ");
|
|
237
|
+
report += "\n\n";
|
|
126
238
|
|
|
127
|
-
report += "
|
|
239
|
+
report += "## Waves\n\n| Wave | Artifact | Verdict |\n|------|----------|---------|";
|
|
128
240
|
if (waveRows.length) {
|
|
129
241
|
for (const r of waveRows) report += "\n| " + r.label + " | `" + r.file + "` | " + r.verdict + " |";
|
|
130
242
|
} else {
|
|
131
243
|
report += "\n| (no wave artifacts found in `" + resultsDir + "`) | | |";
|
|
132
244
|
}
|
|
245
|
+
report += "\n\n";
|
|
246
|
+
|
|
247
|
+
report += "## Gate & quality\n\n";
|
|
248
|
+
if (gateSec) report += gateSec.join("\n") + "\n";
|
|
249
|
+
if (testsSec) report += (gateSec ? "\n" : "") + testsSec.join("\n") + "\n";
|
|
250
|
+
if (!gateSec && !testsSec) report += "n/a\n";
|
|
251
|
+
report += "\n";
|
|
133
252
|
|
|
134
|
-
report += "
|
|
253
|
+
report += "## Evidence files\n\n";
|
|
254
|
+
report += evidenceFiles.length ? evidenceFiles.map(f => "- " + f).join("\n") : "n/a";
|
|
255
|
+
report += "\n\n";
|
|
256
|
+
|
|
257
|
+
report += "## Review & blockers\n\n";
|
|
135
258
|
report += "Review + security files: " + (reviewFiles.length ? reviewFiles.join(", ") : "none") + "\n";
|
|
136
259
|
report += "Findings: " + reviewFindings + "\n";
|
|
137
260
|
report += "Blocker ledger rows: " + issueRows.length + (issueRows.length ? "\n" + issueRows.map(r => "- " + r).join("\n") : "");
|
|
261
|
+
report += "\n\n";
|
|
138
262
|
|
|
139
|
-
report += "
|
|
263
|
+
report += "## State\n\n| Field | Value |\n|-------|-------|\n";
|
|
140
264
|
report += "| Wave | " + wave + " |\n";
|
|
141
265
|
report += "| Tasks | " + (tasks.done || 0) + "/" + (tasks.total || 0) + " done |\n";
|
|
142
266
|
report += "| Blockers open | " + blockers + " |\n";
|
|
@@ -144,9 +268,8 @@ report += "| Heal cycles | " + healCycle + " |\n";
|
|
|
144
268
|
report += "| Tokens used | " + tokens.toLocaleString() + " / " + budget.toLocaleString() + " |\n\n";
|
|
145
269
|
|
|
146
270
|
report += "## Evidence\n\n";
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
report += "\n## Updated\n\n" + updated + "\n";
|
|
271
|
+
report += evidence.length ? evidence.map(e => "- " + e).join("\n") : "n/a";
|
|
272
|
+
report += "\n\n## Updated\n\n" + updated + "\n";
|
|
150
273
|
|
|
151
274
|
fs.writeFileSync(reportFile, report);
|
|
152
275
|
console.log("\u2713 mission report: " + reportFile);
|
package/scripts/onboard.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { createInterface } from "node:readline";
|
|
|
7
7
|
const root = join(import.meta.dirname, "..");
|
|
8
8
|
const mugiwaraDir = join(root, ".mugiwara");
|
|
9
9
|
const configPath = join(mugiwaraDir, "config");
|
|
10
|
-
const onboardPath = join(mugiwaraDir, "onboard.json");
|
|
11
10
|
|
|
12
11
|
function ask(rl: import("node:readline").Interface, prompt: string): Promise<string> {
|
|
13
12
|
return new Promise((resolve) => {
|
|
@@ -38,7 +37,7 @@ async function main() {
|
|
|
38
37
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
39
38
|
console.log("Usage: bun scripts/onboard.ts");
|
|
40
39
|
console.log("Runs the Mugiwara onboarding wizard (10 fixed questions).");
|
|
41
|
-
console.log("Writes .mugiwara/config
|
|
40
|
+
console.log("Writes .mugiwara/config.");
|
|
42
41
|
process.exit(0);
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -64,10 +63,6 @@ async function main() {
|
|
|
64
63
|
console.log("");
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
const answers: Record<string, unknown> = {
|
|
68
|
-
started_at: new Date().toISOString(),
|
|
69
|
-
};
|
|
70
|
-
|
|
71
66
|
// ---- Phase 1: Project Context ----
|
|
72
67
|
console.log("── Phase 1: Project Context ──");
|
|
73
68
|
console.log("");
|
|
@@ -80,7 +75,6 @@ async function main() {
|
|
|
80
75
|
"Backend service / API",
|
|
81
76
|
"Other",
|
|
82
77
|
]);
|
|
83
|
-
answers.project_type = q1;
|
|
84
78
|
console.log("");
|
|
85
79
|
|
|
86
80
|
const q2 = await pick(rl, "Q2 — Primary language:\n [1] TypeScript\n [2] JavaScript\n [3] Python\n [4] Go\n [5] Rust\n [6] Java\n [7] Other\n > ", [
|
|
@@ -92,7 +86,6 @@ async function main() {
|
|
|
92
86
|
"Java",
|
|
93
87
|
"Other",
|
|
94
88
|
]);
|
|
95
|
-
answers.primary_language = q2;
|
|
96
89
|
console.log("");
|
|
97
90
|
|
|
98
91
|
const q3 = await pick(rl, "Q3 — Team size:\n [1] Solo\n [2] 2–5\n [3] 6–15\n [4] 16+\n > ", [
|
|
@@ -101,7 +94,6 @@ async function main() {
|
|
|
101
94
|
"6–15",
|
|
102
95
|
"16+",
|
|
103
96
|
]);
|
|
104
|
-
answers.team_size = q3;
|
|
105
97
|
console.log("");
|
|
106
98
|
|
|
107
99
|
const q4 = await pick(rl, "Q4 — Git workflow:\n [1] Trunk-based (feature/{type}-{issue}-{slug})\n [2] GitFlow (feature/{slug})\n [3] GitHub Flow (feat/{slug})\n [4] Other (feature/{slug})\n > ", [
|
|
@@ -110,7 +102,6 @@ async function main() {
|
|
|
110
102
|
"github-flow",
|
|
111
103
|
"other",
|
|
112
104
|
]);
|
|
113
|
-
answers.git_workflow = q4;
|
|
114
105
|
console.log("");
|
|
115
106
|
|
|
116
107
|
const q5 = await pick(rl, "Q5 — CI/CD platform:\n [1] GitHub Actions\n [2] GitLab CI\n [3] CircleCI\n [4] Jenkins\n [5] None / manual\n [6] Other\n > ", [
|
|
@@ -121,7 +112,6 @@ async function main() {
|
|
|
121
112
|
"None / manual",
|
|
122
113
|
"Other",
|
|
123
114
|
]);
|
|
124
|
-
answers.ci_cd = q5;
|
|
125
115
|
console.log("");
|
|
126
116
|
|
|
127
117
|
// ---- Phase 2: Mugiwara Preferences ----
|
|
@@ -133,27 +123,20 @@ async function main() {
|
|
|
133
123
|
"semi",
|
|
134
124
|
"auto",
|
|
135
125
|
]);
|
|
136
|
-
answers.autonomy_mode = q6;
|
|
137
|
-
console.log("");
|
|
138
|
-
|
|
139
|
-
const q7 = await ask(rl, "Q7 — Agents to enable (comma-separated or 'all'):\n Available: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing\n Default: all\n > ");
|
|
140
|
-
answers.enabled_agents = q7 || "all";
|
|
141
126
|
console.log("");
|
|
142
127
|
|
|
143
|
-
const q8a = await pick(rl, "
|
|
128
|
+
const q8a = await pick(rl, "Q7 — Code review depth:\n [1] full — breaking-change map, five-axis review, ≤3 cycles\n [2] standard — five-axis review, 1 cycle\n [3] quick — diff-only, no caller-map\n > ", [
|
|
144
129
|
"full",
|
|
145
130
|
"standard",
|
|
146
131
|
"quick",
|
|
147
132
|
]);
|
|
148
|
-
answers.review_depth = q8a;
|
|
149
133
|
console.log("");
|
|
150
134
|
|
|
151
|
-
const q8b = await pick(rl, "
|
|
135
|
+
const q8b = await pick(rl, "Q8 — Quality check depth:\n [1] full — format, lint, typecheck, test, build\n [2] standard — lint, typecheck, test\n [3] quick — test only\n > ", [
|
|
152
136
|
"full",
|
|
153
137
|
"standard",
|
|
154
138
|
"quick",
|
|
155
139
|
]);
|
|
156
|
-
answers.quality_depth = q8b;
|
|
157
140
|
console.log("");
|
|
158
141
|
|
|
159
142
|
console.log("Q9 — Test coverage threshold:");
|
|
@@ -180,9 +163,6 @@ async function main() {
|
|
|
180
163
|
coverageNew = 0;
|
|
181
164
|
coverageModified = 0;
|
|
182
165
|
}
|
|
183
|
-
answers.coverage_threshold = q9;
|
|
184
|
-
answers.coverage_new = coverageNew;
|
|
185
|
-
answers.coverage_modified = coverageModified;
|
|
186
166
|
console.log("");
|
|
187
167
|
|
|
188
168
|
const q10 = await pick(rl, "Q10 — Commit style:\n [1] Conventional Commits (feat:, fix:, chore:, docs:)\n [2] Semantic (type(scope): message)\n [3] Free-form\n > ", [
|
|
@@ -190,8 +170,6 @@ async function main() {
|
|
|
190
170
|
"semantic",
|
|
191
171
|
"free-form",
|
|
192
172
|
]);
|
|
193
|
-
answers.commit_style = q10;
|
|
194
|
-
answers.completed_at = new Date().toISOString();
|
|
195
173
|
console.log("");
|
|
196
174
|
|
|
197
175
|
// ---- Branch name format ----
|
|
@@ -230,11 +208,9 @@ async function main() {
|
|
|
230
208
|
commit,
|
|
231
209
|
review_depth: reviewDepth,
|
|
232
210
|
quality_depth: qualityDepth,
|
|
233
|
-
enabled_agents: answers.enabled_agents,
|
|
234
211
|
};
|
|
235
212
|
|
|
236
213
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
237
|
-
writeFileSync(onboardPath, JSON.stringify(answers, null, 2) + "\n");
|
|
238
214
|
|
|
239
215
|
// ---- Summary ----
|
|
240
216
|
const qLabels: Record<string, Record<number, string>> = {
|
|
@@ -275,14 +251,12 @@ async function main() {
|
|
|
275
251
|
console.log(" Git workflow: ", qLabels.git_workflow[q4]);
|
|
276
252
|
console.log(" CI/CD: ", qLabels.ci_cd[q5]);
|
|
277
253
|
console.log(" Autonomy mode: ", qLabels.autonomy_mode[q6]);
|
|
278
|
-
console.log(" Enabled agents: ", answers.enabled_agents);
|
|
279
254
|
console.log(" Review depth: ", qLabels.review_depth[q8a]);
|
|
280
255
|
console.log(" Quality depth: ", qLabels.quality_depth[q8b]);
|
|
281
256
|
console.log(" Coverage: ", q9 === 3 ? `${coverageNew}/${coverageModified}` : qLabels.coverage_threshold[q9]);
|
|
282
257
|
console.log(" Commit style: ", qLabels.commit_style[q10]);
|
|
283
258
|
console.log("");
|
|
284
259
|
console.log(` Config written: ${configPath}`);
|
|
285
|
-
console.log(` Audit trail: ${onboardPath}`);
|
|
286
260
|
console.log("");
|
|
287
261
|
} finally {
|
|
288
262
|
rl.close();
|
package/scripts/release-notes.ts
CHANGED
|
@@ -1,94 +1,171 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// scripts/release-notes.ts — generates a detailed GitHub Release description
|
|
3
3
|
// from git history between the previous tag and HEAD. Pulls the full commit
|
|
4
|
-
// message (subject + body), groups by conventional-commit
|
|
5
|
-
//
|
|
4
|
+
// message (subject + body), groups by conventional-commit scope (feature area)
|
|
5
|
+
// in first-appearance order, falls back to type for unscoped commits, flags
|
|
6
|
+
// breaking changes, and lists the affected scopes. No gh dependency.
|
|
6
7
|
//
|
|
7
8
|
// bun scripts/release-notes.ts notes since the last tag → stdout
|
|
8
9
|
// bun scripts/release-notes.ts --since v0.3.0 notes from that tag → stdout
|
|
9
10
|
import { execFileSync } from 'node:child_process';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.split(/\r?\n/).filter(Boolean);
|
|
22
|
-
since = tags.length >= 2 ? tags[1] : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const range = since ? `${since}..HEAD` : '';
|
|
26
|
-
const raw = execFileSync('git', ['log', '--pretty=%H%n%s%n%n%b%n__END__', ...(range ? [range] : [])], { encoding: 'utf8' });
|
|
27
|
-
const commits = raw
|
|
28
|
-
.split('__END__\n')
|
|
29
|
-
.map(c => c.trim())
|
|
30
|
-
.filter(Boolean)
|
|
31
|
-
.map(c => {
|
|
32
|
-
const [sha, subject, ...rest] = c.split('\n');
|
|
33
|
-
return { sha: sha.slice(0, 7), subject: subject ?? '', body: rest.join('\n').trim() };
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const groups: Record<string, { label: string; items: string[] }> = {
|
|
37
|
-
feat: { label: 'New', items: [] },
|
|
38
|
-
fix: { label: 'Fixed', items: [] },
|
|
39
|
-
refactor: { label: 'Refactored', items: [] },
|
|
40
|
-
perf: { label: 'Performance', items: [] },
|
|
41
|
-
docs: { label: 'Docs', items: [] },
|
|
42
|
-
chore: { label: 'Housekeeping', items: [] },
|
|
43
|
-
test: { label: 'Housekeeping', items: [] },
|
|
44
|
-
ci: { label: 'Housekeeping', items: [] },
|
|
12
|
+
// human label for a conventional-commit type: section fallback + per-bullet tag
|
|
13
|
+
const TYPE_LABEL: Record<string, string> = {
|
|
14
|
+
feat: 'New',
|
|
15
|
+
fix: 'Fixed',
|
|
16
|
+
refactor: 'Refactored',
|
|
17
|
+
perf: 'Performance',
|
|
18
|
+
docs: 'Docs',
|
|
19
|
+
chore: 'Housekeeping',
|
|
20
|
+
test: 'Housekeeping',
|
|
21
|
+
ci: 'Housekeeping',
|
|
45
22
|
};
|
|
46
23
|
|
|
47
|
-
const
|
|
24
|
+
const TYPE_ORDER = ['feat', 'fix', 'refactor', 'perf', 'docs', 'chore'];
|
|
48
25
|
|
|
49
26
|
// strip signature trailers (Co-authored-by, Signed-off-by, review notes)
|
|
50
27
|
const TRAILER = /^(Co-authored-by|Signed-off-by|Reviewed-by|Helped-by|Reported-by|Tested-by|Acked-by):/i;
|
|
51
28
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.map(l => l.trim())
|
|
66
|
-
.filter(l => l && !TRAILER.test(l) && l !== '---')
|
|
67
|
-
// the release-version marker commit repeats the subject as body — drop it
|
|
68
|
-
.filter(l => l !== text);
|
|
69
|
-
|
|
70
|
-
let entry = `- ${title}${scopeTag}${breakingTag} \`${commit.sha}\``;
|
|
71
|
-
if (bodyLines.length) {
|
|
72
|
-
entry += '\n' + bodyLines.map(l => ` - ${l}`).join('\n');
|
|
73
|
-
}
|
|
74
|
-
const g = groups[type] ?? groups.chore;
|
|
75
|
-
g.items.push(entry);
|
|
29
|
+
// capitalize the first letter of each word; split on non-alphanumerics
|
|
30
|
+
// (opencode → Opencode, release-notes → Release Notes)
|
|
31
|
+
const titleCase = (s: string) =>
|
|
32
|
+
s
|
|
33
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
36
|
+
.join(' ');
|
|
37
|
+
|
|
38
|
+
export interface ReleaseCommit {
|
|
39
|
+
sha: string;
|
|
40
|
+
subject: string;
|
|
41
|
+
body: string;
|
|
76
42
|
}
|
|
77
43
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Group parsed commits into per-scope sections (first-appearance order) with a
|
|
46
|
+
* type-fallback for unscoped commits. Returns the rendered markdown body and
|
|
47
|
+
* the change count. Pure — no git access, deterministic. Exported so tests
|
|
48
|
+
* exercise the grouping without invoking git.
|
|
49
|
+
*/
|
|
50
|
+
export function buildNotes(commits: ReleaseCommit[]): { count: number; markdown: string } {
|
|
51
|
+
// scoped (feature-area) sections, keyed by lowercased scope, first-appearance order
|
|
52
|
+
const scoped = new Map<string, { label: string; items: string[] }>();
|
|
53
|
+
const scopedOrder: string[] = [];
|
|
54
|
+
// unscoped fallback sections by type, rendered in canonical type order
|
|
55
|
+
const byType = new Map<string, { label: string; items: string[] }>();
|
|
56
|
+
for (const t of TYPE_ORDER) byType.set(t, { label: TYPE_LABEL[t], items: [] });
|
|
57
|
+
|
|
58
|
+
for (const commit of commits) {
|
|
59
|
+
const m = /^(\w+)(?:\((.*?)\))?(!)?: (.*)/.exec(commit.subject);
|
|
60
|
+
const type = m ? m[1] : 'chore';
|
|
61
|
+
const scope = m?.[2] || '';
|
|
62
|
+
const text = m ? m[4] : commit.subject;
|
|
63
|
+
// the conventional-commits `!` breaking marker sits between scope and
|
|
64
|
+
// colon; capture it positionally so `fix: handle a! in parser` is NOT
|
|
65
|
+
// flagged breaking (a bare `!` anywhere in the subject would be)
|
|
66
|
+
const breaking = m?.[3] === '!' || /^BREAKING CHANGE:/m.test(commit.body);
|
|
67
|
+
|
|
68
|
+
const title = text.charAt(0).toUpperCase() + text.slice(1);
|
|
69
|
+
const breakingTag = breaking ? ' ⚠️ **BREAKING**' : '';
|
|
70
|
+
|
|
71
|
+
const bodyLines = commit.body
|
|
72
|
+
.split(/\r?\n/)
|
|
73
|
+
.map(l => l.trim())
|
|
74
|
+
.filter(l => l && !TRAILER.test(l) && l !== '---')
|
|
75
|
+
// the release-version marker commit repeats the subject as body — drop it
|
|
76
|
+
.filter(l => l !== text);
|
|
77
|
+
|
|
78
|
+
const typeLabel = TYPE_LABEL[type] ?? 'Housekeeping';
|
|
79
|
+
const base = `${title}${breakingTag} \`${commit.sha}\``;
|
|
80
|
+
const bodyBlock = bodyLines.length ? '\n' + bodyLines.map(l => ` - ${l}`).join('\n') : '';
|
|
81
|
+
|
|
82
|
+
if (scope) {
|
|
83
|
+
const key = scope.toLowerCase();
|
|
84
|
+
let g = scoped.get(key);
|
|
85
|
+
if (!g) {
|
|
86
|
+
g = { label: titleCase(scope), items: [] };
|
|
87
|
+
scoped.set(key, g);
|
|
88
|
+
scopedOrder.push(key);
|
|
89
|
+
}
|
|
90
|
+
// scoped (feature-area) sections: the type label adds information because
|
|
91
|
+
// the heading names the area, not the type
|
|
92
|
+
g.items.push(`- **${typeLabel}** ${base}${bodyBlock}`);
|
|
93
|
+
} else {
|
|
94
|
+
// type-fallback sections: the heading already names the type, so a
|
|
95
|
+
// per-bullet label would be redundant (R5a)
|
|
96
|
+
(byType.get(type) ?? byType.get('chore')!).items.push(`- ${base}${bodyBlock}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Render into a heading-keyed map (case-insensitive) so a type-fallback
|
|
101
|
+
// heading that collides with an earlier scoped heading merges into it
|
|
102
|
+
// instead of emitting a duplicate section (R5b).
|
|
103
|
+
const sections = new Map<string, { heading: string; items: string[] }>();
|
|
104
|
+
const order: string[] = [];
|
|
105
|
+
const addSection = (heading: string, items: string[]) => {
|
|
106
|
+
const key = heading.toLowerCase();
|
|
107
|
+
let s = sections.get(key);
|
|
108
|
+
if (!s) {
|
|
109
|
+
s = { heading, items: [] };
|
|
110
|
+
sections.set(key, s);
|
|
111
|
+
order.push(key);
|
|
112
|
+
}
|
|
113
|
+
s.items.push(...items);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// scoped (feature-area) sections first, in order of first appearance
|
|
117
|
+
for (const key of scopedOrder) addSection(scoped.get(key)!.label, scoped.get(key)!.items);
|
|
118
|
+
// then unscoped type-fallback sections, canonical type order
|
|
119
|
+
for (const t of TYPE_ORDER) {
|
|
120
|
+
const g = byType.get(t)!;
|
|
121
|
+
if (g.items.length) addSection(g.label, g.items);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines: string[] = [];
|
|
125
|
+
for (const key of order) {
|
|
126
|
+
const s = sections.get(key)!;
|
|
127
|
+
lines.push(`## ${s.heading}`);
|
|
128
|
+
lines.push('');
|
|
129
|
+
for (const item of s.items) lines.push(item);
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const bumpIdx = commits.findIndex(c => /chore: release|chore: bump/.test(c.subject));
|
|
134
|
+
const body = commits.slice(0, bumpIdx === -1 ? commits.length : bumpIdx);
|
|
135
|
+
const count = body.length || commits.length;
|
|
136
|
+
|
|
137
|
+
return { count, markdown: lines.join('\n').trimEnd() };
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
140
|
+
// Guard the CLI path so importing buildNotes in tests does not run git.
|
|
141
|
+
if (import.meta.main) {
|
|
142
|
+
const args = process.argv;
|
|
143
|
+
const sinceIdx = args.indexOf('--since');
|
|
144
|
+
let since = sinceIdx !== -1 ? args[sinceIdx + 1] : null;
|
|
145
|
+
|
|
146
|
+
if (!since) {
|
|
147
|
+
// The release workflow tags HEAD BEFORE generating notes, so the newest tag
|
|
148
|
+
// is the release tag itself. The "since" boundary must be the tag BEFORE it,
|
|
149
|
+
// otherwise range = "<release>..HEAD" is empty and the notes come out blank.
|
|
150
|
+
// With only one tag (first release), there is no previous tag — show all.
|
|
151
|
+
const tags = execFileSync('git', ['tag', '--sort=-version:refname'], { encoding: 'utf8' })
|
|
152
|
+
.split(/\r?\n/).filter(Boolean);
|
|
153
|
+
since = tags.length >= 2 ? tags[1] : null;
|
|
154
|
+
}
|
|
91
155
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
156
|
+
const range = since ? `${since}..HEAD` : '';
|
|
157
|
+
const raw = execFileSync('git', ['log', '--pretty=%H%n%s%n%n%b%n__END__', ...(range ? [range] : [])], { encoding: 'utf8' });
|
|
158
|
+
const commits: ReleaseCommit[] = raw
|
|
159
|
+
.split('__END__\n')
|
|
160
|
+
.map(c => c.trim())
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.map(c => {
|
|
163
|
+
const [sha, subject, ...rest] = c.split('\n');
|
|
164
|
+
return { sha: sha.slice(0, 7), subject: subject ?? '', body: rest.join('\n').trim() };
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const { count, markdown } = buildNotes(commits);
|
|
168
|
+
console.log(`**${count} change${count === 1 ? '' : 's'} since ${since ?? 'the start.'}**`);
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log(markdown);
|
|
171
|
+
}
|