@ionivetech/mugiwara 0.6.1 → 0.6.3
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-continue.md +42 -10
- package/.opencode/commands/mugiwara-execute.md +1 -1
- package/.opencode/commands/mugiwara-heal.md +1 -1
- package/.opencode/commands/mugiwara-onboard.md +15 -0
- 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/mugiwara-helpers.mjs +0 -1
- package/.opencode/plugins/mugiwara.mjs +33 -22
- package/AGENTS.md +16 -5
- package/README.md +80 -27
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/chopper-checkpoint.md +1 -1
- package/content/agents/eval-runner.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/jinbe-security.md +1 -1
- package/content/agents/luffy-orchestrator.md +14 -4
- package/content/agents/memory-keeper.md +1 -1
- package/content/agents/nami-planner.md +2 -2
- package/content/agents/onboarding-guide.md +24 -45
- package/content/agents/resume-coordinator.md +11 -11
- package/content/agents/robin-reviewer.md +1 -1
- package/content/agents/sanji-quality.md +1 -1
- package/content/agents/skeptic-verifier.md +1 -1
- package/content/agents/usopp-brainstorm.md +2 -2
- package/content/agents/zoro-execution.md +6 -2
- package/content/skills/mugiwara-agent-security/SKILL.md +1 -18
- package/content/skills/mugiwara-agent-security/references/checklist.md +20 -0
- package/content/skills/mugiwara-backend/SKILL.md +1 -1
- package/content/skills/mugiwara-brainstorm/SKILL.md +7 -1
- 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 +22 -22
- package/content/skills/mugiwara-execution/references/dispatch.md +4 -3
- package/content/skills/mugiwara-execution/references/resume-batching.md +4 -4
- package/content/skills/mugiwara-frontend/SKILL.md +1 -1
- package/content/skills/mugiwara-healing/SKILL.md +3 -38
- package/content/skills/mugiwara-healing/references/workers.md +38 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +30 -33
- package/content/skills/mugiwara-orchestration/references/check-ins.md +42 -0
- package/content/skills/mugiwara-orchestration/references/closure.md +1 -1
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +9 -12
- package/content/skills/mugiwara-planning/SKILL.md +7 -11
- package/content/skills/mugiwara-planning/references/plan-template.md +4 -4
- package/content/skills/mugiwara-pr/SKILL.md +11 -9
- package/content/skills/mugiwara-resume/SKILL.md +37 -22
- package/content/skills/mugiwara-ship/SKILL.md +2 -24
- package/content/skills/mugiwara-ship/references/cleanup.md +25 -0
- package/content/skills/mugiwara-sunset/SKILL.md +1 -1
- package/content/skills/mugiwara-workflow/SKILL.md +4 -2
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +16 -7
- package/content/skills/using-mugiwara/SKILL.md +5 -1
- package/dist/mugiwara.js +107 -37
- package/gemini-extension.json +1 -1
- package/hooks/session-start.ts +113 -2
- package/package.json +3 -3
- package/plugin.json +1 -1
- package/references/multi-actor.md +39 -14
- package/references/skill-versioning.md +3 -3
- package/references/token-budget.md +1 -1
- package/scripts/evidence.sh +9 -0
- package/scripts/gate-selftest.ts +123 -0
- package/scripts/lane-base.ts +114 -0
- package/scripts/lane.sh +4 -2
- package/scripts/lib/lane-base.sh +19 -0
- package/scripts/lib/patterns.sh +15 -0
- package/scripts/mission-report.sh +21 -5
- package/scripts/onboard.ts +3 -29
- package/scripts/release-notes.ts +152 -75
- package/scripts/savepoint.sh +178 -56
- package/scripts/setup-fixtures.ts +108 -0
- package/scripts/validate-content.ts +61 -0
- package/src/cli.ts +5 -1
- package/src/installer.ts +70 -8
- package/src/mission.ts +39 -24
- package/src/targets/claude.ts +5 -3
- package/src/targets/opencode.ts +12 -8
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/setup-fixtures.ts — materialize test/fixtures/*.json into real git
|
|
3
|
+
// repos in a temp dir. Each fixture = a JSON manifest with base/branch file
|
|
4
|
+
// trees. The script writes the base tree on the trunk branch, creates the
|
|
5
|
+
// feature branch, applies the branch tree, and commits. Used by tests to get
|
|
6
|
+
// a deterministic repo per scenario. Never touches the main repo's .git.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// bun scripts/setup-fixtures.ts <fixture> <targetDir>
|
|
10
|
+
// bun scripts/setup-fixtures.ts --list
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const fixturesDir = join(import.meta.dirname, '..', 'test', 'fixtures');
|
|
17
|
+
|
|
18
|
+
function fixtureNames(): string[] {
|
|
19
|
+
return readdirSync(fixturesDir).filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (process.argv[2] === '--list') {
|
|
23
|
+
console.log(fixtureNames().join('\n'));
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const name = process.argv[2];
|
|
28
|
+
const target = process.argv[3];
|
|
29
|
+
if (!name || !target) {
|
|
30
|
+
console.error('usage: bun scripts/setup-fixtures.ts <fixture> <targetDir>');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Fixture = {
|
|
35
|
+
name: string;
|
|
36
|
+
baseBranch: string;
|
|
37
|
+
base: Record<string, string>;
|
|
38
|
+
branch: Record<string, string>;
|
|
39
|
+
binary?: string[];
|
|
40
|
+
gitInit?: boolean;
|
|
41
|
+
note?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const file = join(fixturesDir, name + '.json');
|
|
45
|
+
if (!existsSync(file)) {
|
|
46
|
+
console.error(`fixture not found: ${name} (have: ${fixtureNames().join(', ')})`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const fx = JSON.parse(readFileSync(file, 'utf8')) as Fixture;
|
|
50
|
+
|
|
51
|
+
// target must be a fresh empty dir
|
|
52
|
+
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
|
|
53
|
+
mkdirSync(target, { recursive: true });
|
|
54
|
+
|
|
55
|
+
if (fx.gitInit === false) {
|
|
56
|
+
// no-git fixture: just an empty dir with one file, no .git ever created
|
|
57
|
+
mkdirSync(join(target, 'src'), { recursive: true });
|
|
58
|
+
writeFileSync(join(target, 'src', 'main.ts'), 'export const main = () => 1;\n');
|
|
59
|
+
console.log(`fixture ${name}: materialized (no .git)`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const writeTree = (tree: Record<string, string>) => {
|
|
64
|
+
for (const [path, content] of Object.entries(tree)) {
|
|
65
|
+
const full = join(target, path);
|
|
66
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
67
|
+
writeFileSync(full, content);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// names feed unquoted git branch args — validate before interpolation (F3).
|
|
72
|
+
// Fixture files are repo-owned, but `name` is a CLI arg and `trunk` comes from
|
|
73
|
+
// fixture JSON; both must be safe git ref names.
|
|
74
|
+
const SAFE_REF = /^[A-Za-z0-9._/-]+$/;
|
|
75
|
+
if (!SAFE_REF.test(name) || !SAFE_REF.test(fx.baseBranch || '')) {
|
|
76
|
+
console.error(`fixture ${name}: unsafe branch name rejected`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
execSync('git init -q', { cwd: target });
|
|
81
|
+
execSync('git config user.email test@test.com && git config user.name Test', { cwd: target });
|
|
82
|
+
|
|
83
|
+
const trunk = fx.baseBranch || 'main';
|
|
84
|
+
// branch named after trunk if trunk isn't main (so savepoint base-resolve paths differ)
|
|
85
|
+
execSync(`git checkout -q -b ${trunk}`, { cwd: target });
|
|
86
|
+
|
|
87
|
+
// base commit on trunk
|
|
88
|
+
writeTree(fx.base);
|
|
89
|
+
for (const rel of fx.binary ?? []) {
|
|
90
|
+
if (fx.base[rel]) writeFileSync(join(target, rel), fx.base[rel]);
|
|
91
|
+
}
|
|
92
|
+
execSync('git add -A && git commit -q -m base', { cwd: target });
|
|
93
|
+
|
|
94
|
+
// feature branch applies the branch tree (files removed from base not in
|
|
95
|
+
// branch get deleted; contents updated; new files added)
|
|
96
|
+
execSync(`git checkout -q -b feat-${fx.name}`, { cwd: target });
|
|
97
|
+
for (const rel of Object.keys(fx.base)) {
|
|
98
|
+
if (!(rel in fx.branch)) {
|
|
99
|
+
execSync(`git rm -q --ignore-unmatch "${rel}"`, { cwd: target });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
writeTree(fx.branch);
|
|
103
|
+
for (const rel of fx.binary ?? []) {
|
|
104
|
+
if (fx.branch[rel]) writeFileSync(join(target, rel), fx.branch[rel]);
|
|
105
|
+
}
|
|
106
|
+
execSync('git add -A && git commit -q -m wip', { cwd: target });
|
|
107
|
+
|
|
108
|
+
console.log(`fixture ${name}: materialized at ${target} (trunk=${trunk}, branch=feat-${fx.name})`);
|
|
@@ -319,5 +319,66 @@ const agentsDoc = join(docsDir, 'concepts', 'agents.md');
|
|
|
319
319
|
if (docErrors === 0) console.log('✓ docs in sync with content/');
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
// --- doc-integrity check: documented thresholds must match source constants ---
|
|
323
|
+
// Docs are claims; a claim that drifts from the source is a lie. LANE_BASE +
|
|
324
|
+
// BUDGET live in scripts/lib/lane-base.sh (validated by lane-base.ts); the
|
|
325
|
+
// cost docs must cite the same numbers. Evidence logs carry an exit code /
|
|
326
|
+
// verdict claim — a test must back it (lane-integrity cases 22-24).
|
|
327
|
+
const integrityArg = process.argv.indexOf('--check-doc-integrity');
|
|
328
|
+
if (integrityArg !== -1) {
|
|
329
|
+
const src = join(import.meta.dirname, '..', 'scripts', 'lib', 'lane-base.sh');
|
|
330
|
+
if (!existsSync(src)) {
|
|
331
|
+
errors.push('doc-integrity: scripts/lib/lane-base.sh not found — cannot verify thresholds');
|
|
332
|
+
} else {
|
|
333
|
+
const constants = readFileSync(src, 'utf8');
|
|
334
|
+
const docs = ['docs/concepts/cost.md', 'docs/concepts/lanes.md', 'README.md'];
|
|
335
|
+
const expected: [string, string][] = [
|
|
336
|
+
['lean', '7,000'], ['standard', '13,000'], ['full', '23,000'],
|
|
337
|
+
['lean', '12,000'], ['standard', '25,000'], ['full', '50,000'],
|
|
338
|
+
];
|
|
339
|
+
for (const doc of docs) {
|
|
340
|
+
const p = join(import.meta.dirname, '..', doc);
|
|
341
|
+
if (!existsSync(p)) { errors.push(`doc-integrity: ${doc} not found`); continue; }
|
|
342
|
+
const text = readFileSync(p, 'utf8');
|
|
343
|
+
for (const [lane, num] of expected) {
|
|
344
|
+
// each doc must cite the budget/LANE_BASE number for that lane; accept
|
|
345
|
+
// both the comma form (7,000) and the k-shorthand (7k / ~7k)
|
|
346
|
+
const compact = num.replace(',', '').replace(/0+$/, '');
|
|
347
|
+
const kForm = /^(\d+)000$/.exec(num);
|
|
348
|
+
const variants = [num, compact, ...(kForm ? [`${kForm[1]}k`, `~${kForm[1]}k`] : [])];
|
|
349
|
+
if (!variants.some(v => text.includes(v))) {
|
|
350
|
+
errors.push(`doc-integrity: ${doc} missing ${lane} threshold ${num} (source: ${src})`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// warn/stop are 1.5x / 3x of BUDGET (code: WARN_AT=BUDGET*3/2,
|
|
354
|
+
// STOP_AT=BUDGET*3) — presence is not enough, the arithmetic must be
|
|
355
|
+
// right. A doc claiming 1.5x/3x of LANE_BASE is the exact lie this gate
|
|
356
|
+
// exists to catch. Checked on the authoritative cost doc only.
|
|
357
|
+
if (doc === 'docs/concepts/cost.md') {
|
|
358
|
+
const budgets: [string, number][] = [['lean', 12000], ['standard', 25000], ['full', 50000]];
|
|
359
|
+
for (const [lane, budget] of budgets) {
|
|
360
|
+
const warn = budget * 3 / 2;
|
|
361
|
+
const stop = budget * 3;
|
|
362
|
+
const warnForms = [`${warn}`, `${Math.round(warn / 1000)}k`, `${warn.toLocaleString()}`, `${warn / 1000}k`];
|
|
363
|
+
const stopForms = [`${stop}`, `${stop / 1000}k`, `${stop.toLocaleString()}`];
|
|
364
|
+
const hasWarn = warnForms.some(f => text.includes(f));
|
|
365
|
+
const hasStop = stopForms.some(f => text.includes(f));
|
|
366
|
+
if (!hasWarn || !hasStop) {
|
|
367
|
+
errors.push(`doc-integrity: ${doc} ${lane} warn/stop must be 1.5x/3x of budget ${budget} (warn ${warn}, stop ${stop})`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!constants.includes('LANE_BASE_lean=7000')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 7000)');
|
|
373
|
+
if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
|
|
374
|
+
}
|
|
375
|
+
// evidence exit-code claim must be test-backed
|
|
376
|
+
const evTest = join(import.meta.dirname, '..', 'test', 'lane-integrity.test.ts');
|
|
377
|
+
const testText = existsSync(evTest) ? readFileSync(evTest, 'utf8') : '';
|
|
378
|
+
if (!testText.includes('# Exit:') || !testText.includes('# Verdict:')) {
|
|
379
|
+
errors.push('doc-integrity: audit-trail claims evidence logs carry exit code + verdict, but no test asserts them');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
322
383
|
if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
|
|
323
384
|
console.log(`✓ content valid: ${skillDirs.length} skills, ${agentFiles.length} agents`);
|
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { parseArgs, type FlagValue, type Args } from './args.ts';
|
|
8
8
|
import { createRl, choose, multiChoose, confirm } from './prompt.ts';
|
|
9
9
|
import { targets, TARGET_IDS } from './targets/index.ts';
|
|
10
|
-
import { installTo, removeInstalled, VERSION, ensureProjectGitignore } from './installer.ts';
|
|
10
|
+
import { installTo, removeInstalled, VERSION, ensureProjectGitignore, removeProjectGitignore } from './installer.ts';
|
|
11
11
|
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
12
12
|
import { resetMission, archiveMission } from './mission.ts';
|
|
13
13
|
|
|
@@ -151,6 +151,10 @@ async function uninstall(flags: Args['flags']): Promise<void> {
|
|
|
151
151
|
}
|
|
152
152
|
const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
|
|
153
153
|
if (!flag(flags.dryRun)) {
|
|
154
|
+
if (scope === 'project') {
|
|
155
|
+
const gi = removeProjectGitignore(projectDir);
|
|
156
|
+
for (const n of gi.notes) console.log(` note: ${n}`);
|
|
157
|
+
}
|
|
154
158
|
rmSync(file);
|
|
155
159
|
const mugiDir = join(scope === 'global' ? home : projectDir, '.mugiwara');
|
|
156
160
|
if (existsSync(mugiDir) && readdirSync(mugiDir).length === 0) {
|
package/src/installer.ts
CHANGED
|
@@ -182,28 +182,90 @@ function assertNotSymlink(file: string): void {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
const GITIGNORE_BLOCK_START = '# >>> mugiwara >>>';
|
|
186
|
+
const GITIGNORE_BLOCK_END = '# <<< mugiwara <<<';
|
|
187
|
+
// legacy marker from v0.6.2 — block was undelimited, single '# mugiwara'
|
|
188
|
+
// header. Kept for detection so an old install is not double-appended.
|
|
185
189
|
const GITIGNORE_MARKER = '# mugiwara';
|
|
186
|
-
const GITIGNORE_BLOCK = `# mugiwara — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
|
|
190
|
+
const GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
|
|
187
191
|
# Ignore session state and regenerated files.
|
|
188
|
-
.mugiwara/state
|
|
189
|
-
.mugiwara/
|
|
192
|
+
.mugiwara/state/
|
|
193
|
+
.mugiwara/continue/
|
|
190
194
|
.mugiwara/config
|
|
191
|
-
.mugiwara/continue.md
|
|
192
195
|
.mugiwara/refs/
|
|
196
|
+
# <<< mugiwara <<<
|
|
193
197
|
`;
|
|
194
198
|
|
|
195
199
|
export function ensureProjectGitignore(projectDir: string, opts: { dryRun?: boolean } = {}): { appended: boolean; notes: string[] } {
|
|
196
200
|
const { dryRun = false } = opts;
|
|
197
201
|
const path = join(projectDir, '.gitignore');
|
|
198
202
|
assertNotSymlink(path);
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
let existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
204
|
+
if (existing) {
|
|
205
|
+
// Delimited block present with the current entries → nothing to do.
|
|
206
|
+
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes('.mugiwara/state/')) {
|
|
207
|
+
return { appended: false, notes: [] };
|
|
208
|
+
}
|
|
209
|
+
// Legacy (v0.6.2) or outdated delimited block → upgrade in place:
|
|
210
|
+
// strip the old mugiwara entries (both forms), then append the new block.
|
|
211
|
+
// Without this, upgraded projects keep committing per-wave state/continue
|
|
212
|
+
// JSON to git — session state must stay ignored (Robin MAJOR, Jinbe Low).
|
|
213
|
+
const hadOld = existing.includes(GITIGNORE_MARKER) || existing.includes(GITIGNORE_BLOCK_START);
|
|
214
|
+
if (hadOld) {
|
|
215
|
+
const clean = removeProjectGitignore(projectDir, { dryRun });
|
|
216
|
+
existing = clean.removed ? (existsSync(path) ? readFileSync(path, 'utf8') : '') : existing;
|
|
217
|
+
}
|
|
201
218
|
}
|
|
202
|
-
const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
203
219
|
const separator = existing.length && !existing.endsWith('\n') ? '\n' : '';
|
|
204
220
|
if (!dryRun) {
|
|
205
221
|
mkdirSync(dirname(path), { recursive: true });
|
|
206
222
|
writeFileSync(path, existing + separator + GITIGNORE_BLOCK);
|
|
207
223
|
}
|
|
208
|
-
return { appended: true, notes: [`.gitignore ${dryRun ? 'would append' : '
|
|
224
|
+
return { appended: true, notes: [`.gitignore ${dryRun ? 'would upgrade+append' : 'upgraded'} mugiwara audit-trail block`] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// remove the delimited mugiwara block, preserving user lines. Handles both
|
|
228
|
+
// the current delimited block and the legacy undelimited block (v0.6.2).
|
|
229
|
+
export function removeProjectGitignore(projectDir: string, { dryRun = false }: { dryRun?: boolean } = {}): { removed: boolean; notes: string[] } {
|
|
230
|
+
const path = join(projectDir, '.gitignore');
|
|
231
|
+
assertNotSymlink(path);
|
|
232
|
+
if (!existsSync(path)) return { removed: false, notes: [] };
|
|
233
|
+
const current = readFileSync(path, 'utf8');
|
|
234
|
+
|
|
235
|
+
const delimited = current.includes(GITIGNORE_BLOCK_START);
|
|
236
|
+
let cleaned: string;
|
|
237
|
+
if (delimited) {
|
|
238
|
+
// strip everything between (and including) the delimiters. Both
|
|
239
|
+
// delimiters must be present — a missing END (hand-edit) with end=-1
|
|
240
|
+
// would slice() from char 18 and truncate the user's file.
|
|
241
|
+
const start = current.indexOf(GITIGNORE_BLOCK_START);
|
|
242
|
+
const end = current.indexOf(GITIGNORE_BLOCK_END);
|
|
243
|
+
if (end < start) return { removed: false, notes: ['delimiter mismatch — .gitignore left untouched'] };
|
|
244
|
+
cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
|
|
245
|
+
} else if (current.includes(GITIGNORE_MARKER)) {
|
|
246
|
+
// legacy (v0.6.2): strip the exact undelimited block — the header, the
|
|
247
|
+
// explanatory comment, and the five known lines. Prefix-matching would
|
|
248
|
+
// delete user-owned lines that merely start with a mugiwara path.
|
|
249
|
+
const LEGACY_LINES = new Set([
|
|
250
|
+
'.mugiwara/state.json',
|
|
251
|
+
'.mugiwara/state-*.json',
|
|
252
|
+
'.mugiwara/config',
|
|
253
|
+
'.mugiwara/continue.md',
|
|
254
|
+
'.mugiwara/refs/',
|
|
255
|
+
]);
|
|
256
|
+
const lines = current.split('\n').filter(l => {
|
|
257
|
+
const t = l.trim();
|
|
258
|
+
if (t.startsWith(GITIGNORE_MARKER)) return false; // header + comment
|
|
259
|
+
return !LEGACY_LINES.has(t);
|
|
260
|
+
});
|
|
261
|
+
cleaned = lines.join('\n');
|
|
262
|
+
} else {
|
|
263
|
+
return { removed: false, notes: [] };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').replace(/^\n+/, '').replace(/\n+$/, '\n');
|
|
267
|
+
if (cleaned.trim() === '') cleaned = '';
|
|
268
|
+
if (cleaned === current) return { removed: false, notes: [] };
|
|
269
|
+
if (!dryRun) writeFileSync(path, cleaned);
|
|
270
|
+
return { removed: true, notes: [`.gitignore ${dryRun ? 'would strip' : 'stripped'} mugiwara block`] };
|
|
209
271
|
}
|
package/src/mission.ts
CHANGED
|
@@ -4,12 +4,23 @@ import { existsSync, rmSync, readFileSync, readdirSync, mkdirSync, appendFileSyn
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
function activeActor(projectDir: string): string | null {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// state now lives at .mugiwara/state/<mission>/[member].json — scan the
|
|
8
|
+
// latest state file for its actor
|
|
9
|
+
const stateDir = join(projectDir, '.mugiwara', 'state');
|
|
10
|
+
if (!existsSync(stateDir)) return null;
|
|
11
|
+
const missions = readdirSync(stateDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name);
|
|
12
|
+
let latest: { actor: string; updated: number } | null = null;
|
|
13
|
+
for (const mission of missions) {
|
|
14
|
+
const d = join(stateDir, mission);
|
|
15
|
+
for (const f of readdirSync(d).filter(f => f.endsWith('.json'))) {
|
|
16
|
+
try {
|
|
17
|
+
const s = JSON.parse(readFileSync(join(d, f), 'utf8'));
|
|
18
|
+
const t = Date.parse(s.updated_at || '') || 0;
|
|
19
|
+
if (!latest || t > latest.updated) latest = { actor: s.actor || null, updated: t };
|
|
20
|
+
} catch { /* corrupt — skip */ }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return latest ? latest.actor : null;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export function resetMission(projectDir: string, keepLogs: boolean, force?: boolean): { removed: string[]; kept: string[]; blocked?: string } {
|
|
@@ -30,7 +41,12 @@ export function resetMission(projectDir: string, keepLogs: boolean, force?: bool
|
|
|
30
41
|
const p = join(root, dir);
|
|
31
42
|
if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push(dir); }
|
|
32
43
|
}
|
|
33
|
-
// mission state
|
|
44
|
+
// mission state + continue folders — state/<mission>/, continue/<mission>/
|
|
45
|
+
for (const dir of ['state', 'continue']) {
|
|
46
|
+
const p = join(root, dir);
|
|
47
|
+
if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push(dir); }
|
|
48
|
+
}
|
|
49
|
+
// legacy flat state files — state.json + branch-specific state-*.json
|
|
34
50
|
for (const f of readdirSync(root)) {
|
|
35
51
|
if (/^state(-.+)?\.json$/.test(f)) {
|
|
36
52
|
const p = join(root, f);
|
|
@@ -80,15 +96,13 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
80
96
|
if (f) report = join('reports', f);
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
99
|
+
// step results 01..05 + todos.md are evidence — kept; archive removes
|
|
100
|
+
// step results 01..05 + todos.md are evidence — kept; archive removes
|
|
101
|
+
// only spec/review/issues/logs + continue/<mission>/ + state/<mission>/
|
|
85
102
|
const resultsDir = join(root, 'results', mission);
|
|
86
103
|
if (existsSync(resultsDir)) {
|
|
87
104
|
for (const f of readdirSync(resultsDir)) {
|
|
88
|
-
|
|
89
|
-
const p = join(resultsDir, f);
|
|
90
|
-
if (!dryRun) rmSync(p, { recursive: true, force: true });
|
|
91
|
-
removed.push(join('results', mission, f));
|
|
105
|
+
kept.push(join('results', mission, f));
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|
|
@@ -124,21 +138,22 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
// continue
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
// continue/<mission>/ is a session handoff — remove this mission's folder
|
|
142
|
+
const contDir = join(root, 'continue', mission);
|
|
143
|
+
if (existsSync(contDir)) {
|
|
144
|
+
if (!dryRun) rmSync(contDir, { recursive: true, force: true });
|
|
145
|
+
removed.push(join('continue', mission));
|
|
146
|
+
}
|
|
147
|
+
// state/<mission>/ — remove this mission's computed state (archived)
|
|
148
|
+
const stateDir = join(root, 'state', mission);
|
|
149
|
+
if (existsSync(stateDir)) {
|
|
150
|
+
if (!dryRun) rmSync(stateDir, { recursive: true, force: true });
|
|
151
|
+
removed.push(join('state', mission));
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
// kept: report + the audit-trail survivors
|
|
140
155
|
if (report) kept.push(report);
|
|
141
|
-
for (const k of ['plans', 'config',
|
|
156
|
+
for (const k of ['plans', 'config', join('logs', 'lessons.md')]) {
|
|
142
157
|
if (existsSync(join(root, k))) kept.push(k);
|
|
143
158
|
}
|
|
144
159
|
|
package/src/targets/claude.ts
CHANGED
|
@@ -10,8 +10,10 @@ const HOOKS_SRC = join(here, '..', '..', 'hooks');
|
|
|
10
10
|
const COMMANDS_SRC = join(here, '..', '..', '.claude', 'commands');
|
|
11
11
|
|
|
12
12
|
// Claude Code has no path-scoped permission. write-scope maps to a partial
|
|
13
|
-
// `tools:` list
|
|
14
|
-
//
|
|
13
|
+
// `tools:` list — applied to internal subagent-only agents only: artifacts
|
|
14
|
+
// agents lose Edit (cannot modify existing source) but keep Write (must create
|
|
15
|
+
// .mugiwara/**). User-facing crew run inline in the main thread and keep the
|
|
16
|
+
// default toolset (incl. Edit); discipline is enforced by rules, not tools.
|
|
15
17
|
function toolsFromScope(scope?: string): string | undefined {
|
|
16
18
|
if (scope === 'artifacts') return 'Read, Grep, Glob, Write, Bash, WebFetch, WebSearch';
|
|
17
19
|
return undefined;
|
|
@@ -35,7 +37,7 @@ export const target: Target = {
|
|
|
35
37
|
transformAgent(data: FrontmatterData, body: string) {
|
|
36
38
|
const fm: FrontmatterData = { name: data.name, description: data.description };
|
|
37
39
|
if (data.tools) fm.tools = data.tools;
|
|
38
|
-
else {
|
|
40
|
+
else if (data['internal-agent'] === 'true') {
|
|
39
41
|
const generated = toolsFromScope(data['write-scope']);
|
|
40
42
|
if (generated) fm.tools = generated;
|
|
41
43
|
}
|
package/src/targets/opencode.ts
CHANGED
|
@@ -32,23 +32,27 @@ const CREW: Record<string, CrewConfig> = {
|
|
|
32
32
|
'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// write-scope is the single source of truth (content/agents/*.md frontmatter)
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
35
|
+
// write-scope is the single source of truth (content/agents/*.md frontmatter),
|
|
36
|
+
// but it is a RULE for user-facing crew agents: they run inline in the main
|
|
37
|
+
// thread, so a runtime permission bound to the active-agent identity would
|
|
38
|
+
// force tab-switching per wave and break auto mode + resume. Runtime
|
|
39
|
+
// enforcement stays for internal subagent-only agents (internal: true) — the
|
|
40
|
+
// path boundary IS expressible in opencode: permission.edit accepts
|
|
41
|
+
// glob/pattern -> action, last match wins. Artifacts internal agents get
|
|
42
|
+
// deny-all-edit except .mugiwara/**; source internal agents get full edit
|
|
43
|
+
// allow. Derived at install time from the frontmatter field.
|
|
40
44
|
function permissionFromScope(scope: string | undefined): Record<string, string | Record<string, string>> | undefined {
|
|
41
45
|
if (scope === 'source') return { edit: 'allow' };
|
|
42
46
|
if (scope === 'artifacts') return { edit: { '*': 'deny', '.mugiwara/**': 'allow' } };
|
|
43
47
|
return undefined;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
function agentFrontmatter(name: string, description: string, writeScope?: string) {
|
|
50
|
+
function agentFrontmatter(name: string, description: string, internal: boolean, writeScope?: string) {
|
|
47
51
|
const crew = CREW[name];
|
|
48
52
|
const lines = [`description: ${description}`, `mode: all`];
|
|
49
53
|
if (crew) {
|
|
50
54
|
lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
|
|
51
|
-
const perm = permissionFromScope(writeScope);
|
|
55
|
+
const perm = internal ? permissionFromScope(writeScope) : undefined;
|
|
52
56
|
if (perm) {
|
|
53
57
|
lines.push('permission:');
|
|
54
58
|
for (const [k, v] of Object.entries(perm)) {
|
|
@@ -81,7 +85,7 @@ export const target: Target = {
|
|
|
81
85
|
},
|
|
82
86
|
transformAgent(data: FrontmatterData, body: string) {
|
|
83
87
|
const desc = data['internal-agent'] === 'true' ? `[INTERNAL] ${data.description}` : data.description;
|
|
84
|
-
const fm = agentFrontmatter(data.name, desc, data['write-scope']);
|
|
88
|
+
const fm = agentFrontmatter(data.name, desc, data['internal-agent'] === 'true', data['write-scope']);
|
|
85
89
|
return { relPath: `${data.name}.md`, text: `---\n${fm}\n---\n${body}` };
|
|
86
90
|
},
|
|
87
91
|
refsDir({ scope, projectDir, home }, skillName: string) {
|