@ionivetech/mugiwara 0.5.5 → 0.6.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 +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/plugins/mugiwara.mjs +18 -6
- package/AGENTS.md +1 -1
- package/GEMINI.md +1 -1
- package/README.md +251 -120
- 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 +5 -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 +30 -13
- 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 +41 -25
- package/content/skills/mugiwara-orchestration/references/delegation-pillars.md +40 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +96 -0
- package/content/skills/mugiwara-planning/SKILL.md +23 -27
- package/content/skills/mugiwara-planning/references/plan-template.md +42 -0
- package/content/skills/mugiwara-pr/SKILL.md +2 -2
- package/content/skills/mugiwara-quality/SKILL.md +10 -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 +5 -4
- 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 +8 -19
- package/dist/mugiwara.js +107 -41
- package/gemini-extension.json +1 -1
- 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 +12 -5
- package/scripts/gate-selftest.ts +266 -0
- package/scripts/initiative.ts +248 -0
- package/scripts/lane.sh +15 -0
- package/scripts/mission-report.sh +90 -12
- package/scripts/onboard.ts +292 -0
- package/scripts/release-notes.ts +42 -13
- package/scripts/savepoint.sh +97 -50
- package/scripts/validate-content.ts +77 -0
- package/scripts/verify-install.ts +72 -0
- package/src/installer.ts +34 -7
- package/src/mission.ts +7 -5
- package/src/targets/claude.ts +20 -3
- 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/savepoint.sh
CHANGED
|
@@ -8,21 +8,30 @@ 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}}"
|
|
24
27
|
fi
|
|
25
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
|
+
|
|
26
35
|
# per-branch state file when --branch used
|
|
27
36
|
BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
|
|
28
37
|
if [ "$BRANCH_MODE" -eq 1 ]; then
|
|
@@ -69,6 +78,19 @@ else
|
|
|
69
78
|
LANE_REASON="$FILES_TOUCHED file(s) under 20 LOC"
|
|
70
79
|
fi
|
|
71
80
|
|
|
81
|
+
# path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
|
|
82
|
+
# surface never escalate to full from file count alone; sensitive-path
|
|
83
|
+
# escalation above still wins.
|
|
84
|
+
PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
|
|
85
|
+
if [ "$LANE" = "full" ] && [ -n "$CHANGED_FILES" ]; then
|
|
86
|
+
CODE_COUNT=$(echo "$CHANGED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
87
|
+
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
88
|
+
PREV="$LANE"
|
|
89
|
+
LANE="standard"
|
|
90
|
+
LANE_REASON="$FILES_TOUCHED files, docs-only — path-weighted down from $PREV"
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
72
94
|
# task counts from plan doc
|
|
73
95
|
PLAN_FILE=$(ls "$MUGIWARA_DIR/plans/${MISSION}.md" 2>/dev/null || true)
|
|
74
96
|
TASKS_DONE=0
|
|
@@ -85,22 +107,29 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
|
|
|
85
107
|
BLOCKERS_OPEN=$(grep -c '|' "$BLOCKERS_FILE" 2>/dev/null || echo 0)
|
|
86
108
|
fi
|
|
87
109
|
|
|
88
|
-
# heal cycle
|
|
110
|
+
# heal cycle — count WAVE-8 banner occurrences in the trace, not the word
|
|
111
|
+
# "heal" anywhere (heal workers/healing text would inflate the counter and
|
|
112
|
+
# cause a premature halt)
|
|
89
113
|
HEAL_CYCLE=1
|
|
90
|
-
TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}
|
|
114
|
+
TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
|
|
91
115
|
if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
|
|
92
|
-
HEAL_COUNT=$(grep -ci 'Wave 8
|
|
116
|
+
HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null || echo 0)
|
|
93
117
|
HEAL_CYCLE=$((HEAL_COUNT + 1))
|
|
94
118
|
fi
|
|
95
119
|
|
|
96
|
-
# evidence paths
|
|
97
|
-
|
|
120
|
+
# evidence paths — per-mission results folder (quoted printf, no sed — mission
|
|
121
|
+
# name is allowlisted above, but avoid sed metacharacter semantics entirely)
|
|
122
|
+
EVIDENCE=""
|
|
123
|
+
for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
|
|
124
|
+
[ -f "$evf" ] || continue
|
|
125
|
+
EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
|
|
126
|
+
done
|
|
98
127
|
|
|
99
|
-
# skill version from package.json
|
|
128
|
+
# skill version from package.json (argv-passing — never interpolate paths into node -e)
|
|
100
129
|
SKILL_VERSION="1"
|
|
101
130
|
if [ -f package.json ]; then
|
|
102
131
|
PKG_JSON="$MUGIWARA_DIR/../package.json"
|
|
103
|
-
[ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(
|
|
132
|
+
[ -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
133
|
fi
|
|
105
134
|
|
|
106
135
|
# tokens from env var (harness exports estimated tokens consumed)
|
|
@@ -126,41 +155,59 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
|
|
|
126
155
|
STATUS="warn"
|
|
127
156
|
fi
|
|
128
157
|
|
|
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
158
|
mkdir -p "$MUGIWARA_DIR"
|
|
139
159
|
|
|
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
|
|
160
|
+
# --- lane-rise compare (F9): read previous state, flag escalation ---
|
|
161
|
+
LANE_PREV=""
|
|
162
|
+
LANE_ROSE=false
|
|
163
|
+
if [ -f "$STATE_FILE" ]; then
|
|
164
|
+
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)
|
|
165
|
+
if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
|
|
166
|
+
# lane order: direct < lean < standard < full < spike (spike resizes, not a rise)
|
|
167
|
+
case "$LANE_PREV:$LANE" in
|
|
168
|
+
direct:lean|direct:standard|direct:full|lean:standard|lean:full|standard:full) LANE_ROSE=true ;;
|
|
169
|
+
*) LANE_ROSE=false ;;
|
|
170
|
+
esac
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
165
173
|
|
|
174
|
+
node -e "
|
|
175
|
+
const data = {
|
|
176
|
+
mission: process.argv[1],
|
|
177
|
+
actor: process.argv[2],
|
|
178
|
+
branch: process.argv[3],
|
|
179
|
+
lane: process.argv[4],
|
|
180
|
+
lane_reason: process.argv[5],
|
|
181
|
+
lane_prev: process.argv[24] || null,
|
|
182
|
+
lane_rose: process.argv[25] === 'true',
|
|
183
|
+
wave: parseInt(process.argv[6], 10),
|
|
184
|
+
mode: process.argv[7],
|
|
185
|
+
base_sha: process.argv[8],
|
|
186
|
+
head_sha: process.argv[9],
|
|
187
|
+
files_touched: parseInt(process.argv[10], 10),
|
|
188
|
+
loc_delta: parseInt(process.argv[11], 10),
|
|
189
|
+
sensitive_paths: process.argv[12] ? process.argv[12].split(',').filter(Boolean) : [],
|
|
190
|
+
tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
|
|
191
|
+
blockers_open: parseInt(process.argv[15], 10),
|
|
192
|
+
heal_cycle: parseInt(process.argv[16], 10),
|
|
193
|
+
tokens_est: parseInt(process.argv[17], 10) || 0,
|
|
194
|
+
budget: parseInt(process.argv[18], 10) || 0,
|
|
195
|
+
budget_status: process.argv[19],
|
|
196
|
+
skill_version: process.argv[20],
|
|
197
|
+
evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
|
|
198
|
+
updated_at: process.argv[22]
|
|
199
|
+
};
|
|
200
|
+
require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
|
|
201
|
+
" \
|
|
202
|
+
"$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
|
|
203
|
+
"$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
|
|
204
|
+
"$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
|
|
205
|
+
"$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
|
|
206
|
+
"$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
|
|
207
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
208
|
+
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE"
|
|
209
|
+
|
|
210
|
+
if [ "$LANE_ROSE" = true ]; then
|
|
211
|
+
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
212
|
+
fi
|
|
166
213
|
echo "✓ savepoint written: $STATE_FILE (lane=$LANE, wave=$WAVE, files=$FILES_TOUCHED)"
|
|
@@ -27,6 +27,30 @@ 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') {
|
|
31
|
+
const lines = body.split(/\r?\n/);
|
|
32
|
+
const headingRe = /^## /;
|
|
33
|
+
const sections: { heading: string; start: number; end: number }[] = [];
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
if (headingRe.test(lines[i])) sections.push({ heading: lines[i].replace(/^## /, ''), start: i, end: 0 });
|
|
36
|
+
}
|
|
37
|
+
for (let si = 0; si < sections.length; si++) {
|
|
38
|
+
sections[si].end = si + 1 < sections.length ? sections[si + 1].start : lines.length;
|
|
39
|
+
// count non-empty, non-table, non-code-fence content lines
|
|
40
|
+
let inFence = false;
|
|
41
|
+
let contentLines = 0;
|
|
42
|
+
for (let li = sections[si].start; li < sections[si].end; li++) {
|
|
43
|
+
const l = lines[li].trim();
|
|
44
|
+
if (!l) continue;
|
|
45
|
+
if (l.startsWith('```')) { inFence = !inFence; continue; }
|
|
46
|
+
if (inFence) continue;
|
|
47
|
+
if (l.startsWith('|') || l.startsWith('- |')) continue;
|
|
48
|
+
contentLines++;
|
|
49
|
+
}
|
|
50
|
+
if (contentLines >= 20) errors.push(`skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥20) — move to references/`);
|
|
51
|
+
else if (contentLines >= 15) console.warn(`⚠ skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥15, consider moving to references/)`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
30
54
|
return data;
|
|
31
55
|
}
|
|
32
56
|
|
|
@@ -110,6 +134,59 @@ for (const dir of skillDirs) {
|
|
|
110
134
|
if (dir !== 'mugiwara-workflow' && !usedSkills.has(dir)) errors.push(`skill ${dir}: not referenced by any agent`);
|
|
111
135
|
}
|
|
112
136
|
|
|
137
|
+
// --- write-scope gate (F2): every agent declares a scope; only executors may write source ---
|
|
138
|
+
const SOURCE_SCOPED = new Set(['zoro-execution', 'brook-healing']);
|
|
139
|
+
for (const f of agentFiles) {
|
|
140
|
+
const name = f.replace(/\.md$/, '');
|
|
141
|
+
const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
|
|
142
|
+
const scope = parsed.data['write-scope'];
|
|
143
|
+
if (!scope) { errors.push(`agent ${f}: missing write-scope (artifacts | source)`); continue; }
|
|
144
|
+
if (scope !== 'artifacts' && scope !== 'source') errors.push(`agent ${f}: write-scope must be artifacts | source (got "${scope}")`);
|
|
145
|
+
if (scope === 'source' && !SOURCE_SCOPED.has(name)) errors.push(`agent ${f}: only zoro-execution and brook-healing may declare write-scope: source`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- hub-rule gate (F3): every non-Luffy agent carries both hub sections ---
|
|
149
|
+
for (const f of agentFiles) {
|
|
150
|
+
const name = f.replace(/\.md$/, '');
|
|
151
|
+
if (name === 'luffy-orchestrator') continue;
|
|
152
|
+
const text = readFileSync(join(agentDir, f), 'utf8');
|
|
153
|
+
if (!text.includes('## Before you start')) errors.push(`agent ${f}: missing "## Before you start" entry protocol`);
|
|
154
|
+
if (!text.includes('## Return to Luffy')) errors.push(`agent ${f}: missing "## Return to Luffy" hub rule`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- hub-skill gate (F3): every agent lists mugiwara-orchestration (the hub rule's home) ---
|
|
158
|
+
for (const f of agentFiles) {
|
|
159
|
+
const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
|
|
160
|
+
const skills = (parsed.data.skills ?? '').split(',').map(s => s.trim());
|
|
161
|
+
if (!skills.includes('mugiwara-orchestration')) errors.push(`agent ${f}: missing mugiwara-orchestration in skills (hub rule lives there)`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- handoff-target gate (F4): a body naming another crew member as a handoff
|
|
165
|
+
// target without Luffy in the same line fails CI ---
|
|
166
|
+
const HANDOFF_CREW = ['usopp', 'nami', 'zoro', 'chopper', 'sanji', 'franky', 'robin', 'jinbe', 'brook', 'skeptic', 'memory-keeper', 'resume-coordinator', 'eval-runner'];
|
|
167
|
+
// Verb class covers direct handoff language plus synonyms (route/send/pass/
|
|
168
|
+
// escalate/delegate/transfer) so the F4 gate does not leak through paraphrase.
|
|
169
|
+
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');
|
|
170
|
+
// Negation must DIRECTLY precede the handoff verb, not sit anywhere in the
|
|
171
|
+
// line ("do not hand off" later in the same line is a different clause).
|
|
172
|
+
const NEGATION_VERB = /\b(never|not|no|don't|do not)\s+(hand|dispatch|route|send|pass|escalate|delegate|transfer)\b/i;
|
|
173
|
+
for (const f of [...agentFiles, ...skillDirs.map(d => join(root, 'skills', d, 'SKILL.md'))]) {
|
|
174
|
+
if (!existsSync(f)) continue;
|
|
175
|
+
const text = readFileSync(f, 'utf8');
|
|
176
|
+
const lines = text.split(/\r?\n/);
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
const line = lines[i];
|
|
179
|
+
const m = handoffRe.exec(line);
|
|
180
|
+
if (!m) continue;
|
|
181
|
+
// clause boundary: only the clause containing the handoff verb counts
|
|
182
|
+
const clauseStart = line.lastIndexOf('.', m.index) + 1;
|
|
183
|
+
const clause = line.slice(clauseStart, m.index + m[0].length);
|
|
184
|
+
if (NEGATION_VERB.test(clause)) continue; // "never dispatch X" — constraint
|
|
185
|
+
if (/\bLuffy\b|to Luffy\b/.test(line)) continue; // hub named in same line ("main thread" is NOT the hub — Luffy is)
|
|
186
|
+
errors.push(`handoff leak ${f.replace(root + '/', '')}:${i + 1}: "${line.trim()}" — handoff target must be Luffy or name Luffy in the same line`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
113
190
|
// --- manifest-sync check: .claude-plugin/plugin.json must set-equal content/ ---
|
|
114
191
|
const manifestArg = process.argv.indexOf('--check-manifest');
|
|
115
192
|
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/installer.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type ContentItem = {
|
|
|
11
11
|
data: FrontmatterData;
|
|
12
12
|
body: string;
|
|
13
13
|
refs: { relPath: string; text: string }[];
|
|
14
|
+
internal?: boolean;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export type InstallOptions = {
|
|
@@ -35,6 +36,7 @@ export interface Target {
|
|
|
35
36
|
label: string;
|
|
36
37
|
native: boolean;
|
|
37
38
|
tier?: 1 | 2 | 3;
|
|
39
|
+
refPointerPrefix?: string;
|
|
38
40
|
paths(opts: { scope: Scope; projectDir: string; home: string }): { skillsDir: string; agentsDir: string };
|
|
39
41
|
transformSkill(data: FrontmatterData, body: string): TransformOut;
|
|
40
42
|
transformAgent(data: FrontmatterData, body: string): TransformOut;
|
|
@@ -48,7 +50,7 @@ export const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '
|
|
|
48
50
|
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')) as { version: string };
|
|
49
51
|
export const VERSION = pkg.version;
|
|
50
52
|
|
|
51
|
-
export function collectContent(): { skills: ContentItem[]; agents: ContentItem[] } {
|
|
53
|
+
export function collectContent(): { skills: ContentItem[]; agents: ContentItem[]; sharedRefs: { relPath: string; text: string }[] } {
|
|
52
54
|
const skillNames = readdirSync(join(CONTENT_DIR, 'skills'), { withFileTypes: true })
|
|
53
55
|
.filter(e => e.isDirectory()).map(e => e.name);
|
|
54
56
|
const skills = skillNames.map(name => {
|
|
@@ -59,9 +61,17 @@ export function collectContent(): { skills: ContentItem[]; agents: ContentItem[]
|
|
|
59
61
|
.filter(f => f.endsWith('.md'))
|
|
60
62
|
.map(f => {
|
|
61
63
|
const { data, body } = parseFrontmatter(readFileSync(join(CONTENT_DIR, 'agents', f), 'utf8'));
|
|
62
|
-
return { name: f.replace(/\.md$/, ''), data, body, refs: [] as { relPath: string; text: string }[] };
|
|
64
|
+
return { name: f.replace(/\.md$/, ''), data, body, refs: [] as { relPath: string; text: string }[], internal: data.internal === 'true' };
|
|
63
65
|
});
|
|
64
|
-
|
|
66
|
+
const sharedRefsDir = join(dirname(CONTENT_DIR), 'references');
|
|
67
|
+
const sharedRefs: { relPath: string; text: string }[] = [];
|
|
68
|
+
if (existsSync(sharedRefsDir)) {
|
|
69
|
+
for (const f of readdirSync(sharedRefsDir)) {
|
|
70
|
+
if (!f.endsWith('.md')) continue;
|
|
71
|
+
sharedRefs.push({ relPath: f, text: readFileSync(join(sharedRefsDir, f), 'utf8') });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { skills, agents, sharedRefs };
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function collectRefs(skillDir: string): { relPath: string; text: string }[] {
|
|
@@ -74,7 +84,7 @@ function collectRefs(skillDir: string): { relPath: string; text: string }[] {
|
|
|
74
84
|
export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
75
85
|
const { scope, projectDir, dryRun = false, force = false } = opts;
|
|
76
86
|
const home = opts.home ?? homedir();
|
|
77
|
-
const { skills, agents } = collectContent();
|
|
87
|
+
const { skills, agents, sharedRefs } = collectContent();
|
|
78
88
|
const dirs = target.paths({ scope, projectDir, home });
|
|
79
89
|
const backupRoot = join(scope === 'global' ? home : projectDir, '.mugiwara');
|
|
80
90
|
const result: InstallResult = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
@@ -100,10 +110,22 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
100
110
|
|
|
101
111
|
for (const s of skills) {
|
|
102
112
|
const out = target.transformSkill(s.data, s.body);
|
|
103
|
-
if (out)
|
|
113
|
+
if (out) {
|
|
114
|
+
let text = out.text;
|
|
115
|
+
if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== '') {
|
|
116
|
+
text = text.replace(/`_shared\/references\//g, '`' + target.refPointerPrefix + '_shared/references/');
|
|
117
|
+
}
|
|
118
|
+
writeOne(join(dirs.skillsDir, out.relPath), text);
|
|
119
|
+
}
|
|
104
120
|
if (target.transformSkillFull) {
|
|
105
121
|
const full = target.transformSkillFull(s.data, s.body);
|
|
106
|
-
if (full && target.refsDir)
|
|
122
|
+
if (full && target.refsDir) {
|
|
123
|
+
let text = full.text;
|
|
124
|
+
if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== '') {
|
|
125
|
+
text = text.replace(/`_shared\/references\//g, '`' + target.refPointerPrefix + '_shared/references/');
|
|
126
|
+
}
|
|
127
|
+
writeOne(join(target.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
|
|
128
|
+
}
|
|
107
129
|
}
|
|
108
130
|
if (s.refs.length && target.refsDir) {
|
|
109
131
|
const refsRoot = target.refsDir({ scope, projectDir, home }, s.name);
|
|
@@ -111,7 +133,7 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
for (const a of agents) {
|
|
114
|
-
const out = target.transformAgent(a.data, a.body);
|
|
136
|
+
const out = target.transformAgent({ ...a.data, ...(a.internal ? { 'internal-agent': 'true' } : {}) }, a.body);
|
|
115
137
|
if (out) writeOne(join(dirs.agentsDir, out.relPath), out.text);
|
|
116
138
|
if (target.transformAgentFull) {
|
|
117
139
|
const full = target.transformAgentFull(a.data, a.body);
|
|
@@ -119,6 +141,11 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
119
141
|
}
|
|
120
142
|
}
|
|
121
143
|
|
|
144
|
+
if (sharedRefs.length) {
|
|
145
|
+
const sharedRoot = join(dirs.skillsDir, '_shared', 'references');
|
|
146
|
+
for (const r of sharedRefs) writeOne(join(sharedRoot, r.relPath), r.text);
|
|
147
|
+
}
|
|
148
|
+
|
|
122
149
|
if (target.postInstall) {
|
|
123
150
|
const post = target.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
124
151
|
result.written.push(...post.written);
|
package/src/mission.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/mission.ts
|
|
2
2
|
// Mission-state helpers for the mugiwara CLI (installer + reset only).
|
|
3
|
-
import { existsSync, rmSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, rmSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
function activeActor(projectDir: string): string | null {
|
|
@@ -30,10 +30,12 @@ export function resetMission(projectDir: string, keepLogs: boolean, force?: bool
|
|
|
30
30
|
const p = join(root, dir);
|
|
31
31
|
if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push(dir); }
|
|
32
32
|
}
|
|
33
|
-
// mission state files
|
|
34
|
-
for (const f of
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
// mission state files — state.json + branch-specific state-*.json
|
|
34
|
+
for (const f of readdirSync(root)) {
|
|
35
|
+
if (/^state(-.+)?\.json$/.test(f)) {
|
|
36
|
+
const p = join(root, f);
|
|
37
|
+
if (existsSync(p)) { rmSync(p); removed.push(f); }
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
if (!keepLogs) {
|
|
39
41
|
const p = join(root, 'logs');
|
package/src/targets/claude.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/targets/claude.ts
|
|
2
|
-
import { existsSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, copyFileSync, chmodSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
|
|
@@ -7,11 +7,13 @@ import type { Target } from '../installer.ts';
|
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const HOOK_SRC = join(here, '..', '..', 'hooks', 'session-start.ts');
|
|
10
|
+
const COMMANDS_SRC = join(here, '..', '..', '.claude', 'commands');
|
|
10
11
|
|
|
11
12
|
export const target: Target = {
|
|
12
13
|
id: 'claude',
|
|
13
14
|
label: 'Claude Code',
|
|
14
15
|
native: true,
|
|
16
|
+
refPointerPrefix: '../',
|
|
15
17
|
paths({ scope, projectDir, home }) {
|
|
16
18
|
const root = scope === 'global' ? join(home, '.claude') : join(projectDir, '.claude');
|
|
17
19
|
return { skillsDir: join(root, 'skills'), agentsDir: join(root, 'agents') };
|
|
@@ -35,12 +37,27 @@ export const target: Target = {
|
|
|
35
37
|
// Wire the SessionStart hook (inline doctrine) into the installed .claude dir.
|
|
36
38
|
const root = scope === 'global' ? join(home, '.claude') : join(projectDir, '.claude');
|
|
37
39
|
const hookFile = join(root, 'hooks', 'session-start.ts');
|
|
40
|
+
const written: string[] = [];
|
|
41
|
+
const notes: string[] = [];
|
|
38
42
|
if (dryRun) return { written: [], notes: [] };
|
|
39
43
|
if (existsSync(HOOK_SRC) && !existsSync(hookFile)) {
|
|
40
44
|
mkdirSync(dirname(hookFile), { recursive: true });
|
|
41
45
|
copyFileSync(HOOK_SRC, hookFile);
|
|
42
|
-
|
|
46
|
+
chmodSync(hookFile, 0o755);
|
|
47
|
+
written.push(hookFile);
|
|
43
48
|
}
|
|
44
|
-
|
|
49
|
+
// Port the /mugiwara commands into the installed .claude dir.
|
|
50
|
+
if (existsSync(COMMANDS_SRC)) {
|
|
51
|
+
const dstDir = join(root, 'commands');
|
|
52
|
+
mkdirSync(dstDir, { recursive: true });
|
|
53
|
+
for (const f of readdirSync(COMMANDS_SRC)) {
|
|
54
|
+
if (!f.endsWith('.md')) continue;
|
|
55
|
+
const src = join(COMMANDS_SRC, f);
|
|
56
|
+
const dst = join(dstDir, f);
|
|
57
|
+
if (!existsSync(dst)) { copyFileSync(src, dst); written.push(dst); }
|
|
58
|
+
else notes.push(`existing command kept: ${dst}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { written, notes };
|
|
45
62
|
},
|
|
46
63
|
};
|
package/src/targets/generic.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function makeGeneric(opts: {
|
|
|
44
44
|
if (stubOnly) {
|
|
45
45
|
return {
|
|
46
46
|
relPath: `agent-${data.name}.md`,
|
|
47
|
-
text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills: ${data.skills ?? ''}. Read \`.mugiwara/refs/${data.name}.md\` when embodying this role.`,
|
|
47
|
+
text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills: ${data.skills ?? ''}. Read \`.mugiwara/refs/${data.name}.md\` when embodying this role.\n\nOnly zoro-execution and brook-healing may modify source code.\nReturn your output to luffy-orchestrator; do not choose the next step.`,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
return { relPath: `agent-${data.name}.md`, text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills used: ${data.skills ?? ''}\n\n${body}` };
|
package/src/targets/opencode.ts
CHANGED
|
@@ -20,26 +20,45 @@ const CREW: Record<string, CrewConfig> = {
|
|
|
20
20
|
'usopp-brainstorm': { color: '#f59e0b', temperature: 0.6, steps: 15 },
|
|
21
21
|
'nami-planner': { color: '#f97316', temperature: 0.2, steps: 15 },
|
|
22
22
|
'zoro-execution': { color: '#22c55e', temperature: 0.1, steps: 30 },
|
|
23
|
-
'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1,
|
|
24
|
-
'sanji-quality': { color: '#a855f7', temperature: 0.1,
|
|
25
|
-
'franky-gates': { color: '#06b6d4', temperature: 0.1,
|
|
26
|
-
'robin-reviewer': { color: '#8b5cf6', temperature: 0.2,
|
|
27
|
-
'jinbe-security': { color: '#6366f1', temperature: 0.2,
|
|
23
|
+
'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1, steps: 15 },
|
|
24
|
+
'sanji-quality': { color: '#a855f7', temperature: 0.1, steps: 10 },
|
|
25
|
+
'franky-gates': { color: '#06b6d4', temperature: 0.1, steps: 10 },
|
|
26
|
+
'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, steps: 15 },
|
|
27
|
+
'jinbe-security': { color: '#6366f1', temperature: 0.2, steps: 15 },
|
|
28
28
|
'brook-healing': { color: '#ec4899', temperature: 0.1, steps: 20 },
|
|
29
|
-
'skeptic-verifier': { color: '#64748b', temperature: 0.1,
|
|
29
|
+
'skeptic-verifier': { color: '#64748b', temperature: 0.1, steps: 12 },
|
|
30
30
|
'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 15 },
|
|
31
31
|
'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 10 },
|
|
32
32
|
'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// write-scope is the single source of truth (content/agents/*.md frontmatter).
|
|
36
|
+
// The path boundary (artifacts vs source) IS expressible in opencode:
|
|
37
|
+
// permission.edit accepts glob/pattern -> action, last match wins. Artifacts
|
|
38
|
+
// agents get deny-all-edit except .mugiwara/**; source agents (zoro, brook)
|
|
39
|
+
// get full edit allow. Derived at install time from the frontmatter field.
|
|
40
|
+
function permissionFromScope(scope: string | undefined): Record<string, string | Record<string, string>> | undefined {
|
|
41
|
+
if (scope === 'source') return { edit: 'allow' };
|
|
42
|
+
if (scope === 'artifacts') return { edit: { '*': 'deny', '.mugiwara/**': 'allow' } };
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function agentFrontmatter(name: string, description: string, writeScope?: string) {
|
|
36
47
|
const crew = CREW[name];
|
|
37
48
|
const lines = [`description: ${description}`, `mode: all`];
|
|
38
49
|
if (crew) {
|
|
39
50
|
lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
|
|
40
|
-
|
|
51
|
+
const perm = permissionFromScope(writeScope);
|
|
52
|
+
if (perm) {
|
|
41
53
|
lines.push('permission:');
|
|
42
|
-
for (const [k, v] of Object.entries(
|
|
54
|
+
for (const [k, v] of Object.entries(perm)) {
|
|
55
|
+
if (typeof v === 'string') {
|
|
56
|
+
lines.push(` ${k}: ${v}`);
|
|
57
|
+
} else {
|
|
58
|
+
lines.push(` ${k}:`);
|
|
59
|
+
for (const [pk, pv] of Object.entries(v)) lines.push(` "${pk}": ${pv}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
64
|
return lines.join('\n');
|
|
@@ -49,6 +68,7 @@ export const target: Target = {
|
|
|
49
68
|
id: 'opencode',
|
|
50
69
|
label: 'opencode',
|
|
51
70
|
native: true,
|
|
71
|
+
refPointerPrefix: '../',
|
|
52
72
|
paths({ scope, projectDir, home }) {
|
|
53
73
|
const root = scope === 'global' ? join(home, '.config', 'opencode') : join(projectDir, '.opencode');
|
|
54
74
|
return { skillsDir: join(root, 'skills'), agentsDir: join(root, 'agents') };
|
|
@@ -60,7 +80,8 @@ export const target: Target = {
|
|
|
60
80
|
};
|
|
61
81
|
},
|
|
62
82
|
transformAgent(data: FrontmatterData, body: string) {
|
|
63
|
-
const
|
|
83
|
+
const desc = data['internal-agent'] === 'true' ? `[INTERNAL] ${data.description}` : data.description;
|
|
84
|
+
const fm = agentFrontmatter(data.name, desc, data['write-scope']);
|
|
64
85
|
return { relPath: `${data.name}.md`, text: `---\n${fm}\n---\n${body}` };
|
|
65
86
|
},
|
|
66
87
|
refsDir({ scope, projectDir, home }, skillName: string) {
|