feat(agents): integrate isolated Pi conversion foundation from PR 3022

Carry the canonical IR, Pi emitter and source-schema doctor from https://github.com/affaan-m/ECC/pull/3022. Cursor and OpenCode emitters remain with the open source PR pending capability fixes.

Co-authored-by: alizahidraja <alizahidrajaa@gmail.com>
This commit is contained in:
haelyra
2026-10-10 17:43:18 -04:00
co-authored by alizahidraja
parent 58fc8f25a5
commit eb7ffd8d64
14 changed files with 1159 additions and 4 deletions
+65 -1
View File
@@ -24,7 +24,7 @@
* degrades to a warning and never terminates the Pi session.
*/
import { execFile } from "node:child_process"
import { execFile, execFileSync } from "node:child_process"
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
@@ -556,11 +556,74 @@ function describeRulesStatus(): string {
return `${cachedRuleFileCount}/${PORTABLE_RULE_FILES.length} rule file(s), ${rules.length} chars, from rules/common/${shortfall}`
}
const AGENT_CHECK_TIMEOUT_MS = 5_000
type AgentPortability =
| { status: "validated"; count: number }
| { status: "failed"; reason: string }
| { status: "unvalidated"; reason: string }
/**
* Verify agent portability by actually running `agent-convert.js --check` rather
* than trusting file existence. A converter that exists but fails (schema drift,
* a frontmatter variant that fails ajv) must not report a green doctor line.
*/
function validateAgentPortability(converterScript: string): AgentPortability {
if (!fs.existsSync(converterScript)) {
return { status: "unvalidated", reason: "converter script not found" }
}
let nodeExec: string
try {
nodeExec = resolveHookRuntime()
} catch (err) {
return { status: "unvalidated", reason: `no node runtime: ${messageOf(err)}` }
}
try {
const stdout = execFileSync(nodeExec, [converterScript, "--check"], {
timeout: AGENT_CHECK_TIMEOUT_MS,
encoding: "utf8",
})
const match = stdout.match(/OK: (\d+) agents parsed and validated/)
if (match) {
return { status: "validated", count: parseInt(match[1], 10) }
}
return { status: "failed", reason: `unexpected output: ${stdout.slice(0, 200).trim()}` }
} catch (err) {
return { status: "failed", reason: messageOf(err) }
}
}
function messageOf(err: unknown): string {
if (err && typeof err === "object" && "message" in err) {
return String((err as { message: unknown }).message)
}
return "unknown error"
}
function describeAgentPortability(agentCount: number, portability: AgentPortability): string {
if (agentCount === 0) {
return "NOT FOUND"
}
if (portability.status === "validated") {
return `${portability.count} agent(s) source schema validated; Pi conversion available via scripts/agent-convert.js`
}
if (portability.status === "failed") {
return `${agentCount} agent(s) present but validation FAILED: ${portability.reason}`
}
return `${agentCount} agent(s) present (unvalidated — ${portability.reason})`
}
function buildDoctorReport(ctx: ExtensionContext): string {
const skillsDir = path.join(ECC_ROOT, "skills")
const commandsDir = path.join(ECC_ROOT, "commands")
const skillCount = countDirectories(skillsDir)
const commandCount = countMarkdownFiles(commandsDir)
const agentsDir = path.join(ECC_ROOT, "agents")
const agentCount = countMarkdownFiles(agentsDir)
const converterScript = path.join(ECC_ROOT, "scripts", "agent-convert.js")
const agentLine = describeAgentPortability(agentCount, validateAgentPortability(converterScript))
const lines = [
"ECC adapter for Pi",
@@ -572,6 +635,7 @@ function buildDoctorReport(ctx: ExtensionContext): string {
"Canonical resources",
` skills/ ${skillCount > 0 ? `${skillCount} skill(s)` : "NOT FOUND"} (${skillsDir})`,
` commands/ ${commandCount > 0 ? `${commandCount} command(s)` : "NOT FOUND"} (${commandsDir})`,
` agents/ ${agentLine} (${agentsDir})`,
"",
"Engineering rules (injected into the system prompt)",
` ${describeRulesStatus()}`,
@@ -39,7 +39,7 @@ The matrix below is rendered from
| Claude Code | Native | Claude plugin assets; skills; commands; hooks; MCP config; local rules; statusline-oriented workflows | Claude-native hooks do not imply parity in other harnesses | `./install.sh --profile minimal --target claude`; Claude plugin install | `npm run harness:audit -- --format json`; `node scripts/session-inspect.js --list-adapters` | Avoid loading every skill by default; keep hooks opt-in and inspectable. |
| Codex | Instruction-backed | `AGENTS.md`; Codex plugin metadata; skills; MCP reference config; command patterns | Native hook enforcement and Claude slash-command semantics are not equivalent | `./install.sh --profile minimal --target codex`; repo-local `AGENTS.md` review | `npm run harness:audit -- --format json` | Treat hooks as policy text unless a native Codex hook surface exists. |
| OpenCode | Adapter-backed | OpenCode package/plugin metadata; shared skills; MCP config; event adapter patterns | Event names, plugin packaging, and command dispatch differ from Claude Code | OpenCode package or plugin surface from this repo | `node tests/scripts/build-opencode.test.js`; `npm run harness:audit -- --format json` | Keep hook logic in shared scripts and adapt only event shape at the edge. |
| Pi | Adapter-backed | Pi package manifest; canonical ECC skills (skills/); canonical ECC commands as prompt templates (commands/); canonical ECC engineering rules (rules/common/) injected into the system prompt; session lifecycle hook adapter; /ecc-doctor diagnostics command | Subagents, chains, approval prompts, and persistent todos require companion Pi packages and are not part of this adapter; Pi core has no MCP surface, though ECC MCP configs load verbatim through the community pi-mcp-adapter package, which ECC neither installs nor depends on | `pi install git:github.com/affaan-m/ECC`; `pi install /path/to/ECC` from a local checkout | `node tests/pi/pi-package-manifest.test.js`; `node tests/pi/pi-extension-adapter.test.js`; `npm run harness:adapters -- --check` | Pi extensions execute with full user permissions, and hooks run without a shell and resolve from the installed package rather than the user project; Keep canonical skills and commands as the single source of truth, and never generate copies under .pi/ |
| Pi | Adapter-backed | Pi package manifest; canonical ECC skills (skills/); canonical ECC commands as prompt templates (commands/); canonical ECC engineering rules (rules/common/) injected into the system prompt; session lifecycle hook adapter; /ecc-doctor diagnostics command; canonical ECC agents (agents/) portable via scripts/agent-convert.js (claude -> pi) | Agent chains, approval prompts, and persistent todos require companion Pi packages and are not part of this adapter (individual agent definitions are portable via scripts/agent-convert.js); Pi core has no MCP surface, though ECC MCP configs load verbatim through the community pi-mcp-adapter package, which ECC neither installs nor depends on | `pi install git:github.com/affaan-m/ECC`; `pi install /path/to/ECC` from a local checkout | `node tests/pi/pi-package-manifest.test.js`; `node tests/pi/pi-extension-adapter.test.js`; `node tests/pi/agent-convert.test.js`; `npm run harness:adapters -- --check` | Pi extensions execute with full user permissions, and hooks run without a shell and resolve from the installed package rather than the user project; Keep canonical skills and commands as the single source of truth, and never generate copies under .pi/ |
| Cursor | Adapter-backed | Cursor rules; project-local skills; hook adapter; shared scripts | Cursor hook events and rule loading differ from Claude Code | `./install.sh --profile minimal --target cursor` | `node tests/lib/install-targets.test.js`; `npm run harness:audit -- --format json` | Cursor adapters must preserve existing project rules and avoid silent overwrite. |
| Gemini | Instruction-backed | Gemini project-local instructions; shared skills; rules; compatibility docs | No full ECC hook parity; ecosystem ports must document drift from upstream ECC | `./install.sh --profile minimal --target gemini` | `node tests/lib/install-targets.test.js` | Treat Gemini ports as ecosystem adapters until validated end to end inside Gemini CLI. |
| Zed | Adapter-backed | Zed project settings; flattened project rules; shared skills; commands; agents | Zed external agents and native Agent Panel permissions are not Claude hooks | `./install.sh --profile minimal --target zed` | `node tests/lib/install-targets.test.js`; `npm run harness:audit -- --format json` | Keep project settings conservative and do not copy BYOK/OpenRouter secrets into `.zed/`. |
+1
View File
@@ -109,6 +109,7 @@
"plugins/ecc/",
"rules/",
"schemas/",
"scripts/agent-convert.js",
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
+59
View File
@@ -0,0 +1,59 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "ecc.agent-ir.v1",
"title": "ECC Agent IR",
"description": "Canonical harness-neutral representation of an ECC agent, produced from a source harness definition (e.g. agents/*.md Claude frontmatter). Emitters consume this IR and produce native agent/subagent definitions per harness. The `tools` array holds source tool names verbatim (lossless); each emitter maps them through its own tool table.",
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"id",
"name",
"description",
"tools",
"body"
],
"properties": {
"schema": {
"const": "ecc.agent-ir.v1"
},
"id": {
"type": "string",
"description": "Stable canonical id; the source filename stem (kebab-case).",
"pattern": "^[a-z0-9-]+$"
},
"name": {
"type": "string",
"description": "Agent name from the source frontmatter."
},
"description": {
"type": "string",
"description": "Agent description from the source frontmatter."
},
"model": {
"type": "string",
"description": "Source model tier, when present. Claude aliases only in v1.",
"enum": ["haiku", "sonnet", "opus"]
},
"tools": {
"type": "array",
"description": "Source tool names verbatim (Claude tool identifiers such as Read, Grep, Glob, Bash, Edit, Write, WebSearch, WebFetch, mcp__*).",
"items": {
"type": "string"
}
},
"color": {
"type": "string",
"description": "Optional source-only metadata (Claude color tag); emitters may ignore it."
},
"body": {
"type": "string",
"description": "The agent system prompt / markdown body after the frontmatter."
},
"frontmatter": {
"type": "object",
"description": "Lossless raw source frontmatter for round-trip fidelity and forward compatibility.",
"additionalProperties": true
}
}
}
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env node
'use strict';
/**
* ECC Agent IR — converter CLI.
*
* Converts ECC's canonical agents from one harness format to another. v1
* supports a single direction: `claude -> pi`. Future emitters (cursor,
* opencode, gemini, codex, zed) plug into the same IR.
*
* Usage:
* node scripts/agent-convert.js --check
* node scripts/agent-convert.js --from claude --to pi [--json]
* node scripts/agent-convert.js --from claude --to pi --out <dir> [--dry-run]
*
* When `--json` is set, stdout carries only the machine-readable summary;
* progress lines (wrote / would-write) go to stderr so automation can parse
* stdout as JSON.
*/
const fs = require('fs');
const path = require('path');
const { parseAllAgents } = require('./lib/agent-ir');
const { emitAllPiAgents } = require('./lib/agent-emit-pi');
const SUPPORTED = { claude: ['pi'] };
const EMITTERS = {
pi: emitAllPiAgents,
};
function requireValue(argv, index, flag) {
const value = argv[index + 1];
if (value === undefined || value === '' || value.startsWith('--')) {
throw new Error(`missing value for ${flag}`);
}
return value;
}
function parseArgs(argv) {
let args = { from: 'claude', to: 'pi', json: false, dryRun: false, check: false, out: null };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--from') {
args = { ...args, from: requireValue(argv, i, '--from') };
i++;
} else if (a === '--to') {
args = { ...args, to: requireValue(argv, i, '--to') };
i++;
} else if (a === '--json') {
args = { ...args, json: true };
} else if (a === '--dry-run') {
args = { ...args, dryRun: true };
} else if (a === '--check') {
args = { ...args, check: true };
} else if (a === '--out') {
args = { ...args, out: requireValue(argv, i, '--out') };
i++;
} else if (a === '--help' || a === '-h') {
args = { ...args, help: true };
} else {
throw new Error(`unknown argument: ${a}`);
}
}
return args;
}
function showHelp() {
console.log(`ECC Agent IR converter (v1)
Usage:
node scripts/agent-convert.js --check
Parse and validate every agent under agents/ against the IR schema.
node scripts/agent-convert.js --from claude --to pi [--json]
Emit all agents as Pi subagent definitions and print a summary.
node scripts/agent-convert.js --from claude --to pi --out <dir> [--dry-run]
Write emitted Pi agent files (<dir>/<id>.md). --dry-run prints only.
Options:
--from <claude> Source harness (v1: claude only)
--to <pi> Target harness (v1: pi only)
--json Machine-readable summary on stdout
--out <dir> Write emitted files to a directory
--dry-run With --out, report writes without writing
--check Validate parsing only (no emit)
`);
}
function main() {
let args;
try {
args = parseArgs(process.argv.slice(2));
} catch (err) {
console.error(`error: ${err.message}`);
process.exit(2);
}
if (args.help) {
showHelp();
process.exit(0);
}
if (!SUPPORTED[args.from] || !SUPPORTED[args.from].includes(args.to)) {
console.error(`error: unsupported conversion ${args.from} -> ${args.to}`);
console.error(`supported: ${Object.entries(SUPPORTED).map(([f, ts]) => `${f} -> ${ts.join(',')}`).join('; ')}`);
process.exit(2);
}
let irs;
try {
irs = parseAllAgents();
} catch (err) {
console.error(`error: ${err.message}`);
process.exit(1);
}
if (args.check) {
if (args.json) {
console.log(JSON.stringify({ schema: 'ecc.agent-ir.v1', checked: irs.length, ok: true }, null, 2));
} else {
console.log(`OK: ${irs.length} agents parsed and validated against ecc.agent-ir.v1`);
}
process.exit(0);
}
const { results, warnings, notes, skipped } = EMITTERS[args.to](irs);
// Progress lines must never corrupt machine-readable stdout.
const progress = msg => {
if (args.json) process.stderr.write(`${msg}\n`);
else console.log(msg);
};
if (args.out) {
const dir = path.resolve(args.out);
if (!args.dryRun) fs.mkdirSync(dir, { recursive: true });
for (const r of results) {
const target = path.join(dir, `${r.id}.md`);
if (args.dryRun) {
progress(`[dry-run] would write ${target}`);
} else {
fs.writeFileSync(target, r.markdown);
progress(`wrote ${target}`);
}
}
}
const summary = {
schema: 'ecc.agent-ir.v1',
from: args.from,
to: args.to,
converted: results.length,
warnings: warnings.length,
warningsDetail: warnings,
skipped: skipped ? skipped.length : 0,
notes,
};
if (args.json) {
console.log(JSON.stringify(summary, null, 2));
} else {
console.log(`converted ${results.length} agents (${args.from} -> ${args.to})`);
for (const note of notes) {
console.log(` ${note}`);
}
if (warnings.length) {
console.log(`\n${warnings.length} actionable warning(s):`);
for (const w of warnings) console.log(` - ${w}`);
}
}
}
main();
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env node
'use strict';
/**
* ECC Agent IR — Pi emitter.
*
* Turns one IR object into a native Pi subagent definition (markdown file with
* Pi frontmatter). The frontmatter contract is verified against
* `@tintinweb/pi-subagents@0.19.0`: fields are `name` (the dispatch id),
* `description`, `tools` (a comma-separated strict child allowlist), and
* `prompt_mode` (`replace` keeps the body as the complete system prompt). The
* prompt lives in the document body.
*
* v1 behavior:
* - The source model tier is preserved as a YAML comment
* (`# source model tier: opus`) rather than a `model:` field, because
* Claude tiers are not valid Pi model ids. Pi's default child model
* applies; the tier is lossless in the IR and visible in the emitted file.
* - Unmapped tools (WebSearch, WebFetch, mcp__*) are never silently dropped;
* they surface in per-agent `warnings`.
* - Specialist isolation disables inherited extensions and skills, so the
* builtin allowlist cannot silently inherit configured extension tools.
* - The allowlist is always written. An empty allowlist is emitted as
* `tools: none`, never blank, because the companion's `csvList` treats a
* blank/missing `tools:` as *all* built-ins — which would escalate a
* fully-unmapped agent to every tool.
*/
const { mapToolToPi } = require('./agent-tool-map');
// Pi core built-in agent names (case-sensitive). ECC agent names are kebab-case
// and do not collide today; warn if a future one ever does.
const RESERVED_AGENT_NAMES = new Set(['general-purpose', 'Explore', 'Plan']);
/**
* Emit a Pi agent definition for one IR object.
*
* @param {object} ir
* @returns {{ markdown: string, warnings: string[], tools: string[] }}
*/
function emitPiAgent(ir) {
const warnings = [];
const seen = new Set();
const tools = [];
for (const sourceTool of ir.tools) {
const mapped = mapToolToPi(sourceTool);
if (mapped.tool && !seen.has(mapped.tool)) {
seen.add(mapped.tool);
tools.push(mapped.tool);
}
if (mapped.note) {
warnings.push(`${ir.id}: ${mapped.note}`);
}
}
if (RESERVED_AGENT_NAMES.has(ir.name)) {
warnings.push(`${ir.id}: name '${ir.name}' collides with a Pi built-in agent; rename to avoid shadowing`);
}
// Never emit a blank allowlist: `none` is the explicit "no tools" sentinel.
const toolsValue = tools.length > 0 ? tools.join(', ') : 'none';
const frontmatter = [
'---',
...(ir.model ? [`# source model tier: ${ir.model}`] : []),
`name: ${yamlScalar(ir.name)}`,
`description: ${yamlScalar(ir.description)}`,
`tools: ${toolsValue}`,
'prompt_mode: replace',
'isolated: true',
'extensions: false',
'skills: false',
'---',
];
const body = (ir.body || '').replace(/^\n+/, '').trimEnd();
const markdown = frontmatter.join('\n') + '\n\n' + body + '\n';
return { markdown, warnings, tools };
}
/** Emit a single-line YAML scalar, quoted only when needed. */
function yamlScalar(value) {
const s = String(value);
// Quote when it contains a leading/trailing space, a colon followed by a
// space, a comment-starting hash, a leading special char, or a newline.
if (/^\s|\s$|: |\n|\s#|^[-?*&|>#@`"'\][{}!,]/.test(s)) {
return JSON.stringify(s);
}
return s;
}
/**
* Emit the full set of Pi agents, sorted by id for determinism, plus notes on
* the deliberate (documented) lossy conversions.
*
* @param {object[]} irs
* @returns {{ results: object[], warnings: string[], notes: string[] }}
*/
function emitAllPiAgents(irs) {
const results = [];
const warnings = [];
let modelTiers = {};
let unsupported = 0;
for (const ir of [...irs].sort((a, b) => a.id.localeCompare(b.id))) {
const { markdown, warnings: w, tools } = emitPiAgent(ir);
results.push({ id: ir.id, name: ir.name, tools, markdown });
warnings.push(...w);
if (ir.model) {
modelTiers = { ...modelTiers, [ir.model]: (modelTiers[ir.model] || 0) + 1 };
}
unsupported += ir.tools.filter(t => mapToolToPi(t).unsupported).length;
}
const notes = [];
if (Object.keys(modelTiers).length) {
const tiers = Object.entries(modelTiers).map(([t, n]) => `${t} x${n}`).join(', ');
notes.push(`model tiers preserved as comments (${tiers}) — Pi uses its default child model`);
}
if (unsupported) {
notes.push(`unmapped tools: ${unsupported} (WebSearch/WebFetch/mcp__* are not Pi built-ins) — see warnings`);
}
return { results, warnings, notes };
}
module.exports = {
RESERVED_AGENT_NAMES,
emitPiAgent,
emitAllPiAgents,
yamlScalar,
};
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env node
'use strict';
/**
* ECC Agent IR — parser.
*
* Reads ECC's canonical agent files (Claude frontmatter markdown) and emits
* harness-neutral IR objects validated against `schemas/agent.schema.json`.
*
* Design constraints:
* - Deterministic: stable key order, no ambient state.
* - Lossless: the raw frontmatter is retained so emitters can round-trip.
* - Strict: invalid model tiers and non-string tool entries are rejected,
* never silently coerced or dropped.
* - No shell, no network, no new dependencies (uses `js-yaml` + `ajv`,
* which are already in `package.json`).
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const Ajv = require('ajv');
const AGENTS_DIR = path.join(__dirname, '..', '..', 'agents');
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schemas', 'agent.schema.json');
const IR_SCHEMA_CONST = 'ecc.agent-ir.v1';
const VALID_MODELS = ['haiku', 'sonnet', 'opus'];
let _ajv = null;
function ajv() {
if (!_ajv) {
_ajv = new Ajv({ allErrors: true, strict: false });
const schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
_ajv.addSchema(schema, IR_SCHEMA_CONST);
}
return _ajv;
}
/** Split frontmatter + body, tolerating UTF-8 BOM and LF/CRLF. */
function splitFrontmatter(content) {
const clean = content.replace(/^\uFEFF/, '');
const match = clean.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) {
return { frontmatter: null, body: null };
}
return { frontmatter: match[1], body: match[2] || '' };
}
/**
* Normalize a frontmatter `tools` value to a string[].
* Claude agents use comma-separated strings (e.g. `Read, Grep, Glob`).
* Non-string scalars and non-string array entries are rejected.
*/
function normalizeTools(value, filename) {
if (value === null || value === undefined) return [];
const entries = Array.isArray(value)
? value
: typeof value === 'string'
? value.split(',')
: null;
if (entries === null) {
throw new Error(`${filename}: tools must be a comma-separated string or a list of strings`);
}
const tools = [];
for (const entry of entries) {
if (typeof entry !== 'string') {
throw new Error(`${filename}: tools must contain only strings`);
}
const trimmed = entry.trim();
if (trimmed) tools.push(trimmed);
}
return tools;
}
/** Parse one agent file into an IR object (throws on malformed input). */
function parseAgentFile(filePath) {
const filename = path.basename(filePath);
const id = filename.replace(/\.md$/, '');
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (err) {
throw new Error(`${filename}: cannot read: ${err.message}`);
}
const { frontmatter, body } = splitFrontmatter(content);
if (frontmatter === null) {
throw new Error(`${filename}: missing frontmatter (expected leading --- block)`);
}
let raw;
try {
raw = yaml.load(frontmatter);
} catch (err) {
throw new Error(`${filename}: invalid YAML frontmatter: ${err.message}`);
}
if (!raw || typeof raw !== 'object') {
throw new Error(`${filename}: frontmatter must be a YAML mapping`);
}
const model = raw.model;
if (model !== undefined && !VALID_MODELS.includes(model)) {
throw new Error(`${filename}: invalid model tier '${model}' (expected haiku, sonnet, or opus)`);
}
const ir = {
schema: IR_SCHEMA_CONST,
id,
name: typeof raw.name === 'string' ? raw.name : id,
description: typeof raw.description === 'string' ? raw.description : '',
tools: normalizeTools(raw.tools, filename),
body,
frontmatter: raw,
...(model !== undefined ? { model } : {}),
...(raw.color !== undefined ? { color: String(raw.color) } : {}),
};
validateIr(ir, filename);
return ir;
}
function validateIr(ir, filename) {
const validate = ajv().getSchema(IR_SCHEMA_CONST);
const ok = validate(ir);
if (!ok) {
const detail = (validate.errors || []).map(e => `${e.instancePath} ${e.message}`).join('; ');
throw new Error(`${filename}: IR validation failed: ${detail}`);
}
}
/** Parse every agent under `agents/`, sorted for determinism. */
function parseAllAgents(dir = AGENTS_DIR) {
if (!fs.existsSync(dir)) {
throw new Error(`agents directory not found: ${dir}`);
}
const files = fs
.readdirSync(dir)
.filter(f => f.endsWith('.md'))
.sort();
return files.map(f => parseAgentFile(path.join(dir, f)));
}
module.exports = {
AGENTS_DIR,
SCHEMA_PATH,
IR_SCHEMA_CONST,
VALID_MODELS,
parseAgentFile,
parseAllAgents,
splitFrontmatter,
normalizeTools,
};
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env node
'use strict';
/**
* ECC Agent IR — tool and model mapping tables.
*
* The IR stores source (Claude) tool names verbatim. Each emitter maps those
* names to its own harness primitives through a table. Unmapped tools are
* never silently dropped: they are surfaced as `unsupported` so the emitter
* can warn and the conformance test can assert the mapping is complete.
*
* Pi tool names are verified against the installed
* `@earendil-works/pi-coding-agent` `dist/core/tools/index.d.ts`:
* read | bash | powershell | edit | write | grep | find | ls
* (`powershell` is Windows-only and not emitted here.)
*
* Security invariant: a Claude tool must never map to a Pi tool with MORE
* authority than the source. Read-only Claude tools (Read, Grep, Glob) map to
* read-only Pi tools (read, grep, find), never to `bash`.
*/
/** Claude tool name -> Pi tool name. */
const CLAUDE_TO_PI_TOOLS = Object.freeze({
Read: 'read',
Grep: 'grep',
Glob: 'find',
Bash: 'bash',
Edit: 'edit',
Write: 'write',
// WebSearch and WebFetch are intentionally NOT mapped: Pi exposes no
// `web_search` or `fetch_content` built-in. They fall through to the
// "unmapped tool" warning rather than being guessed.
});
/**
* Map one Claude tool name to a Pi tool allowlist entry.
*
* @param {string} claudeTool
* @returns {{ tool: string|null, unsupported: boolean, note?: string }}
* `tool` is the Pi tool name, or null when unsupported. `unsupported` is
* true only for source tools that have no Pi built-in equivalent in v1.
*/
function mapToolToPi(claudeTool) {
const name = String(claudeTool).trim();
if (Object.prototype.hasOwnProperty.call(CLAUDE_TO_PI_TOOLS, name)) {
return { tool: CLAUDE_TO_PI_TOOLS[name], unsupported: false };
}
if (name.startsWith('mcp__')) {
return {
tool: null,
unsupported: true,
note: `MCP tool ${name} not a Pi built-in (configure the MCP server explicitly)`,
};
}
return { tool: null, unsupported: true, note: `unmapped tool: ${name} (not a Pi built-in)` };
}
/**
* Claude model tiers. Pi resolves its own child model; v1 does not emit a
* `model` field, so Pi's default applies. The tier is retained in the IR for
* lossless round-trips and for emitters that do have a model table.
*/
const CLAUDE_MODEL_TIERS = Object.freeze(['haiku', 'sonnet', 'opus']);
module.exports = {
CLAUDE_TO_PI_TOOLS,
CLAUDE_MODEL_TIERS,
mapToolToPi,
};
+6 -2
View File
@@ -133,27 +133,31 @@ const ADAPTER_RECORDS = Object.freeze([
'canonical ECC engineering rules (rules/common/) injected into the system prompt',
'session lifecycle hook adapter',
'/ecc-doctor diagnostics command',
'canonical ECC agents (agents/) portable via scripts/agent-convert.js (claude -> pi)',
],
unsupported_surfaces: [
'Subagents, chains, approval prompts, and persistent todos require companion Pi packages and are not part of this adapter',
'Agent chains, approval prompts, and persistent todos require companion Pi packages and are not part of this adapter (individual agent definitions are portable via scripts/agent-convert.js)',
'Pi core has no MCP surface, though ECC MCP configs load verbatim through the community pi-mcp-adapter package, which ECC neither installs nor depends on',
],
install_or_onramp: ['`pi install git:github.com/affaan-m/ECC`', '`pi install /path/to/ECC` from a local checkout'],
verification_commands: [
'`node tests/pi/pi-package-manifest.test.js`',
'`node tests/pi/pi-extension-adapter.test.js`',
'`node tests/pi/agent-convert.test.js`',
'`npm run harness:adapters -- --check`',
],
risk_notes: [
'Pi extensions execute with full user permissions, and hooks run without a shell and resolve from the installed package rather than the user project',
'Keep canonical skills and commands as the single source of truth, and never generate copies under .pi/',
],
last_verified_at: '2026-08-10',
last_verified_at: '2026-09-08',
owner: 'ECC maintainers',
source_docs: [
'.pi/extensions/index.ts',
'.pi/README.md',
'package.json',
'schemas/agent.schema.json',
'scripts/agent-convert.js',
],
},
{
+122
View File
@@ -0,0 +1,122 @@
const AGENT_COUNT = require('fs').readdirSync(require('path').join(__dirname,'../../agents')).filter(f => f.endsWith('.md')).length;
/**
* Tests for the ECC Agent IR converter CLI (scripts/agent-convert.js).
*
* Verifies the CLI contract: `--check`, `--json`, `--out` (write and
* side-effect-free `--dry-run`), and argument/exit-code behavior.
*/
const assert = require("assert")
const fs = require("fs")
const os = require("os")
const path = require("path")
const { execFileSync } = require("child_process")
const CLI = path.join(__dirname, "..", "..", "scripts", "agent-convert.js")
const NODE = process.execPath
function runTest(name, fn) {
try {
fn()
console.log(` ✓ ${name}`)
return true
} catch (error) {
console.log(` ✗ ${name}`)
console.error(` ${error.message}`)
return false
}
}
function runCli(args) {
return execFileSync(NODE, [CLI, ...args], { encoding: "utf8" })
}
function tmpdir() {
return fs.mkdtempSync(path.join(os.tmpdir(), "ecc-agent-convert-"))
}
function main() {
let passed = 0
let failed = 0
const tests = [
["--check exits 0 and reports AGENT_COUNT agents", () => {
const out = runCli(["--check"])
assert.match(out, new RegExp(`OK: ${AGENT_COUNT} agents parsed and validated`))
}],
["--json emits a machine-readable summary", () => {
const out = runCli(["--from", "claude", "--to", "pi", "--json"])
const summary = JSON.parse(out)
assert.strictEqual(summary.schema, "ecc.agent-ir.v1")
assert.strictEqual(summary.from, "claude")
assert.strictEqual(summary.to, "pi")
assert.strictEqual(summary.converted, AGENT_COUNT)
assert.ok(Array.isArray(summary.warningsDetail))
}],
["--out writes AGENT_COUNT files", () => {
const dir = tmpdir()
runCli(["--from", "claude", "--to", "pi", "--out", dir])
const files = fs.readdirSync(dir).filter(f => f.endsWith(".md"))
assert.strictEqual(files.length, AGENT_COUNT, "expected AGENT_COUNT emitted files")
const planner = fs.readFileSync(path.join(dir, "planner.md"), "utf8")
assert.match(planner, /name: planner\n/);
assert.match(planner, /prompt_mode: replace/);
}],
["--out --dry-run writes nothing", () => {
const dir = tmpdir()
const out = runCli(["--from", "claude", "--to", "pi", "--out", dir, "--dry-run"])
assert.match(out, /\[dry-run\] would write/)
assert.strictEqual(fs.readdirSync(dir).length, 0, "dry-run must not create files")
}],
["unsupported conversion exits non-zero", () => {
assert.throws(() => runCli(["--from", "claude", "--to", "zed"]), /unsupported conversion/);
}],
["unknown flag exits non-zero", () => {
assert.throws(() => runCli(["--bogus"]), /unknown argument/)
}],
["--json --out keeps stdout parseable JSON", () => {
const dir = tmpdir()
const out = runCli(["--from", "claude", "--to", "pi", "--out", dir, "--json"])
const summary = JSON.parse(out)
assert.strictEqual(summary.converted, AGENT_COUNT)
assert.strictEqual(summary.schema, "ecc.agent-ir.v1")
}],
["missing value for --out exits non-zero", () => {
assert.throws(() => runCli(["--from", "claude", "--to", "pi", "--out"]), /missing value for --out/)
}],
["--check --json emits JSON", () => {
const out = runCli(["--check", "--json"])
const summary = JSON.parse(out)
assert.strictEqual(summary.ok, true)
assert.strictEqual(summary.checked, AGENT_COUNT)
}],
["--out followed by a flag is rejected (no unintended write)", () => {
const dir = tmpdir()
assert.throws(() => runCli(["--from", "claude", "--to", "pi", "--out", "--dry-run"]), /missing value for --out/)
assert.strictEqual(fs.readdirSync(dir).length, 0, "must not write anything")
}],
]
for (const [name, fn] of tests) {
if (runTest(name, fn)) {
passed += 1
} else {
failed += 1
}
}
console.log(`\nPassed: ${passed}`)
console.log(`Failed: ${failed}`)
process.exit(failed > 0 ? 1 : 0)
}
main()
+176
View File
@@ -0,0 +1,176 @@
const AGENT_COUNT = require('fs').readdirSync(require('path').join(__dirname,'../../agents')).filter(f => f.endsWith('.md')).length;
/**
* Tests for the ECC Agent IR → Pi emitter (scripts/lib/agent-emit-pi.js).
*
* Verifies that every IR object becomes a valid Pi subagent definition against
* the verified `@tintinweb/pi-subagents@0.19.0` surface: `name` / `description`
* / `tools` (comma-separated allowlist) / `prompt_mode`, with the prompt in the
* body. Also enforces the permission boundary and the empty-allowlist guard.
*/
const assert = require("assert")
const yaml = require("js-yaml")
const { parseAllAgents } = require("../../scripts/lib/agent-ir")
const { emitPiAgent, emitAllPiAgents, RESERVED_AGENT_NAMES } = require("../../scripts/lib/agent-emit-pi")
const { CLAUDE_TO_PI_TOOLS, mapToolToPi } = require("../../scripts/lib/agent-tool-map")
const VALID_PI_TOOLS = new Set(Object.values(CLAUDE_TO_PI_TOOLS))
function runTest(name, fn) {
try {
fn()
console.log(` ✓ ${name}`)
return true
} catch (error) {
console.log(` ✗ ${name}`)
console.error(` ${error.message}`)
return false
}
}
function parseEmittedFrontmatter(markdown) {
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/)
assert.ok(match, "emitted markdown must start with a frontmatter block")
const out = {}
for (const line of match[1].split(/\r?\n/)) {
const idx = line.indexOf(":")
if (idx <= 0) continue
const key = line.slice(0, idx).trim()
out[key] = line.slice(idx + 1).trim()
}
return out
}
function main() {
let passed = 0
let failed = 0
let irs
let results
let warnings
try {
irs = parseAllAgents()
;({ results, warnings } = emitAllPiAgents(irs))
} catch (error) {
console.log(` ✗ setup failed: ${error.message}`)
console.log("\nPassed: 0")
console.log("Failed: 1")
process.exit(1)
}
const byId = new Map(irs.map(ir => [ir.id, ir]))
const tests = [
["emits all AGENT_COUNT agents", () => {
assert.strictEqual(results.length, AGENT_COUNT)
}],
["frontmatter uses the verified prompt_mode contract (no package, no systemPromptMode)", () => {
for (const r of results) {
const fm = parseEmittedFrontmatter(r.markdown)
assert.strictEqual(fm.prompt_mode, "replace", `${r.id}: prompt_mode must be replace`)
assert.strictEqual(fm.systemPromptMode, undefined, `${r.id}: systemPromptMode must not be emitted`)
assert.strictEqual(fm.package, undefined, `${r.id}: package must not be emitted (name is the dispatch id)`)
assert.strictEqual(fm.model, undefined, `${r.id}: model must be omitted (Pi default applies)`)
assert.strictEqual(fm.name, r.name, `${r.id}: name mismatch`)
assert.ok(fm.description.length > 0, `${r.id}: empty description`)
}
}],
["tool allowlist contains only verified Pi built-ins", () => {
for (const r of results) {
for (const tool of r.tools) {
assert.ok(VALID_PI_TOOLS.has(tool), `${r.id}: invalid Pi tool '${tool}'`)
}
}
}],
["no Claude tool name leaks into the Pi allowlist", () => {
const claudeNames = new Set(["Read", "Grep", "Glob", "Bash", "Edit", "Write", "WebSearch", "WebFetch"])
for (const r of results) {
for (const tool of r.tools) {
assert.ok(!claudeNames.has(tool), `${r.id}: leaked Claude tool '${tool}'`)
}
}
}],
["read-only agents never gain bash", () => {
for (const r of results) {
const source = byId.get(r.id)
if (!source.tools.includes("Bash")) {
assert.ok(!r.tools.includes("bash"), `${r.id}: read-only agent must not emit 'bash'`)
}
}
}],
["mcp__* tools are never mapped to a tool", () => {
assert.ok(!results.some(r => r.tools.includes("mcp")), "no 'mcp' tool should be emitted")
assert.ok(warnings.some(w => w.includes("docs-lookup") && w.includes("MCP tool")), "docs-lookup MCP must warn")
}],
["every unsupported source tool is warned, never silently dropped", () => {
const unsupportedCount = irs.flatMap(ir => ir.tools).filter(t => mapToolToPi(t).unsupported).length
const warnedCount = warnings.filter(w => /not a Pi built-in|unmapped tool/.test(w)).length
assert.strictEqual(warnedCount, unsupportedCount, "each unsupported source tool must warn exactly once")
assert.ok(unsupportedCount > 0, "expected WebSearch/WebFetch/mcp__* to be unsupported")
}],
["empty allowlist is emitted as tools: none (never blank, never escalates)", () => {
const noTools = { id: "x", name: "x", description: "x", tools: ["WebSearch"], body: "hi" }
const { markdown, tools } = emitPiAgent(noTools)
assert.deepStrictEqual(tools, [], "WebSearch-only agent must have an empty allowlist")
assert.match(markdown, /^tools: none$/m, "empty allowlist must be emitted as 'tools: none'")
}],
["reserved built-in agent names are warned", () => {
const colliding = { id: "general-purpose", name: "general-purpose", description: "x", tools: ["Read"], body: "hi" }
const { warnings: w } = emitPiAgent(colliding)
assert.ok(w.some(x => x.includes("collides with a Pi built-in agent")), "built-in name collision must warn")
assert.ok(RESERVED_AGENT_NAMES.has("general-purpose"), "reserved set is pinned")
}],
["body is preserved losslessly", () => {
for (const ir of irs) {
const emitted = results.find(r => r.id === ir.id)
const body = ir.body.replace(/^\n+/, "").trimEnd()
assert.ok(emitted.markdown.includes(body), `${ir.id}: body not preserved`)
}
}],
["planner maps to read, grep, find", () => {
const planner = results.find(r => r.id === "planner")
assert.deepStrictEqual(planner.tools, ["read", "grep", "find"])
}],
["emission is deterministic", () => {
const a = emitAllPiAgents(irs).results.map(r => r.markdown).join("\n")
const b = emitAllPiAgents(irs).results.map(r => r.markdown).join("\n")
assert.strictEqual(a, b)
}],
["emitted frontmatter is valid YAML and round-trips key fields", () => {
for (const r of results) {
const match = r.markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/)
const fm = yaml.load(match[1])
assert.strictEqual(fm.name, r.name, `${r.id}: name round-trip`)
assert.strictEqual(fm.prompt_mode, "replace", `${r.id}: prompt_mode round-trip`)
assert.deepStrictEqual(String(fm.tools).split(", "), r.tools, `${r.id}: tools round-trip`)
}
}],
]
for (const [name, fn] of tests) {
if (runTest(name, fn)) {
passed += 1
} else {
failed += 1
}
}
console.log(`\nPassed: ${passed}`)
console.log(`Failed: ${failed}`)
process.exit(failed > 0 ? 1 : 0)
}
main()
+130
View File
@@ -0,0 +1,130 @@
const AGENT_COUNT = require('fs').readdirSync(require('path').join(__dirname,'../../agents')).filter(f => f.endsWith('.md')).length;
/**
* Tests for the ECC Agent IR parser (scripts/lib/agent-ir.js).
*
* Verifies the canonical-agent parse path: every Claude-format agent under
* agents/ must parse into a valid ecc.agent-ir.v1 object, deterministically,
* with lossless frontmatter retention.
*/
const assert = require("assert")
const fs = require("fs")
const path = require("path")
const { parseAgentFile, parseAllAgents, IR_SCHEMA_CONST } = require("../../scripts/lib/agent-ir")
const REPO_ROOT = path.join(__dirname, "..", "..")
const AGENTS_DIR = path.join(REPO_ROOT, "agents")
function runTest(name, fn) {
try {
fn()
console.log(` ✓ ${name}`)
return true
} catch (error) {
console.log(` ✗ ${name}`)
console.error(` ${error.message}`)
return false
}
}
function main() {
let passed = 0
let failed = 0
const tests = [
["parses all AGENT_COUNT agents", () => {
const agents = parseAllAgents()
assert.strictEqual(agents.length, AGENT_COUNT, "expected AGENT_COUNT agents")
for (const ir of agents) {
assert.strictEqual(ir.schema, IR_SCHEMA_CONST, `${ir.id}: wrong schema const`)
assert.ok(/^[a-z0-9-]+$/.test(ir.id), `${ir.id}: id must be kebab-case`)
assert.ok(ir.name.length > 0, `${ir.id}: empty name`)
assert.ok(ir.description.length > 0, `${ir.id}: empty description`)
assert.ok(Array.isArray(ir.tools) && ir.tools.length > 0, `${ir.id}: empty tools`)
assert.ok(ir.body.length > 0, `${ir.id}: empty body`)
}
}],
["is deterministic (stable key order + sorted ids)", () => {
const a = JSON.stringify(parseAllAgents())
const b = JSON.stringify(parseAllAgents())
assert.strictEqual(a, b, "parseAllAgents must be deterministic")
}],
["retains current model tier distribution (61 sonnet / 6 haiku / 4 opus)", () => {
const agents = parseAllAgents()
for (const ir of agents) {
assert.ok(ir.model, `${ir.id}: missing model tier`)
}
const tally = agents.reduce(
(acc, ir) => ({ ...acc, [ir.model]: (acc[ir.model] || 0) + 1 }),
{}
)
assert.strictEqual(tally.sonnet, 61, "sonnet count")
assert.strictEqual(tally.haiku, 6, "haiku count")
assert.strictEqual(tally.opus, 4, "opus count")
}],
["retains source-only color metadata", () => {
const agents = parseAllAgents()
const withColor = agents.filter(ir => ir.color)
assert.strictEqual(withColor.length, 5, "exactly 5 agents carry color")
for (const ir of withColor) {
assert.ok(typeof ir.color === "string" && ir.color.length > 0, `${ir.id}: bad color`)
}
}],
["parses a known agent losslessly", () => {
const ir = parseAgentFile(path.join(AGENTS_DIR, "planner.md"))
assert.strictEqual(ir.id, "planner")
assert.strictEqual(ir.name, "planner")
assert.strictEqual(ir.model, "opus")
assert.deepStrictEqual(ir.tools, ["Read", "Grep", "Glob"])
assert.ok(ir.frontmatter, "raw frontmatter must be retained")
assert.strictEqual(ir.frontmatter.name, "planner", "frontmatter.name lossless")
assert.ok(ir.body.includes("You are an expert planning specialist"), "body preserved")
}],
["throws on a missing frontmatter block", () => {
const badPath = path.join(__dirname, "..", "fixtures", "agent-no-frontmatter.md")
fs.mkdirSync(path.dirname(badPath), { recursive: true })
fs.writeFileSync(badPath, "# no frontmatter here\n")
try {
assert.throws(() => parseAgentFile(badPath), /missing frontmatter/)
} finally {
fs.rmSync(badPath, { force: true })
}
}],
["throws on an invalid model tier instead of silently dropping it", () => {
const badPath = path.join(__dirname, "..", "fixtures", "agent-invalid-model.md")
fs.mkdirSync(path.dirname(badPath), { recursive: true })
fs.writeFileSync(badPath, "---\nname: x\ndescription: x\ntools: Read\nmodel: sonnet4\n---\n\nbody\n")
try {
assert.throws(() => parseAgentFile(badPath), /invalid model tier 'sonnet4'/)
} finally {
fs.rmSync(badPath, { force: true })
}
}],
["throws when the agents directory is absent", () => {
const missingDir = path.join(__dirname, "..", "fixtures", "agents-does-not-exist")
assert.throws(() => parseAllAgents(missingDir), /agents directory not found/)
}],
]
for (const [name, fn] of tests) {
if (runTest(name, fn)) {
passed += 1
} else {
failed += 1
}
}
console.log(`\nPassed: ${passed}`)
console.log(`Failed: ${failed}`)
process.exit(failed > 0 ? 1 : 0)
}
main()
+58
View File
@@ -319,6 +319,22 @@ function isDisabledByEnvMirror(value) {
return typeof value === "string" && DISABLED_VALUES_MIRROR.has(value.trim().toLowerCase())
}
/**
* Mirror of the adapter's `describeAgentPortability` (same three states, same
* exact strings). The source-text assertions in the matching test pin the real
* function's wording out of `.pi/extensions/index.ts` so this copy cannot drift.
*/
function describeAgentPortabilityMirror(agentCount, portability) {
if (agentCount === 0) return "NOT FOUND"
if (portability.status === "validated") {
return `${portability.count} agent(s) source schema validated; Pi conversion available via scripts/agent-convert.js`
}
if (portability.status === "failed") {
return `${agentCount} agent(s) present but validation FAILED: ${portability.reason}`
}
return `${agentCount} agent(s) present (unvalidated — ${portability.reason})`
}
/** Run the Pi adapter regression suite. */
async function main() {
console.log("\n=== Testing .pi/extensions/index.ts (Pi thin adapter) ===\n")
@@ -1495,6 +1511,48 @@ async function main() {
)
}],
["/ecc-doctor validates agent portability instead of trusting file existence (source contract)", () => {
const start = extensionSource.indexOf("function validateAgentPortability")
assert.ok(start !== -1, "expected .pi/extensions/index.ts to define validateAgentPortability")
const end = extensionSource.indexOf("\nfunction ", start + 1)
const src = end === -1 ? extensionSource.slice(start) : extensionSource.slice(start, end)
assert.ok(src.includes("execFileSync("), "must use execFileSync (no shell) to run the check")
assert.ok(src.includes('"--check"'), "must run agent-convert.js in --check mode")
assert.ok(/timeout:\s*AGENT_CHECK_TIMEOUT_MS/.test(src), "must bound the check with a finite timeout")
assert.ok(src.includes("resolveHookRuntime()"), "must reuse the node-runtime resolver, not hardcode 'node'")
assert.ok(src.includes('"validated"'), "must model the validated state")
assert.ok(src.includes('"failed"'), "must model the failed state")
assert.ok(src.includes('"unvalidated"'), "must model the unvalidated state")
assert.ok(/OK: \(\\d\+\) agents parsed and validated/.test(src), "must parse the real --check output")
}],
["/ecc-doctor renders three honest agent states (behavioral mirror)", () => {
assert.strictEqual(
describeAgentPortabilityMirror(68, { status: "validated", count: 68 }),
"68 agent(s) source schema validated; Pi conversion available via scripts/agent-convert.js"
)
assert.strictEqual(
describeAgentPortabilityMirror(68, { status: "failed", reason: "boom" }),
"68 agent(s) present but validation FAILED: boom"
)
assert.strictEqual(
describeAgentPortabilityMirror(68, { status: "unvalidated", reason: "no node" }),
"68 agent(s) present (unvalidated — no node)"
)
assert.strictEqual(
describeAgentPortabilityMirror(0, { status: "unvalidated", reason: "none" }),
"NOT FOUND"
)
assert.ok(
extensionSource.includes("agent(s) source schema validated; Pi conversion available via scripts/agent-convert.js"),
"expected the validated line pinned in the source"
)
assert.ok(extensionSource.includes("validation FAILED:"), "expected the failed line pinned in the source")
assert.ok(extensionSource.includes("unvalidated —"), "expected the unvalidated line pinned in the source")
}],
["isDisabledByEnv() behavioral mirror: recognizes 0/false/off/none/disabled case- and whitespace-insensitively, and the real function reads ECC_PI_RULES", () => {
for (const disabledValue of ["0", "false", "off", "none", "disabled"]) {
assert.strictEqual(
@@ -67,6 +67,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/eval-harness.js",
"examples/eval-harness",
"scripts/feedback.js",
"scripts/agent-convert.js",
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
@@ -303,6 +304,11 @@ function main() {
"skills/backend-patterns/SKILL.md",
"skills/skill-comply/SKILL.md",
"skills/unified-memory/SKILL.md",
"scripts/agent-convert.js",
"scripts/lib/agent-ir.js",
"scripts/lib/agent-tool-map.js",
"scripts/lib/agent-emit-pi.js",
"schemas/agent.schema.json",
]) {
assert.ok(
packagedPaths.has(requiredPath),