@ionivetech/mugiwara 0.6.5 → 0.6.6

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 -3
  3. package/.codex-plugin/plugin.json +1 -2
  4. package/.cursor-plugin/plugin.json +1 -2
  5. package/.kimi-plugin/plugin.json +1 -2
  6. package/.opencode/commands/mugiwara-continue.md +44 -31
  7. package/.opencode/commands/mugiwara-execute.md +1 -1
  8. package/.opencode/commands/mugiwara-heal.md +1 -1
  9. package/.opencode/commands/mugiwara-onboard.md +8 -9
  10. package/.opencode/commands/mugiwara-plan.md +1 -1
  11. package/.opencode/commands/mugiwara-review.md +1 -1
  12. package/.opencode/commands/mugiwara-security.md +1 -1
  13. package/.opencode/commands/mugiwara-ship.md +1 -1
  14. package/.opencode/commands/mugiwara.md +2 -2
  15. package/.opencode/plugins/mugiwara.mjs +1 -2
  16. package/AGENTS.md +1 -2
  17. package/README.md +33 -31
  18. package/content/agents/brook-healing.md +5 -8
  19. package/content/agents/chopper-checkpoint.md +8 -11
  20. package/content/agents/eval-runner.md +2 -5
  21. package/content/agents/franky-gates.md +4 -7
  22. package/content/agents/jinbe-security.md +4 -7
  23. package/content/agents/luffy-orchestrator.md +13 -13
  24. package/content/agents/memory-keeper.md +5 -8
  25. package/content/agents/nami-planner.md +4 -7
  26. package/content/agents/resume-coordinator.md +7 -10
  27. package/content/agents/robin-reviewer.md +6 -9
  28. package/content/agents/sanji-quality.md +7 -10
  29. package/content/agents/skeptic-verifier.md +5 -8
  30. package/content/agents/usopp-brainstorm.md +3 -6
  31. package/content/agents/zoro-execution.md +7 -10
  32. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  33. package/content/skills/mugiwara-checkpoint/SKILL.md +13 -11
  34. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +4 -4
  35. package/content/skills/mugiwara-context-budget/SKILL.md +5 -3
  36. package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
  37. package/content/skills/mugiwara-execution/SKILL.md +20 -20
  38. package/content/skills/mugiwara-execution/references/dispatch.md +2 -2
  39. package/content/skills/mugiwara-execution/references/resume-batching.md +1 -1
  40. package/content/skills/mugiwara-frontend/SKILL.md +2 -4
  41. package/content/skills/mugiwara-gates/SKILL.md +8 -7
  42. package/content/skills/mugiwara-git/SKILL.md +7 -2
  43. package/content/skills/mugiwara-healing/SKILL.md +6 -4
  44. package/content/skills/mugiwara-healing/references/workers.md +2 -2
  45. package/content/skills/mugiwara-orchestration/SKILL.md +26 -24
  46. package/content/skills/mugiwara-orchestration/references/check-ins.md +16 -15
  47. package/content/skills/mugiwara-orchestration/references/closure.md +5 -5
  48. package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
  49. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +22 -22
  50. package/content/skills/mugiwara-planning/SKILL.md +3 -1
  51. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  52. package/content/skills/mugiwara-pr/SKILL.md +6 -6
  53. package/content/skills/mugiwara-pr/references/verdict-format.md +3 -3
  54. package/content/skills/mugiwara-quality/SKILL.md +7 -7
  55. package/content/skills/mugiwara-resume/SKILL.md +18 -20
  56. package/content/skills/mugiwara-review/SKILL.md +5 -1
  57. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
  58. package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
  59. package/content/skills/mugiwara-security/SKILL.md +5 -1
  60. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  61. package/content/skills/mugiwara-ship/references/cleanup.md +1 -1
  62. package/content/skills/mugiwara-testcases/SKILL.md +5 -3
  63. package/content/skills/mugiwara-workflow/SKILL.md +27 -27
  64. package/content/skills/mugiwara-workflow/references/workspace-layout.md +15 -15
  65. package/content/skills/using-mugiwara/SKILL.md +5 -5
  66. package/dist/mugiwara.js +808 -33
  67. package/gemini-extension.json +1 -1
  68. package/hooks/auto-savepoint.js +106 -0
  69. package/hooks/auto-savepoint.ts +102 -0
  70. package/hooks/engagement-marker.js +59 -0
  71. package/hooks/engagement-marker.ts +97 -0
  72. package/hooks/hooks.json +41 -2
  73. package/hooks/mugiwara-mode-tracker.js +79 -0
  74. package/hooks/pipeline-guard.js +182 -0
  75. package/hooks/pipeline-guard.ts +267 -0
  76. package/hooks/session-start.js +106 -0
  77. package/hooks/session-start.ts +44 -32
  78. package/package.json +7 -3
  79. package/plugin.json +1 -1
  80. package/references/agent-protocol.md +15 -0
  81. package/references/complexity.md +54 -0
  82. package/references/definition-of-done.md +1 -1
  83. package/references/multi-actor.md +5 -5
  84. package/references/skill-versioning.md +1 -1
  85. package/references/token-budget.md +30 -8
  86. package/references/wave-banners.md +16 -16
  87. package/scripts/build-hooks.ts +56 -0
  88. package/scripts/conformance.ts +17 -0
  89. package/scripts/coverage-gate.ts +179 -0
  90. package/scripts/install.ps1 +5 -4
  91. package/scripts/install.sh +4 -4
  92. package/scripts/lane.sh +6 -2
  93. package/scripts/lib/patterns.sh +84 -0
  94. package/scripts/mission-report.sh +12 -11
  95. package/scripts/savepoint.sh +141 -37
  96. package/scripts/verify-install.ts +154 -44
  97. package/src/args.ts +1 -1
  98. package/src/cli.ts +217 -0
  99. package/src/continue.ts +243 -0
  100. package/src/installer.ts +39 -0
  101. package/src/onboard.ts +207 -0
  102. package/src/run.ts +82 -0
  103. package/src/targets/claude.ts +140 -2
  104. package/src/targets/copilot.ts +20 -1
  105. package/src/targets/generic.ts +9 -4
  106. package/src/targets/opencode.ts +0 -1
  107. package/content/agents/onboarding-guide.md +0 -124
  108. package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
  109. package/scripts/onboard.ts +0 -266
  110. package/scripts/probe.ts +0 -40
