@ionivetech/mugiwara 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +49 -2
- package/.cursor-plugin/plugin.json +49 -2
- package/.kimi-plugin/plugin.json +49 -2
- package/.opencode/mugiwara-helpers.mjs +3 -0
- package/.opencode/plugins/mugiwara.mjs +1 -0
- package/AGENTS.md +11 -0
- package/README.md +20 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +1 -0
- package/content/skills/mugiwara-execution/SKILL.md +10 -10
- package/content/skills/mugiwara-gates/SKILL.md +1 -0
- package/content/skills/mugiwara-lessons/SKILL.md +1 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +14 -17
- package/content/skills/mugiwara-orchestration/references/check-ins.md +4 -4
- package/content/skills/mugiwara-orchestration/references/output-contract.md +77 -0
- package/content/skills/mugiwara-quality/SKILL.md +1 -0
- package/content/skills/mugiwara-resume/SKILL.md +1 -0
- package/content/skills/mugiwara-review/SKILL.md +1 -0
- package/content/skills/mugiwara-workflow/SKILL.md +14 -15
- package/dist/mugiwara.js +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/wave-banners.md +9 -19
- package/scripts/conformance.ts +215 -0
- package/scripts/evidence.sh +17 -4
- package/scripts/gate-selftest.ts +14 -0
- package/scripts/initiative.ts +51 -17
- package/scripts/savepoint.sh +12 -1
- package/scripts/validate-content.ts +22 -2
- package/src/targets/opencode.ts +1 -0
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ionivetech/mugiwara",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
|
|
5
5
|
"homepage": "https://github.com/ionivetech/mugiwara#readme",
|
|
6
6
|
"repository": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"retrieval-eval": "bun scripts/retrieval-eval.ts",
|
|
60
60
|
"verify-install": "bun scripts/verify-install.ts",
|
|
61
61
|
"sync-version": "bun scripts/sync-version.ts",
|
|
62
|
-
"gate": "bun run typecheck && bun run test && bun run build && bun scripts/validate-content.ts --check-manifest --check-docs --check-doc-integrity && bun scripts/lane-base.ts && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts && bun scripts/verify-install.ts",
|
|
62
|
+
"gate": "bun run typecheck && bun run test && bun run build && bun scripts/validate-content.ts --check-manifest --check-docs --check-doc-integrity && bun scripts/lane-base.ts && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts && bun scripts/verify-install.ts && bun scripts/conformance.ts",
|
|
63
63
|
"verify-pack": "npm pack --dry-run 2>&1 | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{if(!s.includes('ionivetech-mugiwara')){console.error('npm pack failed');process.exit(1)};console.log('npm package clean')})\"",
|
|
64
64
|
"prepack": "bun run build && bun run sync-version"
|
|
65
65
|
},
|
package/plugin.json
CHANGED
|
@@ -5,16 +5,19 @@ plugin and the opencode-target generator derive agent UI colors from the table
|
|
|
5
5
|
below — change a color HERE, never in code. This file is machine-parsed:
|
|
6
6
|
keep the table format exact (one row per agent, pipes, no extra columns).
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Banner format (one form)
|
|
9
9
|
|
|
10
10
|
Every wave opens with the banner line in the owning agent's color, and closes
|
|
11
|
-
with a handoff line
|
|
11
|
+
with a handoff line:
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
|
|
14
|
+
===== ⚔️ WAVE 3 — ZORO (EXECUTION) =====
|
|
15
15
|
→ Wave 4 — Chopper (Checkpoint)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
- Terminal: wrap the whole line in ANSI truecolor `\x1b[38;2;R;G;Bm` ... `\x1b[0m`.
|
|
19
|
+
- Markdown UI: emit the plain equals line, no ANSI (UIs strip or garble escapes).
|
|
20
|
+
- The crew emoji leads the line, before the `WAVE N` text.
|
|
18
21
|
- RGB values come from the `hex` column below; R/G/B are the hex channels in
|
|
19
22
|
decimal (truecolor `38;2;R;G;B`). If the terminal lacks truecolor, use the
|
|
20
23
|
`ansi-256` index: `\x1b[38;5;Nm`.
|
|
@@ -22,20 +25,8 @@ with a handoff line. Wrap the whole line in the ANSI truecolor of the agent:
|
|
|
22
25
|
- The literal `WAVE N —` text must stay exact: `scripts/savepoint.sh` counts
|
|
23
26
|
heal cycles by grepping `wave 8` (case-insensitive) in the trace. A banner
|
|
24
27
|
that drops the literal silently resets the heal loop.
|
|
25
|
-
- Never convey the wave by color alone — the crew name
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## UI format (markdown UIs)
|
|
29
|
-
|
|
30
|
-
Rich UIs (Claude Code UI, VSCode, Codex) strip or garble ANSI escapes. Emit
|
|
31
|
-
the emoji-heading form instead — no ANSI, no equals line:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
## ⚔️ WAVE 3 — ZORO (EXECUTION)
|
|
35
|
-
→ Wave 4 — Chopper (Checkpoint)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Same literal `WAVE N —` rule applies.
|
|
28
|
+
- Never convey the wave by color alone — the crew name always accompanies
|
|
29
|
+
the color.
|
|
39
30
|
|
|
40
31
|
## Crew colors
|
|
41
32
|
|
|
@@ -65,8 +56,7 @@ wizard agents — their banners appear only when a wave or worker names them.
|
|
|
65
56
|
1. Banner before EVERY wave; handoff after it. No wave starts without its
|
|
66
57
|
banner (orchestration red flag).
|
|
67
58
|
2. The color comes from this table only — never invent a hex mid-mission.
|
|
68
|
-
3.
|
|
69
|
-
markdown-rendering UIs. When unsure, the UI variant is safe everywhere.
|
|
59
|
+
3. One form everywhere: equals line `===== <emoji> WAVE N — <CREW> (ROLE) =====` — five `=` per side, the crew emoji from the table leading the line, ANSI-wrapped in terminals, plain in markdown-rendering UIs. When unsure, the plain form is safe everywhere.
|
|
70
60
|
4. Only the crew table's colors and the two SGR forms above (truecolor,
|
|
71
61
|
256-index) may appear in a banner — never other escape families (OSC,
|
|
72
62
|
title, cursor, other SGR codes). The banner is a fixed template, not a
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/conformance.ts — C1: cross-platform conformance suite.
|
|
3
|
+
// Every installable target: materialize the standard-feature fixture repo,
|
|
4
|
+
// install the target, run the four core scripts, and compare a normalized
|
|
5
|
+
// snapshot (state fields, report sections, evidence header, gitignore block,
|
|
6
|
+
// file count) against test/golden/<target>.json. Exit 1 on any difference
|
|
7
|
+
// with a diff; --update-golden regenerates. The snapshots are normalized:
|
|
8
|
+
// timestamps, hashes, and random filenames are excluded so a golden is stable
|
|
9
|
+
// across runs.
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { installTo, ensureProjectGitignore } from '../src/installer.ts';
|
|
16
|
+
import { targets, TARGET_IDS } from '../src/targets/index.ts';
|
|
17
|
+
|
|
18
|
+
const root = join(import.meta.dirname, '..');
|
|
19
|
+
const goldenDir = join(root, 'test', 'golden');
|
|
20
|
+
const UPDATE = process.argv.includes('--update-golden');
|
|
21
|
+
|
|
22
|
+
// tier mapping per docs/reference/harness-matrix.md (target defs carry tier only
|
|
23
|
+
// on some; the matrix is the source of truth for the rest)
|
|
24
|
+
const TIER_OF: Record<string, number> = {
|
|
25
|
+
claude: 1, opencode: 1,
|
|
26
|
+
gemini: 2, codex: 2, copilot: 2,
|
|
27
|
+
windsurf: 3, cline: 3, kilo: 3, antigravity: 3,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TARGETS = TARGET_IDS.map(id => ({ id, tier: TIER_OF[id] ?? 2 }));
|
|
31
|
+
|
|
32
|
+
// marketplace-plugin platforms: installed from the repo itself via the host's
|
|
33
|
+
// plugin system (no rules-dir install). Conformance = the manifest an
|
|
34
|
+
// operator's marketplace will consume: it parses, its version matches the
|
|
35
|
+
// package, its pointers resolve, and its metadata set-equals content/.
|
|
36
|
+
const MARKETPLACE = [
|
|
37
|
+
{ id: 'cursor', manifest: '.cursor-plugin/plugin.json' },
|
|
38
|
+
{ id: 'kimi', manifest: '.kimi-plugin/plugin.json' },
|
|
39
|
+
{ id: 'pi', manifest: null, piField: true },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function marketplaceSnapshot(id: string, manifestRel: string | null, piField: boolean): Record<string, unknown> {
|
|
43
|
+
const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
44
|
+
const skillDirs = readdirSync(join(root, 'content', 'skills'), { withFileTypes: true })
|
|
45
|
+
.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
46
|
+
const agentFiles = readdirSync(join(root, 'content', 'agents'))
|
|
47
|
+
.filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, '')).sort();
|
|
48
|
+
|
|
49
|
+
const base = {
|
|
50
|
+
target: id,
|
|
51
|
+
kind: 'marketplace-plugin',
|
|
52
|
+
version_matches_package: false,
|
|
53
|
+
skills_pointer_resolves: false,
|
|
54
|
+
skills_count: 0,
|
|
55
|
+
agents_metadata_set_equal: false,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (piField) {
|
|
59
|
+
const pi = packageJson.pi;
|
|
60
|
+
const list = pi?.skills;
|
|
61
|
+
const pointers = Array.isArray(list) ? list : [];
|
|
62
|
+
base.skills_pointer_resolves = pointers.every(p => existsSync(join(root, p)));
|
|
63
|
+
base.skills_count = existsSync(join(root, 'content', 'skills')) ? skillDirs.length : 0;
|
|
64
|
+
base.version_matches_package = true; // package.json is the package itself
|
|
65
|
+
base.agents_metadata_set_equal = true; // pi loads skills only; agents ship in the same tree
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const m = JSON.parse(readFileSync(join(root, manifestRel!), 'utf8'));
|
|
70
|
+
const skillsPtr = m.skills;
|
|
71
|
+
const skillsDir = typeof skillsPtr === 'string' ? join(root, skillsPtr) : null;
|
|
72
|
+
const actual = skillsDir && existsSync(skillsDir)
|
|
73
|
+
? readdirSync(skillsDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort()
|
|
74
|
+
: [];
|
|
75
|
+
const mSkills = Array.isArray(m.metadata?.skills) ? [...m.metadata.skills].sort() : [];
|
|
76
|
+
const mAgents = Array.isArray(m.metadata?.agents) ? [...m.metadata.agents].sort() : [];
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
version_matches_package: m.version === packageJson.version,
|
|
81
|
+
skills_pointer_resolves: skillsDir !== null && existsSync(skillsDir) && actual.length === skillDirs.length,
|
|
82
|
+
skills_count: actual.length,
|
|
83
|
+
agents_metadata_set_equal: JSON.stringify(mAgents) === JSON.stringify(agentFiles),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MISSION = 'conform';
|
|
88
|
+
|
|
89
|
+
function sh(cmd: string, cwd: string) {
|
|
90
|
+
execSync(cmd, { cwd, stdio: 'pipe', env: { ...process.env, MUGIWARA_DIR: join(cwd, '.mugiwara') } });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function snapshot(targetId: string): Record<string, unknown> {
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), `mugi-conform-${targetId}-`));
|
|
95
|
+
try {
|
|
96
|
+
// 1. materialize the standard-feature fixture repo (trunk + feat branch)
|
|
97
|
+
sh(`bun scripts/setup-fixtures.ts standard-feature "${dir}"`, root);
|
|
98
|
+
|
|
99
|
+
// 2. install the target into the project
|
|
100
|
+
const target = targets[targetId];
|
|
101
|
+
if (!target) throw new Error(`unknown target ${targetId}`);
|
|
102
|
+
installTo(target, { scope: 'project', projectDir: dir, force: true, dryRun: false });
|
|
103
|
+
|
|
104
|
+
// 3. run the four core scripts + the gitignore write the CLI does post-install
|
|
105
|
+
sh(`bash "${root}/scripts/lane.sh" main --json`, dir);
|
|
106
|
+
sh(`bash "${root}/scripts/savepoint.sh" ${MISSION} "" 1 auto`, dir);
|
|
107
|
+
sh(`bash "${root}/scripts/evidence.sh" ${MISSION} lint -- printf 'ok\\n'`, dir);
|
|
108
|
+
sh(`bash "${root}/scripts/mission-report.sh" ${MISSION}`, dir);
|
|
109
|
+
ensureProjectGitignore(dir, { dryRun: false });
|
|
110
|
+
|
|
111
|
+
// 4. normalize + collect
|
|
112
|
+
const state = JSON.parse(readFileSync(join(dir, '.mugiwara', 'state', MISSION, 'state.json'), 'utf8'));
|
|
113
|
+
const reportFile = readdirSync(join(dir, '.mugiwara', 'reports')).find(f => f.endsWith(`-${MISSION}.md`));
|
|
114
|
+
const report = reportFile ? readFileSync(join(dir, '.mugiwara', 'reports', reportFile), 'utf8') : '';
|
|
115
|
+
const evFile = readdirSync(join(dir, '.mugiwara', 'results', MISSION)).find(f => f.includes('lint-'));
|
|
116
|
+
const ev = evFile ? readFileSync(join(dir, '.mugiwara', 'results', MISSION, evFile), 'utf8') : '';
|
|
117
|
+
const gitignore = existsSync(join(dir, '.gitignore')) ? readFileSync(join(dir, '.gitignore'), 'utf8') : '';
|
|
118
|
+
|
|
119
|
+
const countFiles = (p: string): number => {
|
|
120
|
+
if (!existsSync(p)) return 0;
|
|
121
|
+
return readdirSync(p, { recursive: true, withFileTypes: true }).filter(e => e.isFile()).length;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
target: targetId,
|
|
126
|
+
tier: TIER_OF[targetId] ?? 2,
|
|
127
|
+
state: {
|
|
128
|
+
mission: state.mission,
|
|
129
|
+
member: state.member,
|
|
130
|
+
lane: state.lane,
|
|
131
|
+
lane_reason: state.lane_reason,
|
|
132
|
+
files_touched: state.files_touched,
|
|
133
|
+
loc_delta: state.loc_delta,
|
|
134
|
+
loc_ins: state.loc_ins,
|
|
135
|
+
loc_del: state.loc_del,
|
|
136
|
+
loc_churn: state.loc_churn,
|
|
137
|
+
mode: state.mode,
|
|
138
|
+
verbosity: state.verbosity,
|
|
139
|
+
tasks_done: state.tasks?.done,
|
|
140
|
+
tasks_total: state.tasks?.total,
|
|
141
|
+
blockers_open: state.blockers_open,
|
|
142
|
+
heal_cycle: state.heal_cycle,
|
|
143
|
+
tokens_source: state.tokens_source,
|
|
144
|
+
budget_status: state.budget_status,
|
|
145
|
+
sensitive_paths: state.sensitive_paths,
|
|
146
|
+
},
|
|
147
|
+
report_sections: [...report.matchAll(/^#{2,3} .*$/gm)].map(m => m[0]),
|
|
148
|
+
evidence: {
|
|
149
|
+
header: ev.split('\n').filter(l => l.startsWith('# ') && !l.startsWith('# At:') && !/^# (Exit|Verdict):/.test(l)),
|
|
150
|
+
trailer: ev.split('\n').filter(l => /^# (Exit|Verdict):/.test(l)),
|
|
151
|
+
},
|
|
152
|
+
gitignore_block: gitignore.split('\n').filter(l => l.includes('mugiwara') || l.includes('# ---')),
|
|
153
|
+
file_count: {
|
|
154
|
+
skills: countFiles(target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir),
|
|
155
|
+
agents: countFiles(target.paths({ scope: 'project', projectDir: dir, home: '' }).agentsDir),
|
|
156
|
+
state: countFiles(join(dir, '.mugiwara', 'state')),
|
|
157
|
+
evidence: countFiles(join(dir, '.mugiwara', 'results', MISSION)),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(dir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function diff(a: unknown, b: unknown, path = ''): string[] {
|
|
166
|
+
const out: string[] = [];
|
|
167
|
+
if (JSON.stringify(a) === JSON.stringify(b)) return out;
|
|
168
|
+
if (typeof a !== typeof b || a === null || b === null || typeof a !== 'object') {
|
|
169
|
+
out.push(`${path}: ${JSON.stringify(a)} ≠ ${JSON.stringify(b)}`);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
for (const k of new Set([...Object.keys(a as object), ...Object.keys(b as object)])) {
|
|
173
|
+
out.push(...diff((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k], `${path}.${k}`));
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let failed = false;
|
|
179
|
+
mkdirSync(goldenDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
const ALL = [
|
|
182
|
+
...TARGETS.map(t => ({ id: t.id, label: `tier ${t.tier}`, snap: () => snapshot(t.id) })),
|
|
183
|
+
...MARKETPLACE.map(m => ({ id: m.id, label: 'marketplace', snap: () => marketplaceSnapshot(m.id, m.manifest, m.piField === true) })),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
for (const t of ALL) {
|
|
187
|
+
const snap = t.snap();
|
|
188
|
+
const goldenFile = join(goldenDir, `${t.id}.json`);
|
|
189
|
+
if (UPDATE) {
|
|
190
|
+
writeFileSync(goldenFile, JSON.stringify(snap, null, 2) + '\n');
|
|
191
|
+
console.log(`✓ golden updated: test/golden/${t.id}.json`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!existsSync(goldenFile)) {
|
|
195
|
+
console.log(`✗ ${t.id}: golden missing — run with --update-golden first`);
|
|
196
|
+
failed = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const golden = JSON.parse(readFileSync(goldenFile, 'utf8'));
|
|
200
|
+
const diffs = diff(snap, golden);
|
|
201
|
+
if (diffs.length > 0) {
|
|
202
|
+
failed = true;
|
|
203
|
+
console.log(`✗ ${t.id} (${t.label}) differs:`);
|
|
204
|
+
for (const d of diffs) console.log(` ${d}`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`✓ ${t.id} (${t.label}) conforms`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (UPDATE) {
|
|
211
|
+
console.log('goldens regenerated — review the diff before committing');
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
if (failed) process.exit(1);
|
|
215
|
+
console.log(`✓ ${ALL.length} platforms pass conformance`);
|
package/scripts/evidence.sh
CHANGED
|
@@ -40,20 +40,33 @@ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
|
40
40
|
HASH=$(echo "${LABEL}-${TIMESTAMP}-$$-${RANDOM}" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || openssl sha256) | cut -c1-12 2>/dev/null || echo "${TIMESTAMP}")
|
|
41
41
|
EVIDENCE_FILE="$RESULTS_DIR/${LABEL}-${HASH}.log"
|
|
42
42
|
|
|
43
|
+
# Neutralize forged verdict/exit header lines in captured output. The agent
|
|
44
|
+
# trusts the real trailer only (# Exit:/# Verdict: appended after the block);
|
|
45
|
+
# attacker output may try to impersonate it under any spelling — leading
|
|
46
|
+
# whitespace, ANSI prefix, no space, CRLF. Idempotent: already-neutralized
|
|
47
|
+
# #-Verdict: / #-Exit: lines pass through unchanged.
|
|
48
|
+
SANITIZE='s/^[[:space:]]*(\x1b\[[0-9;]*m)*#[[:space:]]*(Verdict|Exit):/#-\2:/'
|
|
49
|
+
|
|
50
|
+
# command line echoed in the header — collapse embedded newlines so an arg
|
|
51
|
+
# cannot forge a header line inside "# Command:" (the header block is written
|
|
52
|
+
# verbatim, outside the sanitizer).
|
|
53
|
+
COMMAND_LINE=$(printf '%s ' "$@" 2>/dev/null | tr '\n\r' ' ' | sed 's/ *$//')
|
|
54
|
+
|
|
43
55
|
{
|
|
44
56
|
echo "# Evidence: $LABEL"
|
|
45
57
|
echo "# At: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
46
|
-
echo "# Command: ${
|
|
58
|
+
echo "# Command: ${COMMAND_LINE:-<stdin pipeline>}"
|
|
47
59
|
echo "# ---"
|
|
48
60
|
echo
|
|
49
61
|
|
|
50
62
|
if [ $# -gt 0 ]; then
|
|
51
|
-
"$@" 2>&1
|
|
63
|
+
"$@" 2>&1 | sed -E "$SANITIZE"
|
|
64
|
+
EXIT_CODE=${PIPESTATUS[0]}
|
|
52
65
|
else
|
|
53
|
-
|
|
66
|
+
sed -E "$SANITIZE"
|
|
67
|
+
EXIT_CODE=0
|
|
54
68
|
fi
|
|
55
69
|
} > "$EVIDENCE_FILE"
|
|
56
|
-
EXIT_CODE=$?
|
|
57
70
|
|
|
58
71
|
# trailer: exit code + verdict (D6). The verdict is PASS/FAIL derived from the
|
|
59
72
|
# exit code — the check's own outcome, not the harness's opinion.
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -68,6 +68,20 @@ if (!existsSync(join(root, 'scripts', 'validate-content.ts'))) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// --- G5: conditional-assertion guard — prove expect-in-conditional goes red ---
|
|
72
|
+
console.log('\nG5 — conditional-assertion guard');
|
|
73
|
+
{
|
|
74
|
+
const testFile = join(root, 'test', 'targets.test.ts');
|
|
75
|
+
const original = readFileSync(testFile, 'utf8');
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(testFile, `${original}\nif (x) { expect(1).toBe(1); }\n`);
|
|
78
|
+
assert('expect() in non-invariant conditional → exit 1', false, () => run('G5', 'bun scripts/validate-content.ts'));
|
|
79
|
+
} finally {
|
|
80
|
+
writeFileSync(testFile, original);
|
|
81
|
+
assert('restored → exit 0', true, () => run('G5', 'bun scripts/validate-content.ts'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
// --- G3: savepoint fixtures — prove test fails when a field is broken ---
|
|
72
86
|
console.log('\nG3 — savepoint fixtures');
|
|
73
87
|
if (!existsSync(join(root, 'test', 'savepoint.test.ts'))) {
|
package/scripts/initiative.ts
CHANGED
|
@@ -55,40 +55,47 @@ function assertPlanContent(content: string, planFile: string): void {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function parseSubMissions(content: string): SubMission[] {
|
|
58
|
+
function parseSubMissions(content: string): { subs: SubMission[]; hasSection: boolean } {
|
|
59
59
|
const lines = content.split('\n');
|
|
60
60
|
let inTable = false;
|
|
61
61
|
let inSubSection = false;
|
|
62
|
+
let hasSection = false;
|
|
62
63
|
const subs: SubMission[] = [];
|
|
63
64
|
|
|
64
65
|
for (let i = 0; i < lines.length; i++) {
|
|
65
66
|
const line = lines[i];
|
|
66
|
-
if (line.startsWith('##
|
|
67
|
+
if (line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
67
68
|
inSubSection = true;
|
|
69
|
+
hasSection = true;
|
|
68
70
|
continue;
|
|
69
71
|
}
|
|
70
|
-
if (inSubSection && line.startsWith('## ') && !line.startsWith('##
|
|
72
|
+
if (inSubSection && line.startsWith('## ') && !line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
71
73
|
break;
|
|
72
74
|
}
|
|
73
75
|
if (!inSubSection) continue;
|
|
74
76
|
|
|
75
77
|
const trimmed = line.trim();
|
|
76
|
-
|
|
78
|
+
const lower = trimmed.toLowerCase();
|
|
79
|
+
if (lower.startsWith('| id ') && lower.includes('| name ')) {
|
|
77
80
|
inTable = true;
|
|
78
81
|
continue;
|
|
79
82
|
}
|
|
80
83
|
if (inTable && trimmed.startsWith('|---')) continue;
|
|
81
84
|
if (inTable && trimmed.startsWith('|')) {
|
|
82
|
-
|
|
85
|
+
// Keep interior empty cells (e.g. empty "depends on"): filter(Boolean)
|
|
86
|
+
// would shift columns and drop the touched-files tail.
|
|
87
|
+
const cols = trimmed.split('|').map(c => c.trim());
|
|
88
|
+
while (cols.length && cols[0] === '') cols.shift();
|
|
89
|
+
while (cols.length && cols[cols.length - 1] === '') cols.pop();
|
|
83
90
|
if (cols.length >= 6) {
|
|
84
91
|
subs.push({
|
|
85
92
|
id: cols[0],
|
|
86
93
|
name: cols[1],
|
|
87
94
|
assignee: cols[2],
|
|
88
95
|
branch: cols[3],
|
|
89
|
-
status: cols[4],
|
|
96
|
+
status: cols[4].replace('[X]', '[x]'),
|
|
90
97
|
dependsOn: cols[5],
|
|
91
|
-
touchedFiles: cols.slice(6).join(' ').split(
|
|
98
|
+
touchedFiles: cols.slice(6).join(' ').split(/[,\s]+/).map(s => s.trim()).filter(Boolean),
|
|
92
99
|
});
|
|
93
100
|
}
|
|
94
101
|
}
|
|
@@ -97,13 +104,23 @@ function parseSubMissions(content: string): SubMission[] {
|
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
return subs;
|
|
107
|
+
return { subs, hasSection };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertRowsParsed(planFile: string, content: string): { subs: SubMission[]; hasSection: boolean } {
|
|
111
|
+
const { subs, hasSection } = parseSubMissions(content);
|
|
112
|
+
if (hasSection && subs.length === 0) {
|
|
113
|
+
console.error('initiative: "## Sub-missions" section found but no rows parsed.');
|
|
114
|
+
console.error(' Expected header: | ID | Name | Assignee | Branch | Status | Depends On | Touched Files |');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
return { subs, hasSection };
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
function cmdStatus(planFile: string): void {
|
|
104
121
|
const content = readFileSync(planFile, 'utf8');
|
|
105
122
|
assertPlanContent(content, planFile);
|
|
106
|
-
const subs =
|
|
123
|
+
const { subs } = assertRowsParsed(planFile, content);
|
|
107
124
|
|
|
108
125
|
if (subs.length === 0) {
|
|
109
126
|
console.log('No sub-missions found — solo mission.');
|
|
@@ -117,14 +134,24 @@ function cmdStatus(planFile: string): void {
|
|
|
117
134
|
const filled = Math.round((doneCount / total) * barLen);
|
|
118
135
|
const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
|
|
119
136
|
|
|
137
|
+
// dependency blocking: a sub whose dependency is not done is blocked
|
|
138
|
+
const statusById = new Map(subs.map(s => [s.id, s.status]));
|
|
139
|
+
const blockedBy = new Map<string, string>();
|
|
140
|
+
for (const s of subs) {
|
|
141
|
+
if (s.dependsOn && statusById.has(s.dependsOn) && statusById.get(s.dependsOn) !== '[x]') {
|
|
142
|
+
blockedBy.set(s.id, s.dependsOn);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
console.log(`\n${doneCount}/${total} done [${bar}] ${pct}%\n`);
|
|
121
147
|
console.log(`${'ID'.padEnd(8)} ${'Name'.padEnd(20)} ${'Assignee'.padEnd(12)} ${'Branch'.padEnd(28)} ${'Status'.padEnd(8)} ${'Depends'.padEnd(10)} ${'Files'}`);
|
|
122
148
|
console.log('─'.repeat(120));
|
|
123
149
|
|
|
124
150
|
for (const s of subs) {
|
|
125
151
|
const icon = { '[ ]': '◻', '[~]': '◉', '[x]': '✓', '[!]': '✗' }[s.status] || '?';
|
|
152
|
+
const blocked = blockedBy.has(s.id) ? ` ⛔ blocked-by ${blockedBy.get(s.id)}` : '';
|
|
126
153
|
console.log(
|
|
127
|
-
`${s.id.padEnd(8)} ${s.name.padEnd(20)} ${s.assignee.padEnd(12)} ${s.branch.padEnd(28)} ${icon} ${s.status.padEnd(3)} ${s.dependsOn.padEnd(10)} ${s.touchedFiles.join(', ')}`
|
|
154
|
+
`${s.id.padEnd(8)} ${s.name.padEnd(20)} ${s.assignee.padEnd(12)} ${s.branch.padEnd(28)} ${icon} ${s.status.padEnd(3)} ${s.dependsOn.padEnd(10)} ${s.touchedFiles.join(', ')}${blocked}`
|
|
128
155
|
);
|
|
129
156
|
}
|
|
130
157
|
console.log();
|
|
@@ -133,7 +160,7 @@ function cmdStatus(planFile: string): void {
|
|
|
133
160
|
function cmdConflictCheck(planFile: string): void {
|
|
134
161
|
const content = readFileSync(planFile, 'utf8');
|
|
135
162
|
assertPlanContent(content, planFile);
|
|
136
|
-
const subs =
|
|
163
|
+
const { subs } = assertRowsParsed(planFile, content);
|
|
137
164
|
|
|
138
165
|
if (subs.length === 0) {
|
|
139
166
|
console.log('No sub-missions — no conflicts possible.');
|
|
@@ -168,6 +195,7 @@ function cmdConflictCheck(planFile: string): void {
|
|
|
168
195
|
console.log(`\n${conflicts.length} file conflict(s) detected:\n`);
|
|
169
196
|
console.log(conflicts.join('\n'));
|
|
170
197
|
console.log();
|
|
198
|
+
process.exit(1);
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
|
|
@@ -187,28 +215,34 @@ function cmdSetStatus(planFile: string, id: string, status: string): void {
|
|
|
187
215
|
|
|
188
216
|
for (let i = 0; i < lines.length; i++) {
|
|
189
217
|
const line = lines[i];
|
|
190
|
-
if (line.startsWith('##
|
|
218
|
+
if (line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
191
219
|
inSubSection = true;
|
|
192
220
|
continue;
|
|
193
221
|
}
|
|
194
|
-
if (inSubSection && line.startsWith('## ') && !line.startsWith('##
|
|
222
|
+
if (inSubSection && line.startsWith('## ') && !line.trim().toLowerCase().startsWith('## sub-missions')) {
|
|
195
223
|
break;
|
|
196
224
|
}
|
|
197
225
|
if (!inSubSection) continue;
|
|
198
226
|
|
|
199
227
|
const trimmed = line.trim();
|
|
200
|
-
if (trimmed.startsWith('|
|
|
228
|
+
if (trimmed.toLowerCase().startsWith('| id ')) { inTable = true; continue; }
|
|
201
229
|
if (!inTable || !trimmed.startsWith('|')) continue;
|
|
202
230
|
if (trimmed.startsWith('|---')) continue;
|
|
203
231
|
|
|
204
|
-
|
|
232
|
+
// same cell handling as parseSubMissions: interior empty cells preserved
|
|
233
|
+
const cols = trimmed.split('|').map(c => c.trim());
|
|
234
|
+
while (cols.length && cols[0] === '') cols.shift();
|
|
235
|
+
while (cols.length && cols[cols.length - 1] === '') cols.pop();
|
|
205
236
|
if (cols[0] === id) {
|
|
206
|
-
|
|
237
|
+
let oldMarker = cols[4];
|
|
238
|
+
if (oldMarker === '[X]') oldMarker = '[x]';
|
|
207
239
|
if (!Object.values(STATUS_MARKERS).includes(oldMarker)) {
|
|
208
240
|
console.error(`Row ${id}: status column "${oldMarker}" is not a recognized marker`);
|
|
209
241
|
process.exit(1);
|
|
210
242
|
}
|
|
211
|
-
|
|
243
|
+
// rewrite only the status cell — a marker string elsewhere in the row
|
|
244
|
+
// (name, touched files) must not be clobbered by a row-wide replace
|
|
245
|
+
lines[i] = `| ${cols.slice(0, 4).join(' | ')} | ${newMarker} | ${cols.slice(5).join(' | ')} |`;
|
|
212
246
|
found = true;
|
|
213
247
|
break;
|
|
214
248
|
}
|
package/scripts/savepoint.sh
CHANGED
|
@@ -42,6 +42,16 @@ MISSION="${1:-${STATE_MISSION:-}}"
|
|
|
42
42
|
MEMBER="${2:-${STATE_MEMBER:-}}"
|
|
43
43
|
WAVE="${3:-${STATE_WAVE:-1}}"
|
|
44
44
|
MODE="${4:-${STATE_MODE:-guided}}"
|
|
45
|
+
# verbosity from config (project .mugiwara/config), default normal; env override
|
|
46
|
+
VERBOSITY="${STATE_VERBOSITY:-normal}"
|
|
47
|
+
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
48
|
+
CFG_VERBOSITY=$(grep -E '^verbosity=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
49
|
+
[ -n "$CFG_VERBOSITY" ] && VERBOSITY="$CFG_VERBOSITY"
|
|
50
|
+
fi
|
|
51
|
+
case "$VERBOSITY" in
|
|
52
|
+
normal|full) ;;
|
|
53
|
+
*) VERBOSITY="normal" ;;
|
|
54
|
+
esac
|
|
45
55
|
ACTOR="${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${GIT_ID:-${USER:-}}}}"
|
|
46
56
|
BRANCH="$(git branch --show-current 2>/dev/null || echo 'unknown')"
|
|
47
57
|
|
|
@@ -296,6 +306,7 @@ const data = {
|
|
|
296
306
|
lane_rose: process.argv[25] === 'true',
|
|
297
307
|
wave: parseInt(process.argv[6], 10),
|
|
298
308
|
mode: process.argv[7],
|
|
309
|
+
verbosity: process.argv[32] || 'normal',
|
|
299
310
|
base_sha: process.argv[8],
|
|
300
311
|
head_sha: process.argv[9],
|
|
301
312
|
files_touched: parseInt(process.argv[10], 10),
|
|
@@ -324,7 +335,7 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
|
|
|
324
335
|
"$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
|
|
325
336
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
326
337
|
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
|
|
327
|
-
"$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER"
|
|
338
|
+
"$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY"
|
|
328
339
|
|
|
329
340
|
if [ "$LANE_ROSE" = true ]; then
|
|
330
341
|
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
@@ -207,10 +207,15 @@ for (const f of [...agentFiles, ...skillDirs.map(d => join(root, 'skills', d, 'S
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
// --- manifest-sync check:
|
|
210
|
+
// --- manifest-sync check: every marketplace plugin.json set-equal content/ ---
|
|
211
211
|
const manifestArg = process.argv.indexOf('--check-manifest');
|
|
212
212
|
if (manifestArg !== -1) {
|
|
213
|
-
const manifests = [
|
|
213
|
+
const manifests = [
|
|
214
|
+
'.claude-plugin/plugin.json',
|
|
215
|
+
'.codex-plugin/plugin.json',
|
|
216
|
+
'.cursor-plugin/plugin.json',
|
|
217
|
+
'.kimi-plugin/plugin.json',
|
|
218
|
+
];
|
|
214
219
|
let manifestErrors = 0;
|
|
215
220
|
|
|
216
221
|
for (const mp of manifests) {
|
|
@@ -380,5 +385,20 @@ if (integrityArg !== -1) {
|
|
|
380
385
|
}
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
// Conditional-assertion guard: an expect() reachable only inside a truthiness
|
|
389
|
+
// check silently passes when the value is absent. This class produced 9 defects.
|
|
390
|
+
// Allowed: checks keyed on a declared invariant (tier, fixture keys).
|
|
391
|
+
const ALLOWED_COND = /if \((?:t\.tier === 3|keys\.length|label === 'exact'|fx\.expect\.(?:lane|sensitive_paths_(?:min|max)))/;
|
|
392
|
+
const repoRoot = join(import.meta.dirname, '..');
|
|
393
|
+
for (const f of readdirSync(join(repoRoot, 'test')).filter(x => x.endsWith('.test.ts'))) {
|
|
394
|
+
const src = readFileSync(join(repoRoot, 'test', f), 'utf8');
|
|
395
|
+
// matches both braced and brace-less conditionals whose body reaches an expect()
|
|
396
|
+
for (const m of src.matchAll(/if \([^)]+\)(?:\s*\{[^}]{0,400}?)?expect\(/gs)) {
|
|
397
|
+
if (!ALLOWED_COND.test(m[0])) {
|
|
398
|
+
errors.push(`test/${f}: expect() inside a conditional — assert presence explicitly instead:\n ${m[0].slice(0, 80)}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
383
403
|
if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
|
|
384
404
|
console.log(`✓ content valid: ${skillDirs.length} skills, ${agentFiles.length} agents`);
|
package/src/targets/opencode.ts
CHANGED
|
@@ -33,6 +33,7 @@ const CREW: Record<string, CrewConfig> = {
|
|
|
33
33
|
'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 15 },
|
|
34
34
|
'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 10 },
|
|
35
35
|
'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
|
|
36
|
+
'onboarding-guide': { color: '#0ea5e9', temperature: 0.3, steps: 15 },
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
// Crew colors from the wave-banners table (single source of truth). Returns
|