@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionivetech/mugiwara",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
5
5
  "homepage": "https://github.com/ionivetech/mugiwara#readme",
6
6
  "repository": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mugiwara",
3
3
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
4
- "version": "0.8.2",
4
+ "version": "0.9.0",
5
5
  "author": {
6
6
  "name": "ionivetech"
7
7
  },
@@ -9,13 +9,16 @@ its own file inside the mission folder, so parallel work never collides:
9
9
 
10
10
  ```
11
11
  .mugiwara/
12
- ├── state/<mission>/state.json # solo mission state
13
- ├── state/<mission>/<member>.json # team member state
14
- ├── continue/<mission>/state.json # solo resume point (D10)
15
- ├── continue/<mission>/<member>.json # team member resume point
16
- ├── plans/<mission>.md # shared plan (source of truth)
17
- ├── reports/YYYY-MM-DD-<mission>.md
18
- ├── results/<mission>-*.md
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 `logs/lessons.md` and `config`.
37
+ `--force` still preserves `.mugiwara/lessons.md` and `config`.
35
38
 
36
39
  ## Shared state
37
40
 
38
- The lessons ledger (`logs/lessons.md`) stays shared — that is the point of it.
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 → state/dark-mode/state.json
48
- mugiwara savepoint payment-gateway john # team → state/payment-gateway/john.json
49
- mugiwara savepoint payment-gateway patty # state/payment-gateway/patty.json
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: `continue/<mission>/<member>.json`
53
- (team) or `continue/<mission>/state.json` (solo), so parallel members never
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
@@ -830,5 +830,143 @@ 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
+
833
971
  console.log(`\n${passed} passed, ${failed} failed`);
834
972
  process.exit(failed > 0 ? 1 : 0);
@@ -63,9 +63,27 @@ if [ -n "$GIT_NAME" ] && [ -n "$GIT_EMAIL" ]; then GIT_ID="$GIT_NAME <$GIT_EMAIL
63
63
 
64
64
  # --- parse mission args: <mission> [member] [wave] [mode] ---
65
65
  MISSION="${1:-${STATE_MISSION:-}}"
66
+ # member: positional > env > config team_member > empty (solo). Never derive
67
+ # silently from git identity — solo vs team is a recorded Flow 0 decision, not
68
+ # an inference. (W3)
66
69
  MEMBER="${2:-${STATE_MEMBER:-}}"
70
+ if [ -z "$MEMBER" ] && [ -f "$MUGIWARA_DIR/config" ]; then
71
+ MEMBER=$(grep -E '^team_member=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
72
+ fi
67
73
  WAVE="${3:-${STATE_WAVE:-1}}"
68
- MODE="${4:-${STATE_MODE:-guided}}"
74
+ # mode: positional > env > project config > global config > guided. The hook
75
+ # passes it positionally; direct script and CLI calls must fall back to config
76
+ # or 11 of 12 harnesses record the wrong mode. (W1)
77
+ MODE="${4:-${STATE_MODE:-}}"
78
+ if [ -z "$MODE" ]; then
79
+ for _cfg in "$MUGIWARA_DIR/config" "$HOME/.mugiwara/config"; do
80
+ [ -f "$_cfg" ] || continue
81
+ _m=$(grep -E '^mode=' "$_cfg" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
82
+ [ -n "$_m" ] && { MODE="$_m"; break; }
83
+ done
84
+ fi
85
+ MODE="${MODE:-guided}"
86
+ case "$MODE" in guided|semi|auto) ;; *) MODE="guided" ;; esac
69
87
  # Triage lane (M7): the lane Luffy assigned at Flow 0. Without it savepoint
70
88
  # recomputed the lane from file counts alone and silently discarded the
71
89
  # triage decision — a Lane 3 mission recorded itself as "direct". Explicit
@@ -91,7 +109,7 @@ esac
91
109
  # verbosity from config (project .mugiwara/config), default normal; env override
92
110
  VERBOSITY="${STATE_VERBOSITY:-normal}"
93
111
  if [ -f "$MUGIWARA_DIR/config" ]; then
94
- CFG_VERBOSITY=$(grep -E '^verbosity=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
112
+ CFG_VERBOSITY=$(grep -E '^verbosity=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
95
113
  [ -n "$CFG_VERBOSITY" ] && VERBOSITY="$CFG_VERBOSITY"
96
114
  fi
97
115
  case "$VERBOSITY" in
@@ -102,7 +120,7 @@ esac
102
120
  # heal_max_cycles from config (project .mugiwara/config), default 3; env override
103
121
  HEAL_MAX_CYCLES="${STATE_HEAL_MAX_CYCLES:-3}"
104
122
  if [ -f "$MUGIWARA_DIR/config" ]; then
105
- CFG_HEAL_MAX=$(grep -E '^heal_max_cycles=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
123
+ CFG_HEAL_MAX=$(grep -E '^heal_max_cycles=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
106
124
  [ -n "$CFG_HEAL_MAX" ] && HEAL_MAX_CYCLES="$CFG_HEAL_MAX"
107
125
  fi
108
126
  case "$HEAL_MAX_CYCLES" in
@@ -113,7 +131,7 @@ esac
113
131
  # delegate_threshold from config (project .mugiwara/config), default 60; env override
114
132
  DELEGATE_THRESHOLD="${STATE_DELEGATE_THRESHOLD:-60}"
115
133
  if [ -f "$MUGIWARA_DIR/config" ]; then
116
- CFG_DELEGATE=$(grep -E '^delegate_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
134
+ CFG_DELEGATE=$(grep -E '^delegate_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
117
135
  [ -n "$CFG_DELEGATE" ] && DELEGATE_THRESHOLD="$CFG_DELEGATE"
118
136
  fi
119
137
  case "$DELEGATE_THRESHOLD" in
@@ -183,6 +201,17 @@ else
183
201
  CONTINUE_FILE="$MISSION_DIR/continue.json"
184
202
  fi
185
203
 
204
+ # A mission is solo or team, never both. Two layouts side by side orphan one of
205
+ # them: hidden from `status`, still read by the integrity gate. (W4)
206
+ if [ -n "$MEMBER" ] && [ -f "$MISSION_DIR/state.json" ] && [ "${MUGIWARA_ALLOW_LAYOUT_SWITCH:-0}" != "1" ]; then
207
+ die "mission '$MISSION' is solo (state.json exists) — refusing to add member '$MEMBER'. Run: mugiwara migrate --to-team $MEMBER"
208
+ fi
209
+ if [ -z "$MEMBER" ] && ls "$MISSION_DIR"/*.json >/dev/null 2>&1; then
210
+ if ls "$MISSION_DIR"/*.json | grep -qv -e 'state.json' -e 'continue'; then
211
+ die "mission '$MISSION' is team — pass a member: savepoint.sh $MISSION <member> ..."
212
+ fi
213
+ fi
214
+
186
215
  [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
187
216
 
188
217
  # --- computed fields ---
@@ -192,7 +221,7 @@ HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
192
221
  # lane_scope_glob (T5): monorepo scoping — count only files matching the glob
193
222
  LANE_SCOPE_GLOB=""
194
223
  if [ -f "$MUGIWARA_DIR/config" ]; then
195
- _cfg_scope=$(grep -E '^lane_scope_glob=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
224
+ _cfg_scope=$(grep -E '^lane_scope_glob=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
196
225
  [ -n "$_cfg_scope" ] && LANE_SCOPE_GLOB="$_cfg_scope"
197
226
  fi
198
227
  # union of committed + staged + unstaged + untracked (F) — see patterns.sh
@@ -395,6 +424,16 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
395
424
  HEAL_HALT=true
396
425
  fi
397
426
 
427
+ # W10: register plan.md read so repeated_reads is not structurally zero (evidence.registerRead)
428
+ if [ -f "$MISSION_DIR/plan.md" ] && [ ! -s "$MISSION_DIR/context-registry.jsonl" ]; then
429
+ mkdir -p "$MISSION_DIR"
430
+ _plan_fp=$(node -e "const crypto=require('crypto');const fs=require('fs');try{const d=fs.readFileSync(process.argv[1],'utf8');process.stdout.write(crypto.createHash('sha256').update(d).digest('hex'))}catch(e){process.stdout.write('')}" "$MISSION_DIR/plan.md" 2>/dev/null || true)
431
+ if [ -n "$_plan_fp" ]; then
432
+ _plan_chars=$(wc -c < "$MISSION_DIR/plan.md" 2>/dev/null | tr -d ' ' || echo 0)
433
+ printf '{"fingerprint":"%s","kind":"file","file":"plan.md","id":"E001","reads":1,"chars":%s,"ref":"E001 plan.md"}\n' "$_plan_fp" "$_plan_chars" >> "$MISSION_DIR/context-registry.jsonl" 2>/dev/null || true
434
+ fi
435
+ fi
436
+
398
437
  # slop — context (repeated reads) per cost-governor §§21-24,31-32 — T5 wire all crews Luffy/Nami/Zoro/Brook
399
438
  REPEATED_READS=0
400
439
  REPEATED_THRESHOLD=3
@@ -410,11 +449,11 @@ fi
410
449
  # gates flow stage can read, not prose.
411
450
  DEPTH_REVIEW="full"; DEPTH_QUALITY="full"; DEPTH_VERIFY="off"
412
451
  if [ -f "$MUGIWARA_DIR/config" ]; then
413
- _cfg_r=$(grep -E '^review_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
452
+ _cfg_r=$(grep -E '^review_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
414
453
  [ -n "$_cfg_r" ] && DEPTH_REVIEW="$_cfg_r"
415
- _cfg_q=$(grep -E '^quality_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
454
+ _cfg_q=$(grep -E '^quality_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
416
455
  [ -n "$_cfg_q" ] && DEPTH_QUALITY="$_cfg_q"
417
- _cfg_v=$(grep -E '^verify_merged=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
456
+ _cfg_v=$(grep -E '^verify_merged=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
418
457
  [ -n "$_cfg_v" ] && DEPTH_VERIFY="$_cfg_v"
419
458
  fi
420
459
  case "$DEPTH_REVIEW" in full|standard|lean) ;; *) DEPTH_REVIEW="full" ;; esac
@@ -522,6 +561,75 @@ if [ "$BUDGET" -gt 0 ] 2>/dev/null; then
522
561
  fi
523
562
  fi
524
563
 
564
+ # team_members for posture (W5) — config key team_members, default 1
565
+ TEAM_MEMBERS=1
566
+ if [ -f "$MUGIWARA_DIR/config" ]; then
567
+ _tm=$(grep -E '^team_members=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
568
+ [ -n "$_tm" ] && TEAM_MEMBERS="$_tm"
569
+ fi
570
+ case "$TEAM_MEMBERS" in ''|*[!0-9]*) TEAM_MEMBERS=1 ;; esac
571
+ # plan metrics for posture decision (phase-isolated / parallel)
572
+ PLAN_LINES=0
573
+ PHASES=1
574
+ INDEPENDENT_TASKS=0
575
+ if [ -f "$MISSION_DIR/plan.md" ]; then
576
+ PLAN_LINES=$(wc -l < "$MISSION_DIR/plan.md" 2>/dev/null | tr -d ' ' || echo 0)
577
+ PH=$(grep -c "^## Wave" "$MISSION_DIR/plan.md" 2>/dev/null || true)
578
+ [ "$PH" -gt 0 ] 2>/dev/null && PHASES="$PH"
579
+ INDEPENDENT_TASKS=$(grep -c "\[PARALLEL\]" "$MISSION_DIR/plan.md" 2>/dev/null || true)
580
+ fi
581
+ # governor for posture
582
+ GOVERNOR="normal"
583
+ case "$STATUS" in
584
+ stop) GOVERNOR="stop" ;;
585
+ warn) GOVERNOR="avoid" ;;
586
+ *) GOVERNOR="normal" ;;
587
+ esac
588
+ CONTEXT_PRESSURE=false
589
+ if [ "$BUDGET" -gt 0 ] 2>/dev/null && [ "$TOKENS_EST" -gt $(( BUDGET * 6 / 10 )) ] 2>/dev/null; then
590
+ CONTEXT_PRESSURE=true
591
+ fi
592
+ POSTURE="inline-sequential"
593
+ POSTURE_REASON="no parallel/phase/team/relief trigger — default inline in plan order"
594
+ POSTURE_PAUSE=false
595
+ if [ "$GOVERNOR" = "stop" ]; then
596
+ POSTURE="inline-sequential"
597
+ POSTURE_REASON="governor stop — pause safely, keep inline; state + continue emitted"
598
+ POSTURE_PAUSE=true
599
+ elif [ "$TEAM_MEMBERS" -gt 1 ] 2>/dev/null; then
600
+ POSTURE="team-scoped"
601
+ POSTURE_REASON="$TEAM_MEMBERS team members with non-overlapping scope"
602
+ elif [ "$PHASES" -gt 3 ] 2>/dev/null || [ "$PLAN_LINES" -gt 1500 ] 2>/dev/null; then
603
+ POSTURE="phase-isolated"
604
+ POSTURE_REASON="large campaign — $PHASES phases / $PLAN_LINES lines"
605
+ elif [ "$CONTEXT_PRESSURE" = true ]; then
606
+ POSTURE="context-relief"
607
+ POSTURE_REASON="context pressure with ordered dependent tasks — one worker at a time, order preserved"
608
+ elif [ "$INDEPENDENT_TASKS" -ge 2 ] 2>/dev/null; then
609
+ POSTURE="parallel-workers"
610
+ POSTURE_REASON="$INDEPENDENT_TASKS independent tasks, no shared files/interfaces"
611
+ fi
612
+ # investigation config (W8) — three keys, defaults 2/5/2
613
+ INV_MAX_PASSES=2
614
+ INV_MAX_UNRELATED=5
615
+ INV_REPEATED_THRESH=2
616
+ if [ -f "$MUGIWARA_DIR/config" ]; then
617
+ _v=$(grep -E '^investigation_max_passes=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
618
+ [ -n "$_v" ] && INV_MAX_PASSES="$_v"
619
+ _v=$(grep -E '^investigation_max_unrelated_files=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
620
+ [ -n "$_v" ] && INV_MAX_UNRELATED="$_v"
621
+ _v=$(grep -E '^investigation_repeated_read_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
622
+ [ -n "$_v" ] && INV_REPEATED_THRESH="$_v"
623
+ fi
624
+ case "$INV_MAX_PASSES" in ''|*[!0-9]*) INV_MAX_PASSES=2 ;; esac
625
+ case "$INV_MAX_UNRELATED" in ''|*[!0-9]*) INV_MAX_UNRELATED=5 ;; esac
626
+ case "$INV_REPEATED_THRESH" in ''|*[!0-9]*) INV_REPEATED_THRESH=2 ;; esac
627
+ # investigation_status: simple threshold check on repeated_reads (W8/W10)
628
+ INVESTIGATION_STATUS="continue"
629
+ if [ "$REPEATED_READS" -ge "$INV_REPEATED_THRESH" ] 2>/dev/null; then
630
+ INVESTIGATION_STATUS="stop"
631
+ fi
632
+
525
633
  mkdir -p "$MISSION_DIR"
526
634
 
527
635
  node -e "
@@ -565,7 +673,12 @@ const data = {
565
673
  evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
566
674
  updated_at: process.argv[22],
567
675
  schema_version: 2,
568
- repeated_reads: parseInt(process.argv[41], 10) || 0
676
+ repeated_reads: parseInt(process.argv[41], 10) || 0,
677
+ team_members: parseInt(process.argv[42], 10) || 1,
678
+ posture: process.argv[43] || 'inline-sequential',
679
+ posture_reason: process.argv[44] || '',
680
+ posture_pause: process.argv[45] === 'true',
681
+ investigation_status: process.argv[46] || 'continue'
569
682
  };
570
683
  require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
571
684
  " \
@@ -579,7 +692,7 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
579
692
  "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
580
693
  "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
581
694
  "$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY" \
582
- "$REPEATED_READS"
695
+ "$REPEATED_READS" "$TEAM_MEMBERS" "$POSTURE" "$POSTURE_REASON" "$POSTURE_PAUSE" "$INVESTIGATION_STATUS"
583
696
 
584
697
  if [ "$LANE_ROSE" = true ]; then
585
698
  echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
@@ -414,7 +414,175 @@ if (integrityArg !== -1) {
414
414
  }
415
415
  if (!constants.includes('LANE_BASE_lean=8421')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 8421)');
416
416
  if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
417
+ // W12 stale path check: obsolete layout must not appear
418
+ const staleChecks: [string, string[]][] = [
419
+ ['state/<mission>', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
420
+ ['continue/<mission>', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
421
+ ['plans/<mission>', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
422
+ ['logs/lessons', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
423
+ ];
424
+ for (const [pat, docs] of staleChecks) {
425
+ for (const doc of docs) {
426
+ const p = join(import.meta.dirname, '..', doc);
427
+ if (existsSync(p) && readFileSync(p, 'utf8').includes(pat)) {
428
+ errors.push(`doc-integrity: ${doc} contains obsolete path "${pat}" — use missions/<mission>/ layout`);
429
+ }
430
+ }
431
+ }
432
+ // W17 metrics must come from .metrics/latest.json — check for hardcoded stale numbers not in metrics
433
+ const metricsPath2 = join(import.meta.dirname, '..', '.metrics/latest.json');
434
+ if (existsSync(metricsPath2)) {
435
+ const m2 = JSON.parse(readFileSync(metricsPath2, 'utf8'));
436
+ const readme2 = readFileSync(join(import.meta.dirname, '..', 'README.md'), 'utf8');
437
+ // ensure README rank-1 and pointers match metrics (also checked in --check-readme-metrics, but this is integrity)
438
+ const rankMatch2 = readme2.match(/Retrieval routing rank-1[^\n]*?(\d+\.\d+)%/);
439
+ if (rankMatch2 && parseFloat(rankMatch2[1]) !== Number(m2.retrieval_rank1)) {
440
+ errors.push(`doc-integrity: README rank-1 ${rankMatch2[1]}% != metrics ${m2.retrieval_rank1}%`);
441
+ }
442
+ }
443
+ // stale CLI commands: any `mugiwara <word>` where word is not a valid CLI case, appearing as code, is stale
444
+ const validCmds = new Set(['install','update','uninstall','list','reset','archive','clean','continue','status','cost','run','savepoint','blame','handoff','sign','migrate','lesson','help','version','mode','off']);
445
+ const docsToScan = ['docs/concepts/workflow.md','docs/concepts/config.md','README.md','references/multi-actor.md'];
446
+ for (const doc of docsToScan) {
447
+ const p = join(import.meta.dirname, '..', doc);
448
+ if (!existsSync(p)) continue;
449
+ const txt = readFileSync(p, 'utf8');
450
+ for (const m of txt.matchAll(/`mugiwara ([a-z-]+)/g)) {
451
+ const cmd = m[1];
452
+ if (!validCmds.has(cmd) && cmd !== '--help' && cmd !== '--version') {
453
+ errors.push(`doc-integrity: ${doc} contains stale command "mugiwara ${cmd}" not in src/cli.ts`);
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ // --- config drift gate (W11): every key code reads must appear in DEFAULT_CONFIG and docs, and vice versa ---
461
+ if (process.argv.includes('--check-config')) {
462
+ const cfgSrc = readFileSync(join(import.meta.dirname, '..', 'src/config.ts'), 'utf8');
463
+ const m = cfgSrc.match(/DEFAULT_CONFIG\s*=\s*\[([\s\S]*?)\]\.join/);
464
+ let defaultKeys: string[] = [];
465
+ if (m) {
466
+ const block = m[1];
467
+ for (const line of block.split(/\r?\n/)) {
468
+ const t = line.trim();
469
+ if (!t) continue;
470
+ // extract string content between quotes
471
+ const q = t.match(/['"`]([^'"`]*?)['"`]/);
472
+ if (!q) continue;
473
+ let s = q[1].trim();
474
+ if (!s) continue;
475
+ if (s.startsWith('#')) s = s.slice(1).trim();
476
+ if (!s) continue;
477
+ const eq = s.indexOf('=');
478
+ if (eq === -1) continue;
479
+ const key = s.slice(0, eq).trim();
480
+ if (key) defaultKeys.push(key);
481
+ }
482
+ }
483
+ // docs keys from config.md table (only the ## Keys section, not template examples)
484
+ const docPath = join(import.meta.dirname, '..', 'docs/concepts/config.md');
485
+ let docKeys: string[] = [];
486
+ if (existsSync(docPath)) {
487
+ const docText = readFileSync(docPath, 'utf8');
488
+ const keysSectionMatch = docText.match(/## Keys([\s\S]*?)(?:\n## |\n#|$)/);
489
+ const keysSection = keysSectionMatch ? keysSectionMatch[1] : docText;
490
+ for (const line of keysSection.split(/\r?\n/)) {
491
+ const cm = line.match(/\|\s*`([^`]+)`\s*\|/);
492
+ if (cm) {
493
+ const k = cm[1].trim();
494
+ if (k && !docKeys.includes(k)) docKeys.push(k);
495
+ }
496
+ }
497
+ } else {
498
+ errors.push('config-drift: docs/concepts/config.md not found');
499
+ }
500
+ // code keys: scan src/*.ts, scripts/*.sh, hooks/*.ts for key patterns
501
+ const codeRoots = [
502
+ join(import.meta.dirname, '..', 'src'),
503
+ join(import.meta.dirname, '..', 'scripts'),
504
+ join(import.meta.dirname, '..', 'hooks'),
505
+ ];
506
+ const codeTextAll = codeRoots.map(r => {
507
+ if (!existsSync(r)) return '';
508
+ const files = readdirSync(r, { withFileTypes: true }).filter(e => e.isFile() && (e.name.endsWith('.ts') || e.name.endsWith('.sh'))).map(e => readFileSync(join(r, e.name), 'utf8')).join('\n');
509
+ // also need subdirectories
510
+ let sub = '';
511
+ try {
512
+ for (const e of readdirSync(r, { withFileTypes: true })) {
513
+ if (e.isDirectory()) {
514
+ const subdir = join(r, e.name);
515
+ for (const f of readdirSync(subdir, { withFileTypes: true }).filter(x => x.isFile() && (x.name.endsWith('.ts') || x.name.endsWith('.sh')))) {
516
+ sub += readFileSync(join(subdir, f.name), 'utf8') + '\n';
517
+ }
518
+ }
519
+ }
520
+ } catch {}
521
+ return files + sub;
522
+ }).join('\n');
523
+ // check each default key appears in code
524
+ for (const k of defaultKeys) {
525
+ if (!codeTextAll.includes(k)) {
526
+ errors.push(`config-drift: DEFAULT_CONFIG key "${k}" not found in code (src/*.ts, scripts/*.sh, hooks/*.ts)`);
527
+ }
528
+ if (!docKeys.includes(k)) {
529
+ errors.push(`config-drift: DEFAULT_CONFIG key "${k}" missing from docs/concepts/config.md`);
530
+ }
531
+ }
532
+ for (const k of docKeys) {
533
+ if (!defaultKeys.includes(k)) {
534
+ errors.push(`config-drift: docs/concepts/config.md key "${k}" not in DEFAULT_CONFIG`);
535
+ }
536
+ if (!codeTextAll.includes(k)) {
537
+ errors.push(`config-drift: docs key "${k}" not found in code`);
538
+ }
539
+ }
540
+ if (!errors.some(e => e.startsWith('config-drift'))) {
541
+ console.log(`✓ config in sync: ${defaultKeys.length} keys (${defaultKeys.join(', ')})`);
542
+ }
417
543
  }
544
+
545
+ // --- wiring gate (W7): every src module must be imported somewhere ---
546
+ if (process.argv.includes('--check-wiring')) {
547
+ const srcDir = join(import.meta.dirname, '..', 'src');
548
+ const ENTRY = new Set(['cli.ts', 'index.ts', 'installer.ts']);
549
+ const srcFiles = readdirSync(srcDir).filter(n => n.endsWith('.ts'));
550
+ const searchDirs = [srcDir, join(import.meta.dirname, '..', 'hooks'), join(import.meta.dirname, '..', 'scripts')];
551
+ const allFiles: string[] = [];
552
+ for (const d of searchDirs) {
553
+ if (!existsSync(d)) continue;
554
+ const walk = (dir: string) => {
555
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
556
+ const full = join(dir, e.name);
557
+ if (e.isDirectory()) walk(full);
558
+ else if (e.isFile() && (e.name.endsWith('.ts') || e.name.endsWith('.js') || e.name.endsWith('.sh') || e.name.endsWith('.mjs'))) {
559
+ allFiles.push(full);
560
+ }
561
+ }
562
+ };
563
+ walk(d);
564
+ }
565
+ for (const f of srcFiles) {
566
+ if (ENTRY.has(f)) continue;
567
+ const stem = f.slice(0, -3);
568
+ let found = false;
569
+ for (const full of allFiles) {
570
+ if (full.endsWith(`/src/${f}`) || full === join(srcDir, f)) continue;
571
+ try {
572
+ const txt = readFileSync(full, 'utf8');
573
+ if (txt.includes(`'./${stem}`) || txt.includes(`"./${stem}`) || txt.includes(`'./${stem}.ts'`) || txt.includes(`"./${stem}.ts"`)) {
574
+ found = true;
575
+ break;
576
+ }
577
+ } catch {}
578
+ }
579
+ if (!found) {
580
+ errors.push(`src/${f}: imported by nothing — wire it, or delete it with its tests and its features.md claim`);
581
+ }
582
+ }
583
+ if (!errors.some(e => e.includes('imported by nothing'))) {
584
+ console.log(`✓ wiring: all src modules imported`);
585
+ }
418
586
  }
419
587
 
420
588
  // --- README metrics gate (D3): README table must match .metrics/latest.json ---
package/src/args.ts CHANGED
@@ -6,7 +6,7 @@ export type Args = {
6
6
  flags: Record<string, FlagValue>;
7
7
  };
8
8
 
9
- const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before', '--backend': 'backend', '--mission': 'mission' };
9
+ const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before', '--backend': 'backend', '--mission': 'mission', '--to-team': 'toTeam', '--to-solo': 'toSolo' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
12
  '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',