@ericrisco/rsc 1.1.5 → 1.2.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/README.md +72 -14
- package/manifest.json +888 -231
- package/package.json +3 -2
- package/schema/ecc-originality.json +16 -0
- package/schema/ecc-originality.u64 +0 -0
- package/scripts/build-manifest.js +43 -3
- package/scripts/doctor.js +47 -1
- package/scripts/install-apply.js +148 -31
- package/scripts/lib/capabilities.js +21 -0
- package/scripts/lib/domains.js +3 -3
- package/scripts/lib/manifest-file.js +4 -1
- package/scripts/originality-check.js +137 -0
- package/scripts/rsc.js +122 -14
- package/skills/design/SKILL.md +3 -3
- package/skills/design/references/design-systems.md +1 -1
- package/skills/design/references/inspiration-sources.md +2 -2
- package/skills/design/references/motion-and-interaction.md +2 -2
- package/skills/design-dna/evals/cases.yaml +1 -1
- package/skills/design-loop/SKILL.md +3 -3
- package/skills/design-loop/evals/cases.yaml +3 -3
- package/skills/expo/SKILL.md +1 -1
- package/skills/motion-craft/SKILL.md +125 -0
- package/skills/motion-craft/evals/cases.yaml +38 -0
- package/skills/motion-craft/scripts/verify.sh +75 -0
- package/skills/react/SKILL.md +2 -2
- package/skills/react-native/SKILL.md +1 -1
- package/skills/swift-ios/SKILL.md +2 -2
- package/skills/ui-engineering/SKILL.md +114 -0
- package/skills/ui-engineering/evals/cases.yaml +38 -0
- package/skills/variant-explorer/SKILL.md +93 -0
- package/skills/variant-explorer/evals/cases.yaml +38 -0
- package/targets/agent-catalog.js +220 -0
- package/targets/agent-sources.json +48 -0
- package/targets/agents.js +61 -13
- package/targets/commands.js +212 -0
- package/targets/memory.js +276 -0
- package/targets/opencode-memory-plugin.js +65 -0
- package/targets/session-memory-adapter.mjs +117 -0
- package/targets/session-memory-core.mjs +477 -0
- package/targets/session-memory.mjs +27 -0
- package/skills/animate/SKILL.md +0 -851
- package/skills/animate/evals/cases.yaml +0 -36
- package/skills/animate/references/recipes.md +0 -324
- package/skills/animate-expo/SKILL.md +0 -281
- package/skills/animate-expo/evals/cases.yaml +0 -36
- package/skills/animate-expo/references/recipes.md +0 -385
- package/skills/animation-vocabulary/SKILL.md +0 -189
- package/skills/animation-vocabulary/evals/cases.yaml +0 -31
- package/skills/apple-design/SKILL.md +0 -301
- package/skills/apple-design/evals/cases.yaml +0 -33
- package/skills/ask-sonner/SKILL.md +0 -104
- package/skills/ask-sonner/evals/cases.yaml +0 -33
- package/skills/ask-sonner/references/api.md +0 -64
- package/skills/design-eng/SKILL.md +0 -88
- package/skills/design-eng/evals/cases.yaml +0 -36
- package/skills/find-animation-opportunities/SKILL.md +0 -150
- package/skills/find-animation-opportunities/evals/cases.yaml +0 -31
- package/skills/improve-animations/SKILL.md +0 -126
- package/skills/improve-animations/evals/cases.yaml +0 -31
- package/skills/improve-animations/references/audit.md +0 -115
- package/skills/improve-animations/references/plan-template.md +0 -73
- package/skills/pick-ui-library/SKILL.md +0 -101
- package/skills/pick-ui-library/evals/cases.yaml +0 -33
- package/skills/pick-ui-library/references/component-galleries.md +0 -59
- package/skills/prototype/SKILL.md +0 -113
- package/skills/prototype/evals/cases.yaml +0 -31
- package/skills/prototype/references/picker.md +0 -197
- package/skills/review-animations/SKILL.md +0 -188
- package/skills/review-animations/evals/cases.yaml +0 -34
- package/skills/review-animations/references/standards.md +0 -187
- package/skills/review-animations/scripts/verify.sh +0 -164
- package/skills/write-swift/SKILL.md +0 -413
- package/skills/write-swift/evals/cases.yaml +0 -36
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"agent": "go-reviewer",
|
|
4
|
+
"researchedAt": "2026-09-02",
|
|
5
|
+
"sources": [
|
|
6
|
+
{
|
|
7
|
+
"url": "https://go.dev/wiki/CodeReviewComments",
|
|
8
|
+
"coverage": ["error identity and wrapping", "goroutine lifetime", "interface values", "receiver and API review"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"url": "https://go.dev/doc/comment",
|
|
12
|
+
"coverage": ["public API comments", "package and symbol contracts"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"url": "https://go.dev/doc/contribute",
|
|
16
|
+
"coverage": ["review evidence and change scope"]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"agent": "cpp-reviewer",
|
|
22
|
+
"researchedAt": "2026-09-02",
|
|
23
|
+
"sources": [
|
|
24
|
+
{
|
|
25
|
+
"url": "https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html",
|
|
26
|
+
"coverage": ["RAII and ownership", "lifetime and bounds safety", "concurrency", "exception guarantees"]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"agent": "python-reviewer",
|
|
32
|
+
"researchedAt": "2026-09-02",
|
|
33
|
+
"sources": [
|
|
34
|
+
{
|
|
35
|
+
"url": "https://docs.python.org/3.14/library/asyncio-dev.html",
|
|
36
|
+
"coverage": ["un-awaited coroutines", "blocking async code", "thread boundaries"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"url": "https://docs.python.org/3.14/library/asyncio-task.html",
|
|
40
|
+
"coverage": ["TaskGroup failure propagation", "cancellation", "task lifetime"]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"url": "https://docs.python.org/3.14/reference/datamodel.html",
|
|
44
|
+
"coverage": ["descriptors and protocols", "object and iterator lifetime"]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
package/targets/agents.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// written by `init` at onboarding and read here so re-syncs honor it.
|
|
11
11
|
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
12
12
|
import { join, dirname } from 'node:path';
|
|
13
|
+
import {
|
|
14
|
+
stackAgents, stackAgentNames, stackAgentByName,
|
|
15
|
+
resolveStackAgentNames, validateAgentCatalog,
|
|
16
|
+
} from './agent-catalog.js';
|
|
13
17
|
|
|
14
18
|
// Concrete model per provider per tier. June 2026 defaults — EDIT to your account's
|
|
15
19
|
// models; the TIER is the contract, the id is yours to change. `light` is deliberately
|
|
@@ -42,13 +46,15 @@ export function targetHasAgents(target) { return Boolean(AGENT_TARGETS[target]);
|
|
|
42
46
|
// (decided in clarify 2026-08-18: a lens passed as a parameter reintroduces the
|
|
43
47
|
// does-anyone-remember dependency this whole spec exists to remove) — but P5 says length is a cost,
|
|
44
48
|
// so the contract they share is composed, not written three times.
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
**You get exactly four inputs, and nothing else:**
|
|
49
|
+
export const REFUTER_FOUR_INPUTS = `**You get exactly four inputs, and nothing else:**
|
|
48
50
|
1. The task contract — the original request **plus every scope change a human explicitly approved since**. Without the approved changes, a legitimate scope revision reads as a spec gap and you will report a confident false positive.
|
|
49
51
|
2. The approved spec.
|
|
50
52
|
3. The exact source state (commit SHA, or a tree hash when git is absent). A verdict attaches to the state you saw, not to the project.
|
|
51
|
-
4. The entry point — the one command that reruns the checks
|
|
53
|
+
4. The entry point — the one command that reruns the checks.`;
|
|
54
|
+
|
|
55
|
+
const REFUTER_CONTRACT = `Your mandate is to **refute readiness**, not confirm it. A reviewer looking for confirmation finds confirmation; the asymmetry is the point.
|
|
56
|
+
|
|
57
|
+
${REFUTER_FOUR_INPUTS}
|
|
52
58
|
|
|
53
59
|
**You do NOT get** the builder's conversation, reasoning, defences, or draft verdict. If a claim needs the builder's justification to stand, it is not proven.
|
|
54
60
|
|
|
@@ -56,6 +62,16 @@ const REFUTER_CONTRACT = `Your mandate is to **refute readiness**, not confirm i
|
|
|
56
62
|
|
|
57
63
|
**The attack list is the deliverable, not just the findings.** "Nothing found" without saying where you looked is indistinguishable from not having looked.
|
|
58
64
|
|
|
65
|
+
**Before reporting any finding, answer all four questions:**
|
|
66
|
+
1. Can you cite the **exact changed line**?
|
|
67
|
+
2. Can you state the **concrete input, state, and wrong result**? The concrete input and state must be explicit.
|
|
68
|
+
3. Did you inspect the relevant **caller, import, and relevant test**?
|
|
69
|
+
4. Can the severity survive the **existing guards** you verified?
|
|
70
|
+
|
|
71
|
+
If an answer is no, lower the severity or omit the finding. Every **HIGH or CRITICAL** needs the line and failure mode in the report. **Zero findings with an attack list is valid.**
|
|
72
|
+
|
|
73
|
+
**Common false positives to reject:** an equivalent mutant with no diverging input; a documented dummy value that never reaches a sink; a deliberate boundary already enforced by a caller; generated/vendor code outside the change; style preference presented as correctness; and a theoretical race with no shared state or overlapping lifetime.
|
|
74
|
+
|
|
59
75
|
**A finding blocks only if it is caused by this change, is severe, and carries evidence** — a repro or a concrete failure scenario. A suspicion without one is a question, and questions do not block. You fix nothing: findings return through the normal loop, and a SPEC gap goes to the human, never to the builder to self-amend.`;
|
|
60
76
|
|
|
61
77
|
const AGENTS = [
|
|
@@ -112,11 +128,18 @@ ${REFUTER_CONTRACT}
|
|
|
112
128
|
},
|
|
113
129
|
];
|
|
114
130
|
|
|
131
|
+
export const BASE_AGENT_NAMES = Object.freeze(AGENTS.map((agent) => agent.name));
|
|
132
|
+
export { stackAgents, stackAgentNames, validateAgentCatalog };
|
|
133
|
+
export const allAgentNames = () => [...BASE_AGENT_NAMES, ...stackAgentNames()];
|
|
134
|
+
export function resolveAgentNames(skillIds = [], explicitAgentIds = []) {
|
|
135
|
+
return [...BASE_AGENT_NAMES, ...resolveStackAgentNames(skillIds, explicitAgentIds)];
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
// Back-compat: the tier file and its reader are named for `developer` because that is what they
|
|
116
139
|
// configure. The refuters run at the same tier — a deliberate, reversible default: nobody has measured
|
|
117
140
|
// whether a heavier model finds more here, and silently tripling tier-2 cost on an unmeasured hunch is
|
|
118
141
|
// the wrong way to find out.
|
|
119
|
-
const byName = (name) => AGENTS.find((a) => a.name === name);
|
|
142
|
+
const byName = (name) => AGENTS.find((a) => a.name === name) || stackAgentByName(name);
|
|
120
143
|
export const agentNames = () => AGENTS.map((a) => a.name);
|
|
121
144
|
|
|
122
145
|
// `.rsc/developer.json` — the chosen tier (balanced default; never light). `init` writes
|
|
@@ -137,14 +160,16 @@ export function writeDeveloperTier(cwd, tier) {
|
|
|
137
160
|
function renderMd(spec, model, agent) {
|
|
138
161
|
const fm = ['---', `name: ${agent.name}`, `description: "${agent.desc}"`, `model: ${model}`];
|
|
139
162
|
if (spec.mode) fm.push(`mode: ${spec.mode}`);
|
|
163
|
+
if (agent.tools) fm.push(`tools: [${agent.tools.join(', ')}]`);
|
|
140
164
|
fm.push('---', '');
|
|
141
165
|
return `${fm.join('\n')}${agent.body}\n`;
|
|
142
166
|
}
|
|
143
|
-
const renderJson = (model, agent) => `${JSON.stringify({ name: agent.name, description: agent.desc, model, prompt: agent.body }, null, 2)}\n`;
|
|
167
|
+
const renderJson = (model, agent) => `${JSON.stringify({ name: agent.name, description: agent.desc, model, ...(agent.tools ? { tools: agent.tools } : {}), prompt: agent.body }, null, 2)}\n`;
|
|
144
168
|
function renderToml(model, agent) {
|
|
145
169
|
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
146
170
|
// body as a TOML multiline LITERAL string ('''…''') — no escape processing.
|
|
147
|
-
|
|
171
|
+
const tools = agent.tools ? `tools = [${agent.tools.map((tool) => `"${tool}"`).join(', ')}]\n` : '';
|
|
172
|
+
return `name = "${agent.name}"\ndescription = "${esc(agent.desc)}"\nmodel = "${model}"\n${tools}developer_instructions = '''\n${agent.body}\n'''\n`;
|
|
148
173
|
}
|
|
149
174
|
|
|
150
175
|
export function agentPath(target, cwd, name = 'developer') {
|
|
@@ -152,12 +177,15 @@ export function agentPath(target, cwd, name = 'developer') {
|
|
|
152
177
|
return spec ? join(cwd, ...spec.dir.split('/'), `${name}${spec.ext}`) : null;
|
|
153
178
|
}
|
|
154
179
|
|
|
155
|
-
export function writeAgents(target, cwd, tier = readDeveloperTier(cwd)) {
|
|
180
|
+
export function writeAgents(target, cwd, tier = readDeveloperTier(cwd), names = agentNames()) {
|
|
156
181
|
const spec = AGENT_TARGETS[target];
|
|
157
182
|
if (!spec) return [];
|
|
158
|
-
const model = spec.model(tier);
|
|
159
183
|
const written = [];
|
|
160
|
-
for (const
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const agent = byName(name);
|
|
186
|
+
if (!agent) continue;
|
|
187
|
+
const effectiveTier = agent.tier === 'heavy' ? 'heavy' : tier;
|
|
188
|
+
const model = spec.model(effectiveTier);
|
|
161
189
|
const content = spec.format === 'json' ? renderJson(model, agent)
|
|
162
190
|
: spec.format === 'toml' ? renderToml(model, agent)
|
|
163
191
|
: renderMd(spec, model, agent);
|
|
@@ -169,14 +197,34 @@ export function writeAgents(target, cwd, tier = readDeveloperTier(cwd)) {
|
|
|
169
197
|
return written;
|
|
170
198
|
}
|
|
171
199
|
|
|
200
|
+
export function reconcileAgents(target, cwd, tier, previousNames = [], desiredNames = []) {
|
|
201
|
+
if (!targetHasAgents(target)) return { written: [], removed: [], collisions: [], names: [] };
|
|
202
|
+
const previous = new Set(previousNames);
|
|
203
|
+
const desired = new Set(desiredNames);
|
|
204
|
+
const removed = removeAgents(target, cwd, [...previous].filter((name) => !desired.has(name)));
|
|
205
|
+
const written = [];
|
|
206
|
+
const collisions = [];
|
|
207
|
+
const names = [];
|
|
208
|
+
for (const name of desiredNames) {
|
|
209
|
+
const path = agentPath(target, cwd, name);
|
|
210
|
+
if (existsSync(path) && !previous.has(name)) {
|
|
211
|
+
collisions.push(path);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
written.push(...writeAgents(target, cwd, tier, [name]));
|
|
215
|
+
if (existsSync(path)) names.push(name);
|
|
216
|
+
}
|
|
217
|
+
return { written, removed, collisions, names };
|
|
218
|
+
}
|
|
219
|
+
|
|
172
220
|
/**
|
|
173
221
|
* Remove only the agents this catalog ships. An uninstaller that takes an agent the user wrote by hand
|
|
174
222
|
* is worse than one that leaves residue.
|
|
175
223
|
*/
|
|
176
|
-
export function removeAgents(target, cwd) {
|
|
224
|
+
export function removeAgents(target, cwd, names = allAgentNames()) {
|
|
177
225
|
const removed = [];
|
|
178
|
-
for (const
|
|
179
|
-
const path = agentPath(target, cwd,
|
|
226
|
+
for (const name of names) {
|
|
227
|
+
const path = agentPath(target, cwd, name);
|
|
180
228
|
if (path && existsSync(path)) { rmSync(path, { force: true }); removed.push(path); }
|
|
181
229
|
}
|
|
182
230
|
return removed;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { agentByName, allAgentNames } from './agents.js';
|
|
4
|
+
|
|
5
|
+
const COMMAND_TARGETS = Object.freeze({
|
|
6
|
+
claude: { dir: '.claude/commands', ext: '.md', format: 'claude', skillsAreCommands: true },
|
|
7
|
+
cursor: { dir: '.cursor/commands', ext: '.md', format: 'plain' },
|
|
8
|
+
gemini: { dir: '.gemini/commands', ext: '.toml', format: 'gemini' },
|
|
9
|
+
opencode: { dir: '.opencode/commands', ext: '.md', format: 'opencode' },
|
|
10
|
+
copilot: { dir: '.github/prompts', ext: '.prompt.md', format: 'copilot' },
|
|
11
|
+
windsurf: { dir: '.windsurf/workflows', ext: '.md', format: 'windsurf' },
|
|
12
|
+
cline: { dir: '.clinerules/workflows', ext: '.md', format: 'plain', invocationSuffix: '.md' },
|
|
13
|
+
roo: { dir: '.roo/commands', ext: '.md', format: 'roo' },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const COMMAND_TARGET_IDS = Object.freeze(Object.keys(COMMAND_TARGETS));
|
|
17
|
+
export const targetHasCommands = (target) => Boolean(COMMAND_TARGETS[target]);
|
|
18
|
+
|
|
19
|
+
const delegate = (kind, backing) => `Delegate this request to the ${backing} ${kind}. Pass the invocation arguments unchanged: {{ARGS}}\nDo not reproduce that ${kind}'s method here; load it and follow its own stopping and verification rules.`;
|
|
20
|
+
|
|
21
|
+
const skillCommand = (name, backing = name) => ({
|
|
22
|
+
name,
|
|
23
|
+
kind: 'skill',
|
|
24
|
+
backing,
|
|
25
|
+
description: `Run the ${backing} skill through a short, non-duplicating entry point.`,
|
|
26
|
+
body: delegate('skill', backing),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const FIXED_COMMANDS = Object.freeze([
|
|
30
|
+
...['specify', 'clarify', 'plan', 'tasks', 'analyze', 'implement', 'verify', 'review', 'ship', 'debug'].map((name) => skillCommand(name)),
|
|
31
|
+
{
|
|
32
|
+
name: 'build-fix', kind: 'resolver-selector', backing: 'build-resolver',
|
|
33
|
+
description: 'Route a build failure to the installed stack resolver without guessing.',
|
|
34
|
+
body: `Use an installed build-resolver for this failure: {{ARGS}}\nChoose from diagnostic text and affected paths. If more than one remains plausible, list the candidates and ask the user; never dispatch at random.`,
|
|
35
|
+
},
|
|
36
|
+
skillCommand('refactor-clean', 'simplify-code'),
|
|
37
|
+
{
|
|
38
|
+
name: 'test-coverage', kind: 'testing-skill', backing: 'testing-*',
|
|
39
|
+
description: 'Delegate coverage work to the installed stack testing skill.',
|
|
40
|
+
body: delegate('skill', 'testing-*'),
|
|
41
|
+
},
|
|
42
|
+
skillCommand('update-docs', 'knowledge-ops'),
|
|
43
|
+
{
|
|
44
|
+
name: 'checkpoint', kind: 'capability', backing: 'sello',
|
|
45
|
+
description: 'Freeze the current candidate for review and seal it only after the required lenses approve.',
|
|
46
|
+
body: `Use the sello backing for this checkpoint: {{ARGS}}\nRun \`npx @ericrisco/rsc sello freeze\`, complete the required review, then run \`sello approve\` only with the observed lenses. Do not treat freeze as approval.`,
|
|
47
|
+
},
|
|
48
|
+
skillCommand('harness-audit', 'harness'),
|
|
49
|
+
skillCommand('security-scan', 'security-scan'),
|
|
50
|
+
{
|
|
51
|
+
name: 'learn', kind: 'memory', backing: 'memory:learn',
|
|
52
|
+
description: 'Propose one local lesson; saving still requires individual explicit approval.',
|
|
53
|
+
body: `Invoke memory:learn for exactly one proposed lesson: {{ARGS}}\nAfter individual explicit approval, run \`npx @ericrisco/rsc memory learn\` with its text, evidence, scope, confidence and \`--approve\`. Without that approval, write nothing.`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'save-session', kind: 'memory', backing: 'memory:save',
|
|
57
|
+
description: 'Force a local deterministic session checkpoint.',
|
|
58
|
+
body: `Invoke memory:save with the current local session identifiers: {{ARGS}}\nRun \`npx @ericrisco/rsc memory save --session <id>\`. Persist only allowed git and SDD ledger metadata; never include conversation or file content.`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'resume-session', kind: 'memory', backing: 'memory:resume',
|
|
62
|
+
description: 'Read the bounded local continuation record for this branch and worktree.',
|
|
63
|
+
body: `Invoke memory:resume for the current branch and worktree: {{ARGS}}\nRun \`npx @ericrisco/rsc memory resume\`. Label a nearby branch result as nearby; never merge it silently into the exact continuation.`,
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const fixedCommands = () => FIXED_COMMANDS.map((command) => ({ ...command }));
|
|
68
|
+
export const fixedCommandNames = () => FIXED_COMMANDS.map((command) => command.name);
|
|
69
|
+
|
|
70
|
+
function withBacking(command, backing, body) {
|
|
71
|
+
return { ...command, backing, body: body || command.body.replace(command.backing, backing) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fixedFor({ target, skills, agents, memoryMode }) {
|
|
75
|
+
const spec = COMMAND_TARGETS[target];
|
|
76
|
+
if (!spec) return [];
|
|
77
|
+
const skillSet = new Set(skills);
|
|
78
|
+
const resolverIds = agents.filter((id) => agentByName(id)?.role === 'build-resolver');
|
|
79
|
+
const testing = skills.filter((id) => id.startsWith('testing-')).sort();
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const command of FIXED_COMMANDS) {
|
|
82
|
+
if (command.kind === 'skill') {
|
|
83
|
+
if (!skillSet.has(command.backing) || spec.skillsAreCommands) continue;
|
|
84
|
+
out.push({ ...command });
|
|
85
|
+
} else if (command.kind === 'testing-skill') {
|
|
86
|
+
if (!testing.length || spec.skillsAreCommands) continue;
|
|
87
|
+
out.push(withBacking(command, testing[0]));
|
|
88
|
+
} else if (command.kind === 'resolver-selector') {
|
|
89
|
+
if (!resolverIds.length) continue;
|
|
90
|
+
const backing = resolverIds.length === 1 ? resolverIds[0] : 'build-resolver';
|
|
91
|
+
const candidates = resolverIds.join(', ');
|
|
92
|
+
out.push(withBacking(command, backing,
|
|
93
|
+
`Choose the matching installed build-resolver from: ${candidates}. Pass the failure and invocation arguments unchanged: {{ARGS}}\nUse diagnostics and affected paths. If the choice is ambiguous, list candidates and ask; never dispatch at random.`));
|
|
94
|
+
} else if (command.kind === 'memory') {
|
|
95
|
+
if (!['full', 'assisted'].includes(memoryMode)) continue;
|
|
96
|
+
out.push({ ...command });
|
|
97
|
+
} else {
|
|
98
|
+
out.push({ ...command });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stackAliases({ skills, agents }) {
|
|
105
|
+
const skillSet = new Set(skills);
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const id of agents) {
|
|
108
|
+
const agent = agentByName(id);
|
|
109
|
+
if (!agent || !['reviewer', 'build-resolver'].includes(agent.role) || !agent.skills?.length) continue;
|
|
110
|
+
const installedAliases = agent.skills.filter((skill) => skillSet.has(skill));
|
|
111
|
+
const aliases = installedAliases.length ? installedAliases : [agent.skills[0]];
|
|
112
|
+
const suffix = agent.role === 'reviewer' ? 'review' : 'build';
|
|
113
|
+
for (const skill of aliases) {
|
|
114
|
+
out.push({
|
|
115
|
+
name: `${skill}-${suffix}`,
|
|
116
|
+
kind: 'agent',
|
|
117
|
+
backing: id,
|
|
118
|
+
description: `Delegate ${skill} ${suffix} work to the installed ${id} agent.`,
|
|
119
|
+
body: delegate('agent', id),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function resolveCommands({ target, skills = [], agents = [], memoryMode = 'unsupported' }) {
|
|
127
|
+
if (!targetHasCommands(target)) return [];
|
|
128
|
+
const byName = new Map();
|
|
129
|
+
for (const command of [...fixedFor({ target, skills, agents, memoryMode }), ...stackAliases({ skills, agents })]) {
|
|
130
|
+
byName.set(command.name, command);
|
|
131
|
+
}
|
|
132
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function validateCommandCatalog(definitions = FIXED_COMMANDS) {
|
|
136
|
+
const errors = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
for (const command of definitions) {
|
|
139
|
+
const prefix = command?.name || '<unnamed>';
|
|
140
|
+
if (!command?.name || seen.has(command.name)) errors.push(`${prefix}: unique name required`);
|
|
141
|
+
seen.add(command?.name);
|
|
142
|
+
if (!command?.backing || !command?.body?.includes(command.backing)) errors.push(`${prefix}: body must name its exact backing`);
|
|
143
|
+
if ((command?.body || '').split('\n').length > 40) errors.push(`${prefix}: body exceeds 40 lines`);
|
|
144
|
+
if (/everything[- ]claude|affaan|worldflow/iu.test(`${command?.description || ''}\n${command?.body || ''}`)) errors.push(`${prefix}: foreign project vocabulary`);
|
|
145
|
+
}
|
|
146
|
+
return errors;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function commandPath(target, cwd, name) {
|
|
150
|
+
const spec = COMMAND_TARGETS[target];
|
|
151
|
+
return spec ? join(cwd, ...spec.dir.split('/'), `${name}${spec.ext}`) : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderArgs(target, body) {
|
|
155
|
+
const placeholder = target === 'claude' || target === 'opencode' ? '$ARGUMENTS'
|
|
156
|
+
: target === 'gemini' ? '{{args}}'
|
|
157
|
+
: 'any text following the slash command';
|
|
158
|
+
return body.replaceAll('{{ARGS}}', placeholder);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function markdown(command, target) {
|
|
162
|
+
const body = renderArgs(target, command.body);
|
|
163
|
+
if (target === 'cursor' || target === 'cline') return `# /${command.name}\n\n${command.description}\n\n${body}\n`;
|
|
164
|
+
if (target === 'windsurf') return `# /${command.name}\n\n${command.description}\n\n1. ${body.replaceAll('\n', '\n2. ')}\n`;
|
|
165
|
+
const fm = ['---', `description: "${command.description.replaceAll('"', '\\"')}"`, 'argument-hint: "[arguments]"'];
|
|
166
|
+
if (target === 'copilot') fm.push('mode: agent');
|
|
167
|
+
if (target === 'opencode' && command.kind === 'agent') fm.push(`agent: ${command.backing}`);
|
|
168
|
+
fm.push('---', '');
|
|
169
|
+
return `${fm.join('\n')}${body}\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function render(command, target) {
|
|
173
|
+
if (target !== 'gemini') return markdown(command, target);
|
|
174
|
+
const body = renderArgs(target, command.body);
|
|
175
|
+
return `description = ${JSON.stringify(command.description)}\nprompt = ${JSON.stringify(body)}\n`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function reconcileCommands(target, cwd, previousNames = [], desiredCommands = []) {
|
|
179
|
+
if (!targetHasCommands(target)) return { written: [], removed: [], collisions: [], names: [] };
|
|
180
|
+
const previous = new Set(previousNames);
|
|
181
|
+
const desiredNames = new Set(desiredCommands.map((command) => command.name));
|
|
182
|
+
const removed = [];
|
|
183
|
+
for (const name of previous) {
|
|
184
|
+
if (desiredNames.has(name)) continue;
|
|
185
|
+
const path = commandPath(target, cwd, name);
|
|
186
|
+
if (path && existsSync(path)) { rmSync(path, { force: true }); removed.push(path); }
|
|
187
|
+
}
|
|
188
|
+
const written = [];
|
|
189
|
+
const collisions = [];
|
|
190
|
+
const names = [];
|
|
191
|
+
for (const command of desiredCommands) {
|
|
192
|
+
const path = commandPath(target, cwd, command.name);
|
|
193
|
+
if (existsSync(path) && !previous.has(command.name)) {
|
|
194
|
+
collisions.push(path);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
198
|
+
writeFileSync(path, render(command, target));
|
|
199
|
+
written.push(path);
|
|
200
|
+
names.push(command.name);
|
|
201
|
+
}
|
|
202
|
+
return { written, removed, collisions, names };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function allPotentialCommandNames() {
|
|
206
|
+
const names = new Set(fixedCommandNames());
|
|
207
|
+
// Every stack alias follows directly from catalog metadata; no second mapping.
|
|
208
|
+
const agents = allAgentNames();
|
|
209
|
+
const skills = [...new Set(agents.flatMap((id) => agentByName(id)?.skills || []))];
|
|
210
|
+
for (const command of stackAliases({ skills, agents })) names.add(command.name);
|
|
211
|
+
return [...names];
|
|
212
|
+
}
|