@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.
Files changed (110) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -7
  3. package/.codex-plugin/plugin.json +2 -7
  4. package/.cursor-plugin/plugin.json +2 -7
  5. package/.kimi-plugin/plugin.json +2 -7
  6. package/.opencode/plugins/mugiwara.mjs +3 -18
  7. package/AGENTS.md +5 -4
  8. package/GEMINI.md +3 -3
  9. package/README.md +69 -63
  10. package/content/agents/brook-healing.md +8 -4
  11. package/content/agents/chopper-checkpoint.md +7 -3
  12. package/content/agents/eval-runner.md +4 -4
  13. package/content/agents/franky-gates.md +3 -3
  14. package/content/agents/jinbe-security.md +6 -7
  15. package/content/agents/luffy-orchestrator.md +11 -13
  16. package/content/agents/memory-keeper.md +4 -4
  17. package/content/agents/nami-planner.md +4 -4
  18. package/content/agents/resume-coordinator.md +7 -7
  19. package/content/agents/robin-reviewer.md +7 -3
  20. package/content/agents/sanji-quality.md +2 -2
  21. package/content/agents/skeptic-verifier.md +2 -2
  22. package/content/agents/usopp-brainstorm.md +3 -3
  23. package/content/agents/zoro-execution.md +6 -6
  24. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  25. package/content/skills/mugiwara-backend/references/database.md +61 -0
  26. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  27. package/content/skills/mugiwara-checkpoint/SKILL.md +6 -5
  28. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
  29. package/content/skills/mugiwara-execution/SKILL.md +8 -8
  30. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  31. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  32. package/content/skills/mugiwara-frontend/SKILL.md +3 -2
  33. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  34. package/content/skills/mugiwara-gates/SKILL.md +8 -5
  35. package/content/skills/mugiwara-healing/SKILL.md +2 -2
  36. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  37. package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
  38. package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
  39. package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
  40. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
  41. package/content/skills/mugiwara-planning/SKILL.md +3 -18
  42. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  43. package/content/skills/mugiwara-quality/SKILL.md +5 -2
  44. package/content/skills/mugiwara-resume/SKILL.md +2 -2
  45. package/content/skills/mugiwara-review/SKILL.md +3 -2
  46. package/content/skills/mugiwara-security/SKILL.md +1 -1
  47. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  48. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  49. package/content/skills/mugiwara-workflow/SKILL.md +9 -12
  50. package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
  51. package/dist/mugiwara.js +904 -462
  52. package/gemini-extension.json +1 -1
  53. package/hooks/auto-savepoint.js +18 -10
  54. package/hooks/auto-savepoint.ts +23 -23
  55. package/hooks/engagement-marker.js +1 -1
  56. package/hooks/engagement-marker.ts +1 -1
  57. package/hooks/pipeline-guard.js +16 -12
  58. package/hooks/pipeline-guard.ts +22 -19
  59. package/hooks/session-start.js +13 -10
  60. package/hooks/session-start.ts +24 -19
  61. package/package.json +2 -2
  62. package/plugin.json +1 -1
  63. package/references/complexity.md +27 -4
  64. package/references/definition-of-done.md +1 -1
  65. package/references/multi-actor.md +0 -3
  66. package/references/prose-style.md +54 -0
  67. package/scripts/check-doc-links.ts +40 -0
  68. package/scripts/conformance.ts +6 -16
  69. package/scripts/coverage-gate.ts +20 -6
  70. package/scripts/gate-selftest.ts +118 -7
  71. package/scripts/lane.sh +54 -8
  72. package/scripts/lib/patterns.sh +5 -0
  73. package/scripts/policy-force.ts +22 -0
  74. package/scripts/retrieval-eval.ts +1 -1
  75. package/scripts/savepoint.sh +136 -58
  76. package/scripts/validate-content.ts +22 -9
  77. package/scripts/verify-install.ts +9 -7
  78. package/src/args.ts +2 -2
  79. package/src/budget.ts +56 -0
  80. package/src/cli.ts +161 -110
  81. package/src/continue.ts +29 -10
  82. package/src/installer.ts +25 -20
  83. package/src/integrity.ts +158 -0
  84. package/src/mission.ts +228 -95
  85. package/src/policy.ts +156 -0
  86. package/src/provenance.ts +116 -0
  87. package/src/rollback.ts +95 -0
  88. package/src/routing.ts +69 -0
  89. package/src/run.ts +2 -2
  90. package/src/sign.ts +61 -0
  91. package/src/targets/claude.ts +2 -2
  92. package/.opencode/commands/mugiwara-execute.md +0 -13
  93. package/.opencode/commands/mugiwara-heal.md +0 -13
  94. package/.opencode/commands/mugiwara-onboard.md +0 -14
  95. package/.opencode/commands/mugiwara-plan.md +0 -17
  96. package/.opencode/commands/mugiwara-ship.md +0 -13
  97. package/.opencode/commands/using-mugiwara.md +0 -20
  98. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  99. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  100. package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
  101. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  102. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  103. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  104. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  105. package/content/skills/using-mugiwara/SKILL.md +0 -51
  106. package/references/token-budget.md +0 -56
  107. package/scripts/evidence.sh +0 -81
  108. package/scripts/initiative.ts +0 -296
  109. package/scripts/mission-report.sh +0 -293
  110. package/src/onboard.ts +0 -207
