@ionivetech/mugiwara 0.6.4 → 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 (115) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.codex-plugin/plugin.json +48 -2
  4. package/.cursor-plugin/plugin.json +48 -2
  5. package/.kimi-plugin/plugin.json +48 -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/mugiwara-helpers.mjs +3 -0
  16. package/.opencode/plugins/mugiwara.mjs +1 -1
  17. package/AGENTS.md +12 -2
  18. package/README.md +53 -32
  19. package/content/agents/brook-healing.md +5 -8
  20. package/content/agents/chopper-checkpoint.md +8 -11
  21. package/content/agents/eval-runner.md +2 -5
  22. package/content/agents/franky-gates.md +4 -7
  23. package/content/agents/jinbe-security.md +4 -7
  24. package/content/agents/luffy-orchestrator.md +13 -13
  25. package/content/agents/memory-keeper.md +5 -8
  26. package/content/agents/nami-planner.md +4 -7
  27. package/content/agents/resume-coordinator.md +7 -10
  28. package/content/agents/robin-reviewer.md +6 -9
  29. package/content/agents/sanji-quality.md +7 -10
  30. package/content/agents/skeptic-verifier.md +5 -8
  31. package/content/agents/usopp-brainstorm.md +3 -6
  32. package/content/agents/zoro-execution.md +7 -10
  33. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  34. package/content/skills/mugiwara-checkpoint/SKILL.md +14 -11
  35. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +4 -4
  36. package/content/skills/mugiwara-context-budget/SKILL.md +5 -3
  37. package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
  38. package/content/skills/mugiwara-execution/SKILL.md +27 -27
  39. package/content/skills/mugiwara-execution/references/dispatch.md +2 -2
  40. package/content/skills/mugiwara-execution/references/resume-batching.md +1 -1
  41. package/content/skills/mugiwara-frontend/SKILL.md +2 -4
  42. package/content/skills/mugiwara-gates/SKILL.md +9 -7
  43. package/content/skills/mugiwara-git/SKILL.md +7 -2
  44. package/content/skills/mugiwara-healing/SKILL.md +6 -4
  45. package/content/skills/mugiwara-healing/references/workers.md +2 -2
  46. package/content/skills/mugiwara-lessons/SKILL.md +1 -0
  47. package/content/skills/mugiwara-orchestration/SKILL.md +34 -35
  48. package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
  49. package/content/skills/mugiwara-orchestration/references/closure.md +5 -5
  50. package/content/skills/mugiwara-orchestration/references/output-contract.md +77 -0
  51. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +22 -22
  52. package/content/skills/mugiwara-planning/SKILL.md +3 -1
  53. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  54. package/content/skills/mugiwara-pr/SKILL.md +6 -6
  55. package/content/skills/mugiwara-pr/references/verdict-format.md +3 -3
  56. package/content/skills/mugiwara-quality/SKILL.md +8 -7
  57. package/content/skills/mugiwara-resume/SKILL.md +19 -20
  58. package/content/skills/mugiwara-review/SKILL.md +6 -1
  59. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
  60. package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
  61. package/content/skills/mugiwara-security/SKILL.md +5 -1
  62. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  63. package/content/skills/mugiwara-ship/references/cleanup.md +1 -1
  64. package/content/skills/mugiwara-testcases/SKILL.md +5 -3
  65. package/content/skills/mugiwara-workflow/SKILL.md +38 -39
  66. package/content/skills/mugiwara-workflow/references/workspace-layout.md +15 -15
  67. package/content/skills/using-mugiwara/SKILL.md +5 -5
  68. package/dist/mugiwara.js +807 -31
  69. package/gemini-extension.json +1 -1
  70. package/hooks/auto-savepoint.js +106 -0
  71. package/hooks/auto-savepoint.ts +102 -0
  72. package/hooks/engagement-marker.js +59 -0
  73. package/hooks/engagement-marker.ts +97 -0
  74. package/hooks/hooks.json +41 -2
  75. package/hooks/mugiwara-mode-tracker.js +79 -0
  76. package/hooks/pipeline-guard.js +182 -0
  77. package/hooks/pipeline-guard.ts +267 -0
  78. package/hooks/session-start.js +106 -0
  79. package/hooks/session-start.ts +44 -32
  80. package/package.json +7 -3
  81. package/plugin.json +1 -1
  82. package/references/agent-protocol.md +15 -0
  83. package/references/complexity.md +54 -0
  84. package/references/definition-of-done.md +1 -1
  85. package/references/multi-actor.md +5 -5
  86. package/references/skill-versioning.md +1 -1
  87. package/references/token-budget.md +30 -8
  88. package/references/wave-banners.md +21 -31
  89. package/scripts/build-hooks.ts +56 -0
  90. package/scripts/conformance.ts +232 -0
  91. package/scripts/coverage-gate.ts +179 -0
  92. package/scripts/evidence.sh +17 -4
  93. package/scripts/gate-selftest.ts +14 -0
  94. package/scripts/initiative.ts +51 -17
  95. package/scripts/install.ps1 +5 -4
  96. package/scripts/install.sh +4 -4
  97. package/scripts/lane.sh +6 -2
  98. package/scripts/lib/patterns.sh +84 -0
  99. package/scripts/mission-report.sh +12 -11
  100. package/scripts/savepoint.sh +152 -37
  101. package/scripts/validate-content.ts +22 -2
  102. package/scripts/verify-install.ts +154 -44
  103. package/src/args.ts +1 -1
  104. package/src/cli.ts +217 -0
  105. package/src/continue.ts +243 -0
  106. package/src/installer.ts +39 -0
  107. package/src/onboard.ts +207 -0
  108. package/src/run.ts +82 -0
  109. package/src/targets/claude.ts +140 -2
  110. package/src/targets/copilot.ts +20 -1
  111. package/src/targets/generic.ts +9 -4
  112. package/content/agents/onboarding-guide.md +0 -124
  113. package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
  114. package/scripts/onboard.ts +0 -266
  115. package/scripts/probe.ts +0 -40
