@ionivetech/mugiwara 0.6.5 → 0.7.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.
Files changed (135) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -9
  3. package/.codex-plugin/plugin.json +2 -8
  4. package/.cursor-plugin/plugin.json +2 -8
  5. package/.kimi-plugin/plugin.json +2 -8
  6. package/.opencode/commands/mugiwara-continue.md +44 -31
  7. package/.opencode/commands/mugiwara-review.md +1 -1
  8. package/.opencode/commands/mugiwara-security.md +1 -1
  9. package/.opencode/commands/mugiwara.md +2 -2
  10. package/.opencode/plugins/mugiwara.mjs +3 -19
  11. package/AGENTS.md +6 -6
  12. package/GEMINI.md +3 -3
  13. package/README.md +82 -74
  14. package/content/agents/brook-healing.md +12 -11
  15. package/content/agents/chopper-checkpoint.md +14 -13
  16. package/content/agents/eval-runner.md +6 -9
  17. package/content/agents/franky-gates.md +7 -10
  18. package/content/agents/jinbe-security.md +10 -14
  19. package/content/agents/luffy-orchestrator.md +20 -22
  20. package/content/agents/memory-keeper.md +9 -12
  21. package/content/agents/nami-planner.md +7 -10
  22. package/content/agents/resume-coordinator.md +12 -15
  23. package/content/agents/robin-reviewer.md +13 -12
  24. package/content/agents/sanji-quality.md +9 -12
  25. package/content/agents/skeptic-verifier.md +7 -10
  26. package/content/agents/usopp-brainstorm.md +6 -9
  27. package/content/agents/zoro-execution.md +9 -12
  28. package/content/skills/mugiwara-backend/SKILL.md +4 -0
  29. package/content/skills/mugiwara-backend/references/database.md +61 -0
  30. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  31. package/content/skills/mugiwara-checkpoint/SKILL.md +17 -14
  32. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +5 -5
  33. package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
  34. package/content/skills/mugiwara-execution/SKILL.md +24 -24
  35. package/content/skills/mugiwara-execution/references/dispatch.md +3 -3
  36. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  37. package/content/skills/mugiwara-frontend/SKILL.md +5 -6
  38. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  39. package/content/skills/mugiwara-gates/SKILL.md +14 -10
  40. package/content/skills/mugiwara-git/SKILL.md +7 -2
  41. package/content/skills/mugiwara-healing/SKILL.md +7 -5
  42. package/content/skills/mugiwara-healing/references/workers.md +2 -2
  43. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  44. package/content/skills/mugiwara-orchestration/SKILL.md +28 -26
  45. package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
  46. package/content/skills/mugiwara-orchestration/references/closure.md +18 -13
  47. package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
  48. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +32 -22
  49. package/content/skills/mugiwara-planning/SKILL.md +5 -18
  50. package/content/skills/mugiwara-planning/references/plan-template.md +3 -3
  51. package/content/skills/mugiwara-quality/SKILL.md +10 -7
  52. package/content/skills/mugiwara-resume/SKILL.md +20 -22
  53. package/content/skills/mugiwara-review/SKILL.md +7 -2
  54. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
  55. package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
  56. package/content/skills/mugiwara-security/SKILL.md +6 -2
  57. package/content/skills/mugiwara-ship/SKILL.md +2 -2
  58. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  59. package/content/skills/mugiwara-testcases/SKILL.md +5 -3
  60. package/content/skills/mugiwara-workflow/SKILL.md +32 -35
  61. package/content/skills/mugiwara-workflow/references/workspace-layout.md +41 -40
  62. package/dist/mugiwara.js +1370 -153
  63. package/gemini-extension.json +1 -1
  64. package/hooks/auto-savepoint.js +114 -0
  65. package/hooks/auto-savepoint.ts +102 -0
  66. package/hooks/engagement-marker.js +59 -0
  67. package/hooks/engagement-marker.ts +97 -0
  68. package/hooks/hooks.json +41 -2
  69. package/hooks/mugiwara-mode-tracker.js +79 -0
  70. package/hooks/pipeline-guard.js +186 -0
  71. package/hooks/pipeline-guard.ts +270 -0
  72. package/hooks/session-start.js +109 -0
  73. package/hooks/session-start.ts +62 -45
  74. package/package.json +7 -3
  75. package/plugin.json +1 -1
  76. package/references/agent-protocol.md +15 -0
  77. package/references/complexity.md +77 -0
  78. package/references/definition-of-done.md +1 -1
  79. package/references/multi-actor.md +4 -7
  80. package/references/prose-style.md +54 -0
  81. package/references/skill-versioning.md +1 -1
  82. package/references/wave-banners.md +16 -16
  83. package/scripts/build-hooks.ts +56 -0
  84. package/scripts/check-doc-links.ts +40 -0
  85. package/scripts/conformance.ts +23 -16
  86. package/scripts/coverage-gate.ts +193 -0
  87. package/scripts/gate-selftest.ts +118 -7
  88. package/scripts/install.ps1 +5 -4
  89. package/scripts/install.sh +4 -4
  90. package/scripts/lane.sh +56 -6
  91. package/scripts/lib/patterns.sh +89 -0
  92. package/scripts/policy-force.ts +22 -0
  93. package/scripts/retrieval-eval.ts +1 -1
  94. package/scripts/savepoint.sh +249 -67
  95. package/scripts/validate-content.ts +22 -9
  96. package/scripts/verify-install.ts +156 -44
  97. package/src/args.ts +2 -2
  98. package/src/budget.ts +56 -0
  99. package/src/cli.ts +272 -4
  100. package/src/continue.ts +262 -0
  101. package/src/installer.ts +59 -15
  102. package/src/integrity.ts +158 -0
  103. package/src/mission.ts +228 -95
  104. package/src/policy.ts +156 -0
  105. package/src/provenance.ts +116 -0
  106. package/src/rollback.ts +95 -0
  107. package/src/routing.ts +69 -0
  108. package/src/run.ts +82 -0
  109. package/src/sign.ts +61 -0
  110. package/src/targets/claude.ts +140 -2
  111. package/src/targets/copilot.ts +20 -1
  112. package/src/targets/generic.ts +9 -4
  113. package/src/targets/opencode.ts +0 -1
  114. package/.opencode/commands/mugiwara-execute.md +0 -13
  115. package/.opencode/commands/mugiwara-heal.md +0 -13
  116. package/.opencode/commands/mugiwara-onboard.md +0 -15
  117. package/.opencode/commands/mugiwara-plan.md +0 -17
  118. package/.opencode/commands/mugiwara-ship.md +0 -13
  119. package/.opencode/commands/using-mugiwara.md +0 -20
  120. package/content/agents/onboarding-guide.md +0 -124
  121. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  122. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  123. package/content/skills/mugiwara-context-budget/SKILL.md +0 -64
  124. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  125. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  126. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  127. package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
  128. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  129. package/content/skills/using-mugiwara/SKILL.md +0 -51
  130. package/references/token-budget.md +0 -34
  131. package/scripts/evidence.sh +0 -81
  132. package/scripts/initiative.ts +0 -296
  133. package/scripts/mission-report.sh +0 -292
  134. package/scripts/onboard.ts +0 -266
  135. package/scripts/probe.ts +0 -40
