@ionivetech/mugiwara 0.5.4 → 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 +3 -3
- package/.claude-plugin/plugin.json +4 -3
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +2 -2
- 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 +2 -2
- package/GEMINI.md +1 -1
- package/README.md +240 -217
- 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 +19 -5
- 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 +206 -87
- package/gemini-extension.json +1 -1
- package/hooks/session-start.ts +1 -1
- package/package.json +12 -5
- package/plugin.json +2 -2
- package/references/multi-actor.md +1 -1
- package/scripts/evidence.sh +13 -6
- package/scripts/gate-selftest.ts +266 -0
- package/scripts/initiative.ts +248 -0
- package/scripts/lane.sh +15 -0
- package/scripts/mission-report.sh +141 -76
- package/scripts/onboard.ts +292 -0
- package/scripts/release-notes.ts +42 -13
- package/scripts/retrieval-eval.ts +1 -2
- package/scripts/run-evals.ts +1 -1
- package/scripts/savepoint.sh +97 -50
- package/scripts/validate-content.ts +84 -7
- package/scripts/verify-install.ts +72 -0
- package/src/args.ts +1 -1
- package/src/cli.ts +49 -22
- package/src/frontmatter.ts +3 -3
- package/src/installer.ts +44 -15
- 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 +53 -11
- package/docs/adoption-guide.md +0 -72
- package/docs/agent-anatomy.md +0 -72
- package/docs/agents.md +0 -53
- package/docs/audit-trail.md +0 -65
- package/docs/claude-setup.md +0 -43
- package/docs/codex-setup.md +0 -26
- package/docs/comparison.md +0 -58
- package/docs/compliance-matrix.md +0 -81
- package/docs/config.md +0 -55
- package/docs/copilot-setup.md +0 -29
- package/docs/cost.md +0 -45
- package/docs/cursor-setup.md +0 -25
- package/docs/developer-onboarding.md +0 -89
- package/docs/enforcement.md +0 -35
- package/docs/execution-model.md +0 -92
- package/docs/gemini-setup.md +0 -27
- package/docs/getting-started.md +0 -158
- package/docs/git-strategy.md +0 -62
- package/docs/harness-matrix.md +0 -41
- package/docs/index.md +0 -56
- package/docs/install-antigravity.md +0 -45
- package/docs/install-claude.md +0 -75
- package/docs/install-cli.md +0 -103
- 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-kimi.md +0 -45
- package/docs/install-opencode.md +0 -129
- package/docs/install-pi.md +0 -46
- package/docs/install.md +0 -53
- package/docs/lanes.md +0 -82
- package/docs/modes.md +0 -74
- package/docs/opencode-setup.md +0 -54
- package/docs/pr-summary.md +0 -54
- package/docs/rule-based-setup.md +0 -31
- package/docs/skill-anatomy.md +0 -71
- package/docs/skills.md +0 -55
- package/docs/troubleshooting.md +0 -91
- package/docs/windsurf-setup.md +0 -18
- package/docs/workflow.md +0 -89
- 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=$(
|
|
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)"
|
|
@@ -14,7 +14,7 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
|
|
|
14
14
|
const { data, body } = parsed;
|
|
15
15
|
if (data.name !== wantName) errors.push(`${kind} ${file}: name "${data.name}" != "${wantName}"`);
|
|
16
16
|
const d = data.description ?? '';
|
|
17
|
-
if (kind === 'skill' && (d.length < 20 || d.length >
|
|
17
|
+
if (kind === 'skill' && (d.length < 20 || d.length > 220)) errors.push(`skill ${file}: description must be 20-220 chars (got ${d.length})`);
|
|
18
18
|
if (kind === 'agent' && d.length < 20) errors.push(`agent ${file}: description too short`);
|
|
19
19
|
if (kind === 'skill' && body.replace(/\r?\n$/, '').split(/\r?\n/).length > 120) errors.push(`skill ${file}: body exceeds 120 lines`);
|
|
20
20
|
if (kind === 'skill' && !body.includes('## Skip when')) errors.push(`skill ${file}: missing required "## Skip when" block (≤4 lines, numeric threshold)`);
|
|
@@ -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) {
|
|
@@ -188,20 +265,20 @@ if (totalDescChars > INDEX_BUDGET) {
|
|
|
188
265
|
const docsArg = process.argv.indexOf('--check-docs');
|
|
189
266
|
if (docsArg !== -1) {
|
|
190
267
|
const docsDir = join(import.meta.dirname, '..', 'docs');
|
|
191
|
-
|
|
192
|
-
|
|
268
|
+
const skillsDoc = join(docsDir, 'concepts', 'skills.md');
|
|
269
|
+
const agentsDoc = join(docsDir, 'concepts', 'agents.md');
|
|
193
270
|
let docErrors = 0;
|
|
194
271
|
|
|
195
272
|
if (existsSync(skillsDoc)) {
|
|
196
273
|
const content = readFileSync(skillsDoc, 'utf8');
|
|
197
274
|
for (const dir of skillDirs) {
|
|
198
275
|
if (!content.includes(dir)) {
|
|
199
|
-
errors.push(`docs/skills.md: missing skill "${dir}"`);
|
|
276
|
+
errors.push(`docs/concepts/skills.md: missing skill "${dir}"`);
|
|
200
277
|
docErrors++;
|
|
201
278
|
}
|
|
202
279
|
}
|
|
203
280
|
} else {
|
|
204
|
-
errors.push('docs/skills.md: file not found');
|
|
281
|
+
errors.push('docs/concepts/skills.md: file not found');
|
|
205
282
|
docErrors++;
|
|
206
283
|
}
|
|
207
284
|
|
|
@@ -210,12 +287,12 @@ if (docsArg !== -1) {
|
|
|
210
287
|
for (const f of agentFiles) {
|
|
211
288
|
const name = f.replace(/\.md$/, '');
|
|
212
289
|
if (!content.includes(name)) {
|
|
213
|
-
errors.push(`docs/agents.md: missing agent "${name}"`);
|
|
290
|
+
errors.push(`docs/concepts/agents.md: missing agent "${name}"`);
|
|
214
291
|
docErrors++;
|
|
215
292
|
}
|
|
216
293
|
}
|
|
217
294
|
} else {
|
|
218
|
-
errors.push('docs/agents.md: file not found');
|
|
295
|
+
errors.push('docs/concepts/agents.md: file not found');
|
|
219
296
|
docErrors++;
|
|
220
297
|
}
|
|
221
298
|
|
|
@@ -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/args.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type Args = {
|
|
|
9
9
|
const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
|
|
10
10
|
const BOOL_FLAGS: Record<string, string> = {
|
|
11
11
|
'--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
|
|
12
|
-
'--dry-run': 'dryRun', '--keep-logs': 'keepLogs',
|
|
12
|
+
'--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check',
|
|
13
13
|
'--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
|
|
14
14
|
};
|
|
15
15
|
|
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/cli.ts
|
|
3
|
-
import { existsSync, readFileSync,
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
import {
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
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';
|
|
@@ -43,12 +43,15 @@ function resetCmd(flags: Args['flags']): void {
|
|
|
43
43
|
|
|
44
44
|
async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
|
|
45
45
|
const interactive = !flag(flags.yes);
|
|
46
|
+
if (interactive && !process.stdin.isTTY) {
|
|
47
|
+
throw new Error('Not a terminal. Run with --yes and --global or --project <dir> --target <ids|all>');
|
|
48
|
+
}
|
|
46
49
|
const rl = interactive ? createRl() : null;
|
|
47
50
|
try {
|
|
48
51
|
let scope: Scope | null = flag(flags.global) ? 'global' : str(flags.project) ? 'project' : null;
|
|
49
52
|
if (!scope) {
|
|
50
|
-
if (!interactive)
|
|
51
|
-
scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
|
|
53
|
+
if (!interactive) { scope = 'project'; }
|
|
54
|
+
else scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
|
|
52
55
|
}
|
|
53
56
|
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
54
57
|
if (scope === 'project' && !existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
|
|
@@ -56,9 +59,11 @@ async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; pro
|
|
|
56
59
|
let targetIds = str(flags.target)?.split(',').map(s => s.trim()) ?? null;
|
|
57
60
|
if (targetIds && targetIds.includes('all')) targetIds = [...TARGET_IDS];
|
|
58
61
|
if (!targetIds) {
|
|
59
|
-
if (!interactive)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
if (!interactive) { targetIds = [...TARGET_IDS]; }
|
|
63
|
+
else {
|
|
64
|
+
const idx = await multiChoose(rl!, 'Target AI agents?', ['all targets', ...TARGET_IDS]);
|
|
65
|
+
targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map(i => TARGET_IDS[i - 1]);
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
for (const id of targetIds) {
|
|
64
69
|
if (!targets[id]) throw new Error(`Unknown target: ${id} (valid: ${TARGET_IDS.join(', ')}, all)`);
|
|
@@ -110,7 +115,17 @@ async function uninstall(flags: Args['flags']): Promise<void> {
|
|
|
110
115
|
const home = homedir();
|
|
111
116
|
const file = manifestPath({ scope, projectDir, home });
|
|
112
117
|
const manifest = readManifest(file);
|
|
113
|
-
if (!manifest) {
|
|
118
|
+
if (!manifest) {
|
|
119
|
+
if (scope === 'project') {
|
|
120
|
+
const globalFile = manifestPath({ scope: 'global', projectDir, home });
|
|
121
|
+
if (existsSync(globalFile)) {
|
|
122
|
+
console.log('No project manifest found, but a global install exists. Try:\n mugiwara uninstall --global');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log('Nothing installed (no manifest found).');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
114
129
|
console.log(`Will remove ${manifest.files.length} files (targets: ${manifest.targets.join(', ')}).`);
|
|
115
130
|
if (!flag(flags.yes)) {
|
|
116
131
|
const rl = createRl();
|
|
@@ -119,7 +134,20 @@ async function uninstall(flags: Args['flags']): Promise<void> {
|
|
|
119
134
|
if (!ok) { console.log('Aborted.'); return; }
|
|
120
135
|
}
|
|
121
136
|
const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
|
|
122
|
-
if (!flag(flags.dryRun))
|
|
137
|
+
if (!flag(flags.dryRun)) {
|
|
138
|
+
rmSync(file);
|
|
139
|
+
const mugiDir = join(scope === 'global' ? home : projectDir, '.mugiwara');
|
|
140
|
+
if (existsSync(mugiDir) && readdirSync(mugiDir).length === 0) {
|
|
141
|
+
rmSync(mugiDir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
if (manifest.targets.includes('opencode')) {
|
|
144
|
+
const opencodeCache = join(home, '.cache', 'opencode', 'packages', '@ionivetech');
|
|
145
|
+
if (existsSync(opencodeCache)) {
|
|
146
|
+
rmSync(opencodeCache, { recursive: true, force: true });
|
|
147
|
+
console.log(' cleared opencode npm cache (stale plugin versions)');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
123
151
|
console.log(`OK removed ${removed.length} files`);
|
|
124
152
|
}
|
|
125
153
|
|
|
@@ -134,7 +162,12 @@ function list(flags: Args['flags']): void {
|
|
|
134
162
|
const m = readManifest(file);
|
|
135
163
|
if (!m) continue;
|
|
136
164
|
found = true;
|
|
137
|
-
|
|
165
|
+
if (flag(flags.check)) {
|
|
166
|
+
const missing = m.files.filter(f => !existsSync(f));
|
|
167
|
+
console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
168
|
+
} else {
|
|
169
|
+
console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} installed=${m.installedAt}`);
|
|
170
|
+
}
|
|
138
171
|
}
|
|
139
172
|
if (!found) console.log('No mugiwara installation found.');
|
|
140
173
|
}
|
|
@@ -143,10 +176,11 @@ function help(): void {
|
|
|
143
176
|
console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
|
|
144
177
|
|
|
145
178
|
Usage:
|
|
146
|
-
mugiwara [install] install the crew (
|
|
179
|
+
mugiwara [install] install the crew (wizard; with --yes uses project + all)
|
|
147
180
|
mugiwara update replace existing files (backs up differences first)
|
|
148
181
|
mugiwara uninstall remove installed files via manifest
|
|
149
182
|
mugiwara list show installations
|
|
183
|
+
mugiwara list --check health check: show installations + missing files
|
|
150
184
|
mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
|
|
151
185
|
mugiwara --help this help
|
|
152
186
|
mugiwara --version print version
|
|
@@ -155,21 +189,14 @@ Flags:
|
|
|
155
189
|
--global user-wide install
|
|
156
190
|
--project <dir> project install (default: cwd)
|
|
157
191
|
--target <ids|all> comma-separated: ${TARGET_IDS.join(', ')}
|
|
158
|
-
--yes, -y non-interactive (
|
|
192
|
+
--yes, -y non-interactive (defaults: project, all targets)
|
|
159
193
|
--force overwrite differing files (with backup)
|
|
160
194
|
--dry-run print actions without writing
|
|
195
|
+
--check with list: report missing files (health check)
|
|
161
196
|
--keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
|
|
162
197
|
}
|
|
163
198
|
|
|
164
|
-
|
|
165
|
-
if (entry !== undefined) {
|
|
166
|
-
try {
|
|
167
|
-
entry = realpathSync(entry);
|
|
168
|
-
} catch {
|
|
169
|
-
// ponytail: realpath can throw on a missing/odd path — fall back to the raw resolve
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const isMain = entry !== undefined && import.meta.url === pathToFileURL(entry).href;
|
|
199
|
+
const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
173
200
|
if (isMain) {
|
|
174
201
|
run(process.argv.slice(2)).catch(err => {
|
|
175
202
|
console.error(`mugiwara: ${err.message}`);
|
package/src/frontmatter.ts
CHANGED
|
@@ -7,9 +7,9 @@ export function parseFrontmatter(text: string): { data: FrontmatterData; body: s
|
|
|
7
7
|
const data: FrontmatterData = {};
|
|
8
8
|
for (const line of m[1].split(/\r?\n/)) {
|
|
9
9
|
if (!line.trim()) continue;
|
|
10
|
-
const i = line.indexOf(':');
|
|
11
|
-
if (i === -1) throw new Error(`Bad frontmatter line: ${line}`);
|
|
12
|
-
data[line.slice(0, i).trim()] = line.slice(i +
|
|
10
|
+
const i = line.indexOf(': ');
|
|
11
|
+
if (i === -1) throw new Error(`Bad frontmatter line (missing ': ' separator): ${line}`);
|
|
12
|
+
data[line.slice(0, i).trim()] = line.slice(i + 2).trim();
|
|
13
13
|
}
|
|
14
14
|
return { data, body: text.slice(m[0].length) };
|
|
15
15
|
}
|