Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22.14.0'
cache: 'pnpm'

- name: Install deps
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm run lint

- name: Typecheck
run: pnpm run typecheck

- name: Test
run: pnpm run test
4 changes: 4 additions & 0 deletions packages/workload-router/routing-profiles/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
"sage-cloud-e2e-conduction": {
"tier": "best-value",
"rationale": "End-to-end conduction is orchestration-heavy work where strong reasoning is useful without requiring the top tier by default."
},
"capability-discovery": {
"tier": "best-value",
"rationale": "Searching skill.sh and prpm.dev for existing skills, agents, and hooks is lightweight research; the balanced default is sufficient when guided by the skill.sh/find-skills and @prpm/self-improving skills."
}
}
}
8 changes: 6 additions & 2 deletions packages/workload-router/scripts/generate-personas.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const exportNameMap = new Map([
['sage-slack-egress-migrator', 'sageSlackEgressMigrator'],
['sage-proactive-rewirer', 'sageProactiveRewirer'],
['cloud-slack-proxy-guard', 'cloudSlackProxyGuard'],
['agent-relay-e2e-conductor', 'agentRelayE2eConductor']
['agent-relay-e2e-conductor', 'agentRelayE2eConductor'],
['capability-discoverer', 'capabilityDiscoverer']
]);