@@ -3,7 +3,7 @@
3
3
  # wave 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] [wave] [mode] [lane]
7
7
  set -u
8
8
 
9
9
  die() { echo "savepoint: $*" >&2; exit 1; }
@@ -42,6 +42,28 @@ MISSION="${1:-${STATE_MISSION:-}}"
42
42
  MEMBER="${2:-${STATE_MEMBER:-}}"
43
43
  WAVE="${3:-${STATE_WAVE:-1}}"
44
44
  MODE="${4:-${STATE_MODE:-guided}}"
45
+ # Triage lane (M7): the lane Luffy assigned at Flow 0. Without it savepoint
46
+ # recomputed the lane from file counts alone and silently discarded the
47
+ # triage decision — a Lane 3 mission recorded itself as "direct". Explicit
48
+ # lane acts as a FLOOR: the computed lane may still raise it, never lower it.
49
+ LANE_EXPLICIT="${5:-${STATE_LANE:-}}"
50
+ case "$LANE_EXPLICIT" in
51
+ ""|direct|lean|standard|full|spike) ;;
52
+ *) die "invalid lane \"$LANE_EXPLICIT\" (one of: direct lean standard full spike)" ;;
53
+ esac
54
+ # Wave may be fractional in the docs (4.5 = the checkpoint between waves).
55
+ # parseInt("4.5")=4 while tr -cd '0-9' gives "45", so state and continue
56
+ # disagreed on the same mission. Normalize once, here, for both writers.
57
+ # Keep only digits and the decimal point, then take the integer part. Garbage
58
+ # input still sanitizes to 0 (the N2 contract the continue writer already had);
59
+ # a fractional wave now truncates identically in BOTH writers instead of
60
+ # splitting into 4 (parseInt) and 45 (digit-strip).
61
+ WAVE_INT=$(printf '%s' "$WAVE" | tr -cd '0-9.')
62
+ WAVE_INT=${WAVE_INT%%.*}
63
+ case "$WAVE_INT" in
64
+ ''|*[!0-9]*) WAVE_INT=0 ;;
65
+ esac
66
+
45
67
  # verbosity from config (project .mugiwara/config), default normal; env override
