mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
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:
@@ -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();
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user