@@ -1,15 +1,27 @@
1
1
  #!/usr/bin/env bash
2
- # scripts/savepoint.sh — write .mugiwara/state/<mission>/<member>.json at every
3
- # wave boundary. Computed from git + file counts; zero model judgement.
2
+ # scripts/savepoint.sh — write .mugiwara/missions/<mission>/<member>.json at every
3
+ # flow-stage boundary. Computed from git + file counts; zero model judgement.
4
4
  # Identity is (mission, member), never branch. Solo = member empty → the file
5
5
  # is named state.json. Usage:
6
- # savepoint.sh <mission> [member] [wave] [mode]
6
+ # savepoint.sh <mission> [member] [flow] [mode] [lane]
7
7
  set -u
8
8
 
9
9
  die() { echo "savepoint: $*" >&2; exit 1; }
10
10
 
11
11
  MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
12
12
 
13
+ # optional provider-reported tokens file (T4): --tokens-file <path> JSON {input_tokens, output_tokens}
14
+ TOKENS_FILE=""
15
+ while [ $# -gt 0 ]; do
16
+ case "$1" in
17
+ --tokens-file) TOKENS_FILE="${2:-}"; [ -n "$TOKENS_FILE" ] || die "--tokens-file requires a path"; shift 2 ;;
18
+ --tokens-file=*) TOKENS_FILE="${1#*=}"; shift ;;
19
+ --) shift; break ;;
20
+ -*) die "unknown flag \"$1\"" ;;
21
+ *) break ;;
22
+ esac
23
+ done
24
+
13
25
  # shared path patterns — single source of truth (D3)
14
26
  # shellcheck source=scripts/lib/patterns.sh
15
27
  source "$(dirname "$0")/lib/patterns.sh"
@@ -42,6 +54,28 @@ MISSION="${1:-${STATE_MISSION:-}}"
42
54
  MEMBER="${2:-${STATE_MEMBER:-}}"
43
55
  WAVE="${3:-${STATE_WAVE:-1}}"
44
56
  MODE="${4:-${STATE_MODE:-guided}}"
57
+ # Triage lane (M7): the lane Luffy assigned at Flow 0. Without it savepoint
58
+ # recomputed the lane from file counts alone and silently discarded the
59
+ # triage decision — a Lane 3 mission recorded itself as "direct". Explicit
60
+ # lane acts as a FLOOR: the computed lane may still raise it, never lower it.
61
+ LANE_EXPLICIT="${5:-${STATE_LANE:-}}"
62
+ case "$LANE_EXPLICIT" in
63
+ ""|direct|lean|standard|full|spike) ;;
64
+ *) die "invalid lane \"$LANE_EXPLICIT\" (one of: direct lean standard full spike)" ;;
65
+ esac
66
+ # Wave may be fractional in the docs (4.5 = the checkpoint between waves).
67
+ # parseInt("4.5")=4 while tr -cd '0-9' gives "45", so state and continue
68
+ # disagreed on the same mission. Normalize once, here, for both writers.
69
+ # Keep only digits and the decimal point, then take the integer part. Garbage
70
+ # input still sanitizes to 0 (the N2 contract the continue writer already had);
71
+ # a fractional wave now truncates identically in BOTH writers instead of
72
+ # splitting into 4 (parseInt) and 45 (digit-strip).
73
+ WAVE_INT=$(printf '%s' "$WAVE" | tr -cd '0-9.')
74
+ WAVE_INT=${WAVE_INT%%.*}
75
+ case "$WAVE_INT" in
76
+ ''|*[!0-9]*) WAVE_INT=0 ;;
77
+ esac
78
+
45
79
  # verbosity from config (project .mugiwara/config), default normal; env override
