Files
PaperClipAI/packages/paperclip-runner/scripts/lib/capability-inventory.mjs
T
DottaandPaperclip 7e6d512597 fix(onboarding): make chief-of-staff hiring reliable (#13317)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The first agent helps the board define work and hire other agents.
> - That agent can have the general role while its instructions require
hiring skills.
> - Missing skills and blocked schema discovery make valid requests
fail.
> - Repeated confirmation and invalid waiting guidance can turn these
failures into extra runs.
> - This PR supplies the required skills, opens read-only schema
discovery, and corrects the guidance.
> - The agent can complete an authorized hire while company approval and
duplicate checks still apply.

## Linked Issues or Issue Description

Refs #13068 — the first-task onboarding flow that this change repairs.
Refs #12029 — related drift between the sandbox allowlist and bundled
hiring guidance. This PR adds schema access; it does not replace the
earlier hiring-route fix.

**What happened?**

A general-role onboarding chief received hiring instructions without the
core hiring skills. Sandbox requests to the documented OpenAPI endpoint
failed. The agent then guessed question and hire payloads. The persona
required new confirmation after validation errors and described waiting
states that agents cannot set.

**Expected behavior**

A direct request authorizes the requested hire. The chief asks only for
material missing details, uses valid API payloads, and completes the
task. Formal company approval gates still apply. A saved human-input
card gives the task a valid waiting state.

**Steps to reproduce**

1. Create an onboarding chief with role `general` through the board.
2. Ask it to hire a friendly robot with a supplied name and
responsibilities.
3. Check its assigned skills, schema requests, question cards, hire
requests, and final task state.

**Paperclip version or commit**

Reproduced on the first-task onboarding implementation after #13068. The
live local verification used this branch at `112f44610`.

**Deployment mode**

The original failure used a hosted sandbox with legacy Codex ACP. Live
verification used an isolated local instance and real `codex_local`
execution. Queue and HTTP/2 transport access is covered by automated
tests.

## What Changed

- Give board-created onboarding chiefs the existing core skills
regardless of role. Preserve explicit skill version pins, including
aliases. Keep ordinary general-agent defaults and authorization checks.
- Allow exactly `GET /api/openapi.json` through both sandbox bridge
transports.
- Publish validator-tested question, free-text, hire, and waiting
examples. Regenerate the runner API reference and capability inventory.
- Clarify direct authorization, material ambiguity, and correction of
confirmed pre-creation validation failures. Preserve uncertain-outcome
reconciliation, duplicate protection, and company approval gates.
- Align disposition instructions with agent permissions and the saved
human-input waiting path.

## Verification

- After rebasing onto current `master`: 69 targeted server tests, 110
queue/HTTP2 bridge tests, and 4 capability inventory tests passed. These
cover core skill defaults, version pins, actor restrictions, schema
access, published examples, hire validation, idempotency, and approval
gates. Waiting recovery tests and live question flows also passed before
the rebase.
- `pnpm -r typecheck` and `pnpm build` passed again after the rebase.
Frozen dependency installation and both generated capability checks
passed.
- Ran the full `pnpm test:run` suite. The initial run had 14 failed
server files due to local database resource limits, a missing built test
fixture, and socket failures. All 14 files passed after fixture repair
and isolated retries. UI, CLI, workspace packages, database tests, and
all 145 serialized server files passed.
- Real one-request hiring replay: one hire, one successful run, task
done in 2m16s. No repeated approval or recovery escalation.
- Real two-turn browser conversation: start with an unspecified hire,
then supply a name and friendly robot responsibilities. One
clarification card, one hire, two successful runs, task done in 3m27s of
execution. No failed writes, confirmation cards, or recovery actions.
- Assigned the hired robot a welcome-message task through the browser.
It produced a warm message under 100 words and finished in one
successful 66-second run, with no questions or recovery actions.
- The two-turn flow still asked an optional preferences question and
gave a technical final reply. These are remaining presentation limits.
- Greptile: 5/5 on `b71f83ba2`, with zero unresolved review threads.
Fixed its generator finding and passed 1,655 published-example/runtime
API tests plus server typecheck. All latest-head CI checks are green (32
passed; 2 unrelated Storybook checks skipped). The signoff-policy
browser test initially timed out while waiting for an approver run. Its
shard passed on one rerun without code changes. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/34698211049).

## Risks