package/scripts/lane.sh CHANGED
@@ -19,9 +19,13 @@ if ! git rev-parse "$BASE" >/dev/null 2>&1; then
19
19
  [ -n "$ALT" ] && BASE="$ALT" || BASE="HEAD~1"
20
20
  fi
21
21
 
22
- CHANGED=$(git diff --name-only "$BASE"..HEAD 2>/dev/null || git diff --name-only --cached 2>/dev/null || true)
22
+ # union of committed + staged + unstaged + untracked (F) see patterns.sh
23
+ CHANGED=$(changed_files "$BASE")
23
24
  FILE_COUNT=0
24
25
  [ -n "$CHANGED" ] && FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
26
+ read -r ADDED_INS ADDED_DEL <<EOF
27
+ $(changed_loc "$BASE")
28
+ EOF
25
29
 
26
30
  SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
27
31
  HAS_SENSITIVE=0
@@ -35,7 +39,7 @@ if [ "$FILE_COUNT" -eq 0 ] 2>/dev/null; then
35
39
  LANE="direct"
36
40
  REASON="no changed files"
37
41
  elif [ "$FILE_COUNT" -le 1 ] 2>/dev/null; then
38
- ADDED=$(git diff --numstat "$BASE"..HEAD 2>/dev/null | awk '{s+=$1} END {print s+0}' || echo 0)
42
+ ADDED="${ADDED_INS:-0}"
39
43
  if [ "$ADDED" -lt 20 ] 2>/dev/null; then
40
44
  LANE="direct"
41
45
  REASON="1 file, <20 LOC"
@@ -19,3 +19,87 @@ SENSITIVE_PATS="auth/|oauth2?/|payment/|payments/|billing/|crypto/|secrets/|cred
19
19
  # outside this surface are docs/config/asset and never escalate to full on
20
20
  # count alone (path-weighted sizing).
21
21
  PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