46
80
  VERBOSITY="${STATE_VERBOSITY:-normal}"
47
81
  if [ -f "$MUGIWARA_DIR/config" ]; then
@@ -52,8 +86,37 @@ case "$VERBOSITY" in
52
86
  normal|full) ;;
53
87
  *) VERBOSITY="normal" ;;
54
88
  esac
55
- ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-}}}}"
89
+
90
+ # heal_max_cycles from config (project .mugiwara/config), default 3; env override
91
+ HEAL_MAX_CYCLES="${STATE_HEAL_MAX_CYCLES:-3}"
92
+ if [ -f "$MUGIWARA_DIR/config" ]; then
93
+ CFG_HEAL_MAX=$(grep -E '^heal_max_cycles=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
94
+ [ -n "$CFG_HEAL_MAX" ] && HEAL_MAX_CYCLES="$CFG_HEAL_MAX"
95
+ fi
96
+ case "$HEAL_MAX_CYCLES" in
97
+ ''|*[!0-9]*) HEAL_MAX_CYCLES=3 ;;
98
+ esac
99
+ [ "$HEAL_MAX_CYCLES" -lt 1 ] 2>/dev/null && HEAL_MAX_CYCLES=3
100
+
101
+ # delegate_threshold from config (project .mugiwara/config), default 60; env override
102
+ DELEGATE_THRESHOLD="${STATE_DELEGATE_THRESHOLD:-60}"
103
+ if [ -f "$MUGIWARA_DIR/config" ]; then
104
+ CFG_DELEGATE=$(grep -E '^delegate_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
105
+ [ -n "$CFG_DELEGATE" ] && DELEGATE_THRESHOLD="$CFG_DELEGATE"
106
+ fi
107
+ case "$DELEGATE_THRESHOLD" in
108
+ ''|*[!0-9]*) DELEGATE_THRESHOLD=60 ;;
109
+ esac
110
+ [ "$DELEGATE_THRESHOLD" -lt 1 ] 2>/dev/null && DELEGATE_THRESHOLD=1
111
+ [ "$DELEGATE_THRESHOLD" -gt 100 ] 2>/dev/null && DELEGATE_THRESHOLD=100
112
+ ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-${USERNAME:-}}}}}"
56
113
  BRANCH="$(git branch --show-current 2>/dev/null || echo 'unknown')"
114
+ # Per-stage model attribution (A4): record which model produced THIS stage.
115
+ # MUGIWARA_MODEL wins over ANTHROPIC_MODEL; 'unknown' beats a lie. Closure
116
+ # renders the unique set across stage files so mid-mission switches stay
117
+ # visible in provenance instead of every line attributing to the last value.
118
+ MODEL="${MUGIWARA_MODEL:-${ANTHROPIC_MODEL:-}}"
119
+ [ -n "$MODEL" ] || MODEL="unknown"
57
120
 
58
121
  # treat the legacy empty-actor placeholder '""' as "no member" (solo). The old
59
122
  # interface used '""' for the actor slot; the new (mission, member) interface
@@ -78,6 +141,12 @@ esac
78
141
  if [[ "$MEMBER" =~ ^\.+$ ]]; then
79
142
  die "invalid member name \"$MEMBER\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
80
143
  fi
144
+ # reserved stems — <member>.json and continue-<member>.json share the mission
145
+ # dir with state.json / continue.json; member "state" or "continue" would
146
+ # overwrite the solo files (state pointer or resume pointer).
147
+ case "$MEMBER" in
148
+ state|continue) die "reserved member name \"$MEMBER\" (state, continue)" ;;
149
+ esac
81
150
 
82
151
  # BRANCH_SLUG sanitized to [A-Za-z0-9._-] — it feeds the continue branch field;
83
152
  # a newline/control char in BRANCH would corrupt the JSON (F5). Dropping illegal
@@ -85,44 +154,64 @@ fi
85
154
  # (BSD/macOS-safe: no \+ BRE).
86
155
  BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-' | tr -cd 'A-Za-z0-9._-' | sed 's/^\.\{1,\}$//' )
87
156
 
88
- # state + continue live per (mission, member). Solo (member empty) → state.json.
89
- STATE_DIR="$MUGIWARA_DIR/state/$MISSION"
90
- CONTINUE_DIR="$MUGIWARA_DIR/continue/$MISSION"
157
+ # state + continue live in the mission dir. Solo (member empty) → state.json
158
+ # + continue.json; team writes <member>.json + continue-<member>.json so
159
+ # parallel members never clobber each other.
160
+ MISSION_DIR="$MUGIWARA_DIR/missions/$MISSION"
91
161
  if [ -n "$MEMBER" ]; then
92
- STATE_FILE="$STATE_DIR/$MEMBER.json"
93
- CONTINUE_FILE="$CONTINUE_DIR/$MEMBER.json"
162
+ STATE_FILE="$MISSION_DIR/$MEMBER.json"
163
+ CONTINUE_FILE="$MISSION_DIR/continue-$MEMBER.json"
94
164
  else
