@ionivetech/mugiwara 0.6.5 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -3
- package/.codex-plugin/plugin.json +1 -2
- package/.cursor-plugin/plugin.json +1 -2
- package/.kimi-plugin/plugin.json +1 -2
- package/.opencode/commands/mugiwara-continue.md +44 -31
- package/.opencode/commands/mugiwara-execute.md +1 -1
- package/.opencode/commands/mugiwara-heal.md +1 -1
- package/.opencode/commands/mugiwara-onboard.md +8 -9
- package/.opencode/commands/mugiwara-plan.md +1 -1
- package/.opencode/commands/mugiwara-review.md +1 -1
- package/.opencode/commands/mugiwara-security.md +1 -1
- package/.opencode/commands/mugiwara-ship.md +1 -1
- package/.opencode/commands/mugiwara.md +2 -2
- package/.opencode/plugins/mugiwara.mjs +1 -2
- package/AGENTS.md +1 -2
- package/README.md +33 -31
- package/content/agents/brook-healing.md +5 -8
- package/content/agents/chopper-checkpoint.md +8 -11
- package/content/agents/eval-runner.md +2 -5
- package/content/agents/franky-gates.md +4 -7
- package/content/agents/jinbe-security.md +4 -7
- package/content/agents/luffy-orchestrator.md +13 -13
- package/content/agents/memory-keeper.md +5 -8
- package/content/agents/nami-planner.md +4 -7
- package/content/agents/resume-coordinator.md +7 -10
- package/content/agents/robin-reviewer.md +6 -9
- package/content/agents/sanji-quality.md +7 -10
- package/content/agents/skeptic-verifier.md +5 -8
- package/content/agents/usopp-brainstorm.md +3 -6
- package/content/agents/zoro-execution.md +7 -10
- package/content/skills/mugiwara-backend/SKILL.md +2 -0
- package/content/skills/mugiwara-checkpoint/SKILL.md +13 -11
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +4 -4
- package/content/skills/mugiwara-context-budget/SKILL.md +5 -3
- package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +20 -20
- package/content/skills/mugiwara-execution/references/dispatch.md +2 -2
- package/content/skills/mugiwara-execution/references/resume-batching.md +1 -1
- package/content/skills/mugiwara-frontend/SKILL.md +2 -4
- package/content/skills/mugiwara-gates/SKILL.md +8 -7
- package/content/skills/mugiwara-git/SKILL.md +7 -2
- package/content/skills/mugiwara-healing/SKILL.md +6 -4
- package/content/skills/mugiwara-healing/references/workers.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +26 -24
- package/content/skills/mugiwara-orchestration/references/check-ins.md +16 -15
- package/content/skills/mugiwara-orchestration/references/closure.md +5 -5
- package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +22 -22
- package/content/skills/mugiwara-planning/SKILL.md +3 -1
- package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
- package/content/skills/mugiwara-pr/SKILL.md +6 -6
- package/content/skills/mugiwara-pr/references/verdict-format.md +3 -3
- package/content/skills/mugiwara-quality/SKILL.md +7 -7
- package/content/skills/mugiwara-resume/SKILL.md +18 -20
- package/content/skills/mugiwara-review/SKILL.md +5 -1
- package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +5 -1
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +5 -3
- package/content/skills/mugiwara-workflow/SKILL.md +27 -27
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +15 -15
- package/content/skills/using-mugiwara/SKILL.md +5 -5
- package/dist/mugiwara.js +808 -33
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +106 -0
- package/hooks/auto-savepoint.ts +102 -0
- package/hooks/engagement-marker.js +59 -0
- package/hooks/engagement-marker.ts +97 -0
- package/hooks/hooks.json +41 -2
- package/hooks/mugiwara-mode-tracker.js +79 -0
- package/hooks/pipeline-guard.js +182 -0
- package/hooks/pipeline-guard.ts +267 -0
- package/hooks/session-start.js +106 -0
- package/hooks/session-start.ts +44 -32
- package/package.json +7 -3
- package/plugin.json +1 -1
- package/references/agent-protocol.md +15 -0
- package/references/complexity.md +54 -0
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +5 -5
- package/references/skill-versioning.md +1 -1
- package/references/token-budget.md +30 -8
- package/references/wave-banners.md +16 -16
- package/scripts/build-hooks.ts +56 -0
- package/scripts/conformance.ts +17 -0
- package/scripts/coverage-gate.ts +179 -0
- package/scripts/install.ps1 +5 -4
- package/scripts/install.sh +4 -4
- package/scripts/lane.sh +6 -2
- package/scripts/lib/patterns.sh +84 -0
- package/scripts/mission-report.sh +12 -11
- package/scripts/savepoint.sh +141 -37
- package/scripts/verify-install.ts +154 -44
- package/src/args.ts +1 -1
- package/src/cli.ts +217 -0
- package/src/continue.ts +243 -0
- package/src/installer.ts +39 -0
- package/src/onboard.ts +207 -0
- package/src/run.ts +82 -0
- package/src/targets/claude.ts +140 -2
- package/src/targets/copilot.ts +20 -1
- package/src/targets/generic.ts +9 -4
- package/src/targets/opencode.ts +0 -1
- package/content/agents/onboarding-guide.md +0 -124
- package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
- package/scripts/onboard.ts +0 -266
- package/scripts/probe.ts +0 -40
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/cli.ts
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import { dirname, join, resolve } from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,6 +11,9 @@ import { targets, TARGET_IDS } from './targets/index.ts';
|
|
|
10
11
|
import { installTo, removeInstalled, VERSION, ensureProjectGitignore, removeProjectGitignore } from './installer.ts';
|
|
11
12
|
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
12
13
|
import { resetMission, archiveMission } from './mission.ts';
|
|
14
|
+
import { runOnboard } from './onboard.ts';
|
|
15
|
+
import { runScript, RUNNABLE } from './run.ts';
|
|
16
|
+
import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor } from './continue.ts';
|
|
13
17
|
|
|
14
18
|
const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
15
19
|
const flag = (v: FlagValue): boolean => v === true;
|
|
@@ -25,6 +29,13 @@ export async function run(argv: string[]): Promise<void> {
|
|
|
25
29
|
case 'list': return list(flags);
|
|
26
30
|
case 'reset': return resetCmd(flags);
|
|
27
31
|
case 'archive': return archive(flags, _);
|
|
32
|
+
case 'onboard': return onboardCmd(flags);
|
|
33
|
+
case 'continue': return continueCmd(flags, _);
|
|
34
|
+
case 'status': return statusCmd(flags);
|
|
35
|
+
case 'run': return runCmd(flags, _);
|
|
36
|
+
case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
|
|
37
|
+
case 'initiative': return initiativeCmd(flags, _);
|
|
38
|
+
case 'start': return startCmd(flags, _);
|
|
28
39
|
default: throw new Error(`Unknown command: ${command}`);
|
|
29
40
|
}
|
|
30
41
|
}
|
|
@@ -53,6 +64,12 @@ function archive(flags: Args['flags'], positionals: string[]): void {
|
|
|
53
64
|
if (result.index) console.log(`index updated: ${result.index}`);
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
async function onboardCmd(flags: Args['flags']): Promise<void> {
|
|
68
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
69
|
+
if (!existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
|
|
70
|
+
await runOnboard(projectDir);
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
|
|
57
74
|
const interactive = !flag(flags.yes);
|
|
58
75
|
if (interactive && !process.stdin.isTTY) {
|
|
@@ -123,6 +140,22 @@ async function install(flags: Args['flags']): Promise<void> {
|
|
|
123
140
|
});
|
|
124
141
|
console.log(`\nOK mugiwara ${VERSION} installed (manifest: ${file})`);
|
|
125
142
|
if (allNotes.length) console.log(`${allNotes.length} note(s) above may need attention.`);
|
|
143
|
+
// TASK 8: name exactly one next step, and state the active mode + enforce
|
|
144
|
+
// value since both change behaviour.
|
|
145
|
+
if (scope === 'project') {
|
|
146
|
+
const cfg = join(projectDir, '.mugiwara', 'config');
|
|
147
|
+
let mode = 'guided', enforce = 'block';
|
|
148
|
+
if (existsSync(cfg)) {
|
|
149
|
+
for (const line of readFileSync(cfg, 'utf8').split(/\r?\n/)) {
|
|
150
|
+
const [k, v] = line.split('=').map((s) => s.trim());
|
|
151
|
+
if (k === 'mode') mode = v;
|
|
152
|
+
if (k === 'enforce') enforce = v;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(`\nNext: run \`mugiwara onboard\` to customise (mode=${mode}, enforce=${enforce} now).`);
|
|
156
|
+
} else {
|
|
157
|
+
console.log('\nNext: run `mugiwara onboard` in a project to write its .mugiwara/config.');
|
|
158
|
+
}
|
|
126
159
|
}
|
|
127
160
|
|
|
128
161
|
async function uninstall(flags: Args['flags']): Promise<void> {
|
|
@@ -150,6 +183,16 @@ async function uninstall(flags: Args['flags']): Promise<void> {
|
|
|
150
183
|
if (!ok) { console.log('Aborted.'); return; }
|
|
151
184
|
}
|
|
152
185
|
const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
|
|
186
|
+
// Un-merge anything we injected into files the user owns. These are
|
|
187
|
+
// deliberately absent from the manifest — deleting them would destroy the
|
|
188
|
+
// user's own configuration alongside ours.
|
|
189
|
+
for (const id of manifest.targets) {
|
|
190
|
+
const t = targets[id];
|
|
191
|
+
if (!t?.postUninstall) continue;
|
|
192
|
+
const post = t.postUninstall({ scope, projectDir, home, dryRun: flag(flags.dryRun) });
|
|
193
|
+
for (const f of post.changed) console.log(` unwired mugiwara hooks from ${f}`);
|
|
194
|
+
for (const n of post.notes) console.log(` note: ${n}`);
|
|
195
|
+
}
|
|
153
196
|
if (!flag(flags.dryRun)) {
|
|
154
197
|
if (scope === 'project') {
|
|
155
198
|
const gi = removeProjectGitignore(projectDir);
|
|
@@ -192,6 +235,166 @@ function list(flags: Args['flags']): void {
|
|
|
192
235
|
if (!found) console.log('No mugiwara installation found.');
|
|
193
236
|
}
|
|
194
237
|
|
|
238
|
+
/**
|
|
239
|
+
* `mugiwara continue [mission] [member]` — the deterministic half of resume.
|
|
240
|
+
*
|
|
241
|
+
* Selecting which mission/member to resume is a directory scan, not a judgement
|
|
242
|
+
* call, so it runs here instead of costing the host model a reasoning turn.
|
|
243
|
+
* Only the last step (verifying next_action against the plan) needs a model,
|
|
244
|
+
* and that happens after this prints.
|
|
245
|
+
*
|
|
246
|
+
* Exit codes: 0 = a single resume point was printed; 2 = ambiguous or absent,
|
|
247
|
+
* the caller must stop and let the user pick.
|
|
248
|
+
*/
|
|
249
|
+
function continueCmd(flags: Args['flags'], positionals: string[]): void {
|
|
250
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
251
|
+
const [mission, member] = positionals.slice(1);
|
|
252
|
+
let entries = readContinue(projectDir);
|
|
253
|
+
|
|
254
|
+
// default to this actor's work; --all crosses actors on a shared checkout
|
|
255
|
+
if (!flag(flags.all)) {
|
|
256
|
+
const actor = gitActor(projectDir);
|
|
257
|
+
const mine = entries.filter((e) => e.actor === actor);
|
|
258
|
+
// an actor-less savepoint (older file, or git identity unset) is still the
|
|
259
|
+
// only thing on disk — showing nothing would look like "no missions"
|
|
260
|
+
if (mine.length) entries = mine;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const r = resolveContinue(entries, mission, member);
|
|
264
|
+
if (r.kind === 'resume') { console.log(formatResume(r.entry)); return; }
|
|
265
|
+
|
|
266
|
+
if (r.kind === 'none') {
|
|
267
|
+
console.log('No mission in flight. Start one with Flow 0 triage (mugiwara-orchestration).');
|
|
268
|
+
} else if (r.kind === 'missions') {
|
|
269
|
+
console.log(`${new Set(r.entries.map((e) => e.mission)).size} missions in flight:\n`);
|
|
270
|
+
console.log(formatTable(r.entries));
|
|
271
|
+
console.log('\nPick one: mugiwara continue <mission> [member]');
|
|
272
|
+
} else if (r.kind === 'members') {
|
|
273
|
+
console.log(`Mission "${r.mission}" has ${r.entries.length} members in flight:\n`);
|
|
274
|
+
console.log(formatTable(r.entries));
|
|
275
|
+
console.log(`\nPick one: mugiwara continue ${r.mission} <member>`);
|
|
276
|
+
} else if (r.kind === 'unknown-mission') {
|
|
277
|
+
console.error(`No in-flight mission "${r.mission}". Known: ${r.known.join(', ') || '(none)'}`);
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`Mission "${r.mission}" has no member "${r.member}". Known: ${r.known.join(', ')}`);
|
|
280
|
+
}
|
|
281
|
+
process.exit(2);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** `mugiwara status` — one screen of computed mission state, no model needed. */
|
|
285
|
+
function statusCmd(flags: Args['flags']): void {
|
|
286
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
287
|
+
const states = readState(projectDir);
|
|
288
|
+
if (!states.length) { console.log('No mission state on disk.'); return; }
|
|
289
|
+
const actor = flag(flags.all) ? null : gitActor(projectDir);
|
|
290
|
+
const rows = actor ? (states.filter((s) => s.actor === actor).length ? states.filter((s) => s.actor === actor) : states) : states;
|
|
291
|
+
for (const s of rows) {
|
|
292
|
+
const scope = s.member ? ` [${s.member}]` : '';
|
|
293
|
+
console.log(`${s.mission}${scope}`);
|
|
294
|
+
console.log(` flow ${s.flow} · ${s.tasks_done}/${s.tasks_total} tasks · lane ${s.lane}${s.lane_rose ? ' ⬆ ROSE' : ''}${s.lane_reason ? ` (${s.lane_reason})` : ''} · mode ${s.mode}`);
|
|
295
|
+
console.log(` blockers ${s.blockers_open} · heal cycle ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? ' — HALT' : ''} · files touched ${s.files_touched}`);
|
|
296
|
+
if (s.budget) console.log(` tokens ${s.tokens_est}/${s.budget} (${s.budget_status})${s.delegate_due ? ' · delegate due' : ''}`);
|
|
297
|
+
console.log(` branch ${s.branch} · updated ${s.updated_at}`);
|
|
298
|
+
if (s.evidence.length) console.log(` evidence: ${s.evidence.join(', ')}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
|
|
303
|
+
function runCmd(flags: Args['flags'], positionals: string[]): void {
|
|
304
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
305
|
+
const name = positionals[1];
|
|
306
|
+
if (!name) {
|
|
307
|
+
console.error(`usage: mugiwara run <script> [args...]\n scripts: ${RUNNABLE.join(', ')}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const code = runScript(name, positionals.slice(2), projectDir);
|
|
311
|
+
if (code !== 0) process.exit(code);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** `mugiwara initiative <status|conflict-check|set-status> <plan> [args]` — team sub-mission coordination. */
|
|
315
|
+
function initiativeCmd(flags: Args['flags'], positionals: string[]): void {
|
|
316
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
317
|
+
const sub = positionals[1];
|
|
318
|
+
if (!sub) {
|
|
319
|
+
console.error('usage: mugiwara initiative <status|conflict-check|set-status> <plan-file> [--id <id> --status <s>]');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
const script = join(import.meta.dirname, '..', 'scripts', 'initiative.ts');
|
|
323
|
+
// initiative.ts is TypeScript — spawn `bun` (which strips types), not node,
|
|
324
|
+
// which only strips types on >=22.18. engines floor is >=20.11.
|
|
325
|
+
const bun = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
|
326
|
+
const r = spawnSync(bun, [script, ...positionals.slice(1)], {
|
|
327
|
+
cwd: projectDir,
|
|
328
|
+
stdio: 'inherit',
|
|
329
|
+
env: process.env,
|
|
330
|
+
});
|
|
331
|
+
if (r.error) throw r.error;
|
|
332
|
+
if (r.status) process.exit(r.status);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** `mugiwara start <mission> [member]` — begin a mission from its plan. */
|
|
336
|
+
function startCmd(flags: Args['flags'], positionals: string[]): void {
|
|
337
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
338
|
+
const mission = positionals[1];
|
|
339
|
+
if (!mission) {
|
|
340
|
+
console.error('usage: mugiwara start <mission> [member]');
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
// find the plan doc: plans/<mission>.md or plans/YYYY-MM-DD-<mission>.md
|
|
344
|
+
const plansDir = join(projectDir, '.mugiwara', 'plans');
|
|
345
|
+
let planFile = '';
|
|
346
|
+
if (existsSync(plansDir)) {
|
|
347
|
+
const bare = join(plansDir, `${mission}.md`);
|
|
348
|
+
if (existsSync(bare)) planFile = bare;
|
|
349
|
+
else {
|
|
350
|
+
const dated = readdirSync(plansDir).filter((f) => f.endsWith(`-${mission}.md`)).sort().pop();
|
|
351
|
+
if (dated) planFile = join(plansDir, dated);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!planFile) {
|
|
355
|
+
console.error(`mugiwara start: no plan found for "${mission}" in ${plansDir}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const plan = readFileSync(planFile, 'utf8');
|
|
359
|
+
|
|
360
|
+
// team plan = has a ## Sub-missions table; solo = none.
|
|
361
|
+
const subMatch = plan.match(/##\s*Sub-missions/i);
|
|
362
|
+
let member = positionals[2] ?? '';
|
|
363
|
+
if (subMatch && !member) {
|
|
364
|
+
// team: find the row whose Assignee matches this git actor
|
|
365
|
+
const actor = gitActor(projectDir);
|
|
366
|
+
const name = actor.split('<')[0].trim();
|
|
367
|
+
const rowRe = /^\|\s*([\w.-]+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm;
|
|
368
|
+
let m: RegExpExecArray | null;
|
|
369
|
+
let found = '';
|
|
370
|
+
while ((m = rowRe.exec(plan))) {
|
|
371
|
+
const assignee = m[3].trim();
|
|
372
|
+
if (assignee === name || assignee === actor) { found = m[1].trim(); break; }
|
|
373
|
+
}
|
|
374
|
+
if (!found) {
|
|
375
|
+
console.error(`mugiwara start: no sub-mission assigned to "${name}" in ${planFile}. Pass a member explicitly: mugiwara start ${mission} <member>`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
member = found;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// write the savepoint: mission [member] flow=0 mode=auto lane=full (start)
|
|
382
|
+
const code = runScript('savepoint.sh', [mission, member, '0', 'auto', 'full'], projectDir);
|
|
383
|
+
if (code !== 0) process.exit(code);
|
|
384
|
+
|
|
385
|
+
// team plan: mark the sub-mission in-progress in the plan (source of truth)
|
|
386
|
+
if (subMatch && member) {
|
|
387
|
+
const init = join(import.meta.dirname, '..', 'scripts', 'initiative.ts');
|
|
388
|
+
const bun = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
|
389
|
+
const r = spawnSync(bun, [init, 'set-status', planFile, '--id', member, '--status', 'in-progress'], {
|
|
390
|
+
cwd: projectDir, stdio: 'inherit', env: process.env,
|
|
391
|
+
});
|
|
392
|
+
if (r.status) process.exit(r.status);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(`mugiwara start: ${mission}${member ? ` [${member}]` : ''} begun at Flow 0. Resume anytime with: mugiwara continue ${mission}${member ? ` ${member}` : ''}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
195
398
|
function help(): void {
|
|
196
399
|
console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
|
|
197
400
|
|
|
@@ -203,6 +406,19 @@ Usage:
|
|
|
203
406
|
mugiwara list --check health check: show installations + missing files
|
|
204
407
|
mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
|
|
205
408
|
mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
|
|
409
|
+
mugiwara onboard run the zero-LLM onboarding wizard (writes .mugiwara/config)
|
|
410
|
+
mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
|
|
411
|
+
mugiwara continue <m> [member]
|
|
412
|
+
print the exact resume point for that mission/member
|
|
413
|
+
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
414
|
+
mugiwara run <script> [args...]
|
|
415
|
+
run a bundled harness script here (${RUNNABLE.join(', ')})
|
|
416
|
+
mugiwara savepoint <mission> [member] [wave] [mode]
|
|
417
|
+
shorthand for: mugiwara run savepoint.sh ...
|
|
418
|
+
mugiwara initiative <status|conflict-check|set-status> <plan> [args]
|
|
419
|
+
team sub-mission coordination (status / conflict-check / set-status)
|
|
420
|
+
mugiwara start <mission> [member]
|
|
421
|
+
begin a mission from its plan (solo, or team sub-mission by git actor)
|
|
206
422
|
mugiwara --help this help
|
|
207
423
|
mugiwara --version print version
|
|
208
424
|
|
|
@@ -214,6 +430,7 @@ Flags:
|
|
|
214
430
|
--force overwrite differing files (with backup)
|
|
215
431
|
--dry-run print actions without writing
|
|
216
432
|
--check with list: report missing files (health check)
|
|
433
|
+
--all with continue/status: every actor, not just yours
|
|
217
434
|
--keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
|
|
218
435
|
}
|
|
219
436
|
|
package/src/continue.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// src/continue.ts
|
|
2
|
+
// Deterministic resume-point reader. Powers `mugiwara continue` and
|
|
3
|
+
// `mugiwara status` — pure fs + JSON, zero model judgement. The prose skills
|
|
4
|
+
// used to describe this scan so the host model would perform it; that cost a
|
|
5
|
+
// full reasoning turn to answer "what is in flight?". This is the same answer
|
|
6
|
+
// in one process spawn.
|
|
7
|
+
//
|
|
8
|
+
// Reads what scripts/savepoint.sh writes:
|
|
9
|
+
// .mugiwara/continue/<mission>/state.json (solo)
|
|
10
|
+
// .mugiwara/continue/<mission>/<member>.json (team)
|
|
11
|
+
// .mugiwara/state/<mission>/... (same shape, more fields)
|
|
12
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
/** Mission/member allowlist — identical to savepoint.sh and mission.ts. */
|
|
17
|
+
const SAFE = /^[A-Za-z0-9._-]+$/;
|
|
18
|
+
const isSafeKey = (s: string): boolean => SAFE.test(s) && !/^\.+$/.test(s);
|
|
19
|
+
|
|
20
|
+
export type ContinueEntry = {
|
|
21
|
+
mission: string;
|
|
22
|
+
member: string | null;
|
|
23
|
+
actor: string;
|
|
24
|
+
branch: string;
|
|
25
|
+
flow: number;
|
|
26
|
+
mode: string;
|
|
27
|
+
tasks_done: number;
|
|
28
|
+
tasks_total: number;
|
|
29
|
+
lane: string;
|
|
30
|
+
next_action: string;
|
|
31
|
+
next_session_prompt: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type StateEntry = ContinueEntry & {
|
|
36
|
+
lane_reason: string;
|
|
37
|
+
lane_rose: boolean;
|
|
38
|
+
lane_prev: string;
|
|
39
|
+
lane_peak: string;
|
|
40
|
+
blockers_open: number;
|
|
41
|
+
heal_cycle: number;
|
|
42
|
+
heal_max_cycles: number;
|
|
43
|
+
heal_halt: boolean;
|
|
44
|
+
delegate_threshold: number;
|
|
45
|
+
delegate_due: boolean;
|
|
46
|
+
tokens_est: number;
|
|
47
|
+
budget: number;
|
|
48
|
+
budget_status: string;
|
|
49
|
+
files_touched: number;
|
|
50
|
+
evidence: string[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const num = (v: unknown): number => {
|
|
54
|
+
const n = Number(v);
|
|
55
|
+
return Number.isFinite(n) ? n : 0;
|
|
56
|
+
};
|
|
57
|
+
const text = (v: unknown): string => (typeof v === 'string' ? v : '');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the git actor exactly as scripts/savepoint.sh does:
|
|
61
|
+
* STATE_ACTOR → GIT_AUTHOR_NAME → "name <email>" → USER. A byte difference
|
|
62
|
+
* here silently filters every mission out of the listing.
|
|
63
|
+
*/
|
|
64
|
+
export function gitActor(cwd: string): string {
|
|
65
|
+
const env = (process.env.STATE_ACTOR ?? '').trim();
|
|
66
|
+
if (env) return env;
|
|
67
|
+
const author = (process.env.GIT_AUTHOR_NAME ?? '').trim();
|
|
68
|
+
if (author) return author;
|
|
69
|
+
const git = (args: string[]): string => {
|
|
70
|
+
try {
|
|
71
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
72
|
+
} catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const name = git(['config', 'user.name']);
|
|
77
|
+
const email = git(['config', 'user.email']);
|
|
78
|
+
if (name && email) return `${name} <${email}>`;
|
|
79
|
+
// USERNAME is the Windows spelling of USER; savepoint.sh resolves the same
|
|
80
|
+
// chain, and the two must stay byte-identical or the actor filter drops rows.
|
|
81
|
+
return name || process.env.USER || process.env.USERNAME || '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Read every JSON under `.mugiwara/<root>/<mission>/*.json`. Corrupt files are skipped. */
|
|
85
|
+
function scan<T>(projectDir: string, root: string, map: (raw: Record<string, unknown>, member: string | null) => T): T[] {
|
|
86
|
+
const base = join(projectDir, '.mugiwara', root);
|
|
87
|
+
if (!existsSync(base)) return [];
|
|
88
|
+
const out: T[] = [];
|
|
89
|
+
const missions = readdirSync(base, { withFileTypes: true })
|
|
90
|
+
.filter((e) => e.isDirectory() && isSafeKey(e.name))
|
|
91
|
+
.map((e) => e.name)
|
|
92
|
+
.sort();
|
|
93
|
+
for (const mission of missions) {
|
|
94
|
+
const dir = join(base, mission);
|
|
95
|
+
for (const f of readdirSync(dir).filter((n) => n.endsWith('.json')).sort()) {
|
|
96
|
+
// state.json is the solo (member-less) file; <member>.json is a team member
|
|
97
|
+
const stem = f.slice(0, -'.json'.length);
|
|
98
|
+
const member = stem === 'state' ? null : stem;
|
|
99
|
+
if (member !== null && !isSafeKey(member)) continue;
|
|
100
|
+
try {
|
|
101
|
+
const raw = JSON.parse(readFileSync(join(dir, f), 'utf8')) as Record<string, unknown>;
|
|
102
|
+
// trust the path over the file body: a mission field that disagrees
|
|
103
|
+
// with its own directory is a corrupt or hand-edited file
|
|
104
|
+
if (text(raw.mission) !== mission) continue;
|
|
105
|
+
out.push(map(raw, member));
|
|
106
|
+
} catch {
|
|
107
|
+
// corrupt savepoint — skip, never crash the listing
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readContinue(projectDir: string): ContinueEntry[] {
|
|
115
|
+
return scan(projectDir, 'continue', (r, member) => ({
|
|
116
|
+
mission: text(r.mission),
|
|
117
|
+
member,
|
|
118
|
+
actor: text(r.actor),
|
|
119
|
+
branch: text(r.branch),
|
|
120
|
+
flow: num(r.flow ?? r.wave),
|
|
121
|
+
mode: text(r.mode) || 'guided',
|
|
122
|
+
tasks_done: num(r.tasks_done),
|
|
123
|
+
tasks_total: num(r.tasks_total),
|
|
124
|
+
lane: text(r.lane) || 'direct',
|
|
125
|
+
next_action: text(r.next_action),
|
|
126
|
+
next_session_prompt: text(r.next_session_prompt),
|
|
127
|
+
updated_at: text(r.updated_at),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function readState(projectDir: string): StateEntry[] {
|
|
132
|
+
return scan(projectDir, 'state', (r, member) => {
|
|
133
|
+
const tasks = (r.tasks ?? {}) as Record<string, unknown>;
|
|
134
|
+
return {
|
|
135
|
+
mission: text(r.mission),
|
|
136
|
+
member,
|
|
137
|
+
actor: text(r.actor),
|
|
138
|
+
branch: text(r.branch),
|
|
139
|
+
flow: num(r.flow ?? r.wave),
|
|
140
|
+
mode: text(r.mode) || 'guided',
|
|
141
|
+
tasks_done: num(tasks.done),
|
|
142
|
+
tasks_total: num(tasks.total),
|
|
143
|
+
lane: text(r.lane) || 'direct',
|
|
144
|
+
lane_reason: text(r.lane_reason),
|
|
145
|
+
lane_rose: r.lane_rose === true,
|
|
146
|
+
lane_prev: text(r.lane_prev),
|
|
147
|
+
lane_peak: text(r.lane_peak),
|
|
148
|
+
next_action: '',
|
|
149
|
+
next_session_prompt: '',
|
|
150
|
+
updated_at: text(r.updated_at),
|
|
151
|
+
blockers_open: num(r.blockers_open),
|
|
152
|
+
heal_cycle: num(r.heal_cycle),
|
|
153
|
+
heal_max_cycles: num(r.heal_max_cycles) || 3,
|
|
154
|
+
heal_halt: r.heal_halt === true,
|
|
155
|
+
delegate_threshold: num(r.delegate_threshold) || 60,
|
|
156
|
+
delegate_due: r.delegate_due === true,
|
|
157
|
+
tokens_est: num(r.tokens_est),
|
|
158
|
+
budget: num(r.budget),
|
|
159
|
+
budget_status: text(r.budget_status) || 'ok',
|
|
160
|
+
files_touched: num(r.files_touched),
|
|
161
|
+
evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : [],
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type Resolution =
|
|
167
|
+
| { kind: 'none' }
|
|
168
|
+
| { kind: 'missions'; entries: ContinueEntry[] }
|
|
169
|
+
| { kind: 'members'; mission: string; entries: ContinueEntry[] }
|
|
170
|
+
| { kind: 'unknown-mission'; mission: string; known: string[] }
|
|
171
|
+
| { kind: 'unknown-member'; mission: string; member: string; known: string[] }
|
|
172
|
+
| { kind: 'resume'; entry: ContinueEntry };
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The three command forms, decided from disk alone:
|
|
176
|
+
* (none) → list every in-flight mission
|
|
177
|
+
* <mission> → solo resumes; team lists its members
|
|
178
|
+
* <mission> <member>→ resume that member
|
|
179
|
+
*
|
|
180
|
+
* Solo-vs-team needs no plan-doc parse: savepoint writes `state.json` with a
|
|
181
|
+
* null member for solo and `<member>.json` per team member.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveContinue(entries: ContinueEntry[], mission?: string, member?: string): Resolution {
|
|
184
|
+
if (!entries.length) return { kind: 'none' };
|
|
185
|
+
|
|
186
|
+
if (!mission) {
|
|
187
|
+
const missions = [...new Set(entries.map((e) => e.mission))];
|
|
188
|
+
if (missions.length === 1) return resolveContinue(entries, missions[0], member);
|
|
189
|
+
return { kind: 'missions', entries };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const inMission = entries.filter((e) => e.mission === mission);
|
|
193
|
+
if (!inMission.length) {
|
|
194
|
+
return { kind: 'unknown-mission', mission, known: [...new Set(entries.map((e) => e.mission))] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (member) {
|
|
198
|
+
const hit = inMission.find((e) => e.member === member);
|
|
199
|
+
if (!hit) {
|
|
200
|
+
return {
|
|
201
|
+
kind: 'unknown-member',
|
|
202
|
+
mission,
|
|
203
|
+
member,
|
|
204
|
+
known: inMission.map((e) => e.member ?? '(solo)'),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return { kind: 'resume', entry: hit };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const solo = inMission.find((e) => e.member === null);
|
|
211
|
+
// a solo file resumes directly; anything else needs the member picked, and
|
|
212
|
+
// is never guessed — resuming the wrong member's work is unrecoverable
|
|
213
|
+
if (solo && inMission.length === 1) return { kind: 'resume', entry: solo };
|
|
214
|
+
return { kind: 'members', mission, entries: inMission };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function pad(s: string, n: number): string {
|
|
218
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Fixed-width table — the whole point is that the host prints this verbatim. */
|
|
222
|
+
export function formatTable(entries: ContinueEntry[]): string {
|
|
223
|
+
const rows = entries.map((e) => [
|
|
224
|
+
e.mission,
|
|
225
|
+
e.member ?? '—',
|
|
226
|
+
String(e.flow),
|
|
227
|
+
`${e.tasks_done}/${e.tasks_total}`,
|
|
228
|
+
e.lane,
|
|
229
|
+
e.mode,
|
|
230
|
+
]);
|
|
231
|
+
const head = ['MISSION', 'MEMBER', 'FLOW', 'TASKS', 'LANE', 'MODE'];
|
|
232
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
233
|
+
const line = (cells: string[]): string =>
|
|
234
|
+
' ' + cells.map((c, i) => pad(c, widths[i])).join(' ').trimEnd();
|
|
235
|
+
return [line(head), ...rows.map(line)].join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The single resume line the mugiwara-resume skill contracts for. */
|
|
239
|
+
export function formatResume(e: ContinueEntry): string {
|
|
240
|
+
const scope = e.member ? ` [${e.member}]` : '';
|
|
241
|
+
const next = e.next_session_prompt || '(no next_session_prompt recorded)';
|
|
242
|
+
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
243
|
+
}
|
package/src/installer.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface Target {
|
|
|
44
44
|
transformSkillFull?(data: FrontmatterData, body: string): TransformOut | null;
|
|
45
45
|
transformAgentFull?(data: FrontmatterData, body: string): TransformOut | null;
|
|
46
46
|
postInstall?(opts: { scope: Scope; projectDir: string; home: string; dryRun: boolean; files: string[] }): { written: string[]; notes: string[] };
|
|
47
|
+
/**
|
|
48
|
+
* Undo anything postInstall did to a file mugiwara does not own.
|
|
49
|
+
*
|
|
50
|
+
* Files listed in the manifest are deleted wholesale on uninstall, so a
|
|
51
|
+
* shared file we merely EDITED (settings.json) must never be reported as
|
|
52
|
+
* written — deleting it destroys the user's own configuration. Such files are
|
|
53
|
+
* un-merged here instead.
|
|
54
|
+
*/
|
|
55
|
+
postUninstall?(opts: { scope: Scope; projectDir: string; home: string; dryRun: boolean }): { changed: string[]; notes: string[] };
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'content');
|
|
@@ -151,6 +160,36 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
151
160
|
result.written.push(...post.written);
|
|
152
161
|
result.notes.push(...post.notes);
|
|
153
162
|
}
|
|
163
|
+
|
|
164
|
+
// TASK 8: a fresh install must be immediately usable — write a default
|
|
165
|
+
// .mugiwara/config (the same defaults `mugiwara onboard` would write) so no
|
|
166
|
+
// key silently falls back. Only for project scope; global installs don't own
|
|
167
|
+
// a project config. Never overwrite an existing config.
|
|
168
|
+
if (scope === 'project') {
|
|
169
|
+
const configPath = join(projectDir, '.mugiwara', 'config');
|
|
170
|
+
// lstat, not existsSync: a pre-created symlinked config must not be
|
|
171
|
+
// followed and overwritten (TOCTOU / symlink defense-in-depth).
|
|
172
|
+
let configExists = false;
|
|
173
|
+
try { configExists = lstatSync(configPath).isFile() || lstatSync(configPath).isSymbolicLink(); } catch { configExists = false; }
|
|
174
|
+
if (!configExists) {
|
|
175
|
+
const body = [
|
|
176
|
+
'mode=guided',
|
|
177
|
+
'branch=feature/{type}-{issue}-{slug}',
|
|
178
|
+
'commit=conventional',
|
|
179
|
+
'auto_commit=on',
|
|
180
|
+
'coverage_new=90',
|
|
181
|
+
'coverage_modified=80',
|
|
182
|
+
'review_depth=full',
|
|
183
|
+
'quality_depth=full',
|
|
184
|
+
'delegate_threshold=60',
|
|
185
|
+
'heal_max_cycles=3',
|
|
186
|
+
'verbosity=normal',
|
|
187
|
+
].join('\n') + '\n';
|
|
188
|
+
if (!dryRun) { mkdirSync(dirname(configPath), { recursive: true }); writeFileSync(configPath, body); }
|
|
189
|
+
result.written.push(configPath);
|
|
190
|
+
result.notes.push(`default config written: ${configPath} (run \`mugiwara onboard\` to customise)`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
154
193
|
return result;
|
|
155
194
|
}
|
|
156
195
|
|