@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.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/mugiwara-helpers.mjs +24 -0
  7. package/.opencode/plugins/mugiwara.mjs +10 -2
  8. package/AGENTS.md +1 -1
  9. package/README.md +115 -62
  10. package/content/skills/mugiwara-brainstorm/SKILL.md +7 -0
  11. package/content/skills/mugiwara-execution/SKILL.md +41 -41
  12. package/content/skills/mugiwara-execution/references/dispatch.md +41 -0
  13. package/content/skills/mugiwara-orchestration/SKILL.md +28 -24
  14. package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
  15. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +12 -11
  16. package/content/skills/mugiwara-planning/SKILL.md +6 -0
  17. package/content/skills/mugiwara-pr/SKILL.md +7 -0
  18. package/content/skills/mugiwara-quality/SKILL.md +7 -0
  19. package/content/skills/mugiwara-ship/SKILL.md +10 -8
  20. package/content/skills/mugiwara-testcases/SKILL.md +7 -0
  21. package/content/skills/mugiwara-workflow/SKILL.md +2 -2
  22. package/content/skills/using-mugiwara/SKILL.md +7 -1
  23. package/dist/mugiwara.js +190 -10
  24. package/gemini-extension.json +1 -1
  25. package/hooks/mugiwara-mode-tracker.ts +0 -0
  26. package/hooks/session-start.ts +0 -0
  27. package/package.json +1 -1
  28. package/plugin.json +1 -1
  29. package/scripts/evidence.sh +16 -1
  30. package/scripts/gate-selftest.ts +52 -1
  31. package/scripts/initiative.ts +34 -20
  32. package/scripts/lane.sh +4 -2
  33. package/scripts/mission-report.sh +152 -29
  34. package/scripts/savepoint.sh +57 -13
  35. package/scripts/validate-content.ts +20 -0
  36. package/src/cli.ts +20 -3
  37. package/src/installer.ts +37 -1
  38. package/src/mission.ts +111 -1
  39. 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
+ }
@@ -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 HOOK_SRC = join(here, '..', '..', 'hooks', 'session-start.ts');
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 the SessionStart hook (inline doctrine) into the installed .claude dir.
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(HOOK_SRC) && !existsSync(hookFile)) {
44
- mkdirSync(dirname(hookFile), { recursive: true });
45
- copyFileSync(HOOK_SRC, hookFile);
46
- chmodSync(hookFile, 0o755);
47
- written.push(hookFile);
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)) {