@ionivetech/mugiwara 0.6.4 → 0.6.6

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 (115) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.codex-plugin/plugin.json +48 -2
  4. package/.cursor-plugin/plugin.json +48 -2
  5. package/.kimi-plugin/plugin.json +48 -2
  6. package/.opencode/commands/mugiwara-continue.md +44 -31
  7. package/.opencode/commands/mugiwara-execute.md +1 -1
  8. package/.opencode/commands/mugiwara-heal.md +1 -1
  9. package/.opencode/commands/mugiwara-onboard.md +8 -9
  10. package/.opencode/commands/mugiwara-plan.md +1 -1
  11. package/.opencode/commands/mugiwara-review.md +1 -1
  12. package/.opencode/commands/mugiwara-security.md +1 -1
  13. package/.opencode/commands/mugiwara-ship.md +1 -1
  14. package/.opencode/commands/mugiwara.md +2 -2
  15. package/.opencode/mugiwara-helpers.mjs +3 -0
  16. package/.opencode/plugins/mugiwara.mjs +1 -1
  17. package/AGENTS.md +12 -2
  18. package/README.md +53 -32
  19. package/content/agents/brook-healing.md +5 -8
  20. package/content/agents/chopper-checkpoint.md +8 -11
  21. package/content/agents/eval-runner.md +2 -5
  22. package/content/agents/franky-gates.md +4 -7
  23. package/content/agents/jinbe-security.md +4 -7
  24. package/content/agents/luffy-orchestrator.md +13 -13
  25. package/content/agents/memory-keeper.md +5 -8
  26. package/content/agents/nami-planner.md +4 -7
  27. package/content/agents/resume-coordinator.md +7 -10
  28. package/content/agents/robin-reviewer.md +6 -9
  29. package/content/agents/sanji-quality.md +7 -10
  30. package/content/agents/skeptic-verifier.md +5 -8
  31. package/content/agents/usopp-brainstorm.md +3 -6
  32. package/content/agents/zoro-execution.md +7 -10
  33. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  34. package/content/skills/mugiwara-checkpoint/SKILL.md +14 -11
  35. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +4 -4
  36. package/content/skills/mugiwara-context-budget/SKILL.md +5 -3
  37. package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
  38. package/content/skills/mugiwara-execution/SKILL.md +27 -27
  39. package/content/skills/mugiwara-execution/references/dispatch.md +2 -2
  40. package/content/skills/mugiwara-execution/references/resume-batching.md +1 -1
  41. package/content/skills/mugiwara-frontend/SKILL.md +2 -4
  42. package/content/skills/mugiwara-gates/SKILL.md +9 -7
  43. package/content/skills/mugiwara-git/SKILL.md +7 -2
  44. package/content/skills/mugiwara-healing/SKILL.md +6 -4
  45. package/content/skills/mugiwara-healing/references/workers.md +2 -2
  46. package/content/skills/mugiwara-lessons/SKILL.md +1 -0
  47. package/content/skills/mugiwara-orchestration/SKILL.md +34 -35
  48. package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
  49. package/content/skills/mugiwara-orchestration/references/closure.md +5 -5
  50. package/content/skills/mugiwara-orchestration/references/output-contract.md +77 -0
  51. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +22 -22
  52. package/content/skills/mugiwara-planning/SKILL.md +3 -1
  53. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  54. package/content/skills/mugiwara-pr/SKILL.md +6 -6
  55. package/content/skills/mugiwara-pr/references/verdict-format.md +3 -3
  56. package/content/skills/mugiwara-quality/SKILL.md +8 -7
  57. package/content/skills/mugiwara-resume/SKILL.md +19 -20
  58. package/content/skills/mugiwara-review/SKILL.md +6 -1
  59. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
  60. package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
  61. package/content/skills/mugiwara-security/SKILL.md +5 -1
  62. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  63. package/content/skills/mugiwara-ship/references/cleanup.md +1 -1
  64. package/content/skills/mugiwara-testcases/SKILL.md +5 -3
  65. package/content/skills/mugiwara-workflow/SKILL.md +38 -39
  66. package/content/skills/mugiwara-workflow/references/workspace-layout.md +15 -15
  67. package/content/skills/using-mugiwara/SKILL.md +5 -5
  68. package/dist/mugiwara.js +807 -31
  69. package/gemini-extension.json +1 -1
  70. package/hooks/auto-savepoint.js +106 -0
  71. package/hooks/auto-savepoint.ts +102 -0
  72. package/hooks/engagement-marker.js +59 -0
  73. package/hooks/engagement-marker.ts +97 -0
  74. package/hooks/hooks.json +41 -2
  75. package/hooks/mugiwara-mode-tracker.js +79 -0
  76. package/hooks/pipeline-guard.js +182 -0
  77. package/hooks/pipeline-guard.ts +267 -0
  78. package/hooks/session-start.js +106 -0
  79. package/hooks/session-start.ts +44 -32
  80. package/package.json +7 -3
  81. package/plugin.json +1 -1
  82. package/references/agent-protocol.md +15 -0
  83. package/references/complexity.md +54 -0
  84. package/references/definition-of-done.md +1 -1
  85. package/references/multi-actor.md +5 -5
  86. package/references/skill-versioning.md +1 -1
  87. package/references/token-budget.md +30 -8
  88. package/references/wave-banners.md +21 -31
  89. package/scripts/build-hooks.ts +56 -0
  90. package/scripts/conformance.ts +232 -0
  91. package/scripts/coverage-gate.ts +179 -0
  92. package/scripts/evidence.sh +17 -4
  93. package/scripts/gate-selftest.ts +14 -0
  94. package/scripts/initiative.ts +51 -17
  95. package/scripts/install.ps1 +5 -4
  96. package/scripts/install.sh +4 -4
  97. package/scripts/lane.sh +6 -2
  98. package/scripts/lib/patterns.sh +84 -0
  99. package/scripts/mission-report.sh +12 -11
  100. package/scripts/savepoint.sh +152 -37
  101. package/scripts/validate-content.ts +22 -2
  102. package/scripts/verify-install.ts +154 -44
  103. package/src/args.ts +1 -1
  104. package/src/cli.ts +217 -0
  105. package/src/continue.ts +243 -0
  106. package/src/installer.ts +39 -0
  107. package/src/onboard.ts +207 -0
  108. package/src/run.ts +82 -0
  109. package/src/targets/claude.ts +140 -2
  110. package/src/targets/copilot.ts +20 -1
  111. package/src/targets/generic.ts +9 -4
  112. package/content/agents/onboarding-guide.md +0 -124
  113. package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
  114. package/scripts/onboard.ts +0 -266
  115. package/scripts/probe.ts +0 -40
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // hooks/auto-savepoint.ts
5
+ import { existsSync as existsSync2, readFileSync, readdirSync as readdirSync2 } from "fs";
6
+ import { spawnSync as spawnSync2 } from "child_process";
7
+
8
+ // src/run.ts
9
+ import { existsSync, readdirSync } from "node:fs";
10
+ import { spawnSync } from "node:child_process";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ var here = dirname(fileURLToPath(import.meta.url));
14
+ var SCRIPTS_DIR = join(here, "..", "scripts");
15
+ function findBash() {
16
+ const explicit = process.env.MUGIWARA_BASH?.trim();
17
+ if (explicit)
18
+ return existsSync(explicit) ? explicit : null;
19
+ const candidates = process.platform === "win32" ? [
20
+ "C:\\Program Files\\Git\\bin\\bash.exe",
21
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
22
+ join(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
23
+ ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
24
+ for (const p of candidates) {
25
+ if (p && existsSync(p))
26
+ return p;
27
+ }
28
+ const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
29
+ encoding: "utf8"
30
+ });
31
+ const first = probe.stdout?.split(/\r?\n/).find((l) => l.trim());
32
+ return first?.trim() || null;
33
+ }
34
+
35
+ // hooks/auto-savepoint.ts
36
+ import { dirname as dirname2, join as join2 } from "path";
37
+ import { fileURLToPath as fileURLToPath2 } from "url";
38
+ var root = join2(dirname2(fileURLToPath2(import.meta.url)), "..");
39
+ var cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
40
+ var SAFE = /^[A-Za-z0-9._-]+$/;
41
+ function bootstrapMission() {
42
+ for (const dir of ["plans", "spec", "logs"]) {
43
+ const d = join2(cwd, ".mugiwara", dir);
44
+ if (!existsSync2(d))
45
+ continue;
46
+ const files = readdirSync2(d).filter((f) => f.endsWith(".md") && f !== "lessons.md").sort().reverse();
47
+ for (const f of files) {
48
+ const name = f.replace(/\.md$/, "").replace(/^\d{4}-\d{2}-\d{2}-/, "");
49
+ if (name && SAFE.test(name) && !/^\.+$/.test(name)) {
50
+ return { mission: name, member: "", wave: "0", mode: readMode(), updated: 0 };
51
+ }
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ function readMode() {
57
+ const file = join2(cwd, ".mugiwara", "config");
58
+ if (!existsSync2(file))
59
+ return "guided";
60
+ for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
61
+ const [k, v] = line.split("=").map((s) => s.trim());
62
+ if (k === "mode" && ["guided", "semi", "auto"].includes(v))
63
+ return v;
64
+ }
65
+ return "guided";
66
+ }
67
+ function activeMission() {
68
+ const base = join2(cwd, ".mugiwara", "state");
69
+ if (!existsSync2(base))
70
+ return bootstrapMission();
71
+ let best = null;
72
+ for (const e of readdirSync2(base, { withFileTypes: true })) {
73
+ if (!e.isDirectory() || !SAFE.test(e.name) || /^\.+$/.test(e.name))
74
+ continue;
75
+ for (const f of readdirSync2(join2(base, e.name))) {
76
+ if (!f.endsWith(".json"))
77
+ continue;
78
+ const stem = f.slice(0, -5);
79
+ const member = stem === "state" ? "" : stem;
80
+ if (member && (!SAFE.test(member) || /^\.+$/.test(member)))
81
+ continue;
82
+ try {
83
+ const s = JSON.parse(readFileSync(join2(base, e.name, f), "utf8"));
84
+ const updated = Date.parse(s.updated_at ?? "") || 0;
85
+ if (best && updated <= best.updated)
86
+ continue;
87
+ const wave = String(s.flow ?? s.wave ?? "").replace(/\D/g, "") || "1";
88
+ const mode = ["guided", "semi", "auto"].includes(s.mode) ? s.mode : "guided";
89
+ best = { mission: e.name, member, wave, mode, updated };
90
+ } catch {}
91
+ }
92
+ }
93
+ return best ?? bootstrapMission();
94
+ }
95
+ try {
96
+ const active = activeMission();
97
+ const script = join2(root, "scripts", "savepoint.sh");
98
+ const bash = findBash();
99
+ if (active && bash && existsSync2(script)) {
100
+ spawnSync2(bash, [script, active.mission, active.member, active.wave, active.mode], {
101
+ cwd,
102
+ stdio: "ignore",
103
+ timeout: 15000
104
+ });
105
+ }
106
+ } catch {}
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bun
2
+ // hooks/auto-savepoint.ts — Stop hook: refresh the active mission's savepoint.
3
+ //
4
+ // State used to depend on the model remembering the prose instruction "run
5
+ // scripts/savepoint.sh". It forgot, so .mugiwara/state/ and .mugiwara/continue/
6
+ // stayed empty and everything reading them (resume, heal_cycle cap, lane-rise
7
+ // detection, the SessionStart auto-resume) was dead. This makes the write
8
+ // happen whether or not the crew remembers.
9
+ //
10
+ // Refreshes the CURRENT wave — it never advances one. Wave transitions stay an
11
+ // explicit crew action; this only keeps tasks/lane/blockers honest.
12
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
13
+ import { spawnSync } from 'node:child_process';
14
+ // Shared with `mugiwara run` — a second copy here drifted (it lacked the PATH
15
+ // probe), so the hook found no bash where the CLI did. One definition, bundled in.
16
+ import { findBash } from '../src/run.ts';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
21
+ const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
22
+ const SAFE = /^[A-Za-z0-9._-]+$/;
23
+
24
+ type Active = { mission: string; member: string; wave: string; mode: string; updated: number };
25
+
26
+ /**
27
+ * Name the mission when no savepoint exists yet, so the first one can be
28
+ * written. Ordered most- to least-authoritative; returns null when nothing
29
+ * indicates a mission is underway, which keeps this hook silent on projects
30
+ * that never engaged the crew.
31
+ */
32
+ function bootstrapMission(): Active | null {
33
+ // A plan, spec, or decision log means Flow 0 produced artifacts but no
34
+ // savepoint — exactly the drift this hook exists to end.
35
+ for (const dir of ['plans', 'spec', 'logs']) {
36
+ const d = join(cwd, '.mugiwara', dir);
37
+ if (!existsSync(d)) continue;
38
+ const files = readdirSync(d)
39
+ .filter((f) => f.endsWith('.md') && f !== 'lessons.md')
40
+ .sort()
41
+ .reverse();
42
+ for (const f of files) {
43
+ // strip the YYYY-MM-DD- prefix the crew writes; what remains is the mission
44
+ const name = f.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '');
45
+ if (name && SAFE.test(name) && !/^\.+$/.test(name)) {
46
+ return { mission: name, member: '', wave: '0', mode: readMode(), updated: 0 };
47
+ }
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function readMode(): string {
54
+ const file = join(cwd, '.mugiwara', 'config');
55
+ if (!existsSync(file)) return 'guided';
56
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
57
+ const [k, v] = line.split('=').map((s) => s.trim());
58
+ if (k === 'mode' && ['guided', 'semi', 'auto'].includes(v)) return v;
59
+ }
60
+ return 'guided';
61
+ }
62
+
63
+ function activeMission(): Active | null {
64
+ const base = join(cwd, '.mugiwara', 'state');
65
+ if (!existsSync(base)) return bootstrapMission();
66
+ let best: Active | null = null;
67
+ for (const e of readdirSync(base, { withFileTypes: true })) {
68
+ if (!e.isDirectory() || !SAFE.test(e.name) || /^\.+$/.test(e.name)) continue;
69
+ for (const f of readdirSync(join(base, e.name))) {
70
+ if (!f.endsWith('.json')) continue;
71
+ const stem = f.slice(0, -5);
72
+ const member = stem === 'state' ? '' : stem;
73
+ if (member && (!SAFE.test(member) || /^\.+$/.test(member))) continue;
74
+ try {
75
+ const s = JSON.parse(readFileSync(join(base, e.name, f), 'utf8'));
76
+ const updated = Date.parse(s.updated_at ?? '') || 0;
77
+ if (best && updated <= best.updated) continue;
78
+ const wave = String(s.flow ?? s.wave ?? '').replace(/\D/g, '') || '1';
79
+ const mode = ['guided', 'semi', 'auto'].includes(s.mode) ? s.mode : 'guided';
80
+ best = { mission: e.name, member, wave, mode, updated };
81
+ } catch {
82
+ // corrupt savepoint — skip, never break the Stop hook
83
+ }
84
+ }
85
+ }
86
+ return best ?? bootstrapMission();
87
+ }
88
+
89
+ try {
90
+ const active = activeMission();
91
+ const script = join(root, 'scripts', 'savepoint.sh');
92
+ const bash = findBash();
93
+ if (active && bash && existsSync(script)) {
94
+ spawnSync(bash, [script, active.mission, active.member, active.wave, active.mode], {
95
+ cwd,
96
+ stdio: 'ignore',
97
+ timeout: 15_000,
98
+ });
99
+ }
100
+ } catch {
101
+ // a Stop hook must never fail the turn
102
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // hooks/engagement-marker.ts
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { join } from "path";
7
+ var cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ async function main() {
9
+ let input = "";
10
+ for await (const chunk of process.stdin)
11
+ input += chunk;
12
+ let payload = {};
13
+ try {
14
+ payload = JSON.parse(input);
15
+ } catch {}
16
+ const toolName = typeof payload.tool_name === "string" ? payload.tool_name.toLowerCase() : "";
17
+ const toolInput = payload.tool_input ?? {};
18
+ const subagentType = typeof toolInput.subagent_type === "string" ? toolInput.subagent_type.toLowerCase() : "";
19
+ const skillName = typeof toolInput.skill === "string" ? toolInput.skill.toLowerCase() : "";
20
+ const agentField = `${toolName} ${subagentType} ${skillName}`;
21
+ if (!agentField.includes("mugiwara"))
22
+ return;
23
+ const dispatched = /zoro-execution|brook-healing|mugiwara-execution|mugiwara-healing|mugiwara-execute|mugiwara-heal/.test(agentField);
24
+ const planned = /nami-planner|mugiwara-planning|mugiwara-plan/.test(agentField);
25
+ const dir = join(cwd, ".mugiwara", "state");
26
+ const file = join(dir, ".engaged");
27
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
28
+ try {
29
+ mkdirSync(dir, { recursive: true });
30
+ let firstSeen = new Date().toISOString();
31
+ let dispatchedAt = "";
32
+ let plannedAt = "";
33
+ if (existsSync(file)) {
34
+ try {
35
+ const prev = JSON.parse(readFileSync(file, "utf8"));
36
+ const sameSession = !sessionId || !prev.session_id || prev.session_id === sessionId;
37
+ if (sameSession && typeof prev.first_seen === "string")
38
+ firstSeen = prev.first_seen;
39
+ if (sameSession && typeof prev.executor_dispatched_at === "string")
40
+ dispatchedAt = prev.executor_dispatched_at;
41
+ if (sameSession && typeof prev.planner_dispatched_at === "string")
42
+ plannedAt = prev.planner_dispatched_at;
43
+ } catch {}
44
+ }
45
+ if (dispatched)
46
+ dispatchedAt = new Date().toISOString();
47
+ if (planned)
48
+ plannedAt = new Date().toISOString();
49
+ writeFileSync(file, JSON.stringify({
50
+ session_id: sessionId,
51
+ first_seen: firstSeen,
52
+ touched_at: new Date().toISOString(),
53
+ executor_dispatched_at: dispatchedAt,
54
+ planner_dispatched_at: plannedAt
55
+ }, null, 2) + `
56
+ `);
57
+ } catch {}
58
+ }
59
+ main().catch(() => {});
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ // hooks/engagement-marker.ts — PostToolUse on Task|Skill: record that this
3
+ // session engaged the mugiwara crew.
4
+ //
5
+ // The pipeline guard only polices sessions that actually used mugiwara. Without
6
+ // this marker the guard would either police every session in every repo that
7
+ // happens to have a .mugiwara dir (infuriating, and users would disable it) or
8
+ // police nothing. Engagement is the scope.
9
+ //
10
+ // Never throws: a failed marker write degrades to "not engaged", which means
11
+ // the guard stays silent. Failing open is deliberate — an enforcement layer
12
+ // that can wedge a session gets turned off, and then it enforces nothing.
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
17
+
18
+ async function main(): Promise<void> {
19
+ let input = '';
20
+ for await (const chunk of process.stdin) input += chunk;
21
+
22
+ let payload: Record<string, unknown> = {};
23
+ try { payload = JSON.parse(input) as Record<string, unknown>; } catch { /* not JSON — treat as no payload */ }
24
+
25
+ // Only mugiwara work counts as engagement. The tool input carries the agent
26
+ // or skill name; anything else in the session is none of our business.
27
+ // Match the STRUCTURED agent/skill-name field only — never free-text tool
28
+ // input. A Task description that merely cites a skill, or a doc read that
29
+ // mentions one, must not count as a dispatch (that would let the actor the
30
+ // guard constrains spoof the marker and neuter the check).
31
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name.toLowerCase() : '';
32
+ const toolInput = (payload.tool_input ?? {}) as Record<string, unknown>;
33
+ const subagentType = typeof toolInput.subagent_type === 'string' ? toolInput.subagent_type.toLowerCase() : '';
34
+ const skillName = typeof toolInput.skill === 'string' ? toolInput.skill.toLowerCase() : '';
35
+ const agentField = `${toolName} ${subagentType} ${skillName}`;
36
+ if (!agentField.includes('mugiwara')) return;
37
+
38
+ // Executor dispatch is a second, narrower fact recorded in the same marker:
39
+ // did the session hand source work to the only two roles allowed to write it
40
+ // (Zoro / Brook)? Both the subagent form and the inline-embody skill form
41
+ // count — on Codex-style harnesses embodying the role IS the dispatch.
42
+ // The guard uses this to catch a captain implementing Lane 1+ work inline.
43
+ const dispatched = /zoro-execution|brook-healing|mugiwara-execution|mugiwara-healing|mugiwara-execute|mugiwara-heal/.test(agentField);
44
+
45
+ // Planner dispatch is a third fact: did the session hand plan-writing to the
46
+ // only role allowed to write the plan doc (Nami)? Same two forms count —
47
+ // subagent `nami-planner` and inline skill `mugiwara-planning`/`mugiwara-plan`.
48
+ // The guard uses this to catch a captain (or anyone else) writing the plan
49
+ // inline, which is escape #2: only Nami writes the plan.
50
+ const planned = /nami-planner|mugiwara-planning|mugiwara-plan/.test(agentField);
51
+
52
+ const dir = join(cwd, '.mugiwara', 'state');
53
+ const file = join(dir, '.engaged');
54
+ // session_id scopes the marker when the payload carries one. Its presence on
55
+ // every hook event is NOT verified, so the guard also accepts a recent mtime
56
+ // — correct under both, rather than assuming a payload shape.
57
+ const sessionId = typeof payload.session_id === 'string' ? payload.session_id : '';
58
+
59
+ try {
60
+ mkdirSync(dir, { recursive: true });
61
+ // preserve the first engagement time; only refresh the touch timestamp.
62
+ // BUT: first_seen is the "this session" anchor for the plan guard's
63
+ // planTouched() (a plan is "written this session" if its mtime >=
64
+ // first_seen). A continuation session is a NEW session — carrying the old
65
+ // first_seen forward would make every prior plan count as "written this
66
+ // session" while the new session's planner dispatch is empty, warning on
67
+ // every guard event. Reset first_seen when the session changes.
68
+ let firstSeen = new Date().toISOString();
69
+ let dispatchedAt = '';
70
+ let plannedAt = '';
71
+ if (existsSync(file)) {
72
+ try {
73
+ const prev = JSON.parse(readFileSync(file, 'utf8')) as { session_id?: string; first_seen?: string; executor_dispatched_at?: string; planner_dispatched_at?: string };
74
+ const sameSession = !sessionId || !prev.session_id || prev.session_id === sessionId;
75
+ if (sameSession && typeof prev.first_seen === 'string') firstSeen = prev.first_seen;
76
+ // A dispatch belongs to the session that made it. Carrying it into the
77
+ // next session would excuse a captain who implements inline today
78
+ // because Zoro ran yesterday.
79
+ if (sameSession && typeof prev.executor_dispatched_at === 'string') dispatchedAt = prev.executor_dispatched_at;
80
+ if (sameSession && typeof prev.planner_dispatched_at === 'string') plannedAt = prev.planner_dispatched_at;
81
+ } catch { /* corrupt marker — rewrite it */ }
82
+ }
83
+ if (dispatched) dispatchedAt = new Date().toISOString();
84
+ if (planned) plannedAt = new Date().toISOString();
85
+ writeFileSync(file, JSON.stringify({
86
+ session_id: sessionId,
87
+ first_seen: firstSeen,
88
+ touched_at: new Date().toISOString(),
89
+ executor_dispatched_at: dispatchedAt,
90
+ planner_dispatched_at: plannedAt,
91
+ }, null, 2) + '\n');
92
+ } catch {
93
+ // cannot write (read-only fs, permissions) — stay silent, guard stays off
94
+ }
95
+ }
96
+
97
+ main().catch(() => { /* a hook must never fail the turn */ });
package/hooks/hooks.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/session-start.ts",
8
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/session-start.js",
9
9
  "timeout": 10
10
10
  }
11
11
  ]
@@ -16,11 +16,50 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/mugiwara-mode-tracker.ts",
19
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/mugiwara-mode-tracker.js",
20
20
  "timeout": 5
21
21
  }
22
22
  ]
