@ionivetech/mugiwara 0.5.5 → 0.6.1
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 +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/commands/mugiwara-continue.md +16 -0
- package/.opencode/commands/mugiwara-execute.md +6 -4
- package/.opencode/commands/mugiwara-heal.md +6 -4
- package/.opencode/commands/mugiwara-plan.md +6 -4
- package/.opencode/commands/mugiwara-review.md +6 -4
- package/.opencode/commands/mugiwara-security.md +6 -4
- package/.opencode/commands/mugiwara-ship.md +6 -4
- package/.opencode/commands/mugiwara.md +4 -2
- package/.opencode/commands/using-mugiwara.md +7 -13
- package/.opencode/mugiwara-helpers.mjs +24 -0
- package/.opencode/plugins/mugiwara.mjs +28 -8
- package/AGENTS.md +1 -1
- package/GEMINI.md +1 -1
- package/README.md +311 -127
- package/content/agents/brook-healing.md +17 -3
- package/content/agents/chopper-checkpoint.md +17 -3
- package/content/agents/eval-runner.md +17 -2
- package/content/agents/franky-gates.md +19 -5
- package/content/agents/jinbe-security.md +19 -3
- package/content/agents/luffy-orchestrator.md +6 -3
- package/content/agents/memory-keeper.md +15 -0
- package/content/agents/nami-planner.md +21 -6
- package/content/agents/onboarding-guide.md +145 -0
- package/content/agents/resume-coordinator.md +21 -5
- package/content/agents/robin-reviewer.md +18 -3
- package/content/agents/sanji-quality.md +19 -4
- package/content/agents/skeptic-verifier.md +18 -3
- package/content/agents/usopp-brainstorm.md +17 -3
- package/content/agents/zoro-execution.md +18 -3
- package/content/skills/mugiwara-backend/SKILL.md +1 -1
- package/content/skills/mugiwara-brainstorm/SKILL.md +12 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +2 -2
- package/content/skills/mugiwara-contract-first/SKILL.md +2 -36
- package/content/skills/mugiwara-contract-first/references/process.md +37 -0
- package/content/skills/mugiwara-execution/SKILL.md +51 -34
- package/content/skills/mugiwara-execution/references/dispatch.md +41 -0
- package/content/skills/mugiwara-execution/references/resume-batching.md +32 -0
- package/content/skills/mugiwara-frontend/SKILL.md +1 -1
- package/content/skills/mugiwara-gates/SKILL.md +32 -30
- package/content/skills/mugiwara-healing/SKILL.md +1 -1
- package/content/skills/mugiwara-orchestration/SKILL.md +59 -39
- package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
- package/content/skills/mugiwara-orchestration/references/delegation-pillars.md +40 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +97 -0
- package/content/skills/mugiwara-planning/SKILL.md +29 -27
- package/content/skills/mugiwara-planning/references/plan-template.md +42 -0
- package/content/skills/mugiwara-pr/SKILL.md +9 -2
- package/content/skills/mugiwara-quality/SKILL.md +17 -6
- package/content/skills/mugiwara-resume/SKILL.md +7 -2
- package/content/skills/mugiwara-review/SKILL.md +20 -1
- package/content/skills/mugiwara-review/references/code-attributes.md +38 -0
- package/content/skills/mugiwara-root-cause/SKILL.md +3 -30
- package/content/skills/mugiwara-root-cause/references/process.md +35 -0
- package/content/skills/mugiwara-security/SKILL.md +17 -32
- package/content/skills/mugiwara-security/references/owasp-top10.md +12 -0
- package/content/skills/mugiwara-security/references/rationalizations.md +8 -0
- package/content/skills/mugiwara-ship/SKILL.md +13 -10
- package/content/skills/mugiwara-testcases/SKILL.md +7 -0
- package/content/skills/mugiwara-workflow/SKILL.md +74 -77
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +50 -0
- package/content/skills/using-mugiwara/SKILL.md +13 -18
- package/dist/mugiwara.js +293 -47
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.ts +0 -0
- package/hooks/session-start.ts +1 -1
- package/package.json +11 -4
- package/plugin.json +1 -1
- package/references/multi-actor.md +1 -1
- package/scripts/evidence.sh +28 -6
- package/scripts/gate-selftest.ts +317 -0
- package/scripts/initiative.ts +262 -0
- package/scripts/lane.sh +18 -1
- package/scripts/mission-report.sh +237 -36
- package/scripts/onboard.ts +292 -0
- package/scripts/release-notes.ts +42 -13
- package/scripts/savepoint.sh +151 -60
- package/scripts/validate-content.ts +97 -0
- package/scripts/verify-install.ts +72 -0
- package/src/cli.ts +20 -3
- package/src/installer.ts +71 -8
- package/src/mission.ts +117 -5
- package/src/targets/claude.ts +45 -9
- package/src/targets/generic.ts +1 -1
- package/src/targets/opencode.ts +31 -10
- package/docs/concepts/agents.md +0 -53
- package/docs/concepts/audit-trail.md +0 -65
- package/docs/concepts/comparison.md +0 -58
- package/docs/concepts/config.md +0 -55
- package/docs/concepts/cost.md +0 -45
- package/docs/concepts/execution-model.md +0 -92
- package/docs/concepts/git-strategy.md +0 -62
- package/docs/concepts/lanes.md +0 -82
- package/docs/concepts/modes.md +0 -73
- package/docs/concepts/pr-summary.md +0 -54
- package/docs/concepts/skills.md +0 -55
- package/docs/concepts/workflow.md +0 -89
- package/docs/getting-started.md +0 -158
- package/docs/index.md +0 -56
- package/docs/install/antigravity.md +0 -45
- package/docs/install/claude.md +0 -77
- package/docs/install/cli.md +0 -115
- package/docs/install/codex.md +0 -44
- package/docs/install/copilot.md +0 -45
- package/docs/install/cursor.md +0 -45
- package/docs/install/gemini.md +0 -44
- package/docs/install/index.md +0 -53
- package/docs/install/kimi.md +0 -45
- package/docs/install/opencode.md +0 -143
- package/docs/install/pi.md +0 -46
- package/docs/reference/adoption-guide.md +0 -72
- package/docs/reference/agent-anatomy.md +0 -72
- package/docs/reference/compliance-matrix.md +0 -81
- package/docs/reference/developer-onboarding.md +0 -89
- package/docs/reference/enforcement.md +0 -35
- package/docs/reference/harness-matrix.md +0 -41
- package/docs/reference/skill-anatomy.md +0 -71
- package/docs/troubleshooting.md +0 -91
- package/evals/cases/_no-skill.json +0 -16
- package/evals/cases/adversarial-pressure-fake-pass.json +0 -25
- package/evals/cases/adversarial-pressure-skip-review.json +0 -25
- package/evals/cases/lane-exploratory-vague.json +0 -24
- package/evals/cases/lane-sensitivity-payment.json +0 -24
- package/evals/cases/positive-refactor-existing-tests.json +0 -25
- package/evals/cases/positive-resume-mid-mission.json +0 -24
- package/evals/cases/routing-agent-security.json +0 -25
- package/evals/cases/routing-auth-feature.json +0 -26
- package/evals/cases/routing-backend.json +0 -25
- package/evals/cases/routing-bug-one-file.json +0 -26
- package/evals/cases/routing-claim-audit.json +0 -25
- package/evals/cases/routing-context-budget.json +0 -25
- package/evals/cases/routing-contract-first.json +0 -25
- package/evals/cases/routing-execution.json +0 -25
- package/evals/cases/routing-frontend.json +0 -26
- package/evals/cases/routing-gates.json +0 -25
- package/evals/cases/routing-git.json +0 -25
- package/evals/cases/routing-healing.json +0 -25
- package/evals/cases/routing-lessons.json +0 -25
- package/evals/cases/routing-orchestration.json +0 -25
- package/evals/cases/routing-planning.json +0 -26
- package/evals/cases/routing-pr.json +0 -25
- package/evals/cases/routing-quality.json +0 -25
- package/evals/cases/routing-ship.json +0 -26
- package/evals/cases/routing-sunset.json +0 -25
- package/evals/cases/routing-using-mugiwara.json +0 -25
- package/evals/cases/routing-workflow.json +0 -25
- package/evals/floor.json +0 -6
package/scripts/release-notes.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// scripts/release-notes.ts — generates a
|
|
3
|
-
// from git history between the previous tag and HEAD.
|
|
4
|
-
// conventional-commit
|
|
2
|
+
// scripts/release-notes.ts — generates a detailed GitHub Release description
|
|
3
|
+
// from git history between the previous tag and HEAD. Pulls the full commit
|
|
4
|
+
// message (subject + body), groups by conventional-commit type, flags breaking
|
|
5
|
+
// changes, and lists the affected scopes. No gh dependency.
|
|
5
6
|
//
|
|
6
|
-
// bun scripts/release-notes.ts
|
|
7
|
+
// bun scripts/release-notes.ts notes since the last tag → stdout
|
|
7
8
|
// bun scripts/release-notes.ts --since v0.3.0 notes from that tag → stdout
|
|
8
9
|
import { execFileSync } from 'node:child_process';
|
|
9
10
|
|
|
@@ -22,15 +23,22 @@ if (!since) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const range = since ? `${since}..HEAD` : '';
|
|
25
|
-
const raw = execFileSync('git', ['log', '--pretty=%s', ...(range ? [range] : [])], { encoding: 'utf8' });
|
|
26
|
-
const commits = raw
|
|
26
|
+
const raw = execFileSync('git', ['log', '--pretty=%H%n%s%n%n%b%n__END__', ...(range ? [range] : [])], { encoding: 'utf8' });
|
|
27
|
+
const commits = raw
|
|
28
|
+
.split('__END__\n')
|
|
29
|
+
.map(c => c.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map(c => {
|
|
32
|
+
const [sha, subject, ...rest] = c.split('\n');
|
|
33
|
+
return { sha: sha.slice(0, 7), subject: subject ?? '', body: rest.join('\n').trim() };
|
|
34
|
+
});
|
|
27
35
|
|
|
28
36
|
const groups: Record<string, { label: string; items: string[] }> = {
|
|
29
37
|
feat: { label: 'New', items: [] },
|
|
30
38
|
fix: { label: 'Fixed', items: [] },
|
|
31
|
-
docs: { label: 'Docs', items: [] },
|
|
32
39
|
refactor: { label: 'Refactored', items: [] },
|
|
33
40
|
perf: { label: 'Performance', items: [] },
|
|
41
|
+
docs: { label: 'Docs', items: [] },
|
|
34
42
|
chore: { label: 'Housekeeping', items: [] },
|
|
35
43
|
test: { label: 'Housekeeping', items: [] },
|
|
36
44
|
ci: { label: 'Housekeeping', items: [] },
|
|
@@ -38,12 +46,33 @@ const groups: Record<string, { label: string; items: string[] }> = {
|
|
|
38
46
|
|
|
39
47
|
const order = ['feat', 'fix', 'refactor', 'perf', 'docs', 'chore'];
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
// strip signature trailers (Co-authored-by, Signed-off-by, review notes)
|
|
50
|
+
const TRAILER = /^(Co-authored-by|Signed-off-by|Reviewed-by|Helped-by|Reported-by|Tested-by|Acked-by):/i;
|
|
51
|
+
|
|
52
|
+
for (const commit of commits) {
|
|
53
|
+
const m = /^(\w+)(?:\((.*?)\))?!?: (.*)/.exec(commit.subject);
|
|
43
54
|
const type = m ? m[1] : 'chore';
|
|
44
|
-
const
|
|
55
|
+
const scope = m?.[2] || '';
|
|
56
|
+
const text = m ? m[3] : commit.subject;
|
|
57
|
+
const breaking = Boolean(m?.[0]?.includes('!')) || /^BREAKING CHANGE:/m.test(commit.body);
|
|
58
|
+
|
|
59
|
+
const title = text.charAt(0).toUpperCase() + text.slice(1);
|
|
60
|
+
const scopeTag = scope ? ` \`${scope}\`` : '';
|
|
61
|
+
const breakingTag = breaking ? ' ⚠️ **BREAKING**' : '';
|
|
62
|
+
|
|
63
|
+
const bodyLines = commit.body
|
|
64
|
+
.split(/\r?\n/)
|
|
65
|
+
.map(l => l.trim())
|
|
66
|
+
.filter(l => l && !TRAILER.test(l) && l !== '---')
|
|
67
|
+
// the release-version marker commit repeats the subject as body — drop it
|
|
68
|
+
.filter(l => l !== text);
|
|
69
|
+
|
|
70
|
+
let entry = `- ${title}${scopeTag}${breakingTag} \`${commit.sha}\``;
|
|
71
|
+
if (bodyLines.length) {
|
|
72
|
+
entry += '\n' + bodyLines.map(l => ` - ${l}`).join('\n');
|
|
73
|
+
}
|
|
45
74
|
const g = groups[type] ?? groups.chore;
|
|
46
|
-
g.items.push(
|
|
75
|
+
g.items.push(entry);
|
|
47
76
|
}
|
|
48
77
|
|
|
49
78
|
const lines: string[] = [];
|
|
@@ -52,11 +81,11 @@ for (const t of order) {
|
|
|
52
81
|
if (!g.items.length) continue;
|
|
53
82
|
lines.push(`## ${g.label}`);
|
|
54
83
|
lines.push('');
|
|
55
|
-
for (const item of g.items) lines.push(
|
|
84
|
+
for (const item of g.items) lines.push(item);
|
|
56
85
|
lines.push('');
|
|
57
86
|
}
|
|
58
87
|
|
|
59
|
-
const bumpIdx = commits.findIndex(c => /chore: release|chore: bump/.test(c));
|
|
88
|
+
const bumpIdx = commits.findIndex(c => /chore: release|chore: bump/.test(c.subject));
|
|
60
89
|
const body = commits.slice(0, bumpIdx === -1 ? commits.length : bumpIdx);
|
|
61
90
|
const count = body.length || commits.length;
|
|
62
91
|
|
package/scripts/savepoint.sh
CHANGED
|
@@ -8,19 +8,33 @@ die() { echo "savepoint: $*" >&2; exit 1; }
|
|
|
8
8
|
MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
|
|
9
9
|
|
|
10
10
|
# --- parse mission args ---
|
|
11
|
-
|
|
12
|
-
ACTOR="${2:-${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${USER:-}}}}"
|
|
13
|
-
BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
|
|
14
|
-
WAVE="${4:-${STATE_WAVE:-1}}"
|
|
15
|
-
MODE="${5:-${STATE_MODE:-guided}}"
|
|
16
|
-
|
|
17
|
-
# --branch flag for per-branch state
|
|
11
|
+
# --branch flag must parse FIRST — it shifts positionals
|
|
18
12
|
BRANCH_MODE=0
|
|
19
13
|
if [ "${1:-}" = "--branch" ]; then
|
|
20
14
|
BRANCH_MODE=1
|
|
21
|
-
|
|
15
|
+
shift
|
|
16
|
+
MISSION="${1:-${STATE_MISSION:-}}"
|
|
17
|
+
ACTOR="${2:-${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${USER:-}}}}"
|
|
18
|
+
BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
|
|
19
|
+
WAVE="${4:-${STATE_WAVE:-1}}"
|
|
20
|
+
MODE="${5:-${STATE_MODE:-guided}}"
|
|
21
|
+
else
|
|
22
|
+
MISSION="${1:-${STATE_MISSION:-}}"
|
|
23
|
+
ACTOR="${2:-${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${USER:-}}}}"
|
|
22
24
|
BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
|
|
23
|
-
|
|
25
|
+
WAVE="${4:-${STATE_WAVE:-1}}"
|
|
26
|
+
MODE="${5:-${STATE_MODE:-guided}}"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# mission allowlist — MISSION feeds paths + sed + node argv; traversal or
|
|
30
|
+
# sed metacharacters must not reach filesystem operations
|
|
31
|
+
case "$MISSION" in
|
|
32
|
+
""|*[!a-zA-Z0-9._-]*) die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-])" ;;
|
|
33
|
+
esac
|
|
34
|
+
# dot-only names (".", "..", "...") pass the char allowlist but resolve upward
|
|
35
|
+
# through join(...,"..") — reject them before any path is built from MISSION.
|
|
36
|
+
if [[ "$MISSION" =~ ^\.+$ ]]; then
|
|
37
|
+
die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
|
|
24
38
|
fi
|
|
25
39
|
|
|
26
40
|
# per-branch state file when --branch used
|
|
@@ -50,61 +64,119 @@ if [ "$BASE_SHA" != "unknown" ]; then
|
|
|
50
64
|
fi
|
|
51
65
|
[ -z "$LOC_DELTA" ] && LOC_DELTA=0
|
|
52
66
|
|
|
53
|
-
SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env|
|
|
67
|
+
SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env$|config/.*key|migration/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$"
|
|
54
68
|
SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATTERNS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
|
55
69
|
|
|
56
70
|
LANE="direct"
|
|
57
71
|
LANE_REASON=""
|
|
58
|
-
|
|
72
|
+
# Sensitive-path escalation is unconditional — it wins over any count-based
|
|
73
|
+
# lane AND over the docs-only downgrade below (bug C9).
|
|
74
|
+
if [ -n "$SENSITIVE_PATHS" ]; then
|
|
75
|
+
LANE="full"
|
|
76
|
+
LANE_REASON="sensitive paths: $SENSITIVE_PATHS"
|
|
77
|
+
elif [ "$FILES_TOUCHED" -ge 9 ] 2>/dev/null; then
|
|
59
78
|
LANE="full"
|
|
60
|
-
LANE_REASON="$
|
|
79
|
+
LANE_REASON="$FILES_TOUCHED files"
|
|
61
80
|
elif [ "$FILES_TOUCHED" -ge 3 ] 2>/dev/null; then
|
|
62
81
|
LANE="standard"
|
|
63
82
|
LANE_REASON="$FILES_TOUCHED files"
|
|
64
83
|
elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
|
|
65
84
|
LANE="lean"
|
|
66
85
|
LANE_REASON="$FILES_TOUCHED files"
|
|
86
|
+
elif [ "$FILES_TOUCHED" -eq 1 ] 2>/dev/null; then
|
|
87
|
+
# 1-file rule mirrors lane.sh: >=20 added LOC -> lean, else direct
|
|
88
|
+
ADDED=$(git diff --numstat "$BASE_SHA"..HEAD 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
89
|
+
if [ "${ADDED:-0}" -ge 20 ] 2>/dev/null; then
|
|
90
|
+
LANE="lean"
|
|
91
|
+
LANE_REASON="1 file, $ADDED LOC"
|
|
92
|
+
else
|
|
93
|
+
LANE="direct"
|
|
94
|
+
LANE_REASON="1 file, <20 LOC"
|
|
95
|
+
fi
|
|
67
96
|
else
|
|
68
97
|
LANE="direct"
|
|
69
98
|
LANE_REASON="$FILES_TOUCHED file(s) under 20 LOC"
|
|
70
99
|
fi
|
|
71
100
|
|
|
72
|
-
#
|
|
73
|
-
|
|
101
|
+
# path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
|
|
102
|
+
# surface never escalate to full from file count alone; sensitive-path
|
|
103
|
+
# escalation above still wins.
|
|
104
|
+
PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
|
|
105
|
+
if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ]; then
|
|
106
|
+
CODE_COUNT=$(echo "$CHANGED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
107
|
+
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
108
|
+
PREV="$LANE"
|
|
109
|
+
LANE="standard"
|
|
110
|
+
LANE_REASON="$FILES_TOUCHED files, docs-only — path-weighted down from $PREV"
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# task counts from plan doc — plan is written date-prefixed (plans/YYYY-MM-DD-<mission>.md)
|
|
115
|
+
# or bare (plans/<mission>.md); glob both, first match wins.
|
|
116
|
+
PLAN_FILE=$(ls "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md 2>/dev/null | head -1 || true)
|
|
74
117
|
TASKS_DONE=0
|
|
75
118
|
TASKS_TOTAL=0
|
|
76
119
|
if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
77
|
-
|
|
78
|
-
|
|
120
|
+
# total counts ALL task lines (checked + unchecked); done counts checked only.
|
|
121
|
+
# A fully-completed plan must read total=N done=N, never total=0 (the old
|
|
122
|
+
# unchecked-only grep degenerated a done plan to tasks.total=0).
|
|
123
|
+
TASKS_TOTAL=$(grep -cE '^\s*-\s*\[[ xX]\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
124
|
+
TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
79
125
|
fi
|
|
80
126
|
|
|
81
127
|
# blocker count
|
|
82
128
|
BLOCKERS_FILE=$(ls "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" 2>/dev/null || true)
|
|
83
129
|
BLOCKERS_OPEN=0
|
|
84
130
|
if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
|
|
85
|
-
|
|
131
|
+
# data rows start with a wave number; header "| wave |" and separator
|
|
132
|
+
# "|---|" are excluded by the ^\| ?[0-9]+ \| pattern
|
|
133
|
+
BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
|
|
86
134
|
fi
|
|
87
135
|
|
|
88
|
-
# heal cycle
|
|
136
|
+
# heal cycle — count WAVE-8 banner occurrences in the trace, not the word
|
|
137
|
+
# "heal" anywhere (heal workers/healing text would inflate the counter and
|
|
138
|
+
# cause a premature halt)
|
|
89
139
|
HEAL_CYCLE=1
|
|
90
|
-
TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}
|
|
140
|
+
TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
|
|
91
141
|
if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
|
|
92
|
-
HEAL_COUNT=$(grep -ci 'Wave 8
|
|
142
|
+
HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null || true)
|
|
93
143
|
HEAL_CYCLE=$((HEAL_COUNT + 1))
|
|
94
144
|
fi
|
|
95
145
|
|
|
96
|
-
# evidence paths
|
|
97
|
-
|
|
146
|
+
# evidence paths — per-mission results folder (quoted printf, no sed — mission
|
|
147
|
+
# name is allowlisted above, but avoid sed metacharacter semantics entirely)
|
|
148
|
+
EVIDENCE=""
|
|
149
|
+
for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
|
|
150
|
+
[ -f "$evf" ] || continue
|
|
151
|
+
EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
|
|
152
|
+
done
|
|
98
153
|
|
|
99
|
-
# skill version from package.json
|
|
154
|
+
# skill version from package.json (argv-passing — never interpolate paths into node -e)
|
|
100
155
|
SKILL_VERSION="1"
|
|
101
156
|
if [ -f package.json ]; then
|
|
102
157
|
PKG_JSON="$MUGIWARA_DIR/../package.json"
|
|
103
|
-
[ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(
|
|
158
|
+
[ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version.split('.')[0])}catch(e){console.log('1')}" "$PKG_JSON" 2>/dev/null || echo "1")
|
|
104
159
|
fi
|
|
105
160
|
|
|
106
|
-
# tokens
|
|
107
|
-
|
|
161
|
+
# tokens proxy (F7): deterministic estimate when the harness does not report
|
|
162
|
+
# real usage. Monotonic beats precise — LANE_BASE stands in for the skills
|
|
163
|
+
# loaded this lane; loc_delta and written-artifact words scale with growth.
|
|
164
|
+
# MUGIWARA_TOKENS overrides as the reported value.
|
|
165
|
+
LANE_BASE=0
|
|
166
|
+
case "$LANE" in
|
|
167
|
+
lean) LANE_BASE=1500 ;;
|
|
168
|
+
standard) LANE_BASE=4000 ;;
|
|
169
|
+
full) LANE_BASE=9000 ;;
|
|
170
|
+
spike) LANE_BASE=1000 ;;
|
|
171
|
+
esac
|
|
172
|
+
DOC_WORDS=$(cat "$MUGIWARA_DIR"/results/${MISSION}/*.md "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md "$MUGIWARA_DIR"/spec/${MISSION}.md "$MUGIWARA_DIR"/spec/*-${MISSION}.md "$MUGIWARA_DIR"/logs/${MISSION}.md "$MUGIWARA_DIR"/logs/*-${MISSION}.md 2>/dev/null | wc -w | tr -d ' ')
|
|
173
|
+
LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))
|
|
174
|
+
TOKENS_SOURCE="computed"
|
|
175
|
+
TOKENS_EST=$(( LANE_BASE + DOC_WORDS * 135 / 100 + LOC_TOKENS ))
|
|
176
|
+
if [ -n "${MUGIWARA_TOKENS:-}" ]; then
|
|
177
|
+
TOKENS_EST="${MUGIWARA_TOKENS}"
|
|
178
|
+
TOKENS_SOURCE="reported"
|
|
179
|
+
fi
|
|
108
180
|
|
|
109
181
|
# budget per lane
|
|
110
182
|
BUDGET=0
|
|
@@ -126,41 +198,60 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
|
|
|
126
198
|
STATUS="warn"
|
|
127
199
|
fi
|
|
128
200
|
|
|
129
|
-
SENSITIVE_ARR=""
|
|
130
|
-
if [ -n "$SENSITIVE_PATHS" ]; then
|
|
131
|
-
SENSITIVE_ARR=$(echo "$SENSITIVE_PATHS" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
|
|
132
|
-
fi
|
|
133
|
-
EVIDENCE_ARR=""
|
|
134
|
-
if [ -n "$EVIDENCE" ]; then
|
|
135
|
-
EVIDENCE_ARR=$(echo "$EVIDENCE" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
201
|
mkdir -p "$MUGIWARA_DIR"
|
|
139
202
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"sensitive_paths": [${SENSITIVE_ARR}],
|
|
154
|
-
"tasks": { "done": $TASKS_DONE, "total": $TASKS_TOTAL },
|
|
155
|
-
"blockers_open": $BLOCKERS_OPEN,
|
|
156
|
-
"heal_cycle": $HEAL_CYCLE,
|
|
157
|
-
"tokens_est": $TOKENS_EST,
|
|
158
|
-
"budget": $BUDGET,
|
|
159
|
-
"budget_status": "$STATUS",
|
|
160
|
-
"skill_version": "$SKILL_VERSION",
|
|
161
|
-
"evidence": [${EVIDENCE_ARR}],
|
|
162
|
-
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
163
|
-
}
|
|
164
|
-
JSON
|
|
203
|
+
# --- lane-rise compare (F9): read previous state, flag escalation ---
|
|
204
|
+
LANE_PREV=""
|
|
205
|
+
LANE_ROSE=false
|
|
206
|
+
if [ -f "$STATE_FILE" ]; then
|
|
207
|
+
LANE_PREV=$(node -e "try{const s=require(process.argv[1]);process.stdout.write(s.lane||'')}catch(e){process.stdout.write('')}" "$STATE_FILE" 2>/dev/null || true)
|
|
208
|
+
if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
|
|
209
|
+
# lane order: direct < lean < standard < full < spike (spike resizes, not a rise)
|
|
210
|
+
case "$LANE_PREV:$LANE" in
|
|
211
|
+
direct:lean|direct:standard|direct:full|lean:standard|lean:full|standard:full) LANE_ROSE=true ;;
|
|
212
|
+
*) LANE_ROSE=false ;;
|
|
213
|
+
esac
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
165
216
|
|
|
217
|
+
node -e "
|
|
218
|
+
const data = {
|
|
219
|
+
mission: process.argv[1],
|
|
220
|
+
actor: process.argv[2],
|
|
221
|
+
branch: process.argv[3],
|
|
222
|
+
lane: process.argv[4],
|
|
223
|
+
lane_reason: process.argv[5],
|
|
224
|
+
lane_prev: process.argv[24] || null,
|
|
225
|
+
lane_rose: process.argv[25] === 'true',
|
|
226
|
+
wave: parseInt(process.argv[6], 10),
|
|
227
|
+
mode: process.argv[7],
|
|
228
|
+
base_sha: process.argv[8],
|
|
229
|
+
head_sha: process.argv[9],
|
|
230
|
+
files_touched: parseInt(process.argv[10], 10),
|
|
231
|
+
loc_delta: parseInt(process.argv[11], 10),
|
|
232
|
+
sensitive_paths: process.argv[12] ? process.argv[12].split(',').filter(Boolean) : [],
|
|
233
|
+
tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
|
|
234
|
+
blockers_open: parseInt(process.argv[15], 10),
|
|
235
|
+
heal_cycle: parseInt(process.argv[16], 10),
|
|
236
|
+
tokens_est: parseInt(process.argv[17], 10) || 0,
|
|
237
|
+
tokens_source: process.argv[26] || 'computed',
|
|
238
|
+
budget: parseInt(process.argv[18], 10) || 0,
|
|
239
|
+
budget_status: process.argv[19],
|
|
240
|
+
skill_version: process.argv[20],
|
|
241
|
+
evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
|
|
242
|
+
updated_at: process.argv[22]
|
|
243
|
+
};
|
|
244
|
+
require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
|
|
245
|
+
" \
|
|
246
|
+
"$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
|
|
247
|
+
"$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
|
|
248
|
+
"$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
|
|
249
|
+
"$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
|
|
250
|
+
"$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
|
|
251
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
252
|
+
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE"
|
|
253
|
+
|
|
254
|
+
if [ "$LANE_ROSE" = true ]; then
|
|
255
|
+
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
256
|
+
fi
|
|
166
257
|
echo "✓ savepoint written: $STATE_FILE (lane=$LANE, wave=$WAVE, files=$FILES_TOUCHED)"
|
|
@@ -27,6 +27,31 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
|
|
|
27
27
|
if (bullets.length === 0) errors.push(`skill ${file}: "## Skip when" needs ≥1 bullet`);
|
|
28
28
|
if (bullets.length > 4) errors.push(`skill ${file}: "## Skip when" block exceeds 4 bullets`);
|
|
29
29
|
}
|
|
30
|
+
if (kind === 'skill' && !body.includes('## Red flags')) errors.push(`skill ${file}: missing required "## Red flags" block`);
|
|
31
|
+
if (kind === 'skill') {
|
|
32
|
+
const lines = body.split(/\r?\n/);
|
|
33
|
+
const headingRe = /^## /;
|
|
34
|
+
const sections: { heading: string; start: number; end: number }[] = [];
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
if (headingRe.test(lines[i])) sections.push({ heading: lines[i].replace(/^## /, ''), start: i, end: 0 });
|
|
37
|
+
}
|
|
38
|
+
for (let si = 0; si < sections.length; si++) {
|
|
39
|
+
sections[si].end = si + 1 < sections.length ? sections[si + 1].start : lines.length;
|
|
40
|
+
// count non-empty, non-table, non-code-fence content lines
|
|
41
|
+
let inFence = false;
|
|
42
|
+
let contentLines = 0;
|
|
43
|
+
for (let li = sections[si].start; li < sections[si].end; li++) {
|
|
44
|
+
const l = lines[li].trim();
|
|
45
|
+
if (!l) continue;
|
|
46
|
+
if (l.startsWith('```')) { inFence = !inFence; continue; }
|
|
47
|
+
if (inFence) continue;
|
|
48
|
+
if (l.startsWith('|') || l.startsWith('- |')) continue;
|
|
49
|
+
contentLines++;
|
|
50
|
+
}
|
|
51
|
+
if (contentLines >= 20) errors.push(`skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥20) — move to references/`);
|
|
52
|
+
else if (contentLines >= 15) console.warn(`⚠ skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥15, consider moving to references/)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
30
55
|
return data;
|
|
31
56
|
}
|
|
32
57
|
|
|
@@ -110,6 +135,78 @@ for (const dir of skillDirs) {
|
|
|
110
135
|
if (dir !== 'mugiwara-workflow' && !usedSkills.has(dir)) errors.push(`skill ${dir}: not referenced by any agent`);
|
|
111
136
|
}
|
|
112
137
|
|
|
138
|
+
// --- write-scope gate (F2): every agent declares a scope; only executors may write source ---
|
|
139
|
+
const SOURCE_SCOPED = new Set(['zoro-execution', 'brook-healing']);
|
|
140
|
+
for (const f of agentFiles) {
|
|
141
|
+
const name = f.replace(/\.md$/, '');
|
|
142
|
+
const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
|
|
143
|
+
const scope = parsed.data['write-scope'];
|
|
144
|
+
if (!scope) { errors.push(`agent ${f}: missing write-scope (artifacts | source)`); continue; }
|
|
145
|
+
if (scope !== 'artifacts' && scope !== 'source') errors.push(`agent ${f}: write-scope must be artifacts | source (got "${scope}")`);
|
|
146
|
+
if (scope === 'source' && !SOURCE_SCOPED.has(name)) errors.push(`agent ${f}: only zoro-execution and brook-healing may declare write-scope: source`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- write-boundary gate (W1): the hub skill must carry the refusal rule ---
|
|
150
|
+
const hubFile = join(root, 'skills', 'mugiwara-orchestration', 'SKILL.md');
|
|
151
|
+
if (existsSync(hubFile) && !readFileSync(hubFile, 'utf8').includes('## Write boundary')) {
|
|
152
|
+
errors.push('skill mugiwara-orchestration: missing "## Write boundary" section (non-executors refuse source writes)');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- agent-count gate (F14): user-facing/internal split derivable from content/agents/ ---
|
|
156
|
+
const internalAgents = agentFiles.filter(f => {
|
|
157
|
+
const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
|
|
158
|
+
return parsed.data.internal === 'true';
|
|
159
|
+
});
|
|
160
|
+
const canonicalCount = `${agentFiles.length - internalAgents.length} agents (+${internalAgents.length} internal)`;
|
|
161
|
+
for (const doc of ['README.md', 'docs/index.md', 'docs/concepts/agents.md']) {
|
|
162
|
+
const p = join(import.meta.dirname, '..', doc);
|
|
163
|
+
if (existsSync(p) && !readFileSync(p, 'utf8').includes(canonicalCount)) {
|
|
164
|
+
errors.push(`${doc}: missing canonical agent count "${canonicalCount}"`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- hub-rule gate (F3): every non-Luffy agent carries both hub sections ---
|
|
169
|
+
for (const f of agentFiles) {
|
|
170
|
+
const name = f.replace(/\.md$/, '');
|
|
171
|
+
if (name === 'luffy-orchestrator') continue;
|
|
172
|
+
const text = readFileSync(join(agentDir, f), 'utf8');
|
|
173
|
+
if (!text.includes('## Before you start')) errors.push(`agent ${f}: missing "## Before you start" entry protocol`);
|
|
174
|
+
if (!text.includes('## Return to Luffy')) errors.push(`agent ${f}: missing "## Return to Luffy" hub rule`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- hub-skill gate (F3): every agent lists mugiwara-orchestration (the hub rule's home) ---
|
|
178
|
+
for (const f of agentFiles) {
|
|
179
|
+
const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
|
|
180
|
+
const skills = (parsed.data.skills ?? '').split(',').map(s => s.trim());
|
|
181
|
+
if (!skills.includes('mugiwara-orchestration')) errors.push(`agent ${f}: missing mugiwara-orchestration in skills (hub rule lives there)`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- handoff-target gate (F4): a body naming another crew member as a handoff
|
|
185
|
+
// target without Luffy in the same line fails CI ---
|
|
186
|
+
const HANDOFF_CREW = ['usopp', 'nami', 'zoro', 'chopper', 'sanji', 'franky', 'robin', 'jinbe', 'brook', 'skeptic', 'memory-keeper', 'resume-coordinator', 'eval-runner'];
|
|
187
|
+
// Verb class covers direct handoff language plus synonyms (route/send/pass/
|
|
188
|
+
// escalate/delegate/transfer) so the F4 gate does not leak through paraphrase.
|
|
189
|
+
const handoffRe = new RegExp(`\\b(hand to|hand off to|dispatch|route to|send to|pass to|escalate to|delegate to|transfer to)\\s+(?:a\\s+|another\\s+|the\\s+)?(?:crew members?|${HANDOFF_CREW.join('|')})`, 'i');
|
|
190
|
+
// Negation must DIRECTLY precede the handoff verb, not sit anywhere in the
|
|
191
|
+
// line ("do not hand off" later in the same line is a different clause).
|
|
192
|
+
const NEGATION_VERB = /\b(never|not|no|don't|do not)\s+(hand|dispatch|route|send|pass|escalate|delegate|transfer)\b/i;
|
|
193
|
+
for (const f of [...agentFiles, ...skillDirs.map(d => join(root, 'skills', d, 'SKILL.md'))]) {
|
|
194
|
+
if (!existsSync(f)) continue;
|
|
195
|
+
const text = readFileSync(f, 'utf8');
|
|
196
|
+
const lines = text.split(/\r?\n/);
|
|
197
|
+
for (let i = 0; i < lines.length; i++) {
|
|
198
|
+
const line = lines[i];
|
|
199
|
+
const m = handoffRe.exec(line);
|
|
200
|
+
if (!m) continue;
|
|
201
|
+
// clause boundary: only the clause containing the handoff verb counts
|
|
202
|
+
const clauseStart = line.lastIndexOf('.', m.index) + 1;
|
|
203
|
+
const clause = line.slice(clauseStart, m.index + m[0].length);
|
|
204
|
+
if (NEGATION_VERB.test(clause)) continue; // "never dispatch X" — constraint
|
|
205
|
+
if (/\bLuffy\b|to Luffy\b/.test(line)) continue; // hub named in same line ("main thread" is NOT the hub — Luffy is)
|
|
206
|
+
errors.push(`handoff leak ${f.replace(root + '/', '')}:${i + 1}: "${line.trim()}" — handoff target must be Luffy or name Luffy in the same line`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
113
210
|
// --- manifest-sync check: .claude-plugin/plugin.json must set-equal content/ ---
|
|
114
211
|
const manifestArg = process.argv.indexOf('--check-manifest');
|
|
115
212
|
if (manifestArg !== -1) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/verify-install.ts — G1: verify all references/*.md pointers resolve after install.
|
|
3
|
+
// Installs to temp dir for one target per tier, then checks every pointer.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdtempSync, rmSync, readFileSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { installTo } from '../src/installer.ts';
|
|
9
|
+
import { targets } from '../src/targets/index.ts';
|
|
10
|
+
|
|
11
|
+
const tierTargets = [
|
|
12
|
+
{ id: 'claude', tier: 1 },
|
|
13
|
+
{ id: 'codex', tier: 2 },
|
|
14
|
+
{ id: 'kilo', tier: 3 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
let total = 0;
|
|
18
|
+
let broken = 0;
|
|
19
|
+
|
|
20
|
+
for (const tt of tierTargets) {
|
|
21
|
+
const target = targets[tt.id];
|
|
22
|
+
if (!target) { console.log(`⚠ target ${tt.id} not found, skipping`); continue; }
|
|
23
|
+
const dir = mkdtempSync(join(tmpdir(), `mugi-verify-${tt.id}-`));
|
|
24
|
+
try {
|
|
25
|
+
installTo(target, { scope: 'project', projectDir: dir, dryRun: false, force: true });
|
|
26
|
+
|
|
27
|
+
const skillsDir = target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir;
|
|
28
|
+
const mugiwaraRefsDir = join(dir, '.mugiwara', 'refs');
|
|
29
|
+
|
|
30
|
+
function findSkillFiles(root: string): string[] {
|
|
31
|
+
const out: string[] = [];
|
|
32
|
+
if (!existsSync(root)) return out;
|
|
33
|
+
for (const ent of readdirSync(root, { withFileTypes: true })) {
|
|
34
|
+
const p = join(root, ent.name);
|
|
35
|
+
if (ent.isDirectory()) out.push(...findSkillFiles(p));
|
|
36
|
+
else if (ent.name.endsWith('.md')) out.push(p);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const skillFiles = findSkillFiles(skillsDir)
|
|
42
|
+
.filter(f => !f.replace(skillsDir, '').includes('/references/'));
|
|
43
|
+
|
|
44
|
+
for (const file of skillFiles) {
|
|
45
|
+
const body = readFileSync(file, 'utf8');
|
|
46
|
+
const refs = [...body.matchAll(/`([^`]*references\/[^`]+\.md)`/g)];
|
|
47
|
+
|
|
48
|
+
for (const m of refs) {
|
|
49
|
+
total++;
|
|
50
|
+
const pointer = m[1];
|
|
51
|
+
const localResolve = join(dirname(file), pointer);
|
|
52
|
+
const mugiwaraResolve = join(mugiwaraRefsDir, pointer.replace(/^(?:_shared\/)?references\//, ''));
|
|
53
|
+
const resolved = [localResolve, mugiwaraResolve].find(p => existsSync(p));
|
|
54
|
+
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
broken++;
|
|
57
|
+
const relFile = file.replace(dir + '/', '');
|
|
58
|
+
console.log(`✗ ${relFile}: \`${pointer}\` → not found`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (broken > 0) {
|
|
68
|
+
console.log(`\n✗ ${broken}/${total} pointers cannot resolve after install`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`✓ ${total} pointers checked across ${tierTargets.length} targets — all resolve`);
|
package/src/cli.ts
CHANGED
|
@@ -7,15 +7,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { parseArgs, type FlagValue, type Args } from './args.ts';
|
|
8
8
|
import { createRl, choose, multiChoose, confirm } from './prompt.ts';
|
|
9
9
|
import { targets, TARGET_IDS } from './targets/index.ts';
|
|
10
|
-
import { installTo, removeInstalled, VERSION } from './installer.ts';
|
|
10
|
+
import { installTo, removeInstalled, VERSION, ensureProjectGitignore } from './installer.ts';
|
|
11
11
|
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
12
|
-
import { resetMission } from './mission.ts';
|
|
12
|
+
import { resetMission, archiveMission } from './mission.ts';
|
|
13
13
|
|
|
14
14
|
const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
15
15
|
const flag = (v: FlagValue): boolean => v === true;
|
|
16
16
|
|
|
17
17
|
export async function run(argv: string[]): Promise<void> {
|
|
18
|
-
const { command, flags } = parseArgs(argv);
|
|
18
|
+
const { command, flags, _ } = parseArgs(argv);
|
|
19
19
|
if (flag(flags.help) || command === 'help') return help();
|
|
20
20
|
if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
|
|
21
21
|
switch (command) {
|
|
@@ -24,6 +24,7 @@ export async function run(argv: string[]): Promise<void> {
|
|
|
24
24
|
case 'uninstall': return uninstall(flags);
|
|
25
25
|
case 'list': return list(flags);
|
|
26
26
|
case 'reset': return resetCmd(flags);
|
|
27
|
+
case 'archive': return archive(flags, _);
|
|
27
28
|
default: throw new Error(`Unknown command: ${command}`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -41,6 +42,17 @@ function resetCmd(flags: Args['flags']): void {
|
|
|
41
42
|
if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function archive(flags: Args['flags'], positionals: string[]): void {
|
|
46
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
47
|
+
const mission = positionals[1];
|
|
48
|
+
if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
|
|
49
|
+
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
50
|
+
if (result.report) console.log(`archive target: ${result.report}`);
|
|
51
|
+
if (result.removed.length) console.log(`${flag(flags.dryRun) ? 'would remove' : 'removed'}: ${result.removed.join(', ')}`);
|
|
52
|
+
if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
|
|
53
|
+
if (result.index) console.log(`index updated: ${result.index}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
|
|
45
57
|
const interactive = !flag(flags.yes);
|
|
46
58
|
if (interactive && !process.stdin.isTTY) {
|
|
@@ -95,6 +107,10 @@ async function install(flags: Args['flags']): Promise<void> {
|
|
|
95
107
|
allFiles.push(...r.written);
|
|
96
108
|
allNotes.push(...r.notes);
|
|
97
109
|
}
|
|
110
|
+
if (scope === 'project') {
|
|
111
|
+
const gi = ensureProjectGitignore(projectDir, { dryRun: flag(flags.dryRun) });
|
|
112
|
+
allNotes.push(...gi.notes);
|
|
113
|
+
}
|
|
98
114
|
if (flag(flags.dryRun)) { console.log('\nDry run — nothing written.'); return; }
|
|
99
115
|
const file = manifestPath({ scope, projectDir, home });
|
|
100
116
|
const prev = readManifest(file);
|
|
@@ -182,6 +198,7 @@ Usage:
|
|
|
182
198
|
mugiwara list show installations
|
|
183
199
|
mugiwara list --check health check: show installations + missing files
|
|
184
200
|
mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
|
|
201
|
+
mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
|
|
185
202
|
mugiwara --help this help
|
|
186
203
|
mugiwara --version print version
|
|
187
204
|
|