46
68
  VERBOSITY="${STATE_VERBOSITY:-normal}"
47
69
  if [ -f "$MUGIWARA_DIR/config" ]; then
@@ -52,7 +74,30 @@ case "$VERBOSITY" in
52
74
  normal|full) ;;
53
75
  *) VERBOSITY="normal" ;;
54
76
  esac
55
- ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-}}}}"
77
+
78
+ # heal_max_cycles from config (project .mugiwara/config), default 3; env override
79
+ HEAL_MAX_CYCLES="${STATE_HEAL_MAX_CYCLES:-3}"
80
+ if [ -f "$MUGIWARA_DIR/config" ]; then
81
+ CFG_HEAL_MAX=$(grep -E '^heal_max_cycles=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
82
+ [ -n "$CFG_HEAL_MAX" ] && HEAL_MAX_CYCLES="$CFG_HEAL_MAX"
83
+ fi
84
+ case "$HEAL_MAX_CYCLES" in
85
+ ''|*[!0-9]*) HEAL_MAX_CYCLES=3 ;;
86
+ esac
87
+ [ "$HEAL_MAX_CYCLES" -lt 1 ] 2>/dev/null && HEAL_MAX_CYCLES=3
88
+
89
+ # delegate_threshold from config (project .mugiwara/config), default 60; env override
90
+ DELEGATE_THRESHOLD="${STATE_DELEGATE_THRESHOLD:-60}"
91
+ if [ -f "$MUGIWARA_DIR/config" ]; then
92
+ CFG_DELEGATE=$(grep -E '^delegate_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
93
+ [ -n "$CFG_DELEGATE" ] && DELEGATE_THRESHOLD="$CFG_DELEGATE"
94
+ fi
95
+ case "$DELEGATE_THRESHOLD" in
96
+ ''|*[!0-9]*) DELEGATE_THRESHOLD=60 ;;
97
+ esac
98
+ [ "$DELEGATE_THRESHOLD" -lt 1 ] 2>/dev/null && DELEGATE_THRESHOLD=1
99
+ [ "$DELEGATE_THRESHOLD" -gt 100 ] 2>/dev/null && DELEGATE_THRESHOLD=100
100
+ ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-${USERNAME:-}}}}}"
56
101
  BRANCH="$(git branch --show-current 2>/dev/null || echo 'unknown')"
57
102
 
58
103
  # treat the legacy empty-actor placeholder '""' as "no member" (solo). The old
@@ -96,31 +141,25 @@ else
96
141
  CONTINUE_FILE="$CONTINUE_DIR/state.json"
97
142
  fi
98
143
 
