@ionivetech/mugiwara 0.6.6 → 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 (110) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -7
  3. package/.codex-plugin/plugin.json +2 -7
  4. package/.cursor-plugin/plugin.json +2 -7
  5. package/.kimi-plugin/plugin.json +2 -7
  6. package/.opencode/plugins/mugiwara.mjs +3 -18
  7. package/AGENTS.md +5 -4
  8. package/GEMINI.md +3 -3
  9. package/README.md +69 -63
  10. package/content/agents/brook-healing.md +8 -4
  11. package/content/agents/chopper-checkpoint.md +7 -3
  12. package/content/agents/eval-runner.md +4 -4
  13. package/content/agents/franky-gates.md +3 -3
  14. package/content/agents/jinbe-security.md +6 -7
  15. package/content/agents/luffy-orchestrator.md +11 -13
  16. package/content/agents/memory-keeper.md +4 -4
  17. package/content/agents/nami-planner.md +4 -4
  18. package/content/agents/resume-coordinator.md +7 -7
  19. package/content/agents/robin-reviewer.md +7 -3
  20. package/content/agents/sanji-quality.md +2 -2
  21. package/content/agents/skeptic-verifier.md +2 -2
  22. package/content/agents/usopp-brainstorm.md +3 -3
  23. package/content/agents/zoro-execution.md +6 -6
  24. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  25. package/content/skills/mugiwara-backend/references/database.md +61 -0
  26. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  27. package/content/skills/mugiwara-checkpoint/SKILL.md +6 -5
  28. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
  29. package/content/skills/mugiwara-execution/SKILL.md +8 -8
  30. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  31. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  32. package/content/skills/mugiwara-frontend/SKILL.md +3 -2
  33. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  34. package/content/skills/mugiwara-gates/SKILL.md +8 -5
  35. package/content/skills/mugiwara-healing/SKILL.md +2 -2
  36. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  37. package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
  38. package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
  39. package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
  40. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
  41. package/content/skills/mugiwara-planning/SKILL.md +3 -18
  42. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  43. package/content/skills/mugiwara-quality/SKILL.md +5 -2
  44. package/content/skills/mugiwara-resume/SKILL.md +2 -2
  45. package/content/skills/mugiwara-review/SKILL.md +3 -2
  46. package/content/skills/mugiwara-security/SKILL.md +1 -1
  47. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  48. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  49. package/content/skills/mugiwara-workflow/SKILL.md +9 -12
  50. package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
  51. package/dist/mugiwara.js +904 -462
  52. package/gemini-extension.json +1 -1
  53. package/hooks/auto-savepoint.js +18 -10
  54. package/hooks/auto-savepoint.ts +23 -23
  55. package/hooks/engagement-marker.js +1 -1
  56. package/hooks/engagement-marker.ts +1 -1
  57. package/hooks/pipeline-guard.js +16 -12
  58. package/hooks/pipeline-guard.ts +22 -19
  59. package/hooks/session-start.js +13 -10
  60. package/hooks/session-start.ts +24 -19
  61. package/package.json +2 -2
  62. package/plugin.json +1 -1
  63. package/references/complexity.md +27 -4
  64. package/references/definition-of-done.md +1 -1
  65. package/references/multi-actor.md +0 -3
  66. package/references/prose-style.md +54 -0
  67. package/scripts/check-doc-links.ts +40 -0
  68. package/scripts/conformance.ts +6 -16
  69. package/scripts/coverage-gate.ts +20 -6
  70. package/scripts/gate-selftest.ts +118 -7
  71. package/scripts/lane.sh +54 -8
  72. package/scripts/lib/patterns.sh +5 -0
  73. package/scripts/policy-force.ts +22 -0
  74. package/scripts/retrieval-eval.ts +1 -1
  75. package/scripts/savepoint.sh +136 -58
  76. package/scripts/validate-content.ts +22 -9
  77. package/scripts/verify-install.ts +9 -7
  78. package/src/args.ts +2 -2
  79. package/src/budget.ts +56 -0
  80. package/src/cli.ts +161 -110
  81. package/src/continue.ts +29 -10
  82. package/src/installer.ts +25 -20
  83. package/src/integrity.ts +158 -0
  84. package/src/mission.ts +228 -95
  85. package/src/policy.ts +156 -0
  86. package/src/provenance.ts +116 -0
  87. package/src/rollback.ts +95 -0
  88. package/src/routing.ts +69 -0
  89. package/src/run.ts +2 -2
  90. package/src/sign.ts +61 -0
  91. package/src/targets/claude.ts +2 -2
  92. package/.opencode/commands/mugiwara-execute.md +0 -13
  93. package/.opencode/commands/mugiwara-heal.md +0 -13
  94. package/.opencode/commands/mugiwara-onboard.md +0 -14
  95. package/.opencode/commands/mugiwara-plan.md +0 -17
  96. package/.opencode/commands/mugiwara-ship.md +0 -13
  97. package/.opencode/commands/using-mugiwara.md +0 -20
  98. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  99. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  100. package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
  101. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  102. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  103. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  104. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  105. package/content/skills/using-mugiwara/SKILL.md +0 -51
  106. package/references/token-budget.md +0 -56
  107. package/scripts/evidence.sh +0 -81
  108. package/scripts/initiative.ts +0 -296
  109. package/scripts/mission-report.sh +0 -293
  110. package/src/onboard.ts +0 -207