23
23
  }
24
+ ],
25
+ "Stop": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/auto-savepoint.js",
31
+ "timeout": 20
32
+ },
33
+ {
34
+ "type": "command",
35
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/pipeline-guard.js",
36
+ "timeout": 15
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "PostToolUse": [
42
+ {
43
+ "matcher": "Task|Skill",
44
+ "hooks": [
45
+ {
46
+ "type": "command",
47
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/engagement-marker.js",
48
+ "timeout": 5
49
+ }
50
+ ]
51
+ }
52
+ ],
53
+ "SubagentStop": [
54
+ {
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/pipeline-guard.js",
59
+ "timeout": 15
60
+ }
61
+ ]
62
+ }
24
63
  ]
25
64
  }
26
65
  }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // hooks/mugiwara-mode-tracker.ts
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
6
+ import { tmpdir } from "os";
7
+ import { join } from "path";
8
+ var VALID_MODES = new Set(["guided", "semi", "auto"]);
9
+ function parseModeChange(promptRaw) {
10
+ if (typeof promptRaw !== "string")
11
+ return null;
12
+ let prompt = promptRaw.trim();
13
+ const wrapped = /^(["'`])([\s\S]*)\1$/.exec(prompt);
14
+ if (wrapped)
15
+ prompt = wrapped[2].trim();
16
+ prompt = prompt.toLowerCase();
17
+ if (!prompt)
18
+ return null;
19
+ const tplSet = /^(?:set |)mugiwara mode:[ \t]*(\S*)/.exec(prompt);
20
+ if (tplSet && VALID_MODES.has(tplSet[1]))
21
+ return tplSet[1];
22
+ const slashMode = /^\/(?:mugiwara[-\s]?)?mode[ \t]+(\S*)/.exec(prompt);
23
+ if (slashMode && VALID_MODES.has(slashMode[1]))
24
+ return slashMode[1];
25
+ const slashMain = /^\/mugiwara[ \t]+(\S*)/.exec(prompt);
26
+ if (slashMain && VALID_MODES.has(slashMain[1]))
27
+ return slashMain[1];
28
+ const natural = /^mugiwara mode[ \t]+(\S*)/.exec(prompt);
29
+ if (natural && VALID_MODES.has(natural[1]))
30
+ return natural[1];
31
+ return null;
32
+ }
33
+ function applyModeChange(mode) {
34
+ if (!VALID_MODES.has(mode))
35
+ return;
36
+ const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
37
+ const dir = join(cwd, ".mugiwara");
38
+ const file = join(dir, "config");
39
+ mkdirSync(dir, { recursive: true });
40
+ const lines = [];
41
+ if (existsSync(file)) {
42
+ for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
43
+ const t = line.trim();
44
+ if (t.startsWith("mode="))
45
+ continue;
46
+ lines.push(line);
47
+ }
48
+ }
49
+ lines.push(`mode=${mode}`);
50
+ const body = lines.filter((_l, i) => !(lines[i] === "" && (i === lines.length - 1 || i === 0))).join(`
51
+ `) + `
52
+ `;
53
+ const tmp = join(tmpdir(), `mugiwara-config-${process.pid}-${Date.now()}.tmp`);
54
+ writeFileSync(tmp, body);
55
+ renameSync(tmp, file);
56
+ }
57
+ async function main() {
58
+ let input = "";
59
+ for await (const chunk of process.stdin)
60
+ input += chunk;
61
+ if (!input.trim()) {
62
+ process.stdout.write(JSON.stringify({ prompt: "" }));
63
+ return;
64
+ }
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(input);
68
+ } catch {
69
+ parsed = { prompt: input };
70
+ }
71
+ const prompt = parsed.prompt ?? "";
72
+ const change = parseModeChange(prompt);
73
+ if (change)
74
+ applyModeChange(change);
75
+ process.stdout.write(JSON.stringify({ prompt }));
76
+ }
77
+ main().catch(() => {
78
+ process.stdout.write(JSON.stringify({ prompt: "" }));
79
+ });