99
- [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode]"
144
+ [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
100
145
  [ -d .git ] || die "not a git repository"
101
146
 
102
147
  # --- computed fields ---
103
148
  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
149
  HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
105
150
 
106
- CHANGED_FILES=$(git diff --name-only "$BASE_SHA"..HEAD 2>/dev/null || git diff --name-only --cached 2>/dev/null || true)
151
+ # union of committed + staged + unstaged + untracked (F) see patterns.sh
152
+ CHANGED_FILES=$(changed_files "$BASE_SHA")
107
153
  FILES_TOUCHED=$( [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | wc -l | tr -d ' ' || echo 0 )
108
154
 
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
155
+ read -r LOC_INS LOC_DEL <<EOF
156
+ $(changed_loc "$BASE_SHA")
157
+ EOF
158
+ LOC_INS=$(( ${LOC_INS:-0} + 0 ))
159
+ LOC_DEL=$(( ${LOC_DEL:-0} + 0 ))
160
+ LOC_DELTA=$(( LOC_INS - LOC_DEL ))
161
+ # churn is insertion+deletion refactors and deletions are work too (D4)
162
+ LOC_CHURN=$(( LOC_INS + LOC_DEL ))
124
163
 
125
164
  SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
126
165
 
@@ -142,7 +181,7 @@ elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
142
181
  LANE_REASON="$FILES_TOUCHED files"
143
182
  elif [ "$FILES_TOUCHED" -eq 1 ] 2>/dev/null; then
144
183
  # 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}')
184
+ ADDED="$LOC_INS"
146
185
  if [ "${ADDED:-0}" -ge 20 ] 2>/dev/null; then
147
186
  LANE="lean"
148
187
  LANE_REASON="1 file, $ADDED LOC"
@@ -167,6 +206,19 @@ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ];
167
206
  fi
168
207
  fi
169
208
 
209
+ # explicit triage lane (M7) applied as a floor — the computed lane above may
210
+ # raise it, never lower it. Sensitive-path escalation still wins
211
+ # unconditionally: spike is a resize, not a rise, so it never displaces the
212
+ # full lane a sensitive path forced.
213
+ if [ -n "$LANE_EXPLICIT" ]; then
214
+ if [ "$LANE_EXPLICIT" = "spike" ] && [ -n "$SENSITIVE_PATHS" ]; then
215
+ LANE_REASON="$LANE_REASON (explicit spike ignored — sensitive paths)"
216
+ elif [ "$(lane_rank "$LANE_EXPLICIT")" -gt "$(lane_rank "$LANE")" ]; then
217
+ LANE_REASON="triage lane $LANE_EXPLICIT (floor; computed $LANE — $LANE_REASON)"
218
+ LANE="$LANE_EXPLICIT"
219
+ fi
220
+ fi
221
+
170
222
  # --- lane-rise compare (F9) + monotonic clamp (D2): read previous state ---
171
223
  # Clamp runs here — before LANE_BASE/BUDGET — so tokens and budget follow the
172
224
  # held lane, never the raw shrunken one.
@@ -217,7 +269,11 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
217
269
  fi
218
270
 
219
271
  # blocker count
220
- BLOCKERS_FILE=$(ls "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" 2>/dev/null || true)
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)
221
277
  BLOCKERS_OPEN=0
222
278
  if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
223
279
  # data rows start with a wave number; header "| wave |" and separator
@@ -225,16 +281,39 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
225
281
  BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
226
282
  fi
227
283
 
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)
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.
231
291
  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)
292
+ 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
305
+ if [ "$HEAL_COUNT" -gt 0 ]; then
235
306
  HEAL_CYCLE=$((HEAL_COUNT + 1))
236
307
  fi
237
308
 
309
+ # heal halt — computed from the heal counter and the config cap (task 4f wires
310
+ # heal_max_cycles into the counter's home). heal_cycle starts at 1, so
311
+ # heal_cycle >= heal_max_cycles means the cap is reached: escalate, never re-run.
312
+ HEAL_HALT=false
313
+ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
314
+ HEAL_HALT=true
315
+ fi
316
+
238
317
  # evidence paths — per-mission results folder (quoted printf, no sed — mission
239
318
  # name is allowlisted above, but avoid sed metacharacter semantics entirely)
240
319
  EVIDENCE=""
@@ -243,12 +322,19 @@ for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
243
322
  EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
244
323
  done
245
324
 
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")
325
+ # skill version MUGIWARA's own package.json, resolved from the script
326
+ # location (B3). The old "$MUGIWARA_DIR/../package.json" was the USER's
327
+ # project manifest: the version-mismatch resume prompt fired when a user
328
+ # shipped their own v2.0.0 and stayed silent across every real mugiwara
329
+ # release. Never fall back to the user's file — "unknown" is the honest
330
+ # answer. Full version, not the major: mugiwara's breaking changes land in
331
+ # 0.x minors, which a major-only string can never see.
332
+ SKILL_VERSION="unknown"
333
+ PKG_JSON="$(dirname "$0")/../package.json"
334
+ if [ -f "$PKG_JSON" ]; then
335
+ 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
336
  fi
337
+ [ -z "$SKILL_VERSION" ] && SKILL_VERSION="unknown"
252
338
 
253
339
  # tokens proxy (F7): deterministic estimate when the harness does not report
254
340
  # real usage. Monotonic beats precise — LANE_BASE stands in for the skills
