@ionivetech/mugiwara 0.6.0 → 0.6.1
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/mugiwara-helpers.mjs +24 -0
- package/.opencode/plugins/mugiwara.mjs +10 -2
- package/AGENTS.md +1 -1
- package/README.md +115 -62
- package/content/skills/mugiwara-brainstorm/SKILL.md +7 -0
- package/content/skills/mugiwara-execution/SKILL.md +41 -41
- package/content/skills/mugiwara-execution/references/dispatch.md +41 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +28 -24
- package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +12 -11
- package/content/skills/mugiwara-planning/SKILL.md +6 -0
- package/content/skills/mugiwara-pr/SKILL.md +7 -0
- package/content/skills/mugiwara-quality/SKILL.md +7 -0
- package/content/skills/mugiwara-ship/SKILL.md +10 -8
- package/content/skills/mugiwara-testcases/SKILL.md +7 -0
- package/content/skills/mugiwara-workflow/SKILL.md +2 -2
- package/content/skills/using-mugiwara/SKILL.md +7 -1
- package/dist/mugiwara.js +190 -10
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.ts +0 -0
- package/hooks/session-start.ts +0 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/evidence.sh +16 -1
- package/scripts/gate-selftest.ts +52 -1
- package/scripts/initiative.ts +34 -20
- package/scripts/lane.sh +4 -2
- package/scripts/mission-report.sh +152 -29
- package/scripts/savepoint.sh +57 -13
- package/scripts/validate-content.ts +20 -0
- package/src/cli.ts +20 -3
- package/src/installer.ts +37 -1
- package/src/mission.ts +111 -1
- package/src/targets/claude.ts +27 -8
package/src/cli.ts
CHANGED
|
@@ -7,15 +7,15 @@ 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 } from './installer.ts';
|
|
10
|
+
import { installTo, removeInstalled, VERSION, ensureProjectGitignore } from './installer.ts';
|
|
11
11
|
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
12
|
-
import { resetMission } from './mission.ts';
|
|
12
|
+
import { resetMission, archiveMission } from './mission.ts';
|
|
13
13
|
|
|
14
14
|
const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
15
15
|
const flag = (v: FlagValue): boolean => v === true;
|
|
16
16
|
|
|
17
17
|
export async function run(argv: string[]): Promise<void> {
|
|
18
|
-
const { command, flags } = parseArgs(argv);
|
|
18
|
+
const { command, flags, _ } = parseArgs(argv);
|
|
19
19
|
if (flag(flags.help) || command === 'help') return help();
|
|
20
20
|
if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
|
|
21
21
|
switch (command) {
|
|
@@ -24,6 +24,7 @@ export async function run(argv: string[]): Promise<void> {
|
|
|
24
24
|
case 'uninstall': return uninstall(flags);
|
|
25
25
|
case 'list': return list(flags);
|
|
26
26
|
case 'reset': return resetCmd(flags);
|
|
27
|
+
case 'archive': return archive(flags, _);
|
|
27
28
|
default: throw new Error(`Unknown command: ${command}`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -41,6 +42,17 @@ function resetCmd(flags: Args['flags']): void {
|
|
|
41
42
|
if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function archive(flags: Args['flags'], positionals: string[]): void {
|
|
46
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
47
|
+
const mission = positionals[1];
|
|
48
|
+
if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
|
|
49
|
+
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
50
|
+
if (result.report) console.log(`archive target: ${result.report}`);
|
|
51
|
+
if (result.removed.length) console.log(`${flag(flags.dryRun) ? 'would remove' : 'removed'}: ${result.removed.join(', ')}`);
|
|
52
|
+
if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
|
|
53
|
+
if (result.index) console.log(`index updated: ${result.index}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
|
|
45
57
|
const interactive = !flag(flags.yes);
|
|
46
58
|
if (interactive && !process.stdin.isTTY) {
|
|
@@ -95,6 +107,10 @@ async function install(flags: Args['flags']): Promise<void> {
|
|
|
95
107
|
allFiles.push(...r.written);
|
|
96
108
|
allNotes.push(...r.notes);
|
|
97
109
|
}
|
|
110
|
+
if (scope === 'project') {
|
|
111
|
+
const gi = ensureProjectGitignore(projectDir, { dryRun: flag(flags.dryRun) });
|
|
112
|
+
allNotes.push(...gi.notes);
|
|
113
|
+
}
|
|
98
114
|
if (flag(flags.dryRun)) { console.log('\nDry run — nothing written.'); return; }
|
|
99
115
|
const file = manifestPath({ scope, projectDir, home });
|
|
100
116
|
const prev = readManifest(file);
|
|
@@ -182,6 +198,7 @@ Usage:
|
|
|
182
198
|
mugiwara list show installations
|
|
183
199
|
mugiwara list --check health check: show installations + missing files
|
|
184
200
|
mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
|
|
201
|
+
mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
|
|
185
202
|
mugiwara --help this help
|
|
186
203
|
mugiwara --version print version
|
|
187
204
|
|
package/src/installer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/installer.ts
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, rmSync, lstatSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -171,3 +171,39 @@ export function removeInstalled(manifest: { files: string[] }, { dryRun = false
|
|
|
171
171
|
}
|
|
172
172
|
return removed;
|
|
173
173
|
}
|
|
174
|
+
|
|
175
|
+
function assertNotSymlink(file: string): void {
|
|
176
|
+
if (!existsSync(file)) return;
|
|
177
|
+
try {
|
|
178
|
+
if (lstatSync(file).isSymbolicLink()) throw new Error(`refusing to follow symlink: ${file}`);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
if ((e as { code?: string }).code === 'ENOENT') return;
|
|
181
|
+
throw e;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const GITIGNORE_MARKER = '# mugiwara';
|
|
186
|
+
const GITIGNORE_BLOCK = `# mugiwara — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
|
|
187
|
+
# Ignore session state and regenerated files.
|
|
188
|
+
.mugiwara/state.json
|
|
189
|
+
.mugiwara/state-*.json
|
|
190
|
+
.mugiwara/config
|
|
191
|
+
.mugiwara/continue.md
|
|
192
|
+
.mugiwara/refs/
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
export function ensureProjectGitignore(projectDir: string, opts: { dryRun?: boolean } = {}): { appended: boolean; notes: string[] } {
|
|
196
|
+
const { dryRun = false } = opts;
|
|
197
|
+
const path = join(projectDir, '.gitignore');
|
|
198
|
+
assertNotSymlink(path);
|
|
199
|
+
if (existsSync(path) && readFileSync(path, 'utf8').includes(GITIGNORE_MARKER)) {
|
|
200
|
+
return { appended: false, notes: [] };
|
|
201
|
+
}
|
|
202
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
203
|
+
const separator = existing.length && !existing.endsWith('\n') ? '\n' : '';
|
|
204
|
+
if (!dryRun) {
|
|
205
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
206
|
+
writeFileSync(path, existing + separator + GITIGNORE_BLOCK);
|
|
207
|
+
}
|
|
208
|
+
return { appended: true, notes: [`.gitignore ${dryRun ? 'would append' : 'appended'} mugiwara audit-trail block`] };
|
|
209
|
+
}
|
package/src/mission.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/mission.ts
|
|
2
2
|
// Mission-state helpers for the mugiwara CLI (installer + reset only).
|
|
3
|
-
import { existsSync, rmSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, rmSync, readFileSync, readdirSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
function activeActor(projectDir: string): string | null {
|
|
@@ -48,3 +48,113 @@ export function resetMission(projectDir: string, keepLogs: boolean, force?: bool
|
|
|
48
48
|
}
|
|
49
49
|
return { removed, kept };
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export function archiveMission(projectDir: string, mission: string, opts: { dryRun?: boolean } = {}): { report: string | null; removed: string[]; kept: string[]; index?: string } {
|
|
53
|
+
const { dryRun = false } = opts;
|
|
54
|
+
const root = join(projectDir, '.mugiwara');
|
|
55
|
+
// mission allowlist — same as savepoint.sh / mission-report.sh. Dot-only
|
|
56
|
+
// names (".", "..") would resolve upward through join(...,"..") and let
|
|
57
|
+
// rmSync reach state.json/config outside the mission dir.
|
|
58
|
+
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission)) throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
59
|
+
const removed: string[] = [];
|
|
60
|
+
const kept: string[] = [];
|
|
61
|
+
|
|
62
|
+
// A file belongs to this mission when stripping the optional YYYY-MM-DD-
|
|
63
|
+
// prefix leaves `<mission>.md` or `<mission>-<suffix>.md`. Covers both the
|
|
64
|
+
// bare names and the date-prefixed names the prose writes (audit-trail.md).
|
|
65
|
+
const belongs = (f: string): boolean => {
|
|
66
|
+
const base = f.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
67
|
+
return base === `${mission}.md` || base.startsWith(`${mission}-`);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// locate the report (the archive target that must survive). Reports are
|
|
71
|
+
// date-prefixed (`reports/YYYY-MM-DD-<mission>.md`); compare the stripped
|
|
72
|
+
// mission name so `bar-foo.md` is not mistaken for mission `foo`.
|
|
73
|
+
let report: string | null = null;
|
|
74
|
+
const reportsDir = join(root, 'reports');
|
|
75
|
+
if (existsSync(reportsDir)) {
|
|
76
|
+
const f = readdirSync(reportsDir).find(n => {
|
|
77
|
+
const m = n.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
|
|
78
|
+
return !!m && m[2] === mission;
|
|
79
|
+
});
|
|
80
|
+
if (f) report = join('reports', f);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// remove per-mission wave intermediates from results/<mission>/, EXCEPT
|
|
84
|
+
// 06-closure.md and 07-pr-verdict.md (PR material + closure stay)
|
|
85
|
+
const resultsDir = join(root, 'results', mission);
|
|
86
|
+
if (existsSync(resultsDir)) {
|
|
87
|
+
for (const f of readdirSync(resultsDir)) {
|
|
88
|
+
if (f === '06-closure.md' || f === '07-pr-verdict.md') { kept.push(join('results', mission, f)); continue; }
|
|
89
|
+
const p = join(resultsDir, f);
|
|
90
|
+
if (!dryRun) rmSync(p, { recursive: true, force: true });
|
|
91
|
+
removed.push(join('results', mission, f));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// spec, review, issues, per-mission decision log — bare + date-prefixed
|
|
96
|
+
const specDir = join(root, 'spec');
|
|
97
|
+
if (existsSync(specDir)) {
|
|
98
|
+
for (const f of readdirSync(specDir)) {
|
|
99
|
+
if (!belongs(f)) continue;
|
|
100
|
+
const p = join(specDir, f);
|
|
101
|
+
if (!dryRun) rmSync(p);
|
|
102
|
+
removed.push(join('spec', f));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const dir of ['review', 'issues']) {
|
|
107
|
+
const d = join(root, dir);
|
|
108
|
+
if (!existsSync(d)) continue;
|
|
109
|
+
for (const f of readdirSync(d)) {
|
|
110
|
+
if (!belongs(f)) continue;
|
|
111
|
+
const p = join(d, f);
|
|
112
|
+
if (!dryRun) rmSync(p, { force: true });
|
|
113
|
+
removed.push(join(dir, f));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const logsDir = join(root, 'logs');
|
|
118
|
+
if (existsSync(logsDir)) {
|
|
119
|
+
for (const f of readdirSync(logsDir)) {
|
|
120
|
+
if (!belongs(f)) continue;
|
|
121
|
+
const p = join(logsDir, f);
|
|
122
|
+
if (!dryRun) rmSync(p);
|
|
123
|
+
removed.push(join('logs', f));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// continue.md is a session handoff — only remove it if it belongs to THIS
|
|
128
|
+
// mission (its content references the mission name); otherwise leave it.
|
|
129
|
+
const cont = join(root, 'continue.md');
|
|
130
|
+
if (existsSync(cont)) {
|
|
131
|
+
try {
|
|
132
|
+
if (readFileSync(cont, 'utf8').includes(mission)) {
|
|
133
|
+
if (!dryRun) rmSync(cont);
|
|
134
|
+
removed.push('continue.md');
|
|
135
|
+
}
|
|
136
|
+
} catch { /* unreadable — leave it */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// kept: report + the audit-trail survivors
|
|
140
|
+
if (report) kept.push(report);
|
|
141
|
+
for (const k of ['plans', 'config', 'state.json', join('logs', 'lessons.md')]) {
|
|
142
|
+
if (existsSync(join(root, k))) kept.push(k);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// summary index: append one line per archived mission (retention aid),
|
|
146
|
+
// idempotently — never duplicate a line for an already-indexed mission.
|
|
147
|
+
let index: string | undefined;
|
|
148
|
+
const indexFile = join(root, 'reports', 'index.md');
|
|
149
|
+
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}${report ? ` → ${report}` : ''}\n`;
|
|
150
|
+
if (!dryRun) {
|
|
151
|
+
mkdirSync(join(root, 'reports'), { recursive: true });
|
|
152
|
+
const existing = existsSync(indexFile) ? readFileSync(indexFile, 'utf8') : '';
|
|
153
|
+
if (!existing.split(/\r?\n/).some(l => l.startsWith(`- ${mission} —`))) {
|
|
154
|
+
const header = existing ? '' : '# Mission index\n\n';
|
|
155
|
+
appendFileSync(indexFile, header + line);
|
|
156
|
+
}
|
|
157
|
+
index = join('reports', 'index.md');
|
|
158
|
+
}
|
|
159
|
+
return { report, removed, kept, index };
|
|
160
|
+
}
|
package/src/targets/claude.ts
CHANGED
|
@@ -6,9 +6,17 @@ import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
|
|
|
6
6
|
import type { Target } from '../installer.ts';
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const
|
|
9
|
+
const HOOKS_SRC = join(here, '..', '..', 'hooks');
|
|
10
10
|
const COMMANDS_SRC = join(here, '..', '..', '.claude', 'commands');
|
|
11
11
|
|
|
12
|
+
// Claude Code has no path-scoped permission. write-scope maps to a partial
|
|
13
|
+
// `tools:` list: artifacts agents lose Edit (cannot modify existing source)
|
|
14
|
+
// but keep Write (must create .mugiwara/**); source agents get the default set.
|
|
15
|
+
function toolsFromScope(scope?: string): string | undefined {
|
|
16
|
+
if (scope === 'artifacts') return 'Read, Grep, Glob, Write, Bash, WebFetch, WebSearch';
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export const target: Target = {
|
|
13
21
|
id: 'claude',
|
|
14
22
|
label: 'Claude Code',
|
|
@@ -27,6 +35,10 @@ export const target: Target = {
|
|
|
27
35
|
transformAgent(data: FrontmatterData, body: string) {
|
|
28
36
|
const fm: FrontmatterData = { name: data.name, description: data.description };
|
|
29
37
|
if (data.tools) fm.tools = data.tools;
|
|
38
|
+
else {
|
|
39
|
+
const generated = toolsFromScope(data['write-scope']);
|
|
40
|
+
if (generated) fm.tools = generated;
|
|
41
|
+
}
|
|
30
42
|
return { relPath: `${data.name}.md`, text: stringifyFrontmatter(fm, body) };
|
|
31
43
|
},
|
|
32
44
|
refsDir({ scope, projectDir, home }, skillName: string) {
|
|
@@ -34,17 +46,24 @@ export const target: Target = {
|
|
|
34
46
|
return join(root, 'skills', skillName, 'references');
|
|
35
47
|
},
|
|
36
48
|
postInstall({ scope, projectDir, home, dryRun }) {
|
|
37
|
-
// Wire
|
|
49
|
+
// Wire hook scripts (SessionStart + UserPromptSubmit) into the installed .claude dir.
|
|
38
50
|
const root = scope === 'global' ? join(home, '.claude') : join(projectDir, '.claude');
|
|
39
|
-
const hookFile = join(root, 'hooks', 'session-start.ts');
|
|
40
51
|
const written: string[] = [];
|
|
41
52
|
const notes: string[] = [];
|
|
42
53
|
if (dryRun) return { written: [], notes: [] };
|
|
43
|
-
if (existsSync(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
if (existsSync(HOOKS_SRC)) {
|
|
55
|
+
for (const f of readdirSync(HOOKS_SRC)) {
|
|
56
|
+
if (!f.endsWith('.ts')) continue;
|
|
57
|
+
const dst = join(root, 'hooks', f);
|
|
58
|
+
if (!existsSync(dst)) {
|
|
59
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
60
|
+
copyFileSync(join(HOOKS_SRC, f), dst);
|
|
61
|
+
// /bin/sh executes hooks via shebang — a non-executable copy is a
|
|
62
|
+
// "Permission denied" at first user prompt. chmod every hook file.
|
|
63
|
+
chmodSync(dst, 0o755);
|
|
64
|
+
written.push(dst);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
48
67
|
}
|
|
49
68
|
// Port the /mugiwara commands into the installed .claude dir.
|
|
50
69
|
if (existsSync(COMMANDS_SRC)) {
|