22
+
23
+ # Installed-harness rules dirs — what `mugiwara install` writes into a project
24
+ # (~50 files across the 9 targets). In a CONSUMER project these are installed
25
+ # config, no different from the crew's own .mugiwara/ bookkeeping: the first
26
+ # savepoint after an install counted them and sized every fresh mission Lane 3
27
+ # before a single line of the user's own work existed. In MUGIWARA'S OWN repo
28
+ # the same dirs are the product surface (they are in PRODUCT_PAT), so the
29
+ # exclusion is conditional on the repo identity, never unconditional.
30
+ # Unanchored on purpose — call sites anchor with ^ (name lists) or a tab
31
+ # (numstat rows).
32
+ HARNESS_PAT="\.claude/|\.opencode/|\.github/(instructions|agents)/|\.gemini/mugiwara/|\.codex/mugiwara/|\.devin/rules/|\.clinerules/|\.kilo/rules/|\.agents/rules/"
33
+
34
+ # harness_excluded — true (exit 0) when harness rules dirs must NOT count
35
+ # toward mission sizing, i.e. this repo is not mugiwara itself. Identity comes
36
+ # from the repo root package.json name ("mugiwara" or "@scope/mugiwara"), the
37
+ # same manifest savepoint already trusts for skill_version.
38
+ harness_excluded() {
39
+ local root
40
+ root=$(git rev-parse --show-toplevel 2>/dev/null || echo .)
41
+ ! grep -qE '"name"[[:space:]]*:[[:space:]]*"(@[^"/]+/)?mugiwara"' "$root/package.json" 2>/dev/null
42
+ }
43
+
44
+ # drop_harness <anchor> — filter stdin, dropping harness paths in consumer
45
+ # repos and passing everything through in mugiwara's own.
46
+ drop_harness() {
47
+ if harness_excluded; then grep -vE "$1($HARNESS_PAT)" || true; else cat; fi
48
+ }
49
+
50
+ # --- working-tree-aware change set (F) -------------------------------------
51
+ # `git diff BASE..HEAD` alone is blind to uncommitted work, and the old
52
+ # `|| git diff --cached` fallback was dead code (|| fires on non-zero exit,
53
+ # never on empty output). With auto_commit=off nothing is committed, so lane
54
+ # sizing and files_touched read 0 while the tree holds real changes.
55
+ # These two helpers are the single source of truth for BOTH lane.sh and
56
+ # savepoint.sh so the two can never drift (lane-integrity case 15).
57
+
58
+ # Path of the crew's bookkeeping dir, relative to the repo root. MUGIWARA_DIR
59
+ # may be absolute (the harness passes a full path); git reports repo-relative
60
+ # paths, so normalize before comparing.
61
+ mugiwara_rel() {
62
+ local d="${MUGIWARA_DIR:-.mugiwara}" root
63
+ case "$d" in
64
+ /*) root=$(git rev-parse --show-toplevel 2>/dev/null || true)
65
+ [ -n "$root" ] && d="${d#"$root"/}"
66
+ # symlinked repo roots (/var vs /private/var on macOS) defeat the
67
+ # prefix strip; fall back to the leaf name, which is what git reports.
68
+ case "$d" in /*) d=$(basename "$d") ;; esac ;;
69
+ esac
70
+ printf '%s' "$d"
71
+ }
72
+
73
+ # changed_files <base-ref> — union of committed (BASE..HEAD), staged, unstaged
74
+ # and untracked files. .gitignore'd files are excluded (git status default),
75
+ # and so is the crew's own .mugiwara/ bookkeeping: savepoint writes state on
76
+ # every wave, and mission state must never size the mission it measures.
77
+ changed_files() {
78
+ local base="${1:-}"
79
+ {
80
+ if [ -n "$base" ] && [ "$base" != "unknown" ]; then
81
+ git diff --name-only "$base"..HEAD 2>/dev/null || true
82
+ fi
83
+ # porcelain covers staged + unstaged + untracked in one pass; strip the
84
+ # 2-char status + space, and keep the destination side of a rename.
85
+ git status --porcelain --untracked-files=all 2>/dev/null | sed 's/^...//; s/^.* -> //' || true
86
+ } | grep -v '^[[:space:]]*$' | grep -v "^$(mugiwara_rel)/" | drop_harness '^' | sort -u
87
+ }
88
+
89
+ # changed_loc <base-ref> — echoes "<insertions> <deletions>" over the same
90
+ # union. Untracked files count every line as an insertion.
91
+ changed_loc() {
92
+ local base="${1:-}" MUGI_REL
93
+ MUGI_REL=$(mugiwara_rel)
94
+ {
95
+ if [ -n "$base" ] && [ "$base" != "unknown" ]; then
96
+ git diff --numstat "$base"..HEAD 2>/dev/null || true
97
+ fi
98
+ git diff --numstat HEAD 2>/dev/null || true
99
+ git ls-files --others --exclude-standard 2>/dev/null | while IFS= read -r f; do
100
+ [ -f "$f" ] || continue
101
+ case "$f" in "$MUGI_REL"/*) continue ;; esac
102
+ printf '%s\t0\t%s\n' "$(wc -l < "$f" 2>/dev/null | tr -d ' ' || echo 0)" "$f"
103
+ done
104
+ } | grep -v " $MUGI_REL/" | drop_harness ' ' | awk '{ if ($1 ~ /^[0-9]+$/) i+=$1; if ($2 ~ /^[0-9]+$/) d+=$2 } END { print (i+0)" "(d+0) }'
105
+ }
@@ -70,11 +70,12 @@ const now = new Date().toISOString().slice(0, 10);
70
70
  const reportFile = path.join(reportDir, now + "-" + mission + ".md");
71
71
  const lane = val(s, 'lane', 'n/a');
72
72
  const lanePeak = val(s, 'lane_peak', 'n/a');
73
+ const laneRose = val(s, 'lane_rose', false);
73
74
  const laneReason = val(s, 'lane_reason', 'n/a');
74
75
  const mode = val(s, 'mode', 'n/a');
75
76
  const actor = val(s, 'actor', 'n/a');
76
77
  const branch = val(s, 'branch', 'n/a');
77
- const wave = val(s, 'wave', 'n/a');
78
+ const wave = val(s, 'flow', val(s, 'wave', 'n/a'));
78
79
  const filesTouched = val(s, 'files_touched', 0);
79
80
  const locDelta = val(s, 'loc_delta', 0);
80
81
  const locChurn = val(s, 'loc_churn', 0);
@@ -91,12 +92,12 @@ const updated = val(s, 'updated_at', now);
91
92
 
92
93
  // --- wave artifact scan: results/<mission>/NN-*.md with verdict sniffing ---
93
94
  const WAVE_LABEL = {
94
- "01-execution": "Execute (Wave 3)",
95
- "02-audit": "Checkpoint (Wave 4)",
96
- "03-quality": "Quality (Wave 5)",
97
- "04-gates": "Gates (Wave 6)",
98
- "05-healing": "Healing (Wave 8)",
99
- "06-closure": "Closure (Wave 9)",
95
+ "01-execution": "Execute (Flow 3)",
96
+ "02-audit": "Checkpoint (Flow 4)",
97
+ "03-quality": "Quality (Flow 5)",
98
+ "04-gates": "Gates (Flow 6)",
99
+ "05-healing": "Healing (Flow 8)",
100
+ "06-closure": "Closure (Flow 9)",
100
101
  "07-pr-verdict": "PR material",
101
102
  };
102
103
 
@@ -227,11 +228,11 @@ let report = "# Mission: " + mission + " . " + now + "\n\n";
227
228
  report += "## Mission header\n\n| Field | Value |\n|-------|-------|\n";
228
229
  report += "| Mission | " + mission + " |\n";
229
230
  report += "| Branch | " + branch + " |\n";
230
- report += "| Lane | " + lane + " |\n";
231
+ report += "| Lane | " + lane + (laneRose ? " ⬆ ROSE" : "") + " |\n";
231
232
  report += "| Lane peak | " + lanePeak + " |\n";
232
233
  report += "| Lane reason | " + laneReason + " |\n";
233
234
  report += "| Mode | " + mode + " |\n";
234
- report += "| Wave | " + wave + " |\n";
235
+ report += "| Flow | " + wave + " |\n";
235
236
  report += "| Actor | " + actor + " |\n\n";
236
237
 
237
238
  report += "## Token budget\n\n| Field | Value |\n|-------|-------|\n";
@@ -252,7 +253,7 @@ report += filesTouched + " files, +" + locDelta + " LOC (" + locChurn + " churn)
252
253
  if (sensitive.length) report += "\nSensitive paths: " + sensitive.join(", ");
253
254
  report += "\n\n";
254
255
 
255
- report += "## Waves\n\n| Wave | Artifact | Verdict |\n|------|----------|---------|";
256
+ report += "## Flow stages\n\n| Flow stage | Artifact | Verdict |\n|------|----------|---------|";
256
257
  if (waveRows.length) {
257
258
  for (const r of waveRows) report += "\n| " + r.label + " | `" + r.file + "` | " + r.verdict + " |";
258
259
  } else {
@@ -277,7 +278,7 @@ report += "Blocker ledger rows: " + issueRows.length + (issueRows.length ? "\n"
277
278
  report += "\n\n";
278
279
 
279
280
  report += "## State\n\n| Field | Value |\n|-------|-------|\n";
280
- report += "| Wave | " + wave + " |\n";
281
+ report += "| Flow | " + wave + " |\n";
281
282
  report += "| Tasks | " + (tasks.done || 0) + "/" + (tasks.total || 0) + " done |\n";
282
283
  report += "| Blockers open | " + blockers + " |\n";
283
284
  report += "| Heal cycles | " + healCycle + " |\n";
@@ -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,7 +42,62 @@ 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
- ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-}}}}"
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
+
67
+ # verbosity from config (project .mugiwara/config), default normal; env override
68
+ VERBOSITY="${STATE_VERBOSITY:-normal}"
69
+ if [ -f "$MUGIWARA_DIR/config" ]; then
70
+ CFG_VERBOSITY=$(grep -E '^verbosity=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
71
+ [ -n "$CFG_VERBOSITY" ] && VERBOSITY="$CFG_VERBOSITY"
72
+ fi
73
+ case "$VERBOSITY" in
74
+ normal|full) ;;
75
+ *) VERBOSITY="normal" ;;
76
+ esac
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:-}}}}}"
46
101
  BRANCH="$(git branch --show-current 2>/dev/null || echo 'unknown')"
47
102
 
48
103
  # treat the legacy empty-actor placeholder '""' as "no member" (solo). The old
@@ -86,31 +141,25 @@ else
86
141
  CONTINUE_FILE="$CONTINUE_DIR/state.json"
87
142
  fi
88
143
 
89
- [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode]"
144
+ [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
90
145
  [ -d .git ] || die "not a git repository"
91
146
 
92
147
  # --- computed fields ---
93
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")
94
149
  HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
95
150
 
96
- 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")
97
153
  FILES_TOUCHED=$( [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | wc -l | tr -d ' ' || echo 0 )
98
154
 
99
- LOC_INS=0
100
- LOC_DEL=0
101
- LOC_DELTA=0
102
- LOC_CHURN=0
103
- if [ "$BASE_SHA" != "unknown" ]; then
104
- STAT=$(git diff --shortstat "$BASE_SHA"..HEAD 2>/dev/null || echo "")
105
- INS=$(echo "$STAT" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
106
- DEL=$(echo "$STAT" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
107
- LOC_INS=$(( ${INS:-0} + 0 ))
108
- LOC_DEL=$(( ${DEL:-0} + 0 ))
109
- LOC_DELTA=$(( LOC_INS - LOC_DEL ))
110
- # churn is insertion+deletion — refactors and deletions are work too (D4)
111
- LOC_CHURN=$(( LOC_INS + LOC_DEL ))
112
- fi
113
- [ -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 ))
114
163
 
115
164
  SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
116
165
 
@@ -132,7 +181,7 @@ elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
132
181
  LANE_REASON="$FILES_TOUCHED files"
133
182
  elif [ "$FILES_TOUCHED" -eq 1 ] 2>/dev/null; then
134
183
  # 1-file rule mirrors lane.sh: >=20 added LOC -> lean, else direct
135
- ADDED=$(git diff --numstat "$BASE_SHA"..HEAD 2>/dev/null | awk '{s+=$1} END {print s+0}')
184
+ ADDED="$LOC_INS"
136
185
  if [ "${ADDED:-0}" -ge 20 ] 2>/dev/null; then
137
186
  LANE="lean"
138
187
  LANE_REASON="1 file, $ADDED LOC"
@@ -157,6 +206,19 @@ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ];
157
206
  fi
158
207
  fi
159
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
+
160
222
  # --- lane-rise compare (F9) + monotonic clamp (D2): read previous state ---
161
223
  # Clamp runs here — before LANE_BASE/BUDGET — so tokens and budget follow the
162
224
  # held lane, never the raw shrunken one.
@@ -207,7 +269,11 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
207
269
  fi
208
270
 
209
271
  # blocker count
210
- 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)
211
277
  BLOCKERS_OPEN=0
212
278
  if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
213
279
  # data rows start with a wave number; header "| wave |" and separator
@@ -215,16 +281,39 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
215
281
  BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
216
282
  fi
217
283
 
218
- # heal cycle — count WAVE-8 banner occurrences in the trace, not the word
219
- # "heal" anywhere (heal workers/healing text would inflate the counter and
220
- # 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.
221
291
  HEAL_CYCLE=1
222
- TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
223
- if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
224
- 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
225
306
  HEAL_CYCLE=$((HEAL_COUNT + 1))
226
307
  fi
227
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
+
228
317
  # evidence paths — per-mission results folder (quoted printf, no sed — mission
229
318
  # name is allowlisted above, but avoid sed metacharacter semantics entirely)
230
319
  EVIDENCE=""
@@ -233,12 +322,19 @@ for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
233
322
  EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
234
323
  done
235
324
 
236
- # skill version from package.json (argv-passing never interpolate paths into node -e)
237
- SKILL_VERSION="1"
238
- if [ -f package.json ]; then
239
- PKG_JSON="$MUGIWARA_DIR/../package.json"
240
- [ -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")
241
336
  fi
337
+ [ -z "$SKILL_VERSION" ] && SKILL_VERSION="unknown"
242
338
 
243
339
  # tokens proxy (F7): deterministic estimate when the harness does not report
244
340
  # real usage. Monotonic beats precise — LANE_BASE stands in for the skills
@@ -281,6 +377,19 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
281
377
  STATUS="warn"
282
378
  fi
283
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
+
284
393
  mkdir -p "$MUGIWARA_DIR/state/$MISSION" "$MUGIWARA_DIR/continue/$MISSION"
285
394
 
286
395
  node -e "
@@ -294,8 +403,9 @@ const data = {
294
403
  lane_prev: process.argv[24] || null,
295
404
  lane_peak: process.argv[27] || null,
296
405
  lane_rose: process.argv[25] === 'true',
297
- wave: parseInt(process.argv[6], 10),
406
+ flow: parseInt(process.argv[6], 10),
298
407
  mode: process.argv[7],
408
+ verbosity: process.argv[32] || 'normal',
299
409
  base_sha: process.argv[8],
300
410
  head_sha: process.argv[9],
301
411
  files_touched: parseInt(process.argv[10], 10),
@@ -307,6 +417,10 @@ const data = {
307
417
  tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
308
418
  blockers_open: parseInt(process.argv[15], 10),
309
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',
310
424
  tokens_est: parseInt(process.argv[17], 10) || 0,
311
425
  tokens_source: process.argv[26] || 'computed',
312
426
  budget: parseInt(process.argv[18], 10) || 0,
@@ -318,13 +432,14 @@ const data = {
318
432
  require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
319
433
  " \
320
434
  "$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
321
- "$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
435
+ "$WAVE_INT" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
322
436
  "$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
323
437
  "$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
324
438
  "$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
325
439
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
326
440
  "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
327
- "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER"
441
+ "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
442
+ "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE"
328
443
 
329
444
  if [ "$LANE_ROSE" = true ]; then
330
445
  echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
@@ -345,7 +460,7 @@ if [ -n "$MISSION" ]; then
345
460
  fi
346
461
  # N2: every field echoed into continue is validated first. MISSION + MEMBER
347
462
  # are allowlisted upstream; WAVE numeric, MODE enum, BRANCH slug-sanitized.
348
- CONT_WAVE=$(echo "$WAVE" | tr -cd '0-9')
463
+ CONT_WAVE="$WAVE_INT"
349
464
  case "$MODE" in
350
465
  guided|semi|auto) CONT_MODE="$MODE" ;;
351
466
  *) CONT_MODE="guided" ;;
@@ -358,7 +473,7 @@ if [ -n "$MISSION" ]; then
358
473
  member: process.argv[2] || null,
359
474
  actor: process.argv[13] || '',
360
475
  branch: process.argv[3],
361
- wave: parseInt(process.argv[4], 10) || 0,
476
+ flow: parseInt(process.argv[4], 10) || 0,
362
477
  mode: process.argv[5],
363
478
  tasks_done: parseInt(process.argv[6], 10) || 0,
364
479
  tasks_total: parseInt(process.argv[7], 10) || 0,
@@ -207,10 +207,15 @@ for (const f of [...agentFiles, ...skillDirs.map(d => join(root, 'skills', d, 'S
207
207
  }
208
208
  }
209
209
 
210
- // --- manifest-sync check: .claude-plugin/plugin.json must set-equal content/ ---
210
+ // --- manifest-sync check: every marketplace plugin.json set-equal content/ ---
211
211
  const manifestArg = process.argv.indexOf('--check-manifest');
212
212
  if (manifestArg !== -1) {
213
- const manifests = ['.claude-plugin/plugin.json'];
213
+ const manifests = [
214
+ '.claude-plugin/plugin.json',
215
+ '.codex-plugin/plugin.json',
216
+ '.cursor-plugin/plugin.json',
217
+ '.kimi-plugin/plugin.json',
218
+ ];
214
219
  let manifestErrors = 0;
215
220
 
216
221
  for (const mp of manifests) {
@@ -380,5 +385,20 @@ if (integrityArg !== -1) {
380
385
  }
381
386
  }
382
387
 
388
+ // Conditional-assertion guard: an expect() reachable only inside a truthiness
389
+ // check silently passes when the value is absent. This class produced 9 defects.
390
+ // Allowed: checks keyed on a declared invariant (tier, fixture keys).
391
+ const ALLOWED_COND = /if \((?:t\.tier === 3|keys\.length|label === 'exact'|fx\.expect\.(?:lane|sensitive_paths_(?:min|max)))/;
392
+ const repoRoot = join(import.meta.dirname, '..');
393
+ for (const f of readdirSync(join(repoRoot, 'test')).filter(x => x.endsWith('.test.ts'))) {
394
+ const src = readFileSync(join(repoRoot, 'test', f), 'utf8');
395
+ // matches both braced and brace-less conditionals whose body reaches an expect()
396
+ for (const m of src.matchAll(/if \([^)]+\)(?:\s*\{[^}]{0,400}?)?expect\(/gs)) {
397
+ if (!ALLOWED_COND.test(m[0])) {
398
+ errors.push(`test/${f}: expect() inside a conditional — assert presence explicitly instead:\n ${m[0].slice(0, 80)}`);
399
+ }
400
+ }
401
+ }
402
+
383
403
  if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
384
404
  console.log(`✓ content valid: ${skillDirs.length} skills, ${agentFiles.length} agents`);