@ionivetech/mugiwara 0.6.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -7
- package/.codex-plugin/plugin.json +2 -7
- package/.cursor-plugin/plugin.json +2 -7
- package/.kimi-plugin/plugin.json +2 -7
- package/.opencode/plugins/mugiwara.mjs +3 -18
- package/AGENTS.md +5 -4
- package/GEMINI.md +3 -3
- package/README.md +69 -63
- package/content/agents/brook-healing.md +8 -4
- package/content/agents/chopper-checkpoint.md +7 -3
- package/content/agents/eval-runner.md +4 -4
- package/content/agents/franky-gates.md +3 -3
- package/content/agents/jinbe-security.md +6 -7
- package/content/agents/luffy-orchestrator.md +11 -13
- package/content/agents/memory-keeper.md +4 -4
- package/content/agents/nami-planner.md +4 -4
- package/content/agents/resume-coordinator.md +7 -7
- package/content/agents/robin-reviewer.md +7 -3
- package/content/agents/sanji-quality.md +2 -2
- package/content/agents/skeptic-verifier.md +2 -2
- package/content/agents/usopp-brainstorm.md +3 -3
- package/content/agents/zoro-execution.md +6 -6
- package/content/skills/mugiwara-backend/SKILL.md +2 -0
- package/content/skills/mugiwara-backend/references/database.md +61 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +6 -5
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
- package/content/skills/mugiwara-execution/SKILL.md +8 -8
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +3 -2
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +8 -5
- package/content/skills/mugiwara-healing/SKILL.md +2 -2
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
- package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
- package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
- package/content/skills/mugiwara-planning/SKILL.md +3 -18
- package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
- package/content/skills/mugiwara-quality/SKILL.md +5 -2
- package/content/skills/mugiwara-resume/SKILL.md +2 -2
- package/content/skills/mugiwara-review/SKILL.md +3 -2
- package/content/skills/mugiwara-security/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
- package/content/skills/mugiwara-workflow/SKILL.md +9 -12
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
- package/dist/mugiwara.js +904 -462
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +18 -10
- package/hooks/auto-savepoint.ts +23 -23
- package/hooks/engagement-marker.js +1 -1
- package/hooks/engagement-marker.ts +1 -1
- package/hooks/pipeline-guard.js +16 -12
- package/hooks/pipeline-guard.ts +22 -19
- package/hooks/session-start.js +13 -10
- package/hooks/session-start.ts +24 -19
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/complexity.md +27 -4
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +0 -3
- package/references/prose-style.md +54 -0
- package/scripts/check-doc-links.ts +40 -0
- package/scripts/conformance.ts +6 -16
- package/scripts/coverage-gate.ts +20 -6
- package/scripts/gate-selftest.ts +118 -7
- package/scripts/lane.sh +54 -8
- package/scripts/lib/patterns.sh +5 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +1 -1
- package/scripts/savepoint.sh +136 -58
- package/scripts/validate-content.ts +22 -9
- package/scripts/verify-install.ts +9 -7
- package/src/args.ts +2 -2
- package/src/budget.ts +56 -0
- package/src/cli.ts +161 -110
- package/src/continue.ts +29 -10
- package/src/installer.ts +25 -20
- package/src/integrity.ts +158 -0
- package/src/mission.ts +228 -95
- package/src/policy.ts +156 -0
- package/src/provenance.ts +116 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +2 -2
- package/src/sign.ts +61 -0
- package/src/targets/claude.ts +2 -2
- package/.opencode/commands/mugiwara-execute.md +0 -13
- package/.opencode/commands/mugiwara-heal.md +0 -13
- package/.opencode/commands/mugiwara-onboard.md +0 -14
- package/.opencode/commands/mugiwara-plan.md +0 -17
- package/.opencode/commands/mugiwara-ship.md +0 -13
- package/.opencode/commands/using-mugiwara.md +0 -20
- package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
- package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
- package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
- package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
- package/content/skills/mugiwara-pr/SKILL.md +0 -69
- package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
- package/content/skills/mugiwara-sunset/SKILL.md +0 -82
- package/content/skills/using-mugiwara/SKILL.md +0 -51
- package/references/token-budget.md +0 -56
- package/scripts/evidence.sh +0 -81
- package/scripts/initiative.ts +0 -296
- package/scripts/mission-report.sh +0 -293
- package/src/onboard.ts +0 -207
package/scripts/savepoint.sh
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# scripts/savepoint.sh — write .mugiwara/
|
|
3
|
-
#
|
|
2
|
+
# scripts/savepoint.sh — write .mugiwara/missions/<mission>/<member>.json at every
|
|
3
|
+
# flow-stage boundary. Computed from git + file counts; zero model judgement.
|
|
4
4
|
# Identity is (mission, member), never branch. Solo = member empty → the file
|
|
5
5
|
# is named state.json. Usage:
|
|
6
|
-
# savepoint.sh <mission> [member] [
|
|
6
|
+
# savepoint.sh <mission> [member] [flow] [mode] [lane]
|
|
7
7
|
set -u
|
|
8
8
|
|
|
9
9
|
die() { echo "savepoint: $*" >&2; exit 1; }
|
|
10
10
|
|
|
11
11
|
MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
|
|
12
12
|
|
|
13
|
+
# optional provider-reported tokens file (T4): --tokens-file <path> JSON {input_tokens, output_tokens}
|
|
14
|
+
TOKENS_FILE=""
|
|
15
|
+
while [ $# -gt 0 ]; do
|
|
16
|
+
case "$1" in
|
|
17
|
+
--tokens-file) TOKENS_FILE="${2:-}"; [ -n "$TOKENS_FILE" ] || die "--tokens-file requires a path"; shift 2 ;;
|
|
18
|
+
--tokens-file=*) TOKENS_FILE="${1#*=}"; shift ;;
|
|
19
|
+
--) shift; break ;;
|
|
20
|
+
-*) die "unknown flag \"$1\"" ;;
|
|
21
|
+
*) break ;;
|
|
22
|
+
esac
|
|
23
|
+
done
|
|
24
|
+
|
|
13
25
|
# shared path patterns — single source of truth (D3)
|
|
14
26
|
# shellcheck source=scripts/lib/patterns.sh
|
|
15
27
|
source "$(dirname "$0")/lib/patterns.sh"
|
|
@@ -99,6 +111,12 @@ esac
|
|
|
99
111
|
[ "$DELEGATE_THRESHOLD" -gt 100 ] 2>/dev/null && DELEGATE_THRESHOLD=100
|
|
100
112
|
ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-${USERNAME:-}}}}}"
|
|
101
113
|
BRANCH="$(git branch --show-current 2>/dev/null || echo 'unknown')"
|
|
114
|
+
# Per-stage model attribution (A4): record which model produced THIS stage.
|
|
115
|
+
# MUGIWARA_MODEL wins over ANTHROPIC_MODEL; 'unknown' beats a lie. Closure
|
|
116
|
+
# renders the unique set across stage files so mid-mission switches stay
|
|
117
|
+
# visible in provenance instead of every line attributing to the last value.
|
|
118
|
+
MODEL="${MUGIWARA_MODEL:-${ANTHROPIC_MODEL:-}}"
|
|
119
|
+
[ -n "$MODEL" ] || MODEL="unknown"
|
|
102
120
|
|
|
103
121
|
# treat the legacy empty-actor placeholder '""' as "no member" (solo). The old
|
|
104
122
|
# interface used '""' for the actor slot; the new (mission, member) interface
|
|
@@ -123,6 +141,12 @@ esac
|
|
|
123
141
|
if [[ "$MEMBER" =~ ^\.+$ ]]; then
|
|
124
142
|
die "invalid member name \"$MEMBER\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
|
|
125
143
|
fi
|
|
144
|
+
# reserved stems — <member>.json and continue-<member>.json share the mission
|
|
145
|
+
# dir with state.json / continue.json; member "state" or "continue" would
|
|
146
|
+
# overwrite the solo files (state pointer or resume pointer).
|
|
147
|
+
case "$MEMBER" in
|
|
148
|
+
state|continue) die "reserved member name \"$MEMBER\" (state, continue)" ;;
|
|
149
|
+
esac
|
|
126
150
|
|
|
127
151
|
# BRANCH_SLUG sanitized to [A-Za-z0-9._-] — it feeds the continue branch field;
|
|
128
152
|
# a newline/control char in BRANCH would corrupt the JSON (F5). Dropping illegal
|
|
@@ -130,15 +154,16 @@ fi
|
|
|
130
154
|
# (BSD/macOS-safe: no \+ BRE).
|
|
131
155
|
BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-' | tr -cd 'A-Za-z0-9._-' | sed 's/^\.\{1,\}$//' )
|
|
132
156
|
|
|
133
|
-
# state + continue live
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
# state + continue live in the mission dir. Solo (member empty) → state.json
|
|
158
|
+
# + continue.json; team writes <member>.json + continue-<member>.json so
|
|
159
|
+
# parallel members never clobber each other.
|
|
160
|
+
MISSION_DIR="$MUGIWARA_DIR/missions/$MISSION"
|
|
136
161
|
if [ -n "$MEMBER" ]; then
|
|
137
|
-
STATE_FILE="$
|
|
138
|
-
CONTINUE_FILE="$
|
|
162
|
+
STATE_FILE="$MISSION_DIR/$MEMBER.json"
|
|
163
|
+
CONTINUE_FILE="$MISSION_DIR/continue-$MEMBER.json"
|
|
139
164
|
else
|
|
140
|
-
STATE_FILE="$
|
|
141
|
-
CONTINUE_FILE="$
|
|
165
|
+
STATE_FILE="$MISSION_DIR/state.json"
|
|
166
|
+
CONTINUE_FILE="$MISSION_DIR/continue.json"
|
|
142
167
|
fi
|
|
143
168
|
|
|
144
169
|
[ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
|
|
@@ -148,21 +173,46 @@ fi
|
|
|
148
173
|
BASE_SHA=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null || git merge-base HEAD "$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')" 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "unknown")
|
|
149
174
|
HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
150
175
|
|
|
176
|
+
# lane_scope_glob (T5): monorepo scoping — count only files matching the glob
|
|
177
|
+
LANE_SCOPE_GLOB=""
|
|
178
|
+
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
179
|
+
_cfg_scope=$(grep -E '^lane_scope_glob=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '"' | tr -d "'")
|
|
180
|
+
[ -n "$_cfg_scope" ] && LANE_SCOPE_GLOB="$_cfg_scope"
|
|
181
|
+
fi
|
|
151
182
|
# union of committed + staged + unstaged + untracked (F) — see patterns.sh
|
|
152
183
|
CHANGED_FILES=$(changed_files "$BASE_SHA")
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
# sensitive-path escalation always uses unfiltered set (safety never shrinks, T5)
|
|
185
|
+
SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
|
186
|
+
# scoped counting (T5)
|
|
187
|
+
SCOPED_FILES="$CHANGED_FILES"
|
|
188
|
+
if [ -n "$LANE_SCOPE_GLOB" ] && [ -n "$CHANGED_FILES" ]; then
|
|
189
|
+
shopt -s extglob globstar 2>/dev/null || true
|
|
190
|
+
SCOPED_FILES=""
|
|
191
|
+
while IFS= read -r _sf; do
|
|
192
|
+
[ -z "$_sf" ] && continue
|
|
193
|
+
# shellcheck disable=SC2053
|
|
194
|
+
if [[ "$_sf" == $LANE_SCOPE_GLOB ]]; then
|
|
195
|
+
SCOPED_FILES="${SCOPED_FILES}${SCOPED_FILES:+
|
|
196
|
+
}$_sf"
|
|
197
|
+
fi
|
|
198
|
+
done <<< "$CHANGED_FILES"
|
|
199
|
+
fi
|
|
200
|
+
FILES_TOUCHED=$( [ -n "$SCOPED_FILES" ] && echo "$SCOPED_FILES" | wc -l | tr -d ' ' || echo 0 )
|
|
201
|
+
if [ -n "$LANE_SCOPE_GLOB" ]; then
|
|
202
|
+
read -r LOC_INS LOC_DEL <<EOF
|
|
203
|
+
$( { echo "$SCOPED_FILES" | while IFS= read -r _lf; do [ -n "$_lf" ] && git diff --numstat "$BASE_SHA"..HEAD -- "$_lf" 2>/dev/null; git diff --numstat HEAD -- "$_lf" 2>/dev/null; done; echo "$SCOPED_FILES" | while IFS= read -r _uf; do [ -z "$_uf" ] && continue; if git ls-files --others --exclude-standard -- "$_uf" 2>/dev/null | grep -qx "$_uf"; then printf '%s\t0\t%s\n' "$(wc -l < "$_uf" 2>/dev/null | tr -d ' ' || echo 0)" "$_uf"; fi; done; } | awk '{ if ($1 ~ /^[0-9]+$/) i+=$1; if ($2 ~ /^[0-9]+$/) d+=$2 } END { print (i+0)" "(d+0) }')
|
|
204
|
+
EOF
|
|
205
|
+
else
|
|
206
|
+
read -r LOC_INS LOC_DEL <<EOF
|
|
156
207
|
$(changed_loc "$BASE_SHA")
|
|
157
208
|
EOF
|
|
209
|
+
fi
|
|
158
210
|
LOC_INS=$(( ${LOC_INS:-0} + 0 ))
|
|
159
211
|
LOC_DEL=$(( ${LOC_DEL:-0} + 0 ))
|
|
160
212
|
LOC_DELTA=$(( LOC_INS - LOC_DEL ))
|
|
161
213
|
# churn is insertion+deletion — refactors and deletions are work too (D4)
|
|
162
214
|
LOC_CHURN=$(( LOC_INS + LOC_DEL ))
|
|
163
215
|
|
|
164
|
-
SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
|
165
|
-
|
|
166
216
|
LANE="direct"
|
|
167
217
|
LANE_REASON=""
|
|
168
218
|
# Sensitive-path escalation is unconditional — it wins over any count-based
|
|
@@ -196,9 +246,10 @@ fi
|
|
|
196
246
|
|
|
197
247
|
# path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
|
|
198
248
|
# surface never escalate to full from file count alone; sensitive-path
|
|
199
|
-
# escalation above still wins.
|
|
200
|
-
|
|
201
|
-
|
|
249
|
+
# escalation above still wins. With lane_scope_glob the check applies to the
|
|
250
|
+
# scoped set — but sensitive wins unfiltered above, so safety never shrinks.
|
|
251
|
+
if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$SCOPED_FILES" ]; then
|
|
252
|
+
CODE_COUNT=$(echo "$SCOPED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
202
253
|
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
203
254
|
PREV="$LANE"
|
|
204
255
|
LANE="standard"
|
|
@@ -206,6 +257,20 @@ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ];
|
|
|
206
257
|
fi
|
|
207
258
|
fi
|
|
208
259
|
|
|
260
|
+
# policy as code: mugiwara.policy.yml lanes.force_full raises the
|
|
261
|
+
# lane to full — upward only. Optional: no file or no bun = no-op.
|
|
262
|
+
if command -v bun >/dev/null 2>&1 && { [ -f mugiwara.policy.yml ] || [ -f mugiwara.policy.yaml ]; }; then
|
|
263
|
+
POLICY_HITS=$(printf '%s\n' "$CHANGED_FILES" | bun "$(dirname "$0")/policy-force.ts")
|
|
264
|
+
RC=$?
|
|
265
|
+
if [ "$RC" -ne 0 ]; then
|
|
266
|
+
die "mugiwara.policy.yml is invalid — fix or remove it (fail-closed)"
|
|
267
|
+
fi
|
|
268
|
+
if [ -n "$POLICY_HITS" ]; then
|
|
269
|
+
LANE="full"
|
|
270
|
+
LANE_REASON="policy force_full ($POLICY_HITS)"
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
|
|
209
274
|
# explicit triage lane (M7) applied as a floor — the computed lane above may
|
|
210
275
|
# raise it, never lower it. Sensitive-path escalation still wins
|
|
211
276
|
# unconditionally: spike is a resize, not a rise, so it never displaces the
|
|
@@ -255,9 +320,8 @@ if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
|
|
|
255
320
|
esac
|
|
256
321
|
fi
|
|
257
322
|
|
|
258
|
-
# task counts from plan doc —
|
|
259
|
-
|
|
260
|
-
PLAN_FILE=$(ls "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md 2>/dev/null | head -1 || true)
|
|
323
|
+
# task counts from plan doc — bare name, one file per mission.
|
|
324
|
+
PLAN_FILE="$MISSION_DIR/plan.md"
|
|
261
325
|
TASKS_DONE=0
|
|
262
326
|
TASKS_TOTAL=0
|
|
263
327
|
if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
@@ -269,11 +333,7 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
|
269
333
|
fi
|
|
270
334
|
|
|
271
335
|
# blocker count
|
|
272
|
-
|
|
273
|
-
# what 15 prose sites mandate and what lands on disk. Matching only the bare
|
|
274
|
-
# name made blockers_open structurally 0 forever, and that zero feeds the DoD
|
|
275
|
-
# ship-readiness axis. Accept both; newest wins.
|
|
276
|
-
BLOCKERS_FILE=$(ls -t "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" "$MUGIWARA_DIR"/????-??-??-"${MISSION}"-blockers.md "$MUGIWARA_DIR/issues"/????-??-??-"${MISSION}"-blockers.md 2>/dev/null | head -1 || true)
|
|
336
|
+
BLOCKERS_FILE="$MISSION_DIR/blockers.md"
|
|
277
337
|
BLOCKERS_OPEN=0
|
|
278
338
|
if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
|
|
279
339
|
# data rows start with a wave number; header "| wave |" and separator
|
|
@@ -281,27 +341,18 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
|
|
|
281
341
|
BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
|
|
282
342
|
fi
|
|
283
343
|
|
|
284
|
-
# heal cycle — count Flow-8 (healing) section headings in the DECISION LOG
|
|
285
|
-
# which every mission writes (luffy-
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
# the [^0-9a-z] guard keeps
|
|
289
|
-
#
|
|
290
|
-
# reading only the newest would undercount earlier days' heal cycles.
|
|
344
|
+
# heal cycle — count Flow-8 (healing) section headings in the DECISION LOG
|
|
345
|
+
# (missions/<mission>/decisions.md), which every mission writes (luffy-
|
|
346
|
+
# orchestrator rule 10). Not the word "heal" anywhere (heal workers/healing
|
|
347
|
+
# text would inflate the counter and cause a premature halt). Each heal cycle
|
|
348
|
+
# is logged as a "## Flow 8 — healing" section; the [^0-9a-z] guard keeps
|
|
349
|
+
# adjacent "## Flow 8b"-style sections from counting.
|
|
291
350
|
HEAL_CYCLE=1
|
|
292
351
|
HEAL_COUNT=0
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
[ -f "$LOG_FILE" ] || continue
|
|
298
|
-
LOG_DATE=$(basename "$LOG_FILE" | cut -c1-10)
|
|
299
|
-
case "$LOG_DATE" in
|
|
300
|
-
''|*[!0-9-]*) continue ;;
|
|
301
|
-
esac
|
|
302
|
-
if [ "$LOG_DATE" \> "$TODAY" ]; then continue; fi
|
|
303
|
-
HEAL_COUNT=$((HEAL_COUNT + $(grep -ciE '^## flow 8([^0-9a-z]|$)' "$LOG_FILE" 2>/dev/null || true)))
|
|
304
|
-
done
|
|
352
|
+
LOG_FILE="$MISSION_DIR/decisions.md"
|
|
353
|
+
if [ -f "$LOG_FILE" ]; then
|
|
354
|
+
HEAL_COUNT=$(grep -ciE '^## flow 8([^0-9a-z]|$)' "$LOG_FILE" 2>/dev/null || true)
|
|
355
|
+
fi
|
|
305
356
|
if [ "$HEAL_COUNT" -gt 0 ]; then
|
|
306
357
|
HEAL_CYCLE=$((HEAL_COUNT + 1))
|
|
307
358
|
fi
|
|
@@ -314,12 +365,18 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
|
|
|
314
365
|
HEAL_HALT=true
|
|
315
366
|
fi
|
|
316
367
|
|
|
317
|
-
# evidence paths —
|
|
318
|
-
# name is allowlisted above, but avoid sed metacharacter semantics entirely)
|
|
368
|
+
# evidence paths — the mission's flow folder (quoted printf, no sed — mission
|
|
369
|
+
# name is allowlisted above, but avoid sed metacharacter semantics entirely).
|
|
370
|
+
# New missions use flows/; a legacy mission that already keeps waves/ stays on
|
|
371
|
+
# waves/ so an in-flight trail never splits across two directories.
|
|
372
|
+
FLOWDIR="flows"
|
|
373
|
+
if [ -d "$MISSION_DIR/waves" ] && [ ! -d "$MISSION_DIR/flows" ]; then
|
|
374
|
+
FLOWDIR="waves"
|
|
375
|
+
fi
|
|
319
376
|
EVIDENCE=""
|
|
320
|
-
for evf in "$
|
|
377
|
+
for evf in "$MISSION_DIR/$FLOWDIR/"*.md; do
|
|
321
378
|
[ -f "$evf" ] || continue
|
|
322
|
-
EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/
|
|
379
|
+
EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/missions/${MISSION}/${FLOWDIR}/$(basename "$evf")"
|
|
323
380
|
done
|
|
324
381
|
|
|
325
382
|
# skill version — MUGIWARA's own package.json, resolved from the script
|
|
@@ -340,7 +397,21 @@ fi
|
|
|
340
397
|
# real usage. Monotonic beats precise — LANE_BASE stands in for the skills
|
|
341
398
|
# loaded this lane (measured from content, validated by scripts/lane-base.ts);
|
|
342
399
|
# loc_churn and written-artifact words scale with growth.
|
|
400
|
+
# auto-detect harness for token reporting (Tier-1: Claude Code / opencode)
|
|
401
|
+
# If --tokens-file not given, try env/file fallbacks so estimator is last resort.
|
|
402
|
+
if [ -z "$TOKENS_FILE" ]; then
|
|
403
|
+
for _cand in "${MUGIWARA_TOKENS_FILE:-}" "${CLAUDE_TOKENS_FILE:-}" "${OPENCODE_TOKENS_FILE:-}" "/tmp/mugiwara-tokens.json" "$HOME/.cache/mugiwara/tokens.json" ".mugiwara/tokens.json"; do
|
|
404
|
+
[ -n "$_cand" ] && [ -f "$_cand" ] && TOKENS_FILE="$_cand" && break
|
|
405
|
+
done
|
|
406
|
+
fi
|
|
407
|
+
# detect harness (for logging / future tier checks)
|
|
408
|
+
HARNESS="unknown"
|
|
409
|
+
if [ -n "${CLAUDECODE:-}" ] || [ -n "${CLAUDE_CODE_ENTRYPOINT:-}" ] || echo "${ANTHROPIC_MODEL:-}" | grep -qi claude 2>/dev/null; then HARNESS="claude";
|
|
410
|
+
elif [ -n "${OPENCODE:-}" ] || [ -f ".opencode/config.json" ] || [ -n "${OPENCODE_TOKENS_FILE:-}" ]; then HARNESS="opencode";
|
|
411
|
+
elif [ -n "${CURSOR:-}" ] || [ -n "${VSCODE_GIT_ASKPASS_NODE:-}" ]; then HARNESS="cursor/vscode"; fi
|
|
412
|
+
|
|
343
413
|
# MUGIWARA_TOKENS overrides as the reported value.
|
|
414
|
+
# --tokens-file (T4) is the first-class provider-reported path: JSON {input_tokens, output_tokens}
|
|
344
415
|
LANE_BASE=0
|
|
345
416
|
case "$LANE" in
|
|
346
417
|
lean) LANE_BASE=$LANE_BASE_lean ;;
|
|
@@ -348,14 +419,19 @@ case "$LANE" in
|
|
|
348
419
|
full) LANE_BASE=$LANE_BASE_full ;;
|
|
349
420
|
spike) LANE_BASE=$LANE_BASE_spike ;;
|
|
350
421
|
esac
|
|
351
|
-
DOC_WORDS=$(cat "$
|
|
422
|
+
DOC_WORDS=$(cat "$MISSION_DIR"/"$FLOWDIR"/*.md "$MISSION_DIR"/plan.md "$MISSION_DIR"/spec.md "$MISSION_DIR"/decisions.md 2>/dev/null | wc -w | tr -d ' ')
|
|
352
423
|
LOC_TOKENS=$(( LOC_CHURN * 12 ))
|
|
353
|
-
TOKENS_SOURCE="
|
|
424
|
+
TOKENS_SOURCE="estimator"
|
|
354
425
|
TOKENS_EST=$(( LANE_BASE + DOC_WORDS * 135 / 100 + LOC_TOKENS ))
|
|
355
426
|
if [ -n "${MUGIWARA_TOKENS:-}" ]; then
|
|
356
427
|
TOKENS_EST="${MUGIWARA_TOKENS}"
|
|
357
428
|
TOKENS_SOURCE="reported"
|
|
358
429
|
fi
|
|
430
|
+
if [ -n "$TOKENS_FILE" ]; then
|
|
431
|
+
[ -f "$TOKENS_FILE" ] || die "tokens file not found: $TOKENS_FILE"
|
|
432
|
+
TOKENS_EST=$(node -e "try{const j=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const i=Number(j.input_tokens)||0;const o=Number(j.output_tokens)||0;console.log(i+o)}catch(e){console.log(0)}" "$TOKENS_FILE" 2>/dev/null || echo 0)
|
|
433
|
+
TOKENS_SOURCE="reported"
|
|
434
|
+
fi
|
|
359
435
|
|
|
360
436
|
# budget per lane
|
|
361
437
|
BUDGET=0
|
|
@@ -390,7 +466,7 @@ if [ "$BUDGET" -gt 0 ] 2>/dev/null; then
|
|
|
390
466
|
fi
|
|
391
467
|
fi
|
|
392
468
|
|
|
393
|
-
mkdir -p "$
|
|
469
|
+
mkdir -p "$MISSION_DIR"
|
|
394
470
|
|
|
395
471
|
node -e "
|
|
396
472
|
const data = {
|
|
@@ -406,6 +482,7 @@ const data = {
|
|
|
406
482
|
flow: parseInt(process.argv[6], 10),
|
|
407
483
|
mode: process.argv[7],
|
|
408
484
|
verbosity: process.argv[32] || 'normal',
|
|
485
|
+
model: process.argv[37] || 'unknown',
|
|
409
486
|
base_sha: process.argv[8],
|
|
410
487
|
head_sha: process.argv[9],
|
|
411
488
|
files_touched: parseInt(process.argv[10], 10),
|
|
@@ -439,20 +516,21 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
|
|
|
439
516
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
440
517
|
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
|
|
441
518
|
"$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
|
|
442
|
-
"$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE"
|
|
519
|
+
"$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
|
|
520
|
+
"$MODEL"
|
|
443
521
|
|
|
444
522
|
if [ "$LANE_ROSE" = true ]; then
|
|
445
523
|
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
446
524
|
fi
|
|
447
525
|
|
|
448
|
-
# --- continue
|
|
526
|
+
# --- continue.json (D10): machine-written resume point ---
|
|
449
527
|
# Written alongside state.json so an interrupted session (step-limit truncation,
|
|
450
528
|
# crash, new session) can resume without human recall. Same trust as state.json:
|
|
451
529
|
# computed fields, never model judgement. The resume skill treats it as data to
|
|
452
530
|
# verify against the plan/todos, never verbatim instructions.
|
|
453
|
-
# Scoped by (mission, member) like state.json — solo writes
|
|
454
|
-
# writes
|
|
455
|
-
#
|
|
531
|
+
# Scoped by (mission, member) like state.json — solo writes continue.json, team
|
|
532
|
+
# writes continue-<member>.json. The crew-written next_session_prompt line is
|
|
533
|
+
# preserved across savepoints.
|
|
456
534
|
if [ -n "$MISSION" ]; then
|
|
457
535
|
NEXT_SESSION_PROMPT=""
|
|
458
536
|
if [ -f "$CONTINUE_FILE" ]; then
|
|
@@ -491,4 +569,4 @@ if [ -n "$MISSION" ]; then
|
|
|
491
569
|
"$CONTINUE_FILE" "$CONTINUE_FILE" "$ACTOR"
|
|
492
570
|
fi
|
|
493
571
|
|
|
494
|
-
echo "✓ savepoint written: $STATE_FILE (lane=$LANE,
|
|
572
|
+
echo "✓ savepoint written: $STATE_FILE (lane=$LANE, flow=$WAVE, files=$FILES_TOUCHED)"
|
|
@@ -286,6 +286,25 @@ if (totalDescChars > INDEX_BUDGET) {
|
|
|
286
286
|
console.log(`✓ index budget: ${totalDescChars}/${INDEX_BUDGET} chars`);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// The measured index size is published in docs/concepts/cost.md. A measured
|
|
290
|
+
// number in prose that drifted from reality is a defect (it survived one
|
|
291
|
+
// release: cost.md claimed 5,437 chars after the prune measured 4,688).
|
|
292
|
+
// Gate the doc against the measurement so the class stays closed.
|
|
293
|
+
const costDoc = join(root, '..', 'docs', 'concepts', 'cost.md');
|
|
294
|
+
if (existsSync(costDoc)) {
|
|
295
|
+
const m = readFileSync(costDoc, 'utf8').match(/\*\*Current:\*\* (\d[\d,]*) chars/);
|
|
296
|
+
if (!m) {
|
|
297
|
+
errors.push('docs/concepts/cost.md: missing "**Current:** <n> chars" line for the measured index size');
|
|
298
|
+
} else {
|
|
299
|
+
const stated = parseInt(m[1].replace(/,/g, ''), 10);
|
|
300
|
+
if (stated !== totalDescChars) {
|
|
301
|
+
errors.push(`docs/concepts/cost.md states ${stated} index chars but content measures ${totalDescChars} — update the doc`);
|
|
302
|
+
} else {
|
|
303
|
+
console.log(`✓ cost.md index chars match measurement (${stated})`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
289
308
|
// --- docs-drift check: docs/skills.md and docs/agents.md must reference all content/ entries ---
|
|
290
309
|
const docsArg = process.argv.indexOf('--check-docs');
|
|
291
310
|
if (docsArg !== -1) {
|
|
@@ -374,15 +393,9 @@ if (integrityArg !== -1) {
|
|
|
374
393
|
}
|
|
375
394
|
}
|
|
376
395
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// evidence exit-code claim must be test-backed
|
|
381
|
-
const evTest = join(import.meta.dirname, '..', 'test', 'lane-integrity.test.ts');
|
|
382
|
-
const testText = existsSync(evTest) ? readFileSync(evTest, 'utf8') : '';
|
|
383
|
-
if (!testText.includes('# Exit:') || !testText.includes('# Verdict:')) {
|
|
384
|
-
errors.push('doc-integrity: audit-trail claims evidence logs carry exit code + verdict, but no test asserts them');
|
|
385
|
-
}
|
|
396
|
+
if (!constants.includes('LANE_BASE_lean=7000')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 7000)');
|
|
397
|
+
if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
|
|
398
|
+
}
|
|
386
399
|
}
|
|
387
400
|
|
|
388
401
|
// Conditional-assertion guard: an expect() reachable only inside a truthiness
|
|
@@ -87,13 +87,15 @@ for (const id of TARGET_IDS) {
|
|
|
87
87
|
const RUNTIME_SHAPES: RegExp[] = [
|
|
88
88
|
/^\.mugiwara\/?(\*\*)?$/, // bare mention of the dir
|
|
89
89
|
/^~?\/?\.mugiwara\/config$/, // project + global config
|
|
90
|
-
/^\.mugiwara\/
|
|
91
|
-
/^\.mugiwara\/
|
|
92
|
-
/^\.mugiwara\/
|
|
93
|
-
/^\.mugiwara\/(
|
|
94
|
-
/^\.mugiwara\/
|
|
95
|
-
/^\.mugiwara\/
|
|
96
|
-
/^\.mugiwara\/
|
|
90
|
+
/^\.mugiwara\/lessons\.md$/,
|
|
91
|
+
/^\.mugiwara\/index\.md$/,
|
|
92
|
+
/^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/(plan|spec|decisions|blockers|review|security|report)\.md$/,
|
|
93
|
+
/^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/flows\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/,
|
|
94
|
+
/^\.mugiwara\/missions\/<mission>\/flows\/?$/,
|
|
95
|
+
/^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/waves\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/, // legacy layout, still read
|
|
96
|
+
/^\.mugiwara\/missions\/<mission>\/waves\/?$/, // legacy layout, still read
|
|
97
|
+
/^\.mugiwara\/missions\/<mission>\/(state\.json|<member>\.json)$/,
|
|
98
|
+
/^\.mugiwara\/missions\/<mission>\/(continue\.json|continue-<member>\.json)$/,
|
|
97
99
|
];
|
|
98
100
|
|
|
99
101
|
// Shape violations that exist today and are NOT fixed in this mission because
|
package/src/args.ts
CHANGED
|
@@ -6,10 +6,10 @@ export type Args = {
|
|
|
6
6
|
flags: Record<string, FlagValue>;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
|
|
9
|
+
const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before' };
|
|
10
10
|
const BOOL_FLAGS: Record<string, string> = {
|
|
11
11
|
'--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
|
|
12
|
-
'--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all',
|
|
12
|
+
'--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',
|
|
13
13
|
'--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
|
|
14
14
|
};
|
|
15
15
|
|
package/src/budget.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/budget.ts
|
|
2
|
+
// Context budget as a gate: a mission's trail is itself
|
|
3
|
+
// context the next reader must load. At archive time, measure the total
|
|
4
|
+
// footprint of the artifacts that survive and compare against the configured
|
|
5
|
+
// ceiling — a bloated trail is caught like a failed test, with a visible
|
|
6
|
+
// number in the report.
|
|
7
|
+
//
|
|
8
|
+
// Token telemetry stays honest elsewhere: the estimator remains the default;
|
|
9
|
+
// `tokens_source: reported` activates only where the harness exposes real
|
|
10
|
+
// usage (see docs/concepts/cost.md). This module does not estimate tokens.
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export function readBudgetConfig(projectDir: string): number {
|
|
16
|
+
for (const base of [projectDir, homedir()]) {
|
|
17
|
+
const file = join(base, '.mugiwara', 'config');
|
|
18
|
+
if (!existsSync(file)) continue;
|
|
19
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
20
|
+
const t = line.trim();
|
|
21
|
+
if (!t || t.startsWith('#')) continue;
|
|
22
|
+
const eq = t.indexOf('=');
|
|
23
|
+
if (eq === -1) continue;
|
|
24
|
+
if (t.slice(0, eq).trim() !== 'context_budget_chars') continue;
|
|
25
|
+
const n = Number(t.slice(eq + 1).trim());
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return 0; // unset — measurement still recorded, never enforced
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Sum of bytes across the trail: top-level *.md + flows/* (legacy waves/* counts too). */
|
|
33
|
+
export function measureContextChars(missionDir: string): number {
|
|
34
|
+
let total = 0;
|
|
35
|
+
const add = (p: string): void => {
|
|
36
|
+
try { total += statSync(p).size; } catch { /* vanished mid-measure */ }
|
|
37
|
+
};
|
|
38
|
+
for (const f of readdirSync(missionDir)) {
|
|
39
|
+
if (/\.md$/.test(f)) add(join(missionDir, f));
|
|
40
|
+
}
|
|
41
|
+
for (const sub of ['flows', 'waves']) {
|
|
42
|
+
const dir = join(missionDir, sub);
|
|
43
|
+
if (existsSync(dir)) {
|
|
44
|
+
for (const f of readdirSync(dir)) add(join(dir, f));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return total;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatFootprint(chars: number, budget: number): string {
|
|
51
|
+
const base = `Context footprint: ${chars} chars`;
|
|
52
|
+
if (!budget) return `${base} (no budget configured)`;
|
|
53
|
+
return chars > budget
|
|
54
|
+
? `${base} — OVER budget ${budget}`
|
|
55
|
+
: `${base} (budget ${budget})`;
|
|
56
|
+
}
|