@@ -291,6 +377,19 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
291
377
  STATUS="warn"
292
378
  fi
293
379
 
380
+ # delegate due — the execution skill's context-pressure trigger (task 4f wires
381
+ # delegate_threshold into a computed flag). tokens_est >= threshold% of budget
382
+ # → remaining sequential tasks dispatch to workers. Threshold stays relative:
383
+ # a bigger budget raises the bar, it does not remove it. Budget 0 (unknown
384
+ # lane) → never due.
385
+ DELEGATE_DUE=false
386
+ if [ "$BUDGET" -gt 0 ] 2>/dev/null; then
387
+ DELEGATE_AT=$(( BUDGET * DELEGATE_THRESHOLD / 100 ))
388
+ if [ "$TOKENS_EST" -ge "$DELEGATE_AT" ] 2>/dev/null; then
389
+ DELEGATE_DUE=true
390
+ fi
391
+ fi
392
+
294
393
  mkdir -p "$MUGIWARA_DIR/state/$MISSION" "$MUGIWARA_DIR/continue/$MISSION"
295
394
 
296
395
  node -e "
@@ -304,7 +403,7 @@ const data = {
304
403
  lane_prev: process.argv[24] || null,
305
404
  lane_peak: process.argv[27] || null,
306
405
  lane_rose: process.argv[25] === 'true',
307
- wave: parseInt(process.argv[6], 10),
406
+ flow: parseInt(process.argv[6], 10),
308
407
  mode: process.argv[7],
309
408
  verbosity: process.argv[32] || 'normal',
310
409
  base_sha: process.argv[8],
@@ -318,6 +417,10 @@ const data = {
318
417
  tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
319
418
  blockers_open: parseInt(process.argv[15], 10),
320
419
  heal_cycle: parseInt(process.argv[16], 10),
420
+ heal_max_cycles: parseInt(process.argv[33], 10) || 3,
421
+ heal_halt: process.argv[34] === 'true',
422
+ delegate_threshold: parseInt(process.argv[35], 10) || 60,
423
+ delegate_due: process.argv[36] === 'true',
321
424
  tokens_est: parseInt(process.argv[17], 10) || 0,
322
425
  tokens_source: process.argv[26] || 'computed',
323
426
  budget: parseInt(process.argv[18], 10) || 0,
@@ -329,13 +432,14 @@ const data = {
329
432
  require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
330
433
  " \
331
434
  "$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
332
- "$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
435
+ "$WAVE_INT" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
333
436
  "$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
334
437
  "$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
335
438
  "$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
336
439
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
337
440
  "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
338
- "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY"
441
+ "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
442
+ "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE"
339
443
 
340
444
  if [ "$LANE_ROSE" = true ]; then
341
445
  echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
@@ -356,7 +460,7 @@ if [ -n "$MISSION" ]; then
356
460
  fi
357
461
  # N2: every field echoed into continue is validated first. MISSION + MEMBER
358
462
  # are allowlisted upstream; WAVE numeric, MODE enum, BRANCH slug-sanitized.
359
- CONT_WAVE=$(echo "$WAVE" | tr -cd '0-9')
463
+ CONT_WAVE="$WAVE_INT"
360
464
  case "$MODE" in
361
465
  guided|semi|auto) CONT_MODE="$MODE" ;;
362
466
  *) CONT_MODE="guided" ;;
@@ -369,7 +473,7 @@ if [ -n "$MISSION" ]; then
369
473
  member: process.argv[2] || null,
370
474
  actor: process.argv[13] || '',
371
475
  branch: process.argv[3],
372
- wave: parseInt(process.argv[4], 10) || 0,
476
+ flow: parseInt(process.argv[4], 10) || 0,
373
477
  mode: process.argv[5],
374
478
  tasks_done: parseInt(process.argv[6], 10) || 0,
375
479
  tasks_total: parseInt(process.argv[7], 10) || 0,
@@ -1,61 +1,72 @@
1
1
  #!/usr/bin/env bun
2
- // scripts/verify-install.ts — G1: verify all references/*.md pointers resolve after install.
3
- // Installs to temp dir for one target per tier, then checks every pointer.
2
+ // scripts/verify-install.ts — G1: prove the shipped prose does not lie.
3
+ //
4
+ // Three checks, all of which failed silently before:
5
+ // A. pointer resolution — every `references/*.md` pointer resolves after install
6
+ // B. prose path validity — every `.mugiwara/…`, `scripts/…`, `docs/…` path a
7
+ // skill or agent NAMES either exists in the repo or matches a declared
8
+ // runtime shape. Four BLOCKER/MAJOR findings shared one root cause:
9
+ // nothing verified that a path named in prose actually exists.
10
+ // C. reference reachability — every reference file has an inbound pointer.
11
+ // Unreachable files still install into every project.
4
12
 
5
13
  import { existsSync, mkdtempSync, rmSync, readFileSync, readdirSync } from 'node:fs';
6
- import { join, dirname } from 'node:path';
14
+ import { join, dirname, basename, relative } from 'node:path';
7
15
  import { tmpdir } from 'node:os';
8
16
  import { installTo } from '../src/installer.ts';
9
- import { targets } from '../src/targets/index.ts';
17
+ import { targets, TARGET_IDS } from '../src/targets/index.ts';
10
18
 
11
- const tierTargets = [
12
- { id: 'claude', tier: 1 },
13
- { id: 'codex', tier: 2 },
14
- { id: 'kilo', tier: 3 },
15
- ];
19
+ const repoRoot = join(import.meta.dirname, '..');
20
+ const fail: string[] = [];
21
+
22
+ function findMd(root: string, out: string[] = []): string[] {
23
+ if (!existsSync(root)) return out;
24
+ for (const ent of readdirSync(root, { withFileTypes: true })) {
25
+ const p = join(root, ent.name);
26
+ if (ent.isDirectory()) findMd(p, out);
27
+ else if (ent.name.endsWith('.md')) out.push(p);
28
+ }
29
+ return out;
30
+ }
16
31
 
17
- let total = 0;
18
- let broken = 0;
32
+ // ---------------------------------------------------------------------------
33
+ // A. pointer resolution, across EVERY install target (was 3 of 9)
34
+ // ---------------------------------------------------------------------------
35
+ let pointers = 0;
36
+ let brokenPointers = 0;
19
37
 
20
- for (const tt of tierTargets) {
21
- const target = targets[tt.id];
22
- if (!target) { console.log(`⚠ target ${tt.id} not found, skipping`); continue; }
23
- const dir = mkdtempSync(join(tmpdir(), `mugi-verify-${tt.id}-`));
38
+ for (const id of TARGET_IDS) {
39
+ const target = targets[id];
40
+ const dir = mkdtempSync(join(tmpdir(), `mugi-verify-${id}-`));
24
41
  try {
25
42
  installTo(target, { scope: 'project', projectDir: dir, dryRun: false, force: true });
26
43
 
27
44
  const skillsDir = target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir;
28
45
  const mugiwaraRefsDir = join(dir, '.mugiwara', 'refs');
29
46
 
30
- function findSkillFiles(root: string): string[] {
31
- const out: string[] = [];
32
- if (!existsSync(root)) return out;
33
- for (const ent of readdirSync(root, { withFileTypes: true })) {
34
- const p = join(root, ent.name);
35
- if (ent.isDirectory()) out.push(...findSkillFiles(p));
36
- else if (ent.name.endsWith('.md')) out.push(p);
37
- }
38
- return out;
39
- }
40
-
41
- const skillFiles = findSkillFiles(skillsDir)
42
- .filter(f => !f.replace(skillsDir, '').includes('/references/'));
47
+ const skillFiles = findMd(skillsDir).filter((f) => !f.replace(skillsDir, '').includes('/references/'));
43
48
 
44
49
  for (const file of skillFiles) {
45
- const body = readFileSync(file, 'utf8');
46
- const refs = [...body.matchAll(/`([^`]*references\/[^`]+\.md)`/g)];
50
+ // Targets emit skills either as `<skillsDir>/<name>/SKILL.md` (claude) or
51
+ // as a flat `<skillsDir>/<name>.instructions.md` (copilot). Both forms
52
+ // must yield the skill name, because non-native targets store references
53
+ // per-skill under `.mugiwara/refs/<name>/`.
54
+ const relSkill = relative(skillsDir, file);
55
+ const skillName = relSkill.includes('/') ? relSkill.split('/')[0] : basename(relSkill).split('.')[0];
47
56
 
48
- for (const m of refs) {
49
- total++;
57
+ for (const m of readFileSync(file, 'utf8').matchAll(/`([^`]*references\/[^`]+\.md)`/g)) {
58
+ pointers++;
50
59
  const pointer = m[1];
51
- const localResolve = join(dirname(file), pointer);
52
- const mugiwaraResolve = join(mugiwaraRefsDir, pointer.replace(/^(?:_shared\/)?references\//, ''));
53
- const resolved = [localResolve, mugiwaraResolve].find(p => existsSync(p));
54
-
55
- if (!resolved) {
56
- broken++;
57
- const relFile = file.replace(dir + '/', '');
58
- console.log(`✗ ${relFile}: \`${pointer}\` → not found`);
60
+ const candidates = [
61
+ join(dirname(file), pointer),
62
+ join(mugiwaraRefsDir, pointer.replace(/^(?:_shared\/)?references\//, '')),
63
+ ...(target.refsDir
64
+ ? [join(target.refsDir({ scope: 'project', projectDir: dir, home: '' }, skillName), basename(pointer))]
65
+ : []),
66
+ ];
67
+ if (!candidates.some((p) => existsSync(p))) {
68
+ brokenPointers++;
69
+ fail.push(`✗ [${id}] ${file.replace(dir + '/', '')}: \`${pointer}\` → not found`);
59
70
  }
60
71
  }
61
72
  }
@@ -64,9 +75,108 @@ for (const tt of tierTargets) {
64
75
  }
65
76
  }
66
77
 
67
- if (broken > 0) {
68
- console.log(`\n✗ ${broken}/${total} pointers cannot resolve after install`);
69
- process.exit(1);
78
+ // ---------------------------------------------------------------------------
79
+ // B. prose path validity
80
+ // ---------------------------------------------------------------------------
81
+ // `.mugiwara/**` paths are written at RUNTIME — they never exist in a clone
82
+ // (`.mugiwara/` is gitignored), so existence proves nothing. What can be
83
+ // verified is the SHAPE: prose must name the same filename template the
84
+ // scripts actually write. This is the check that would have caught
85
+ // savepoint.sh globbing `issues/<mission>-blockers.md` while 15 prose sites
86
+ // mandated the dated `issues/YYYY-MM-DD-<mission>-blockers.md`.
87
+ const RUNTIME_SHAPES: RegExp[] = [
88
+ /^\.mugiwara\/?(\*\*)?$/, // bare mention of the dir
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\/?$/,
97
+ ];
98
+
99
+ // Shape violations that exist today and are NOT fixed in this mission because
100
+ // another worker holds `content/**` prose this wave. Ratchet: a NEW violation
101
+ // fails the gate; these two are reported, then must be deleted from this list.
102
+ // Prose paths whose SHAPE is known-wrong but not yet fixed. Reported every run
103
+ // so they cannot rot quietly; emptied as each is repaired. Keep this empty —
104
+ // an entry here is a defect with a due date, never a permanent exemption.
105
+ const KNOWN_SHAPE_DRIFT = new Map<string, string>([]);
106
+
107
+ /** `[label](path)`, `bun scripts/x.ts --flag`, trailing punctuation → the bare path. */
108
+ function normalizePath(raw: string): string {
109
+ let s = raw.trim();
110
+ const link = s.match(/^\[[^\]]*\]\(([^)]+)\)$/);
111
+ if (link) s = link[1];
112
+ s = s.replace(/^(?:bun|bash|sh|npx|node)\s+/, '');
113
+ s = s.split(/\s/)[0];
114
+ return s.replace(/[.,;:]$/, '');
115
+ }
116
+
117
+ const PROSE_PATH = /`([^`\n]*?(?:\.mugiwara\/|scripts\/|docs\/)[^`\n]*?)`/g;
118
+ const proseFiles = [...findMd(join(repoRoot, 'content')), ...findMd(join(repoRoot, 'references'))];
119
+
120
+ let prosePaths = 0;
121
+ const drift: string[] = [];
122
+ for (const file of proseFiles) {
123
+ const rel = relative(repoRoot, file);
124
+ readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
125
+ for (const m of line.matchAll(PROSE_PATH)) {
126
+ const p = normalizePath(m[1]);
127
+ if (!p.includes('/')) continue;
128
+ prosePaths++;
129
+ const at = `${rel}:${i + 1}`;
130
+
131
+ if (/(^|\/)\.mugiwara\//.test(p) || p === '.mugiwara/' || p === '.mugiwara/**') {
132
+ if (RUNTIME_SHAPES.some((re) => re.test(p))) continue;
133
+ if (KNOWN_SHAPE_DRIFT.has(p)) { drift.push(` ${at}: \`${p}\` — ${KNOWN_SHAPE_DRIFT.get(p)}`); continue; }
134
+ fail.push(`✗ ${at}: \`${p}\` → not a declared runtime path shape`);
135
+ continue;
136
+ }
137
+ // scripts/ and docs/ are SHIPPED files — they must exist in the repo
138
+ if (!existsSync(join(repoRoot, p))) fail.push(`✗ ${at}: \`${p}\` → file does not exist`);
139
+ }
140
+ });
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // C. reference reachability (file → pointer, the inverse of check A)
145
+ // ---------------------------------------------------------------------------
146
+ // Hard zero. The 18 orphans this ratchet was holding were all substantive —
147
+ // worksheets, rubrics and worked examples their own skill visibly needed — so
148
+ // every one was wired to its parent section rather than deleted. With the
149
+ // backlog at zero the ratchet becomes what it should always have been: a new
150
+ // reference file must be pointed at by the prose that needs it, or it is not
151
+ // a reference, it is dead weight shipped to every project.
152
+ const ORPHAN_BASELINE = 0;
153
+
154
+ const corpus = proseFiles.map((f) => ({ file: f, body: readFileSync(f, 'utf8') }));
155
+ const refFiles = proseFiles.filter((f) => f.includes('/references/') || relative(repoRoot, f).startsWith('references/'));
156
+ const orphans: string[] = [];
157
+ for (const rf of refFiles) {
158
+ const needle = `references/${basename(rf)}`;
159
+ const reachable = corpus.some((c) => c.file !== rf && c.body.includes(needle));
160
+ if (!reachable) orphans.push(relative(repoRoot, rf));
161
+ }
162
+ if (orphans.length > ORPHAN_BASELINE) {
163
+ fail.push(
164
+ `✗ ${orphans.length} unreachable reference files (baseline ${ORPHAN_BASELINE}) — a new orphan was added:\n` +
165
+ orphans.map((o) => ` ${o}`).join('\n'),
166
+ );
70
167
  }
71
168
 
72
- console.log(`✓ ${total} pointers checked across ${tierTargets.length} targets — all resolve`);
169
+ // ---------------------------------------------------------------------------
170
+ console.log(` ${pointers} pointers checked across ${TARGET_IDS.length} targets`);
171
+ console.log(` ${prosePaths} prose paths checked in ${proseFiles.length} files`);
172
+ console.log(` ${orphans.length}/${refFiles.length} reference files unreachable (baseline ${ORPHAN_BASELINE})`);
173
+ if (orphans.length) orphans.forEach((o) => console.log(` orphan: ${o}`));
174
+ if (drift.length) { console.log(' known prose-path drift (owned elsewhere, must be fixed):'); drift.forEach((d) => console.log(d)); }
175
+
176
+ if (fail.length) {
177
+ console.log('');
178
+ fail.forEach((f) => console.log(f));
179
+ console.log(`\n✗ verify-install: ${fail.length} problem(s) (${brokenPointers} broken pointers)`);
180
+ process.exit(1);
181
+ }
182
+ console.log('✓ verify-install: pointers resolve, prose paths valid, no new orphans');
package/src/args.ts CHANGED
@@ -9,7 +9,7 @@ export type Args = {
9
9
  const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
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',
12
+ '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all',
13
13
  '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
14
  };
15
15