95
- STATE_FILE="$STATE_DIR/state.json"
96
- CONTINUE_FILE="$CONTINUE_DIR/state.json"
165
+ STATE_FILE="$MISSION_DIR/state.json"
166
+ CONTINUE_FILE="$MISSION_DIR/continue.json"
97
167
  fi
98
168
 
99
- [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode]"
169
+ [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
100
170
  [ -d .git ] || die "not a git repository"
101
171
 
102
172
  # --- computed fields ---
103
173
  BASE_SHA=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null || git merge-base HEAD "$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')" 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "unknown")
104
174
  HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
105
175
 
106
- CHANGED_FILES=$(git diff --name-only "$BASE_SHA"..HEAD 2>/dev/null || git diff --name-only --cached 2>/dev/null || true)
107
- FILES_TOUCHED=$( [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | wc -l | tr -d ' ' || echo 0 )
108
-
109
- LOC_INS=0
110
- LOC_DEL=0
111
- LOC_DELTA=0
112
- LOC_CHURN=0
113
- if [ "$BASE_SHA" != "unknown" ]; then
114
- STAT=$(git diff --shortstat "$BASE_SHA"..HEAD 2>/dev/null || echo "")
115
- INS=$(echo "$STAT" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
116
- DEL=$(echo "$STAT" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
117
- LOC_INS=$(( ${INS:-0} + 0 ))
118
- LOC_DEL=$(( ${DEL:-0} + 0 ))
119
- LOC_DELTA=$(( LOC_INS - LOC_DEL ))
120
- # churn is insertion+deletion — refactors and deletions are work too (D4)
121
- LOC_CHURN=$(( LOC_INS + LOC_DEL ))
122
- fi
123
- [ -z "$LOC_DELTA" ] && LOC_DELTA=0
124
-
176
+ # lane_scope_glob (T5): monorepo scoping count only files matching the glob
177
+ LANE_SCOPE_GLOB=""
178
+ if [ -f "$MUGIWARA_DIR/config" ]; then
179
+ _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 "'")
180
+ [ -n "$_cfg_scope" ] && LANE_SCOPE_GLOB="$_cfg_scope"
181
+ fi
182
+ # union of committed + staged + unstaged + untracked (F) — see patterns.sh
183
+ CHANGED_FILES=$(changed_files "$BASE_SHA")
184
+ # sensitive-path escalation always uses unfiltered set (safety never shrinks, T5)
125
185
  SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
186
+ # scoped counting (T5)
187
+ SCOPED_FILES="$CHANGED_FILES"
188
+ if [ -n "$LANE_SCOPE_GLOB" ] && [ -n "$CHANGED_FILES" ]; then
189
+ shopt -s extglob globstar 2>/dev/null || true
190
+ SCOPED_FILES=""
191
+ while IFS= read -r _sf; do
192
+ [ -z "$_sf" ] && continue
193
+ # shellcheck disable=SC2053
194
+ if [[ "$_sf" == $LANE_SCOPE_GLOB ]]; then
195
+ SCOPED_FILES="${SCOPED_FILES}${SCOPED_FILES:+
196
+ }$_sf"
197
+ fi
198
+ done <<< "$CHANGED_FILES"
199
+ fi
200
+ FILES_TOUCHED=$( [ -n "$SCOPED_FILES" ] && echo "$SCOPED_FILES" | wc -l | tr -d ' ' || echo 0 )
201
+ if [ -n "$LANE_SCOPE_GLOB" ]; then
202
+ read -r LOC_INS LOC_DEL <<EOF
203
+ $( { echo "$SCOPED_FILES" | while IFS= read -r _lf; do [ -n "$_lf" ] && git diff --numstat "$BASE_SHA"..HEAD -- "$_lf" 2>/dev/null; git diff --numstat HEAD -- "$_lf" 2>/dev/null; done; echo "$SCOPED_FILES" | while IFS= read -r _uf; do [ -z "$_uf" ] && continue; if git ls-files --others --exclude-standard -- "$_uf" 2>/dev/null | grep -qx "$_uf"; then printf '%s\t0\t%s\n' "$(wc -l < "$_uf" 2>/dev/null | tr -d ' ' || echo 0)" "$_uf"; fi; done; } | awk '{ if ($1 ~ /^[0-9]+$/) i+=$1; if ($2 ~ /^[0-9]+$/) d+=$2 } END { print (i+0)" "(d+0) }')
204
+ EOF
205
+ else
206
+ read -r LOC_INS LOC_DEL <<EOF
207
+ $(changed_loc "$BASE_SHA")
208
+ EOF
209
+ fi
210
+ LOC_INS=$(( ${LOC_INS:-0} + 0 ))
211
+ LOC_DEL=$(( ${LOC_DEL:-0} + 0 ))
212
+ LOC_DELTA=$(( LOC_INS - LOC_DEL ))
213
+ # churn is insertion+deletion — refactors and deletions are work too (D4)
214
+ LOC_CHURN=$(( LOC_INS + LOC_DEL ))
126
215
 
127
216
  LANE="direct"
128
217
  LANE_REASON=""
@@ -142,7 +231,7 @@ elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
142
231
  LANE_REASON="$FILES_TOUCHED files"
143
232
  elif [ "$FILES_TOUCHED" -eq 1 ] 2>/dev/null; then
144
233
  # 1-file rule mirrors lane.sh: >=20 added LOC -> lean, else direct
145
- ADDED=$(git diff --numstat "$BASE_SHA"..HEAD 2>/dev/null | awk '{s+=$1} END {print s+0}')
234
+ ADDED="$LOC_INS"
146
235
  if [ "${ADDED:-0}" -ge 20 ] 2>/dev/null; then
147
236
  LANE="lean"
148
237
  LANE_REASON="1 file, $ADDED LOC"
@@ -157,9 +246,10 @@ fi
157
246
 
158
247
  # path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
159
248
  # surface never escalate to full from file count alone; sensitive-path
160
- # escalation above still wins.
161
- if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ]; then
162
- CODE_COUNT=$(echo "$CHANGED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
249
+ # escalation above still wins. With lane_scope_glob the check applies to the
250
+ # scoped set but sensitive wins unfiltered above, so safety never shrinks.
251
+ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$SCOPED_FILES" ]; then
252
+ CODE_COUNT=$(echo "$SCOPED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
163
253
  if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
164
254
  PREV="$LANE"
165
255
  LANE="standard"
@@ -167,6 +257,33 @@ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ];
167
257
  fi
168
258
  fi
169
259
 
260
+ # policy as code: mugiwara.policy.yml lanes.force_full raises the
261
+ # lane to full — upward only. Optional: no file or no bun = no-op.
262
+ if command -v bun >/dev/null 2>&1 && { [ -f mugiwara.policy.yml ] || [ -f mugiwara.policy.yaml ]; }; then
263
+ POLICY_HITS=$(printf '%s\n' "$CHANGED_FILES" | bun "$(dirname "$0")/policy-force.ts")
264
+ RC=$?
265
+ if [ "$RC" -ne 0 ]; then
266
+ die "mugiwara.policy.yml is invalid — fix or remove it (fail-closed)"
267
+ fi
268
+ if [ -n "$POLICY_HITS" ]; then
269
+ LANE="full"
270
+ LANE_REASON="policy force_full ($POLICY_HITS)"
271
+ fi
272
+ fi
273
+
274
+ # explicit triage lane (M7) applied as a floor — the computed lane above may
275
+ # raise it, never lower it. Sensitive-path escalation still wins
276
+ # unconditionally: spike is a resize, not a rise, so it never displaces the
277
+ # full lane a sensitive path forced.
278
+ if [ -n "$LANE_EXPLICIT" ]; then
279
+ if [ "$LANE_EXPLICIT" = "spike" ] && [ -n "$SENSITIVE_PATHS" ]; then
280
+ LANE_REASON="$LANE_REASON (explicit spike ignored — sensitive paths)"
281
+ elif [ "$(lane_rank "$LANE_EXPLICIT")" -gt "$(lane_rank "$LANE")" ]; then
282
+ LANE_REASON="triage lane $LANE_EXPLICIT (floor; computed $LANE — $LANE_REASON)"
283
+ LANE="$LANE_EXPLICIT"
284
+ fi
285
+ fi
286
+
170
287
  # --- lane-rise compare (F9) + monotonic clamp (D2): read previous state ---
171
288
  # Clamp runs here — before LANE_BASE/BUDGET — so tokens and budget follow the
172
289
  # held lane, never the raw shrunken one.
@@ -203,9 +320,8 @@ if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
203
320
  esac
204
321
  fi
205
322
 
206
- # task counts from plan doc — plan is written date-prefixed (plans/YYYY-MM-DD-<mission>.md)
207
- # or bare (plans/<mission>.md); glob both, first match wins.
208
- PLAN_FILE=$(ls "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md 2>/dev/null | head -1 || true)
323
+ # task counts from plan doc — bare name, one file per mission.
324
+ PLAN_FILE="$MISSION_DIR/plan.md"
209
325
  TASKS_DONE=0
210
326
  TASKS_TOTAL=0
211
327
  if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
@@ -217,7 +333,7 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
217
333
  fi
218
334
 
219
335
  # blocker count
220
- BLOCKERS_FILE=$(ls "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" 2>/dev/null || true)
336
+ BLOCKERS_FILE="$MISSION_DIR/blockers.md"
221
337
  BLOCKERS_OPEN=0
222
338
  if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
223
339
  # data rows start with a wave number; header "| wave |" and separator
@@ -225,36 +341,77 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
225
341
  BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
226
342
  fi
227
343
 
228
- # heal cycle — count WAVE-8 banner occurrences in the trace, not the word
229
- # "heal" anywhere (heal workers/healing text would inflate the counter and
230
- # cause a premature halt)
344
+ # heal cycle — count Flow-8 (healing) section headings in the DECISION LOG
345
+ # (missions/<mission>/decisions.md), which every mission writes (luffy-
346
+ # orchestrator rule 10). Not the word "heal" anywhere (heal workers/healing
347
+ # text would inflate the counter and cause a premature halt). Each heal cycle
348
+ # is logged as a "## Flow 8 — healing" section; the [^0-9a-z] guard keeps
349
+ # adjacent "## Flow 8b"-style sections from counting.
231
350
  HEAL_CYCLE=1
232
- TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
233
- if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
234
- HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null || true)
351
+ HEAL_COUNT=0
352
+ LOG_FILE="$MISSION_DIR/decisions.md"
353
+ if [ -f "$LOG_FILE" ]; then
354
+ HEAL_COUNT=$(grep -ciE '^## flow 8([^0-9a-z]|$)' "$LOG_FILE" 2>/dev/null || true)
355
+ fi
356
+ if [ "$HEAL_COUNT" -gt 0 ]; then
235
357
  HEAL_CYCLE=$((HEAL_COUNT + 1))
236
358
  fi
237
359
 
238
- # evidence pathsper-mission results folder (quoted printf, no sed mission
239
- # name is allowlisted above, but avoid sed metacharacter semantics entirely)
360
+ # heal haltcomputed from the heal counter and the config cap (task 4f wires
361
+ # heal_max_cycles into the counter's home). heal_cycle starts at 1, so
362
+ # heal_cycle >= heal_max_cycles means the cap is reached: escalate, never re-run.
363
+ HEAL_HALT=false
364
+ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
365
+ HEAL_HALT=true
366
+ fi
367
+
368
+ # evidence paths — the mission's flow folder (quoted printf, no sed — mission
369
+ # name is allowlisted above, but avoid sed metacharacter semantics entirely).
370
+ # New missions use flows/; a legacy mission that already keeps waves/ stays on
371
+ # waves/ so an in-flight trail never splits across two directories.
372
+ FLOWDIR="flows"
373
+ if [ -d "$MISSION_DIR/waves" ] && [ ! -d "$MISSION_DIR/flows" ]; then
374
+ FLOWDIR="waves"
375
+ fi
240
376
  EVIDENCE=""
241
- for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
377
+ for evf in "$MISSION_DIR/$FLOWDIR/"*.md; do
242
378
  [ -f "$evf" ] || continue
243
- EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
379
+ EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/missions/${MISSION}/${FLOWDIR}/$(basename "$evf")"
244
380
  done
245
381
 
246
- # skill version from package.json (argv-passing never interpolate paths into node -e)
247
- SKILL_VERSION="1"
248
- if [ -f package.json ]; then
249
- PKG_JSON="$MUGIWARA_DIR/../package.json"
250
- [ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version.split('.')[0])}catch(e){console.log('1')}" "$PKG_JSON" 2>/dev/null || echo "1")
382
+ # skill version MUGIWARA's own package.json, resolved from the script
383
+ # location (B3). The old "$MUGIWARA_DIR/../package.json" was the USER's
384
+ # project manifest: the version-mismatch resume prompt fired when a user
385
+ # shipped their own v2.0.0 and stayed silent across every real mugiwara
386
+ # release. Never fall back to the user's file — "unknown" is the honest
387
+ # answer. Full version, not the major: mugiwara's breaking changes land in
388
+ # 0.x minors, which a major-only string can never see.
389
+ SKILL_VERSION="unknown"
390
+ PKG_JSON="$(dirname "$0")/../package.json"
391
+ if [ -f "$PKG_JSON" ]; then
392
+ SKILL_VERSION=$(node -e "try{const v=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version;console.log(v||'unknown')}catch(e){console.log('unknown')}" "$PKG_JSON" 2>/dev/null || echo "unknown")
251
393
  fi
