@ionivetech/mugiwara 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/.opencode/mugiwara-helpers.mjs +1 -1
- package/.opencode/plugins/mugiwara.mjs +173 -1
- package/README.md +63 -58
- package/content/agents/luffy-orchestrator.md +16 -1
- package/content/agents/zoro-execution.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +1 -0
- package/content/skills/mugiwara-execution/SKILL.md +5 -5
- package/content/skills/mugiwara-gates/SKILL.md +1 -0
- package/content/skills/mugiwara-healing/SKILL.md +1 -0
- package/content/skills/mugiwara-lessons/SKILL.md +2 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +15 -20
- package/content/skills/mugiwara-orchestration/references/check-ins.md +4 -5
- package/content/skills/mugiwara-orchestration/references/output-contract.md +2 -2
- 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-quality/SKILL.md +1 -0
- package/content/skills/mugiwara-review/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +2 -2
- package/content/skills/mugiwara-ship/SKILL.md +12 -0
- package/content/skills/mugiwara-workflow/SKILL.md +3 -3
- package/dist/mugiwara.js +934 -158
- package/gemini-extension.json +1 -1
- package/hooks/engagement-marker.js +9 -1
- package/hooks/engagement-marker.ts +9 -1
- package/hooks/hooks.json +12 -0
- package/hooks/pipeline-guard.js +137 -3
- package/hooks/pipeline-guard.ts +161 -3
- package/hooks/pretool-guard.js +84 -0
- package/hooks/pretool-guard.ts +60 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/references/multi-actor.md +17 -14
- package/references/wave-banners.md +22 -27
- package/scripts/build-hooks.ts +1 -1
- package/scripts/gate-selftest.ts +480 -0
- package/scripts/savepoint.sh +139 -14
- package/scripts/validate-content.ts +345 -2
- package/scripts/write-metrics.ts +25 -1
- package/src/args.ts +1 -1
- package/src/cli.ts +158 -3
- package/src/config.ts +33 -11
- package/src/guards.ts +40 -0
- package/src/initiative.ts +174 -0
- package/src/mission.ts +137 -52
- package/src/targets/claude.ts +1 -0
|
@@ -9,13 +9,16 @@ its own file inside the mission folder, so parallel work never collides:
|
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
.mugiwara/
|
|
12
|
-
├──
|
|
13
|
-
├──
|
|
14
|
-
├──
|
|
15
|
-
├──
|
|
16
|
-
├──
|
|
17
|
-
├──
|
|
18
|
-
├──
|
|
12
|
+
├── missions/<mission>/state.json # solo mission state
|
|
13
|
+
├── missions/<mission>/<member>.json # team member state
|
|
14
|
+
├── missions/<mission>/continue.json # solo resume point (D10)
|
|
15
|
+
├── missions/<mission>/continue-<member>.json # team member resume point
|
|
16
|
+
├── missions/<mission>/plan.md # shared plan (source of truth)
|
|
17
|
+
├── missions/<mission>/flows/ # flow evidence
|
|
18
|
+
├── missions/<mission>/report.md # closure report
|
|
19
|
+
├── lessons.md # shared ledger (append-only)
|
|
20
|
+
├── config
|
|
21
|
+
└── index.md
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
Branch is an implementation detail, not an identity: a member can hold several
|
|
@@ -31,11 +34,11 @@ $ mugiwara reset
|
|
|
31
34
|
✗ Active mission for 'john'. Use --force to override.
|
|
32
35
|
```
|
|
33
36
|
|
|
34
|
-
`--force` still preserves
|
|
37
|
+
`--force` still preserves `.mugiwara/lessons.md` and `config`.
|
|
35
38
|
|
|
36
39
|
## Shared state
|
|
37
40
|
|
|
38
|
-
The lessons ledger (
|
|
41
|
+
The lessons ledger (`.mugiwara/lessons.md`) stays shared — that is the point of it.
|
|
39
42
|
All actors read and write to the same file. Append-only, never overwrite.
|
|
40
43
|
|
|
41
44
|
## Member namespacing
|
|
@@ -44,13 +47,13 @@ All actors read and write to the same file. Append-only, never overwrite.
|
|
|
44
47
|
member argument) write `state.json`; team missions write `<member>.json`:
|
|
45
48
|
|
|
46
49
|
```bash
|
|
47
|
-
mugiwara savepoint dark-mode # solo →
|
|
48
|
-
mugiwara savepoint payment-gateway john # team →
|
|
49
|
-
mugiwara savepoint payment-gateway patty #
|
|
50
|
+
mugiwara savepoint dark-mode # solo → missions/dark-mode/state.json
|
|
51
|
+
mugiwara savepoint payment-gateway john # team → missions/payment-gateway/john.json
|
|
52
|
+
mugiwara savepoint payment-gateway patty # missions/payment-gateway/patty.json
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
The resume point follows the same scoping: `
|
|
53
|
-
(team) or `
|
|
55
|
+
The resume point follows the same scoping: `missions/<mission>/continue-<member>.json`
|
|
56
|
+
(team) or `missions/<mission>/continue.json` (solo), so parallel members never
|
|
54
57
|
clobber each other's resume position.
|
|
55
58
|
|
|
56
59
|
## Resuming your own work
|
|
@@ -5,29 +5,28 @@ plugin and the opencode-target generator derive agent UI colors from the table
|
|
|
5
5
|
below — change a color HERE, never in code. This file is machine-parsed:
|
|
6
6
|
keep the table format exact (one row per agent, pipes, no extra columns).
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## The banner
|
|
9
9
|
|
|
10
|
-
Every flow stage opens with
|
|
11
|
-
with a handoff line:
|
|
10
|
+
Every flow stage opens with this line and closes with a handoff:
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
→ Flow 4 — Chopper (Checkpoint)
|
|
16
|
-
```
|
|
12
|
+
## ⚔️ Flow 3 — Zoro (Execution)
|
|
13
|
+
→ Flow 4 — Chopper
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Flow
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
Rules, all unconditional:
|
|
16
|
+
|
|
17
|
+
- A markdown heading (`## `), the crew emoji, then `Flow N — Crew (Role)`.
|
|
18
|
+
- **Never emit ANSI escapes.** The model cannot tell a terminal from a markdown
|
|
19
|
+
UI, so it must not try. Colour is applied by the harness plugin, which knows
|
|
20
|
+
the surface — see "Colour" below.
|
|
21
|
+
- Keep `Flow N —` literal: the check-in protocol reads it.
|
|
22
|
+
- Handoff is the LAST line of the stage's final response. Flow 9 closes with
|
|
23
|
+
`→ closure`.
|
|
24
|
+
|
|
25
|
+
## Colour (harness, not model)
|
|
26
|
+
|
|
27
|
+
The crew colour table below is consumed by the opencode plugin
|
|
28
|
+
(`readBannerColors()`) and by any harness that can style output. The model never
|
|
29
|
+
renders colour and never needs the hex values.
|
|
31
30
|
|
|
32
31
|
## Crew colors
|
|
33
32
|
|
|
@@ -53,12 +52,8 @@ their banners appear only when a wave or worker names them.
|
|
|
53
52
|
|
|
54
53
|
## Rules
|
|
55
54
|
|
|
56
|
-
1. Banner before EVERY flow stage; handoff after it. Main thread emits
|
|
57
|
-
2. The color comes from this table only — never invent a hex mid-mission.
|
|
58
|
-
3. One form everywhere:
|
|
59
|
-
4. Only the crew table's colors and the two SGR forms above (truecolor,
|
|
60
|
-
256-index) may appear in a banner — never other escape families (OSC,
|
|
61
|
-
title, cursor, other SGR codes). The banner is a fixed template, not a
|
|
62
|
-
formatting playground.
|
|
55
|
+
1. Banner before EVERY flow stage; handoff after it. Main thread emits `## <emoji> Flow N — Crew (Role)` FIRST line and `→ Flow N+1 — Crew` LAST line even when subagent does work — covers Flow 0 Luffy, 1 Usopp, 2 Nami, 3 Zoro, 4 Chopper, 5 Sanji, 6 Franky, 7 Robin/Jinbe, 8 Brook, 9 Luffy. No flow stage starts without its banner (orchestration red flag).
|
|
56
|
+
2. The color comes from this table only — never invent a hex mid-mission. The model never emits color itself; the harness styles the heading.
|
|
57
|
+
3. One form everywhere: heading `## <emoji> Flow N — Crew (Role)` — renders as a visual break in every markdown UI and stays readable as plain text in a terminal.
|
|
63
58
|
5. Adding a crew member? Add the row here first; the plugin and target
|
|
64
59
|
generator pick it up automatically.
|
package/scripts/build-hooks.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { dirname, join } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
14
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
-
export const HOOK_ENTRIES = ['session-start', 'mugiwara-mode-tracker', 'auto-savepoint', 'engagement-marker', 'pipeline-guard'];
|
|
15
|
+
export const HOOK_ENTRIES = ['session-start', 'mugiwara-mode-tracker', 'auto-savepoint', 'engagement-marker', 'pipeline-guard', 'pretool-guard'];
|
|
16
16
|
|
|
17
17
|
// --check verifies the committed .js files are present and current, without
|
|
18
18
|
// rewriting them. hooks.json points at the .js builds, so a missing or stale
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -830,5 +830,485 @@ console.log('\nB7 — zero evidence');
|
|
|
830
830
|
}
|
|
831
831
|
}
|
|
832
832
|
|
|
833
|
+
// --- W1: mode from config — revert to positional-only → gate fails ---
|
|
834
|
+
console.log('\nW1 — mode from config');
|
|
835
|
+
{
|
|
836
|
+
const sp = join(root, 'scripts', 'savepoint.sh');
|
|
837
|
+
const original = readFileSync(sp, 'utf8');
|
|
838
|
+
try {
|
|
839
|
+
const broken = original.replace(
|
|
840
|
+
/# mode: positional > env > project config > global config > guided[\s\S]*?MODE="\$\{MODE:-guided\}"/,
|
|
841
|
+
'MODE="${4:-${STATE_MODE:-guided}}"'
|
|
842
|
+
);
|
|
843
|
+
if (broken === original) {
|
|
844
|
+
console.error('✗ W1: mutation target not found');
|
|
845
|
+
failed++;
|
|
846
|
+
} else {
|
|
847
|
+
writeFileSync(sp, broken);
|
|
848
|
+
assert('reverted MODE → savepoint mode test fails', false, () => run('W1', 'grep -q "for _cfg in.*MUGIWARA_DIR/config" scripts/savepoint.sh'));
|
|
849
|
+
}
|
|
850
|
+
} finally {
|
|
851
|
+
writeFileSync(sp, original);
|
|
852
|
+
assert('restored → mode fallback present', true, () => run('W1-restore', 'grep -q "for _cfg in.*MUGIWARA_DIR/config" scripts/savepoint.sh'));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// --- W2: Solo-or-team section — remove → content validation fails ---
|
|
857
|
+
console.log('\nW2 — Solo-or-team section');
|
|
858
|
+
{
|
|
859
|
+
const f = join(root, 'content', 'skills', 'mugiwara-orchestration', 'SKILL.md');
|
|
860
|
+
const original = readFileSync(f, 'utf8');
|
|
861
|
+
try {
|
|
862
|
+
const broken = original.replace(/## Solo or team \(Flow 0\)[\s\S]*?## Request classifier/, '## Request classifier');
|
|
863
|
+
if (broken === original) {
|
|
864
|
+
console.error('✗ W2: mutation target not found');
|
|
865
|
+
failed++;
|
|
866
|
+
} else {
|
|
867
|
+
writeFileSync(f, broken);
|
|
868
|
+
assert('removed Solo-or-team → content validation fails', false, () => run('W2', 'grep -q "Solo or team" content/skills/mugiwara-orchestration/SKILL.md'));
|
|
869
|
+
}
|
|
870
|
+
} finally {
|
|
871
|
+
writeFileSync(f, original);
|
|
872
|
+
assert('restored → Solo-or-team present', true, () => run('W2-restore', 'grep -q "Solo or team" content/skills/mugiwara-orchestration/SKILL.md'));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// --- W4: allow layout switch without migrate → lane-integrity fails ---
|
|
877
|
+
console.log('\nW4 — layout switch guard');
|
|
878
|
+
{
|
|
879
|
+
const sp = join(root, 'scripts', 'savepoint.sh');
|
|
880
|
+
const original = readFileSync(sp, 'utf8');
|
|
881
|
+
try {
|
|
882
|
+
const broken = original.replace(/is solo \(state\.json exists\)/, 'REMOVED');
|
|
883
|
+
if (broken === original) {
|
|
884
|
+
console.error('✗ W4: mutation target not found');
|
|
885
|
+
failed++;
|
|
886
|
+
} else {
|
|
887
|
+
writeFileSync(sp, broken);
|
|
888
|
+
assert('guard removed → check fails', false, () => run('W4', 'grep -q "is solo (state.json exists)" scripts/savepoint.sh'));
|
|
889
|
+
}
|
|
890
|
+
} finally {
|
|
891
|
+
writeFileSync(sp, original);
|
|
892
|
+
assert('restored → guard present', true, () => run('W4-restore', 'grep -q "is solo (state.json exists)" scripts/savepoint.sh'));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// --- W7: remove posture import → --check-wiring fails ---
|
|
897
|
+
console.log('\nW7 — wiring gate');
|
|
898
|
+
{
|
|
899
|
+
const mf = join(root, 'src', 'mission.ts');
|
|
900
|
+
const original = readFileSync(mf, 'utf8');
|
|
901
|
+
try {
|
|
902
|
+
const broken = original.replace(/import \{ selectPosture \} from '\.\/posture\.ts';/, '// removed');
|
|
903
|
+
if (broken === original) {
|
|
904
|
+
console.error('✗ W7: mutation target not found');
|
|
905
|
+
failed++;
|
|
906
|
+
} else {
|
|
907
|
+
writeFileSync(mf, broken);
|
|
908
|
+
assert('removed posture import → --check-wiring fails', false, () => run('W7', 'bun scripts/validate-content.ts --check-wiring'));
|
|
909
|
+
}
|
|
910
|
+
} finally {
|
|
911
|
+
writeFileSync(mf, original);
|
|
912
|
+
assert('restored → wiring passes', true, () => run('W7-restore', 'bun scripts/validate-content.ts --check-wiring'));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// --- W11: delete lane_scope_glob from config.md → --check-config fails ---
|
|
917
|
+
console.log('\nW11 — config drift');
|
|
918
|
+
{
|
|
919
|
+
const cf = join(root, 'docs', 'concepts', 'config.md');
|
|
920
|
+
const original = readFileSync(cf, 'utf8');
|
|
921
|
+
try {
|
|
922
|
+
const broken = original.replace(/lane_scope_glob/g, 'LANE_REMOVED');
|
|
923
|
+
if (broken === original) {
|
|
924
|
+
console.error('✗ W11: mutation target not found');
|
|
925
|
+
failed++;
|
|
926
|
+
} else {
|
|
927
|
+
writeFileSync(cf, broken);
|
|
928
|
+
assert('removed lane_scope_glob → --check-config fails', false, () => run('W11', 'bun scripts/validate-content.ts --check-config'));
|
|
929
|
+
}
|
|
930
|
+
} finally {
|
|
931
|
+
writeFileSync(cf, original);
|
|
932
|
+
assert('restored → --check-config passes', true, () => run('W11-restore', 'bun scripts/validate-content.ts --check-config'));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// --- W15: paste raw JSONL into report.md → reporting test fails ---
|
|
937
|
+
console.log('\nW15 — report raw JSONL');
|
|
938
|
+
{
|
|
939
|
+
const mf = join(root, 'src', 'mission.ts');
|
|
940
|
+
const original = readFileSync(mf, 'utf8');
|
|
941
|
+
try {
|
|
942
|
+
const broken = original.replace(/\/\/ W15: no raw JSONL in report.*/, ' if (hasCostEvents) fold.push("cost-events.jsonl"); // W15 revert');
|
|
943
|
+
if (broken === original) {
|
|
944
|
+
console.error('✗ W15: mutation target not found');
|
|
945
|
+
failed++;
|
|
946
|
+
} else {
|
|
947
|
+
writeFileSync(mf, broken);
|
|
948
|
+
assert('reverted raw JSONL → check fails', false, () => run('W15', 'grep -q "hasCostEvents" src/mission.ts | grep -q "fold.push"'));
|
|
949
|
+
}
|
|
950
|
+
} finally {
|
|
951
|
+
writeFileSync(mf, original);
|
|
952
|
+
assert('restored → no raw JSONL', true, () => run('W15-restore', 'grep -q "no raw JSONL" src/mission.ts'));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// --- W12: point doc at state/<mission>/ → --check-doc-integrity fails ---
|
|
957
|
+
console.log('\nW12 — doc integrity stale path');
|
|
958
|
+
{
|
|
959
|
+
const ref = join(root, 'references', 'multi-actor.md');
|
|
960
|
+
const original = readFileSync(ref, 'utf8');
|
|
961
|
+
try {
|
|
962
|
+
const broken = original + '\n`state/<mission>/state.json`\n';
|
|
963
|
+
writeFileSync(ref, broken);
|
|
964
|
+
assert('stale path → --check-doc-integrity fails', false, () => run('W12', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
965
|
+
} finally {
|
|
966
|
+
writeFileSync(ref, original);
|
|
967
|
+
assert('restored → doc-integrity passes', true, () => run('W12-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// --- E1: delete ## Before you start from luffy-orchestrator.md → validate-content fails ---
|
|
972
|
+
console.log('\nE1 — luffy entry protocol');
|
|
973
|
+
{
|
|
974
|
+
const lf = join(root, 'content', 'agents', 'luffy-orchestrator.md');
|
|
975
|
+
const original = readFileSync(lf, 'utf8');
|
|
976
|
+
try {
|
|
977
|
+
const broken = original.replace('## Before you start', '## Before you begin');
|
|
978
|
+
if (broken === original) {
|
|
979
|
+
console.error('✗ E1: mutation target not found');
|
|
980
|
+
failed++;
|
|
981
|
+
} else {
|
|
982
|
+
writeFileSync(lf, broken);
|
|
983
|
+
assert('luffy section renamed → validate-content fails', false, () => run('E1', 'bun scripts/validate-content.ts'));
|
|
984
|
+
}
|
|
985
|
+
} finally {
|
|
986
|
+
writeFileSync(lf, original);
|
|
987
|
+
assert('restored → content passes', true, () => run('E1-restore', 'bun scripts/validate-content.ts'));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// --- E2: restore the luffy exemption → the gate goes blind, selftest catches it ---
|
|
992
|
+
console.log('\nE2 — luffy exemption');
|
|
993
|
+
{
|
|
994
|
+
const vf = join(root, 'scripts', 'validate-content.ts');
|
|
995
|
+
const lf = join(root, 'content', 'agents', 'luffy-orchestrator.md');
|
|
996
|
+
const vOrig = readFileSync(vf, 'utf8');
|
|
997
|
+
const lOrig = readFileSync(lf, 'utf8');
|
|
998
|
+
try {
|
|
999
|
+
const vBroken = vOrig.replace(
|
|
1000
|
+
" const name = f.replace(/\\.md$/, '');\n const text = readFileSync(join(agentDir, f), 'utf8');",
|
|
1001
|
+
" const name = f.replace(/\\.md$/, '');\n if (name === 'luffy-orchestrator') continue;\n const text = readFileSync(join(agentDir, f), 'utf8');",
|
|
1002
|
+
);
|
|
1003
|
+
const lBroken = lOrig.replace('## Before you start', '## Before you begin');
|
|
1004
|
+
if (vBroken === vOrig || lBroken === lOrig) {
|
|
1005
|
+
console.error('✗ E2: mutation target not found');
|
|
1006
|
+
failed++;
|
|
1007
|
+
} else {
|
|
1008
|
+
writeFileSync(vf, vBroken);
|
|
1009
|
+
writeFileSync(lf, lBroken);
|
|
1010
|
+
// Blindness proof: with the exemption back, a captain with no checklist
|
|
1011
|
+
// passes the gate. Demonstrating that blindness IS the passing test —
|
|
1012
|
+
// it proves the gate can go blind, which is what this mutation guards.
|
|
1013
|
+
if (run('E2', 'bun scripts/validate-content.ts')) {
|
|
1014
|
+
passed++;
|
|
1015
|
+
console.log(' ✓ E2: exemption blinds the gate — regression caught');
|
|
1016
|
+
} else {
|
|
1017
|
+
console.error('✗ E2: gate still red — exemption no longer blinds');
|
|
1018
|
+
failed++;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} finally {
|
|
1022
|
+
writeFileSync(vf, vOrig);
|
|
1023
|
+
writeFileSync(lf, lOrig);
|
|
1024
|
+
assert('restored → content passes', true, () => run('E2-restore', 'bun scripts/validate-content.ts'));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// --- E3: restore the source-only predicate → artifact case fails ---
|
|
1029
|
+
console.log('\nE3 — source-only predicate');
|
|
1030
|
+
{
|
|
1031
|
+
const gf = join(root, 'hooks', 'pipeline-guard.ts');
|
|
1032
|
+
const original = readFileSync(gf, 'utf8');
|
|
1033
|
+
try {
|
|
1034
|
+
const broken = original.replace('if (!sourceChangedNow && !artifactWorkNow()) return;', 'if (!sourceChangedNow) return;');
|
|
1035
|
+
if (broken === original) {
|
|
1036
|
+
console.error('✗ E3: mutation target not found');
|
|
1037
|
+
failed++;
|
|
1038
|
+
} else {
|
|
1039
|
+
writeFileSync(gf, broken);
|
|
1040
|
+
execSync('bun scripts/build-hooks.ts', { cwd: root, stdio: 'pipe', timeout: 120000 });
|
|
1041
|
+
assert('source-only predicate → artifact case fails', false, () => run('E3', 'bunx vitest run test/hooks.test.ts -t "artifacts written"'));
|
|
1042
|
+
}
|
|
1043
|
+
} finally {
|
|
1044
|
+
writeFileSync(gf, original);
|
|
1045
|
+
execSync('bun scripts/build-hooks.ts', { cwd: root, stdio: 'pipe', timeout: 120000 });
|
|
1046
|
+
assert('restored → artifact case passes', true, () => run('E3-restore', 'bunx vitest run test/hooks.test.ts -t "artifacts written"'));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// --- E4: remove gh pr create from the FORBIDDEN table → deny case fails ---
|
|
1051
|
+
console.log('\nE4 — forbidden table');
|
|
1052
|
+
{
|
|
1053
|
+
const gf = join(root, 'src', 'guards.ts');
|
|
1054
|
+
const original = readFileSync(gf, 'utf8');
|
|
1055
|
+
try {
|
|
1056
|
+
const broken = original.replace(" [/\\bgh\\s+pr\\s+(create|merge|ready)\\b/, 'opening or merging a PR'],\n", '');
|
|
1057
|
+
if (broken === original) {
|
|
1058
|
+
console.error('✗ E4: mutation target not found');
|
|
1059
|
+
failed++;
|
|
1060
|
+
} else {
|
|
1061
|
+
writeFileSync(gf, broken);
|
|
1062
|
+
execSync('bun scripts/build-hooks.ts', { cwd: root, stdio: 'pipe', timeout: 120000 });
|
|
1063
|
+
assert('missing pr-create row → deny case fails', false, () => run('E4', 'bunx vitest run test/hooks.test.ts -t "gh pr create"'));
|
|
1064
|
+
}
|
|
1065
|
+
} finally {
|
|
1066
|
+
writeFileSync(gf, original);
|
|
1067
|
+
execSync('bun scripts/build-hooks.ts', { cwd: root, stdio: 'pipe', timeout: 120000 });
|
|
1068
|
+
assert('restored → deny case passes', true, () => run('E4-restore', 'bunx vitest run test/hooks.test.ts -t "gh pr create"'));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// --- E5-overcorrection: block every git push → feature-push case fails ---
|
|
1073
|
+
console.log('\nE5 — over-broad push matcher');
|
|
1074
|
+
{
|
|
1075
|
+
const gf = join(root, 'src', 'guards.ts');
|
|
1076
|
+
const original = readFileSync(gf, 'utf8');
|
|
1077
|
+
try {
|
|
1078
|
+
const broken = original.replace(
|
|
1079
|
+
" [/\\bgit\\s+push\\b[^|;&]*\\b(main|master|production|release)\\b/, 'pushing to a protected branch'],",
|
|
1080
|
+
" [/\\bgit\\s+push\\b[^|;&]*\\b(main|master|production|release)\\b/, 'pushing to a protected branch'],\n [/\\bgit\\s+push\\b/, 'pushing anything'],",
|
|
1081
|
+
);
|
|
1082
|
+
if (broken === original) {
|
|
1083
|
+
console.error('✗ E5: mutation target not found');
|
|
1084
|
+
failed++;
|
|
1085
|
+
} else {
|
|
1086
|
+
writeFileSync(gf, broken);
|
|
1087
|
+
execSync('bun scripts/build-hooks.ts', { cwd: root, stdio: 'pipe', timeout: 120000 });
|
|
1088
|
+
assert('push-all matcher → feature-push case fails', false, () => run('E5', 'bunx vitest run test/hooks.test.ts -t "feature-branch push"'));
|
|
1089
|
+
}
|
|
1090
|
+
} finally {
|
|
1091
|
+
writeFileSync(gf, original);
|
|
1092
|
+
execSync('bun scripts/build-hooks.ts', { cwd: root, stdio: 'pipe', timeout: 120000 });
|
|
1093
|
+
assert('restored → feature-push passes', true, () => run('E5-restore', 'bunx vitest run test/hooks.test.ts -t "feature-branch push"'));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// --- E6: remove the harness-matrix guard row → --check-invariants fails ---
|
|
1098
|
+
console.log('\nE6 — matrix guard row');
|
|
1099
|
+
{
|
|
1100
|
+
const mf = join(root, 'docs', 'reference', 'harness-matrix.md');
|
|
1101
|
+
const original = readFileSync(mf, 'utf8');
|
|
1102
|
+
try {
|
|
1103
|
+
const broken = original.replace(/Irreversible-command guard/g, 'Irreversible-command sentry');
|
|
1104
|
+
if (broken === original) {
|
|
1105
|
+
console.error('✗ E6: mutation target not found');
|
|
1106
|
+
failed++;
|
|
1107
|
+
} else {
|
|
1108
|
+
writeFileSync(mf, broken);
|
|
1109
|
+
assert('guard row renamed → --check-invariants fails', false, () => run('E6', 'bun scripts/validate-content.ts --check-invariants'));
|
|
1110
|
+
}
|
|
1111
|
+
} finally {
|
|
1112
|
+
writeFileSync(mf, original);
|
|
1113
|
+
assert('restored → invariants pass', true, () => run('E6-restore', 'bun scripts/validate-content.ts --check-invariants'));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// --- E7: point the mirror at a missing file → mirror test fails ---
|
|
1118
|
+
console.log('\nE7 — todos mirror');
|
|
1119
|
+
{
|
|
1120
|
+
const sf = join(root, 'scripts', 'savepoint.sh');
|
|
1121
|
+
const original = readFileSync(sf, 'utf8');
|
|
1122
|
+
try {
|
|
1123
|
+
const broken = original.replace('TODOS_FILE="$MISSION_DIR/flows/todos.md"', 'TODOS_FILE="$MISSION_DIR/flows/nope.md"');
|
|
1124
|
+
if (broken === original) {
|
|
1125
|
+
console.error('✗ E7: mutation target not found');
|
|
1126
|
+
failed++;
|
|
1127
|
+
} else {
|
|
1128
|
+
writeFileSync(sf, broken);
|
|
1129
|
+
assert('missing mirror → mirror test fails', false, () => run('E7', 'bunx vitest run test/savepoint.test.ts -t "todos mirror"'));
|
|
1130
|
+
}
|
|
1131
|
+
} finally {
|
|
1132
|
+
writeFileSync(sf, original);
|
|
1133
|
+
assert('restored → mirror test passes', true, () => run('E7-restore', 'bunx vitest run test/savepoint.test.ts -t "todos mirror"'));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// --- N1 mutation: remove `case 'initiative'` → --check-doc-integrity red ---
|
|
1138
|
+
console.log('\nN1 — initiative case mutation');
|
|
1139
|
+
{
|
|
1140
|
+
const f = join(root, 'src', 'cli.ts');
|
|
1141
|
+
const original = readFileSync(f, 'utf8');
|
|
1142
|
+
try {
|
|
1143
|
+
const broken = original.replace(" case 'initiative': return initiativeCmd(flags, _);\n", '');
|
|
1144
|
+
if (broken === original) {
|
|
1145
|
+
console.error('✗ N1: mutation target not found');
|
|
1146
|
+
failed++;
|
|
1147
|
+
} else {
|
|
1148
|
+
writeFileSync(f, broken);
|
|
1149
|
+
assert('missing initiative case → doc-integrity fails', false, () => run('N1', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1150
|
+
}
|
|
1151
|
+
} finally {
|
|
1152
|
+
writeFileSync(f, original);
|
|
1153
|
+
assert('restored → doc-integrity passes', true, () => run('N1-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// --- N2 mutation: ANSI escape back into wave-banners.md → banner-format red ---
|
|
1158
|
+
console.log('\nN2 — banner-format mutation');
|
|
1159
|
+
{
|
|
1160
|
+
const f = join(root, 'references', 'wave-banners.md');
|
|
1161
|
+
const original = readFileSync(f, 'utf8');
|
|
1162
|
+
try {
|
|
1163
|
+
const broken = `${original}\nTerminal: wrap in ANSI \`\\x1b[38;2;R;G;Bm\` ... \`\\x1b[0m\`.\n`;
|
|
1164
|
+
writeFileSync(f, broken);
|
|
1165
|
+
assert('ANSI escape in prose → doc-integrity fails', false, () => run('N2', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1166
|
+
} finally {
|
|
1167
|
+
writeFileSync(f, original);
|
|
1168
|
+
assert('restored → doc-integrity passes', true, () => run('N2-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// --- N3 mutation: depth allowlist back to lean → --check-config red ---
|
|
1173
|
+
console.log('\nN3 — depth allowlist mutation');
|
|
1174
|
+
{
|
|
1175
|
+
const f = join(root, 'scripts', 'savepoint.sh');
|
|
1176
|
+
const original = readFileSync(f, 'utf8');
|
|
1177
|
+
try {
|
|
1178
|
+
const broken = original.split('in full|standard|quick').join('in full|standard|lean');
|
|
1179
|
+
if (broken === original) {
|
|
1180
|
+
console.error('✗ N3: mutation target not found');
|
|
1181
|
+
failed++;
|
|
1182
|
+
} else {
|
|
1183
|
+
writeFileSync(f, broken);
|
|
1184
|
+
assert('lean-only allowlist → check-config fails', false, () => run('N3', 'bun scripts/validate-content.ts --check-config'));
|
|
1185
|
+
}
|
|
1186
|
+
} finally {
|
|
1187
|
+
writeFileSync(f, original);
|
|
1188
|
+
assert('restored → check-config passes', true, () => run('N3-restore', 'bun scripts/validate-content.ts --check-config'));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// --- N4 mutation: documented enum value the code rejects → --check-config red ---
|
|
1193
|
+
console.log('\nN4 — documented-enum mutation');
|
|
1194
|
+
{
|
|
1195
|
+
const f = join(root, 'docs', 'concepts', 'config.md');
|
|
1196
|
+
const original = readFileSync(f, 'utf8');
|
|
1197
|
+
try {
|
|
1198
|
+
const broken = original.replace('`review_depth` | full / standard / quick |', '`review_depth` | full / standard / turbo |');
|
|
1199
|
+
if (broken === original) {
|
|
1200
|
+
console.error('✗ N4: mutation target not found');
|
|
1201
|
+
failed++;
|
|
1202
|
+
} else {
|
|
1203
|
+
writeFileSync(f, broken);
|
|
1204
|
+
assert('turbo documented → check-config fails', false, () => run('N4', 'bun scripts/validate-content.ts --check-config'));
|
|
1205
|
+
}
|
|
1206
|
+
} finally {
|
|
1207
|
+
writeFileSync(f, original);
|
|
1208
|
+
assert('restored → check-config passes', true, () => run('N4-restore', 'bun scripts/validate-content.ts --check-config'));
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// --- N5 mutation: flow-summary contract removed → content validation red ---
|
|
1213
|
+
console.log('\nN5 — flow-summary mutation');
|
|
1214
|
+
{
|
|
1215
|
+
const f = join(root, 'content', 'skills', 'mugiwara-orchestration', 'SKILL.md');
|
|
1216
|
+
const original = readFileSync(f, 'utf8');
|
|
1217
|
+
try {
|
|
1218
|
+
const broken = original.replace('## Flow summary line\n', '');
|
|
1219
|
+
if (broken === original) {
|
|
1220
|
+
console.error('✗ N5: mutation target not found');
|
|
1221
|
+
failed++;
|
|
1222
|
+
} else {
|
|
1223
|
+
writeFileSync(f, broken);
|
|
1224
|
+
assert('summary contract removed → validation fails', false, () => run('N5', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1225
|
+
}
|
|
1226
|
+
} finally {
|
|
1227
|
+
writeFileSync(f, original);
|
|
1228
|
+
assert('restored → validation passes', true, () => run('N5-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// --- N6 mutation: banner recording removed from marker → hooks banner test red ---
|
|
1233
|
+
console.log('\nN6 — banner-recording mutation');
|
|
1234
|
+
{
|
|
1235
|
+
const f = join(root, 'hooks', 'engagement-marker.ts');
|
|
1236
|
+
const original = readFileSync(f, 'utf8');
|
|
1237
|
+
try {
|
|
1238
|
+
const broken = original
|
|
1239
|
+
.replace(" if (sameSession && typeof prev.last_banner_flow === 'number') bannerFlow = prev.last_banner_flow;\n", '')
|
|
1240
|
+
.replace(" if (sameSession && typeof prev.last_banner_flow_at === 'string') bannerFlowAt = prev.last_banner_flow_at;\n", '');
|
|
1241
|
+
if (broken === original) {
|
|
1242
|
+
console.error('✗ N6: mutation target not found');
|
|
1243
|
+
failed++;
|
|
1244
|
+
} else {
|
|
1245
|
+
writeFileSync(f, broken);
|
|
1246
|
+
run('N6-build', 'bun run build-hooks');
|
|
1247
|
+
assert('recording removed → banner test fails', false, () => run('N6', 'bun test test/hooks.test.ts -t banner'));
|
|
1248
|
+
}
|
|
1249
|
+
} finally {
|
|
1250
|
+
writeFileSync(f, original);
|
|
1251
|
+
run('N6-rebuild', 'bun run build-hooks');
|
|
1252
|
+
assert('restored → banner test passes', true, () => run('N6-restore', 'bun test test/hooks.test.ts -t banner'));
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// --- N7 mutation: `mugiwara lessons import` with no CLI case → doc-integrity red ---
|
|
1257
|
+
console.log('\nN7 — lessons-import mutation');
|
|
1258
|
+
{
|
|
1259
|
+
const f = join(root, 'docs', 'adoption.md');
|
|
1260
|
+
const original = readFileSync(f, 'utf8');
|
|
1261
|
+
try {
|
|
1262
|
+
writeFileSync(f, `${original}\nImport with \`mugiwara lessons import <file>\`.\n`);
|
|
1263
|
+
assert('dangling lessons verb → doc-integrity fails', false, () => run('N7', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1264
|
+
} finally {
|
|
1265
|
+
writeFileSync(f, original);
|
|
1266
|
+
assert('restored → doc-integrity passes', true, () => run('N7-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// --- N8 mutation: `/mugiwara mode` slash form → in-session phrase check red ---
|
|
1271
|
+
console.log('\nN8 — slash-mode mutation');
|
|
1272
|
+
{
|
|
1273
|
+
const f = join(root, 'content', 'skills', 'mugiwara-workflow', 'SKILL.md');
|
|
1274
|
+
const original = readFileSync(f, 'utf8');
|
|
1275
|
+
try {
|
|
1276
|
+
const broken = original.replace('Say `mugiwara mode <guided|semi|auto>` in session', '`/mugiwara mode <guided|semi|auto>` applies');
|
|
1277
|
+
if (broken === original) {
|
|
1278
|
+
console.error('✗ N8: mutation target not found');
|
|
1279
|
+
failed++;
|
|
1280
|
+
} else {
|
|
1281
|
+
writeFileSync(f, broken);
|
|
1282
|
+
assert('slash mode phrase → doc-integrity fails', false, () => run('N8', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1283
|
+
}
|
|
1284
|
+
} finally {
|
|
1285
|
+
writeFileSync(f, original);
|
|
1286
|
+
assert('restored → doc-integrity passes', true, () => run('N8-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// --- N9 mutation: unqualified "12 platforms" → README platform-count check red ---
|
|
1291
|
+
console.log('\nN9 — platform-count mutation');
|
|
1292
|
+
{
|
|
1293
|
+
const f = join(root, 'README.md');
|
|
1294
|
+
const original = readFileSync(f, 'utf8');
|
|
1295
|
+
try {
|
|
1296
|
+
const broken = original.replace(
|
|
1297
|
+
'9 via install (Claude, opencode, Copilot, Gemini, Codex, Windsurf, Cline, Kilo, Antigravity) + 3 via marketplace manifest (Cursor, Kimi, Pi)',
|
|
1298
|
+
'Claude, opencode, Copilot, Gemini, Codex, Cursor, Kimi, Pi, and 5 more',
|
|
1299
|
+
);
|
|
1300
|
+
if (broken === original) {
|
|
1301
|
+
console.error('✗ N9: mutation target not found');
|
|
1302
|
+
failed++;
|
|
1303
|
+
} else {
|
|
1304
|
+
writeFileSync(f, broken);
|
|
1305
|
+
assert('unqualified count → doc-integrity fails', false, () => run('N9', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1306
|
+
}
|
|
1307
|
+
} finally {
|
|
1308
|
+
writeFileSync(f, original);
|
|
1309
|
+
assert('restored → doc-integrity passes', true, () => run('N9-restore', 'bun scripts/validate-content.ts --check-doc-integrity'));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
833
1313
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
834
1314
|
process.exit(failed > 0 ? 1 : 0);
|