@@ -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] [lane]
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"
@@ -99,6 +111,12 @@ esac
99
111
  [ "$DELEGATE_THRESHOLD" -gt 100 ] 2>/dev/null && DELEGATE_THRESHOLD=100
100
112
  ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-${USERNAME:-}}}}}"
101
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"
102
120
 
103
121
  # treat the legacy empty-actor placeholder '""' as "no member" (solo). The old
104
122
  # interface used '""' for the actor slot; the new (mission, member) interface
@@ -123,6 +141,12 @@ esac
123
141
  if [[ "$MEMBER" =~ ^\.+$ ]]; then
124
142
  die "invalid member name \"$MEMBER\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
125
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
126
150
 
127
151
  # BRANCH_SLUG sanitized to [A-Za-z0-9._-] — it feeds the continue branch field;
128
152
  # a newline/control char in BRANCH would corrupt the JSON (F5). Dropping illegal
@@ -130,15 +154,16 @@ fi
130
154
  # (BSD/macOS-safe: no \+ BRE).
131
155
  BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-' | tr -cd 'A-Za-z0-9._-' | sed 's/^\.\{1,\}$//' )
132
156
 
133
- # state + continue live per (mission, member). Solo (member empty) → state.json.
134
- STATE_DIR="$MUGIWARA_DIR/state/$MISSION"
135
- 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"
136
161
  if [ -n "$MEMBER" ]; then
137
- STATE_FILE="$STATE_DIR/$MEMBER.json"
138
- CONTINUE_FILE="$CONTINUE_DIR/$MEMBER.json"
162
+ STATE_FILE="$MISSION_DIR/$MEMBER.json"
163
+ CONTINUE_FILE="$MISSION_DIR/continue-$MEMBER.json"
139
164
  else
140
- STATE_FILE="$STATE_DIR/state.json"
141
- CONTINUE_FILE="$CONTINUE_DIR/state.json"
165
+ STATE_FILE="$MISSION_DIR/state.json"
166
+ CONTINUE_FILE="$MISSION_DIR/continue.json"
142
167
  fi
143
168
 
144
169
  [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
@@ -148,21 +173,46 @@ fi
148
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")
149
174
  HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
150
175
 
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
151
182
  # union of committed + staged + unstaged + untracked (F) — see patterns.sh
152
183
  CHANGED_FILES=$(changed_files "$BASE_SHA")
153
- FILES_TOUCHED=$( [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | wc -l | tr -d ' ' || echo 0 )
154
-
155
- read -r LOC_INS LOC_DEL <<EOF
184
+ # sensitive-path escalation always uses unfiltered set (safety never shrinks, T5)
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
156
207
  $(changed_loc "$BASE_SHA")
157
208
  EOF
209
+ fi
158
210
  LOC_INS=$(( ${LOC_INS:-0} + 0 ))
159
211
  LOC_DEL=$(( ${LOC_DEL:-0} + 0 ))
160
212
  LOC_DELTA=$(( LOC_INS - LOC_DEL ))
161
213
  # churn is insertion+deletion — refactors and deletions are work too (D4)
162
214
  LOC_CHURN=$(( LOC_INS + LOC_DEL ))
163
215
 
164
- SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
165
-
166
216
  LANE="direct"
167
217
  LANE_REASON=""
168
218
  # Sensitive-path escalation is unconditional — it wins over any count-based
@@ -196,9 +246,10 @@ fi
196
246
 
197
247
  # path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
198
248
  # surface never escalate to full from file count alone; sensitive-path
199
- # escalation above still wins.
200
- if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ]; then
201
- 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)
202
253
  if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