- Onboarding chiefs receive more default skills. Ordinary general agents
retain existing defaults, and explicit versions take precedence.
- Prompt guidance can affect model behavior. The live replays are
examples, not a guarantee that every model follows the guidance.
- Retry guidance applies only when validation confirms that nothing was
created. Uncertain outcomes still require checking existing agents.
- No database migration or new public endpoint. Existing company
boundaries, approval gates, and bounded recovery remain in force.

## Model Used

OpenAI Codex, model `gpt-6-astra`, with reasoning, tool use, code
editing, and live browser verification. The exact context-window size is
not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-12 12:59:42 -05:00

372 lines
19 KiB
JavaScript

import { readFile } from "node:fs/promises";
import { relative, resolve } from "node:path";
import Ajv2020 from "ajv/dist/2020.js";
export const capabilityGroups = [
"hb", "co", "st", "cm", "se", "su", "bl", "dp",
"ix", "ap", "ar", "er", "rf", "mh", "rs", "wk",
];
const groupPolicies = {
hb: ["control_plane_owned", ["control_plane_invariant"], ["run:read"]],
co: ["control_plane_owned", ["control_plane_invariant", "restraint_no_call"], ["task:checkout"]],
st: ["always_agent_tool", ["agent_tool_contract"], ["task:disposition"]],
cm: ["always_agent_tool", ["agent_tool_contract"], ["task:comment"]],
se: ["optional_agent_tool", ["authorization_policy"], ["company:search"]],
su: ["optional_agent_tool", ["authorization_policy"], ["delegation:manage"]],
bl: ["always_agent_tool", ["agent_tool_contract", "combined_multi_hop"], ["task:block"]],
dp: ["always_agent_tool", ["agent_tool_contract"], ["document:write"]],
ix: ["always_agent_tool", ["agent_tool_contract", "combined_multi_hop"], ["interaction:request"]],
ap: ["optional_agent_tool", ["authorization_policy", "combined_multi_hop"], ["approval:read"]],
ar: ["always_agent_tool", ["agent_tool_contract"], ["artifact:register"]],
er: ["control_plane_owned", ["control_plane_invariant", "restraint_no_call"], ["run:recover"]],
rf: ["optional_agent_tool", ["authorization_policy", "combined_multi_hop"], ["admin:extended"]],
mh: ["optional_agent_tool", ["authorization_policy", "combined_multi_hop"], ["delegation:manage"]],
rs: ["control_plane_owned", ["control_plane_invariant", "restraint_no_call"], ["run:resume"]],
wk: ["control_plane_owned", ["control_plane_invariant", "combined_multi_hop"], ["run:wake"]],
};
// The skill and eval corpus are the normative behavior contract. These legacy
// MCP names are retained only as traceability aliases into that contract; they
// are not a second tool catalog and do not receive independent dispositions.
const legacyMcpFoldTargets = {
paperclipMe: "eval:hb-inbox-lite-01",
paperclipInboxLite: "eval:hb-inbox-lite-01",
paperclipListAgents: "eval:rf-api-mgr-heartbeat-01",
paperclipListSkills: "eval:rf-cskill-audit-01",
paperclipGetAgent: "eval:rf-api-mgr-heartbeat-01",
paperclipListIssues: "eval:se-q-filters-01",
paperclipGetIssue: "eval:se-get-issue-01",
paperclipGetHeartbeatContext: "eval:hb-context-01",
paperclipListComments: "eval:se-get-issue-01",
paperclipGetComment: "eval:hb-wake-comment-01",
paperclipListIssueApprovals: "eval:ap-board-approval-01",
paperclipListDocuments: "eval:dp-base-revision-01",
paperclipGetDocument: "eval:dp-base-revision-01",
paperclipListDocumentRevisions: "eval:dp-base-revision-01",
paperclipListProjects: "eval:rf-wf-project-setup-01",
paperclipGetProject: "eval:rf-wf-project-setup-01",
paperclipGetIssueWorkspaceRuntime: "eval:rf-iws-start-url-01",
paperclipControlIssueWorkspaceServices: "eval:rf-iws-start-url-01",
paperclipWaitForIssueWorkspaceService: "eval:rf-iws-target-restart-01",
paperclipListGoals: "eval:su-parent-goal-01",
paperclipGetGoal: "eval:su-parent-goal-01",
paperclipListApprovals: "eval:ap-approval-wake-01",
paperclipCreateApproval: "eval:ap-board-approval-01",
paperclipGetApproval: "eval:ap-approval-wake-01",
paperclipGetApprovalIssues: "eval:ap-approval-wake-01",
paperclipListApprovalComments: "eval:ap-approval-deny-01",
paperclipCreateIssue: "eval:su-parent-goal-01",
paperclipUpdateIssue: "eval:st-done-comment-01",
paperclipCheckoutIssue: "eval:co-body-contract-01",
paperclipReleaseIssue: "eval:er-release-01",
paperclipAddComment: "eval:cm-multiline-01",
paperclipSuggestTasks: "eval:ix-suggest-tasks-01",
paperclipAskUserQuestions: "eval:ix-questions-01",
paperclipRequestConfirmation: "eval:ix-confirmation-plan-01",
paperclipRequestCheckboxConfirmation: "eval:ix-checkbox-01",
paperclipUpsertIssueDocument: "eval:dp-plan-doc-01",
paperclipRestoreIssueDocumentRevision: "eval:dp-base-revision-01",
paperclipLinkIssueApproval: "eval:ap-board-approval-01",
paperclipUnlinkIssueApproval: "eval:ap-board-approval-01",
paperclipApprovalDecision: "eval:ap-approval-wake-01",
paperclipAddApprovalComment: "eval:ap-approval-deny-01",
paperclipApiRequest: "eval:rf-api-404-report-01",
};
const sourceHeader = "# GENERATED FILE — DO NOT EDIT. Run pnpm generate:capability-inventory.\n";
export function encodeInventory(value) {
return `${sourceHeader}${JSON.stringify(value, null, 2)}\n`;
}
export function decodeInventory(source) {
const json = source.startsWith(sourceHeader) ? source.slice(sourceHeader.length) : source;
return JSON.parse(json);
}
function slug(value) {
return value
.toLowerCase()
.replace(/[`*_]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
function classifySkillHeading(heading) {
const value = heading.toLowerCase();
if (/(authentication|identity|scope|checkout|lock|retry|dedupe|budget|wake|inbox|pick work|status quick guide|error handling|server-verified external chat)/.test(value)) {
return "control_plane_owned";
}
if (/(artifact|work product|comment|document|confirmation|question|approval follow-up|block|review|final disposition|issue lifecycle)/.test(value)) {
return "always_agent_tool";
}
return "optional_agent_tool";
}
function parseSkillHeadings(text, file) {
return text.split("\n").flatMap((line, index) => {
const match = /^(#{1,4})\s+(.+?)\s*$/.exec(line);
if (!match) return [];
const title = match[2];
return [{
id: `skill:${file}:${slug(title) || "heading"}:${index + 1}`,
sourceKind: "skill_heading",
sourceAnchor: `${file}:${index + 1}`,
title,
expectedSemantics: `Skill guidance headed “${title}”.`,
primaryDisposition: classifySkillHeading(title),
requiredGrants: [],
assertionClasses: ["control_plane_invariant"],
evidenceIds: [`skill:${file}:${index + 1}`],
}];
});
}
function parseCase(source, sourceAnchor) {
const id = /^id:\s*(.+)$/m.exec(source)?.[1]?.trim();
const title = /^title:\s*(.+)$/m.exec(source)?.[1]?.trim();
const group = /^group:\s*(\d+)$/m.exec(source)?.[1];
const fixture = /^fixture:\s*(.+)$/m.exec(source)?.[1]?.trim() ?? null;
if (!id || !title || !group) throw new Error(`Invalid eval case at ${sourceAnchor}.`);
const prefix = id.split("-")[0];
const policy = groupPolicies[prefix];
if (!policy) throw new Error(`Unknown eval group ${prefix} in ${sourceAnchor}.`);
const expected = [...source.matchAll(/^\s+- method:\s*(\S+)\n\s+path(?:_pattern)?:\s*(.+)$/gm)]
.map((match) => `${match[1]} ${match[2].trim()}`);
const forbidden = [...source.matchAll(/^forbid:\s*$([\s\S]*?)(?=^[a-z_]+:|\n*$)/gm)]
.flatMap((match) => [...match[1].matchAll(/method:\s*(\S+)/g)].map((method) => method[1]));
const skillElements = [...source.matchAll(/- file:\s*(.+)\n\s+section:\s*"?(.+?)"?\n\s+lines:\s*"?(.+?)"?$/gm)]
.map((match) => ({ file: match[1].trim(), section: match[2].trim(), lines: match[3].trim() }));
return {
id,
title,
group: prefix,
legacyGroup: Number(group),
sourceKind: "eval_case",
sourceAnchor,
fixture,
skillElements,
expectedSemantics: expected.length > 0 ? expected : ["No required HTTP call; evaluate the stated restraint or final state."],
forbiddenSemantics: forbidden,
primaryDisposition: policy[0],
requiredGrants: policy[2],
assertionClasses: policy[1],
evidenceIds: [`eval:${id}`],
};
}
function parseMcpTools(source) {
return [...source.matchAll(/makeTool\(\s*"([^"]+)"\s*,\s*"([^"]+)"/g)].map((match) => {
const [name, description] = [match[1], match[2]];
const foldedInto = legacyMcpFoldTargets[name];
return {
id: `mcp:${name}`,
name,
description,
sourceKind: "legacy_mcp_alias",
sourceAnchor: `packages/mcp-server/src/tools.ts:${source.slice(0, match.index).split("\n").length}`,
expectedSemantics: description,
foldedInto,
evidenceId: `mcp:${name}`,
};
});
}
function foldLegacyMcpAliases(evaluations, legacyMcpAliases) {
const aliasesByTarget = new Map();
for (const alias of legacyMcpAliases.rows) {
const aliases = aliasesByTarget.get(alias.foldedInto) ?? [];
aliases.push(alias);
aliasesByTarget.set(alias.foldedInto, aliases);
}
return {
...evaluations,
rows: evaluations.rows.map((row) => {
const aliases = aliasesByTarget.get(`eval:${row.id}`) ?? [];
if (aliases.length === 0) return row;
return {
...row,
legacyMcpAliases: aliases.map((alias) => alias.id),
evidenceIds: [...row.evidenceIds, ...aliases.map((alias) => alias.evidenceId)],
};
}),
};
}
export async function buildInventories({ repoRoot, evalRoot }) {
const capabilities = await buildSkillInventory(repoRoot);
const evalDirectory = resolve(evalRoot, "skills/paperclip/tests/cases");
const { readdir } = await import("node:fs/promises");
const caseNames = (await readdir(evalDirectory)).filter((name) => name.endsWith(".yaml")).sort();
const evalRows = await Promise.all(caseNames.map(async (name) => parseCase(
await readFile(resolve(evalDirectory, name), "utf8"),
`paperclip-evals/paperclip-skill-optimization/skills/paperclip/tests/cases/${name}`,
)));
const legacyMcpAliases = await buildMcpInventory(repoRoot);
const evaluations = foldLegacyMcpAliases({
schemaVersion: 2,
inventoryRole: "normative",
generatedFrom: "paperclip-evals/paperclip-skill-optimization/skills/paperclip/tests/cases",
rows: evalRows,
}, legacyMcpAliases);
return {
capabilities,
evaluations,
legacyMcpAliases,
};
}
export async function buildSkillInventory(repoRoot) {
const skillRoot = resolve(repoRoot, "skills/paperclip");
const skillFiles = ["SKILL.md", "references/artifacts.md", "references/cases.md", "references/company-skills.md", "references/issue-workspaces.md", "references/routines.md", "references/workflows.md", "references/api-reference.md"];
const rows = (await Promise.all(skillFiles.map(async (file) => parseSkillHeadings(
await readFile(resolve(skillRoot, file), "utf8"),
`skills/paperclip/${file}`,
)))).flat();
return {
schemaVersion: 2,
inventoryRole: "normative",
generatedFrom: skillFiles.map((file) => `skills/paperclip/${file}`),
rows,
};
}
export async function buildMcpInventory(repoRoot) {
return {
schemaVersion: 2,
inventoryRole: "legacy_alias_index",
generatedFrom: "packages/mcp-server/src/tools.ts",
normativeSources: ["capabilities", "evaluations"],
rows: parseMcpTools(await readFile(resolve(repoRoot, "packages/mcp-server/src/tools.ts"), "utf8")),
};
}
export function validateInventories(inventories) {
const errors = [];
const expectedCounts = { capabilities: 155, evaluations: 106, legacyMcpAliases: 42 };
const normativeNames = ["capabilities", "evaluations"];
const normativeRows = new Map();
const globalNormativeIds = new Set();
for (const name of normativeNames) {
const inventory = inventories[name];
if (!inventory) {
errors.push(`Missing normative inventory ${name}.`);
continue;
}
if (inventory.schemaVersion !== 2 || inventory.inventoryRole !== "normative") {
errors.push(`${name} must be a schema version 2 normative inventory.`);
}
const seen = new Set();
for (const row of inventory.rows) {
if (!row.id || seen.has(row.id)) errors.push(`${name} contains a missing or duplicate id: ${row.id ?? "<missing>"}.`);
seen.add(row.id);
const normativeId = name === "evaluations" ? `eval:${row.id}` : row.id;
if (globalNormativeIds.has(normativeId)) errors.push(`Normative row ${normativeId} is multiply classified.`);
globalNormativeIds.add(normativeId);
normativeRows.set(normativeId, row);
if (!["control_plane_owned", "always_agent_tool", "optional_agent_tool"].includes(row.primaryDisposition)) {
errors.push(`${name}:${row.id} has invalid primaryDisposition.`);
}
if (!row.sourceAnchor || !row.expectedSemantics || !Array.isArray(row.requiredGrants) || !Array.isArray(row.assertionClasses) || !Array.isArray(row.evidenceIds)) {
errors.push(`${name}:${row.id} is missing normalized traceability fields.`);
}
}
if (expectedCounts[name] && inventory.rows.length !== expectedCounts[name]) {
errors.push(`${name} expected ${expectedCounts[name]} rows but found ${inventory.rows.length}.`);
}
}
const groups = new Set((inventories.evaluations?.rows ?? []).map((row) => row.group));
for (const group of capabilityGroups) if (!groups.has(group)) errors.push(`evaluations is missing group ${group}.`);
if (groups.size !== capabilityGroups.length) errors.push(`evaluations has unexpected groups: ${[...groups].sort().join(", ")}.`);
const legacyMcpAliases = inventories.legacyMcpAliases;
if (!legacyMcpAliases) {
errors.push("Missing legacy MCP alias index.");
return errors;
}
if (legacyMcpAliases.schemaVersion !== 2 || legacyMcpAliases.inventoryRole !== "legacy_alias_index") {
errors.push("legacyMcpAliases must be a schema version 2 legacy alias index.");
}
const seenAliases = new Set();
for (const alias of legacyMcpAliases.rows) {
if (!alias.id || seenAliases.has(alias.id)) {
errors.push(`legacyMcpAliases contains a missing or duplicate id: ${alias.id ?? "<missing>"}.`);
}
seenAliases.add(alias.id);
if (Object.hasOwn(alias, "primaryDisposition")) {
errors.push(`legacyMcpAliases:${alias.id} must not define an independent primaryDisposition.`);
}
if (!alias.sourceAnchor || !alias.expectedSemantics || !alias.foldedInto || !alias.evidenceId) {
errors.push(`legacyMcpAliases:${alias.id} is missing folded traceability fields.`);
continue;
}
const target = normativeRows.get(alias.foldedInto);
if (!target) {
errors.push(`legacyMcpAliases:${alias.id} folds into unknown normative row ${alias.foldedInto}.`);
continue;
}
const targetAliases = target.legacyMcpAliases ?? [];
if (targetAliases.filter((id) => id === alias.id).length !== 1) {
errors.push(`legacyMcpAliases:${alias.id} must be folded exactly once into ${alias.foldedInto}.`);
}
if (target.evidenceIds.filter((id) => id === alias.evidenceId).length !== 1) {
errors.push(`legacyMcpAliases:${alias.id} evidence must be folded exactly once into ${alias.foldedInto}.`);
}
}
if (legacyMcpAliases.rows.length !== expectedCounts.legacyMcpAliases) {
errors.push(`legacyMcpAliases expected ${expectedCounts.legacyMcpAliases} rows but found ${legacyMcpAliases.rows.length}.`);
}
for (const [normativeId, row] of normativeRows) {
for (const aliasId of row.legacyMcpAliases ?? []) {
if (!seenAliases.has(aliasId)) errors.push(`${normativeId} references unknown legacy MCP alias ${aliasId}.`);
}
}
return errors;
}
export function validateInventorySchema(inventories, schema) {
const ajv = new Ajv2020({
allErrors: true,
allowUnionTypes: true,
strict: true,
strictRequired: false,
});
const validate = ajv.compile(schema);
const errors = [];
for (const [name, inventory] of Object.entries(inventories)) {
if (validate(inventory)) continue;
for (const error of validate.errors ?? []) {
errors.push(`${name}${error.instancePath || "/"} ${error.message ?? "failed schema validation"}.`);
}
}
return errors;
}
export function renderContractModule(inventories) {
const counts = {
skillReferenceCapabilities: inventories.capabilities.rows.length,
evalCases: inventories.evaluations.rows.length,
normativeRows: inventories.capabilities.rows.length + inventories.evaluations.rows.length,
legacyMcpAliases: inventories.legacyMcpAliases.rows.length,
};
return `// Generated by scripts/generate-capability-inventory.mjs. Do not edit.\n\nexport type CapabilityPrimaryDisposition =\n | "control_plane_owned"\n | "always_agent_tool"\n | "optional_agent_tool";\n\nexport const capabilityInventoryCounts = ${JSON.stringify(counts, null, 2)} as const;\n\nexport const capabilityEvalGroups = ${JSON.stringify(capabilityGroups, null, 2)} as const;\n\nexport const capabilityInventoryContract = {\n schemaVersion: 2,\n normativeInventories: [\n "spec/capability/capabilities.yaml",\n "spec/capability/eval-traceability.yaml",\n ],\n legacyAliasIndexes: ["spec/capability/mcp-tool-map.yaml"],\n primaryDispositions: [\n "control_plane_owned",\n "always_agent_tool",\n "optional_agent_tool",\n ] as const,\n} as const;\n`;
}
export function renderDocumentation(inventories) {
const groupCounts = Object.fromEntries(capabilityGroups.map((group) => [group, inventories.evaluations.rows.filter((row) => row.group === group).length]));
const capabilityLines = inventories.capabilities.rows.map((row) => `| ${row.id} | ${row.primaryDisposition} | ${row.sourceAnchor} |`).join("\n");
const mcpLines = inventories.legacyMcpAliases.rows.map((alias) => {
const target = inventories.evaluations.rows.find((row) => `eval:${row.id}` === alias.foldedInto);
return `| ${alias.name} | ${alias.foldedInto} | ${target?.primaryDisposition ?? "unknown"} | ${alias.sourceAnchor} |`;
}).join("\n");
return `<!-- GENERATED FILE — DO NOT EDIT. Run pnpm generate:capability-inventory. -->\n\n# Capability Capability Contract\n\nThis generated contract is a self-contained derivative of the Paperclip skill, its seven references, the Paperclip Evals corpus, and the legacy MCP tool surface. It does not import or contact the Paperclip control plane.\n\nThe skill/reference inventory and eval cases are the only normative behavior sources. Paperclip does not use the legacy MCP calls as a production capability surface; all MCP names below are traceability aliases folded into normative eval rows. Their disposition, grants, assertions, and evidence contract are inherited from the target row rather than classified independently.\n\n## Baseline Counts\n\n- Skill/reference headings: ${inventories.capabilities.rows.length}\n- Eval cases: ${inventories.evaluations.rows.length} across ${capabilityGroups.length} groups\n- Total normative rows: ${inventories.capabilities.rows.length + inventories.evaluations.rows.length}\n- Legacy MCP aliases folded into normative rows: ${inventories.legacyMcpAliases.rows.length}\n\n| Eval group | Cases |\n| --- | ---: |\n${Object.entries(groupCounts).map(([group, count]) => `| ${group} | ${count} |`).join("\n")}\n\n## Regeneration\n\n- \`pnpm --dir packages/paperclip-runner generate:capability-inventory\` imports the canonical baselines and rewrites every generated file.\n- \`pnpm --dir packages/paperclip-runner check:capability-inventory\` validates counts, uniqueness, normative dispositions, one-to-one MCP folds, required fields, and generated-file drift without requiring the external eval repository.\n\n## Skill / Reference Rows\n\n| Capability | Primary disposition | Source anchor |\n| --- | --- | --- |\n${capabilityLines}\n\n## Legacy MCP Alias Index\n\nThis is a compatibility/traceability index, not a tool catalog. “Inherited disposition” is shown only to make the normative target easy to audit.\n\n| Legacy MCP name | Folded into normative row | Inherited disposition | Source anchor |\n| --- | --- | --- | --- |\n${mcpLines}\n`;
}
export function packageRelative(repoRoot, path) {
return relative(repoRoot, path).replaceAll("\\", "/");
}