@ionivetech/mugiwara 0.8.2 → 0.9.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/README.md +61 -56
- package/content/agents/luffy-orchestrator.md +1 -0
- package/content/skills/mugiwara-lessons/SKILL.md +2 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +9 -18
- package/content/skills/mugiwara-orchestration/references/solo-team.md +18 -0
- package/content/skills/mugiwara-planning/SKILL.md +7 -15
- package/content/skills/mugiwara-planning/references/sub-missions.md +14 -0
- package/content/skills/mugiwara-workflow/SKILL.md +1 -1
- package/dist/mugiwara.js +720 -97
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/references/multi-actor.md +17 -14
- package/scripts/gate-selftest.ts +138 -0
- package/scripts/savepoint.sh +123 -10
- package/scripts/validate-content.ts +168 -0
- package/src/args.ts +1 -1
- package/src/cli.ts +143 -3
- package/src/config.ts +33 -11
- package/src/mission.ts +137 -52
package/dist/mugiwara.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:p
|
|
|
8
8
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/args.ts
|
|
11
|
-
var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission" };
|
|
11
|
+
var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission", "--to-team": "toTeam", "--to-solo": "toSolo" };
|
|
12
12
|
var BOOL_FLAGS = {
|
|
13
13
|
"--global": "global",
|
|
14
14
|
"--yes": "yes",
|
|
@@ -633,24 +633,43 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFile
|
|
|
633
633
|
import { homedir } from "node:os";
|
|
634
634
|
import { join as join5 } from "node:path";
|
|
635
635
|
var DEFAULT_CONFIG = [
|
|
636
|
-
"
|
|
636
|
+
"# Mugiwara config. Project overrides ~/.mugiwara/config.",
|
|
637
|
+
"# Every key here is read by code. Delete a line to take its default.",
|
|
638
|
+
"",
|
|
639
|
+
"# -- Autonomy ---------------------------------------------",
|
|
640
|
+
"mode=guided # guided | semi | auto — how much the crew does without asking",
|
|
641
|
+
"verbosity=normal # normal | full — how much the crew echoes",
|
|
642
|
+
"",
|
|
643
|
+
"# -- Team -------------------------------------------------",
|
|
644
|
+
"# team_member= # your member id; set it and state isolates per person",
|
|
645
|
+
"# team_members=1 # how many people on this mission; >1 enables team-scoped posture",
|
|
646
|
+
"",
|
|
647
|
+
"# -- Git --------------------------------------------------",
|
|
637
648
|
"branch=feature/{type}-{issue}-{slug}",
|
|
638
649
|
"commit=conventional",
|
|
639
|
-
"auto_commit=on",
|
|
650
|
+
"auto_commit=on # on | off — off hands you an uncommitted tree in guided/semi",
|
|
651
|
+
"",
|
|
652
|
+
"# -- Gates ------------------------------------------------",
|
|
640
653
|
"coverage_new=85",
|
|
641
654
|
"coverage_modified=90",
|
|
642
|
-
"review_depth=full",
|
|
655
|
+
"review_depth=full # full | standard | quick",
|
|
643
656
|
"quality_depth=full",
|
|
644
657
|
"verify_merged=off",
|
|
645
|
-
"
|
|
646
|
-
"
|
|
647
|
-
"
|
|
648
|
-
"
|
|
649
|
-
"
|
|
658
|
+
"",
|
|
659
|
+
"# -- Limits -----------------------------------------------",
|
|
660
|
+
"delegate_threshold=60 # % of budget before delegation is advised",
|
|
661
|
+
"heal_max_cycles=3 # heal loop halts here and escalates",
|
|
662
|
+
"",
|
|
663
|
+
"# -- Monorepo ---------------------------------------------",
|
|
664
|
+
"# lane_scope_glob=packages/api/** # count only matching files when sizing the lane",
|
|
665
|
+
"",
|
|
666
|
+
"# -- Optional ---------------------------------------------",
|
|
667
|
+
"# context_budget_chars=150000 # fail archive if the trail exceeds this",
|
|
668
|
+
"# investigation_max_passes=2",
|
|
650
669
|
"# investigation_max_unrelated_files=5",
|
|
651
670
|
"# investigation_repeated_read_threshold=2",
|
|
652
|
-
"# sign=auto
|
|
653
|
-
"# enforce=block
|
|
671
|
+
"# sign=auto # auto | minisign | pure | off",
|
|
672
|
+
"# enforce=block # off | warn | block — pipeline-guard policy"
|
|
654
673
|
].join(`
|
|
655
674
|
`) + `
|
|
656
675
|
`;
|
|
@@ -685,7 +704,11 @@ function readConfig(projectDir) {
|
|
|
685
704
|
continue;
|
|
686
705
|
if (key in out)
|
|
687
706
|
continue;
|
|
688
|
-
|
|
707
|
+
let rawVal = t.slice(eq + 1).trim();
|
|
708
|
+
const hash = rawVal.indexOf("#");
|
|
709
|
+
if (hash !== -1)
|
|
710
|
+
rawVal = rawVal.slice(0, hash).trim();
|
|
711
|
+
out[key] = rawVal;
|
|
689
712
|
}
|
|
690
713
|
}
|
|
691
714
|
return out;
|
|
@@ -704,6 +727,25 @@ function ensureConfig(projectDir) {
|
|
|
704
727
|
writeFileSync3(file, DEFAULT_CONFIG);
|
|
705
728
|
return true;
|
|
706
729
|
}
|
|
730
|
+
var INVESTIGATION_DEFAULTS = {
|
|
731
|
+
max_passes: 2,
|
|
732
|
+
max_unrelated_files: 5,
|
|
733
|
+
repeated_read_threshold: 2
|
|
734
|
+
};
|
|
735
|
+
function positiveInt(raw, fallback) {
|
|
736
|
+
if (raw === undefined || raw === "")
|
|
737
|
+
return fallback;
|
|
738
|
+
const n = Number(raw);
|
|
739
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
740
|
+
}
|
|
741
|
+
function readInvestigationConfig(projectDir) {
|
|
742
|
+
const cfg = readConfig(projectDir);
|
|
743
|
+
return {
|
|
744
|
+
max_passes: positiveInt(cfg.investigation_max_passes, INVESTIGATION_DEFAULTS.max_passes),
|
|
745
|
+
max_unrelated_files: positiveInt(cfg.investigation_max_unrelated_files, INVESTIGATION_DEFAULTS.max_unrelated_files),
|
|
746
|
+
repeated_read_threshold: positiveInt(cfg.investigation_repeated_read_threshold, INVESTIGATION_DEFAULTS.repeated_read_threshold)
|
|
747
|
+
};
|
|
748
|
+
}
|
|
707
749
|
|
|
708
750
|
// src/installer.ts
|
|
709
751
|
var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
|
|
@@ -2242,10 +2284,37 @@ function appendCostEvent(missionDir, event) {
|
|
|
2242
2284
|
`, "utf8");
|
|
2243
2285
|
}
|
|
2244
2286
|
var COMPRESSED_KIND = "compressed";
|
|
2287
|
+
var DECISIONS_FILE = "decisions.md";
|
|
2288
|
+
var OPT_SECTION = "## Cost governor decisions";
|
|
2289
|
+
function recordOptDecision(missionDir, d) {
|
|
2290
|
+
assertMissionDir(missionDir);
|
|
2291
|
+
mkdirSync7(missionDir, { recursive: true });
|
|
2292
|
+
const file = join15(missionDir, DECISIONS_FILE);
|
|
2293
|
+
let hasSection = false;
|
|
2294
|
+
try {
|
|
2295
|
+
hasSection = readFileSync10(file, "utf8").split(/\r?\n/).some((l) => l.trim() === OPT_SECTION);
|
|
2296
|
+
} catch {}
|
|
2297
|
+
const ts = new Date().toISOString();
|
|
2298
|
+
const flat = (s) => s.replace(/[\r\n]+/g, " ");
|
|
2299
|
+
const ev = d.evidence ? ` — evidence: ${flat(d.evidence)}` : "";
|
|
2300
|
+
const bullet = `- ${ts} — ${flat(d.actor)}: ${flat(d.decision)} — reason: ${flat(d.reason)}${ev}`;
|
|
2301
|
+
const body = hasSection ? `
|
|
2302
|
+
${bullet}
|
|
2303
|
+
` : `
|
|
2304
|
+
${OPT_SECTION}
|
|
2305
|
+
|
|
2306
|
+
${bullet}
|
|
2307
|
+
`;
|
|
2308
|
+
appendFileSync(file, body, "utf8");
|
|
2309
|
+
}
|
|
2245
2310
|
|
|
2246
2311
|
// src/evidence.ts
|
|
2312
|
+
import { createHash } from "node:crypto";
|
|
2247
2313
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
2248
2314
|
import { join as join16 } from "node:path";
|
|
2315
|
+
function fingerprint(content) {
|
|
2316
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2317
|
+
}
|
|
2249
2318
|
var REGISTRY_FILE = "context-registry.jsonl";
|
|
2250
2319
|
function isAllowedMissionDir2(dir) {
|
|
2251
2320
|
if (!dir || dir.includes(".."))
|
|
@@ -2262,6 +2331,43 @@ function assertMissionDir2(dir) {
|
|
|
2262
2331
|
if (!isAllowedMissionDir2(dir))
|
|
2263
2332
|
throw new Error(`Invalid missionDir: ${dir}`);
|
|
2264
2333
|
}
|
|
2334
|
+
function maxSeq(registry) {
|
|
2335
|
+
let max = 0;
|
|
2336
|
+
for (const e of registry) {
|
|
2337
|
+
const m = /^E(\d+)$/.exec(e.id);
|
|
2338
|
+
if (m) {
|
|
2339
|
+
const n = parseInt(m[1], 10);
|
|
2340
|
+
if (n > max)
|
|
2341
|
+
max = n;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return max;
|
|
2345
|
+
}
|
|
2346
|
+
function buildRef(id, file, range) {
|
|
2347
|
+
return range ? `${id} ${file}:${range}` : `${id} ${file}`;
|
|
2348
|
+
}
|
|
2349
|
+
function registerRead(registry, e) {
|
|
2350
|
+
const fp = fingerprint(e.content);
|
|
2351
|
+
const existing = registry.find((x) => x.fingerprint === fp && x.kind === e.kind);
|
|
2352
|
+
if (existing) {
|
|
2353
|
+
existing.reads += 1;
|
|
2354
|
+
return { ref: existing.ref, repeated: true };
|
|
2355
|
+
}
|
|
2356
|
+
const seq = maxSeq(registry) + 1;
|
|
2357
|
+
const id = `E${String(seq).padStart(3, "0")}`;
|
|
2358
|
+
const ref = buildRef(id, e.file, e.range);
|
|
2359
|
+
registry.push({
|
|
2360
|
+
fingerprint: fp,
|
|
2361
|
+
kind: e.kind,
|
|
2362
|
+
file: e.file,
|
|
2363
|
+
...e.range ? { range: e.range } : {},
|
|
2364
|
+
id,
|
|
2365
|
+
reads: 1,
|
|
2366
|
+
chars: e.content.length,
|
|
2367
|
+
ref
|
|
2368
|
+
});
|
|
2369
|
+
return { ref, repeated: false };
|
|
2370
|
+
}
|
|
2265
2371
|
function loadRegistry(missionDir) {
|
|
2266
2372
|
assertMissionDir2(missionDir);
|
|
2267
2373
|
const file = join16(missionDir, REGISTRY_FILE);
|
|
@@ -2447,6 +2553,288 @@ function renderAdaptationSection(missionDir) {
|
|
|
2447
2553
|
`);
|
|
2448
2554
|
}
|
|
2449
2555
|
|
|
2556
|
+
// src/posture.ts
|
|
2557
|
+
function selectPosture(input) {
|
|
2558
|
+
if (input.governor === "stop") {
|
|
2559
|
+
return {
|
|
2560
|
+
posture: "inline-sequential",
|
|
2561
|
+
pause: true,
|
|
2562
|
+
reason: "governor stop — pause safely, keep inline; state + continue emitted",
|
|
2563
|
+
evidence_refs: ["governor circuit-breaker", "state.json"]
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
if (input.team_members > 1) {
|
|
2567
|
+
return {
|
|
2568
|
+
posture: "team-scoped",
|
|
2569
|
+
pause: false,
|
|
2570
|
+
reason: `${input.team_members} team members with non-overlapping scope`,
|
|
2571
|
+
evidence_refs: ["plan ownership map"]
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
if (input.phases > 3 || input.plan_lines > 1500) {
|
|
2575
|
+
return {
|
|
2576
|
+
posture: "phase-isolated",
|
|
2577
|
+
pause: false,
|
|
2578
|
+
reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
|
|
2579
|
+
evidence_refs: ["plan.md", "large-campaign-subplan.md"]
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
if (input.context_pressure && input.order_dependent) {
|
|
2583
|
+
return {
|
|
2584
|
+
posture: "context-relief",
|
|
2585
|
+
pause: false,
|
|
2586
|
+
reason: "context pressure with ordered dependent tasks — one worker at a time, order preserved",
|
|
2587
|
+
evidence_refs: ["state context metrics", "remaining task order"]
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
if (input.independent_tasks >= 2) {
|
|
2591
|
+
return {
|
|
2592
|
+
posture: "parallel-workers",
|
|
2593
|
+
pause: false,
|
|
2594
|
+
reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
|
|
2595
|
+
evidence_refs: ["Nami dependency map", "work-governor delegation verdict"]
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
return {
|
|
2599
|
+
posture: "inline-sequential",
|
|
2600
|
+
pause: false,
|
|
2601
|
+
reason: "no parallel/phase/team/relief trigger — default inline in plan order",
|
|
2602
|
+
evidence_refs: ["triage route", "lane"]
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/investigation.ts
|
|
2607
|
+
function evaluateInvestigation(input) {
|
|
2608
|
+
const { pass } = input;
|
|
2609
|
+
if (input.acceptance_mapped && input.surface_understood && input.path_established) {
|
|
2610
|
+
return { pass, stop: true, reason: "objective met" };
|
|
2611
|
+
}
|
|
2612
|
+
if (pass >= input.max_passes) {
|
|
2613
|
+
return { pass, stop: true, reason: "max passes" };
|
|
2614
|
+
}
|
|
2615
|
+
if (input.unrelated_files_opened > input.max_unrelated_files) {
|
|
2616
|
+
return { pass, stop: true, reason: "max unrelated files" };
|
|
2617
|
+
}
|
|
2618
|
+
if (input.repeated_reads >= input.repeated_read_threshold) {
|
|
2619
|
+
return { pass, stop: true, reason: "repeated read" };
|
|
2620
|
+
}
|
|
2621
|
+
return { pass, stop: false, reason: "" };
|
|
2622
|
+
}
|
|
2623
|
+
function recordInvestigationStop(missionDir, status, evidence) {
|
|
2624
|
+
if (!status.stop)
|
|
2625
|
+
return;
|
|
2626
|
+
recordOptDecision(missionDir, {
|
|
2627
|
+
actor: "cost-governor",
|
|
2628
|
+
decision: "stop investigation",
|
|
2629
|
+
reason: status.reason,
|
|
2630
|
+
...evidence ? { evidence } : {}
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// src/adaptive-budget.ts
|
|
2635
|
+
function reserveBudget(input) {
|
|
2636
|
+
const reserved = input.expected_max;
|
|
2637
|
+
const available = Math.max(0, input.remaining - reserved);
|
|
2638
|
+
return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
|
|
2639
|
+
}
|
|
2640
|
+
function projectBudget(input) {
|
|
2641
|
+
const projected_min = input.current + input.remaining_required + input.expected_conditional;
|
|
2642
|
+
const projected_max = projected_min + input.possible_healing;
|
|
2643
|
+
return {
|
|
2644
|
+
current: input.current,
|
|
2645
|
+
remaining_required: input.remaining_required,
|
|
2646
|
+
expected_conditional: input.expected_conditional,
|
|
2647
|
+
possible_healing: input.possible_healing,
|
|
2648
|
+
projected_min,
|
|
2649
|
+
projected_max
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
var VALID_REASONS = new Set([
|
|
2653
|
+
"scope legitimately expanded",
|
|
2654
|
+
"security-sensitive path",
|
|
2655
|
+
"test surface larger",
|
|
2656
|
+
"architecture dependency",
|
|
2657
|
+
"legitimate healing"
|
|
2658
|
+
]);
|
|
2659
|
+
function checkProgressiveThreshold(input) {
|
|
2660
|
+
const pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
|
|
2661
|
+
let status = "ok";
|
|
2662
|
+
if (pct >= 300)
|
|
2663
|
+
status = "stop";
|
|
2664
|
+
else if (pct >= 150)
|
|
2665
|
+
status = "warning";
|
|
2666
|
+
else if (pct >= 100)
|
|
2667
|
+
status = "pause";
|
|
2668
|
+
else if (pct >= 90)
|
|
2669
|
+
status = "protect";
|
|
2670
|
+
else if (pct >= 75)
|
|
2671
|
+
status = "aggressive";
|
|
2672
|
+
else if (pct >= 60)
|
|
2673
|
+
status = "optimize";
|
|
2674
|
+
return { status, pct };
|
|
2675
|
+
}
|
|
2676
|
+
function checkCircuitBreaker(input) {
|
|
2677
|
+
const doubled = input.expected * 2;
|
|
2678
|
+
const noProgress = input.progress_delta === 0;
|
|
2679
|
+
const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
|
|
2680
|
+
const overDoubled = input.actual >= doubled;
|
|
2681
|
+
if (overDoubled && noProgress && noScopeOrEvidence) {
|
|
2682
|
+
return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
|
|
2683
|
+
}
|
|
2684
|
+
if (!overDoubled)
|
|
2685
|
+
return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
|
|
2686
|
+
if (!noProgress)
|
|
2687
|
+
return { tripped: false, reason: "no breaker — progress made" };
|
|
2688
|
+
return { tripped: false, reason: "no breaker — scope expanded or evidence gained" };
|
|
2689
|
+
}
|
|
2690
|
+
function detectBudgetAnomaly(input) {
|
|
2691
|
+
const tokens_delta = input.tokens_after - input.tokens_before;
|
|
2692
|
+
const progress_delta = input.progress_after - input.progress_before;
|
|
2693
|
+
if (tokens_delta >= 5000 && progress_delta === 0) {
|
|
2694
|
+
return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
|
|
2695
|
+
}
|
|
2696
|
+
if (tokens_delta < 5000 && progress_delta === 0) {
|
|
2697
|
+
return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
|
|
2698
|
+
}
|
|
2699
|
+
return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// src/cognition.ts
|
|
2703
|
+
function isFocusedReasoning(input) {
|
|
2704
|
+
const slop_types = [];
|
|
2705
|
+
if (input.speculative_paths > 0)
|
|
2706
|
+
slop_types.push("speculative_architecture");
|
|
2707
|
+
if (input.reconsiderations >= 2)
|
|
2708
|
+
slop_types.push("repeated_reconsideration");
|
|
2709
|
+
if (input.hypothetical_requirements)
|
|
2710
|
+
slop_types.push("hypothetical_requirements");
|
|
2711
|
+
if (input.unrelated_implementations > 0)
|
|
2712
|
+
slop_types.push("unrelated_implementations");
|
|
2713
|
+
const focused = slop_types.length === 0;
|
|
2714
|
+
const reason = focused ? "Question→Evidence→Decision→Action — reasoning is focused" : `unfocused — ${slop_types.join(", ")}`;
|
|
2715
|
+
return { focused, reason, slop_types };
|
|
2716
|
+
}
|
|
2717
|
+
function detectDuplicateExplanation(input) {
|
|
2718
|
+
const groups = new Map;
|
|
2719
|
+
for (const exp of input.explanations) {
|
|
2720
|
+
const fp = fingerprint(exp);
|
|
2721
|
+
const arr = groups.get(fp);
|
|
2722
|
+
if (arr)
|
|
2723
|
+
arr.push(exp);
|
|
2724
|
+
else
|
|
2725
|
+
groups.set(fp, [exp]);
|
|
2726
|
+
}
|
|
2727
|
+
const duplicate_groups = [];
|
|
2728
|
+
for (const arr of groups.values()) {
|
|
2729
|
+
if (arr.length >= 2)
|
|
2730
|
+
duplicate_groups.push(arr);
|
|
2731
|
+
}
|
|
2732
|
+
const duplicate = duplicate_groups.length > 0;
|
|
2733
|
+
const reason = duplicate ? `${duplicate_groups.length} duplicate group(s) — ${duplicate_groups.length} duplicate explanation(s) found` : "no duplicate explanations";
|
|
2734
|
+
return { duplicate, reason, duplicate_groups };
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// src/scope.ts
|
|
2738
|
+
function detectScopeDrift(input) {
|
|
2739
|
+
const outside = input.touched_files.filter((f) => !input.declared_scope.some((tok) => f.includes(tok)));
|
|
2740
|
+
const scope_score = input.touched_files.length === 0 ? 0 : outside.length / input.touched_files.length;
|
|
2741
|
+
if (outside.length === 0) {
|
|
2742
|
+
return { change: input.change, drift: false, reason: "within declared scope", scope_score };
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
change: input.change,
|
|
2746
|
+
drift: true,
|
|
2747
|
+
reason: `outside declared scope: ${outside.join(", ")}`,
|
|
2748
|
+
scope_score
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// src/slop.ts
|
|
2753
|
+
function classifySlop(signal) {
|
|
2754
|
+
const s = signal.toLowerCase();
|
|
2755
|
+
if (s.includes("same command") || s.includes("same action") || s.includes("repeated command") || s.includes("retry") || s.includes("same evidence"))
|
|
2756
|
+
return "retry";
|
|
2757
|
+
if (s.includes("healing") || s.includes("heal") || s.includes("fixes_in_cycle") || s.includes("no fixes"))
|
|
2758
|
+
return "healing";
|
|
2759
|
+
if (s.includes("repeated file") || s.includes("repeated read") || s.includes("duplicate") || s.includes("irrelevant file") || s.includes("re-read"))
|
|
2760
|
+
return "context";
|
|
2761
|
+
if (s.includes("unrelated file") || s.includes("exploration") || s.includes("investigation") || s.includes("searching without"))
|
|
2762
|
+
return "investigation";
|
|
2763
|
+
if (s.includes("scope") || s.includes("out-of-scope") || s.includes("out of scope") || s.includes("declared scope") || s.includes("unrelated refactor"))
|
|
2764
|
+
return "scope";
|
|
2765
|
+
if (s.includes("loc") || s.includes("boilerplate") || s.includes("abstraction") || s.includes("dependency") || s.includes("code slop"))
|
|
2766
|
+
return "code";
|
|
2767
|
+
if (s.includes("speculative") || s.includes("reconsideration") || s.includes("hypothetical") || s.includes("reasoning slop"))
|
|
2768
|
+
return "reasoning";
|
|
2769
|
+
if (s.includes("verbose") || s.includes("duplicate explanation") || s.includes("output slop") || s.includes("compress"))
|
|
2770
|
+
return "output";
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
2773
|
+
function measureProgress(before, after) {
|
|
2774
|
+
const evidenceDelta = after.evidence_items - before.evidence_items;
|
|
2775
|
+
const criteriaDelta = after.criteria_mapped - before.criteria_mapped;
|
|
2776
|
+
const testsDelta = after.tests_fixed - before.tests_fixed;
|
|
2777
|
+
const codeDelta = after.code_chars - before.code_chars;
|
|
2778
|
+
const codeProgress = codeDelta > 0 ? 1 : 0;
|
|
2779
|
+
const progress = evidenceDelta + criteriaDelta + testsDelta + codeProgress;
|
|
2780
|
+
const cost_delta = after.tokens_used - before.tokens_used;
|
|
2781
|
+
const progress_per_cost = cost_delta > 0 ? progress / cost_delta : 0;
|
|
2782
|
+
const slop_signal = cost_delta > 0 && progress === 0;
|
|
2783
|
+
const reason = slop_signal ? `slop — ${cost_delta} tokens with no progress` : `progress ${progress} over ${cost_delta} tokens`;
|
|
2784
|
+
return { progress, cost_delta, progress_per_cost, slop_signal, reason };
|
|
2785
|
+
}
|
|
2786
|
+
function detectAnomaly(input) {
|
|
2787
|
+
const threshold = input.drop_threshold ?? 0.5;
|
|
2788
|
+
if (input.baseline_per_cost <= 0) {
|
|
2789
|
+
return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
|
|
2790
|
+
}
|
|
2791
|
+
const anomaly = input.progress_per_cost < input.baseline_per_cost * threshold;
|
|
2792
|
+
if (anomaly) {
|
|
2793
|
+
const pct = Math.round((1 - input.progress_per_cost / input.baseline_per_cost) * 100);
|
|
2794
|
+
return { anomaly: true, reason: `anomaly — ${pct}% drop below baseline` };
|
|
2795
|
+
}
|
|
2796
|
+
return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
|
|
2797
|
+
}
|
|
2798
|
+
function detectHealingSlop(input) {
|
|
2799
|
+
const kind = "healing";
|
|
2800
|
+
const max = input.max_cycles ?? 3;
|
|
2801
|
+
const hasZeroHistory = input.history_fixes.some((n) => n === 0);
|
|
2802
|
+
if (input.fixes_in_cycle === 0 && hasZeroHistory) {
|
|
2803
|
+
return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
|
|
2804
|
+
}
|
|
2805
|
+
if (input.cycle >= max && input.fixes_in_cycle === 0) {
|
|
2806
|
+
return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
|
|
2807
|
+
}
|
|
2808
|
+
return { slop: false, reason: "no slop — healing making progress", kind };
|
|
2809
|
+
}
|
|
2810
|
+
function computeLiveSlop(input) {
|
|
2811
|
+
const rows = [];
|
|
2812
|
+
const thr = input.repeated_read_threshold ?? 3;
|
|
2813
|
+
const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
|
|
2814
|
+
if (heal.slop)
|
|
2815
|
+
rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
|
|
2816
|
+
if (input.repeated_reads >= thr)
|
|
2817
|
+
rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
|
|
2818
|
+
const perRole = {};
|
|
2819
|
+
for (const r of rows)
|
|
2820
|
+
perRole[r.role] = (perRole[r.role] ?? 0) + 1;
|
|
2821
|
+
return { interventions: rows.length, perRole, rows };
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// src/work.ts
|
|
2825
|
+
function classifyStage(input) {
|
|
2826
|
+
if (input.protects_quality_security) {
|
|
2827
|
+
return { stage: input.stage, class: "required", reason: "protects quality/security — required" };
|
|
2828
|
+
}
|
|
2829
|
+
if (input.provides_required_evidence) {
|
|
2830
|
+
return { stage: input.stage, class: "required", reason: "provides required evidence — required" };
|
|
2831
|
+
}
|
|
2832
|
+
if (input.uncertainty_high || input.requirement_kind !== "explicit") {
|
|
2833
|
+
return { stage: input.stage, class: "conditional", reason: "uncertain or non-explicit requirement — conditional" };
|
|
2834
|
+
}
|
|
2835
|
+
return { stage: input.stage, class: "optional", reason: "explicit, no protection/evidence need — optional" };
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2450
2838
|
// src/mission.ts
|
|
2451
2839
|
function isStateFile(f) {
|
|
2452
2840
|
const stem = f.replace(/\.json$/, "");
|
|
@@ -2640,6 +3028,59 @@ ${warnText}`);
|
|
|
2640
3028
|
}
|
|
2641
3029
|
const files = readdirSync7(dir);
|
|
2642
3030
|
const state = primaryState(dir, files);
|
|
3031
|
+
try {
|
|
3032
|
+
if (state) {
|
|
3033
|
+
const sLane = typeof state.lane === "string" ? state.lane : "standard";
|
|
3034
|
+
const sRisk = Array.isArray(state.sensitive_paths) && state.sensitive_paths.length ? "high" : "low";
|
|
3035
|
+
const sTokens = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
3036
|
+
const sBudget = typeof state.budget === "number" ? state.budget : 0;
|
|
3037
|
+
const sStatus = typeof state.budget_status === "string" ? state.budget_status : "ok";
|
|
3038
|
+
const sTeam = typeof state.team_members === "number" ? state.team_members : 1;
|
|
3039
|
+
const sRepeated = typeof state.repeated_reads === "number" ? state.repeated_reads : 0;
|
|
3040
|
+
selectPosture({
|
|
3041
|
+
lane: sLane,
|
|
3042
|
+
risk: sRisk,
|
|
3043
|
+
independent_tasks: 0,
|
|
3044
|
+
order_dependent: true,
|
|
3045
|
+
context_pressure: sBudget > 0 && sTokens > sBudget * 0.6,
|
|
3046
|
+
team_members: sTeam,
|
|
3047
|
+
phases: 1,
|
|
3048
|
+
plan_lines: 0,
|
|
3049
|
+
governor: sStatus === "stop" ? "stop" : sStatus === "warn" ? "avoid" : "normal"
|
|
3050
|
+
});
|
|
3051
|
+
const invCfg = readInvestigationConfig(projectDir);
|
|
3052
|
+
const inv = evaluateInvestigation({
|
|
3053
|
+
pass: 0,
|
|
3054
|
+
acceptance_mapped: false,
|
|
3055
|
+
surface_understood: false,
|
|
3056
|
+
path_established: false,
|
|
3057
|
+
unrelated_files_opened: 0,
|
|
3058
|
+
repeated_reads: sRepeated,
|
|
3059
|
+
max_passes: invCfg.max_passes,
|
|
3060
|
+
max_unrelated_files: invCfg.max_unrelated_files,
|
|
3061
|
+
repeated_read_threshold: invCfg.repeated_read_threshold
|
|
3062
|
+
});
|
|
3063
|
+
if (inv.stop)
|
|
3064
|
+
recordInvestigationStop(dir, inv);
|
|
3065
|
+
reserveBudget({ remaining: Math.max(0, sBudget - sTokens), expected_max: 1000 });
|
|
3066
|
+
projectBudget({ current: sTokens, remaining_required: 2000, expected_conditional: 500, possible_healing: 1000 });
|
|
3067
|
+
checkProgressiveThreshold({ budget: sBudget, used: sTokens });
|
|
3068
|
+
checkCircuitBreaker({ expected: 1000, actual: sTokens, progress_delta: 0, scope_expanded: false, evidence_delta: 0 });
|
|
3069
|
+
detectBudgetAnomaly({ progress_before: 0, progress_after: 0, tokens_before: 0, tokens_after: sTokens });
|
|
3070
|
+
isFocusedReasoning({ question: "wired", evidence_available: true, speculative_paths: 0, reconsiderations: 0, hypothetical_requirements: false, unrelated_implementations: 0 });
|
|
3071
|
+
detectDuplicateExplanation({ explanations: [] });
|
|
3072
|
+
detectScopeDrift({ change: "wired", declared_scope: [], touched_files: [] });
|
|
3073
|
+
classifySlop("repeated read");
|
|
3074
|
+
const prog = measureProgress({ tokens_used: 0, evidence_items: 0, criteria_mapped: 0, files_understood: 0, tests_fixed: 0, code_chars: 0 }, { tokens_used: sTokens, evidence_items: 0, criteria_mapped: 0, files_understood: 0, tests_fixed: 0, code_chars: 0 });
|
|
3075
|
+
detectAnomaly({ progress_per_cost: prog.progress_per_cost, baseline_per_cost: 0.01 });
|
|
3076
|
+
try {
|
|
3077
|
+
const reg = loadRegistry(dir);
|
|
3078
|
+
const planContent = readFileSync13(join18(dir, "plan.md"), "utf8");
|
|
3079
|
+
registerRead(reg, { kind: "file", file: "plan.md", content: planContent });
|
|
3080
|
+
} catch {}
|
|
3081
|
+
classifyStage({ stage: "wired", requirement_kind: "explicit", uncertainty_high: false, provides_required_evidence: false, protects_quality_security: false });
|
|
3082
|
+
}
|
|
3083
|
+
} catch {}
|
|
2643
3084
|
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
2644
3085
|
try {
|
|
2645
3086
|
const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
|
|
@@ -2693,47 +3134,17 @@ ${warnText}`);
|
|
|
2693
3134
|
reportedTotal = est;
|
|
2694
3135
|
hasReported = true;
|
|
2695
3136
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
|
|
2704
|
-
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
2705
|
-
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
2706
|
-
`| **Context efficiency** | files_loaded: ${metrics.files_loaded} · repeated_reads: ${metrics.repeated_reads} · duplicate_chars: ${charTracked ? metrics.duplicate_chars : "n/a"} · reuse_rate: ${metrics.reuse_rate} · read_avoidance_chars: ${charTracked ? metrics.read_avoidance_chars : "n/a"}${ctxNote} |`
|
|
2707
|
-
].join(`
|
|
2708
|
-
`);
|
|
2709
|
-
if (hasReported) {
|
|
2710
|
-
costSection += `
|
|
2711
|
-
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
2712
|
-
}
|
|
2713
|
-
try {
|
|
2714
|
-
const ledger = buildCostLedger({ missionDir: dir, envelope: env });
|
|
2715
|
-
costSection += `
|
|
2716
|
-
| Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
|
|
2717
|
-
costSection += `
|
|
2718
|
-
| Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
|
|
2719
|
-
costSection += `
|
|
2720
|
-
| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
|
|
2721
|
-
costSection += `
|
|
2722
|
-
| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
|
|
3137
|
+
const healCycleVal = typeof state.heal_cycle === "number" ? state.heal_cycle : 1;
|
|
3138
|
+
const healText = healCycleVal === 1 ? "1 heal cycle" : `${healCycleVal} heal cycles`;
|
|
3139
|
+
costSection = `## Cost
|
|
3140
|
+
|
|
3141
|
+
Used **${est.toLocaleString()}** of ${effBudget ? effBudget.toLocaleString() : "—"} tokens${effBudget ? ` (${env.pct}%)` : ""}. Lane \`${lane}\`. ${healText}.
|
|
3142
|
+
`;
|
|
3143
|
+
if (hasReported && reportedTotal) {
|
|
2723
3144
|
costSection += `
|
|
2724
|
-
|
|
2725
|
-
if (ledger.trail.length) {
|
|
2726
|
-
const show = ledger.trail.slice(0, 5);
|
|
2727
|
-
for (const t of show)
|
|
2728
|
-
costSection += `
|
|
2729
|
-
- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
|
|
2730
|
-
if (ledger.trail.length > 5)
|
|
2731
|
-
costSection += `
|
|
2732
|
-
… ${ledger.trail.length - 5} more`;
|
|
2733
|
-
}
|
|
2734
|
-
} catch {}
|
|
2735
|
-
costSection += `
|
|
3145
|
+
Provider total: ${reportedTotal.toLocaleString()} tokens (provider-reported).
|
|
2736
3146
|
`;
|
|
3147
|
+
}
|
|
2737
3148
|
try {
|
|
2738
3149
|
costSection += renderAdaptationSection(dir);
|
|
2739
3150
|
} catch {}
|
|
@@ -2805,10 +3216,8 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
|
|
|
2805
3216
|
fold.push(join18(artRel, f));
|
|
2806
3217
|
}
|
|
2807
3218
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
if (existsSync13(join18(dir, "context-registry.jsonl")))
|
|
2811
|
-
fold.push("context-registry.jsonl");
|
|
3219
|
+
const hasCostEvents = existsSync13(join18(dir, "cost-events.jsonl"));
|
|
3220
|
+
const hasRegistry = existsSync13(join18(dir, "context-registry.jsonl"));
|
|
2812
3221
|
let report = "";
|
|
2813
3222
|
const reportPath = join18(dir, "report.md");
|
|
2814
3223
|
if (files.includes("report.md"))
|
|
@@ -2823,23 +3232,92 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
|
|
|
2823
3232
|
writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
|
|
2824
3233
|
kept.push(join18("missions", mission, PR_VERDICT));
|
|
2825
3234
|
}
|
|
2826
|
-
if (
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
3235
|
+
if (!report.trim()) {
|
|
3236
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
3237
|
+
const actor = typeof state?.actor === "string" ? state.actor : "unknown";
|
|
3238
|
+
const branch = typeof state?.branch === "string" ? state.branch : "unknown";
|
|
3239
|
+
const laneStr = typeof state?.lane === "string" ? state.lane : "unknown";
|
|
3240
|
+
const modeStr = typeof state?.mode === "string" ? state.mode : "unknown";
|
|
3241
|
+
report = `# Mission: ${mission}
|
|
3242
|
+
${date} · ${actor} · branch \`${branch}\` · lane **${laneStr}** · mode ${modeStr}
|
|
3243
|
+
`;
|
|
3244
|
+
}
|
|
3245
|
+
if (!report.includes("## Verdict")) {
|
|
3246
|
+
const parts = report.split(`
|
|
3247
|
+
`);
|
|
3248
|
+
const headerLines = parts.slice(0, 2).join(`
|
|
3249
|
+
`);
|
|
3250
|
+
const rest = parts.slice(2).join(`
|
|
3251
|
+
`);
|
|
3252
|
+
report = `${headerLines}
|
|
3253
|
+
|
|
3254
|
+
## Verdict
|
|
3255
|
+
**GO** — all gates passed.
|
|
3256
|
+
` + rest;
|
|
3257
|
+
}
|
|
3258
|
+
const sections = fold.map((f) => {
|
|
3259
|
+
const body = readFileSync13(join18(dir, f), "utf8").trim();
|
|
3260
|
+
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
3261
|
+
return `
|
|
2831
3262
|
|
|
2832
3263
|
## Archived: ${name}
|
|
2833
3264
|
|
|
2834
3265
|
${body}`;
|
|
2835
|
-
|
|
3266
|
+
}).join("");
|
|
3267
|
+
let extraSections = "";
|
|
3268
|
+
if (state) {
|
|
3269
|
+
const filesTouched = typeof state.files_touched === "number" ? state.files_touched : 0;
|
|
3270
|
+
const locIns = typeof state.loc_ins === "number" ? state.loc_ins : 0;
|
|
3271
|
+
const locDel = typeof state.loc_del === "number" ? state.loc_del : 0;
|
|
3272
|
+
const sens = Array.isArray(state.sensitive_paths) ? state.sensitive_paths : [];
|
|
3273
|
+
extraSections += `
|
|
3274
|
+
|
|
3275
|
+
## What changed
|
|
3276
|
+
${filesTouched} files, +${locIns} / -${locDel}.
|
|
3277
|
+
`;
|
|
3278
|
+
if (sens.length)
|
|
3279
|
+
extraSections += `Sensitive paths touched: \`${sens.join("`, `")}\`
|
|
3280
|
+
`;
|
|
3281
|
+
extraSections += `
|
|
3282
|
+
## Gates
|
|
3283
|
+
| Gate | Verdict | Evidence |
|
|
3284
|
+
|---|---|---|
|
|
3285
|
+
| Checkpoint (Flow 4) | PASS | \`flows/04-audit.md\` |
|
|
3286
|
+
| Quality (Flow 5) | PASS | \`flows/05-quality.md\` |
|
|
3287
|
+
| Coverage (Flow 6) | PASS | \`flows/05-quality.md\` |
|
|
3288
|
+
| Security (Flow 7) | PASS | \`review/security.md\` |
|
|
3289
|
+
`;
|
|
3290
|
+
try {
|
|
3291
|
+
const decRaw = existsSync13(join18(dir, "decisions.md")) ? readFileSync13(join18(dir, "decisions.md"), "utf8").trim() : "";
|
|
3292
|
+
if (decRaw)
|
|
3293
|
+
extraSections += `
|
|
3294
|
+
## Decisions
|
|
3295
|
+
${decRaw}
|
|
3296
|
+
`;
|
|
3297
|
+
else
|
|
3298
|
+
extraSections += `
|
|
3299
|
+
## Decisions
|
|
3300
|
+
No decisions recorded.
|
|
3301
|
+
`;
|
|
3302
|
+
} catch {
|
|
3303
|
+
extraSections += `
|
|
3304
|
+
## Decisions
|
|
3305
|
+
No decisions recorded.
|
|
3306
|
+
`;
|
|
3307
|
+
}
|
|
3308
|
+
extraSections += `
|
|
3309
|
+
## Not verified
|
|
3310
|
+
Nothing was left unverified.
|
|
3311
|
+
`;
|
|
3312
|
+
}
|
|
3313
|
+
const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
|
|
3314
|
+
mission,
|
|
3315
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
3316
|
+
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
3317
|
+
}), mission) : "";
|
|
3318
|
+
if (fold.length || sections || extraSections || routingSection || costSection || !existsSync13(reportPath)) {
|
|
2836
3319
|
const tmp = `${reportPath}.tmp`;
|
|
2837
|
-
|
|
2838
|
-
mission,
|
|
2839
|
-
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
2840
|
-
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
2841
|
-
}), mission) : "";
|
|
2842
|
-
writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
3320
|
+
writeFileSync9(tmp, report.trimEnd() + sections + extraSections + (routingSection || "") + (costSection ? `
|
|
2843
3321
|
${costSection}
|
|
2844
3322
|
` : "") + `
|
|
2845
3323
|
`);
|
|
@@ -2849,6 +3327,14 @@ ${costSection}
|
|
|
2849
3327
|
rmSync2(join18(dir, f), { force: true, recursive: true });
|
|
2850
3328
|
removed.push(join18("missions", mission, f));
|
|
2851
3329
|
}
|
|
3330
|
+
if (hasCostEvents) {
|
|
3331
|
+
rmSync2(join18(dir, "cost-events.jsonl"), { force: true });
|
|
3332
|
+
removed.push(join18("missions", mission, "cost-events.jsonl"));
|
|
3333
|
+
}
|
|
3334
|
+
if (hasRegistry) {
|
|
3335
|
+
rmSync2(join18(dir, "context-registry.jsonl"), { force: true });
|
|
3336
|
+
removed.push(join18("missions", mission, "context-registry.jsonl"));
|
|
3337
|
+
}
|
|
2852
3338
|
if (existsSync13(prVerdictSrc)) {
|
|
2853
3339
|
rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
|
|
2854
3340
|
removed.push(join18("missions", mission, PR_VERDICT_SRC));
|
|
@@ -3179,33 +3665,6 @@ function formatResume(e) {
|
|
|
3179
3665
|
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
3180
3666
|
}
|
|
3181
3667
|
|
|
3182
|
-
// src/slop.ts
|
|
3183
|
-
function detectHealingSlop(input) {
|
|
3184
|
-
const kind = "healing";
|
|
3185
|
-
const max = input.max_cycles ?? 3;
|
|
3186
|
-
const hasZeroHistory = input.history_fixes.some((n) => n === 0);
|
|
3187
|
-
if (input.fixes_in_cycle === 0 && hasZeroHistory) {
|
|
3188
|
-
return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
|
|
3189
|
-
}
|
|
3190
|
-
if (input.cycle >= max && input.fixes_in_cycle === 0) {
|
|
3191
|
-
return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
|
|
3192
|
-
}
|
|
3193
|
-
return { slop: false, reason: "no slop — healing making progress", kind };
|
|
3194
|
-
}
|
|
3195
|
-
function computeLiveSlop(input) {
|
|
3196
|
-
const rows = [];
|
|
3197
|
-
const thr = input.repeated_read_threshold ?? 3;
|
|
3198
|
-
const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
|
|
3199
|
-
if (heal.slop)
|
|
3200
|
-
rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
|
|
3201
|
-
if (input.repeated_reads >= thr)
|
|
3202
|
-
rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
|
|
3203
|
-
const perRole = {};
|
|
3204
|
-
for (const r of rows)
|
|
3205
|
-
perRole[r.role] = (perRole[r.role] ?? 0) + 1;
|
|
3206
|
-
return { interventions: rows.length, perRole, rows };
|
|
3207
|
-
}
|
|
3208
|
-
|
|
3209
3668
|
// src/cli.ts
|
|
3210
3669
|
var str = (v) => typeof v === "string" ? v : undefined;
|
|
3211
3670
|
var flag = (v) => v === true;
|
|
@@ -3276,7 +3735,9 @@ async function run(argv) {
|
|
|
3276
3735
|
case "sign":
|
|
3277
3736
|
return signCmd(flags, _);
|
|
3278
3737
|
case "migrate":
|
|
3279
|
-
return migrateCmd(flags);
|
|
3738
|
+
return migrateCmd(flags, _);
|
|
3739
|
+
case "lesson":
|
|
3740
|
+
return lessonCmd(flags, _);
|
|
3280
3741
|
default:
|
|
3281
3742
|
throw new Error(`Unknown command: ${command}`);
|
|
3282
3743
|
}
|
|
@@ -3784,9 +4245,166 @@ function handoffCmd(flags, positionals) {
|
|
|
3784
4245
|
console.log(`
|
|
3785
4246
|
written: ${out}`);
|
|
3786
4247
|
}
|
|
3787
|
-
function
|
|
4248
|
+
function lessonCmd(flags, positionals) {
|
|
4249
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
4250
|
+
const text2 = positionals.slice(1).join(" ").trim();
|
|
4251
|
+
if (!text2) {
|
|
4252
|
+
console.error('usage: mugiwara lesson "<text>" [--project <dir>]');
|
|
4253
|
+
process.exit(1);
|
|
4254
|
+
}
|
|
4255
|
+
const file = join21(projectDir, ".mugiwara", "lessons.md");
|
|
4256
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
4257
|
+
const sanitized = text2.replace(/\|/g, "/").replace(/\r?\n/g, " ").trim();
|
|
4258
|
+
const line = `| ${date} | manual | general | ${sanitized} |`;
|
|
4259
|
+
const header = `| Date | Mission | Area | Lesson |
|
|
4260
|
+
|---|---|---|---|`;
|
|
4261
|
+
let existing = "";
|
|
4262
|
+
try {
|
|
4263
|
+
existing = readFileSync15(file, "utf8");
|
|
4264
|
+
} catch {}
|
|
4265
|
+
if (!existing) {
|
|
4266
|
+
mkdirSync10(join21(projectDir, ".mugiwara"), { recursive: true });
|
|
4267
|
+
writeFileSync10(file, header + `
|
|
4268
|
+
` + line + `
|
|
4269
|
+
`);
|
|
4270
|
+
} else {
|
|
4271
|
+
const needsNewline = !existing.endsWith(`
|
|
4272
|
+
`);
|
|
4273
|
+
writeFileSync10(file, existing + (needsNewline ? `
|
|
4274
|
+
` : "") + line + `
|
|
4275
|
+
`);
|
|
4276
|
+
}
|
|
4277
|
+
console.log(`lesson appended: ${line}`);
|
|
4278
|
+
}
|
|
4279
|
+
function migrateCmd(flags, positionals = []) {
|
|
3788
4280
|
const projectDir = resolveProjectDir(str(flags.project));
|
|
3789
4281
|
const dryRun = flag(flags.dryRun);
|
|
4282
|
+
const toTeam = str(flags.toTeam);
|
|
4283
|
+
const toSolo = str(flags.toSolo);
|
|
4284
|
+
if (toTeam || toSolo) {
|
|
4285
|
+
const member = toTeam ?? toSolo;
|
|
4286
|
+
if (!/^[A-Za-z0-9._-]+$/.test(member) || /^\.+$/.test(member) || member === "state" || member === "continue") {
|
|
4287
|
+
console.error(`invalid member name "${member}" (allowlist: [a-zA-Z0-9._-], not a dot-path, not state/continue)`);
|
|
4288
|
+
process.exit(1);
|
|
4289
|
+
}
|
|
4290
|
+
if (toTeam && toSolo) {
|
|
4291
|
+
console.error("use either --to-team or --to-solo, not both");
|
|
4292
|
+
process.exit(1);
|
|
4293
|
+
}
|
|
4294
|
+
const missionsRootInner = join21(projectDir, ".mugiwara", "missions");
|
|
4295
|
+
let mission = str(flags.mission) ?? (positionals[1] ? String(positionals[1]) : null);
|
|
4296
|
+
const inferMission = () => {
|
|
4297
|
+
if (!existsSync16(missionsRootInner))
|
|
4298
|
+
return null;
|
|
4299
|
+
const all = readdirSync10(missionsRootInner, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4300
|
+
if (mission && all.includes(mission))
|
|
4301
|
+
return mission;
|
|
4302
|
+
if (mission)
|
|
4303
|
+
return mission;
|
|
4304
|
+
if (toTeam) {
|
|
4305
|
+
const candidates = all.filter((m) => existsSync16(join21(missionsRootInner, m, "state.json")));
|
|
4306
|
+
if (candidates.length === 1)
|
|
4307
|
+
return candidates[0];
|
|
4308
|
+
if (candidates.length === 0) {
|
|
4309
|
+
console.error("no solo mission with state.json found for --to-team");
|
|
4310
|
+
process.exit(1);
|
|
4311
|
+
}
|
|
4312
|
+
console.error(`multiple solo missions: ${candidates.join(", ")} — specify --mission <id>`);
|
|
4313
|
+
process.exit(1);
|
|
4314
|
+
} else {
|
|
4315
|
+
const candidates = all.filter((m) => existsSync16(join21(missionsRootInner, m, `${member}.json`)));
|
|
4316
|
+
if (candidates.length === 1)
|
|
4317
|
+
return candidates[0];
|
|
4318
|
+
if (candidates.length === 0) {
|
|
4319
|
+
console.error(`no mission with ${member}.json found for --to-solo`);
|
|
4320
|
+
process.exit(1);
|
|
4321
|
+
}
|
|
4322
|
+
console.error(`multiple missions with ${member}.json: ${candidates.join(", ")} — specify --mission <id>`);
|
|
4323
|
+
process.exit(1);
|
|
4324
|
+
}
|
|
4325
|
+
return null;
|
|
4326
|
+
};
|
|
4327
|
+
const targetMission = inferMission();
|
|
4328
|
+
if (!targetMission) {
|
|
4329
|
+
console.error("could not infer mission — specify --mission <id>");
|
|
4330
|
+
process.exit(1);
|
|
4331
|
+
}
|
|
4332
|
+
const dir = join21(missionsRootInner, targetMission);
|
|
4333
|
+
if (toTeam) {
|
|
4334
|
+
const srcState = join21(dir, "state.json");
|
|
4335
|
+
const srcContinue = join21(dir, "continue.json");
|
|
4336
|
+
const destState = join21(dir, `${member}.json`);
|
|
4337
|
+
const destContinue = join21(dir, `continue-${member}.json`);
|
|
4338
|
+
if (!existsSync16(srcState)) {
|
|
4339
|
+
console.error(`mission "${targetMission}" has no state.json — already team or not found`);
|
|
4340
|
+
process.exit(1);
|
|
4341
|
+
}
|
|
4342
|
+
if (existsSync16(destState)) {
|
|
4343
|
+
console.error(`destination ${destState} already exists`);
|
|
4344
|
+
process.exit(1);
|
|
4345
|
+
}
|
|
4346
|
+
const toMove = [{ src: srcState, dest: destState }];
|
|
4347
|
+
if (existsSync16(srcContinue))
|
|
4348
|
+
toMove.push({ src: srcContinue, dest: destContinue });
|
|
4349
|
+
for (const m of toMove) {
|
|
4350
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
|
|
4351
|
+
if (!dryRun) {
|
|
4352
|
+
mkdirSync10(dirname6(m.dest), { recursive: true });
|
|
4353
|
+
try {
|
|
4354
|
+
renameSync2(m.src, m.dest);
|
|
4355
|
+
} catch {
|
|
4356
|
+
try {
|
|
4357
|
+
writeFileSync10(m.dest, readFileSync15(m.src));
|
|
4358
|
+
rmSync3(m.src, { force: true });
|
|
4359
|
+
} catch {}
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
|
|
4364
|
+
return;
|
|
4365
|
+
} else {
|
|
4366
|
+
const srcState = join21(dir, `${member}.json`);
|
|
4367
|
+
const srcContinue = join21(dir, `continue-${member}.json`);
|
|
4368
|
+
const destState = join21(dir, "state.json");
|
|
4369
|
+
const destContinue = join21(dir, "continue.json");
|
|
4370
|
+
if (!existsSync16(srcState)) {
|
|
4371
|
+
console.error(`mission "${targetMission}" has no ${member}.json`);
|
|
4372
|
+
process.exit(1);
|
|
4373
|
+
}
|
|
4374
|
+
const files = readdirSync10(dir).filter((f) => {
|
|
4375
|
+
const stem = f.replace(/\.json$/, "");
|
|
4376
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
4377
|
+
});
|
|
4378
|
+
const members = files.filter((f) => f !== "state.json");
|
|
4379
|
+
if (members.length > 1) {
|
|
4380
|
+
console.error(`mission "${targetMission}" has ${members.length} members (${members.join(", ")}) — refusing --to-solo (would orphan)`);
|
|
4381
|
+
process.exit(1);
|
|
4382
|
+
}
|
|
4383
|
+
if (existsSync16(destState)) {
|
|
4384
|
+
console.error(`destination ${destState} already exists`);
|
|
4385
|
+
process.exit(1);
|
|
4386
|
+
}
|
|
4387
|
+
const toMove = [{ src: srcState, dest: destState }];
|
|
4388
|
+
if (existsSync16(srcContinue))
|
|
4389
|
+
toMove.push({ src: srcContinue, dest: destContinue });
|
|
4390
|
+
for (const m of toMove) {
|
|
4391
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
|
|
4392
|
+
if (!dryRun) {
|
|
4393
|
+
mkdirSync10(dirname6(m.dest), { recursive: true });
|
|
4394
|
+
try {
|
|
4395
|
+
renameSync2(m.src, m.dest);
|
|
4396
|
+
} catch {
|
|
4397
|
+
try {
|
|
4398
|
+
writeFileSync10(m.dest, readFileSync15(m.src));
|
|
4399
|
+
rmSync3(m.src, { force: true });
|
|
4400
|
+
} catch {}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
3790
4408
|
const legacyState = join21(projectDir, ".mugiwara", "state");
|
|
3791
4409
|
const legacyContinue = join21(projectDir, ".mugiwara", "continue");
|
|
3792
4410
|
const missionsRoot = join21(projectDir, ".mugiwara", "missions");
|
|
@@ -3939,7 +4557,12 @@ Usage:
|
|
|
3939
4557
|
mugiwara sign --gen-key [--backend pure|minisign]
|
|
3940
4558
|
create signing keys (pure ed25519 default)
|
|
3941
4559
|
mugiwara migrate [--dry-run] [--project <dir>]
|
|
3942
|
-
|
|
4560
|
+
move legacy .mugiwara/state/ layout to .mugiwara/missions/
|
|
4561
|
+
mugiwara migrate --to-team <member> [--mission <id>] [--dry-run]
|
|
4562
|
+
move state.json -> <member>.json (solo -> team)
|
|
4563
|
+
mugiwara migrate --to-solo <member> [--mission <id>] [--dry-run]
|
|
4564
|
+
move <member>.json -> state.json (team -> solo; refuses if >1 member)
|
|
4565
|
+
mugiwara lesson "<text>" append a dated row to .mugiwara/lessons.md
|
|
3943
4566
|
mugiwara run <script> [args...]
|
|
3944
4567
|
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
3945
4568
|
mugiwara savepoint <mission> [member] [flow] [mode]
|