203
254
  PREV="$LANE"
204
255
  LANE="standard"
@@ -206,6 +257,20 @@ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ];
206
257
  fi
207
258
  fi
208
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
+
209
274
  # explicit triage lane (M7) applied as a floor — the computed lane above may
210
275
  # raise it, never lower it. Sensitive-path escalation still wins
211
276
  # unconditionally: spike is a resize, not a rise, so it never displaces the
@@ -255,9 +320,8 @@ if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
255
320
  esac
256
321
  fi
257
322
 
258
- # task counts from plan doc — plan is written date-prefixed (plans/YYYY-MM-DD-<mission>.md)
259
- # or bare (plans/<mission>.md); glob both, first match wins.
260
- 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"
261
325
  TASKS_DONE=0
262
326
  TASKS_TOTAL=0
263
327
  if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
@@ -269,11 +333,7 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
269
333
  fi
270
334
 
271
335
  # blocker count
272
- # The crew writes the DATED name (`YYYY-MM-DD-<mission>-blockers.md`) — that is
273
- # what 15 prose sites mandate and what lands on disk. Matching only the bare
274
- # name made blockers_open structurally 0 forever, and that zero feeds the DoD
275
- # ship-readiness axis. Accept both; newest wins.
276
- BLOCKERS_FILE=$(ls -t "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" "$MUGIWARA_DIR"/????-??-??-"${MISSION}"-blockers.md "$MUGIWARA_DIR/issues"/????-??-??-"${MISSION}"-blockers.md 2>/dev/null | head -1 || true)
336
+ BLOCKERS_FILE="$MISSION_DIR/blockers.md"
277
337
  BLOCKERS_OPEN=0
278
338
  if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
279
339
  # data rows start with a wave number; header "| wave |" and separator
@@ -281,27 +341,18 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
281
341
  BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
282
342
  fi
283
343
 
284
- # heal cycle — count Flow-8 (healing) section headings in the DECISION LOG,
285
- # which every mission writes (luffy-orchestrator rule 10). Not the word "heal"
286
- # anywhere (heal workers/healing text would inflate the counter and cause a
287
- # premature halt). Each heal cycle is logged as a "## Flow 8 healing" section;
288
- # the [^0-9a-z] guard keeps adjacent "## Flow 8b"-style sections from counting.
289
- # Aggregate across ALL dated logs (a multi-day mission spans several files) —
290
- # reading only the newest would undercount earlier days' heal cycles.
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.
291
350
  HEAL_CYCLE=1
292
351
  HEAL_COUNT=0
293
- # Cap the date prefix to today: a future-dated log (9999-…) must not inflate
294
- # the heal counter and trigger a premature halt.
295
- TODAY=$(date +%Y-%m-%d)
296
- for LOG_FILE in "$MUGIWARA_DIR"/????-??-??-"${MISSION}".md "$MUGIWARA_DIR/logs"/????-??-??-"${MISSION}".md; do
297
- [ -f "$LOG_FILE" ] || continue
298
- LOG_DATE=$(basename "$LOG_FILE" | cut -c1-10)
299
- case "$LOG_DATE" in
300
- ''|*[!0-9-]*) continue ;;
301
- esac
302
- if [ "$LOG_DATE" \> "$TODAY" ]; then continue; fi
303
- HEAL_COUNT=$((HEAL_COUNT + $(grep -ciE '^## flow 8([^0-9a-z]|$)' "$LOG_FILE" 2>/dev/null || true)))
304
- done
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
305
356
  if [ "$HEAL_COUNT" -gt 0 ]; then
306
357
  HEAL_CYCLE=$((HEAL_COUNT + 1))
307
358
  fi
@@ -314,12 +365,18 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
314
365
  HEAL_HALT=true
315
366
  fi
316
367
 
317
- # evidence paths — per-mission results folder (quoted printf, no sed — mission
318
- # name is allowlisted above, but avoid sed metacharacter semantics entirely)
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
319
376
  EVIDENCE=""