package/src/policy.ts ADDED
@@ -0,0 +1,156 @@
1
+ // src/policy.ts
2
+ // mugiwara.policy.yml — org rules that override crew judgement.
3
+ //
4
+ // One optional file at the repo root; absent means today's
5
+ // behavior everywhere. Policy only ever pushes UP (more scrutiny), never down:
6
+ // lanes forced to full, coverage thresholds raised, paths flagged for human
7
+ // approval.
8
+ //
9
+ // The parser is a deliberate YAML subset — nested maps, string arrays,
10
+ // scalars — enough for the documented schema and nothing more. No dependency:
11
+ // a governance file that needs an npm install to read would not be read.
12
+
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ export type MugiwaraPolicy = {
17
+ lanes?: { force_full?: string[] };
18
+ gates?: {
19
+ coverage?: { new?: number; modified?: number };
20
+ require_human_approval?: string[];
21
+ };
22
+ evidence?: { required?: string[] };
23
+ };
24
+
25
+ const POLICY_FILES = ['mugiwara.policy.yml', 'mugiwara.policy.yaml'];
26
+ const KNOWN_ROOTS = ['lanes', 'gates', 'evidence'];
27
+
28
+ /**
29
+ * Minimal YAML subset: maps, `- item` string lists, scalars.
30
+ *
31
+ * A `key:` with no value stays pending on its scope until a deeper line turns
32
+ * it into a map (first mapping child) or a list (first `- ` child). Scopes are
33
+ * keyed by the indent of their own key line; any line at indent <= a scope's
34
+ * key indent leaves that scope. Enough for the documented schema, nothing more.
35
+ */
36
+ export function parsePolicyYaml(text: string): Record<string, unknown> {
37
+ const root: Record<string, unknown> = {};
38
+ type Scope = { keyIndent: number; obj: Record<string, unknown>; pending?: { key: string; indent: number } };
39
+ const stack: Scope[] = [{ keyIndent: -1, obj: root }];
40
+ const lines = text.split(/\r?\n/);
41
+
42
+ const process = (i: number): void => {
43
+ if (i >= lines.length) return;
44
+ const noComment = lines[i].replace(/(^|\s)#.*$/, '');
45
+ if (!noComment.trim()) return process(i + 1);
46
+ const indent = noComment.length - noComment.trimStart().length;
47
+ const line = noComment.trim();
48
+
49
+ while (stack.length > 1 && indent <= stack[stack.length - 1].keyIndent) stack.pop();
50
+ const top = stack[stack.length - 1];
51
+
52
+ // A pending `parent:` whose first child this line is → become a map scope,
53
+ // then dispatch this same line into the new scope. A `- ` child instead
54
+ // turns the pending key into a LIST below — it must not materialize a map.
55
+ if (top.pending && !line.startsWith('- ') && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
56
+ const { key, indent: pIndent } = top.pending;
57
+ delete top.pending;
58
+ const created: Record<string, unknown> = {};
59
+ top.obj[key] = created;
60
+ stack.push({ keyIndent: pIndent, obj: created });
61
+ return process(i);
62
+ }
63
+
64
+ if (line.startsWith('- ')) {
65
+ if (top.pending) {
66
+ const existing = top.obj[top.pending.key];
67
+ const arr: unknown[] = Array.isArray(existing) ? existing : [];
68
+ arr.push(scalar(line.slice(2)));
69
+ top.obj[top.pending.key] = arr;
70
+ }
71
+ // stray items outside a pending key are ignored in this subset
72
+ return process(i + 1);
73
+ }
74
+
75
+ const colon = line.indexOf(':');
76
+ if (colon === -1) return process(i + 1); // not a mapping line in this subset
77
+ const key = line.slice(0, colon).trim();
78
+ const rest = line.slice(colon + 1).trim();
79
+ if (rest === '') {
80
+ top.pending = { key, indent };
81
+ } else {
82
+ top.obj[key] = scalar(rest);
83
+ delete top.pending;
84
+ }
85
+ process(i + 1);
86
+ };
87
+
88
+ process(0);
89
+ return root;
90
+ }
91
+
92
+ function scalar(v: string): unknown {
93
+ const t = v.trim().replace(/^["']|["']$/g, '');
94
+ if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
95
+ if (t === 'true') return true;
96
+ if (t === 'false') return false;
97
+ return t;
98
+ }
99
+
100
+ export function loadPolicy(projectDir: string): MugiwaraPolicy | null {
101
+ for (const name of POLICY_FILES) {
102
+ const file = join(projectDir, name);
103
+ if (!existsSync(file)) continue;
104
+ return normalize(parsePolicyYaml(readFileSync(file, 'utf8')));
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function normalize(raw: Record<string, unknown>): MugiwaraPolicy {
110
+ // A typo'd root key would otherwise silently disable the rule it carried.
111
+ for (const k of Object.keys(raw)) {
112
+ if (!KNOWN_ROOTS.includes(k)) throw new Error(`unknown policy key "${k}" (known: ${KNOWN_ROOTS.join(', ')})`);
113
+ }
114
+ const out: MugiwaraPolicy = {};
115
+ const lanes = raw.lanes as Record<string, unknown> | undefined;
116
+ if (lanes && Array.isArray(lanes.force_full)) out.lanes = { force_full: strings(lanes.force_full) };
117
+ const gates = raw.gates as Record<string, unknown> | undefined;
118
+ if (gates) {
119
+ out.gates = {};
120
+ const cov = gates.coverage as Record<string, unknown> | undefined;
121
+ if (cov) {
122
+ out.gates.coverage = {};
123
+ if (typeof cov.new === 'number') out.gates.coverage.new = cov.new;
124
+ if (typeof cov.modified === 'number') out.gates.coverage.modified = cov.modified;
125
+ }
126
+ if (Array.isArray(gates.require_human_approval))
127
+ out.gates.require_human_approval = strings(gates.require_human_approval);
128
+ }
129
+ const evidence = raw.evidence as Record<string, unknown> | undefined;
130
+ if (evidence && Array.isArray(evidence.required)) out.evidence = { required: strings(evidence.required) };
131
+ return out;
132
+ }
133
+
134
+ function strings(a: unknown[]): string[] {
135
+ return a.filter((x): x is string => typeof x === 'string' && x.length > 0);
136
+ }
137
+
138
+ /** Glob → RegExp: `**` crosses separators, `*` stays within one. */
139
+ export function globToRegExp(glob: string): RegExp {
140
+ const esc = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
141
+ const src = esc.replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\u0000/g, '.*');
142
+ return new RegExp(`^${src}$`);
143
+ }
144
+
145
+ /** Any changed path matching any policy glob? Returns the matching globs. */
146
+ export function matchedGlobs(paths: string[], globs: string[]): string[] {
147
+ return globs.filter((g) => paths.some((p) => globToRegExp(g).test(p)));
148
+ }
149
+
150
+ /**
151
+ * Coverage thresholds: the max of the .mugiwara/config value (or its
152
+ * fallback) and any policy raise. Policy pushes up, never down.
153
+ */
154
+ export function effectiveThreshold(configured: number, policyValue: number | undefined): number {
155
+ return Math.max(configured, policyValue ?? 0);
156
+ }
@@ -0,0 +1,116 @@
1
+ // src/provenance.ts
2
+ // Provenance ledger: line-of-sight from a commit to the
3
+ // mission that produced it — which agent persona, which lane, verified by
4
+ // what evidence. Distributed in two layers:
5
+ // 1. a git note on refs/notes/mugiwara attached to the branch head
6
+ // (local precision archive; survives rebase via notes.rewriteRef)
7
+ // 2. provenance.md in the mission dir, ready to paste as a PR comment
8
+ // (the layer every hosting UI can show)
9
+ import { execFileSync } from 'node:child_process';
10
+ import { writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ const NOTES_REF = 'refs/notes/mugiwara';
14
+
15
+ /** Structural subset any mission state satisfies — keeps this import-free. */
16
+ export type NoteSource = {
17
+ mission: string;
18
+ actor: string;
19
+ lane: string;
20
+ mode: string;
21
+ branch: string;
22
+ tasks_done: number;
23
+ tasks_total: number;
24
+ evidence: string[];
25
+ models?: string[];
26
+ };
27
+
28
+ function git(cwd: string, args: string[]): string {
29
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
30
+ }
31
+
32
+ export function modelLabel(): string {
33
+ return process.env.MUGIWARA_MODEL?.trim()
34
+ || process.env.ANTHROPIC_MODEL?.trim()
35
+ || 'model-unrecorded (set MUGIWARA_MODEL to attribute)';
36
+ }
37
+
38
+ /** The provenance block — identical wording in the note and the md file. */
39
+ export function buildNote(s: {
40
+ mission: string; actor: string; lane: string; mode: string; branch: string;
41
+ tasks_done: number; tasks_total: number; evidence: string[]; model?: string; models?: string[];
42
+ }): string {
43
+ const gates = s.evidence.length ? s.evidence.join(' · ') : 'no evidence recorded';
44
+ // Per-stage attribution (A4): when flow history recorded models, render the
45
+ // unique set — a mid-mission switch must not collapse to the last env value.
46
+ // With nothing recorded, keep the env-fallback label wording.
47
+ const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
48
+ const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(', ')}` : (s.model ?? modelLabel());
49
+ return [
50
+ `mission: ${s.mission}`,
51
+ `agent: ${s.actor || 'unknown'} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
52
+ `tasks: ${s.tasks_done}/${s.tasks_total}`,
53
+ `gates/evidence: ${gates}`,
54
+ `branch: ${s.branch}`,
55
+ 'human review: pending (PR review is the terminal gate)',
56
+ ].join('\n');
57
+ }
58
+
59
+ /** PR-paste-ready markdown wrapper around the same facts. */
60
+ export function renderProvenanceMd(note: string, sha: string | null): string {
61
+ const lines = [
62
+ '# Provenance',
63
+ '',
64
+ '<!-- paste below into the PR description or a PR comment -->',
65
+ '',
66
+ '```',
67
+ note,
68
+ '```',
69
+ '',
70
+ sha ? `Commit: ${sha}` : 'Commit: not recorded (no git head resolved at closure)',
71
+ '',
72
+ 'Query locally after pushing notes:',
73
+ '`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`.',
74
+ ];
75
+ return lines.join('\n') + '\n';
76
+ }
77
+
78
+ export function attachGitNote(projectDir: string, branch: string, note: string): { sha: string } | null {
79
+ try {
80
+ let sha: string;
81
+ try {
82
+ sha = git(projectDir, ['rev-parse', '--verify', branch]);
83
+ } catch {
84
+ sha = git(projectDir, ['rev-parse', 'HEAD']);
85
+ }
86
+ git(projectDir, ['notes', '--ref=mugiwara', 'add', '-f', '-m', note, sha]);
87
+ return { sha };
88
+ } catch {
89
+ // not a repo, detached oddities, or notes disabled — degrade honestly
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /** `mugiwara blame <path>` — last commit that touched the path + its note. */
95
+ export function blamePath(projectDir: string, path: string): string {
96
+ let sha: string;
97
+ try {
98
+ sha = git(projectDir, ['log', '-1', '--format=%H', '--', path]);
99
+ } catch {
100
+ return `blame: not a git repository (${projectDir})`;
101
+ }
102
+ if (!sha) return `blame: no commit touches "${path}"`;
103
+ try {
104
+ const note = git(projectDir, ['notes', '--ref=mugiwara', 'show', sha]);
105
+ return `${path} @ ${sha.slice(0, 7)}\n${note}`;
106
+ } catch {
107
+ return `${path} @ ${sha.slice(0, 7)}\n(no mugiwara provenance note on this commit)`;
108
+ }
109
+ }
110
+
111
+ /** Closure hook: write provenance.md + attach the git note. */
112
+ export function writeProvenance(projectDir: string, missionDir: string, state: NoteSource): void {
113
+ const note = buildNote(state);
114
+ const attached = attachGitNote(projectDir, state.branch, note);
115
+ writeFileSync(join(missionDir, 'provenance.md'), renderProvenanceMd(note, attached ? attached.sha : null));
116
+ }
@@ -0,0 +1,95 @@
1
+ // src/rollback.ts
2
+ // Executable rollback map: recovery you can run, not prose.
3
+ // Generated from git + state at closure; the human runs it, mugiwara never does.
4
+ import { execFileSync } from 'node:child_process';
5
+ import { writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ export type RollbackInput = {
9
+ mission: string;
10
+ branch: string;
11
+ baseSha: string;
12
+ };
13
+
14
+ function git(cwd: string, args: string[]): string {
15
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
16
+ }
17
+
18
+ /**
19
+ * Build the rollback script body. Pure given the inputs — the shas are read
20
+ * from the repo by the caller so this stays unit-testable without git.
21
+ */
22
+ export function buildRollback(input: RollbackInput, commitsNewestFirst: string[], filesTouched: string[]): string {
23
+ const lines = [
24
+ '#!/usr/bin/env bash',
25
+ `# Rollback map for mission "${input.mission}" — generated at closure.`,
26
+ '# Human-executed. Review before running; mugiwara never runs this.',
27
+ `# Branch: ${input.branch}`,
28
+ `# Base: ${input.baseSha}`,
29
+ '',
30
+ 'set -euo pipefail',
31
+ '',
32
+ ];
33
+ if (!commitsNewestFirst.length && !filesTouched.length) {
34
+ lines.push('# No commits between base and HEAD on this branch — nothing to revert.', '');
35
+ return lines.join('\n');
36
+ }
37
+ if (!commitsNewestFirst.length) {
38
+ // Squash-merge workflows: the branch's changes reached the base ref as one
39
+ // squashed commit, so `rev-list base..branch` can come back empty while
40
+ // `git diff base branch` is not. "Nothing to revert" would be a lie — the
41
+ // changes are live. Emit loud, human-executable guidance and exit 1 so a
42
+ // careless run fails instead of silently doing nothing.
43
+ lines.push(
44
+ '# UNRESOLVED: squash-merged state detected.',
45
+ `# git rev-list ${input.baseSha}..${input.branch} is empty, but the diff`,
46
+ `# ${input.baseSha}..${input.branch} touches ${filesTouched.length} file(s):`,
47
+ '# the mission\'s changes were collapsed into commit(s) already on the base',
48
+ '# ref. No per-commit revert list can be derived automatically.',
49
+ '# Locate the squash commit, review it, then revert it:',
50
+ `# git log --oneline ${input.baseSha}..HEAD --grep="${input.mission}"`,
51
+ '# git revert <squash-commit>',
52
+ '# Files carrying the squashed changes (verify each after reverting):',
53
+ ...filesTouched.map((f) => `# ${f}`),
54
+ '',
55
+ 'echo "ROLLBACK INCOMPLETE: squash-merged state — locate and revert the squash commit manually" >&2',
56
+ 'exit 1',
57
+ '',
58
+ );
59
+ return lines.join('\n');
60
+ }
61
+ lines.push(
62
+ '# Revert newest-first so earlier reverts never conflict with later ones.',
63
+ 'git revert --no-edit \\',
64
+ commitsNewestFirst.map((c) => ` ${c}`).join(' \\\n'),
65
+ '',
66
+ );
67
+ if (filesTouched.length) {
68
+ lines.push(
69
+ '# Files this mission touched (verify the working tree is clean afterwards):',
70
+ ...filesTouched.map((f) => `# ${f}`),
71
+ '',
72
+ );
73
+ }
74
+ return lines.join('\n');
75
+ }
76
+
77
+ /** Read the repo for everything the script needs, then write rollback.sh. */
78
+ export function generateRollback(projectDir: string, missionDir: string, input: RollbackInput): { file: string; commits: number } | null {
79
+ try {
80
+ const range = `${input.baseSha}..${input.branch}`;
81
+ const revList = git(projectDir, ['rev-list', '--reverse', range]).split(/\r?\n/).filter(Boolean);
82
+ const commitsNewestFirst = [...revList].reverse();
83
+ const filesTouched = input.baseSha === 'unknown'
84
+ ? []
85
+ : git(projectDir, ['diff', '--name-only', input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
86
+ const body = buildRollback(input, commitsNewestFirst, filesTouched);
87
+ const file = join(missionDir, 'rollback.sh');
88
+ writeFileSync(file, body, { mode: 0o755 });
89
+ return { file: 'rollback.sh', commits: commitsNewestFirst.length };
90
+ } catch {
91
+ // No git, unknown base, or an empty branch — a rollback map cannot be
92
+ // derived. Absent file beats a wrong one.
93
+ return null;
94
+ }
95
+ }
package/src/routing.ts ADDED
@@ -0,0 +1,69 @@
1
+ // src/routing.ts
2
+ // Review routing: tell the reviewer where to look. A ranked
3
+ // reading order computed from what the mission touched — not a flat diff.
4
+ // Heuristic and labeled as such: the ranking decides reading ORDER, never
5
+ // whether a line is correct.
6
+
7
+ export type RankedFile = { path: string; score: number; reasons: string[] };
8
+
9
+ const DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
10
+ const TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
11
+
12
+ export type RoutingSource = { evidence: string[]; sensitive_paths?: string[] };
13
+
14
+ /**
15
+ * Score one path. Sensitive paths dominate; production code next; tests and
16
+ * docs sink. Files the evidence trail never mentions get a bump — absence of
17
+ * evidence is exactly where review attention pays.
18
+ */
19
+ export function scorePath(path: string, sensitivePaths: string[], evidenceJoined: string): { score: number; reasons: string[] } {
20
+ const reasons: string[] = [];
21
+ let score = 0;
22
+ const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, '/')) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*')).test(path)));
23
+ if (sensitiveHit) {
24
+ score += 100;
25
+ reasons.push('sensitive path');
26
+ }
27
+ if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
28
+ score += 50;
29
+ reasons.push('production code');
30
+ } else if (TEST_PAT.test(path)) {
31
+ score += 10;
32
+ reasons.push('test scaffolding — skim unless behavior changed');
33
+ } else {
34
+ score += 5;
35
+ reasons.push('docs/config');
36
+ }
37
+ if (!evidenceJoined.includes(path)) {
38
+ score += 20;
39
+ reasons.push('not covered by recorded evidence');
40
+ }
41
+ return { score, reasons };
42
+ }
43
+
44
+ export function rankFiles(paths: string[], state: RoutingSource): RankedFile[] {
45
+ const sensitive = state.sensitive_paths ?? [];
46
+ const ev = state.evidence.join(' ');
47
+ return paths
48
+ .map((path) => {
49
+ const { score, reasons } = scorePath(path, sensitive, ev);
50
+ return { path, score, reasons };
51
+ })
52
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
53
+ }
54
+
55
+ export function renderRouting(ranked: RankedFile[], mission: string): string {
56
+ if (!ranked.length) return '';
57
+ const lines = [
58
+ '',
59
+ '## Review routing',
60
+ '',
61
+ `Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
62
+ '',
63
+ ];
64
+ ranked.forEach((r, i) => {
65
+ lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join('; ')}`);
66
+ });
67
+ lines.push('');
68
+ return lines.join('\n');
69
+ }
package/src/run.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // Why this exists: the skills tell the crew to run `scripts/savepoint.sh`, but
6
6
  // the installer only ever copied `content/` and `references/` into a project.
7
7
  // That path resolved against the project's cwd, where the file does not exist,
8
- // so every savepoint / lane / evidence / mission-report call silently did
8
+ // so every savepoint / lane call silently did
9
9
  // nothing. Resolving from the package root fixes it for every install target at
10
10
  // once.
11
11
  import { existsSync, readdirSync } from 'node:fs';
@@ -18,7 +18,7 @@ const here = dirname(fileURLToPath(import.meta.url));
18
18
  export const SCRIPTS_DIR = join(here, '..', 'scripts');
19
19
 
20
20
  /** Scripts a project is meant to call. Anything else stays internal tooling. */
21
- export const RUNNABLE = ['savepoint.sh', 'lane.sh', 'evidence.sh', 'mission-report.sh'] as const;
21
+ export const RUNNABLE = ['savepoint.sh', 'lane.sh'] as const;
22
22
 
23
23
  /**
24
24
  * Locate a POSIX shell. Windows has none natively, but Git for Windows ships
package/src/sign.ts ADDED
@@ -0,0 +1,61 @@
1
+ // src/sign.ts
2
+ // Signed attestation: evidence that cannot be fabricated
3
+ // after the fact — optional, user-keyed, never a hard dependency.
4
+ //
5
+ // Uses minisign when it is installed and the user supplies keys. No minisign,
6
+ // no keys → today's behavior, stated plainly in the output. Detached
7
+ // signature lives beside the report (`report.md.minisig`).
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ export function signArgs(reportPath: string, secretKey: string): string[] {
14
+ return ['-Sm', reportPath, '-s', secretKey];
15
+ }
16
+
17
+ export function verifyArgs(reportPath: string, pubKey: string | null): string[] {
18
+ return pubKey ? ['-Vm', reportPath, '-p', pubKey] : ['-Vm', reportPath];
19
+ }
20
+
21
+ export function hasMinisign(): boolean {
22
+ try {
23
+ execFileSync('minisign', ['-v'], { stdio: ['ignore', 'pipe', 'ignore'] });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function defaultKey(flag: 'secret' | 'public'): string {
31
+ return join(homedir(), '.mugiwara', flag === 'secret' ? 'minisign.key' : 'minisign.pub');
32
+ }
33
+
34
+ export function signReport(projectDir: string, missionDir: string): { ok: boolean; message: string } {
35
+ const report = join(missionDir, 'report.md');
36
+ if (!existsSync(report)) return { ok: false, message: 'no report.md to sign — archive first' };
37
+ if (!hasMinisign()) {
38
+ return { ok: false, message: 'minisign not installed — signing skipped (install minisign to enable attestation)' };
39
+ }
40
+ const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey('secret');
41
+ try {
42
+ execFileSync('minisign', signArgs(report, secretKey), { cwd: projectDir, stdio: 'pipe', input: process.env.MUGIWARA_SIGN_PASSWORD ?? '' });
43
+ return { ok: true, message: `signed ${report}.minisig (key: ${secretKey})` };
44
+ } catch (e) {
45
+ return { ok: false, message: `signing failed: ${(e as Error).message}` };
46
+ }
47
+ }
48
+
49
+ export function verifyReport(projectDir: string, missionDir: string): { ok: boolean; message: string } {
50
+ const report = join(missionDir, 'report.md');
51
+ const sig = `${report}.minisig`;
52
+ if (!existsSync(sig)) return { ok: false, message: 'not signed (no .minisig beside report.md)' };
53
+ if (!hasMinisign()) return { ok: false, message: 'minisign not installed — cannot verify' };
54
+ const pubKey = existsSync(defaultKey('public')) ? defaultKey('public') : null;
55
+ try {
56
+ execFileSync('minisign', verifyArgs(report, pubKey), { cwd: projectDir, stdio: 'pipe' });
57
+ return { ok: true, message: 'signature verifies against report.md' };
58
+ } catch {
59
+ return { ok: false, message: 'SIGNATURE INVALID — report.md changed after signing' };
60
+ }
61
+ }
@@ -40,8 +40,8 @@ function copyScripts(dstDir: string): string[] {
40
40
  if (!existsSync(src)) return;
41
41
  for (const f of readdirSync(src)) {
42
42
  // install.sh is the npx bootstrap, not a harness script — never shipped
43
- // into a project. initiative.ts ships too: `mugiwara initiative` runs it.
44
- if ((!f.endsWith('.sh') && f !== 'initiative.ts') || f === 'install.sh') continue;
43
+ // into a project.
44
+ if (!f.endsWith('.sh') || f === 'install.sh') continue;
45
45
  const to = join(dst, f);
46
46
  mkdirSync(dst, { recursive: true });
47
47
  copyFileSync(join(src, f), to);
@@ -1,13 +0,0 @@
1
- ---
2
- description: Execute an approved mugiwara plan as Zoro (execution stage)
3
- ---
4
- Execute the approved plan as Zoro, inline in the main conversation:
5
-
6
- 1. **Entry protocol first** — read the mission state (`.mugiwara/state/<mission>/[member].json`). No active mission → run Flow 0 triage before anything else. Foreign mission → stop, report owner. Base drift → stop, ask. Non-git → degrade to standard, say so once.
7
- 2. Load the skill: `mugiwara-execution`.
8
- 3. Read the existing plan from `.mugiwara/plans/` — that file is the bridge, never re-plan.
9
- 4. Open a todo list, run tasks sequentially inline; dispatch [PARALLEL] batches to worker subagents only.
10
- 5. Commit per logical task and verify every acceptance criterion with evidence in `.mugiwara/results/`.
11
- 6. **Return the result to Luffy — do not choose the next wave.**
12
-
13
- See skills/mugiwara-execution for the wave-structured protocol.
@@ -1,13 +0,0 @@
1
- ---
2
- description: Heal earlier-wave failures as Brook (healing stage, after review/security findings)
3
- ---
4
- Heal failures as Brook, inline in the main conversation:
5
-
6
- 1. **Entry protocol first** — read the mission state (`.mugiwara/state/<mission>/[member].json`). No active mission → run Flow 0 triage before anything else. Foreign mission → stop, report owner. Base drift → stop, ask. Non-git → degrade to standard, say so once. Read `heal_halt` (savepoint computes it as `heal_cycle ≥ heal_max_cycles`, config default 3) — when `true`, STOP and escalate to the user.
7
- 2. Load the skill: `mugiwara-healing`.
8
- 3. Read the `.mugiwara/issues` ledger first — stop-the-line triage per failure, never fix blind.
9
- 4. Prove-it before fixing: reproduce or verify the failure from `.mugiwara/results/` evidence.
10
- 5. Apply minimal root-cause fixes, update the ledger with evidence.
11
- 6. **Return the result to Luffy — do not choose the next wave.**
12
-
13
- See skills/mugiwara-healing for the triage protocol.
@@ -1,14 +0,0 @@
1
- ---
2
- description: Run the Mugiwara onboarding wizard — zero-LLM terminal wizard via `mugiwara onboard`, writes .mugiwara/config.
3
- ---
4
- Mugiwara onboard: $ARGUMENTS
5
-
6
- Run the onboarding wizard. One path only — the zero-LLM terminal wizard:
7
-
8
- 1. Tell the user to run `mugiwara onboard` in their terminal (or `bunx @ionivetech/mugiwara onboard`).
9
- 2. Do NOT ask the questions yourself. Do NOT write .mugiwara/config. The wizard is a plain
10
- script — no LLM, no network, works on every platform.
11
- 3. After the user finishes, verify `.mugiwara/config` exists and summarize the values.
12
-
13
- The wizard never writes .mugiwara/onboard.json. All 14 crew agents are always active —
14
- no agent-selection step.
@@ -1,17 +0,0 @@
1
- ---
2
- description: Plan a mugiwara mission as Nami (planning stage)
3
- ---
4
- Plan the mission as Nami, inline in the main conversation:
5
-
6
- 1. **Entry protocol first** — read the mission state (`.mugiwara/state/<mission>/[member].json`). No active mission → run Flow 0 triage before anything else. Foreign mission → stop, report owner. Base drift → stop, ask. Non-git → degrade to standard, say so once.
7
- 2. Load the skill: `mugiwara-planning`.
8
- 3. Classify mission size, interview first, scan full context, write the scaled Quick/Standard/Full plan.
9
- 4. Embody the Nami crew role inline — never Task-dispatch.
10
- 5. Persist the plan to `.mugiwara/plans/`; later stages read it from there as the bridge.
11
- 6. **Return the plan to Luffy — do not choose the next wave.**
12
-
13
- Spec input: read `.mugiwara/spec/YYYY-MM-DD-<mission>.md`. If it is empty or
14
- missing, write the spec bridge from the user's request first (goal, acceptance
15
- criteria, constraints) before planning — never plan from an empty spec.
16
-
17
- See skills/mugiwara-planning for the full template and wave structure.
@@ -1,13 +0,0 @@
1
- ---
2
- description: Close the mission as Luffy (closure + ship stage, at mission end)
3
- ---
4
- Close the mission as Luffy, inline in the main conversation:
5
-
6
- 1. **Entry protocol first** — read the mission state (`.mugiwara/state/<mission>/[member].json`). No active mission → run Flow 0 triage before anything else. Foreign mission → stop, report owner. Base drift → stop, ask. Non-git → degrade to standard, say so once.
7
- 2. Load the skills: `mugiwara-orchestration` + `mugiwara-ship`.
8
- 3. Run the ship gate: pre-launch checklist, feature flags, staged rollout, mandatory rollback plan — binary GO/NO-GO with evidence.
9
- 4. Bridge on `.mugiwara/plans/` (promise) vs `.mugiwara/results/` (evidence) for the Definition-of-Done verdict.
10
- 5. Close the mission: lessons ledger update, then the terminal step — save-point commit, push the branch, write the PR verdict, hand to the user.
11
- 6. **The crew never creates a PR, never merges, never deploys — hand the branch + verdict to the user.**
12
-
13
- See skills/mugiwara-ship for the gate and skills/mugiwara-orchestration for closure.
@@ -1,20 +0,0 @@
1
- ---
2
- description: Mugiwara crew reference — how it works, crew overview, pipeline summary. Documentation only.
3
- ---
4
- Mugiwara Reference: $ARGUMENTS
5
-
6
- `mugiwara-orchestration` auto-loads as gatekeeper for every task — no need to call this command.
7
-
8
- ## How it works
9
-
10
- - 11 agents: Luffy (triage), Usopp (brainstorm), Nami (plan), Zoro (execute), Chopper (audit), Sanji (quality), Franky (gates), Robin (review), Jinbe (security), Brook (heal), Resume (+3 internal: Skeptic, Eval Runner, Memory)
11
- - 26 skills — one per crew role + domain skills (frontend, backend, git, security)
12
- - 9-wave pipeline runs inline in the main conversation
13
- - Evidence over claims at every wave
14
- - Autonomy modes: guided, semi, auto
15
-
16
- ## When to use this command
17
-
18
- Use `/using-mugiwara` when you want the crew overview or pipeline summary. For task routing and classification, `mugiwara-orchestration` auto-loads as gatekeeper — you do not need to call this first.
19
-
20
- See skills/mugiwara-workflow for the full pipeline.