@ionivetech/mugiwara 0.6.3 → 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 +4 -0
- package/.opencode/plugins/mugiwara.mjs +40 -7
- package/AGENTS.md +11 -0
- package/README.md +21 -1
- package/content/agents/zoro-execution.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +1 -0
- package/content/skills/mugiwara-execution/SKILL.md +18 -17
- 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 +16 -16
- package/content/skills/mugiwara-orchestration/references/check-ins.md +15 -4
- package/content/skills/mugiwara-orchestration/references/closure.md +9 -0
- package/content/skills/mugiwara-orchestration/references/output-contract.md +77 -0
- package/content/skills/mugiwara-pr/SKILL.md +11 -18
- package/content/skills/mugiwara-pr/references/verdict-format.md +31 -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 -11
- package/dist/mugiwara.js +42 -22
- package/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/wave-banners.md +65 -0
- package/scripts/conformance.ts +215 -0
- package/scripts/evidence.sh +17 -4
- package/scripts/gate-selftest.ts +45 -0
- package/scripts/initiative.ts +51 -17
- package/scripts/lane.sh +1 -1
- package/scripts/lib/patterns.sh +8 -2
- package/scripts/savepoint.sh +12 -1
- package/scripts/validate-content.ts +22 -2
- package/src/targets/opencode.ts +35 -6
|
@@ -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'))) {
|
|
@@ -163,6 +177,37 @@ if (!existsSync(patternsFile)) {
|
|
|
163
177
|
}
|
|
164
178
|
}
|
|
165
179
|
|
|
180
|
+
// --- D3b mutation: strip the NEW pattern categories → lane-integrity red ---
|
|
181
|
+
console.log('\nD3b — new category strip mutation');
|
|
182
|
+
if (!existsSync(patternsFile)) {
|
|
183
|
+
console.log(' ⚠ patterns.sh not found, skipping');
|
|
184
|
+
} else {
|
|
185
|
+
const original = readFileSync(patternsFile, 'utf8');
|
|
186
|
+
try {
|
|
187
|
+
// derive the broken list from the LIVE source, stripping the exact tokens
|
|
188
|
+
// of the v0.6.4 D3 families — a hard-coded baseline rots the moment a
|
|
189
|
+
// category is added (G3). New family tokens must be added here too.
|
|
190
|
+
const D3B_FAMILY_TOKENS = new Set([
|
|
191
|
+
// raw regex tokens as they appear in live SENSITIVE_PATS (backslashes included)
|
|
192
|
+
'oauth2?/', 'credential', 'sessions?/', 'tokens?/', 'rbac', 'permissions?/', 'acls?/', 'iam/',
|
|
193
|
+
'\\.p12$', '\\.key$', '\\.pem$', 'migrate/', 'Dockerfile', 'docker-compose', '\\.github/workflows/',
|
|
194
|
+
'webhooks?/', 'secret/', 'secrets?\\.ya?ml$', '\\.tfvars$', '\\.env$', '\\.env\\.',
|
|
195
|
+
]);
|
|
196
|
+
const live = original.match(/SENSITIVE_PATS="([^"]+)"/)?.[1] ?? '';
|
|
197
|
+
const broken = live.split('|').filter(t => !D3B_FAMILY_TOKENS.has(t)).join('|');
|
|
198
|
+
const brokenLine = `SENSITIVE_PATS="${broken}"`;
|
|
199
|
+
if (broken === live || !live) {
|
|
200
|
+
console.log(' ⚠ D3b: no D3 family tokens found in live SENSITIVE_PATS — skipping');
|
|
201
|
+
} else {
|
|
202
|
+
writeFileSync(patternsFile, original.replace(/SENSITIVE_PATS="[^"]*"/, brokenLine));
|
|
203
|
+
assert('missing new categories → lane-integrity fails', false, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
writeFileSync(patternsFile, original);
|
|
207
|
+
assert('restored → lane-integrity passes', true, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
166
211
|
// --- D4 mutation: zero LOC_TOKENS → lane-integrity red ---
|
|
167
212
|
console.log('\nD4 — churn token mutation');
|
|
168
213
|
if (!existsSync(savepointFile)) {
|
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/lane.sh
CHANGED
|
@@ -23,7 +23,7 @@ CHANGED=$(git diff --name-only "$BASE"..HEAD 2>/dev/null || git diff --name-only
|
|
|
23
23
|
FILE_COUNT=0
|
|
24
24
|
[ -n "$CHANGED" ] && FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
|
|
25
25
|
|
|
26
|
-
SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null |
|
|
26
|
+
SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
|
27
27
|
HAS_SENSITIVE=0
|
|
28
28
|
[ -n "$SENSITIVE" ] && HAS_SENSITIVE=1
|
|
29
29
|
|
package/scripts/lib/patterns.sh
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
# Sensitive-path escalation patterns. A changed file matching any of these
|
|
6
6
|
# always escalates to Lane 3 (Full), regardless of file count. Plural forms
|
|
7
|
-
# included (payments/, migrations/)
|
|
7
|
+
# included (payments/, migrations/) and, since v0.6.4, the D3 categories:
|
|
8
|
+
# oauth, credential(s), session(s)/, token(s)/, rbac, permission(s), acl(s)/,
|
|
9
|
+
# iam/, cert keys (.pem/.key/.p12), migrate/, Dockerfile, docker-compose,
|
|
10
|
+
# .github/workflows/, webhooks?/, secret yaml, .tfvars, and .env variants
|
|
11
|
+
# (.env.local, .env.production — .env$ alone missed them, D3 follow-up).
|
|
12
|
+
# Dir-anchored (not bare): oauth2?/, permissions?/, tokens?/, sessions?/,
|
|
13
|
+
# acls?/ — bare forms over-matched docs (oauth-guide.md, permissionless.ts).
|
|
8
14
|
# Deliberately NOT matched: package.json (dependency churn is policy-as-code,
|
|
9
15
|
# not a sensitive lane trigger), authors/ (contains "auth" but never auth/).
|
|
10
|
-
SENSITIVE_PATS="auth/|payment/|payments/|billing/|crypto/|secrets/|\.env
|
|
16
|
+
SENSITIVE_PATS="auth/|oauth2?/|payment/|payments/|billing/|crypto/|secrets/|credential|sessions?/|tokens?/|rbac|permissions?/|acls?/|iam/|\.env$|\.env\.|config/.*key|\.p12$|\.key$|\.pem$|migration/|migrations/|migrate/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$|Dockerfile|docker-compose|\.github/workflows/|webhooks?/|secret/|secrets?\.ya?ml$|\.tfvars$"
|
|
11
17
|
|
|
12
18
|
# Product surface: paths that count toward file-based lane sizing. Changes
|
|
13
19
|
# outside this surface are docs/config/asset and never escalate to full on
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/targets/opencode.ts
|
|
2
|
-
import { existsSync, readdirSync, copyFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, copyFileSync, mkdirSync, readFileSync } 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,6 +7,7 @@ import type { Target } from '../installer.ts';
|
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const COMMANDS_SRC = join(here, '..', '..', '.opencode', 'commands');
|
|
10
|
+
const BANNER_TABLE = join(here, '..', '..', 'references', 'wave-banners.md');
|
|
10
11
|
|
|
11
12
|
type CrewConfig = {
|
|
12
13
|
color: string;
|
|
@@ -15,23 +16,47 @@ type CrewConfig = {
|
|
|
15
16
|
permission?: Record<string, string>;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
// Colors are fallbacks — the wave-banners table is the source of truth.
|
|
20
|
+
// Temperature/steps stay here (runtime tuning, not banner material).
|
|
18
21
|
const CREW: Record<string, CrewConfig> = {
|
|
19
22
|
'luffy-orchestrator': { color: '#ef4444', temperature: 0.2, steps: 15 },
|
|
20
|
-
'usopp-brainstorm': { color: '#
|
|
23
|
+
'usopp-brainstorm': { color: '#b45309', temperature: 0.6, steps: 15 },
|
|
21
24
|
'nami-planner': { color: '#f97316', temperature: 0.2, steps: 15 },
|
|
22
25
|
'zoro-execution': { color: '#22c55e', temperature: 0.1, steps: 30 },
|
|
23
|
-
'chopper-checkpoint': { color: '#
|
|
24
|
-
'sanji-quality': { color: '#
|
|
26
|
+
'chopper-checkpoint': { color: '#60a5fa', temperature: 0.1, steps: 15 },
|
|
27
|
+
'sanji-quality': { color: '#facc15', temperature: 0.1, steps: 10 },
|
|
25
28
|
'franky-gates': { color: '#06b6d4', temperature: 0.1, steps: 10 },
|
|
26
29
|
'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, steps: 15 },
|
|
27
30
|
'jinbe-security': { color: '#6366f1', temperature: 0.2, steps: 15 },
|
|
28
|
-
'brook-healing': { color: '#
|
|
31
|
+
'brook-healing': { color: '#2dd4bf', temperature: 0.1, steps: 20 },
|
|
29
32
|
'skeptic-verifier': { color: '#64748b', temperature: 0.1, steps: 12 },
|
|
30
33
|
'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 15 },
|
|
31
34
|
'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 10 },
|
|
32
35
|
'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
|
|
36
|
+
'onboarding-guide': { color: '#0ea5e9', temperature: 0.3, steps: 15 },
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
// Crew colors from the wave-banners table (single source of truth). Returns
|
|
40
|
+
// {} on any failure — callers fall back to the CREW map. Same table shape as
|
|
41
|
+
// the opencode plugin parses: | agent-id | role | hex | ansi-256 | emoji |
|
|
42
|
+
function readBannerColors(): Record<string, string> {
|
|
43
|
+
try {
|
|
44
|
+
if (!existsSync(BANNER_TABLE)) return {};
|
|
45
|
+
const text = readFileSync(BANNER_TABLE, 'utf8');
|
|
46
|
+
// null-prototype + CRLF-tolerant: agent ids are trusted repo content, but
|
|
47
|
+
// a future `__proto__` id must never write the object's prototype
|
|
48
|
+
const colors: Record<string, string> = Object.create(null);
|
|
49
|
+
for (const m of text.matchAll(/^\| ([\w-]+) \| [^|]+ \| (#[0-9a-f]{6}) \| (\d+) \| (\S+) \|\r?$/gm)) {
|
|
50
|
+
colors[m[1]] = m[2];
|
|
51
|
+
}
|
|
52
|
+
return colors;
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const BANNER_COLORS = readBannerColors();
|
|
59
|
+
|
|
35
60
|
// write-scope is the single source of truth (content/agents/*.md frontmatter),
|
|
36
61
|
// but it is a RULE for user-facing crew agents: they run inline in the main
|
|
37
62
|
// thread, so a runtime permission bound to the active-agent identity would
|
|
@@ -50,8 +75,12 @@ function permissionFromScope(scope: string | undefined): Record<string, string |
|
|
|
50
75
|
function agentFrontmatter(name: string, description: string, internal: boolean, writeScope?: string) {
|
|
51
76
|
const crew = CREW[name];
|
|
52
77
|
const lines = [`description: ${description}`, `mode: all`];
|
|
78
|
+
// color: wave-banners table wins; CREW fallback for agents the table lacks.
|
|
79
|
+
// temperature/steps only exist for CREW members (runtime tuning).
|
|
80
|
+
const color = BANNER_COLORS[name] ?? crew?.color;
|
|
81
|
+
if (color) lines.push(`color: '${color}'`);
|
|
53
82
|
if (crew) {
|
|
54
|
-
lines.push(`
|
|
83
|
+
lines.push(`temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
|
|
55
84
|
const perm = internal ? permissionFromScope(writeScope) : undefined;
|
|
56
85
|
if (perm) {
|
|
57
86
|
lines.push('permission:');
|