@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -3
- package/.codex-plugin/plugin.json +1 -2
- package/.cursor-plugin/plugin.json +1 -2
- package/.kimi-plugin/plugin.json +1 -2
- package/.opencode/commands/mugiwara-continue.md +44 -31
- package/.opencode/commands/mugiwara-execute.md +1 -1
- package/.opencode/commands/mugiwara-heal.md +1 -1
- package/.opencode/commands/mugiwara-onboard.md +8 -9
- package/.opencode/commands/mugiwara-plan.md +1 -1
- package/.opencode/commands/mugiwara-review.md +1 -1
- package/.opencode/commands/mugiwara-security.md +1 -1
- package/.opencode/commands/mugiwara-ship.md +1 -1
- package/.opencode/commands/mugiwara.md +2 -2
- package/.opencode/plugins/mugiwara.mjs +1 -2
- package/AGENTS.md +1 -2
- package/README.md +33 -31
- package/content/agents/brook-healing.md +5 -8
- package/content/agents/chopper-checkpoint.md +8 -11
- package/content/agents/eval-runner.md +2 -5
- package/content/agents/franky-gates.md +4 -7
- package/content/agents/jinbe-security.md +4 -7
- package/content/agents/luffy-orchestrator.md +13 -13
- package/content/agents/memory-keeper.md +5 -8
- package/content/agents/nami-planner.md +4 -7
- package/content/agents/resume-coordinator.md +7 -10
- package/content/agents/robin-reviewer.md +6 -9
- package/content/agents/sanji-quality.md +7 -10
- package/content/agents/skeptic-verifier.md +5 -8
- package/content/agents/usopp-brainstorm.md +3 -6
- package/content/agents/zoro-execution.md +7 -10
- package/content/skills/mugiwara-backend/SKILL.md +2 -0
- package/content/skills/mugiwara-checkpoint/SKILL.md +13 -11
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +4 -4
- package/content/skills/mugiwara-context-budget/SKILL.md +5 -3
- package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +20 -20
- package/content/skills/mugiwara-execution/references/dispatch.md +2 -2
- package/content/skills/mugiwara-execution/references/resume-batching.md +1 -1
- package/content/skills/mugiwara-frontend/SKILL.md +2 -4
- package/content/skills/mugiwara-gates/SKILL.md +8 -7
- package/content/skills/mugiwara-git/SKILL.md +7 -2
- package/content/skills/mugiwara-healing/SKILL.md +6 -4
- package/content/skills/mugiwara-healing/references/workers.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +26 -24
- package/content/skills/mugiwara-orchestration/references/check-ins.md +16 -15
- package/content/skills/mugiwara-orchestration/references/closure.md +5 -5
- package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +22 -22
- package/content/skills/mugiwara-planning/SKILL.md +3 -1
- package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
- package/content/skills/mugiwara-pr/SKILL.md +6 -6
- package/content/skills/mugiwara-pr/references/verdict-format.md +3 -3
- package/content/skills/mugiwara-quality/SKILL.md +7 -7
- package/content/skills/mugiwara-resume/SKILL.md +18 -20
- package/content/skills/mugiwara-review/SKILL.md +5 -1
- package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +5 -1
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +5 -3
- package/content/skills/mugiwara-workflow/SKILL.md +27 -27
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +15 -15
- package/content/skills/using-mugiwara/SKILL.md +5 -5
- package/dist/mugiwara.js +808 -33
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +106 -0
- package/hooks/auto-savepoint.ts +102 -0
- package/hooks/engagement-marker.js +59 -0
- package/hooks/engagement-marker.ts +97 -0
- package/hooks/hooks.json +41 -2
- package/hooks/mugiwara-mode-tracker.js +79 -0
- package/hooks/pipeline-guard.js +182 -0
- package/hooks/pipeline-guard.ts +267 -0
- package/hooks/session-start.js +106 -0
- package/hooks/session-start.ts +44 -32
- package/package.json +7 -3
- package/plugin.json +1 -1
- package/references/agent-protocol.md +15 -0
- package/references/complexity.md +54 -0
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +5 -5
- package/references/skill-versioning.md +1 -1
- package/references/token-budget.md +30 -8
- package/references/wave-banners.md +16 -16
- package/scripts/build-hooks.ts +56 -0
- package/scripts/conformance.ts +17 -0
- package/scripts/coverage-gate.ts +179 -0
- package/scripts/install.ps1 +5 -4
- package/scripts/install.sh +4 -4
- package/scripts/lane.sh +6 -2
- package/scripts/lib/patterns.sh +84 -0
- package/scripts/mission-report.sh +12 -11
- package/scripts/savepoint.sh +141 -37
- package/scripts/verify-install.ts +154 -44
- package/src/args.ts +1 -1
- package/src/cli.ts +217 -0
- package/src/continue.ts +243 -0
- package/src/installer.ts +39 -0
- package/src/onboard.ts +207 -0
- package/src/run.ts +82 -0
- package/src/targets/claude.ts +140 -2
- package/src/targets/copilot.ts +20 -1
- package/src/targets/generic.ts +9 -4
- package/src/targets/opencode.ts +0 -1
- package/content/agents/onboarding-guide.md +0 -124
- package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
- package/scripts/onboard.ts +0 -266
- package/scripts/probe.ts +0 -40
package/scripts/savepoint.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
229
|
-
#
|
|
230
|
-
# cause a
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
"$
|
|
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
|
|
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
|
-
|
|
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:
|
|
3
|
-
//
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
21
|
-
const target = targets[
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
49
|
-
|
|
57
|
+
for (const m of readFileSync(file, 'utf8').matchAll(/`([^`]*references\/[^`]+\.md)`/g)) {
|
|
58
|
+
pointers++;
|
|
50
59
|
const pointer = m[1];
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|