@ionivetech/mugiwara 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/README.md +61 -56
- package/content/agents/luffy-orchestrator.md +1 -0
- package/content/skills/mugiwara-lessons/SKILL.md +2 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +11 -20
- package/content/skills/mugiwara-orchestration/references/solo-team.md +18 -0
- package/content/skills/mugiwara-planning/SKILL.md +7 -15
- package/content/skills/mugiwara-planning/references/sub-missions.md +14 -0
- package/content/skills/mugiwara-workflow/SKILL.md +20 -20
- package/dist/mugiwara.js +863 -133
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/references/multi-actor.md +17 -14
- package/scripts/gate-selftest.ts +294 -1
- package/scripts/lane-base.ts +16 -0
- package/scripts/lane.sh +5 -1
- package/scripts/lib/lane-base.sh +1 -1
- package/scripts/savepoint.sh +151 -15
- package/scripts/validate-content.ts +168 -0
- package/src/args.ts +1 -1
- package/src/cli.ts +203 -21
- package/src/config.ts +33 -11
- package/src/continue.ts +7 -1
- package/src/cost.ts +1 -1
- package/src/installer.ts +27 -4
- package/src/integrity.ts +41 -10
- package/src/mission.ts +137 -52
- package/src/policy.ts +17 -2
package/scripts/savepoint.sh
CHANGED
|
@@ -8,6 +8,18 @@ set -u
|
|
|
8
8
|
|
|
9
9
|
die() { echo "savepoint: $*" >&2; exit 1; }
|
|
10
10
|
|
|
11
|
+
# count_boxes <file> <char-class> — count markdown checkboxes.
|
|
12
|
+
# Anchored so prose mentioning "- [x]" is not counted; skips fenced code blocks
|
|
13
|
+
# so documentation examples are not counted; matches [x] and [X] alike. (B3)
|
|
14
|
+
count_boxes() {
|
|
15
|
+
[ -f "$1" ] || { echo 0; return; }
|
|
16
|
+
awk -v pat="$2" '
|
|
17
|
+
/^[[:space:]]*```/ { inblock = !inblock; next }
|
|
18
|
+
!inblock && $0 ~ ("^[[:space:]]*-[[:space:]]*\\[" pat "\\]") { n++ }
|
|
19
|
+
END { print n+0 }
|
|
20
|
+
' "$1"
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
|
|
12
24
|
|
|
13
25
|
# optional provider-reported tokens file (T4): --tokens-file <path> JSON {input_tokens, output_tokens}
|
|
@@ -51,9 +63,27 @@ if [ -n "$GIT_NAME" ] && [ -n "$GIT_EMAIL" ]; then GIT_ID="$GIT_NAME <$GIT_EMAIL
|
|
|
51
63
|
|
|
52
64
|
# --- parse mission args: <mission> [member] [wave] [mode] ---
|
|
53
65
|
MISSION="${1:-${STATE_MISSION:-}}"
|
|
66
|
+
# member: positional > env > config team_member > empty (solo). Never derive
|
|
67
|
+
# silently from git identity — solo vs team is a recorded Flow 0 decision, not
|
|
68
|
+
# an inference. (W3)
|
|
54
69
|
MEMBER="${2:-${STATE_MEMBER:-}}"
|
|
70
|
+
if [ -z "$MEMBER" ] && [ -f "$MUGIWARA_DIR/config" ]; then
|
|
71
|
+
MEMBER=$(grep -E '^team_member=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
72
|
+
fi
|
|
55
73
|
WAVE="${3:-${STATE_WAVE:-1}}"
|
|
56
|
-
|
|
74
|
+
# mode: positional > env > project config > global config > guided. The hook
|
|
75
|
+
# passes it positionally; direct script and CLI calls must fall back to config
|
|
76
|
+
# or 11 of 12 harnesses record the wrong mode. (W1)
|
|
77
|
+
MODE="${4:-${STATE_MODE:-}}"
|
|
78
|
+
if [ -z "$MODE" ]; then
|
|
79
|
+
for _cfg in "$MUGIWARA_DIR/config" "$HOME/.mugiwara/config"; do
|
|
80
|
+
[ -f "$_cfg" ] || continue
|
|
81
|
+
_m=$(grep -E '^mode=' "$_cfg" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
82
|
+
[ -n "$_m" ] && { MODE="$_m"; break; }
|
|
83
|
+
done
|
|
84
|
+
fi
|
|
85
|
+
MODE="${MODE:-guided}"
|
|
86
|
+
case "$MODE" in guided|semi|auto) ;; *) MODE="guided" ;; esac
|
|
57
87
|
# Triage lane (M7): the lane Luffy assigned at Flow 0. Without it savepoint
|
|
58
88
|
# recomputed the lane from file counts alone and silently discarded the
|
|
59
89
|
# triage decision — a Lane 3 mission recorded itself as "direct". Explicit
|
|
@@ -79,7 +109,7 @@ esac
|
|
|
79
109
|
# verbosity from config (project .mugiwara/config), default normal; env override
|
|
80
110
|
VERBOSITY="${STATE_VERBOSITY:-normal}"
|
|
81
111
|
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
82
|
-
CFG_VERBOSITY=$(grep -E '^verbosity=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
112
|
+
CFG_VERBOSITY=$(grep -E '^verbosity=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
83
113
|
[ -n "$CFG_VERBOSITY" ] && VERBOSITY="$CFG_VERBOSITY"
|
|
84
114
|
fi
|
|
85
115
|
case "$VERBOSITY" in
|
|
@@ -90,7 +120,7 @@ esac
|
|
|
90
120
|
# heal_max_cycles from config (project .mugiwara/config), default 3; env override
|
|
91
121
|
HEAL_MAX_CYCLES="${STATE_HEAL_MAX_CYCLES:-3}"
|
|
92
122
|
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
93
|
-
CFG_HEAL_MAX=$(grep -E '^heal_max_cycles=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
123
|
+
CFG_HEAL_MAX=$(grep -E '^heal_max_cycles=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
94
124
|
[ -n "$CFG_HEAL_MAX" ] && HEAL_MAX_CYCLES="$CFG_HEAL_MAX"
|
|
95
125
|
fi
|
|
96
126
|
case "$HEAL_MAX_CYCLES" in
|
|
@@ -101,7 +131,7 @@ esac
|
|
|
101
131
|
# delegate_threshold from config (project .mugiwara/config), default 60; env override
|
|
102
132
|
DELEGATE_THRESHOLD="${STATE_DELEGATE_THRESHOLD:-60}"
|
|
103
133
|
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
104
|
-
CFG_DELEGATE=$(grep -E '^delegate_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
134
|
+
CFG_DELEGATE=$(grep -E '^delegate_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
105
135
|
[ -n "$CFG_DELEGATE" ] && DELEGATE_THRESHOLD="$CFG_DELEGATE"
|
|
106
136
|
fi
|
|
107
137
|
case "$DELEGATE_THRESHOLD" in
|
|
@@ -154,6 +184,11 @@ esac
|
|
|
154
184
|
# (BSD/macOS-safe: no \+ BRE).
|
|
155
185
|
BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-' | tr -cd 'A-Za-z0-9._-' | sed 's/^\.\{1,\}$//' )
|
|
156
186
|
|
|
187
|
+
# Resolve the repo root: handles subdirectories and git worktrees, where .git
|
|
188
|
+
# is a file rather than a directory. (B4)
|
|
189
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || die "not a git repository"
|
|
190
|
+
cd "$REPO_ROOT" || die "cannot enter repo root"
|
|
191
|
+
|
|
157
192
|
# state + continue live in the mission dir. Solo (member empty) → state.json
|
|
158
193
|
# + continue.json; team writes <member>.json + continue-<member>.json so
|
|
159
194
|
# parallel members never clobber each other.
|
|
@@ -166,8 +201,18 @@ else
|
|
|
166
201
|
CONTINUE_FILE="$MISSION_DIR/continue.json"
|
|
167
202
|
fi
|
|
168
203
|
|
|
204
|
+
# A mission is solo or team, never both. Two layouts side by side orphan one of
|
|
205
|
+
# them: hidden from `status`, still read by the integrity gate. (W4)
|
|
206
|
+
if [ -n "$MEMBER" ] && [ -f "$MISSION_DIR/state.json" ] && [ "${MUGIWARA_ALLOW_LAYOUT_SWITCH:-0}" != "1" ]; then
|
|
207
|
+
die "mission '$MISSION' is solo (state.json exists) — refusing to add member '$MEMBER'. Run: mugiwara migrate --to-team $MEMBER"
|
|
208
|
+
fi
|
|
209
|
+
if [ -z "$MEMBER" ] && ls "$MISSION_DIR"/*.json >/dev/null 2>&1; then
|
|
210
|
+
if ls "$MISSION_DIR"/*.json | grep -qv -e 'state.json' -e 'continue'; then
|
|
211
|
+
die "mission '$MISSION' is team — pass a member: savepoint.sh $MISSION <member> ..."
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
|
|
169
215
|
[ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
|
|
170
|
-
[ -d .git ] || die "not a git repository"
|
|
171
216
|
|
|
172
217
|
# --- computed fields ---
|
|
173
218
|
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")
|
|
@@ -176,7 +221,7 @@ HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
|
176
221
|
# lane_scope_glob (T5): monorepo scoping — count only files matching the glob
|
|
177
222
|
LANE_SCOPE_GLOB=""
|
|
178
223
|
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
179
|
-
_cfg_scope=$(grep -E '^lane_scope_glob=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
|
|
224
|
+
_cfg_scope=$(grep -E '^lane_scope_glob=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
|
|
180
225
|
[ -n "$_cfg_scope" ] && LANE_SCOPE_GLOB="$_cfg_scope"
|
|
181
226
|
fi
|
|
182
227
|
# union of committed + staged + unstaged + untracked (F) — see patterns.sh
|
|
@@ -328,16 +373,23 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
|
328
373
|
# total counts ALL task lines (checked + unchecked); done counts checked only.
|
|
329
374
|
# A fully-completed plan must read total=N done=N, never total=0 (the old
|
|
330
375
|
# unchecked-only grep degenerated a done plan to tasks.total=0).
|
|
331
|
-
TASKS_TOTAL=$(
|
|
332
|
-
TASKS_DONE=$(
|
|
376
|
+
TASKS_TOTAL=$(count_boxes "$PLAN_FILE" '[ xX]')
|
|
377
|
+
TASKS_DONE=$(count_boxes "$PLAN_FILE" '[xX]')
|
|
333
378
|
fi
|
|
334
379
|
# Fallback for large campaigns (>3 phases, >1500 lines) where master plan.md is an index
|
|
335
380
|
# and tasks live in sub-plan/*.md — only when plan.md has zero checkbox tasks to
|
|
336
381
|
# keep simple missions unchanged.
|
|
337
382
|
if [ "${TASKS_TOTAL:-0}" -eq 0 ] 2>/dev/null && [ -d "$MISSION_DIR/sub-plan" ]; then
|
|
338
|
-
TASKS_TOTAL
|
|
339
|
-
|
|
383
|
+
TASKS_TOTAL=0; TASKS_DONE=0
|
|
384
|
+
for _sp in "$MISSION_DIR"/sub-plan/*.md; do
|
|
385
|
+
[ -f "$_sp" ] || continue
|
|
386
|
+
TASKS_TOTAL=$(( TASKS_TOTAL + $(count_boxes "$_sp" '[ xX]') ))
|
|
387
|
+
TASKS_DONE=$(( TASKS_DONE + $(count_boxes "$_sp" '[xX]') ))
|
|
388
|
+
done
|
|
340
389
|
fi
|
|
390
|
+
# done ≤ total is an invariant of the audit trail — never let a report show
|
|
391
|
+
# progress above 100%, whatever the plan file contains. (B3)
|
|
392
|
+
[ "${TASKS_DONE:-0}" -gt "${TASKS_TOTAL:-0}" ] 2>/dev/null && TASKS_DONE="$TASKS_TOTAL"
|
|
341
393
|
|
|
342
394
|
# blocker count
|
|
343
395
|
BLOCKERS_FILE="$MISSION_DIR/blockers.md"
|
|
@@ -372,6 +424,16 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
|
|
|
372
424
|
HEAL_HALT=true
|
|
373
425
|
fi
|
|
374
426
|
|
|
427
|
+
# W10: register plan.md read so repeated_reads is not structurally zero (evidence.registerRead)
|
|
428
|
+
if [ -f "$MISSION_DIR/plan.md" ] && [ ! -s "$MISSION_DIR/context-registry.jsonl" ]; then
|
|
429
|
+
mkdir -p "$MISSION_DIR"
|
|
430
|
+
_plan_fp=$(node -e "const crypto=require('crypto');const fs=require('fs');try{const d=fs.readFileSync(process.argv[1],'utf8');process.stdout.write(crypto.createHash('sha256').update(d).digest('hex'))}catch(e){process.stdout.write('')}" "$MISSION_DIR/plan.md" 2>/dev/null || true)
|
|
431
|
+
if [ -n "$_plan_fp" ]; then
|
|
432
|
+
_plan_chars=$(wc -c < "$MISSION_DIR/plan.md" 2>/dev/null | tr -d ' ' || echo 0)
|
|
433
|
+
printf '{"fingerprint":"%s","kind":"file","file":"plan.md","id":"E001","reads":1,"chars":%s,"ref":"E001 plan.md"}\n' "$_plan_fp" "$_plan_chars" >> "$MISSION_DIR/context-registry.jsonl" 2>/dev/null || true
|
|
434
|
+
fi
|
|
435
|
+
fi
|
|
436
|
+
|
|
375
437
|
# slop — context (repeated reads) per cost-governor §§21-24,31-32 — T5 wire all crews Luffy/Nami/Zoro/Brook
|
|
376
438
|
REPEATED_READS=0
|
|
377
439
|
REPEATED_THRESHOLD=3
|
|
@@ -387,11 +449,11 @@ fi
|
|
|
387
449
|
# gates flow stage can read, not prose.
|
|
388
450
|
DEPTH_REVIEW="full"; DEPTH_QUALITY="full"; DEPTH_VERIFY="off"
|
|
389
451
|
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
390
|
-
_cfg_r=$(grep -E '^review_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
452
|
+
_cfg_r=$(grep -E '^review_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
391
453
|
[ -n "$_cfg_r" ] && DEPTH_REVIEW="$_cfg_r"
|
|
392
|
-
_cfg_q=$(grep -E '^quality_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
454
|
+
_cfg_q=$(grep -E '^quality_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
393
455
|
[ -n "$_cfg_q" ] && DEPTH_QUALITY="$_cfg_q"
|
|
394
|
-
_cfg_v=$(grep -E '^verify_merged=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
456
|
+
_cfg_v=$(grep -E '^verify_merged=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
395
457
|
[ -n "$_cfg_v" ] && DEPTH_VERIFY="$_cfg_v"
|
|
396
458
|
fi
|
|
397
459
|
case "$DEPTH_REVIEW" in full|standard|lean) ;; *) DEPTH_REVIEW="full" ;; esac
|
|
@@ -499,6 +561,75 @@ if [ "$BUDGET" -gt 0 ] 2>/dev/null; then
|
|
|
499
561
|
fi
|
|
500
562
|
fi
|
|
501
563
|
|
|
564
|
+
# team_members for posture (W5) — config key team_members, default 1
|
|
565
|
+
TEAM_MEMBERS=1
|
|
566
|
+
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
567
|
+
_tm=$(grep -E '^team_members=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
568
|
+
[ -n "$_tm" ] && TEAM_MEMBERS="$_tm"
|
|
569
|
+
fi
|
|
570
|
+
case "$TEAM_MEMBERS" in ''|*[!0-9]*) TEAM_MEMBERS=1 ;; esac
|
|
571
|
+
# plan metrics for posture decision (phase-isolated / parallel)
|
|
572
|
+
PLAN_LINES=0
|
|
573
|
+
PHASES=1
|
|
574
|
+
INDEPENDENT_TASKS=0
|
|
575
|
+
if [ -f "$MISSION_DIR/plan.md" ]; then
|
|
576
|
+
PLAN_LINES=$(wc -l < "$MISSION_DIR/plan.md" 2>/dev/null | tr -d ' ' || echo 0)
|
|
577
|
+
PH=$(grep -c "^## Wave" "$MISSION_DIR/plan.md" 2>/dev/null || true)
|
|
578
|
+
[ "$PH" -gt 0 ] 2>/dev/null && PHASES="$PH"
|
|
579
|
+
INDEPENDENT_TASKS=$(grep -c "\[PARALLEL\]" "$MISSION_DIR/plan.md" 2>/dev/null || true)
|
|
580
|
+
fi
|
|
581
|
+
# governor for posture
|
|
582
|
+
GOVERNOR="normal"
|
|
583
|
+
case "$STATUS" in
|
|
584
|
+
stop) GOVERNOR="stop" ;;
|
|
585
|
+
warn) GOVERNOR="avoid" ;;
|
|
586
|
+
*) GOVERNOR="normal" ;;
|
|
587
|
+
esac
|
|
588
|
+
CONTEXT_PRESSURE=false
|
|
589
|
+
if [ "$BUDGET" -gt 0 ] 2>/dev/null && [ "$TOKENS_EST" -gt $(( BUDGET * 6 / 10 )) ] 2>/dev/null; then
|
|
590
|
+
CONTEXT_PRESSURE=true
|
|
591
|
+
fi
|
|
592
|
+
POSTURE="inline-sequential"
|
|
593
|
+
POSTURE_REASON="no parallel/phase/team/relief trigger — default inline in plan order"
|
|
594
|
+
POSTURE_PAUSE=false
|
|
595
|
+
if [ "$GOVERNOR" = "stop" ]; then
|
|
596
|
+
POSTURE="inline-sequential"
|
|
597
|
+
POSTURE_REASON="governor stop — pause safely, keep inline; state + continue emitted"
|
|
598
|
+
POSTURE_PAUSE=true
|
|
599
|
+
elif [ "$TEAM_MEMBERS" -gt 1 ] 2>/dev/null; then
|
|
600
|
+
POSTURE="team-scoped"
|
|
601
|
+
POSTURE_REASON="$TEAM_MEMBERS team members with non-overlapping scope"
|
|
602
|
+
elif [ "$PHASES" -gt 3 ] 2>/dev/null || [ "$PLAN_LINES" -gt 1500 ] 2>/dev/null; then
|
|
603
|
+
POSTURE="phase-isolated"
|
|
604
|
+
POSTURE_REASON="large campaign — $PHASES phases / $PLAN_LINES lines"
|
|
605
|
+
elif [ "$CONTEXT_PRESSURE" = true ]; then
|
|
606
|
+
POSTURE="context-relief"
|
|
607
|
+
POSTURE_REASON="context pressure with ordered dependent tasks — one worker at a time, order preserved"
|
|
608
|
+
elif [ "$INDEPENDENT_TASKS" -ge 2 ] 2>/dev/null; then
|
|
609
|
+
POSTURE="parallel-workers"
|
|
610
|
+
POSTURE_REASON="$INDEPENDENT_TASKS independent tasks, no shared files/interfaces"
|
|
611
|
+
fi
|
|
612
|
+
# investigation config (W8) — three keys, defaults 2/5/2
|
|
613
|
+
INV_MAX_PASSES=2
|
|
614
|
+
INV_MAX_UNRELATED=5
|
|
615
|
+
INV_REPEATED_THRESH=2
|
|
616
|
+
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
617
|
+
_v=$(grep -E '^investigation_max_passes=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
618
|
+
[ -n "$_v" ] && INV_MAX_PASSES="$_v"
|
|
619
|
+
_v=$(grep -E '^investigation_max_unrelated_files=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
620
|
+
[ -n "$_v" ] && INV_MAX_UNRELATED="$_v"
|
|
621
|
+
_v=$(grep -E '^investigation_repeated_read_threshold=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | cut -d'#' -f1 | tr -d '[:space:]')
|
|
622
|
+
[ -n "$_v" ] && INV_REPEATED_THRESH="$_v"
|
|
623
|
+
fi
|
|
624
|
+
case "$INV_MAX_PASSES" in ''|*[!0-9]*) INV_MAX_PASSES=2 ;; esac
|
|
625
|
+
case "$INV_MAX_UNRELATED" in ''|*[!0-9]*) INV_MAX_UNRELATED=5 ;; esac
|
|
626
|
+
case "$INV_REPEATED_THRESH" in ''|*[!0-9]*) INV_REPEATED_THRESH=2 ;; esac
|
|
627
|
+
# investigation_status: simple threshold check on repeated_reads (W8/W10)
|
|
628
|
+
INVESTIGATION_STATUS="continue"
|
|
629
|
+
if [ "$REPEATED_READS" -ge "$INV_REPEATED_THRESH" ] 2>/dev/null; then
|
|
630
|
+
INVESTIGATION_STATUS="stop"
|
|
631
|
+
fi
|
|
632
|
+
|
|
502
633
|
mkdir -p "$MISSION_DIR"
|
|
503
634
|
|
|
504
635
|
node -e "
|
|
@@ -542,7 +673,12 @@ const data = {
|
|
|
542
673
|
evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
|
|
543
674
|
updated_at: process.argv[22],
|
|
544
675
|
schema_version: 2,
|
|
545
|
-
repeated_reads: parseInt(process.argv[41], 10) || 0
|
|
676
|
+
repeated_reads: parseInt(process.argv[41], 10) || 0,
|
|
677
|
+
team_members: parseInt(process.argv[42], 10) || 1,
|
|
678
|
+
posture: process.argv[43] || 'inline-sequential',
|
|
679
|
+
posture_reason: process.argv[44] || '',
|
|
680
|
+
posture_pause: process.argv[45] === 'true',
|
|
681
|
+
investigation_status: process.argv[46] || 'continue'
|
|
546
682
|
};
|
|
547
683
|
require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
|
|
548
684
|
" \
|
|
@@ -556,7 +692,7 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
|
|
|
556
692
|
"$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
|
|
557
693
|
"$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
|
|
558
694
|
"$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY" \
|
|
559
|
-
"$REPEATED_READS"
|
|
695
|
+
"$REPEATED_READS" "$TEAM_MEMBERS" "$POSTURE" "$POSTURE_REASON" "$POSTURE_PAUSE" "$INVESTIGATION_STATUS"
|
|
560
696
|
|
|
561
697
|
if [ "$LANE_ROSE" = true ]; then
|
|
562
698
|
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
@@ -414,7 +414,175 @@ if (integrityArg !== -1) {
|
|
|
414
414
|
}
|
|
415
415
|
if (!constants.includes('LANE_BASE_lean=8421')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 8421)');
|
|
416
416
|
if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
|
|
417
|
+
// W12 stale path check: obsolete layout must not appear
|
|
418
|
+
const staleChecks: [string, string[]][] = [
|
|
419
|
+
['state/<mission>', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
|
|
420
|
+
['continue/<mission>', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
|
|
421
|
+
['plans/<mission>', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
|
|
422
|
+
['logs/lessons', ['docs/concepts/comparison.md', 'docs/concepts/features.md', 'references/multi-actor.md', 'README.md']],
|
|
423
|
+
];
|
|
424
|
+
for (const [pat, docs] of staleChecks) {
|
|
425
|
+
for (const doc of docs) {
|
|
426
|
+
const p = join(import.meta.dirname, '..', doc);
|
|
427
|
+
if (existsSync(p) && readFileSync(p, 'utf8').includes(pat)) {
|
|
428
|
+
errors.push(`doc-integrity: ${doc} contains obsolete path "${pat}" — use missions/<mission>/ layout`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// W17 metrics must come from .metrics/latest.json — check for hardcoded stale numbers not in metrics
|
|
433
|
+
const metricsPath2 = join(import.meta.dirname, '..', '.metrics/latest.json');
|
|
434
|
+
if (existsSync(metricsPath2)) {
|
|
435
|
+
const m2 = JSON.parse(readFileSync(metricsPath2, 'utf8'));
|
|
436
|
+
const readme2 = readFileSync(join(import.meta.dirname, '..', 'README.md'), 'utf8');
|
|
437
|
+
// ensure README rank-1 and pointers match metrics (also checked in --check-readme-metrics, but this is integrity)
|
|
438
|
+
const rankMatch2 = readme2.match(/Retrieval routing rank-1[^\n]*?(\d+\.\d+)%/);
|
|
439
|
+
if (rankMatch2 && parseFloat(rankMatch2[1]) !== Number(m2.retrieval_rank1)) {
|
|
440
|
+
errors.push(`doc-integrity: README rank-1 ${rankMatch2[1]}% != metrics ${m2.retrieval_rank1}%`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// stale CLI commands: any `mugiwara <word>` where word is not a valid CLI case, appearing as code, is stale
|
|
444
|
+
const validCmds = new Set(['install','update','uninstall','list','reset','archive','clean','continue','status','cost','run','savepoint','blame','handoff','sign','migrate','lesson','help','version','mode','off']);
|
|
445
|
+
const docsToScan = ['docs/concepts/workflow.md','docs/concepts/config.md','README.md','references/multi-actor.md'];
|
|
446
|
+
for (const doc of docsToScan) {
|
|
447
|
+
const p = join(import.meta.dirname, '..', doc);
|
|
448
|
+
if (!existsSync(p)) continue;
|
|
449
|
+
const txt = readFileSync(p, 'utf8');
|
|
450
|
+
for (const m of txt.matchAll(/`mugiwara ([a-z-]+)/g)) {
|
|
451
|
+
const cmd = m[1];
|
|
452
|
+
if (!validCmds.has(cmd) && cmd !== '--help' && cmd !== '--version') {
|
|
453
|
+
errors.push(`doc-integrity: ${doc} contains stale command "mugiwara ${cmd}" not in src/cli.ts`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- config drift gate (W11): every key code reads must appear in DEFAULT_CONFIG and docs, and vice versa ---
|
|
461
|
+
if (process.argv.includes('--check-config')) {
|
|
462
|
+
const cfgSrc = readFileSync(join(import.meta.dirname, '..', 'src/config.ts'), 'utf8');
|
|
463
|
+
const m = cfgSrc.match(/DEFAULT_CONFIG\s*=\s*\[([\s\S]*?)\]\.join/);
|
|
464
|
+
let defaultKeys: string[] = [];
|
|
465
|
+
if (m) {
|
|
466
|
+
const block = m[1];
|
|
467
|
+
for (const line of block.split(/\r?\n/)) {
|
|
468
|
+
const t = line.trim();
|
|
469
|
+
if (!t) continue;
|
|
470
|
+
// extract string content between quotes
|
|
471
|
+
const q = t.match(/['"`]([^'"`]*?)['"`]/);
|
|
472
|
+
if (!q) continue;
|
|
473
|
+
let s = q[1].trim();
|
|
474
|
+
if (!s) continue;
|
|
475
|
+
if (s.startsWith('#')) s = s.slice(1).trim();
|
|
476
|
+
if (!s) continue;
|
|
477
|
+
const eq = s.indexOf('=');
|
|
478
|
+
if (eq === -1) continue;
|
|
479
|
+
const key = s.slice(0, eq).trim();
|
|
480
|
+
if (key) defaultKeys.push(key);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// docs keys from config.md table (only the ## Keys section, not template examples)
|
|
484
|
+
const docPath = join(import.meta.dirname, '..', 'docs/concepts/config.md');
|
|
485
|
+
let docKeys: string[] = [];
|
|
486
|
+
if (existsSync(docPath)) {
|
|
487
|
+
const docText = readFileSync(docPath, 'utf8');
|
|
488
|
+
const keysSectionMatch = docText.match(/## Keys([\s\S]*?)(?:\n## |\n#|$)/);
|
|
489
|
+
const keysSection = keysSectionMatch ? keysSectionMatch[1] : docText;
|
|
490
|
+
for (const line of keysSection.split(/\r?\n/)) {
|
|
491
|
+
const cm = line.match(/\|\s*`([^`]+)`\s*\|/);
|
|
492
|
+
if (cm) {
|
|
493
|
+
const k = cm[1].trim();
|
|
494
|
+
if (k && !docKeys.includes(k)) docKeys.push(k);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
errors.push('config-drift: docs/concepts/config.md not found');
|
|
499
|
+
}
|
|
500
|
+
// code keys: scan src/*.ts, scripts/*.sh, hooks/*.ts for key patterns
|
|
501
|
+
const codeRoots = [
|
|
502
|
+
join(import.meta.dirname, '..', 'src'),
|
|
503
|
+
join(import.meta.dirname, '..', 'scripts'),
|
|
504
|
+
join(import.meta.dirname, '..', 'hooks'),
|
|
505
|
+
];
|
|
506
|
+
const codeTextAll = codeRoots.map(r => {
|
|
507
|
+
if (!existsSync(r)) return '';
|
|
508
|
+
const files = readdirSync(r, { withFileTypes: true }).filter(e => e.isFile() && (e.name.endsWith('.ts') || e.name.endsWith('.sh'))).map(e => readFileSync(join(r, e.name), 'utf8')).join('\n');
|
|
509
|
+
// also need subdirectories
|
|
510
|
+
let sub = '';
|
|
511
|
+
try {
|
|
512
|
+
for (const e of readdirSync(r, { withFileTypes: true })) {
|
|
513
|
+
if (e.isDirectory()) {
|
|
514
|
+
const subdir = join(r, e.name);
|
|
515
|
+
for (const f of readdirSync(subdir, { withFileTypes: true }).filter(x => x.isFile() && (x.name.endsWith('.ts') || x.name.endsWith('.sh')))) {
|
|
516
|
+
sub += readFileSync(join(subdir, f.name), 'utf8') + '\n';
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch {}
|
|
521
|
+
return files + sub;
|
|
522
|
+
}).join('\n');
|
|
523
|
+
// check each default key appears in code
|
|
524
|
+
for (const k of defaultKeys) {
|
|
525
|
+
if (!codeTextAll.includes(k)) {
|
|
526
|
+
errors.push(`config-drift: DEFAULT_CONFIG key "${k}" not found in code (src/*.ts, scripts/*.sh, hooks/*.ts)`);
|
|
527
|
+
}
|
|
528
|
+
if (!docKeys.includes(k)) {
|
|
529
|
+
errors.push(`config-drift: DEFAULT_CONFIG key "${k}" missing from docs/concepts/config.md`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const k of docKeys) {
|
|
533
|
+
if (!defaultKeys.includes(k)) {
|
|
534
|
+
errors.push(`config-drift: docs/concepts/config.md key "${k}" not in DEFAULT_CONFIG`);
|
|
535
|
+
}
|
|
536
|
+
if (!codeTextAll.includes(k)) {
|
|
537
|
+
errors.push(`config-drift: docs key "${k}" not found in code`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!errors.some(e => e.startsWith('config-drift'))) {
|
|
541
|
+
console.log(`✓ config in sync: ${defaultKeys.length} keys (${defaultKeys.join(', ')})`);
|
|
542
|
+
}
|
|
417
543
|
}
|
|
544
|
+
|
|
545
|
+
// --- wiring gate (W7): every src module must be imported somewhere ---
|
|
546
|
+
if (process.argv.includes('--check-wiring')) {
|
|
547
|
+
const srcDir = join(import.meta.dirname, '..', 'src');
|
|
548
|
+
const ENTRY = new Set(['cli.ts', 'index.ts', 'installer.ts']);
|
|
549
|
+
const srcFiles = readdirSync(srcDir).filter(n => n.endsWith('.ts'));
|
|
550
|
+
const searchDirs = [srcDir, join(import.meta.dirname, '..', 'hooks'), join(import.meta.dirname, '..', 'scripts')];
|
|
551
|
+
const allFiles: string[] = [];
|
|
552
|
+
for (const d of searchDirs) {
|
|
553
|
+
if (!existsSync(d)) continue;
|
|
554
|
+
const walk = (dir: string) => {
|
|
555
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
556
|
+
const full = join(dir, e.name);
|
|
557
|
+
if (e.isDirectory()) walk(full);
|
|
558
|
+
else if (e.isFile() && (e.name.endsWith('.ts') || e.name.endsWith('.js') || e.name.endsWith('.sh') || e.name.endsWith('.mjs'))) {
|
|
559
|
+
allFiles.push(full);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
walk(d);
|
|
564
|
+
}
|
|
565
|
+
for (const f of srcFiles) {
|
|
566
|
+
if (ENTRY.has(f)) continue;
|
|
567
|
+
const stem = f.slice(0, -3);
|
|
568
|
+
let found = false;
|
|
569
|
+
for (const full of allFiles) {
|
|
570
|
+
if (full.endsWith(`/src/${f}`) || full === join(srcDir, f)) continue;
|
|
571
|
+
try {
|
|
572
|
+
const txt = readFileSync(full, 'utf8');
|
|
573
|
+
if (txt.includes(`'./${stem}`) || txt.includes(`"./${stem}`) || txt.includes(`'./${stem}.ts'`) || txt.includes(`"./${stem}.ts"`)) {
|
|
574
|
+
found = true;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
} catch {}
|
|
578
|
+
}
|
|
579
|
+
if (!found) {
|
|
580
|
+
errors.push(`src/${f}: imported by nothing — wire it, or delete it with its tests and its features.md claim`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (!errors.some(e => e.includes('imported by nothing'))) {
|
|
584
|
+
console.log(`✓ wiring: all src modules imported`);
|
|
585
|
+
}
|
|
418
586
|
}
|
|
419
587
|
|
|
420
588
|
// --- README metrics gate (D3): README table must match .metrics/latest.json ---
|
package/src/args.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type Args = {
|
|
|
6
6
|
flags: Record<string, FlagValue>;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before', '--backend': 'backend', '--mission': 'mission' };
|
|
9
|
+
const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before', '--backend': 'backend', '--mission': 'mission', '--to-team': 'toTeam', '--to-solo': 'toSolo' };
|
|
10
10
|
const BOOL_FLAGS: Record<string, string> = {
|
|
11
11
|
'--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
|
|
12
12
|
'--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',
|