320
- for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
377
+ for evf in "$MISSION_DIR/$FLOWDIR/"*.md; do
321
378
  [ -f "$evf" ] || continue
322
- EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
379
+ EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/missions/${MISSION}/${FLOWDIR}/$(basename "$evf")"
323
380
  done
324
381
 
325
382
  # skill version — MUGIWARA's own package.json, resolved from the script
@@ -340,7 +397,21 @@ fi
340
397
  # real usage. Monotonic beats precise — LANE_BASE stands in for the skills
341
398
  # loaded this lane (measured from content, validated by scripts/lane-base.ts);
342
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
+
343
413
  # MUGIWARA_TOKENS overrides as the reported value.
414
+ # --tokens-file (T4) is the first-class provider-reported path: JSON {input_tokens, output_tokens}
344
415
  LANE_BASE=0
345
416
  case "$LANE" in
346
417
  lean) LANE_BASE=$LANE_BASE_lean ;;
@@ -348,14 +419,19 @@ case "$LANE" in
348
419
  full) LANE_BASE=$LANE_BASE_full ;;
349
420
  spike) LANE_BASE=$LANE_BASE_spike ;;
350
421
  esac
351
- 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 ' ')
352
423
  LOC_TOKENS=$(( LOC_CHURN * 12 ))
353
- TOKENS_SOURCE="computed"
424
+ TOKENS_SOURCE="estimator"
354
425
  TOKENS_EST=$(( LANE_BASE + DOC_WORDS * 135 / 100 + LOC_TOKENS ))
355
426
  if [ -n "${MUGIWARA_TOKENS:-}" ]; then
356
427
  TOKENS_EST="${MUGIWARA_TOKENS}"
357
428
  TOKENS_SOURCE="reported"
358
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
359
435
 
360
436
  # budget per lane
361
437
  BUDGET=0
@@ -390,7 +466,7 @@ if [ "$BUDGET" -gt 0 ] 2>/dev/null; then
390
466
  fi
391
467
  fi
392
468
 
393
- mkdir -p "$MUGIWARA_DIR/state/$MISSION" "$MUGIWARA_DIR/continue/$MISSION"
469
+ mkdir -p "$MISSION_DIR"
394
470
 
395
471
  node -e "