394
+ [ -z "$SKILL_VERSION" ] && SKILL_VERSION="unknown"
252
395
 
253
396
  # tokens proxy (F7): deterministic estimate when the harness does not report
254
397
  # real usage. Monotonic beats precise — LANE_BASE stands in for the skills
255
398
  # loaded this lane (measured from content, validated by scripts/lane-base.ts);
256
399
  # loc_churn and written-artifact words scale with growth.
400
+ # auto-detect harness for token reporting (Tier-1: Claude Code / opencode)
401
+ # If --tokens-file not given, try env/file fallbacks so estimator is last resort.
402
+ if [ -z "$TOKENS_FILE" ]; then
403
+ for _cand in "${MUGIWARA_TOKENS_FILE:-}" "${CLAUDE_TOKENS_FILE:-}" "${OPENCODE_TOKENS_FILE:-}" "/tmp/mugiwara-tokens.json" "$HOME/.cache/mugiwara/tokens.json" ".mugiwara/tokens.json"; do
404
+ [ -n "$_cand" ] && [ -f "$_cand" ] && TOKENS_FILE="$_cand" && break
405
+ done
406
+ fi
407
+ # detect harness (for logging / future tier checks)
408
+ HARNESS="unknown"
409
+ if [ -n "${CLAUDECODE:-}" ] || [ -n "${CLAUDE_CODE_ENTRYPOINT:-}" ] || echo "${ANTHROPIC_MODEL:-}" | grep -qi claude 2>/dev/null; then HARNESS="claude";
410
+ elif [ -n "${OPENCODE:-}" ] || [ -f ".opencode/config.json" ] || [ -n "${OPENCODE_TOKENS_FILE:-}" ]; then HARNESS="opencode";
411
+ elif [ -n "${CURSOR:-}" ] || [ -n "${VSCODE_GIT_ASKPASS_NODE:-}" ]; then HARNESS="cursor/vscode"; fi
412
+
257
413
  # MUGIWARA_TOKENS overrides as the reported value.
