@ionivetech/mugiwara 0.6.6 → 0.7.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -7
- package/.codex-plugin/plugin.json +2 -7
- package/.cursor-plugin/plugin.json +2 -7
- package/.kimi-plugin/plugin.json +2 -7
- package/.opencode/plugins/mugiwara.mjs +3 -18
- package/AGENTS.md +5 -4
- package/GEMINI.md +3 -3
- package/README.md +69 -63
- package/content/agents/brook-healing.md +8 -4
- package/content/agents/chopper-checkpoint.md +7 -3
- package/content/agents/eval-runner.md +4 -4
- package/content/agents/franky-gates.md +3 -3
- package/content/agents/jinbe-security.md +6 -7
- package/content/agents/luffy-orchestrator.md +11 -13
- package/content/agents/memory-keeper.md +4 -4
- package/content/agents/nami-planner.md +4 -4
- package/content/agents/resume-coordinator.md +7 -7
- package/content/agents/robin-reviewer.md +7 -3
- package/content/agents/sanji-quality.md +2 -2
- package/content/agents/skeptic-verifier.md +2 -2
- package/content/agents/usopp-brainstorm.md +3 -3
- package/content/agents/zoro-execution.md +6 -6
- package/content/skills/mugiwara-backend/SKILL.md +2 -0
- package/content/skills/mugiwara-backend/references/database.md +61 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +6 -5
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
- package/content/skills/mugiwara-execution/SKILL.md +8 -8
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +3 -2
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +8 -5
- package/content/skills/mugiwara-healing/SKILL.md +2 -2
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
- package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
- package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
- package/content/skills/mugiwara-planning/SKILL.md +3 -18
- package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
- package/content/skills/mugiwara-quality/SKILL.md +5 -2
- package/content/skills/mugiwara-resume/SKILL.md +2 -2
- package/content/skills/mugiwara-review/SKILL.md +3 -2
- package/content/skills/mugiwara-security/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
- package/content/skills/mugiwara-workflow/SKILL.md +9 -12
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
- package/dist/mugiwara.js +904 -462
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +18 -10
- package/hooks/auto-savepoint.ts +23 -23
- package/hooks/engagement-marker.js +1 -1
- package/hooks/engagement-marker.ts +1 -1
- package/hooks/pipeline-guard.js +16 -12
- package/hooks/pipeline-guard.ts +22 -19
- package/hooks/session-start.js +13 -10
- package/hooks/session-start.ts +24 -19
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/complexity.md +27 -4
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +0 -3
- package/references/prose-style.md +54 -0
- package/scripts/check-doc-links.ts +40 -0
- package/scripts/conformance.ts +6 -16
- package/scripts/coverage-gate.ts +20 -6
- package/scripts/gate-selftest.ts +118 -7
- package/scripts/lane.sh +54 -8
- package/scripts/lib/patterns.sh +5 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +1 -1
- package/scripts/savepoint.sh +136 -58
- package/scripts/validate-content.ts +22 -9
- package/scripts/verify-install.ts +9 -7
- package/src/args.ts +2 -2
- package/src/budget.ts +56 -0
- package/src/cli.ts +161 -110
- package/src/continue.ts +29 -10
- package/src/installer.ts +25 -20
- package/src/integrity.ts +158 -0
- package/src/mission.ts +228 -95
- package/src/policy.ts +156 -0
- package/src/provenance.ts +116 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +2 -2
- package/src/sign.ts +61 -0
- package/src/targets/claude.ts +2 -2
- package/.opencode/commands/mugiwara-execute.md +0 -13
- package/.opencode/commands/mugiwara-heal.md +0 -13
- package/.opencode/commands/mugiwara-onboard.md +0 -14
- package/.opencode/commands/mugiwara-plan.md +0 -17
- package/.opencode/commands/mugiwara-ship.md +0 -13
- package/.opencode/commands/using-mugiwara.md +0 -20
- package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
- package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
- package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
- package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
- package/content/skills/mugiwara-pr/SKILL.md +0 -69
- package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
- package/content/skills/mugiwara-sunset/SKILL.md +0 -82
- package/content/skills/using-mugiwara/SKILL.md +0 -51
- package/references/token-budget.md +0 -56
- package/scripts/evidence.sh +0 -81
- package/scripts/initiative.ts +0 -296
- package/scripts/mission-report.sh +0 -293
- package/src/onboard.ts +0 -207
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/check-doc-links.ts — every relative .md link in README, ROADMAP,
|
|
3
|
+
// docs/, and examples/ must resolve to a file. Born from a shipped defect:
|
|
4
|
+
// glossary linked provenance.md from reference/ and four broken links passed
|
|
5
|
+
// every other gate, because verify-install covers content/ pointers only.
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
const root = join(import.meta.dirname, "..");
|
|
10
|
+
function walk(d: string, out: string[] = []): string[] {
|
|
11
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
12
|
+
const p = join(d, e.name);
|
|
13
|
+
if (e.isDirectory()) walk(p, out);
|
|
14
|
+
else if (/\.md$/.test(e.name)) out.push(p);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
const files: string[] = [];
|
|
19
|
+
for (const r of ["docs", "examples"]) files.push(...walk(join(root, r)));
|
|
20
|
+
for (const r of ["README.md", "ROADMAP.md"]) files.push(resolve(root, r));
|
|
21
|
+
|
|
22
|
+
let bad = 0;
|
|
23
|
+
for (const f of files) {
|
|
24
|
+
const body = readFileSync(f, "utf8");
|
|
25
|
+
for (const m of body.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
26
|
+
const t = m[1];
|
|
27
|
+
if (/^(https?:|mailto:|#|\/\/)/.test(t)) continue;
|
|
28
|
+
const clean = t.split("#")[0];
|
|
29
|
+
if (!clean.endsWith(".md")) continue;
|
|
30
|
+
if (!existsSync(resolve(f, "..", clean))) {
|
|
31
|
+
bad++;
|
|
32
|
+
console.log(`✗ ${f.replace(root + "/", "")} → ${t}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (bad) {
|
|
37
|
+
console.log(`check-doc-links: ${bad} broken link(s)`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.log("check-doc-links: all relative .md links resolve");
|
package/scripts/conformance.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// scripts/conformance.ts — C1: cross-platform conformance suite.
|
|
3
3
|
// Every installable target: materialize the standard-feature fixture repo,
|
|
4
|
-
// install the target, run the
|
|
5
|
-
// snapshot (state fields,
|
|
6
|
-
//
|
|
4
|
+
// install the target, run the core scripts, and compare a normalized
|
|
5
|
+
// snapshot (state fields, gitignore block, file count) against
|
|
6
|
+
// test/golden/<target>.json. Exit 1 on any difference
|
|
7
7
|
// with a diff; --update-golden regenerates. The snapshots are normalized:
|
|
8
8
|
// timestamps, hashes, and random filenames are excluded so a golden is stable
|
|
9
9
|
// across runs.
|
|
@@ -114,19 +114,13 @@ function snapshot(targetId: string): Record<string, unknown> {
|
|
|
114
114
|
writeFileSync(exclude, (existsSync(exclude) ? readFileSync(exclude, 'utf8') : '') + '\n' + untracked.join('\n') + '\n');
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// 3. run the
|
|
117
|
+
// 3. run the core scripts + the gitignore write the CLI does post-install
|
|
118
118
|
sh(`bash "${root}/scripts/lane.sh" main --json`, dir);
|
|
119
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
120
|
ensureProjectGitignore(dir, { dryRun: false });
|
|
123
121
|
|
|
124
122
|
// 4. normalize + collect
|
|
125
|
-
const state = JSON.parse(readFileSync(join(dir, '.mugiwara', '
|
|
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') : '';
|
|
123
|
+
const state = JSON.parse(readFileSync(join(dir, '.mugiwara', 'missions', MISSION, 'state.json'), 'utf8'));
|
|
130
124
|
const gitignore = existsSync(join(dir, '.gitignore')) ? readFileSync(join(dir, '.gitignore'), 'utf8') : '';
|
|
131
125
|
|
|
132
126
|
const countFiles = (p: string): number => {
|
|
@@ -161,11 +155,7 @@ function snapshot(targetId: string): Record<string, unknown> {
|
|
|
161
155
|
budget_status: state.budget_status,
|
|
162
156
|
sensitive_paths: state.sensitive_paths,
|
|
163
157
|
},
|
|
164
|
-
report_sections: [
|
|
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
|
-
},
|
|
158
|
+
report_sections: [],
|
|
169
159
|
gitignore_block: gitignore.split('\n').filter(l => l.includes('mugiwara') || l.includes('# ---')),
|
|
170
160
|
file_count: {
|
|
171
161
|
skills: countFiles(target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir),
|
package/scripts/coverage-gate.ts
CHANGED
|
@@ -75,10 +75,10 @@ const baseArg = process.argv.indexOf('--base');
|
|
|
75
75
|
let base = baseArg !== -1 ? process.argv[baseArg + 1] : process.env.MUGIWARA_COVERAGE_BASE;
|
|
76
76
|
if (!base) {
|
|
77
77
|
// the mission's own recorded base_sha is the truthful diff origin
|
|
78
|
-
const
|
|
79
|
-
if (existsSync(
|
|
80
|
-
for (const m of readdirSync(
|
|
81
|
-
const f = join(
|
|
78
|
+
const missionsDir = join(root, '.mugiwara', 'missions');
|
|
79
|
+
if (existsSync(missionsDir)) {
|
|
80
|
+
for (const m of readdirSync(missionsDir)) {
|
|
81
|
+
const f = join(missionsDir, m, 'state.json');
|
|
82
82
|
if (!existsSync(f)) continue;
|
|
83
83
|
const sha = JSON.parse(readFileSync(f, 'utf8')).base_sha;
|
|
84
84
|
if (sha) { base = sha; break; }
|
|
@@ -140,9 +140,23 @@ for (const [k, v] of Object.entries<any>(summary)) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// ---- 4. apply the two thresholds ------------------------------------------
|
|
143
|
+
// Policy as code: mugiwara.policy.yml gates.coverage can RAISE a
|
|
144
|
+
// threshold — the max of config and policy wins, never lower.
|
|
143
145
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
+
const { loadPolicy, effectiveThreshold } = await import('../src/policy.ts');
|
|
147
|
+
let policyCoverage: { new?: number; modified?: number } | undefined;
|
|
148
|
+
try {
|
|
149
|
+
const policy = loadPolicy(root);
|
|
150
|
+
policyCoverage = policy?.gates?.coverage;
|
|
151
|
+
if (policy?.gates?.require_human_approval?.length)
|
|
152
|
+
console.log(`coverage-gate: policy requires human approval on: ${policy.gates.require_human_approval.join(', ')}`);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.log(`coverage-gate: ${(e as Error).message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const NEW = effectiveThreshold(threshold('coverage_new', 90), policyCoverage?.new);
|
|
159
|
+
const MOD = effectiveThreshold(threshold('coverage_modified', 80), policyCoverage?.modified);
|
|
146
160
|
|
|
147
161
|
const rows = changed.map((c) => {
|
|
148
162
|
const isNew = c.status === 'A';
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -12,7 +12,7 @@ let passed = 0;
|
|
|
12
12
|
let failed = 0;
|
|
13
13
|
|
|
14
14
|
function run(name: string, cmd: string): boolean {
|
|
15
|
-
try { execSync(cmd, { cwd: root, stdio: 'pipe', timeout:
|
|
15
|
+
try { execSync(cmd, { cwd: root, stdio: 'pipe', timeout: 180000 }); return true; }
|
|
16
16
|
catch { return false; }
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -82,6 +82,28 @@ console.log('\nG5 — conditional-assertion guard');
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// --- Cost gate: prove a drifted stated index size goes red -------------------
|
|
86
|
+
// validate-content fails when docs/concepts/cost.md's "**Current:** N chars"
|
|
87
|
+
// disagrees with the measured skill+agent description total. Without this
|
|
88
|
+
// mutation the gate is unproven — a gate that cannot fail is not a gate.
|
|
89
|
+
console.log('\nCost gate — measured vs stated index chars');
|
|
90
|
+
{
|
|
91
|
+
const costFile = join(root, 'docs', 'concepts', 'cost.md');
|
|
92
|
+
const original = readFileSync(costFile, 'utf8');
|
|
93
|
+
try {
|
|
94
|
+
const drifted = original.replace(/\*\*Current:\*\* \d[\d,]* chars/, '**Current:** 1 chars');
|
|
95
|
+
if (drifted === original) {
|
|
96
|
+
console.log(' ⚠ "**Current:** N chars" pattern not found in cost.md — skipping');
|
|
97
|
+
} else {
|
|
98
|
+
writeFileSync(costFile, drifted);
|
|
99
|
+
assert('drifted stated index chars → exit 1', false, () => run('COST', 'bun scripts/validate-content.ts'));
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
writeFileSync(costFile, original);
|
|
103
|
+
assert('restored → exit 0', true, () => run('COST', 'bun scripts/validate-content.ts'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
// --- G3: savepoint fixtures — prove test fails when a field is broken ---
|
|
86
108
|
console.log('\nG3 — savepoint fixtures');
|
|
87
109
|
if (!existsSync(join(root, 'test', 'savepoint.test.ts'))) {
|
|
@@ -316,19 +338,19 @@ if (!existsSync(skillsDoc)) {
|
|
|
316
338
|
|
|
317
339
|
// --- Index budget ---
|
|
318
340
|
console.log('\nIndex budget');
|
|
319
|
-
const
|
|
320
|
-
if (!existsSync(
|
|
321
|
-
console.log(' ⚠
|
|
341
|
+
const budgetSkillFile = join(root, 'content', 'skills', 'mugiwara-workflow', 'SKILL.md');
|
|
342
|
+
if (!existsSync(budgetSkillFile)) {
|
|
343
|
+
console.log(' ⚠ mugiwara-workflow not found, skipping');
|
|
322
344
|
} else {
|
|
323
|
-
const original = readFileSync(
|
|
345
|
+
const original = readFileSync(budgetSkillFile, 'utf8');
|
|
324
346
|
try {
|
|
325
347
|
// Push budget way past 5500
|
|
326
348
|
const longDesc = 'A'.repeat(6000);
|
|
327
349
|
const broken = original.replace(/description:.*/, `description: ${longDesc}`);
|
|
328
|
-
writeFileSync(
|
|
350
|
+
writeFileSync(budgetSkillFile, broken);
|
|
329
351
|
assert('description past budget → exit 1', false, () => run('budget', 'bun scripts/validate-content.ts'));
|
|
330
352
|
} finally {
|
|
331
|
-
writeFileSync(
|
|
353
|
+
writeFileSync(budgetSkillFile, original);
|
|
332
354
|
assert('restored → exit 0', true, () => run('budget', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
333
355
|
}
|
|
334
356
|
}
|
|
@@ -481,5 +503,94 @@ if (!existsSync(countAgent)) {
|
|
|
481
503
|
}
|
|
482
504
|
}
|
|
483
505
|
|
|
506
|
+
// --- CI: closure integrity gate — a secret in the trail must fail the archive ---
|
|
507
|
+
console.log('\nCI — closure integrity gate');
|
|
508
|
+
{
|
|
509
|
+
const tmp = mkdtempSync(join(tmpdir(), 'mugi-ci-'));
|
|
510
|
+
try {
|
|
511
|
+
const mdir = join(tmp, '.mugiwara', 'missions', 'selftest-mission');
|
|
512
|
+
mkdirSync(mdir, { recursive: true });
|
|
513
|
+
const cli = `bun src/cli.ts archive selftest-mission --project ${tmp}`;
|
|
514
|
+
// A mission with no trail at all archives fine (stub report path).
|
|
515
|
+
assert('clean stub → exit 0', true, () => run('CI-clean', cli));
|
|
516
|
+
// A planted secret must turn the gate red.
|
|
517
|
+
writeFileSync(join(mdir, 'report.md'), 'token = ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\n');
|
|
518
|
+
writeFileSync(join(mdir, 'state.json'), JSON.stringify({ mission: 'selftest-mission' }));
|
|
519
|
+
assert('planted secret → exit 1', false, () => run('CI-secret', cli));
|
|
520
|
+
unlinkSync(join(mdir, 'report.md'));
|
|
521
|
+
unlinkSync(join(mdir, 'state.json'));
|
|
522
|
+
assert('cleaned → exit 0', true, () => run('CI-clean2', cli));
|
|
523
|
+
|
|
524
|
+
// Context-budget gate: a ceiling that the trail exceeds must fail red.
|
|
525
|
+
writeFileSync(join(mdir, 'state.json'), JSON.stringify({ mission: 'selftest-mission' }));
|
|
526
|
+
writeFileSync(join(mdir, 'report.md'), 'x'.repeat(500) + '\n');
|
|
527
|
+
mkdirSync(join(tmp, '.mugiwara'), { recursive: true });
|
|
528
|
+
writeFileSync(join(tmp, '.mugiwara', 'config'), 'context_budget_chars=10\n');
|
|
529
|
+
assert('over context budget → exit 1', false, () => run('CI-budget', cli));
|
|
530
|
+
unlinkSync(join(tmp, '.mugiwara', 'config'));
|
|
531
|
+
assert('budget unset → exit 0', true, () => run('CI-budget2', cli));
|
|
532
|
+
} finally {
|
|
533
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// --- POLICY: an invalid mugiwara.policy.yml fails lane.sh closed ---
|
|
538
|
+
console.log('\nPOLICY — fail-closed policy parsing');
|
|
539
|
+
{
|
|
540
|
+
const repo = mkdtempSync(join(tmpdir(), 'mugi-policy-'));
|
|
541
|
+
// lane.sh must run INSIDE the sandbox repo, not this one
|
|
542
|
+
const runInRepo = (args: string): boolean => {
|
|
543
|
+
try { execSync(`bash ${join(root, 'scripts', 'lane.sh')} ${args}`, { cwd: repo, stdio: 'pipe', timeout: 60000 }); return true; }
|
|
544
|
+
catch { return false; }
|
|
545
|
+
};
|
|
546
|
+
try {
|
|
547
|
+
execSync('git init -q && git config user.email t@t.com && git config user.name T && git commit --allow-empty -qm base', { cwd: repo, stdio: 'pipe' });
|
|
548
|
+
writeFileSync(join(repo, 'changed.ts'), 'export {};\n');
|
|
549
|
+
// no policy file → normal behavior
|
|
550
|
+
assert('no policy → lane runs', true, () => runInRepo('HEAD --json'));
|
|
551
|
+
// unknown root key must fail loud, not silently disable itself
|
|
552
|
+
writeFileSync(join(repo, 'mugiwara.policy.yml'), 'lanez:\n force_full: ["src/**"]\n');
|
|
553
|
+
assert('bogus policy key → exit 1', false, () => runInRepo('HEAD --json'));
|
|
554
|
+
// valid policy parses and can force full
|
|
555
|
+
writeFileSync(join(repo, 'mugiwara.policy.yml'), 'lanes:\n force_full:\n - "**/*.ts"\n');
|
|
556
|
+
assert('valid policy → lane runs', true, () => runInRepo('HEAD --json'));
|
|
557
|
+
} finally {
|
|
558
|
+
rmSync(repo, { recursive: true, force: true });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// --- T7: evidence-thin — fabricated PASS wave with empty body must fail ---
|
|
563
|
+
console.log('\nT7 — evidence-thin gate');
|
|
564
|
+
{
|
|
565
|
+
const tmp = mkdtempSync(join(tmpdir(), 'mugi-thin-'));
|
|
566
|
+
try {
|
|
567
|
+
const mdir = join(tmp, '.mugiwara', 'missions', 'selftest-thin');
|
|
568
|
+
mkdirSync(join(mdir, 'flows'), { recursive: true });
|
|
569
|
+
const cli = `bun src/cli.ts archive selftest-thin --project ${tmp}`;
|
|
570
|
+
writeFileSync(join(mdir, 'state.json'), JSON.stringify({ mission: 'selftest-thin', evidence: ['flows/04-gates.md'] }));
|
|
571
|
+
writeFileSync(join(mdir, 'report.md'), 'Verdict: PASS see [evidence](flows/04-gates.md)');
|
|
572
|
+
writeFileSync(join(mdir, 'flows', '04-gates.md'), 'all good'); // no command shape → thin
|
|
573
|
+
assert('fabricated PASS with empty body → exit 1', false, () => run('T7-thin', cli));
|
|
574
|
+
writeFileSync(join(mdir, 'flows', '04-gates.md'), 'ran `bun run test` → 2 passed, exit 0');
|
|
575
|
+
assert('with command output → exit 0', true, () => run('T7-good', cli));
|
|
576
|
+
} finally {
|
|
577
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// --- DOCLINKS: a relative .md link that does not resolve must fail the gate ---
|
|
582
|
+
console.log('\nDOCLINKS — doc link resolution');
|
|
583
|
+
{
|
|
584
|
+
const probe = join(root, 'docs', 'tmp-doclinks-probe.md');
|
|
585
|
+
try {
|
|
586
|
+
writeFileSync(probe, '[missing](./nowhere-at-all.md)\n');
|
|
587
|
+
assert('broken doc link → exit 1', false, () => run('DL-bad', 'bun scripts/check-doc-links.ts'));
|
|
588
|
+
unlinkSync(probe);
|
|
589
|
+
assert('cleaned → exit 0', true, () => run('DL-good', 'bun scripts/check-doc-links.ts'));
|
|
590
|
+
} finally {
|
|
591
|
+
if (existsSync(probe)) unlinkSync(probe);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
484
595
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
485
596
|
process.exit(failed > 0 ? 1 : 0);
|
package/scripts/lane.sh
CHANGED
|
@@ -19,17 +19,46 @@ if ! git rev-parse "$BASE" >/dev/null 2>&1; then
|
|
|
19
19
|
[ -n "$ALT" ] && BASE="$ALT" || BASE="HEAD~1"
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
+
# lane_scope_glob (T5): monorepo scoping — when set, count only files matching the glob
|
|
23
|
+
LANE_SCOPE_GLOB=""
|
|
24
|
+
if [ -f .mugiwara/config ]; then
|
|
25
|
+
CFG_SCOPE=$(grep -E '^lane_scope_glob=' .mugiwara/config 2>/dev/null | head -1 | cut -d= -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
|
|
26
|
+
[ -n "$CFG_SCOPE" ] && LANE_SCOPE_GLOB="$CFG_SCOPE"
|
|
27
|
+
fi
|
|
28
|
+
|
|
22
29
|
# union of committed + staged + unstaged + untracked (F) — see patterns.sh
|
|
23
30
|
CHANGED=$(changed_files "$BASE")
|
|
24
|
-
|
|
25
|
-
[ -n "$CHANGED" ] && FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
|
|
26
|
-
read -r ADDED_INS ADDED_DEL <<EOF
|
|
27
|
-
$(changed_loc "$BASE")
|
|
28
|
-
EOF
|
|
29
|
-
|
|
31
|
+
# sensitive-path escalation always evaluates the unfiltered set (safety never shrinks)
|
|
30
32
|
SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
|
31
33
|
HAS_SENSITIVE=0
|
|
32
34
|
[ -n "$SENSITIVE" ] && HAS_SENSITIVE=1
|
|
35
|
+
# scoped count for lane sizing (T5)
|
|
36
|
+
SCOPED="$CHANGED"
|
|
37
|
+
if [ -n "$LANE_SCOPE_GLOB" ] && [ -n "$CHANGED" ]; then
|
|
38
|
+
shopt -s extglob globstar 2>/dev/null || true
|
|
39
|
+
SCOPED=""
|
|
40
|
+
while IFS= read -r _f; do
|
|
41
|
+
[ -z "$_f" ] && continue
|
|
42
|
+
# shellcheck disable=SC2053
|
|
43
|
+
if [[ "$_f" == $LANE_SCOPE_GLOB ]]; then
|
|
44
|
+
SCOPED="${SCOPED}${SCOPED:+
|
|
45
|
+
}$_f"
|
|
46
|
+
fi
|
|
47
|
+
done <<< "$CHANGED"
|
|
48
|
+
fi
|
|
49
|
+
FILE_COUNT=0
|
|
50
|
+
[ -n "$SCOPED" ] && FILE_COUNT=$(echo "$SCOPED" | wc -l | tr -d ' ')
|
|
51
|
+
# LOC for the 1-file rule also scopes to the glob (otherwise a single in-scope
|
|
52
|
+
# file with <20 LOC could be inflated by out-of-scope churn)
|
|
53
|
+
if [ -n "$LANE_SCOPE_GLOB" ]; then
|
|
54
|
+
read -r ADDED_INS ADDED_DEL <<EOF
|
|
55
|
+
$( { echo "$SCOPED" | while IFS= read -r _sf; do [ -n "$_sf" ] && git diff --numstat "$BASE"..HEAD -- "$_sf" 2>/dev/null; git diff --numstat HEAD -- "$_sf" 2>/dev/null; done; echo "$SCOPED" | while IFS= read -r _uf; do [ -z "$_uf" ] && continue; if git ls-files --others --exclude-standard -- "$_uf" 2>/dev/null | grep -qx "$_uf"; then printf '%s\t0\t%s\n' "$(wc -l < "$_uf" 2>/dev/null | tr -d ' ' || echo 0)" "$_uf"; fi; done; } | awk '{ if ($1 ~ /^[0-9]+$/) i+=$1; if ($2 ~ /^[0-9]+$/) d+=$2 } END { print (i+0)" "(d+0) }')
|
|
56
|
+
EOF
|
|
57
|
+
else
|
|
58
|
+
read -r ADDED_INS ADDED_DEL <<EOF
|
|
59
|
+
$(changed_loc "$BASE")
|
|
60
|
+
EOF
|
|
61
|
+
fi
|
|
33
62
|
|
|
34
63
|
# lane logic
|
|
35
64
|
LANE="direct"
|
|
@@ -71,8 +100,9 @@ fi
|
|
|
71
100
|
# code surface actually touched. Sensitive-path escalation above still wins.
|
|
72
101
|
# Product surface for mugiwara: content/, src/, scripts/, test/, hooks/,
|
|
73
102
|
# .opencode/, .claude/, evals/ — everything else is docs/config/asset.
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
# With lane_scope_glob the check applies to the scoped set — sensitive still wins unfiltered.
|
|
104
|
+
if [ "$LANE" = "full" ] && [ "$HAS_SENSITIVE" -eq 0 ] && [ -n "$SCOPED" ]; then
|
|
105
|
+
CODE_COUNT=$(echo "$SCOPED" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
76
106
|
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
77
107
|
PREV="$LANE"
|
|
78
108
|
LANE="standard"
|
|
@@ -80,6 +110,22 @@ if [ "$LANE" = "full" ] && [ "$HAS_SENSITIVE" -eq 0 ] && [ -n "$CHANGED" ]; then
|
|
|
80
110
|
fi
|
|
81
111
|
fi
|
|
82
112
|
|
|
113
|
+
# policy as code: mugiwara.policy.yml lanes.force_full pushes the lane UP to
|
|
114
|
+
# full — always upward, never downward, and it wins over the docs-only
|
|
115
|
+
# downgrade above. Optional by design: no policy file (or no bun) = no-op.
|
|
116
|
+
if command -v bun >/dev/null 2>&1 && { [ -f mugiwara.policy.yml ] || [ -f mugiwara.policy.yaml ]; }; then
|
|
117
|
+
POLICY_HITS=$(printf '%s\n' "$CHANGED" | bun "$(dirname "$0")/policy-force.ts")
|
|
118
|
+
RC=$?
|
|
119
|
+
if [ "$RC" -ne 0 ]; then
|
|
120
|
+
echo "lane: mugiwara.policy.yml is invalid — fix or remove it (fail-closed)" >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
if [ -n "$POLICY_HITS" ]; then
|
|
124
|
+
LANE="full"
|
|
125
|
+
REASON="policy force_full ($POLICY_HITS)"
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
83
129
|
if [ "$JSON_OUT" -eq 1 ]; then
|
|
84
130
|
SENS_ARR=""
|
|
85
131
|
[ -n "$SENSITIVE" ] && SENS_ARR=$(echo "$SENSITIVE" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
|
package/scripts/lib/patterns.sh
CHANGED
|
@@ -31,6 +31,11 @@ PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^e
|
|
|
31
31
|
# (numstat rows).
|
|
32
32
|
HARNESS_PAT="\.claude/|\.opencode/|\.github/(instructions|agents)/|\.gemini/mugiwara/|\.codex/mugiwara/|\.devin/rules/|\.clinerules/|\.kilo/rules/|\.agents/rules/"
|
|
33
33
|
|
|
34
|
+
# lane_scope_glob — optional monorepo scoping (T5). When set in .mugiwara/config
|
|
35
|
+
# as e.g. lane_scope_glob=packages/app/** lane sizing counts only changed
|
|
36
|
+
# files matching the glob; sensitive-path escalation still evaluates the
|
|
37
|
+
# unfiltered set (safety never shrinks).
|
|
38
|
+
|
|
34
39
|
# harness_excluded — true (exit 0) when harness rules dirs must NOT count
|
|
35
40
|
# toward mission sizing, i.e. this repo is not mugiwara itself. Identity comes
|
|
36
41
|
# from the repo root package.json name ("mugiwara" or "@scope/mugiwara"), the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/policy-force.ts — policy-as-code helper for the bash lane logic.
|
|
3
|
+
// Reads changed file paths on stdin, prints the mugiwara.policy.yml
|
|
4
|
+
// `lanes.force_full` globs they match (comma-separated). Empty output = no
|
|
5
|
+
// policy hit. Exits 0 even when nothing matches; a missing bun skips the
|
|
6
|
+
// call site entirely (policy is optional by design).
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { loadPolicy, matchedGlobs } from '../src/policy.ts';
|
|
9
|
+
|
|
10
|
+
const input = readFileSync(0, 'utf8');
|
|
11
|
+
const paths = input.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
12
|
+
if (!paths.length) process.exit(0);
|
|
13
|
+
let policy;
|
|
14
|
+
try {
|
|
15
|
+
policy = loadPolicy(process.cwd());
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// Fail closed: an unreadable rule must not look like "no rule".
|
|
18
|
+
console.error(`policy-force: ${(e as Error).message}`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
const hits = policy?.lanes?.force_full?.length ? matchedGlobs(paths, policy.lanes.force_full) : [];
|
|
22
|
+
if (hits.length) process.stdout.write(hits.join(','));
|
|
@@ -18,7 +18,7 @@ function tokenize(text: string): string[] {
|
|
|
18
18
|
return text.toLowerCase()
|
|
19
19
|
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
20
20
|
.split(/\s+/)
|
|
21
|
-
.filter(t => t.length > 1 && !['the', 'and', 'for', 'use', 'when', 'that', 'with', 'this', 'from', 'its', 'not', 'are', 'has'].includes(t));
|
|
21
|
+
.filter(t => t.length > 1 && !['the', 'and', 'for', 'use', 'when', 'that', 'with', 'this', 'from', 'its', 'not', 'are', 'has', 'to', 'is', 'it', 'of', 'in', 'on', 'at', 'be', 'or', 'as', 'an'].includes(t));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function buildIndex(): Index {
|