396
472
  const data = {
@@ -406,6 +482,7 @@ const data = {
406
482
  flow: parseInt(process.argv[6], 10),
407
483
  mode: process.argv[7],
408
484
  verbosity: process.argv[32] || 'normal',
485
+ model: process.argv[37] || 'unknown',
409
486
  base_sha: process.argv[8],
410
487
  head_sha: process.argv[9],
411
488
  files_touched: parseInt(process.argv[10], 10),
@@ -439,20 +516,21 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
439
516
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
440
517
  "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
441
518
  "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
442
- "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE"
519
+ "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
520
+ "$MODEL"
443
521
 
444
522
  if [ "$LANE_ROSE" = true ]; then
445
523
  echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
446
524
  fi
447
525
 
448
- # --- continue/<mission>/<member>.json (D10): machine-written resume point ---
526
+ # --- continue.json (D10): machine-written resume point ---
449
527
  # Written alongside state.json so an interrupted session (step-limit truncation,
450
528
  # crash, new session) can resume without human recall. Same trust as state.json:
451
529
  # computed fields, never model judgement. The resume skill treats it as data to
452
530
  # verify against the plan/todos, never verbatim instructions.
453
- # Scoped by (mission, member) like state.json — solo writes state.json, team
454
- # writes <member>.json, so parallel members never clobber each other. The
455
- # 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.
456
534
  if [ -n "$MISSION" ]; then
457
535
  NEXT_SESSION_PROMPT=""
458
536
  if [ -f "$CONTINUE_FILE" ]; then
@@ -491,4 +569,4 @@ if [ -n "$MISSION" ]; then
491
569
  "$CONTINUE_FILE" "$CONTINUE_FILE" "$ACTOR"
492
570
  fi
493
571
 
494
- 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
@@ -87,13 +87,15 @@ for (const id of TARGET_IDS) {
87
87
  const RUNTIME_SHAPES: RegExp[] = [
88
88
  /^\.mugiwara\/?(\*\*)?$/, // bare mention of the dir
89
89
  /^~?\/?\.mugiwara\/config$/, // project + global config
90
- /^\.mugiwara\/(state|continue)\/<mission>\/(\[member\]|state|<member>)\.json$/,
91
- /^\.mugiwara\/issues\/(YYYY-MM-DD-<mission>-blockers\.md)?$/,
92
- /^\.mugiwara\/logs\/(YYYY-MM-DD-<mission>\.md|lessons\.md)?$/,
93
- /^\.mugiwara\/(plans|spec|reports)\/(YYYY-MM-DD-<mission>\.md)?$/,
94
- /^\.mugiwara\/review\/(YYYY-MM-DD-<mission>-(review|security|verifier)\.md)?$/,
95
- /^\.mugiwara\/results\/<mission>\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/,
96
- /^\.mugiwara\/results\/?$/,
90
+ /^\.mugiwara\/lessons\.md$/,
91
+ /^\.mugiwara\/index\.md$/,
92
+ /^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/(plan|spec|decisions|blockers|review|security|report)\.md$/,
93
+ /^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/flows\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/,
94
+ /^\.mugiwara\/missions\/<mission>\/flows\/?$/,
95
+ /^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/waves\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/, // legacy layout, still read
96
+ /^\.mugiwara\/missions\/<mission>\/waves\/?$/, // legacy layout, still read
97
+ /^\.mugiwara\/missions\/<mission>\/(state\.json|<member>\.json)$/,
98
+ /^\.mugiwara\/missions\/<mission>\/(continue\.json|continue-<member>\.json)$/,
97
99
  ];
98
100
 
99
101
  // Shape violations that exist today and are NOT fixed in this mission because
package/src/args.ts CHANGED
@@ -6,10 +6,10 @@ export type Args = {
6
6
  flags: Record<string, FlagValue>;
7
7
  };
8
8
 
9
- const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
9
+ const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
- '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all',
12
+ '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',
13
13
  '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
14
  };
15
15
 
package/src/budget.ts ADDED
@@ -0,0 +1,56 @@
1
+ // src/budget.ts
2
+ // Context budget as a gate: a mission's trail is itself
3
+ // context the next reader must load. At archive time, measure the total
4
+ // footprint of the artifacts that survive and compare against the configured
5
+ // ceiling — a bloated trail is caught like a failed test, with a visible
6
+ // number in the report.
7
+ //
8
+ // Token telemetry stays honest elsewhere: the estimator remains the default;
9
+ // `tokens_source: reported` activates only where the harness exposes real
10
+ // usage (see docs/concepts/cost.md). This module does not estimate tokens.
11
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+
15
+ export function readBudgetConfig(projectDir: string): number {
16
+ for (const base of [projectDir, homedir()]) {
17
+ const file = join(base, '.mugiwara', 'config');
18
+ if (!existsSync(file)) continue;
19
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
20
+ const t = line.trim();
21
+ if (!t || t.startsWith('#')) continue;
22
+ const eq = t.indexOf('=');
23
+ if (eq === -1) continue;
24
+ if (t.slice(0, eq).trim() !== 'context_budget_chars') continue;
25
+ const n = Number(t.slice(eq + 1).trim());
26
+ return Number.isFinite(n) && n > 0 ? n : 0;
27
+ }
28
+ }
29
+ return 0; // unset — measurement still recorded, never enforced
30
+ }
31
+
32
+ /** Sum of bytes across the trail: top-level *.md + flows/* (legacy waves/* counts too). */
33
+ export function measureContextChars(missionDir: string): number {
34
+ let total = 0;
35
+ const add = (p: string): void => {
36
+ try { total += statSync(p).size; } catch { /* vanished mid-measure */ }
37
+ };
38
+ for (const f of readdirSync(missionDir)) {
39
+ if (/\.md$/.test(f)) add(join(missionDir, f));
40
+ }
41
+ for (const sub of ['flows', 'waves']) {
42
+ const dir = join(missionDir, sub);
43
+ if (existsSync(dir)) {
44
+ for (const f of readdirSync(dir)) add(join(dir, f));
45
+ }
46
+ }
47
+ return total;
48
+ }
49
+
50
+ export function formatFootprint(chars: number, budget: number): string {
51
+ const base = `Context footprint: ${chars} chars`;
52
+ if (!budget) return `${base} (no budget configured)`;
53
+ return chars > budget
54
+ ? `${base} — OVER budget ${budget}`
55
+ : `${base} (budget ${budget})`;
56
+ }