414
+ # --tokens-file (T4) is the first-class provider-reported path: JSON {input_tokens, output_tokens}
258
415
  LANE_BASE=0
259
416
  case "$LANE" in
260
417
  lean) LANE_BASE=$LANE_BASE_lean ;;
@@ -262,14 +419,19 @@ case "$LANE" in
262
419
  full) LANE_BASE=$LANE_BASE_full ;;
263
420
  spike) LANE_BASE=$LANE_BASE_spike ;;
264
421
  esac
265
- DOC_WORDS=$(cat "$MUGIWARA_DIR"/results/${MISSION}/*.md "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md "$MUGIWARA_DIR"/spec/${MISSION}.md "$MUGIWARA_DIR"/spec/*-${MISSION}.md "$MUGIWARA_DIR"/logs/${MISSION}.md "$MUGIWARA_DIR"/logs/*-${MISSION}.md 2>/dev/null | wc -w | tr -d ' ')
422
+ DOC_WORDS=$(cat "$MISSION_DIR"/"$FLOWDIR"/*.md "$MISSION_DIR"/plan.md "$MISSION_DIR"/spec.md "$MISSION_DIR"/decisions.md 2>/dev/null | wc -w | tr -d ' ')
266
423
  LOC_TOKENS=$(( LOC_CHURN * 12 ))
267
- TOKENS_SOURCE="computed"
424
+ TOKENS_SOURCE="estimator"
268
425
  TOKENS_EST=$(( LANE_BASE + DOC_WORDS * 135 / 100 + LOC_TOKENS ))
269
426
  if [ -n "${MUGIWARA_TOKENS:-}" ]; then
270
427
  TOKENS_EST="${MUGIWARA_TOKENS}"
271
428
  TOKENS_SOURCE="reported"
272
429
  fi
430
+ if [ -n "$TOKENS_FILE" ]; then
431
+ [ -f "$TOKENS_FILE" ] || die "tokens file not found: $TOKENS_FILE"
432
+ TOKENS_EST=$(node -e "try{const j=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const i=Number(j.input_tokens)||0;const o=Number(j.output_tokens)||0;console.log(i+o)}catch(e){console.log(0)}" "$TOKENS_FILE" 2>/dev/null || echo 0)
433
+ TOKENS_SOURCE="reported"
434
+ fi
273
435
 
274
436
  # budget per lane
275
437
  BUDGET=0
@@ -291,7 +453,20 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
291
453
  STATUS="warn"
292
454
  fi
293
455
 
294
- mkdir -p "$MUGIWARA_DIR/state/$MISSION" "$MUGIWARA_DIR/continue/$MISSION"
456
+ # delegate due — the execution skill's context-pressure trigger (task 4f wires
457
+ # delegate_threshold into a computed flag). tokens_est >= threshold% of budget
458
+ # → remaining sequential tasks dispatch to workers. Threshold stays relative:
459
+ # a bigger budget raises the bar, it does not remove it. Budget 0 (unknown
460
+ # lane) → never due.
461
+ DELEGATE_DUE=false
462
+ if [ "$BUDGET" -gt 0 ] 2>/dev/null; then
463
+ DELEGATE_AT=$(( BUDGET * DELEGATE_THRESHOLD / 100 ))
464
+ if [ "$TOKENS_EST" -ge "$DELEGATE_AT" ] 2>/dev/null; then
465
+ DELEGATE_DUE=true
466
+ fi
467
+ fi
468
+
469
+ mkdir -p "$MISSION_DIR"
295
470
 
296
471
  node -e "
297
472
  const data = {
@@ -304,9 +479,10 @@ const data = {
304
479
  lane_prev: process.argv[24] || null,
305
480
  lane_peak: process.argv[27] || null,
306
481
  lane_rose: process.argv[25] === 'true',
307
- wave: parseInt(process.argv[6], 10),
482
+ flow: parseInt(process.argv[6], 10),
308
483
  mode: process.argv[7],
309
484
  verbosity: process.argv[32] || 'normal',
485
+ model: process.argv[37] || 'unknown',
310
486
  base_sha: process.argv[8],
311
487
  head_sha: process.argv[9],
312
488
  files_touched: parseInt(process.argv[10], 10),
@@ -318,6 +494,10 @@ const data = {
318
494
  tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
319
495
  blockers_open: parseInt(process.argv[15], 10),
320
496
  heal_cycle: parseInt(process.argv[16], 10),
497
+ heal_max_cycles: parseInt(process.argv[33], 10) || 3,
498
+ heal_halt: process.argv[34] === 'true',
499
+ delegate_threshold: parseInt(process.argv[35], 10) || 60,
500
+ delegate_due: process.argv[36] === 'true',
321
501
  tokens_est: parseInt(process.argv[17], 10) || 0,
322
502
  tokens_source: process.argv[26] || 'computed',
323
503
  budget: parseInt(process.argv[18], 10) || 0,
@@ -329,26 +509,28 @@ const data = {
329
509
  require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
330
510
  " \
331
511
  "$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
332
- "$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
512
+ "$WAVE_INT" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
333
513
  "$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
334
514
  "$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
335
515
  "$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
336
516
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
337
517
  "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
338
- "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY"
518
+ "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
519
+ "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
520
+ "$MODEL"
339
521
 
340
522
  if [ "$LANE_ROSE" = true ]; then
341
523
  echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
342
524
  fi
343
525
 
344
- # --- continue/<mission>/<member>.json (D10): machine-written resume point ---
526
+ # --- continue.json (D10): machine-written resume point ---
345
527
  # Written alongside state.json so an interrupted session (step-limit truncation,
346
528
  # crash, new session) can resume without human recall. Same trust as state.json:
347
529
  # computed fields, never model judgement. The resume skill treats it as data to
348
530
  # verify against the plan/todos, never verbatim instructions.
349
- # Scoped by (mission, member) like state.json — solo writes state.json, team
350
- # writes <member>.json, so parallel members never clobber each other. The
351
- # crew-written next_session_prompt line is preserved across savepoints.
531
+ # Scoped by (mission, member) like state.json — solo writes continue.json, team
532
+ # writes continue-<member>.json. The crew-written next_session_prompt line is
533
+ # preserved across savepoints.
352
534
  if [ -n "$MISSION" ]; then
353
535
  NEXT_SESSION_PROMPT=""
354
536
  if [ -f "$CONTINUE_FILE" ]; then
@@ -356,7 +538,7 @@ if [ -n "$MISSION" ]; then
356
538
  fi
357
539
  # N2: every field echoed into continue is validated first. MISSION + MEMBER
358
540
  # are allowlisted upstream; WAVE numeric, MODE enum, BRANCH slug-sanitized.
359
- CONT_WAVE=$(echo "$WAVE" | tr -cd '0-9')
541
+ CONT_WAVE="$WAVE_INT"
360
542
  case "$MODE" in
361
543
  guided|semi|auto) CONT_MODE="$MODE" ;;
362
544
  *) CONT_MODE="guided" ;;
@@ -369,7 +551,7 @@ if [ -n "$MISSION" ]; then
369
551
  member: process.argv[2] || null,
370
552
  actor: process.argv[13] || '',
371
553
  branch: process.argv[3],
372
- wave: parseInt(process.argv[4], 10) || 0,
554
+ flow: parseInt(process.argv[4], 10) || 0,
373
555
  mode: process.argv[5],
374
556
  tasks_done: parseInt(process.argv[6], 10) || 0,
375
557
  tasks_total: parseInt(process.argv[7], 10) || 0,
@@ -387,4 +569,4 @@ if [ -n "$MISSION" ]; then
387
569
  "$CONTINUE_FILE" "$CONTINUE_FILE" "$ACTOR"
388
570
  fi
389
571
 
390
- echo "✓ savepoint written: $STATE_FILE (lane=$LANE, wave=$WAVE, files=$FILES_TOUCHED)"
572
+ echo "✓ savepoint written: $STATE_FILE (lane=$LANE, flow=$WAVE, files=$FILES_TOUCHED)"
@@ -286,6 +286,25 @@ if (totalDescChars > INDEX_BUDGET) {
286
286
  console.log(`✓ index budget: ${totalDescChars}/${INDEX_BUDGET} chars`);
287
287
  }
288
288
 
289
+ // The measured index size is published in docs/concepts/cost.md. A measured
290
+ // number in prose that drifted from reality is a defect (it survived one
291
+ // release: cost.md claimed 5,437 chars after the prune measured 4,688).
292
+ // Gate the doc against the measurement so the class stays closed.
293
+ const costDoc = join(root, '..', 'docs', 'concepts', 'cost.md');
294
+ if (existsSync(costDoc)) {
295
+ const m = readFileSync(costDoc, 'utf8').match(/\*\*Current:\*\* (\d[\d,]*) chars/);
296
+ if (!m) {
297
+ errors.push('docs/concepts/cost.md: missing "**Current:** <n> chars" line for the measured index size');
298
+ } else {
299
+ const stated = parseInt(m[1].replace(/,/g, ''), 10);
300
+ if (stated !== totalDescChars) {
301
+ errors.push(`docs/concepts/cost.md states ${stated} index chars but content measures ${totalDescChars} — update the doc`);
302
+ } else {
303
+ console.log(`✓ cost.md index chars match measurement (${stated})`);
304
+ }
305
+ }
306
+ }
307
+
289
308
  // --- docs-drift check: docs/skills.md and docs/agents.md must reference all content/ entries ---
290
309
  const docsArg = process.argv.indexOf('--check-docs');
291
310
  if (docsArg !== -1) {
@@ -374,15 +393,9 @@ if (integrityArg !== -1) {
374
393
  }
375
394
  }
376
395
  }
377
- if (!constants.includes('LANE_BASE_lean=7000')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 7000)');
378
- if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
379
- }
380
- // evidence exit-code claim must be test-backed
381
- const evTest = join(import.meta.dirname, '..', 'test', 'lane-integrity.test.ts');
382
- const testText = existsSync(evTest) ? readFileSync(evTest, 'utf8') : '';
383
- if (!testText.includes('# Exit:') || !testText.includes('# Verdict:')) {
384
- errors.push('doc-integrity: audit-trail claims evidence logs carry exit code + verdict, but no test asserts them');
385
- }
396
+ if (!constants.includes('LANE_BASE_lean=7000')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 7000)');
397
+ if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
398
+ }
386
399
  }
387
400
 
388
401
  // Conditional-assertion guard: an expect() reachable only inside a truthiness