const files = (await fs.readdir(personasDir)).filter((n) => n.endsWith('.json')).sort();
Expand All @@ -38,7 +39,10 @@ const lines = [
for (const file of files) {
const basename = file.replace(/\.json$/, '');
const exportName = exportNameMap.get(basename);
if (!exportName) throw new Error(`No export name mapping for ${basename}`);
if (!exportName) {
console.warn(`[generate-personas] skipping unwired persona: ${basename} (no export name mapping)`);
continue;
}
const json = await fs.readFile(path.join(personasDir, file), 'utf8');
lines.push(`export const ${exportName} = ${json.trim()} as const;`);
lines.push('');
Expand Down
38 changes: 38 additions & 0 deletions packages/workload-router/src/generated/personas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,44 @@ export const architecturePlanner = {
}
} as const;

export const capabilityDiscoverer = {
"id": "capability-discoverer",
"intent": "capability-discovery",
"description": "Finds existing skills, agents, and hooks for a project by searching both the skills.sh ecosystem and prpm.dev instead of hand-rolling new logic. Picks the best fit across providers and emits the exact install command.",
"skills": [
{
"id": "skill.sh/find-skills",
"source": "https://github.com/vercel-labs/skills#find-skills",
"description": "skill.sh find-skills guide for searching skills.sh, proposing matches, and driving `npx skills add` installs."
},
{
"id": "prpm/self-improving",
"source": "https://prpm.dev/packages/@prpm/self-improving",
"description": "prpm skill that teaches an agent to search prpm.dev for skills, agents, and hooks and install them with the right --as flag for the active harness."
}
],
"tiers": {
"best": {
"harness": "codex",
"model": "openai-codex/gpt-5.3-codex",
"systemPrompt": "You are a capability discovery specialist. Your job is to close capability gaps by finding existing skills, agents, or hooks from either the skills.sh ecosystem or prpm.dev, rather than hand-rolling new logic. Process: (1) restate the capability gap in one sentence, (2) classify whether the gap is best filled by a skill (reusable knowledge), an agent (a harness persona), or a hook (lifecycle automation), (3) search BOTH ecosystems — skill.sh via `npx skills find <query>` and prpm.dev — and inspect candidate manifests/SKILL.md before recommending anything, (4) recommend at most two packages total across providers with explicit fit rationale (what each covers, what it does NOT, which provider it comes from), (5) produce the exact install command for the chosen provider: `npx -y skills add <repo-url> --skill <name> -y` for skill.sh or `npx -y prpm install <ref> --as <harness>` for prpm (using the currently active harness flag), and (6) flag any security/permission notes surfaced by skills.sh assessments and any conflicts with already-installed packages. Never recommend a package you have not verified exists. If no candidate fits in either ecosystem, say so plainly and suggest the closest adjacent capability instead of inventing one. Apply the skill.sh/find-skills and prpm/self-improving skills for canonical discovery and install workflow. Output contract: gap summary, type classification, top candidates with provider + fit rationale, exact install command, security/conflict notes, open questions for the user.",
"harnessSettings": { "reasoning": "high", "timeoutSeconds": 600 }
},
"best-value": {
"harness": "opencode",
"model": "opencode/gpt-5-nano",
"systemPrompt": "You are a capability discovery specialist in efficient mode. Same quality bar as top tier; reduce only verbosity. Process: restate the gap, classify it as skill/agent/hook, search BOTH skill.sh (`npx skills find <query>`) and prpm.dev, verify candidate manifests before recommending, recommend at most two packages total across providers with provider-labeled fit rationale, produce the exact install command for the chosen provider (`npx -y skills add <repo-url> --skill <name> -y` for skill.sh or `npx -y prpm install <ref> --as <harness>` for prpm using the active harness), flag security/permission notes and install conflicts. Never recommend unverified packages. If nothing fits in either ecosystem, say so directly. Apply the skill.sh/find-skills and prpm/self-improving skills. Output contract: gap summary, classification, candidates with provider + fit rationale, install command, security/conflict notes, open questions.",
"harnessSettings": { "reasoning": "medium", "timeoutSeconds": 450 }
},
"minimum": {
"harness": "opencode",
"model": "opencode/minimax-m2.5-free",
"systemPrompt": "You are a concise capability discovery specialist. Same quality bar; only limit depth. Required: classify the gap as skill/agent/hook; search BOTH skill.sh via `npx skills find <query>` and prpm.dev; verify candidate manifests before recommending; never fabricate packages; recommend at most two with provider-labeled fit rationale; produce the exact install command for the chosen provider (`npx -y skills add <repo-url> --skill <name> -y` for skill.sh or `npx -y prpm install <ref> --as <harness>` for prpm); call out security notes and install conflicts. If nothing fits, say so. Apply the skill.sh/find-skills and prpm/self-improving skills. Output contract: gap summary, classification, candidates, install command, notes, open questions.",
"harnessSettings": { "reasoning": "low", "timeoutSeconds": 300 }
}
}
} as const;

export const cloudSandboxInfra = {
"id": "cloud-sandbox-infra",
"intent": "cloud-sandbox-infra",
Expand Down
212 changes: 211 additions & 1 deletion packages/workload-router/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
Expand Down Expand Up @@ -114,6 +114,10 @@ test('resolves review from custom routing profile rule', () => {
'sage-cloud-e2e-conduction': {
tier: 'best-value',
rationale: 'e2e conduction benefits from strong reasoning without the highest-cost default'
},
'capability-discovery': {
tier: 'best-value',
rationale: 'lightweight discovery work'
}
}
});
Expand Down Expand Up @@ -264,6 +268,145 @@ test('materializeSkillsFor derives an install plan from a resolved persona', ()
assert.match(cmd, /prpm install @prpm\/npm-trusted-publishing --as /);
});

test('materializeSkills emits a skill.sh install for a github#skill source', () => {
const plan = materializeSkills(
[
{
id: 'skill.sh/find-skills',
source: 'https://github.com/vercel-labs/skills#find-skills',
description: 'skill.sh discovery skill'
}
],
'claude'
);

assert.equal(plan.installs.length, 1);
const [install] = plan.installs;
assert.equal(install.sourceKind, 'skill.sh');
assert.equal(install.packageRef, 'https://github.com/vercel-labs/skills#find-skills');
assert.deepEqual(
[...install.installCommand],
['npx', '-y', 'skills', 'add', 'https://github.com/vercel-labs/skills', '--skill', 'find-skills', '-y']
);
// skill.sh uses a single universal content dir regardless of harness.
assert.equal(install.installedDir, '.agents/skills/find-skills');
assert.equal(install.installedManifest, '.agents/skills/find-skills/SKILL.md');
// Cleanup should target every harness symlink + the universal dir, but
// never the lockfile itself.
assert.deepEqual(
[...install.cleanupPaths],
[
'.agents/skills/find-skills',
'.claude/skills/find-skills',
'.factory/skills/find-skills',
'.kiro/skills/find-skills',
'skills/find-skills'
]
);
assert.ok(!install.cleanupPaths.includes('skills-lock.json'));
});

