@jaybeeuu/agent-cortex 1.20.0 → 1.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/style-versioning/SKILL.md +102 -0
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +227 -6
- package/AGENTS.md +29 -24
- package/CHANGELOG.md +119 -5
- package/README.md +132 -33
- package/agents/README.md +47 -13
- package/agents/plan/agent.md +51 -0
- package/agents/plan/claude/frontmatter.json +6 -0
- package/agents/plan/copilot/frontmatter.json +6 -0
- package/agents/plan/pi/frontmatter.json +6 -0
- package/agents/plan.agent.md +3 -0
- package/agents/ralph/agent.md +262 -0
- package/agents/ralph/claude/frontmatter.json +6 -0
- package/agents/ralph/claude/polling.md +31 -0
- package/agents/ralph/copilot/frontmatter.json +6 -0
- package/agents/ralph/copilot/polling.md +62 -0
- package/agents/ralph/pi/frontmatter.json +6 -0
- package/agents/ralph/pi/polling.md +74 -0
- package/agents/ralph-plan/agent.md +114 -0
- package/agents/ralph-plan/claude/frontmatter.json +6 -0
- package/agents/ralph-plan/copilot/frontmatter.json +6 -0
- package/agents/ralph-plan/pi/frontmatter.json +6 -0
- package/agents/ralph-plan.agent.md +5 -2
- package/agents/ralph.agent.md +83 -57
- package/agents/strategy/agent.md +104 -0
- package/agents/strategy/claude/frontmatter.json +6 -0
- package/agents/strategy/copilot/frontmatter.json +6 -0
- package/agents/strategy/pi/frontmatter.json +6 -0
- package/agents/strategy.agent.md +3 -0
- package/bin/agent-cortex.mjs +68 -1
- package/bin/installers/claude.mjs +262 -0
- package/bin/installers/copilot.mjs +129 -0
- package/bin/installers/pi.mjs +197 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/agents/plan.md +3 -1
- package/claude/agents/ralph-plan.md +5 -3
- package/claude/agents/strategy.md +3 -1
- package/claude/hooks/scripts/notify.mjs +54 -0
- package/claude/hooks.json +15 -0
- package/docs/claude-hooks.md +115 -0
- package/docs/ideas/unified-ci-pipeline.md +28 -0
- package/docs/skills/skill-anatomy.md +9 -0
- package/extensions/agent-modes/README.md +54 -0
- package/extensions/agent-modes/discover.test.ts +241 -0
- package/extensions/agent-modes/discover.ts +458 -0
- package/extensions/agent-modes/index.ts +13 -224
- package/extensions/agent-modes/node_modules/.bin/pi +21 -0
- package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
- package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
- package/extensions/agent-modes/package.json +20 -0
- package/extensions/agent-modes/tsconfig.json +12 -0
- package/extensions/session-start.ts +1 -1
- package/extensions/skill-stats/context-loader.test.ts +1 -1
- package/extensions/skill-stats/node_modules/.bin/pi +21 -0
- package/extensions/skill-stats/node_modules/.bin/tsc +21 -0
- package/extensions/skill-stats/node_modules/.bin/tsserver +21 -0
- package/extensions/skill-stats/node_modules/.bin/tsx +21 -0
- package/extensions/subagent/README.md +19 -0
- package/extensions/subagent/index.ts +131 -45
- package/extensions/subagent/node_modules/.bin/pi +21 -0
- package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
- package/extensions/subagent/node_modules/.bin/tsc +21 -0
- package/extensions/subagent/node_modules/.bin/tsserver +21 -0
- package/extensions/subagent/node_modules/.bin/tsx +21 -0
- package/extensions/subagent/package.json +23 -0
- package/extensions/subagent/tsconfig.json +12 -0
- package/extensions/subagent/wait.test.ts +202 -0
- package/extensions/subagent/wait.ts +118 -0
- package/hooks/claude/hooks.json +38 -0
- package/hooks/claude/scripts/notify.mjs +54 -0
- package/lib/cli.mjs +62 -3
- package/package.json +10 -7
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/build-copilot-agents.mjs +30 -0
- package/scripts/lib/compose-agent.mjs +241 -0
- package/scripts/sync-plugin-version.sh +17 -0
- package/skills/engineering/design-an-interface/SKILL.md +1 -1
- package/skills/engineering/improve-codebase-architecture/SKILL.md +1 -1
- package/skills/workflow/ralph/REFERENCE.md +2 -2
- package/skills/workflow/ralph/SKILL.md +6 -6
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
- package/skills/workflow/using-agent-skills/SKILL.md +15 -2
- package/test/cli.test.mjs +115 -2
- package/test/copilot-installer.test.mjs +298 -0
- package/test/installer.test.mjs +179 -0
- package/test/notify-hook.test.mjs +63 -0
- package/test/pi-installer.test.mjs +371 -0
- package/token-map.README.md +128 -0
- package/token-map.json +106 -0
- package/.beads/README.md +0 -81
- package/.beads/config.yaml +0 -1
- package/.beads/issues.jsonl +0 -236
- package/.beads/metadata.json +0 -7
- package/.github/workflows/ci.test.ts +0 -217
- package/.github/workflows/publish.yml +0 -36
- package/scripts/build-claude-agents.mjs +0 -179
- package/skills/workflow/using-agent-skills/scripts/validate-skill-structure.test.ts +0 -73
- /package/extensions/{context-loader.md → lib/context-loader.md} +0 -0
- /package/extensions/{context-loader.ts → lib/context-loader.ts} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Install-time generator for the Claude Code plugin subtree — the ONLY code path
|
|
2
|
+
// for producing claude/ from the canonical sources. There is no build-time
|
|
3
|
+
// generator: the package script `pnpm build:claude` is a pure alias of the CLI
|
|
4
|
+
// (`node bin/agent-cortex.mjs install claude`), so regenerating the committed
|
|
5
|
+
// subtree and installing the plugin run exactly the same code:
|
|
6
|
+
//
|
|
7
|
+
// node bin/agent-cortex.mjs install claude `pnpm build:claude` (regenerates
|
|
8
|
+
// the committed claude/ subtree in place; `--output
|
|
9
|
+
// <dir>` targets another dir, `--dry-run` plans
|
|
10
|
+
// without writing)
|
|
11
|
+
//
|
|
12
|
+
// claude/ stays committed (not gitignored): the marketplace install flow
|
|
13
|
+
// (`claude plugin marketplace add <repo>` → `claude plugin install
|
|
14
|
+
// agent-cortex@jaybeeuu`) reads the subtree straight from the working tree, so a
|
|
15
|
+
// fresh clone installs as-is with no generation step. CI regenerates it via the
|
|
16
|
+
// same installer and runs `git diff --exit-code claude/...` — byte-for-byte
|
|
17
|
+
// validation that the committed output equals installer output, catching stale
|
|
18
|
+
// output and hand-edits to generated files alike.
|
|
19
|
+
//
|
|
20
|
+
// Generated from:
|
|
21
|
+
// claude/agents/<slug>.md agents/<name>/agent.md +
|
|
22
|
+
// agents/<name>/claude/frontmatter.json +
|
|
23
|
+
// agents/<name>/claude/<section>.md ({{SECTION:...}}),
|
|
24
|
+
// with {{TOOL:...}}/{{PATH:...}} substituted against
|
|
25
|
+
// the claude column of token-map.json. Agents in the
|
|
26
|
+
// DEFER set ship instead from agents-native/<name>.md.
|
|
27
|
+
// claude/skills/<name> symlinks to the grouped skills/<group>/<name> dirs
|
|
28
|
+
// (single source — Claude scans skills one level deep)
|
|
29
|
+
// claude/.claude-plugin/plugin.json name/version/description from package.json
|
|
30
|
+
// (version tracks the package — never stale)
|
|
31
|
+
// claude/hooks.json copied from hooks/claude/hooks.json when present
|
|
32
|
+
// claude/hooks/<scripts> copied from hooks/claude/ (support files bundled
|
|
33
|
+
// alongside hooks.json, referenced via ${CLAUDE_PLUGIN_ROOT})
|
|
34
|
+
//
|
|
35
|
+
// The claude/ subtree stays committed and CI drift-checks it (build:claude +
|
|
36
|
+
// git diff --exit-code), so a fresh clone remains installable as-is. Hand-authored
|
|
37
|
+
// support files under claude/ (claude/.mcp.json, claude/scripts/) are left untouched.
|
|
38
|
+
//
|
|
39
|
+
// Zero dependencies so it runs on the CI Node and local Node alike.
|
|
40
|
+
|
|
41
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, symlinkSync, statSync, existsSync, readdirSync } from "node:fs";
|
|
42
|
+
import { join, dirname, relative } from "node:path";
|
|
43
|
+
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { composeAgent } from "../../scripts/lib/compose-agent.mjs";
|
|
45
|
+
|
|
46
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const DEFAULT_ROOT = join(MODULE_DIR, "..", "..");
|
|
48
|
+
|
|
49
|
+
// Agents NOT transformed from the composable sources. `ralph` is authored natively for Claude
|
|
50
|
+
// instead (agents-native/ralph.md) — its event-driven orchestration can't be produced by
|
|
51
|
+
// section composition from the poll-loop sources.
|
|
52
|
+
const DEFER = new Set(["ralph"]);
|
|
53
|
+
|
|
54
|
+
// Ralph-coupled Copilot skills that don't apply to the lean Claude Ralph (and carry dead
|
|
55
|
+
// ~/.copilot orchestration paths). Not symlinked into the Claude plugin.
|
|
56
|
+
const SKILL_EXCLUDE = new Set(["ralph", "run-pipeline-stage"]);
|
|
57
|
+
|
|
58
|
+
const NAME_PREFIX = "agent-cortex:";
|
|
59
|
+
|
|
60
|
+
function isFile(p) {
|
|
61
|
+
try {
|
|
62
|
+
return statSync(p).isFile();
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function byName(a, b) {
|
|
69
|
+
return a.localeCompare(b);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compose agents from agents/<name>/; DEFERed agents come from agents-native/ instead. */
|
|
73
|
+
function buildAgents(root) {
|
|
74
|
+
const files = [];
|
|
75
|
+
const skipped = [];
|
|
76
|
+
const entries = readdirSync(join(root, "agents"), { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory()) continue;
|
|
80
|
+
const dir = join(root, "agents", entry.name);
|
|
81
|
+
if (!isFile(join(dir, "agent.md")) || !isFile(join(dir, "claude", "frontmatter.json"))) continue;
|
|
82
|
+
|
|
83
|
+
if (DEFER.has(entry.name)) {
|
|
84
|
+
skipped.push(entry.name);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fm = composeAgent(root, entry.name, "claude");
|
|
89
|
+
const slug = fm.name.startsWith(NAME_PREFIX) ? fm.name.slice(NAME_PREFIX.length) : fm.name;
|
|
90
|
+
const header =
|
|
91
|
+
`---\n` +
|
|
92
|
+
`# GENERATED from agents/${entry.name}/ by bin/installers/claude.mjs — DO NOT EDIT.\n` +
|
|
93
|
+
`name: ${slug}\n` +
|
|
94
|
+
`description: ${JSON.stringify(fm.description)}\n` +
|
|
95
|
+
`tools: ${fm.tools.join(", ")}\n` +
|
|
96
|
+
`---\n`;
|
|
97
|
+
files.push({ file: `${slug}.md`, content: `${header}\n${fm.body}\n` });
|
|
98
|
+
}
|
|
99
|
+
return { files, skipped };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Copy hand-authored Claude-native agents into claude/agents/ verbatim. */
|
|
103
|
+
function buildNatives(root, nativeSrc) {
|
|
104
|
+
const files = [];
|
|
105
|
+
if (!existsSync(nativeSrc)) return files;
|
|
106
|
+
for (const file of readdirSync(nativeSrc).filter((f) => f.endsWith(".md")).sort(byName)) {
|
|
107
|
+
files.push({ file, content: readFileSync(join(nativeSrc, file), "utf-8") });
|
|
108
|
+
}
|
|
109
|
+
return files;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Flatten grouped skills (skills/<group>/<name>/) into claude/skills/ as relative symlinks. */
|
|
113
|
+
function buildSkills(root) {
|
|
114
|
+
const links = [];
|
|
115
|
+
const seen = new Map();
|
|
116
|
+
for (const group of readdirSync(join(root, "skills")).sort(byName)) {
|
|
117
|
+
const groupDir = join(root, "skills", group);
|
|
118
|
+
if (!statSync(groupDir).isDirectory()) continue;
|
|
119
|
+
for (const name of readdirSync(groupDir).sort(byName)) {
|
|
120
|
+
const skillDir = join(groupDir, name);
|
|
121
|
+
if (!isFile(join(skillDir, "SKILL.md"))) continue;
|
|
122
|
+
if (SKILL_EXCLUDE.has(name)) continue;
|
|
123
|
+
if (seen.has(name)) {
|
|
124
|
+
throw new Error(`duplicate skill name "${name}" (${seen.get(name)} and ${group}) — names must be unique when flattened`);
|
|
125
|
+
}
|
|
126
|
+
seen.set(name, group);
|
|
127
|
+
links.push({ name, target: skillDir });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return links;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** plugin.json manifest; version tracks the package so it can never go stale. */
|
|
134
|
+
function buildPluginJson(root) {
|
|
135
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
136
|
+
return (
|
|
137
|
+
JSON.stringify(
|
|
138
|
+
{
|
|
139
|
+
name: "agent-cortex",
|
|
140
|
+
version: pkg.version,
|
|
141
|
+
description: "Personal Claude Code plugin with custom agents and skills",
|
|
142
|
+
author: { name: "jaybeeuu" },
|
|
143
|
+
license: pkg.license ?? "MIT",
|
|
144
|
+
repository: pkg.repository,
|
|
145
|
+
hooks: "./hooks.json",
|
|
146
|
+
},
|
|
147
|
+
null,
|
|
148
|
+
2,
|
|
149
|
+
) + "\n"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Hook support files (hooks/claude/ except hooks.json) bundled into
|
|
155
|
+
* claude/hooks/ so hook commands can reference them via ${CLAUDE_PLUGIN_ROOT}.
|
|
156
|
+
* hooks.json itself is special-cased to the plugin root (claude/hooks.json).
|
|
157
|
+
*/
|
|
158
|
+
function buildHookFiles(root) {
|
|
159
|
+
const files = [];
|
|
160
|
+
const srcDir = join(root, "hooks", "claude");
|
|
161
|
+
if (!existsSync(srcDir)) return files;
|
|
162
|
+
const walk = (dir, rel) => {
|
|
163
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => byName(a.name, b.name))) {
|
|
164
|
+
const full = join(dir, entry.name);
|
|
165
|
+
if (entry.isDirectory()) walk(full, join(rel, entry.name));
|
|
166
|
+
else if (entry.name !== "hooks.json")
|
|
167
|
+
files.push({ rel: join(rel, entry.name), content: readFileSync(full, "utf-8") });
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
walk(srcDir, "");
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generate the Claude Code plugin subtree.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} options
|
|
178
|
+
* @param {string} [options.root] Package root (contains agents/, skills/, token-map.json, …).
|
|
179
|
+
* Defaults to the repo containing this module.
|
|
180
|
+
* @param {string} [options.output] Target directory (default: <root>/claude).
|
|
181
|
+
* @param {boolean} [options.dryRun] Plan and report without writing anything.
|
|
182
|
+
* @returns {{ output: string, agents: string[], natives: string[], skills: string[], hooks: boolean, dryRun: boolean }}
|
|
183
|
+
*/
|
|
184
|
+
export function installClaude({ root = DEFAULT_ROOT, output, dryRun = false } = {}) {
|
|
185
|
+
const out = output ?? join(root, "claude");
|
|
186
|
+
|
|
187
|
+
const agents = buildAgents(root);
|
|
188
|
+
const natives = buildNatives(root, join(root, "agents-native"));
|
|
189
|
+
const skills = buildSkills(root);
|
|
190
|
+
const pluginJson = buildPluginJson(root);
|
|
191
|
+
const hooksSrc = join(root, "hooks", "claude", "hooks.json");
|
|
192
|
+
const hooksJson = isFile(hooksSrc) ? readFileSync(hooksSrc, "utf-8") : null;
|
|
193
|
+
const hookFiles = buildHookFiles(root);
|
|
194
|
+
|
|
195
|
+
const summary = {
|
|
196
|
+
output: out,
|
|
197
|
+
agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
|
|
198
|
+
natives: natives.map((f) => f.file.replace(/\.md$/, "")),
|
|
199
|
+
skills: skills.map((l) => l.name),
|
|
200
|
+
hooks: hooksJson !== null,
|
|
201
|
+
hookFiles: hookFiles.map((f) => f.rel),
|
|
202
|
+
dryRun,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (agents.skipped.length) console.log(`Skipped composable source(s): ${agents.skipped.join(", ")}`);
|
|
206
|
+
|
|
207
|
+
if (dryRun) {
|
|
208
|
+
console.log(`DRY-RUN — would generate into ${out}, no files written:`);
|
|
209
|
+
console.log(` agents: ${summary.agents.join(", ")}`);
|
|
210
|
+
if (summary.natives.length) console.log(` native agents: ${summary.natives.join(", ")}`);
|
|
211
|
+
console.log(` skills: ${summary.skills.length} symlink(s)`);
|
|
212
|
+
console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
|
|
213
|
+
if (summary.hooks) console.log(" hooks.json (from hooks/claude/hooks.json)");
|
|
214
|
+
if (summary.hookFiles.length)
|
|
215
|
+
console.log(` hook scripts: ${summary.hookFiles.join(", ")} (bundled into claude/hooks/)`);
|
|
216
|
+
return summary;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Generated subsets only — hand-authored support files (.mcp.json, scripts/)
|
|
220
|
+
// under output are left untouched.
|
|
221
|
+
const agentOut = join(out, "agents");
|
|
222
|
+
rmSync(agentOut, { recursive: true, force: true });
|
|
223
|
+
mkdirSync(agentOut, { recursive: true });
|
|
224
|
+
for (const { file, content } of [...agents.files, ...natives]) {
|
|
225
|
+
writeFileSync(join(agentOut, file), content);
|
|
226
|
+
}
|
|
227
|
+
console.log(`Generated ${summary.agents.length} Claude agent(s): ${summary.agents.join(", ")}`);
|
|
228
|
+
if (summary.natives.length) console.log(`Copied ${summary.natives.length} native Claude agent(s): ${summary.natives.join(", ")}`);
|
|
229
|
+
|
|
230
|
+
const skillOut = join(out, "skills");
|
|
231
|
+
rmSync(skillOut, { recursive: true, force: true });
|
|
232
|
+
mkdirSync(skillOut, { recursive: true });
|
|
233
|
+
for (const { name, target } of skills) {
|
|
234
|
+
symlinkSync(relative(skillOut, target), join(skillOut, name));
|
|
235
|
+
}
|
|
236
|
+
console.log(`Linked ${summary.skills.length} skill(s) into ${skillOut}`);
|
|
237
|
+
|
|
238
|
+
mkdirSync(join(out, ".claude-plugin"), { recursive: true });
|
|
239
|
+
writeFileSync(join(out, ".claude-plugin", "plugin.json"), pluginJson);
|
|
240
|
+
console.log(`Wrote ${join(out, ".claude-plugin", "plugin.json")}`);
|
|
241
|
+
|
|
242
|
+
if (summary.hooks) {
|
|
243
|
+
writeFileSync(join(out, "hooks.json"), hooksJson);
|
|
244
|
+
console.log(`Wrote ${join(out, "hooks.json")} (from ${hooksSrc})`);
|
|
245
|
+
|
|
246
|
+
if (hookFiles.length > 0) {
|
|
247
|
+
const hookOut = join(out, "hooks");
|
|
248
|
+
rmSync(hookOut, { recursive: true, force: true });
|
|
249
|
+
mkdirSync(hookOut, { recursive: true });
|
|
250
|
+
for (const { rel, content } of hookFiles) {
|
|
251
|
+
const dest = join(hookOut, rel);
|
|
252
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
253
|
+
writeFileSync(dest, content);
|
|
254
|
+
}
|
|
255
|
+
console.log(`Bundled ${hookFiles.length} hook support file(s) into ${hookOut}`);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`Skipped hooks.json — ${hooksSrc} not found (existing file left untouched)`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return summary;
|
|
262
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Install-time generator for the Copilot CLI flat agent files (agents/*.agent.md).
|
|
2
|
+
// This is the single code path for producing those files from the canonical
|
|
3
|
+
// composable sources — both entry points below call installCopilot(), so
|
|
4
|
+
// install-time and build-time output can never diverge:
|
|
5
|
+
//
|
|
6
|
+
// - bin/agent-cortex.mjs `agent-cortex install copilot` (install-time;
|
|
7
|
+
// `--output <dir>` overrides the target,
|
|
8
|
+
// `--dry-run` plans without writing)
|
|
9
|
+
// - scripts/build-copilot-agents.mjs `pnpm build:copilot` (dev/CI: regenerates
|
|
10
|
+
// the committed agents/*.agent.md files in place)
|
|
11
|
+
//
|
|
12
|
+
// Output lands in <root>/agents/ (the default `output`) because that is the
|
|
13
|
+
// directory the Copilot plugin actually loads: plugin.json declares
|
|
14
|
+
// "agents": "agents/", which scans for *.agent.md files. The generated flat
|
|
15
|
+
// files therefore share the agents/ directory with the canonical composable
|
|
16
|
+
// dirs they are composed from (agents/<name>/) — only flat *.agent.md files are
|
|
17
|
+
// written, nothing is deleted, so a generated file can never clobber a
|
|
18
|
+
// composable source. `--output` exists for the same preview/test contract as the
|
|
19
|
+
// claude and pi installers: point it at a scratch dir and inspect the output.
|
|
20
|
+
//
|
|
21
|
+
// agents/<name>.agent.md composed from agents/<name>/agent.md +
|
|
22
|
+
// agents/<name>/copilot/frontmatter.json +
|
|
23
|
+
// agents/<name>/copilot/<section>.md ({{SECTION:...}}),
|
|
24
|
+
// with {{TOOL:...}}/{{PATH:...}} substituted against
|
|
25
|
+
// the copilot column of token-map.json.
|
|
26
|
+
//
|
|
27
|
+
// The flat files stay committed and CI drift-checks them (build:copilot +
|
|
28
|
+
// git diff --exit-code -- 'agents/*.agent.md'), so a fresh clone (or a direct
|
|
29
|
+
// path plugin install) ships the plugin read-ready.
|
|
30
|
+
//
|
|
31
|
+
// Zero dependencies so it runs on the CI Node and local Node alike.
|
|
32
|
+
|
|
33
|
+
import { writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
34
|
+
import { join, dirname } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import { composeAgent } from "../../scripts/lib/compose-agent.mjs";
|
|
37
|
+
|
|
38
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const DEFAULT_ROOT = join(MODULE_DIR, "..", "..");
|
|
40
|
+
|
|
41
|
+
function isDirectory(p) {
|
|
42
|
+
try {
|
|
43
|
+
return statSync(p).isDirectory();
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function byName(a, b) {
|
|
50
|
+
return a.localeCompare(b);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Serialise the flat-file YAML frontmatter. Field order and names mirror the
|
|
55
|
+
* pre-migration flat sources (description, name, tools, argument-hint) so
|
|
56
|
+
* Copilot CLI sees the same agent metadata. The GENERATED comment references
|
|
57
|
+
* scripts/build-copilot-agents.mjs — the committed entry point — so the
|
|
58
|
+
* generated bytes are identical whichever of the two entry points ran.
|
|
59
|
+
*/
|
|
60
|
+
function frontmatterYaml(name, fm) {
|
|
61
|
+
const lines = [
|
|
62
|
+
"---",
|
|
63
|
+
`# GENERATED from agents/${name}/ by scripts/build-copilot-agents.mjs — DO NOT EDIT.`,
|
|
64
|
+
`description: ${JSON.stringify(fm.description)}`,
|
|
65
|
+
`name: ${JSON.stringify(fm.name)}`,
|
|
66
|
+
`tools: [${fm.tools.map((t) => JSON.stringify(t)).join(", ")}]`,
|
|
67
|
+
];
|
|
68
|
+
if (fm.argumentHint) lines.push(`argument-hint: ${JSON.stringify(fm.argumentHint)}`);
|
|
69
|
+
lines.push("---");
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Compose agents from agents/<name>/; every composable dir must ship a copilot harness. */
|
|
74
|
+
function buildAgents(root) {
|
|
75
|
+
const files = [];
|
|
76
|
+
const entries = readdirSync(join(root, "agents"), { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory()) continue; // skip generated *.agent.md flat files and README
|
|
80
|
+
const dir = join(root, "agents", entry.name);
|
|
81
|
+
if (!existsSync(join(dir, "agent.md"))) continue; // not a composable agent dir
|
|
82
|
+
if (!isDirectory(join(dir, "copilot")) || !existsSync(join(dir, "copilot", "frontmatter.json"))) {
|
|
83
|
+
throw new Error(`agent "${entry.name}": composable directory without copilot/frontmatter.json — define the copilot harness or exclude the agent`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fm = composeAgent(root, entry.name, "copilot");
|
|
87
|
+
files.push({
|
|
88
|
+
name: entry.name,
|
|
89
|
+
file: `${entry.name}.agent.md`,
|
|
90
|
+
content: `${frontmatterYaml(entry.name, fm)}\n\n${fm.body}\n`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate the Copilot CLI flat agent files.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} options
|
|
100
|
+
* @param {string} [options.root] Package root (contains agents/, token-map.json, …).
|
|
101
|
+
* Defaults to the repo containing this module.
|
|
102
|
+
* @param {string} [options.output] Directory to write the flat *.agent.md files into
|
|
103
|
+
* (default: <root>/agents — the dir plugin.json's
|
|
104
|
+
* "agents": "agents/" scans for the plugin).
|
|
105
|
+
* @param {boolean} [options.dryRun] Plan and report without writing anything.
|
|
106
|
+
* @returns {{ output: string, agents: string[], dryRun: boolean }}
|
|
107
|
+
*/
|
|
108
|
+
export function installCopilot({ root = DEFAULT_ROOT, output, dryRun = false } = {}) {
|
|
109
|
+
const out = output ?? join(root, "agents");
|
|
110
|
+
const files = buildAgents(root);
|
|
111
|
+
const summary = {
|
|
112
|
+
output: out,
|
|
113
|
+
agents: files.map((f) => f.name),
|
|
114
|
+
dryRun,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
console.log(`DRY-RUN — would generate into ${out}, no files written:`);
|
|
119
|
+
console.log(` agents: ${summary.agents.join(", ")}`);
|
|
120
|
+
return summary;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
mkdirSync(out, { recursive: true });
|
|
124
|
+
for (const { file, content } of files) {
|
|
125
|
+
writeFileSync(join(out, file), content);
|
|
126
|
+
}
|
|
127
|
+
console.log(`Generated ${files.length} Copilot agent file(s): ${summary.agents.join(", ")}`);
|
|
128
|
+
return summary;
|
|
129
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Pi harness installer: agent-cortex install pi
|
|
2
|
+
//
|
|
3
|
+
// Materialises the agent-cortex package into the pi runtime's user scope:
|
|
4
|
+
//
|
|
5
|
+
// <output>/agents/<slug>.agent.md composed agents (default ~/.pi/agent/agents)
|
|
6
|
+
// <output>/skills/<group>/<name>/ token-substituted skill copies (default ~/.pi/agent/skills)
|
|
7
|
+
//
|
|
8
|
+
// Composition per the token-map.json contract:
|
|
9
|
+
// 1. {{SECTION:name}} is resolved from the agent's pi/<name>.md section file
|
|
10
|
+
// (shared composer, scripts/lib/compose-agent.mjs).
|
|
11
|
+
// 2. {{TOOL:key}} is substituted with the pi column; a null mapping drops the
|
|
12
|
+
// token from prose with a warning (pi has no ask_user/skill tool).
|
|
13
|
+
// 3. {{PATH:key}} named keys resolve through the paths table; bare relative
|
|
14
|
+
// paths resolve against the plugin root (default: token-map.json's pi value,
|
|
15
|
+
// overridable with --plugin-root for checkout/symlinked installs).
|
|
16
|
+
// Frontmatter tool lists are translated the same way agent-modes does at runtime:
|
|
17
|
+
// canonical → pi column, null → omitted, unknown (native pi tool) → pass through.
|
|
18
|
+
// Skill markdown files get the same TOOL/PATH substitution so pi never serves
|
|
19
|
+
// literal token syntax; other skill files are copied verbatim.
|
|
20
|
+
//
|
|
21
|
+
// Skills install into ~/.pi/agent/skills (pi's user-global skill dir), which pi
|
|
22
|
+
// loads before package skills, so the substituted copies shadow the raw package
|
|
23
|
+
// skills of the same name (pi dedupes by name, user wins).
|
|
24
|
+
//
|
|
25
|
+
// Zero dependencies so it runs on the CI Node and local Node alike.
|
|
26
|
+
|
|
27
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "node:fs";
|
|
28
|
+
import { join, dirname } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { composeAgent, loadTokenMap, substituteTokens, translateToolList } from "../../scripts/lib/compose-agent.mjs";
|
|
32
|
+
|
|
33
|
+
const PI = "pi";
|
|
34
|
+
|
|
35
|
+
// The token-map contract version this installer implements (token-map.json
|
|
36
|
+
// "version" field). A higher map version is rejected: the contract must be
|
|
37
|
+
// extended before this installer can trust it.
|
|
38
|
+
const CONTRACT_VERSION = 1;
|
|
39
|
+
|
|
40
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
41
|
+
const DEFAULT_OUTPUT = join(homedir(), ".pi", "agent");
|
|
42
|
+
|
|
43
|
+
const DEFAULT_WARN = (msg) => console.warn(`[pi-installer] ${msg}`);
|
|
44
|
+
|
|
45
|
+
function isDirectory(p) {
|
|
46
|
+
try {
|
|
47
|
+
return statSync(p).isDirectory();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isFile(p) {
|
|
54
|
+
try {
|
|
55
|
+
return statSync(p).isFile();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Install the agent-cortex pi harness into the pi runtime.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} options
|
|
65
|
+
* @param {string} [options.root] Package root (defaults to this file's repo)
|
|
66
|
+
* @param {string} [options.output] Pi user-scope dir; agents go to
|
|
67
|
+
* <output>/agents, skills to <output>/skills
|
|
68
|
+
* (default ~/.pi/agent)
|
|
69
|
+
* @param {boolean} [options.dryRun] Compute and report without writing anything
|
|
70
|
+
* @param {string} [options.pluginRoot] Override plugin_root used for {{PATH:...}}
|
|
71
|
+
* resolution (default: token-map pi value)
|
|
72
|
+
* @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`
|
|
73
|
+
* @returns {{ agents: {name:string, filePath:string}[], skills: {skills:number, md:number, files:number, dir:string},
|
|
74
|
+
* warnings: string[], dryRun: boolean, agentsDir: string, skillsDir: string }}
|
|
75
|
+
*/
|
|
76
|
+
export function installPi(options = {}) {
|
|
77
|
+
const root = options.root ?? REPO_ROOT;
|
|
78
|
+
const output = options.output ?? DEFAULT_OUTPUT;
|
|
79
|
+
const dryRun = options.dryRun ?? false;
|
|
80
|
+
const warnings = [];
|
|
81
|
+
const warn = (msg) => {
|
|
82
|
+
warnings.push(msg);
|
|
83
|
+
(options.warn ?? DEFAULT_WARN)(msg);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const tokenMap = loadTokenMap(root);
|
|
87
|
+
if (typeof tokenMap.version === "number" && tokenMap.version > CONTRACT_VERSION) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`token-map.json version ${tokenMap.version} is newer than the contract version ${CONTRACT_VERSION} this installer implements — upgrade agent-cortex`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pluginRoot = options.pluginRoot ?? tokenMap.paths.plugin_root?.[PI];
|
|
94
|
+
const agentsDir = join(output, "agents");
|
|
95
|
+
const skillsDir = join(output, "skills");
|
|
96
|
+
|
|
97
|
+
const agents = installAgents({ root, agentsDir, dryRun, pluginRoot, tokenMap, warn });
|
|
98
|
+
const skills = installSkills({ root, skillsDir, dryRun, pluginRoot, tokenMap, warn });
|
|
99
|
+
|
|
100
|
+
return { agents, skills, warnings, dryRun, agentsDir, skillsDir };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Agents ──────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function installAgents({ root, agentsDir, dryRun, pluginRoot, tokenMap, warn }) {
|
|
106
|
+
const written = [];
|
|
107
|
+
const agentsSrc = join(root, "agents");
|
|
108
|
+
for (const entry of readdirSync(agentsSrc, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
109
|
+
if (!entry.isDirectory()) continue; // skip generated *.agent.md flat files
|
|
110
|
+
const dir = join(agentsSrc, entry.name);
|
|
111
|
+
if (!isFile(join(dir, "agent.md"))) continue; // not a composable agent dir
|
|
112
|
+
if (!isDirectory(join(dir, PI)) || !isFile(join(dir, PI, "frontmatter.json"))) {
|
|
113
|
+
throw new Error(`agent "${entry.name}": composable directory without pi/frontmatter.json — define the pi harness or exclude the agent`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fm = composeAgent(root, entry.name, PI, {
|
|
117
|
+
dropNullTools: true,
|
|
118
|
+
pluginRoot,
|
|
119
|
+
resolveRelativePaths: true,
|
|
120
|
+
warn,
|
|
121
|
+
});
|
|
122
|
+
const tools = translateToolList(fm.tools, tokenMap, PI, { warn });
|
|
123
|
+
const content = renderPiAgent(entry.name, fm, tools);
|
|
124
|
+
const filePath = join(agentsDir, `${entry.name}.agent.md`);
|
|
125
|
+
if (!dryRun) {
|
|
126
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
127
|
+
writeFileSync(filePath, content);
|
|
128
|
+
}
|
|
129
|
+
written.push({ name: entry.name, filePath });
|
|
130
|
+
}
|
|
131
|
+
return written;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderPiAgent(name, fm, tools) {
|
|
135
|
+
const lines = [
|
|
136
|
+
"---",
|
|
137
|
+
`# GENERATED from agents/${name}/ by bin/installers/pi.mjs — DO NOT EDIT.`,
|
|
138
|
+
`name: ${JSON.stringify(fm.name)}`,
|
|
139
|
+
`description: ${JSON.stringify(fm.description)}`,
|
|
140
|
+
`tools: ${JSON.stringify(tools.join(" "))}`,
|
|
141
|
+
];
|
|
142
|
+
if (fm.model) lines.push(`model: ${JSON.stringify(fm.model)}`);
|
|
143
|
+
if (fm.argumentHint) lines.push(`argument-hint: ${JSON.stringify(fm.argumentHint)}`);
|
|
144
|
+
lines.push("---");
|
|
145
|
+
return `${lines.join("\n")}\n\n${fm.body}\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Skills ──────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function installSkills({ root, skillsDir, dryRun, pluginRoot, tokenMap, warn }) {
|
|
151
|
+
const skillsSrc = join(root, "skills");
|
|
152
|
+
if (!isDirectory(skillsSrc)) return { skills: 0, md: 0, files: 0, dir: skillsDir };
|
|
153
|
+
|
|
154
|
+
const transform = (content) =>
|
|
155
|
+
substituteTokens(content, PI, tokenMap, { dropNullTools: true, pluginRoot, resolveRelativePaths: true, warn });
|
|
156
|
+
|
|
157
|
+
if (!dryRun) mkdirSync(skillsDir, { recursive: true });
|
|
158
|
+
let skills = 0;
|
|
159
|
+
let md = 0;
|
|
160
|
+
let files = 0;
|
|
161
|
+
for (const group of readdirSync(skillsSrc).sort()) {
|
|
162
|
+
const groupDir = join(skillsSrc, group);
|
|
163
|
+
if (!isDirectory(groupDir)) continue;
|
|
164
|
+
for (const name of readdirSync(groupDir).sort()) {
|
|
165
|
+
if (!isFile(join(groupDir, name, "SKILL.md"))) continue;
|
|
166
|
+
skills += 1;
|
|
167
|
+
const stats = copyTree(join(groupDir, name), join(skillsDir, group, name), transform, dryRun);
|
|
168
|
+
md += stats.md;
|
|
169
|
+
files += stats.files;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { skills, md, files, dir: skillsDir };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Recursively copy a tree; transform .md file contents, copy everything else verbatim. */
|
|
176
|
+
function copyTree(src, dest, transform, dryRun) {
|
|
177
|
+
let md = 0;
|
|
178
|
+
let files = 0;
|
|
179
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const from = join(src, entry.name);
|
|
182
|
+
const to = join(dest, entry.name);
|
|
183
|
+
if (entry.isDirectory()) {
|
|
184
|
+
const stats = copyTree(from, to, transform, dryRun);
|
|
185
|
+
md += stats.md;
|
|
186
|
+
files += stats.files;
|
|
187
|
+
} else if (entry.isFile()) {
|
|
188
|
+
files += 1;
|
|
189
|
+
if (entry.name.endsWith(".md")) md += 1;
|
|
190
|
+
if (dryRun) continue;
|
|
191
|
+
mkdirSync(dest, { recursive: true });
|
|
192
|
+
if (entry.name.endsWith(".md")) writeFileSync(to, transform(readFileSync(from, "utf-8")));
|
|
193
|
+
else copyFileSync(from, to);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { md, files };
|
|
197
|
+
}
|
package/claude/agents/plan.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
# GENERATED from agents/plan
|
|
2
|
+
# GENERATED from agents/plan/ by bin/installers/claude.mjs — DO NOT EDIT.
|
|
3
3
|
name: plan
|
|
4
4
|
description: "Guide a feature from first discussion through PRD and task breakdown to classified beads ready for ralph execution. Full end-to-end planning workflow using the plan skill. Use when starting a new feature, scoping work to hand off to ralph, or saying 'plan this'."
|
|
5
5
|
tools: Bash, Read, Edit, Write, Grep, Glob, AskUserQuestion, Task, Skill
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
# plan — end-to-end feature planning agent
|
|
9
|
+
|
|
8
10
|
You are a planning agent. Your job is to guide a feature from first discussion through PRD and task breakdown to classified beads ready for ralph execution.
|
|
9
11
|
|
|
10
12
|
You follow the **plan** skill workflow. The skill is defined in `skills/workflow/plan/SKILL.md`. Read it now to load the full instructions.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
# GENERATED from agents/ralph-plan
|
|
2
|
+
# GENERATED from agents/ralph-plan/ by bin/installers/claude.mjs — DO NOT EDIT.
|
|
3
3
|
name: ralph-plan
|
|
4
4
|
description: "Plans changes by exploring codebases and creating beads to track the work. Use when you want to scope a feature, bug fix, or refactor — the agent reads, reasons, grills the user, writes a high-level plan into the top-level bead, then files child beads ready for ralph."
|
|
5
5
|
tools: Bash, Read, Edit, Write, Grep, Glob, AskUserQuestion
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
# ralph-plan — planning agent for ralph
|
|
9
|
+
|
|
8
10
|
You are a planning agent. Your job is to understand a task, explore the codebase, grill the user until the design is clear, write a concise implementation plan, and file beads for ralph to implement. You **must not** modify any source code or tracked documentation — your writes are limited to `.agent-cortex/working-docs/` and bead fields.
|
|
9
11
|
|
|
10
12
|
## Permitted writes
|
|
@@ -32,7 +34,7 @@ Invoke the **beads** skill (set workspace root, run `bd prime`). Hold the output
|
|
|
32
34
|
If the input looks like a PRD (product requirements document), invoke the **prd-to-tasks** skill first — it handles exploration, user quizzing, and creates epics and task beads directly. Then continue from step 7 (Agree with the user).
|
|
33
35
|
|
|
34
36
|
### 2. Understand the request
|
|
35
|
-
If the request is ambiguous or incomplete, use
|
|
37
|
+
If the request is ambiguous or incomplete, use AskUserQuestion to resolve blockers before exploring. One question at a time.
|
|
36
38
|
|
|
37
39
|
If the domain language is unclear or inconsistent, invoke the **grill-with-docs** skill to extract canonical terms. It updates `CONTEXT.md` inline and surfaces ambiguities.
|
|
38
40
|
|
|
@@ -116,4 +118,4 @@ Every explore sub-agent prompt must include:
|
|
|
116
118
|
- Never assume — verify by reading actual code before creating a bead.
|
|
117
119
|
- Never write outside `.agent-cortex/working-docs/` or bead fields.
|
|
118
120
|
- Never make commits, open PRs, or run shell commands that modify the repo.
|
|
119
|
-
- Use
|
|
121
|
+
- Use Bash only for read-only commands (`git log`, `find`, `cat`, etc.).
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
# GENERATED from agents/strategy
|
|
2
|
+
# GENERATED from agents/strategy/ by bin/installers/claude.mjs — DO NOT EDIT.
|
|
3
3
|
name: strategy
|
|
4
4
|
description: "Creates top-level design documents across strategy, PRD, and technical direction so delivery planning starts with clear intent and justified decisions. Use when defining product direction before implementation planning, shaping a feature PRD, or selecting technical direction before ralph-plan."
|
|
5
5
|
tools: Bash, Read, Grep, Glob, AskUserQuestion, WebFetch, Skill, Edit, Write
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
# strategy — pre-delivery design agent
|
|
9
|
+
|
|
8
10
|
You are a pre-delivery design agent. Your job is to produce high-quality design artifacts that set up `ralph-plan` for success.
|
|
9
11
|
|
|
10
12
|
## Scope
|