@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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocking wait over background subagent runs.
|
|
3
|
+
*
|
|
4
|
+
* Factored out of the extension entrypoint so the wait semantics can be tested
|
|
5
|
+
* without spawning real `pi` processes: the run store is injected as a lookup
|
|
6
|
+
* function and completion is driven by each entry's `promise` (which resolves
|
|
7
|
+
* exactly when the run's result is committed).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface BackgroundEntry<T> {
|
|
11
|
+
/** The run's result once it finishes; null while the agent is still working. */
|
|
12
|
+
result: T | null;
|
|
13
|
+
/** Resolves when the run finishes (success, failure, or abort). */
|
|
14
|
+
promise: Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CompletedRun<T> {
|
|
18
|
+
agentId: string;
|
|
19
|
+
result: T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WaitResult<T> {
|
|
23
|
+
/** Requested agents whose result is now available, in requested order. */
|
|
24
|
+
completed: CompletedRun<T>[];
|
|
25
|
+
/** Requested agents still working when the wait ended. */
|
|
26
|
+
running: string[];
|
|
27
|
+
/** Requested IDs with no tracked run (never dispatched or expired). */
|
|
28
|
+
notFound: string[];
|
|
29
|
+
/** True when the wait ended because the timeout elapsed with agents still running. */
|
|
30
|
+
timedOut: boolean;
|
|
31
|
+
/** True when the wait ended because the abort signal fired. */
|
|
32
|
+
aborted: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WaitOptions {
|
|
36
|
+
/** Block for at most this many milliseconds before returning (default 120_000). */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
/** Abort the wait early (e.g. user Ctrl+C). */
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_WAIT_TIMEOUT_MS = 120_000;
|
|
43
|
+
|
|
44
|
+
type GetEntry<T> = (agentId: string) => BackgroundEntry<T> | undefined;
|
|
45
|
+
|
|
46
|
+
function scan<T>(getEntry: GetEntry<T>, agentIds: string[], timedOut: boolean, aborted: boolean): WaitResult<T> {
|
|
47
|
+
const completed: CompletedRun<T>[] = [];
|
|
48
|
+
const running: string[] = [];
|
|
49
|
+
const notFound: string[] = [];
|
|
50
|
+
for (const agentId of agentIds) {
|
|
51
|
+
const entry = getEntry(agentId);
|
|
52
|
+
if (!entry) notFound.push(agentId);
|
|
53
|
+
else if (entry.result !== null) completed.push({ agentId, result: entry.result });
|
|
54
|
+
else running.push(agentId);
|
|
55
|
+
}
|
|
56
|
+
return { completed, running, notFound, timedOut, aborted };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function waitForAgents<T>(
|
|
60
|
+
getEntry: GetEntry<T>,
|
|
61
|
+
agentIds: string[],
|
|
62
|
+
options: WaitOptions = {},
|
|
63
|
+
): Promise<WaitResult<T>> {
|
|
64
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
65
|
+
const { signal } = options;
|
|
66
|
+
|
|
67
|
+
const hasResolution = (result: WaitResult<T>): boolean => result.completed.length > 0 || result.notFound.length > 0;
|
|
68
|
+
|
|
69
|
+
// Fast path: any requested agent that already resolved (completed or unknown)
|
|
70
|
+
// ends the wait — the caller dispatches replacements before waiting again.
|
|
71
|
+
const initial = scan(getEntry, agentIds, false, false);
|
|
72
|
+
if (hasResolution(initial)) return initial;
|
|
73
|
+
|
|
74
|
+
// Single shared abort racer so a fired signal always wins the next race.
|
|
75
|
+
const abortRacer: Promise<"aborted"> | undefined = signal
|
|
76
|
+
? new Promise((resolve) => {
|
|
77
|
+
if (signal.aborted) resolve("aborted");
|
|
78
|
+
else signal.addEventListener("abort", () => resolve("aborted"), { once: true });
|
|
79
|
+
})
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
let timedOut = false;
|
|
84
|
+
let aborted = false;
|
|
85
|
+
|
|
86
|
+
while (!timedOut && !aborted) {
|
|
87
|
+
const runningPromises: Promise<unknown>[] = agentIds
|
|
88
|
+
.map(getEntry)
|
|
89
|
+
.filter((entry): entry is BackgroundEntry<T> => entry !== undefined && entry.result === null)
|
|
90
|
+
.map((entry) => entry.promise);
|
|
91
|
+
|
|
92
|
+
const remaining = deadline - Date.now();
|
|
93
|
+
if (remaining <= 0) {
|
|
94
|
+
timedOut = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const racers: Promise<"completed" | "timeout" | "aborted">[] = [];
|
|
99
|
+
if (runningPromises.length > 0) {
|
|
100
|
+
// A settled run promise means its result was committed before settling.
|
|
101
|
+
racers.push(Promise.race(runningPromises.map((p) => p.then(() => "completed" as const, () => "completed" as const))));
|
|
102
|
+
}
|
|
103
|
+
racers.push(new Promise((resolve) => setTimeout(() => resolve("timeout" as const), remaining)));
|
|
104
|
+
if (abortRacer) racers.push(abortRacer);
|
|
105
|
+
|
|
106
|
+
const outcome = await Promise.race(racers);
|
|
107
|
+
if (outcome === "timeout") timedOut = true;
|
|
108
|
+
else if (outcome === "aborted") aborted = true;
|
|
109
|
+
|
|
110
|
+
// "completed": at least one run promise settled — re-scan. Entries whose
|
|
111
|
+
// result is now available end the wait; everything else loops back under
|
|
112
|
+
// the same deadline.
|
|
113
|
+
const current = scan(getEntry, agentIds, timedOut, aborted);
|
|
114
|
+
if (hasResolution(current) || timedOut || aborted) return current;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return scan(getEntry, agentIds, timedOut, aborted);
|
|
118
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "echo 'Load AGENTS.md and active beads context before taking action.'"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "echo 'Style policy: this project ships style skills — prefer them over ad-hoc choices. Invoke style-code BEFORE writing or editing code (and when refactoring); style-tests BEFORE writing or changing tests; style-documentation BEFORE writing or updating docs; style-comms BEFORE composing commit messages, PR descriptions, tickets, or other written deliverables.'"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "command",
|
|
17
|
+
"command": "echo 'Skill policy: agent-cortex ships skills — prefer them over ad-hoc choices. using-agent-skills routes work to the right skill (check it first when unsure); bd-tool tracks beads tasks (run bd ready for available work); git-workflow enforces branch and PR discipline. Invoke skills via the Skill tool.'"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "echo 'Be extremely concise. Sacrifice grammar for the sake of concision.'"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"Notification": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "agent_completed|agent_needs_input|permission_prompt",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/notify.mjs\""
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code Notification hook — desktop notification on task completion/error.
|
|
3
|
+
//
|
|
4
|
+
// Reads the Notification hook JSON payload from stdin and prints a
|
|
5
|
+
// `{ "terminalSequence": "..." }` response for Claude Code to emit. Hooks run
|
|
6
|
+
// without a controlling terminal, so OSC sequences must go through Claude Code
|
|
7
|
+
// itself (it handles tmux/screen re-wrapping); writing /dev/tty directly fails.
|
|
8
|
+
//
|
|
9
|
+
// Mapped from the pi `notify` extension (extensions/notify/): pi notifies on
|
|
10
|
+
// agent_end for multi-turn or long tasks. Claude Code's Notification matcher is an
|
|
11
|
+
// exact-string list over `notification_type` values (there are no `success`/
|
|
12
|
+
// `error` types) — hooks.json selects `agent_completed|agent_needs_input|permission_prompt`
|
|
13
|
+
// to mirror "task completed / failed / needs approval". The payload already carries
|
|
14
|
+
// a message + title, so no summarization LLM call is needed here.
|
|
15
|
+
//
|
|
16
|
+
// Zero dependencies; runs on the Node that ships with Claude Code.
|
|
17
|
+
|
|
18
|
+
let raw = "";
|
|
19
|
+
for await (const chunk of process.stdin) {
|
|
20
|
+
raw += chunk;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let payload = {};
|
|
24
|
+
try {
|
|
25
|
+
payload = raw.trim() ? JSON.parse(raw) : {};
|
|
26
|
+
} catch {
|
|
27
|
+
process.exit(0); // never fail a session on an unparsable payload
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const message = typeof payload.message === "string" ? payload.message.trim() : "";
|
|
31
|
+
if (!message) process.exit(0); // nothing worth notifying (e.g. autocompact events)
|
|
32
|
+
|
|
33
|
+
const title =
|
|
34
|
+
typeof payload.title === "string" && payload.title.trim() ? payload.title.trim() : "Claude Code";
|
|
35
|
+
const type = typeof payload.notification_type === "string" ? payload.notification_type : "";
|
|
36
|
+
|
|
37
|
+
process.stdout.write(JSON.stringify({ terminalSequence: pickSequence(title, message, type) }) + "\n");
|
|
38
|
+
|
|
39
|
+
/** Pick an OSC notification escape sequence for the detected terminal. */
|
|
40
|
+
function pickSequence(title, message, type) {
|
|
41
|
+
const body = message.replace(/\u001b/g, "").replace(/\n/g, " ").trim();
|
|
42
|
+
const label = type ? `${type}: ${title}` : title;
|
|
43
|
+
|
|
44
|
+
if (process.env.KITTY_WINDOW_ID) {
|
|
45
|
+
// Kitty: two-part persistent notification (title + body) — same form as the pi notify extension.
|
|
46
|
+
return `\u001b]99;i=1:d=1;${title}\u001b\\\u001b]99;i=1:p=body;${body}\u001b\\`;
|
|
47
|
+
}
|
|
48
|
+
if (process.env.TERM_PROGRAM === "iTerm.app" || process.env.ITERM_SESSION_ID) {
|
|
49
|
+
// iTerm2 / ConEmu / Windows Terminal / WezTerm: OSC 9.
|
|
50
|
+
return `\u001b]9;${label}: ${body}\u0007`;
|
|
51
|
+
}
|
|
52
|
+
// urxvt / Ghostty / Warp, and default fallback: OSC 777.
|
|
53
|
+
return `\u001b]777;notify;${label};${body}\u0007`;
|
|
54
|
+
}
|
package/lib/cli.mjs
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const SUPPORTED_HARNESSES = ["copilot", "claude", "pi"];
|
|
2
2
|
|
|
3
|
+
// Options accepted by `install <harness>`. `--output`/`--dry-run` are shared by
|
|
4
|
+
// the claude and pi installers; `--plugin-root` is pi-specific. Unknown or
|
|
5
|
+
// malformed options surface as `optionError` on the parsed result; the CLI
|
|
6
|
+
// prints them and exits 1.
|
|
7
|
+
const INSTALL_OPTIONS = {
|
|
8
|
+
"--dry-run": { key: "dryRun" },
|
|
9
|
+
"--output": { key: "output" },
|
|
10
|
+
"--plugin-root": { key: "pluginRoot" },
|
|
11
|
+
};
|
|
12
|
+
|
|
3
13
|
export function parseArgs(argv) {
|
|
4
14
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
5
15
|
return { command: "help" };
|
|
@@ -8,8 +18,42 @@ export function parseArgs(argv) {
|
|
|
8
18
|
const [command, ...rest] = argv;
|
|
9
19
|
|
|
10
20
|
if (command === "install") {
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
let harness = null;
|
|
22
|
+
let i = 0;
|
|
23
|
+
if (rest.length > 0 && !rest[0].startsWith("--")) {
|
|
24
|
+
harness = rest[0];
|
|
25
|
+
i = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const options = {};
|
|
29
|
+
for (; i < rest.length; i++) {
|
|
30
|
+
const arg = rest[i];
|
|
31
|
+
// Support both `--output=<dir>` and `--output <dir>`.
|
|
32
|
+
const inlineValue = arg.startsWith("--output=") ? arg.slice("--output=".length) : null;
|
|
33
|
+
const spec = inlineValue !== null ? INSTALL_OPTIONS["--output"] : INSTALL_OPTIONS[arg];
|
|
34
|
+
if (!spec) {
|
|
35
|
+
return { command: "install", harness, optionError: `Unknown option "${arg}". Run "agent-cortex --help" for usage.` };
|
|
36
|
+
}
|
|
37
|
+
if (spec.key === "dryRun") {
|
|
38
|
+
options.dryRun = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (inlineValue !== null) {
|
|
42
|
+
options[spec.key] = inlineValue;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const value = rest[i + 1];
|
|
46
|
+
if (value == null || value.startsWith("--")) {
|
|
47
|
+
return { command: "install", harness, optionError: `Missing value for "${arg}". Run "agent-cortex --help" for usage.` };
|
|
48
|
+
}
|
|
49
|
+
options[spec.key] = value;
|
|
50
|
+
i += 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Object.keys(options).length === 0) {
|
|
54
|
+
return { command: "install", harness };
|
|
55
|
+
}
|
|
56
|
+
return { command: "install", harness, ...options };
|
|
13
57
|
}
|
|
14
58
|
|
|
15
59
|
return { command: "unknown", name: command };
|
|
@@ -21,6 +65,21 @@ export function buildHelpText() {
|
|
|
21
65
|
Commands:
|
|
22
66
|
install <harness> Install agent-cortex for a harness
|
|
23
67
|
Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}
|
|
68
|
+
Options (claude):
|
|
69
|
+
--output <dir> Write the plugin subtree to <dir>
|
|
70
|
+
(default: <package root>/claude)
|
|
71
|
+
--dry-run Print what would be generated without writing
|
|
72
|
+
Options (copilot):
|
|
73
|
+
--output <dir> Write the flat *.agent.md files to <dir>
|
|
74
|
+
(default: <package root>/agents — the dir
|
|
75
|
+
plugin.json "agents" scans)
|
|
76
|
+
--dry-run Print what would be generated without writing
|
|
77
|
+
Options (pi):
|
|
78
|
+
--dry-run Show what would be installed without writing
|
|
79
|
+
--output <dir> Install into <dir>/agents and <dir>/skills
|
|
80
|
+
(default: ~/.pi/agent)
|
|
81
|
+
--plugin-root <dir> Override the plugin root used for {{PATH:...}}
|
|
82
|
+
tokens (default: token-map.json's pi value)
|
|
24
83
|
--help, -h Show this help text`;
|
|
25
84
|
}
|
|
26
85
|
|
|
@@ -34,4 +93,4 @@ export function validateHarness(harness) {
|
|
|
34
93
|
return { ok: true };
|
|
35
94
|
}
|
|
36
95
|
|
|
37
|
-
export { SUPPORTED_HARNESSES };
|
|
96
|
+
export { SUPPORTED_HARNESSES };
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaybeeuu/agent-cortex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.1",
|
|
4
4
|
"description": "Personal PI package with custom agents, skills, and extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"repository":
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "https://github.com/jaybeeuu/agent-cortex"
|
|
12
|
-
},
|
|
9
|
+
"repository": "https://github.com/jaybeeuu/agent-cortex",
|
|
13
10
|
"bin": {
|
|
14
11
|
"agent-cortex": "./bin/agent-cortex.mjs"
|
|
15
12
|
},
|
|
@@ -17,6 +14,7 @@
|
|
|
17
14
|
"access": "public"
|
|
18
15
|
},
|
|
19
16
|
"devDependencies": {
|
|
17
|
+
"@changesets/cli": "^2.31.1",
|
|
20
18
|
"tsx": "^4.0.0",
|
|
21
19
|
"yaml": "^2.0.0"
|
|
22
20
|
},
|
|
@@ -31,9 +29,14 @@
|
|
|
31
29
|
"scripts": {
|
|
32
30
|
"build": "echo 'Build complete — no compilation needed for PI package'",
|
|
33
31
|
"lint": "pnpm typecheck",
|
|
34
|
-
"test": "pnpm -r test",
|
|
32
|
+
"test": "node --test test/*.test.mjs && pnpm -r test",
|
|
35
33
|
"typecheck": "pnpm -r typecheck",
|
|
36
|
-
"build:claude": "node
|
|
34
|
+
"build:claude": "node bin/agent-cortex.mjs install claude",
|
|
35
|
+
"build:copilot": "node scripts/build-copilot-agents.mjs",
|
|
36
|
+
"changeset": "changeset",
|
|
37
|
+
"version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:claude && pnpm build:copilot",
|
|
38
|
+
"publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
|
|
39
|
+
"release": "changeset publish",
|
|
37
40
|
"postinstall": "bash scripts/install-instructions.sh"
|
|
38
41
|
}
|
|
39
42
|
}
|
package/plugin.json
CHANGED
package/pnpm-workspace.yaml
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Regenerates the committed Copilot CLI flat agent files (agents/*.agent.md)
|
|
2
|
+
// from the canonical composable agent sources (agents/<name>/).
|
|
3
|
+
//
|
|
4
|
+
// This script is a thin wrapper over the shared install-time generator
|
|
5
|
+
// (bin/installers/copilot.mjs) — `agent-cortex install copilot` runs exactly the
|
|
6
|
+
// same code path, so install-time and build-time output can never diverge. CI
|
|
7
|
+
// runs this and checks `git diff --exit-code -- 'agents/*.agent.md'` to
|
|
8
|
+
// guarantee the committed flat files are never stale: a fresh clone (or a
|
|
9
|
+
// direct path plugin install) ships the Copilot plugin read-ready.
|
|
10
|
+
//
|
|
11
|
+
// agents/<name>.agent.md composed from agents/<name>/agent.md +
|
|
12
|
+
// agents/<name>/copilot/frontmatter.json +
|
|
13
|
+
// agents/<name>/copilot/<section>.md ({{SECTION:...}}),
|
|
14
|
+
// with {{TOOL:...}}/{{PATH:...}} substituted against
|
|
15
|
+
// the copilot column of token-map.json.
|
|
16
|
+
//
|
|
17
|
+
// The flat files live in the same agents/ directory the Copilot plugin scans
|
|
18
|
+
// (plugin.json "agents": "agents/"), next to the composable dirs they come
|
|
19
|
+
// from — the generator only writes *.agent.md files and never deletes.
|
|
20
|
+
//
|
|
21
|
+
// Run: pnpm build:copilot (node scripts/build-copilot-agents.mjs)
|
|
22
|
+
// Zero dependencies so it runs on the CI Node (20) and local Node alike.
|
|
23
|
+
|
|
24
|
+
import { join, dirname } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { installCopilot } from "../bin/installers/copilot.mjs";
|
|
27
|
+
|
|
28
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
|
+
|
|
30
|
+
installCopilot({ root: ROOT, output: join(ROOT, "agents") });
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Shared composer: reads a composable agent directory (agents/<name>/) and emits
|
|
2
|
+
// the harness-specific agent content defined by agents/README.md. The Copilot
|
|
3
|
+
// generator (scripts/build-copilot-agents.mjs) and the Claude install-time generator
|
|
4
|
+
// (bin/installers/claude.mjs) compose from here, so the composable dirs are
|
|
5
|
+
// the single source of truth and the flat/generated files stay in lockstep.
|
|
6
|
+
//
|
|
7
|
+
// Composition per the agents/README.md contract:
|
|
8
|
+
// 1. {{SECTION:name}} is replaced with <dir>/<harness>/<name>.md content.
|
|
9
|
+
// 2. {{TOOL:key}} is replaced with the harness column of token-map.json.
|
|
10
|
+
// 3. {{PATH:arg}} with a named key resolves through token-map.json's paths
|
|
11
|
+
// table; anything else is a relative path and stays verbatim.
|
|
12
|
+
// Substitution order is SECTION → TOOL → PATH (matches extensions/agent-modes).
|
|
13
|
+
// Missing sections, unknown tool keys, or schema-invalid frontmatter are hard
|
|
14
|
+
// errors: a committed generated file must never ship a literal token or a
|
|
15
|
+
// silently dropped section.
|
|
16
|
+
//
|
|
17
|
+
// Options (the pi installer passes `dropNullTools`, `pluginRoot` and
|
|
18
|
+
// `resolveRelativePaths`; the copilot generator and claude installer use the strict defaults):
|
|
19
|
+
// dropNullTools replace null-mapped tool tokens with an empty string and
|
|
20
|
+
// warn instead of throwing (pi's prose rule per the
|
|
21
|
+
// token-map contract)
|
|
22
|
+
// pluginRoot override the plugin_root used for resolved paths
|
|
23
|
+
// (installers compute it from the actual install layout)
|
|
24
|
+
// resolveRelativePaths non-keyed {{PATH:...}} args resolve to pluginRoot/arg
|
|
25
|
+
// instead of staying verbatim (install time; the build
|
|
26
|
+
// scripts leave them bare for Copilot CLI resolution)
|
|
27
|
+
// warn callback for drop/omit warnings (default: no-op; the
|
|
28
|
+
// pi installer collects them into its result)
|
|
29
|
+
//
|
|
30
|
+
// Zero dependencies so it runs on the CI Node (20) and local Node alike.
|
|
31
|
+
|
|
32
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
|
|
35
|
+
// ─── Token substitution ──────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const TOOL_RE = /\{\{TOOL:([^}]+)\}\}/g;
|
|
38
|
+
const PATH_RE = /\{\{PATH:([^}]+)\}\}/g;
|
|
39
|
+
|
|
40
|
+
function noop() {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Substitute {{TOOL:...}} and {{PATH:...}} tokens in arbitrary content (agent
|
|
44
|
+
* bodies, skill files) against one harness column of token-map.json.
|
|
45
|
+
*
|
|
46
|
+
* - TOOL: unknown key → throw (the map must stay complete); null mapping → drop
|
|
47
|
+
* with a warning when `dropNullTools`, otherwise throw.
|
|
48
|
+
* - PATH: named key → resolve through the paths table (plugin_root overridable);
|
|
49
|
+
* non-keyed → join(pluginRoot, arg) when `resolveRelativePaths`, else verbatim.
|
|
50
|
+
* - SECTION tokens are *not* handled here — the agent composer resolves them from
|
|
51
|
+
* the harness's section files before calling this.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} content Source text containing {{TOOL:...}}/{{PATH:...}} tokens
|
|
54
|
+
* @param {string} harness Harness id: "copilot" | "claude" | "pi"
|
|
55
|
+
* @param {object} tokenMap Parsed token-map.json (from loadTokenMap)
|
|
56
|
+
* @param {object} options { dropNullTools, pluginRoot, resolveRelativePaths, warn, context }
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function substituteTokens(content, harness, tokenMap, options = {}) {
|
|
60
|
+
const { dropNullTools = false, pluginRoot, resolveRelativePaths = false, warn = noop, context = "" } = options;
|
|
61
|
+
const ctx = context ? `${context}: ` : "";
|
|
62
|
+
|
|
63
|
+
// 2. {{TOOL:key}} → token-map.json tools.<key>.<harness>
|
|
64
|
+
content = content.replace(TOOL_RE, (_token, key) => {
|
|
65
|
+
const entry = tokenMap.tools[key];
|
|
66
|
+
if (!entry) throw new Error(`${ctx}unknown tool token {{TOOL:${key}}} — extend token-map.json`);
|
|
67
|
+
const mapped = entry[harness];
|
|
68
|
+
if (mapped == null) {
|
|
69
|
+
if (dropNullTools) {
|
|
70
|
+
warn(`tool token {{TOOL:${key}}} has no ${harness} mapping — dropped`);
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`${ctx}tool token {{TOOL:${key}}} has no ${harness} mapping (null) — drop the token in agent.md first`);
|
|
74
|
+
}
|
|
75
|
+
return mapped;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 3. {{PATH:arg}} → named keys via token-map.json paths; relative paths verbatim
|
|
79
|
+
// (or resolved against pluginRoot when the installer option is set)
|
|
80
|
+
content = content.replace(PATH_RE, (_token, arg) =>
|
|
81
|
+
resolvePath(arg, harness, tokenMap, { pluginRoot, resolveRelativePaths }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return content;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compose the harness-specific agent content from a composable agent directory.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} root Package root (contains agents/ and token-map.json)
|
|
91
|
+
* @param {string} name Agent directory name (e.g. "ralph", "plan")
|
|
92
|
+
* @param {string} harness Harness id: "copilot" | "claude" | "pi"
|
|
93
|
+
* @returns {{ name: string, description: string, tools: string[], model?: string, argumentHint?: string, body: string }}
|
|
94
|
+
*/
|
|
95
|
+
export function composeAgent(root, name, harness, options = {}) {
|
|
96
|
+
const { dropNullTools = false, pluginRoot, resolveRelativePaths = false, warn = noop } = options;
|
|
97
|
+
const agentDir = join(root, "agents", name);
|
|
98
|
+
const frontmatter = parseFrontmatter(join(agentDir, harness, "frontmatter.json"), name, harness);
|
|
99
|
+
const tokenMap = loadTokenMap(root);
|
|
100
|
+
|
|
101
|
+
let body = readFileSync(join(agentDir, "agent.md"), "utf-8");
|
|
102
|
+
|
|
103
|
+
// 1. {{SECTION:name}} → <harness>/<name>.md (may itself contain TOOL/PATH tokens)
|
|
104
|
+
body = body.replace(/\{\{SECTION:([^}]+)\}\}/g, (_token, section) => {
|
|
105
|
+
const sectionPath = join(agentDir, harness, `${section}.md`);
|
|
106
|
+
if (!existsSync(sectionPath)) {
|
|
107
|
+
throw new Error(`agent "${name}": missing section "${section}" for harness "${harness}" (${sectionPath})`);
|
|
108
|
+
}
|
|
109
|
+
return readFileSync(sectionPath, "utf-8").trim();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 2. + 3. TOOL/PATH substitution (contextualised for error messages)
|
|
113
|
+
body = substituteTokens(body, harness, tokenMap, {
|
|
114
|
+
dropNullTools,
|
|
115
|
+
pluginRoot,
|
|
116
|
+
resolveRelativePaths,
|
|
117
|
+
warn,
|
|
118
|
+
context: `agent "${name}"`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return { ...frontmatter, body: body.trim() };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Frontmatter ─────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
const REQUIRED_FIELDS = ["name", "description", "tools"];
|
|
127
|
+
const KNOWN_OPTIONAL_FIELDS = ["model", "argumentHint"];
|
|
128
|
+
|
|
129
|
+
/** Parse and schema-check a harness frontmatter.json (strict, per agents/README.md). */
|
|
130
|
+
function parseFrontmatter(path, name, harness) {
|
|
131
|
+
const raw = readFileSync(path, "utf-8");
|
|
132
|
+
let fm;
|
|
133
|
+
try {
|
|
134
|
+
fm = JSON.parse(raw);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`agent "${name}": invalid JSON in ${path}: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
if (typeof fm !== "object" || fm === null) {
|
|
139
|
+
throw new Error(`agent "${name}": ${path} must be a JSON object`);
|
|
140
|
+
}
|
|
141
|
+
for (const field of REQUIRED_FIELDS) {
|
|
142
|
+
if (!(field in fm)) throw new Error(`agent "${name}": ${path} missing required field "${field}" (harness ${harness})`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof fm.name !== "string" || fm.name.trim() === "") throw new Error(`agent "${name}": ${path} name must be a non-empty string`);
|
|
145
|
+
if (typeof fm.description !== "string" || fm.description.trim() === "") throw new Error(`agent "${name}": ${path} description must be a non-empty string`);
|
|
146
|
+
if (!Array.isArray(fm.tools) || fm.tools.length === 0 || !fm.tools.every((t) => typeof t === "string")) {
|
|
147
|
+
throw new Error(`agent "${name}": ${path} tools must be a non-empty array of strings`);
|
|
148
|
+
}
|
|
149
|
+
for (const key of Object.keys(fm)) {
|
|
150
|
+
if (![...REQUIRED_FIELDS, ...KNOWN_OPTIONAL_FIELDS].includes(key)) {
|
|
151
|
+
throw new Error(`agent "${name}": ${path} unknown field "${key}" — strict schema per agents/README.md`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
name: fm.name,
|
|
156
|
+
description: fm.description,
|
|
157
|
+
tools: fm.tools,
|
|
158
|
+
...(typeof fm.model === "string" ? { model: fm.model } : {}),
|
|
159
|
+
...(typeof fm.argumentHint === "string" ? { argumentHint: fm.argumentHint } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Path resolution ─────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a {{PATH:...}} argument for a harness's generated output.
|
|
167
|
+
* Named keys resolve through token-map.json's paths table (recursively for
|
|
168
|
+
* {base, relative} specs); non-keyed args are relative paths, kept verbatim —
|
|
169
|
+
* the installer beads own precise runtime path resolution (see token-map.README.md).
|
|
170
|
+
* With the installer options (`pluginRoot`, `resolveRelativePaths`) the
|
|
171
|
+
* plugin_root base and bare relative paths resolve against the actual install
|
|
172
|
+
* layout instead.
|
|
173
|
+
*/
|
|
174
|
+
function resolvePath(arg, harness, tokenMap, opts = {}) {
|
|
175
|
+
const entry = tokenMap.paths?.[arg];
|
|
176
|
+
if (!entry) {
|
|
177
|
+
if (opts.resolveRelativePaths && opts.pluginRoot) return join(opts.pluginRoot, arg);
|
|
178
|
+
return arg;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const spec = entry[harness];
|
|
182
|
+
if (typeof spec === "string") {
|
|
183
|
+
if (arg === "plugin_root" && opts.pluginRoot) return opts.pluginRoot;
|
|
184
|
+
return spec;
|
|
185
|
+
}
|
|
186
|
+
if (spec && typeof spec === "object" && typeof spec.base === "string" && typeof spec.relative === "string") {
|
|
187
|
+
return join(resolvePath(spec.base, harness, tokenMap, opts), spec.relative);
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`unresolvable {{PATH:${arg}}} for harness "${harness}" — extend token-map.json`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Translate a frontmatter tool list into one harness's tool names.
|
|
194
|
+
*
|
|
195
|
+
* Canonical keys resolve through token-map.json's tools table; a null mapping
|
|
196
|
+
* means the harness has no equivalent and the entry is omitted (the contract's
|
|
197
|
+
* tool-list rule); a name that is not a canonical key passes through untouched
|
|
198
|
+
* (it may be a native tool, e.g. pi's fetch_content). Mirrors the runtime
|
|
199
|
+
* translation in extensions/agent-modes/discover.ts.
|
|
200
|
+
*
|
|
201
|
+
* @param {string[]} tools Canonical tool names from a harness frontmatter.json
|
|
202
|
+
* @param {object} tokenMap Parsed token-map.json (from loadTokenMap)
|
|
203
|
+
* @param {string} harness Harness id: "copilot" | "claude" | "pi"
|
|
204
|
+
* @param {object} options { warn }
|
|
205
|
+
* @returns {string[]}
|
|
206
|
+
*/
|
|
207
|
+
export function translateToolList(tools, tokenMap, harness, { warn = noop } = {}) {
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const tool of tools) {
|
|
210
|
+
const mapping = tokenMap.tools[tool];
|
|
211
|
+
if (mapping === undefined) {
|
|
212
|
+
out.push(tool);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const mapped = mapping[harness];
|
|
216
|
+
if (mapped == null) {
|
|
217
|
+
warn(`tool "${tool}" has no ${harness} mapping — omitted from the tool list`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
out.push(mapped);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Token map ───────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/** Load token-map.json from the package root; missing/incomplete maps hard-fail. */
|
|
228
|
+
export function loadTokenMap(root) {
|
|
229
|
+
const path = join(root, "token-map.json");
|
|
230
|
+
let raw;
|
|
231
|
+
try {
|
|
232
|
+
raw = readFileSync(path, "utf-8");
|
|
233
|
+
} catch {
|
|
234
|
+
throw new Error(`missing ${path} — the composer requires token-map.json`);
|
|
235
|
+
}
|
|
236
|
+
const parsed = JSON.parse(raw);
|
|
237
|
+
if (!parsed.tools || !parsed.paths) {
|
|
238
|
+
throw new Error(`${path}: expected "tools" and "paths" tables`);
|
|
239
|
+
}
|
|
240
|
+
return parsed;
|
|
241
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sync plugin.json version with package.json
|
|
3
|
+
# Called by changesets after versioning to keep them in lockstep
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
8
|
+
|
|
9
|
+
# Update plugin.json version field
|
|
10
|
+
node -e "
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const plugin = JSON.parse(fs.readFileSync('plugin.json', 'utf8'));
|
|
13
|
+
plugin.version = '$VERSION';
|
|
14
|
+
fs.writeFileSync('plugin.json', JSON.stringify(plugin, null, 2) + '\n');
|
|
15
|
+
"
|
|
16
|
+
|
|
17
|
+
echo "✓ Synced plugin.json to version $VERSION"
|
|
@@ -23,7 +23,7 @@ Ask: "What does this module need to do? Who will use it?"
|
|
|
23
23
|
|
|
24
24
|
### 2. Generate Designs (Parallel Sub-Agents)
|
|
25
25
|
|
|
26
|
-
Spawn 3+ sub-agents simultaneously using
|
|
26
|
+
Spawn 3+ sub-agents simultaneously using {{TOOL:task}}. Each must produce a **radically different** approach.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
Prompt template for each sub-agent:
|
|
@@ -13,7 +13,7 @@ A **deep module** (John Ousterhout, "A Philosophy of Software Design") has a sma
|
|
|
13
13
|
|
|
14
14
|
### 1. Explore the codebase
|
|
15
15
|
|
|
16
|
-
Explore the codebase using your available tools (bash, rg, view, glob
|
|
16
|
+
Explore the codebase using your available tools ({{TOOL:bash}}, {{TOOL:rg}}, {{TOOL:view}}, {{TOOL:glob}}). Navigate organically and note where you experience friction:
|
|
17
17
|
|
|
18
18
|
- Where does understanding one concept require bouncing between many small files?
|
|
19
19
|
- Where are modules so shallow that the interface is nearly as complex as the implementation?
|