@ionivetech/mugiwara 0.6.4 → 0.6.6
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 +2 -3
- package/.codex-plugin/plugin.json +48 -2
- package/.cursor-plugin/plugin.json +48 -2
- package/.kimi-plugin/plugin.json +48 -2
- package/.opencode/commands/mugiwara-continue.md +44 -31
- package/.opencode/commands/mugiwara-execute.md +1 -1
- package/.opencode/commands/mugiwara-heal.md +1 -1
- package/.opencode/commands/mugiwara-onboard.md +8 -9
- package/.opencode/commands/mugiwara-plan.md +1 -1
- package/.opencode/commands/mugiwara-review.md +1 -1
- package/.opencode/commands/mugiwara-security.md +1 -1
- package/.opencode/commands/mugiwara-ship.md +1 -1
- package/.opencode/commands/mugiwara.md +2 -2
- package/.opencode/mugiwara-helpers.mjs +3 -0
- package/.opencode/plugins/mugiwara.mjs +1 -1
- package/AGENTS.md +12 -2
- package/README.md +53 -32
- package/content/agents/brook-healing.md +5 -8
- package/content/agents/chopper-checkpoint.md +8 -11
- package/content/agents/eval-runner.md +2 -5
- package/content/agents/franky-gates.md +4 -7
- package/content/agents/jinbe-security.md +4 -7
- package/content/agents/luffy-orchestrator.md +13 -13
- package/content/agents/memory-keeper.md +5 -8
- package/content/agents/nami-planner.md +4 -7
- package/content/agents/resume-coordinator.md +7 -10
- package/content/agents/robin-reviewer.md +6 -9
- package/content/agents/sanji-quality.md +7 -10
- package/content/agents/skeptic-verifier.md +5 -8
- package/content/agents/usopp-brainstorm.md +3 -6
- package/content/agents/zoro-execution.md +7 -10
- package/content/skills/mugiwara-backend/SKILL.md +2 -0
- package/content/skills/mugiwara-checkpoint/SKILL.md +14 -11
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +4 -4
- package/content/skills/mugiwara-context-budget/SKILL.md +5 -3
- package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +27 -27
- package/content/skills/mugiwara-execution/references/dispatch.md +2 -2
- package/content/skills/mugiwara-execution/references/resume-batching.md +1 -1
- package/content/skills/mugiwara-frontend/SKILL.md +2 -4
- package/content/skills/mugiwara-gates/SKILL.md +9 -7
- package/content/skills/mugiwara-git/SKILL.md +7 -2
- package/content/skills/mugiwara-healing/SKILL.md +6 -4
- package/content/skills/mugiwara-healing/references/workers.md +2 -2
- package/content/skills/mugiwara-lessons/SKILL.md +1 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +34 -35
- package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
- package/content/skills/mugiwara-orchestration/references/closure.md +5 -5
- package/content/skills/mugiwara-orchestration/references/output-contract.md +77 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +22 -22
- package/content/skills/mugiwara-planning/SKILL.md +3 -1
- package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
- package/content/skills/mugiwara-pr/SKILL.md +6 -6
- package/content/skills/mugiwara-pr/references/verdict-format.md +3 -3
- package/content/skills/mugiwara-quality/SKILL.md +8 -7
- package/content/skills/mugiwara-resume/SKILL.md +19 -20
- package/content/skills/mugiwara-review/SKILL.md +6 -1
- package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +5 -1
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +5 -3
- package/content/skills/mugiwara-workflow/SKILL.md +38 -39
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +15 -15
- package/content/skills/using-mugiwara/SKILL.md +5 -5
- package/dist/mugiwara.js +807 -31
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +106 -0
- package/hooks/auto-savepoint.ts +102 -0
- package/hooks/engagement-marker.js +59 -0
- package/hooks/engagement-marker.ts +97 -0
- package/hooks/hooks.json +41 -2
- package/hooks/mugiwara-mode-tracker.js +79 -0
- package/hooks/pipeline-guard.js +182 -0
- package/hooks/pipeline-guard.ts +267 -0
- package/hooks/session-start.js +106 -0
- package/hooks/session-start.ts +44 -32
- package/package.json +7 -3
- package/plugin.json +1 -1
- package/references/agent-protocol.md +15 -0
- package/references/complexity.md +54 -0
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +5 -5
- package/references/skill-versioning.md +1 -1
- package/references/token-budget.md +30 -8
- package/references/wave-banners.md +21 -31
- package/scripts/build-hooks.ts +56 -0
- package/scripts/conformance.ts +232 -0
- package/scripts/coverage-gate.ts +179 -0
- package/scripts/evidence.sh +17 -4
- package/scripts/gate-selftest.ts +14 -0
- package/scripts/initiative.ts +51 -17
- package/scripts/install.ps1 +5 -4
- package/scripts/install.sh +4 -4
- package/scripts/lane.sh +6 -2
- package/scripts/lib/patterns.sh +84 -0
- package/scripts/mission-report.sh +12 -11
- package/scripts/savepoint.sh +152 -37
- package/scripts/validate-content.ts +22 -2
- package/scripts/verify-install.ts +154 -44
- package/src/args.ts +1 -1
- package/src/cli.ts +217 -0
- package/src/continue.ts +243 -0
- package/src/installer.ts +39 -0
- package/src/onboard.ts +207 -0
- package/src/run.ts +82 -0
- package/src/targets/claude.ts +140 -2
- package/src/targets/copilot.ts +20 -1
- package/src/targets/generic.ts +9 -4
- package/content/agents/onboarding-guide.md +0 -124
- package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
- package/scripts/onboard.ts +0 -266
- package/scripts/probe.ts +0 -40
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/conformance.ts — C1: cross-platform conformance suite.
|
|
3
|
+
// Every installable target: materialize the standard-feature fixture repo,
|
|
4
|
+
// install the target, run the four core scripts, and compare a normalized
|
|
5
|
+
// snapshot (state fields, report sections, evidence header, gitignore block,
|
|
6
|
+
// file count) against test/golden/<target>.json. Exit 1 on any difference
|
|
7
|
+
// with a diff; --update-golden regenerates. The snapshots are normalized:
|
|
8
|
+
// timestamps, hashes, and random filenames are excluded so a golden is stable
|
|
9
|
+
// across runs.
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { installTo, ensureProjectGitignore } from '../src/installer.ts';
|
|
16
|
+
import { targets, TARGET_IDS } from '../src/targets/index.ts';
|
|
17
|
+
|
|
18
|
+
const root = join(import.meta.dirname, '..');
|
|
19
|
+
const goldenDir = join(root, 'test', 'golden');
|
|
20
|
+
const UPDATE = process.argv.includes('--update-golden');
|
|
21
|
+
|
|
22
|
+
// tier mapping per docs/reference/harness-matrix.md (target defs carry tier only
|
|
23
|
+
// on some; the matrix is the source of truth for the rest)
|
|
24
|
+
const TIER_OF: Record<string, number> = {
|
|
25
|
+
claude: 1, opencode: 1,
|
|
26
|
+
gemini: 2, codex: 2, copilot: 2,
|
|
27
|
+
windsurf: 3, cline: 3, kilo: 3, antigravity: 3,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TARGETS = TARGET_IDS.map(id => ({ id, tier: TIER_OF[id] ?? 2 }));
|
|
31
|
+
|
|
32
|
+
// marketplace-plugin platforms: installed from the repo itself via the host's
|
|
33
|
+
// plugin system (no rules-dir install). Conformance = the manifest an
|
|
34
|
+
// operator's marketplace will consume: it parses, its version matches the
|
|
35
|
+
// package, its pointers resolve, and its metadata set-equals content/.
|
|
36
|
+
const MARKETPLACE = [
|
|
37
|
+
{ id: 'cursor', manifest: '.cursor-plugin/plugin.json' },
|
|
38
|
+
{ id: 'kimi', manifest: '.kimi-plugin/plugin.json' },
|
|
39
|
+
{ id: 'pi', manifest: null, piField: true },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function marketplaceSnapshot(id: string, manifestRel: string | null, piField: boolean): Record<string, unknown> {
|
|
43
|
+
const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
44
|
+
const skillDirs = readdirSync(join(root, 'content', 'skills'), { withFileTypes: true })
|
|
45
|
+
.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
46
|
+
const agentFiles = readdirSync(join(root, 'content', 'agents'))
|
|
47
|
+
.filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, '')).sort();
|
|
48
|
+
|
|
49
|
+
const base = {
|
|
50
|
+
target: id,
|
|
51
|
+
kind: 'marketplace-plugin',
|
|
52
|
+
version_matches_package: false,
|
|
53
|
+
skills_pointer_resolves: false,
|
|
54
|
+
skills_count: 0,
|
|
55
|
+
agents_metadata_set_equal: false,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (piField) {
|
|
59
|
+
const pi = packageJson.pi;
|
|
60
|
+
const list = pi?.skills;
|
|
61
|
+
const pointers = Array.isArray(list) ? list : [];
|
|
62
|
+
base.skills_pointer_resolves = pointers.every(p => existsSync(join(root, p)));
|
|
63
|
+
base.skills_count = existsSync(join(root, 'content', 'skills')) ? skillDirs.length : 0;
|
|
64
|
+
base.version_matches_package = true; // package.json is the package itself
|
|
65
|
+
base.agents_metadata_set_equal = true; // pi loads skills only; agents ship in the same tree
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const m = JSON.parse(readFileSync(join(root, manifestRel!), 'utf8'));
|
|
70
|
+
const skillsPtr = m.skills;
|
|
71
|
+
const skillsDir = typeof skillsPtr === 'string' ? join(root, skillsPtr) : null;
|
|
72
|
+
const actual = skillsDir && existsSync(skillsDir)
|
|
73
|
+
? readdirSync(skillsDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort()
|
|
74
|
+
: [];
|
|
75
|
+
const mSkills = Array.isArray(m.metadata?.skills) ? [...m.metadata.skills].sort() : [];
|
|
76
|
+
const mAgents = Array.isArray(m.metadata?.agents) ? [...m.metadata.agents].sort() : [];
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
version_matches_package: m.version === packageJson.version,
|
|
81
|
+
skills_pointer_resolves: skillsDir !== null && existsSync(skillsDir) && actual.length === skillDirs.length,
|
|
82
|
+
skills_count: actual.length,
|
|
83
|
+
agents_metadata_set_equal: JSON.stringify(mAgents) === JSON.stringify(agentFiles),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MISSION = 'conform';
|
|
88
|
+
|
|
89
|
+
function sh(cmd: string, cwd: string) {
|
|
90
|
+
execSync(cmd, { cwd, stdio: 'pipe', env: { ...process.env, MUGIWARA_DIR: join(cwd, '.mugiwara') } });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function snapshot(targetId: string): Record<string, unknown> {
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), `mugi-conform-${targetId}-`));
|
|
95
|
+
try {
|
|
96
|
+
// 1. materialize the standard-feature fixture repo (trunk + feat branch)
|
|
97
|
+
sh(`bun scripts/setup-fixtures.ts standard-feature "${dir}"`, root);
|
|
98
|
+
|
|
99
|
+
// 2. install the target into the project
|
|
100
|
+
const target = targets[targetId];
|
|
101
|
+
if (!target) throw new Error(`unknown target ${targetId}`);
|
|
102
|
+
installTo(target, { scope: 'project', projectDir: dir, force: true, dryRun: false });
|
|
103
|
+
|
|
104
|
+
// The installer's own output is harness config, not mission work — but it
|
|
105
|
+
// lands as untracked files, and savepoint/lane now count the working tree
|
|
106
|
+
// (F). Park it in .git/info/exclude so the snapshot keeps measuring the
|
|
107
|
+
// fixture's 4-file mission diff instead of "how many files does this
|
|
108
|
+
// target install". Local-only: .gitignore (which the golden snapshots)
|
|
109
|
+
// is untouched.
|
|
110
|
+
const untracked = execSync('git status --porcelain --untracked-files=normal', { cwd: dir, encoding: 'utf8' })
|
|
111
|
+
.split('\n').filter(Boolean).map(l => l.slice(3)).filter(Boolean);
|
|
112
|
+
if (untracked.length) {
|
|
113
|
+
const exclude = join(dir, '.git', 'info', 'exclude');
|
|
114
|
+
writeFileSync(exclude, (existsSync(exclude) ? readFileSync(exclude, 'utf8') : '') + '\n' + untracked.join('\n') + '\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. run the four core scripts + the gitignore write the CLI does post-install
|
|
118
|
+
sh(`bash "${root}/scripts/lane.sh" main --json`, dir);
|
|
119
|
+
sh(`bash "${root}/scripts/savepoint.sh" ${MISSION} "" 1 auto`, dir);
|
|
120
|
+
sh(`bash "${root}/scripts/evidence.sh" ${MISSION} lint -- printf 'ok\\n'`, dir);
|
|
121
|
+
sh(`bash "${root}/scripts/mission-report.sh" ${MISSION}`, dir);
|
|
122
|
+
ensureProjectGitignore(dir, { dryRun: false });
|
|
123
|
+
|
|
124
|
+
// 4. normalize + collect
|
|
125
|
+
const state = JSON.parse(readFileSync(join(dir, '.mugiwara', 'state', MISSION, 'state.json'), 'utf8'));
|
|
126
|
+
const reportFile = readdirSync(join(dir, '.mugiwara', 'reports')).find(f => f.endsWith(`-${MISSION}.md`));
|
|
127
|
+
const report = reportFile ? readFileSync(join(dir, '.mugiwara', 'reports', reportFile), 'utf8') : '';
|
|
128
|
+
const evFile = readdirSync(join(dir, '.mugiwara', 'results', MISSION)).find(f => f.includes('lint-'));
|
|
129
|
+
const ev = evFile ? readFileSync(join(dir, '.mugiwara', 'results', MISSION, evFile), 'utf8') : '';
|
|
130
|
+
const gitignore = existsSync(join(dir, '.gitignore')) ? readFileSync(join(dir, '.gitignore'), 'utf8') : '';
|
|
131
|
+
|
|
132
|
+
const countFiles = (p: string): number => {
|
|
133
|
+
if (!existsSync(p)) return 0;
|
|
134
|
+
return readdirSync(p, { recursive: true, withFileTypes: true }).filter(e => e.isFile()).length;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
target: targetId,
|
|
139
|
+
tier: TIER_OF[targetId] ?? 2,
|
|
140
|
+
state: {
|
|
141
|
+
mission: state.mission,
|
|
142
|
+
member: state.member,
|
|
143
|
+
lane: state.lane,
|
|
144
|
+
lane_reason: state.lane_reason,
|
|
145
|
+
files_touched: state.files_touched,
|
|
146
|
+
loc_delta: state.loc_delta,
|
|
147
|
+
loc_ins: state.loc_ins,
|
|
148
|
+
loc_del: state.loc_del,
|
|
149
|
+
loc_churn: state.loc_churn,
|
|
150
|
+
mode: state.mode,
|
|
151
|
+
verbosity: state.verbosity,
|
|
152
|
+
tasks_done: state.tasks?.done,
|
|
153
|
+
tasks_total: state.tasks?.total,
|
|
154
|
+
blockers_open: state.blockers_open,
|
|
155
|
+
heal_cycle: state.heal_cycle,
|
|
156
|
+
heal_max_cycles: state.heal_max_cycles,
|
|
157
|
+
heal_halt: state.heal_halt,
|
|
158
|
+
delegate_threshold: state.delegate_threshold,
|
|
159
|
+
delegate_due: state.delegate_due,
|
|
160
|
+
tokens_source: state.tokens_source,
|
|
161
|
+
budget_status: state.budget_status,
|
|
162
|
+
sensitive_paths: state.sensitive_paths,
|
|
163
|
+
},
|
|
164
|
+
report_sections: [...report.matchAll(/^#{2,3} .*$/gm)].map(m => m[0]),
|
|
165
|
+
evidence: {
|
|
166
|
+
header: ev.split('\n').filter(l => l.startsWith('# ') && !l.startsWith('# At:') && !/^# (Exit|Verdict):/.test(l)),
|
|
167
|
+
trailer: ev.split('\n').filter(l => /^# (Exit|Verdict):/.test(l)),
|
|
168
|
+
},
|
|
169
|
+
gitignore_block: gitignore.split('\n').filter(l => l.includes('mugiwara') || l.includes('# ---')),
|
|
170
|
+
file_count: {
|
|
171
|
+
skills: countFiles(target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir),
|
|
172
|
+
agents: countFiles(target.paths({ scope: 'project', projectDir: dir, home: '' }).agentsDir),
|
|
173
|
+
state: countFiles(join(dir, '.mugiwara', 'state')),
|
|
174
|
+
evidence: countFiles(join(dir, '.mugiwara', 'results', MISSION)),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
} finally {
|
|
178
|
+
rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function diff(a: unknown, b: unknown, path = ''): string[] {
|
|
183
|
+
const out: string[] = [];
|
|
184
|
+
if (JSON.stringify(a) === JSON.stringify(b)) return out;
|
|
185
|
+
if (typeof a !== typeof b || a === null || b === null || typeof a !== 'object') {
|
|
186
|
+
out.push(`${path}: ${JSON.stringify(a)} ≠ ${JSON.stringify(b)}`);
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
for (const k of new Set([...Object.keys(a as object), ...Object.keys(b as object)])) {
|
|
190
|
+
out.push(...diff((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k], `${path}.${k}`));
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let failed = false;
|
|
196
|
+
mkdirSync(goldenDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
const ALL = [
|
|
199
|
+
...TARGETS.map(t => ({ id: t.id, label: `tier ${t.tier}`, snap: () => snapshot(t.id) })),
|
|
200
|
+
...MARKETPLACE.map(m => ({ id: m.id, label: 'marketplace', snap: () => marketplaceSnapshot(m.id, m.manifest, m.piField === true) })),
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
for (const t of ALL) {
|
|
204
|
+
const snap = t.snap();
|
|
205
|
+
const goldenFile = join(goldenDir, `${t.id}.json`);
|
|
206
|
+
if (UPDATE) {
|
|
207
|
+
writeFileSync(goldenFile, JSON.stringify(snap, null, 2) + '\n');
|
|
208
|
+
console.log(`✓ golden updated: test/golden/${t.id}.json`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!existsSync(goldenFile)) {
|
|
212
|
+
console.log(`✗ ${t.id}: golden missing — run with --update-golden first`);
|
|
213
|
+
failed = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const golden = JSON.parse(readFileSync(goldenFile, 'utf8'));
|
|
217
|
+
const diffs = diff(snap, golden);
|
|
218
|
+
if (diffs.length > 0) {
|
|
219
|
+
failed = true;
|
|
220
|
+
console.log(`✗ ${t.id} (${t.label}) differs:`);
|
|
221
|
+
for (const d of diffs) console.log(` ${d}`);
|
|
222
|
+
} else {
|
|
223
|
+
console.log(`✓ ${t.id} (${t.label}) conforms`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (UPDATE) {
|
|
228
|
+
console.log('goldens regenerated — review the diff before committing');
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
if (failed) process.exit(1);
|
|
232
|
+
console.log(`✓ ${ALL.length} platforms pass conformance`);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/coverage-gate.ts — enforce the `coverage_new` / `coverage_modified`
|
|
3
|
+
// thresholds declared in `.mugiwara/config` against the files THIS diff adds
|
|
4
|
+
// and changes, not against a global project number. Two keys exist precisely
|
|
5
|
+
// because the two populations are judged differently (mugiwara-gates SKILL.md
|
|
6
|
+
// §Coverage gate); a global percentage would collapse them into one and let an
|
|
7
|
+
// untested new file hide behind an old well-tested one.
|
|
8
|
+
//
|
|
9
|
+
// Run: bun scripts/coverage-gate.ts enforce (gate step)
|
|
10
|
+
// bun scripts/coverage-gate.ts --base X compare against ref X
|
|
11
|
+
// bun scripts/coverage-gate.ts --show print per-file numbers, never fail
|
|
12
|
+
//
|
|
13
|
+
// Exit 0 = pass or recorded SKIP. Exit 1 = a threshold was missed.
|
|
14
|
+
// A repo with no coverage tooling or no test suite records a SKIP with its
|
|
15
|
+
// reason — never a fake pass (decision log row 50).
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
17
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join, relative, sep } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const root = join(import.meta.dirname, '..');
|
|
22
|
+
const SHOW = process.argv.includes('--show');
|
|
23
|
+
|
|
24
|
+
/** `.mugiwara/config` → project, then `~`. Returns undefined if the key is set nowhere. */
|
|
25
|
+
function config(key: string): string | undefined {
|
|
26
|
+
for (const base of [root, homedir()]) {
|
|
27
|
+
const file = join(base, '.mugiwara', 'config');
|
|
28
|
+
if (!existsSync(file)) continue;
|
|
29
|
+
// \r?\n: a config written on Windows is still a config
|
|
30
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
31
|
+
const t = line.trim();
|
|
32
|
+
if (!t || t.startsWith('#')) continue;
|
|
33
|
+
const eq = t.indexOf('=');
|
|
34
|
+
if (eq === -1) continue;
|
|
35
|
+
if (t.slice(0, eq).trim() !== key) continue;
|
|
36
|
+
return t.slice(eq + 1).trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Documented defaults: new >= 90, modified >= 80. A key set to 0 = no threshold. */
|
|
43
|
+
function threshold(key: string, fallback: number): number {
|
|
44
|
+
const raw = config(key);
|
|
45
|
+
if (raw === undefined || raw === '') return fallback;
|
|
46
|
+
const n = Number(raw);
|
|
47
|
+
if (!Number.isFinite(n) || n < 0 || n > 100) {
|
|
48
|
+
console.log(`coverage-gate: ignoring unparseable ${key}="${raw}", using ${fallback}`);
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function skip(reason: string): never {
|
|
55
|
+
console.log(`coverage-gate: SKIP — ${reason}`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function git(args: string[]): string {
|
|
60
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---- 1. is there anything to measure at all? -------------------------------
|
|
64
|
+
|
|
65
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
66
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
67
|
+
if (!deps['@vitest/coverage-v8'] && !deps['@vitest/coverage-istanbul'])
|
|
68
|
+
skip('no coverage tooling installed (no @vitest/coverage-* in package.json)');
|
|
69
|
+
if (!existsSync(join(root, 'test')))
|
|
70
|
+
skip('no test suite found (no test/ directory)');
|
|
71
|
+
|
|
72
|
+
// ---- 2. which files does this diff add and change? ------------------------
|
|
73
|
+
|
|
74
|
+
const baseArg = process.argv.indexOf('--base');
|
|
75
|
+
let base = baseArg !== -1 ? process.argv[baseArg + 1] : process.env.MUGIWARA_COVERAGE_BASE;
|
|
76
|
+
if (!base) {
|
|
77
|
+
// the mission's own recorded base_sha is the truthful diff origin
|
|
78
|
+
const stateDir = join(root, '.mugiwara', 'state');
|
|
79
|
+
if (existsSync(stateDir)) {
|
|
80
|
+
for (const m of readdirSync(stateDir)) {
|
|
81
|
+
const f = join(stateDir, m, 'state.json');
|
|
82
|
+
if (!existsSync(f)) continue;
|
|
83
|
+
const sha = JSON.parse(readFileSync(f, 'utf8')).base_sha;
|
|
84
|
+
if (sha) { base = sha; break; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!base) {
|
|
89
|
+
for (const ref of ['origin/main', 'main', 'origin/master', 'master']) {
|
|
90
|
+
try { base = git(['merge-base', 'HEAD', ref]); break; } catch { /* next */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!base) skip('no diff base could be resolved (no state base_sha, no main/master ref)');
|
|
94
|
+
|
|
95
|
+
let changed: Array<{ status: string; path: string }>;
|
|
96
|
+
try {
|
|
97
|
+
changed = git(['diff', '--name-status', '--diff-filter=AMR', '-M', `${base}`, 'HEAD'])
|
|
98
|
+
.split(/\r?\n/)
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.map((l) => {
|
|
101
|
+
const parts = l.split('\t');
|
|
102
|
+
// rename rows are `R096\told\tnew` — the new path is the one to judge
|
|
103
|
+
return { status: parts[0][0], path: parts[parts.length - 1] };
|
|
104
|
+
});
|
|
105
|
+
} catch (e) {
|
|
106
|
+
skip(`git diff against ${base} failed (${(e as Error).message})`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!changed.length) skip(`no added or modified files between ${base.slice(0, 7)} and HEAD`);
|
|
110
|
+
|
|
111
|
+
// ---- 3. measure ------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
const summaryPath = join(root, 'coverage', 'coverage-summary.json');
|
|
114
|
+
const needsRun =
|
|
115
|
+
!existsSync(summaryPath) ||
|
|
116
|
+
statSync(summaryPath).mtimeMs < Math.max(...changed
|
|
117
|
+
.map((c) => join(root, c.path))
|
|
118
|
+
.filter(existsSync)
|
|
119
|
+
.map((p) => statSync(p).mtimeMs), 0);
|
|
120
|
+
|
|
121
|
+
if (needsRun) {
|
|
122
|
+
console.log('coverage-gate: measuring (vitest run --coverage)...');
|
|
123
|
+
const r = spawnSync('npx', ['vitest', 'run', '--coverage', '--silent'], {
|
|
124
|
+
cwd: root, stdio: 'inherit', shell: process.platform === 'win32',
|
|
125
|
+
});
|
|
126
|
+
if (r.status !== 0) {
|
|
127
|
+
console.log('coverage-gate: FAIL — test run did not pass, coverage is not measurable');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!existsSync(summaryPath))
|
|
132
|
+
skip('coverage run produced no coverage-summary.json (json-summary reporter not configured)');
|
|
133
|
+
|
|
134
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
|
|
135
|
+
// keys are absolute OS paths; normalise to repo-relative posix to match git
|
|
136
|
+
const measured = new Map<string, number>();
|
|
137
|
+
for (const [k, v] of Object.entries<any>(summary)) {
|
|
138
|
+
if (k === 'total') continue;
|
|
139
|
+
measured.set(relative(root, k).split(sep).join('/'), v.lines?.pct ?? 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- 4. apply the two thresholds ------------------------------------------
|
|
143
|
+
|
|
144
|
+
const NEW = threshold('coverage_new', 90);
|
|
145
|
+
const MOD = threshold('coverage_modified', 80);
|
|
146
|
+
|
|
147
|
+
const rows = changed.map((c) => {
|
|
148
|
+
const isNew = c.status === 'A';
|
|
149
|
+
return {
|
|
150
|
+
path: c.path,
|
|
151
|
+
kind: isNew ? 'new' : 'modified',
|
|
152
|
+
limit: isNew ? NEW : MOD,
|
|
153
|
+
pct: measured.get(c.path),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const scoped = rows.filter((r) => r.pct !== undefined);
|
|
158
|
+
const unscoped = rows.filter((r) => r.pct === undefined);
|
|
159
|
+
|
|
160
|
+
console.log(`coverage-gate: base ${base.slice(0, 7)} · thresholds new>=${NEW || 'off'} modified>=${MOD || 'off'}`);
|
|
161
|
+
console.log(` ${changed.length} changed file(s), ${scoped.length} within coverage scope, ${unscoped.length} outside it`);
|
|
162
|
+
|
|
163
|
+
const failures = scoped.filter((r) => r.limit > 0 && (r.pct as number) < r.limit);
|
|
164
|
+
|
|
165
|
+
for (const r of scoped.sort((a, b) => (a.pct as number) - (b.pct as number))) {
|
|
166
|
+
const bad = r.limit > 0 && (r.pct as number) < r.limit;
|
|
167
|
+
if (SHOW || bad || (r.pct as number) < 100)
|
|
168
|
+
console.log(` ${bad ? '✗' : '✓'} ${r.path} — ${(r.pct as number).toFixed(2)}% ${r.kind} (limit ${r.limit || 'off'})`);
|
|
169
|
+
}
|
|
170
|
+
if (SHOW && unscoped.length)
|
|
171
|
+
console.log(unscoped.map((r) => ` · ${r.path} — outside coverage scope (not a measured source file)`).join('\n'));
|
|
172
|
+
|
|
173
|
+
if (SHOW) process.exit(0);
|
|
174
|
+
if (failures.length) {
|
|
175
|
+
console.log(`\ncoverage-gate: FAIL — ${failures.length} file(s) below their threshold`);
|
|
176
|
+
console.log('Do not lower the thresholds or exclude files to pass: add the missing tests.');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
console.log('\ncoverage-gate: PASS');
|
package/scripts/evidence.sh
CHANGED
|
@@ -40,20 +40,33 @@ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
|
40
40
|
HASH=$(echo "${LABEL}-${TIMESTAMP}-$$-${RANDOM}" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || openssl sha256) | cut -c1-12 2>/dev/null || echo "${TIMESTAMP}")
|
|
41
41
|
EVIDENCE_FILE="$RESULTS_DIR/${LABEL}-${HASH}.log"
|
|
42
42
|
|
|
43
|
+
# Neutralize forged verdict/exit header lines in captured output. The agent
|
|
44
|
+
# trusts the real trailer only (# Exit:/# Verdict: appended after the block);
|
|
45
|
+
# attacker output may try to impersonate it under any spelling — leading
|
|
46
|
+
# whitespace, ANSI prefix, no space, CRLF. Idempotent: already-neutralized
|
|
47
|
+
# #-Verdict: / #-Exit: lines pass through unchanged.
|
|
48
|
+
SANITIZE='s/^[[:space:]]*(\x1b\[[0-9;]*m)*#[[:space:]]*(Verdict|Exit):/#-\2:/'
|
|
49
|
+
|
|
50
|
+
# command line echoed in the header — collapse embedded newlines so an arg
|
|
51
|
+
# cannot forge a header line inside "# Command:" (the header block is written
|
|
52
|
+
# verbatim, outside the sanitizer).
|
|
53
|
+
COMMAND_LINE=$(printf '%s ' "$@" 2>/dev/null | tr '\n\r' ' ' | sed 's/ *$//')
|
|
54
|
+
|
|
43
55
|
{
|
|
44
56
|
echo "# Evidence: $LABEL"
|
|
45
57
|
echo "# At: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
46
|
-
echo "# Command: ${
|
|
58
|
+
echo "# Command: ${COMMAND_LINE:-<stdin pipeline>}"
|
|
47
59
|
echo "# ---"
|
|
48
60
|
echo
|
|
49
61
|
|
|
50
62
|
if [ $# -gt 0 ]; then
|
|
51
|
-
"$@" 2>&1
|
|
63
|
+
"$@" 2>&1 | sed -E "$SANITIZE"
|
|
64
|
+
EXIT_CODE=${PIPESTATUS[0]}
|
|
52
65
|
else
|
|
53
|
-
|
|
66
|
+
sed -E "$SANITIZE"
|
|
67
|
+
EXIT_CODE=0
|
|
54
68
|
fi
|
|
55
69
|
} > "$EVIDENCE_FILE"
|
|
56
|
-
EXIT_CODE=$?
|
|
57
70
|
|
|
58
71
|
# trailer: exit code + verdict (D6). The verdict is PASS/FAIL derived from the
|
|
59
72
|
# exit code — the check's own outcome, not the harness's opinion.
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -68,6 +68,20 @@ if (!existsSync(join(root, 'scripts', 'validate-content.ts'))) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// --- G5: conditional-assertion guard — prove expect-in-conditional goes red ---
|
|
72
|
+
console.log('\nG5 — conditional-assertion guard');
|
|
73
|
+
{
|
|
74
|
+
const testFile = join(root, 'test', 'targets.test.ts');
|
|
75
|
+
const original = readFileSync(testFile, 'utf8');
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(testFile, `${original}\nif (x) { expect(1).toBe(1); }\n`);
|
|
78
|
+
assert('expect() in non-invariant conditional → exit 1', false, () => run('G5', 'bun scripts/validate-content.ts'));
|
|
79
|
+
} finally {
|
|
80
|
+
writeFileSync(testFile, original);
|
|
81
|
+
assert('restored → exit 0', true, () => run('G5', 'bun scripts/validate-content.ts'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
// --- G3: savepoint fixtures — prove test fails when a field is broken ---
|
|
72
86
|
console.log('\nG3 — savepoint fixtures');
|
|
73
87
|
if (!existsSync(join(root, 'test', 'savepoint.test.ts'))) {
|
package/scripts/initiative.ts
CHANGED
|
@@ -55,40 +55,47 @@ function assertPlanContent(content: string, planFile: string): void {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function parseSubMissions(content: string): SubMission[] {
|
|
58
|
+
function parseSubMissions(content: string): { subs: SubMission[]; hasSection: boolean } {
|
|
59
59
|
const lines = content.split('\n');
|
|
60
60
|
let inTable = false;
|
|
61
61
|
let inSubSection = false;
|
|
62
|
+
let hasSection = false;
|
|
62
63
|
const subs: SubMission[] = [];
|
|
63
64
|
|
|
64
65
|
for (let i = 0; i < lines.length; i++) {
|
|
65
66
|
const line = lines[i];
|
|
66
|
-
if (line.startsWith('##
|
|
67
|
+
if (line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
67
68
|
inSubSection = true;
|
|
69
|
+
hasSection = true;
|
|
68
70
|
continue;
|
|
69
71
|
}
|
|
70
|
-
if (inSubSection && line.startsWith('## ') && !line.startsWith('##
|
|
72
|
+
if (inSubSection && line.startsWith('## ') && !line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
71
73
|
break;
|
|
72
74
|
}
|
|
73
75
|
if (!inSubSection) continue;
|
|
74
76
|
|
|
75
77
|
const trimmed = line.trim();
|
|
76
|
-
|
|
78
|
+
const lower = trimmed.toLowerCase();
|
|
79
|
+
if (lower.startsWith('| id ') && lower.includes('| name ')) {
|
|
77
80
|
inTable = true;
|
|
78
81
|
continue;
|
|
79
82
|
}
|
|
80
83
|
if (inTable && trimmed.startsWith('|---')) continue;
|
|
81
84
|
if (inTable && trimmed.startsWith('|')) {
|
|
82
|
-
|
|
85
|
+
// Keep interior empty cells (e.g. empty "depends on"): filter(Boolean)
|
|
86
|
+
// would shift columns and drop the touched-files tail.
|
|
87
|
+
const cols = trimmed.split('|').map(c => c.trim());
|
|
88
|
+
while (cols.length && cols[0] === '') cols.shift();
|
|
89
|
+
while (cols.length && cols[cols.length - 1] === '') cols.pop();
|
|
83
90
|
if (cols.length >= 6) {
|
|
84
91
|
subs.push({
|
|
85
92
|
id: cols[0],
|
|
86
93
|
name: cols[1],
|
|
87
94
|
assignee: cols[2],
|
|
88
95
|
branch: cols[3],
|
|
89
|
-
status: cols[4],
|
|
96
|
+
status: cols[4].replace('[X]', '[x]'),
|
|
90
97
|
dependsOn: cols[5],
|
|
91
|
-
touchedFiles: cols.slice(6).join(' ').split(
|
|
98
|
+
touchedFiles: cols.slice(6).join(' ').split(/[,\s]+/).map(s => s.trim()).filter(Boolean),
|
|
92
99
|
});
|
|
93
100
|
}
|
|
94
101
|
}
|
|
@@ -97,13 +104,23 @@ function parseSubMissions(content: string): SubMission[] {
|
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
return subs;
|
|
107
|
+
return { subs, hasSection };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertRowsParsed(planFile: string, content: string): { subs: SubMission[]; hasSection: boolean } {
|
|
111
|
+
const { subs, hasSection } = parseSubMissions(content);
|
|
112
|
+
if (hasSection && subs.length === 0) {
|
|
113
|
+
console.error('initiative: "## Sub-missions" section found but no rows parsed.');
|
|
114
|
+
console.error(' Expected header: | ID | Name | Assignee | Branch | Status | Depends On | Touched Files |');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
return { subs, hasSection };
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
function cmdStatus(planFile: string): void {
|
|
104
121
|
const content = readFileSync(planFile, 'utf8');
|
|
105
122
|
assertPlanContent(content, planFile);
|
|
106
|
-
const subs =
|
|
123
|
+
const { subs } = assertRowsParsed(planFile, content);
|
|
107
124
|
|
|
108
125
|
if (subs.length === 0) {
|
|
109
126
|
console.log('No sub-missions found — solo mission.');
|
|
@@ -117,14 +134,24 @@ function cmdStatus(planFile: string): void {
|
|
|
117
134
|
const filled = Math.round((doneCount / total) * barLen);
|
|
118
135
|
const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
|
|
119
136
|
|
|
137
|
+
// dependency blocking: a sub whose dependency is not done is blocked
|
|
138
|
+
const statusById = new Map(subs.map(s => [s.id, s.status]));
|
|
139
|
+
const blockedBy = new Map<string, string>();
|
|
140
|
+
for (const s of subs) {
|
|
141
|
+
if (s.dependsOn && statusById.has(s.dependsOn) && statusById.get(s.dependsOn) !== '[x]') {
|
|
142
|
+
blockedBy.set(s.id, s.dependsOn);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
console.log(`\n${doneCount}/${total} done [${bar}] ${pct}%\n`);
|
|
121
147
|
console.log(`${'ID'.padEnd(8)} ${'Name'.padEnd(20)} ${'Assignee'.padEnd(12)} ${'Branch'.padEnd(28)} ${'Status'.padEnd(8)} ${'Depends'.padEnd(10)} ${'Files'}`);
|
|
122
148
|
console.log('─'.repeat(120));
|
|
123
149
|
|
|
124
150
|
for (const s of subs) {
|
|
125
151
|
const icon = { '[ ]': '◻', '[~]': '◉', '[x]': '✓', '[!]': '✗' }[s.status] || '?';
|
|
152
|
+
const blocked = blockedBy.has(s.id) ? ` ⛔ blocked-by ${blockedBy.get(s.id)}` : '';
|
|
126
153
|
console.log(
|
|
127
|
-
`${s.id.padEnd(8)} ${s.name.padEnd(20)} ${s.assignee.padEnd(12)} ${s.branch.padEnd(28)} ${icon} ${s.status.padEnd(3)} ${s.dependsOn.padEnd(10)} ${s.touchedFiles.join(', ')}`
|
|
154
|
+
`${s.id.padEnd(8)} ${s.name.padEnd(20)} ${s.assignee.padEnd(12)} ${s.branch.padEnd(28)} ${icon} ${s.status.padEnd(3)} ${s.dependsOn.padEnd(10)} ${s.touchedFiles.join(', ')}${blocked}`
|
|
128
155
|
);
|
|
129
156
|
}
|
|
130
157
|
console.log();
|
|
@@ -133,7 +160,7 @@ function cmdStatus(planFile: string): void {
|
|
|
133
160
|
function cmdConflictCheck(planFile: string): void {
|
|
134
161
|
const content = readFileSync(planFile, 'utf8');
|
|
135
162
|
assertPlanContent(content, planFile);
|
|
136
|
-
const subs =
|
|
163
|
+
const { subs } = assertRowsParsed(planFile, content);
|
|
137
164
|
|
|
138
165
|
if (subs.length === 0) {
|
|
139
166
|
console.log('No sub-missions — no conflicts possible.');
|
|
@@ -168,6 +195,7 @@ function cmdConflictCheck(planFile: string): void {
|
|
|
168
195
|
console.log(`\n${conflicts.length} file conflict(s) detected:\n`);
|
|
169
196
|
console.log(conflicts.join('\n'));
|
|
170
197
|
console.log();
|
|
198
|
+
process.exit(1);
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
|
|
@@ -187,28 +215,34 @@ function cmdSetStatus(planFile: string, id: string, status: string): void {
|
|
|
187
215
|
|
|
188
216
|
for (let i = 0; i < lines.length; i++) {
|
|
189
217
|
const line = lines[i];
|
|
190
|
-
if (line.startsWith('##
|
|
218
|
+
if (line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
191
219
|
inSubSection = true;
|
|
192
220
|
continue;
|
|
193
221
|
}
|
|
194
|
-
if (inSubSection && line.startsWith('## ') && !line.startsWith('##
|
|
222
|
+
if (inSubSection && line.startsWith('## ') && !line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
195
223
|
break;
|
|
196
224
|
}
|
|
197
225
|
if (!inSubSection) continue;
|
|
198
226
|
|
|
199
227
|
const trimmed = line.trim();
|
|
200
|
-
if (trimmed.startsWith('|
|
|
228
|
+
if (trimmed.toLowerCase().startsWith('| id ')) { inTable = true; continue; }
|
|
201
229
|
if (!inTable || !trimmed.startsWith('|')) continue;
|
|
202
230
|
if (trimmed.startsWith('|---')) continue;
|
|
203
231
|
|
|
204
|
-
|
|
232
|
+
// same cell handling as parseSubMissions: interior empty cells preserved
|
|
233
|
+
const cols = trimmed.split('|').map(c => c.trim());
|
|
234
|
+
while (cols.length && cols[0] === '') cols.shift();
|
|
235
|
+
while (cols.length && cols[cols.length - 1] === '') cols.pop();
|
|
205
236
|
if (cols[0] === id) {
|
|
206
|
-
|
|
237
|
+
let oldMarker = cols[4];
|
|
238
|
+
if (oldMarker === '[X]') oldMarker = '[x]';
|
|
207
239
|
if (!Object.values(STATUS_MARKERS).includes(oldMarker)) {
|
|
208
240
|
console.error(`Row ${id}: status column "${oldMarker}" is not a recognized marker`);
|
|
209
241
|
process.exit(1);
|
|
210
242
|
}
|
|
211
|
-
|
|
243
|
+
// rewrite only the status cell — a marker string elsewhere in the row
|
|
244
|
+
// (name, touched files) must not be clobbered by a row-wide replace
|
|
245
|
+
lines[i] = `| ${cols.slice(0, 4).join(' | ')} | ${newMarker} | ${cols.slice(5).join(' | ')} |`;
|
|
212
246
|
found = true;
|
|
213
247
|
break;
|
|
214
248
|
}
|
package/scripts/install.ps1
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
3
|
|
|
4
4
|
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
5
|
-
Write-Error 'mugiwara requires Node.js >= 20. Install it from https://nodejs.org first.'
|
|
5
|
+
Write-Error 'mugiwara requires Node.js >= 20.11. Install it from https://nodejs.org first.'
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# floor must match package.json "engines.node" exactly, and install.sh
|
|
9
|
+
node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>20||(a===20&&b>=11)?0:1)'
|
|
10
|
+
if ($LASTEXITCODE -ne 0) {
|
|
11
|
+
Write-Error "mugiwara requires Node.js >= 20.11 (found $(node --version))."
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
npx -y @ionivetech/mugiwara@latest @args
|
package/scripts/install.sh
CHANGED
|
@@ -4,13 +4,13 @@ set -euo pipefail
|
|
|
4
4
|
PKG="@ionivetech/mugiwara"
|
|
5
5
|
|
|
6
6
|
if ! command -v node >/dev/null 2>&1; then
|
|
7
|
-
echo "mugiwara requires Node.js >= 20. Install it first: https://nodejs.org" >&2
|
|
7
|
+
echo "mugiwara requires Node.js >= 20.11. Install it first: https://nodejs.org" >&2
|
|
8
8
|
exit 1
|
|
9
9
|
fi
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
if
|
|
13
|
-
echo "mugiwara requires Node.js >= 20 (found $(node --version))." >&2
|
|
11
|
+
# floor must match package.json "engines.node" exactly, and install.ps1
|
|
12
|
+
if ! node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>20||(a===20&&b>=11)?0:1)'; then
|
|
13
|
+
echo "mugiwara requires Node.js >= 20.11 (found $(node --version))." >&2
|
|
14
14
|
exit 1
|
|
15
15
|
fi
|
|
16
16
|
|