test('prpm installs carry a harness-scoped cleanup path (not the lockfile)', () => {
const plan = materializeSkills(
[
{
id: 'prpm/npm-trusted-publishing',
source: '@prpm/npm-trusted-publishing',
description: 'bare ref form'
}
],
'codex'
);
const [install] = plan.installs;
assert.deepEqual([...install.cleanupPaths], ['.agents/skills/npm-trusted-publishing']);
assert.ok(!install.cleanupPaths.includes('prpm.lock'));
});

test('usePersona install command never embeds cleanup (agent must read skills first)', () => {
// Regression guard: previously buildInstallArtifacts inlined `&& rm -rf` into
// the install step, which ran BEFORE the agent step and deleted skill files
// the agent needed to read. Cleanup now lives on a separate post-agent step
// and on install.cleanupCommandString for Mode B callers.
const context = usePersona('npm-provenance');
assert.doesNotMatch(context.install.commandString, /rm -rf/);
assert.match(
context.install.commandString,
/prpm install @prpm\/npm-trusted-publishing --as [a-z]+/
);
});

test('usePersona exposes a post-run cleanupCommandString targeting skill artifact paths', () => {
const context = usePersona('npm-provenance');
assert.ok(Array.isArray(context.install.cleanupCommand));
assert.equal(context.install.cleanupCommand[0], 'sh');
assert.match(context.install.cleanupCommandString, /^rm -rf /);
assert.match(context.install.cleanupCommandString, /npm-trusted-publishing/);
// The provider lockfile must never be cleaned — repeat runs depend on it.
assert.doesNotMatch(context.install.cleanupCommandString, /prpm\.lock|skills-lock\.json/);
});

test('usePersona cleanupCommandString chains paths from every install in the plan', () => {
const context = usePersona('capability-discovery');
const cleanup = context.install.cleanupCommandString;
// Both the skill.sh symlink set and the prpm per-harness dir should appear
// in a single rm -rf chain.
assert.match(cleanup, /^rm -rf /);
assert.match(cleanup, /find-skills/);
assert.match(cleanup, /self-improving/);
// Cover every skill.sh harness symlink, not just the universal dir.
assert.match(cleanup, /\.agents\/skills\/find-skills/);
assert.match(cleanup, /\.claude\/skills\/find-skills/);
assert.match(cleanup, /\.factory\/skills\/find-skills/);
assert.match(cleanup, /\.kiro\/skills\/find-skills/);
});

test('usePersona cleanupCommandString is a shell no-op when the persona declares no skills', () => {
const context = usePersona('architecture-plan');
assert.equal(context.install.cleanupCommandString, ':');
});

test('resolves capability-discovery persona carrying both skill.sh and prpm skills', () => {
const selection = resolvePersona('capability-discovery');
assert.equal(selection.personaId, 'capability-discoverer');
assert.equal(selection.tier, 'best-value');
assert.equal(selection.skills.length, 2);

const byId = new Map(selection.skills.map((s) => [s.id, s]));
const skillSh = byId.get('skill.sh/find-skills');
assert.ok(skillSh, 'missing skill.sh/find-skills skill');
assert.equal(skillSh!.source, 'https://github.com/vercel-labs/skills#find-skills');
const prpm = byId.get('prpm/self-improving');
assert.ok(prpm, 'missing prpm/self-improving skill');
assert.match(prpm!.source, /prpm\.dev\/packages\/@prpm\/self-improving/);
});

test('materializeSkillsFor capability-discovery plans both installs under one shell chain with cleanup', () => {
const selection = resolvePersona('capability-discovery');
const plan = materializeSkillsFor(selection);
assert.equal(plan.installs.length, 2);

const byKind = new Map(plan.installs.map((i) => [i.sourceKind, i]));
const skillShInstall = byKind.get('skill.sh');
const prpmInstall = byKind.get('prpm');
assert.ok(skillShInstall, 'missing skill.sh install');
assert.ok(prpmInstall, 'missing prpm install');
assert.deepEqual(
[...skillShInstall!.installCommand],
['npx', '-y', 'skills', 'add', 'https://github.com/vercel-labs/skills', '--skill', 'find-skills', '-y']
);
assert.equal(prpmInstall!.packageRef, '@prpm/self-improving');

const context = usePersona('capability-discovery');
const cmd = context.install.commandString;
// Both installs should be chained back-to-back with `&&`, with NO inline
// cleanup — cleanup lives on a separate post-agent step.
assert.match(
cmd,
/skills add https:\/\/github\.com\/vercel-labs\/skills --skill find-skills -y && npx -y prpm install @prpm\/self-improving/
);
assert.doesNotMatch(cmd, /rm -rf/);
});

test('materializeSkills rejects unknown skill sources', () => {
assert.throws(
() =>
Expand Down Expand Up @@ -390,6 +533,73 @@ process.stdout.write('agent-after-install');
}
});

test('usePersona.sendMessage keeps skill files on disk for the agent, then cleans them up after', async () => {
// Regression guard for the Devin-flagged P1: cleanup used to be chained
// into the install step with `&& rm -rf`, so by the time the agent step
// ran the skill manifest was already gone. This test:
// 1. Uses a fake `npx` that writes a stub SKILL.md into the exact path
// listed in the install plan's cleanupPaths (simulating prpm install).
// 2. Uses a fake `codex` that asserts the SKILL.md IS present and
// captures its location, then writes an agent-ran sentinel.
// 3. Asserts that after sendMessage settles, the cleanupPaths have been
// removed — proving the post-agent cleanup step ran.
const dir = mkdtempSync(join(tmpdir(), 'use-persona-cleanup-'));
try {
// The install plan cleanupPaths for npm-provenance under codex is
// `.agents/skills/npm-trusted-publishing`. The fake npx must materialize
// a SKILL.md inside that dir so the agent can verify it.
writeNodeExecutable(
dir,
'npx',
`
const { mkdirSync, writeFileSync } = require('node:fs');
const path = require('node:path');
const skillDir = path.join(process.cwd(), '.agents/skills/npm-trusted-publishing');
mkdirSync(skillDir, { recursive: true });
writeFileSync(path.join(skillDir, 'SKILL.md'), '# npm trusted publishing', 'utf8');
`
);
writeNodeExecutable(
dir,
'codex',
`
const { existsSync, writeFileSync } = require('node:fs');
const skillPath = '.agents/skills/npm-trusted-publishing/SKILL.md';
if (!existsSync(skillPath)) {
process.stderr.write('SKILL.md missing during agent step');
process.exit(9);
}
writeFileSync('agent-saw-skill.txt', 'yes', 'utf8');
process.stdout.write('agent-read-skill');
`
);

const context = usePersona('npm-provenance', { harness: 'codex' });
// Sanity check: plan must say this is the path we expect to verify.
assert.deepEqual(
context.install.plan.installs[0]?.cleanupPaths,
['.agents/skills/npm-trusted-publishing']
);

const result = await context.sendMessage('Configure publishing', {
workingDirectory: dir,
env: buildEnv(dir)
});

assert.equal(result.status, 'completed');
// Agent must have seen the skill during its run.
assert.equal(readFileSync(join(dir, 'agent-saw-skill.txt'), 'utf8'), 'yes');
// Post-agent cleanup must have removed the skill artifact path.
assert.equal(
existsSync(join(dir, '.agents/skills/npm-trusted-publishing')),
false,
'cleanup step should have removed the skill artifact dir after the agent ran'
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

test('usePersona.sendMessage maps non-zero exits to PersonaExecutionError with captured stderr', async () => {
const dir = mkdtempSync(join(tmpdir(), 'use-persona-fail-'));
try {
Expand Down
Loading
Loading