@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,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/claude/hooks.json
CHANGED
|
@@ -12,12 +12,27 @@
|
|
|
12
12
|
"type": "command",
|
|
13
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
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
|
+
},
|
|
15
19
|
{
|
|
16
20
|
"type": "command",
|
|
17
21
|
"command": "echo 'Be extremely concise. Sacrifice grammar for the sake of concision.'"
|
|
18
22
|
}
|
|
19
23
|
]
|
|
20
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
|
+
}
|
|
21
36
|
]
|
|
22
37
|
}
|
|
23
38
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Claude Code hooks — extension port audit
|
|
2
|
+
|
|
3
|
+
The pi extensions in `extensions/` were audited for Claude Code equivalents.
|
|
4
|
+
Implemented ports live in `hooks/claude/hooks.json` (copied verbatim into the
|
|
5
|
+
generated plugin as `claude/hooks.json`); support scripts live in
|
|
6
|
+
`hooks/claude/scripts/` and are bundled into `claude/hooks/scripts/` by
|
|
7
|
+
`bin/installers/claude.mjs` (referenced from hooks.json via `$CLAUDE_PLUGIN_ROOT`).
|
|
8
|
+
|
|
9
|
+
Claude Code's hook model differs from PI's: hooks are shell commands fed a JSON
|
|
10
|
+
payload on stdin, with events like `SessionStart`, `SessionEnd`, `Stop`,
|
|
11
|
+
`SubagentStop`, and `Notification` — there is no `before_agent_start` /
|
|
12
|
+
`agent_end` / `tool_call` extension API, no runtime tool registration, and no
|
|
13
|
+
UI surface. Each decision below is grounded in the Claude Code hooks reference.
|
|
14
|
+
|
|
15
|
+
| pi extension | Claude port | Decision |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `session-start.ts` | `SessionStart` hook commands | **Port** (extended) |
|
|
18
|
+
| `auto-name.ts` | — | **Reject** — built-in |
|
|
19
|
+
| `notify/` | `Notification` hook + `scripts/notify.mjs` | **Port** |
|
|
20
|
+
| `skill-stats/` | — | **Reject** — no equivalent |
|
|
21
|
+
| `subagent/` | — | **Reject** — native |
|
|
22
|
+
| `agent-modes/` | — | **Reject** — no equivalent |
|
|
23
|
+
|
|
24
|
+
## `session-start.ts` → SessionStart (port)
|
|
25
|
+
|
|
26
|
+
pi injects skill-availability hints + project context files (AGENTS.md,
|
|
27
|
+
`docs/user-preferences.md`) into every session. Claude Code already loads
|
|
28
|
+
`AGENTS.md` natively and the existing SessionStart hook nudge ("Load AGENTS.md
|
|
29
|
+
and active beads context…") plus the style-skill policy (PR #57) cover the
|
|
30
|
+
context side. This bead closes the remaining gap: a skill-policy command listing
|
|
31
|
+
the meta-skills (`using-agent-skills` for routing, `bd-tool` for beads context,
|
|
32
|
+
`git-workflow` for branch/PR discipline). SessionStart command-hook stdout is
|
|
33
|
+
added to Claude's context, so plain `echo` commands are the right shape —
|
|
34
|
+
keep them fast and dependency-free. Skipped from the pi original: the dynamic
|
|
35
|
+
file read of AGENTS.md/user-preferences.md, since Claude Code loads AGENTS.md
|
|
36
|
+
itself and user-preferences.md is a pi-harness document.
|
|
37
|
+
|
|
38
|
+
## `auto-name.ts` → Reject (built-in)
|
|
39
|
+
|
|
40
|
+
pi names sessions via a tiny side-model call on the first prompt. Claude Code
|
|
41
|
+
already auto-generates descriptive session titles (visible in `claude --resume`;
|
|
42
|
+
a background model request in interactive and headless/SDK sessions) and supports
|
|
43
|
+
explicit naming via `--name` / `/rename`. A hook-based title generator would
|
|
44
|
+
duplicate platform behaviour with an extra LLM call — rejected. (SessionStart
|
|
45
|
+
hooks can also set `sessionTitle` in JSON output for the rare case where a
|
|
46
|
+
custom title is genuinely needed.)
|
|
47
|
+
|
|
48
|
+
## `notify/` → Notification hook (port)
|
|
49
|
+
|
|
50
|
+
pi notifies on `agent_end` for multi-turn/long tasks with a desktop notification
|
|
51
|
+
+ bell. Claude Code's native `Notification` event fires at the equivalent
|
|
52
|
+
completion points and already carries `message`, `title`, and `notification_type`
|
|
53
|
+
in the payload — and hooks emit desktop notifications by returning a
|
|
54
|
+
`terminalSequence` JSON field (hooks have no controlling terminal; Claude Code
|
|
55
|
+
emits the sequence itself, including inside tmux/screen).
|
|
56
|
+
|
|
57
|
+
Implemented as a `Notification` hook with matcher
|
|
58
|
+
`agent_completed|agent_needs_input|permission_prompt` invoking `scripts/notify.mjs`,
|
|
59
|
+
which reads the payload from stdin and prints the OSC sequence for the detected
|
|
60
|
+
terminal (Kitty OSC 99, iTerm2/Windows Terminal OSC 9, default urxvt/Ghostty/Warp
|
|
61
|
+
OSC 777 — same detection as the pi `notify` extension). The Notification matcher
|
|
62
|
+
is an **exact-string list** over the `notification_type` field; there are no
|
|
63
|
+
`success`/`error` types (an earlier draft used that matcher, which silently never
|
|
64
|
+
fired). The documented types are `permission_prompt`, `idle_prompt`, `auth_success`,
|
|
65
|
+
`elicitation_dialog`, `elicitation_url_dialog`, `elicitation_complete`,
|
|
66
|
+
`elicitation_response`, `agent_needs_input`, `agent_completed`,
|
|
67
|
+
`quota_auto_resume_fired`, `quota_auto_resume_stale`, `quota_auto_resume_disabled`
|
|
68
|
+
(the `agent_*` types require Claude Code v2.1.198+, the `quota_auto_resume_*`
|
|
69
|
+
types v2.1.234+). The selection maps to pi's "task completed / failed / needs
|
|
70
|
+
approval" semantics: `agent_completed` covers completion and failure ("finishes
|
|
71
|
+
or fails"), `agent_needs_input` covers a background session waiting on input, and
|
|
72
|
+
`permission_prompt` covers an awaited approval. Explicitly excluded as noise:
|
|
73
|
+
`idle_prompt` (fires ~60s after every turn you don't answer within a minute — a
|
|
74
|
+
desktop ping on every pause), `auth_success` and `quota_auto_resume_*` (not task
|
|
75
|
+
events), and `elicitation_*` (dialog trivia). Skipped from the pi original: the LLM
|
|
76
|
+
response summary (Claude's notification message is already the summary) and the tmux
|
|
77
|
+
session:window.pane title (unavailable to hooks; the plugin title suffices).
|
|
78
|
+
|
|
79
|
+
## `skill-stats/` → Reject (no equivalent)
|
|
80
|
+
|
|
81
|
+
pi tracks skills loaded into the system prompt, `/skill:` invocations, SKILL.md
|
|
82
|
+
reads, and turns, exposing a `/skill-stats` query. Claude Code exposes none of
|
|
83
|
+
the required observability to hooks:
|
|
84
|
+
|
|
85
|
+
- **No "loaded" event** — nothing fires when a skill enters context, and hooks
|
|
86
|
+
never see system-prompt contents, so the primary "loaded" metric has no source.
|
|
87
|
+
- **Partial invocation/read observability only** — `PreToolUse` on the `Skill`
|
|
88
|
+
tool and `UserPromptExpansion` on slash commands would approximate
|
|
89
|
+
invocations, and `PostToolUse` on `Read` would approximate skill reads, but
|
|
90
|
+
the signals are fuzzy (Read covers any file; direct `/skill` typing bypasses
|
|
91
|
+
`PreToolUse`).
|
|
92
|
+
- **No query surface** — Claude Code plugin commands are markdown files, not
|
|
93
|
+
scripts, so there is no equivalent of the pi `/skill-stats` command, and
|
|
94
|
+
persistent state would live outside the plugin model.
|
|
95
|
+
|
|
96
|
+
A half-observable port would diverge from the pi semantics and add moving parts
|
|
97
|
+
for little signal. Rejected; Claude Code ships its own usage analytics
|
|
98
|
+
(`/usage`, sessions list, statsig) if insights are needed.
|
|
99
|
+
|
|
100
|
+
## `subagent/` → Reject (native)
|
|
101
|
+
|
|
102
|
+
pi's `subagent` tool spawns isolated `pi --mode json` processes. Claude Code has
|
|
103
|
+
native subagents — the plugin already ships `claude/agents/*.md` (including
|
|
104
|
+
`ralph`, `plan`, `strategy`, `ralph-plan`) executed via the built-in Agent/Task
|
|
105
|
+
tools with per-session context isolation — so a hook-based workaround would be
|
|
106
|
+
strictly worse.
|
|
107
|
+
|
|
108
|
+
## `agent-modes/` → Reject (no equivalent)
|
|
109
|
+
|
|
110
|
+
pi switches system prompts + tool restrictions at runtime with a TUI mode
|
|
111
|
+
selector. Claude Code exposes no hook or settings surface for dynamic
|
|
112
|
+
tool-restriction/prompt-switching of the main agent; the closest equivalents are
|
|
113
|
+
native (permission modes like plan mode configured via settings, and subagents
|
|
114
|
+
— already covered above). The `--agent <name>` start flag is covered by
|
|
115
|
+
`claude --agent` in newer versions. Rejected.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Idea: Unified CI Pipeline with Parallel Checks
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
Backlog idea (not implementation-ready)
|
|
5
|
+
|
|
6
|
+
## Why it might be useful
|
|
7
|
+
The current CI setup has two problems:
|
|
8
|
+
1. **Slow feedback** — checks run serially, making the pipeline slower than it needs to be.
|
|
9
|
+
2. **Broken releases** — the release job (version branch creation) is a separate pipeline, so it can fire even when checks fail. A broken commit can reach the version branch.
|
|
10
|
+
|
|
11
|
+
Combining them gives faster feedback (parallel checks) and a safety gate (release step only runs if all checks pass).
|
|
12
|
+
|
|
13
|
+
## How we might do it
|
|
14
|
+
- Merge the check pipeline and release pipeline into a single pipeline.
|
|
15
|
+
- Fan out all checks to run in parallel.
|
|
16
|
+
- Make the version branch creation a downstream job/step that depends on all checks passing.
|
|
17
|
+
|
|
18
|
+
## When to think about it
|
|
19
|
+
When CI pipeline speed or release safety becomes a recurring friction — e.g. after the changesets/release workflow is fully set up and seeing real traffic.
|
|
20
|
+
|
|
21
|
+
## Priority
|
|
22
|
+
P2 — infrastructure quality-of-life. Not blocking current work, but a clear improvement once the release flow stabilises.
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
- Setup job with artifact infrastructure added (2026-08): install + build now happen once in a `setup` job, artifacts are shared with downstream jobs via upload/download-artifact. This is the first step toward the unified pipeline vision.
|
|
26
|
+
- Depends on the changesets release workflow being in place (P1 beads `agnt-ctx-d1ht`, `agnt-ctx-5bcu`).
|
|
27
|
+
- Should be straightforward once the pipeline shape is settled.
|
|
28
|
+
- **Release pipeline fires on every push to main** — this wastes CI minutes. It should only run when there are changesets to process (e.g. when the version packages PR is merged, or when new changesets land on main). Add a path filter or a pre-check that skips the job when no `.changeset/*.md` files changed.
|
|
@@ -29,6 +29,14 @@ Optional fields:
|
|
|
29
29
|
- Add one rationale sentence for non-obvious rules so behavior generalizes in novel cases.
|
|
30
30
|
- Avoid ALL-CAPS emphasis.
|
|
31
31
|
|
|
32
|
+
## Tool references
|
|
33
|
+
|
|
34
|
+
Reference agent tools by `{{TOOL:name}}` token (canonical copilot name), never a
|
|
35
|
+
hardcoded harness-specific name such as `bash`, `task`, or `read_agent` — installers
|
|
36
|
+
substitute the per-harness name from `token-map.json` at install time. Add any new tool
|
|
37
|
+
to the map's `tools` section in the same change; unknown tokens are hard errors. See
|
|
38
|
+
`token-map.README.md` for the full authoring rules.
|
|
39
|
+
|
|
32
40
|
## Required sections
|
|
33
41
|
|
|
34
42
|
Every skill must include:
|
|
@@ -81,6 +89,7 @@ Provide completion gates that are testable and stage-scoped.
|
|
|
81
89
|
|
|
82
90
|
A skill passes anatomy review when:
|
|
83
91
|
- [ ] Front matter uses required fields and valid description format.
|
|
92
|
+
- [ ] Tool references use `{{TOOL:name}}` tokens, not hardcoded tool names.
|
|
84
93
|
- [ ] Required sections exist and follow canonical ordering.
|
|
85
94
|
- [ ] Optional sections (if present) include what/when/example guidance.
|
|
86
95
|
- [ ] Verification checklist contains concrete, machine-checkable items.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Agent Modes Extension
|
|
2
|
+
|
|
3
|
+
Makes the agent-cortex agents (ralph, ralph-plan, plan, strategy) available as
|
|
4
|
+
switchable PI modes. Each mode injects the agent's full system prompt into the
|
|
5
|
+
running context, restricts tools to the agent's declared tool set, and shows a
|
|
6
|
+
status indicator with the active agent name.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
| Command | Description |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `/agent` | Show the agent mode selector |
|
|
13
|
+
| `/agent <name>` | Switch to an agent mode directly (e.g. `/agent ralph`) |
|
|
14
|
+
| `/agent default` | Return to default mode (full tool access) |
|
|
15
|
+
| `Ctrl+Shift+A` | Cycle through agent modes |
|
|
16
|
+
| `--agent <name>` | Start PI in a specific agent mode |
|
|
17
|
+
|
|
18
|
+
## How agents are discovered
|
|
19
|
+
|
|
20
|
+
Agents are read from the **composable source format** in the package's
|
|
21
|
+
`agents/` directory:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
agents/<name>/
|
|
25
|
+
├── agent.md # Shared body (harness-agnostic)
|
|
26
|
+
└── pi/
|
|
27
|
+
├── frontmatter.json # PI metadata: name, description, tools
|
|
28
|
+
└── <section>.md # Optional sections referenced by {{SECTION:name}}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The PI system prompt is composed from `agent.md` per the token-map.json
|
|
32
|
+
contract:
|
|
33
|
+
|
|
34
|
+
1. `{{SECTION:name}}` tokens are replaced with the agent's `pi/<name>.md`.
|
|
35
|
+
2. `{{TOOL:key}}` / `{{PATH:key}}` tokens are substituted against the pi column
|
|
36
|
+
of `token-map.json`, with the plugin root resolved from the actual extension
|
|
37
|
+
location. Tools without a PI equivalent (`ask_user`, `skill`) are omitted
|
|
38
|
+
from the tool set.
|
|
39
|
+
|
|
40
|
+
Flat `agents/*.agent.md` files are parsed as a fallback when an agent has no
|
|
41
|
+
composable directory — they are generated output (scripts/build-copilot-agents.mjs)
|
|
42
|
+
but may exist for non-migrated agents. A composable directory always wins over its
|
|
43
|
+
flat counterpart for the same agent id.
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
Discovery and prompt composition live in `discover.ts` (unit-tested against
|
|
48
|
+
fixture directories in `discover.test.ts`); `index.ts` only wires discovery
|
|
49
|
+
into the extension API.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm test # run discover.test.ts
|
|
53
|
+
pnpm typecheck # tsc --noEmit
|
|
54
|
+
```
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type AgentDef,
|
|
10
|
+
type TokenMap,
|
|
11
|
+
discoverAgents,
|
|
12
|
+
loadTokenMap,
|
|
13
|
+
} from "./discover.ts";
|
|
14
|
+
|
|
15
|
+
// Real token-map.json at the package root — the canonical tool/path map the
|
|
16
|
+
// extension ships with. Used to verify translation against the source of truth.
|
|
17
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
18
|
+
const REAL_TOKEN_MAP = loadTokenMap(join(REPO_ROOT, "agents"));
|
|
19
|
+
|
|
20
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface Fixture {
|
|
23
|
+
dir: string;
|
|
24
|
+
agentsDir: string;
|
|
25
|
+
cleanup: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeFixture(): Fixture {
|
|
29
|
+
const dir = mkdtempSync(join(tmpdir(), "agent-modes-"));
|
|
30
|
+
return {
|
|
31
|
+
dir,
|
|
32
|
+
agentsDir: join(dir, "agents"),
|
|
33
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeFixture(fx: Fixture, relPath: string, content: string): void {
|
|
38
|
+
const p = join(fx.dir, relPath);
|
|
39
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
40
|
+
writeFileSync(p, content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeComposableAgent(
|
|
44
|
+
fx: Fixture,
|
|
45
|
+
name: string,
|
|
46
|
+
opts: {
|
|
47
|
+
body?: string;
|
|
48
|
+
frontmatter?: Record<string, unknown>;
|
|
49
|
+
sections?: Record<string, string>;
|
|
50
|
+
} = {},
|
|
51
|
+
): void {
|
|
52
|
+
const base = `agents/${name}`;
|
|
53
|
+
writeFixture(fx, `${base}/agent.md`, opts.body ?? `# ${name}\nBody of ${name}.`);
|
|
54
|
+
writeFixture(
|
|
55
|
+
fx,
|
|
56
|
+
`${base}/pi/frontmatter.json`,
|
|
57
|
+
JSON.stringify(
|
|
58
|
+
opts.frontmatter ?? {
|
|
59
|
+
name: `agent-cortex:${name}`,
|
|
60
|
+
description: `Description of ${name}`,
|
|
61
|
+
tools: ["bash", "view", "task"],
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
for (const [section, body] of Object.entries(opts.sections ?? {})) {
|
|
66
|
+
writeFixture(fx, `${base}/pi/${section}.md`, body);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeFlatAgent(fx: Fixture, name: string, body: string): void {
|
|
71
|
+
writeFixture(fx, `agents/${name}.agent.md`, body);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fixtures: Fixture[] = [];
|
|
75
|
+
function track(fx: Fixture): Fixture {
|
|
76
|
+
fixtures.push(fx);
|
|
77
|
+
return fx;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
after(() => {
|
|
81
|
+
for (const fx of fixtures) fx.cleanup();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const FLAT_FRONTMATTER = (name: string, tools: string) =>
|
|
85
|
+
`---\ndescription: "Flat description of ${name}"\nname: "agent-cortex:${name}"\ntools: ${tools}\n---\n\nFlat body of ${name}.`;
|
|
86
|
+
|
|
87
|
+
// ─── Tokens ───────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const TOKEN_PROSE =
|
|
90
|
+
"Run with {{TOOL:bash}}.\n" +
|
|
91
|
+
"Ask the user with {{TOOL:ask_user}}.\n" +
|
|
92
|
+
"Read the plan skill at {{PATH:skills/workflow/plan/SKILL.md}}.\n" +
|
|
93
|
+
"Agents live in {{PATH:agents_dir}}.";
|
|
94
|
+
|
|
95
|
+
// ─── Composable discovery ────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("discoverAgents (composable source format)", () => {
|
|
98
|
+
it("discovers agents from <name>/agent.md + <name>/pi/frontmatter.json", () => {
|
|
99
|
+
const fx = track(makeFixture());
|
|
100
|
+
writeComposableAgent(fx, "ralph", {
|
|
101
|
+
body: "# ralph\nUse {{TOOL:task}} to spawn subagents.\n\n{{SECTION:polling}}",
|
|
102
|
+
frontmatter: {
|
|
103
|
+
name: "agent-cortex:ralph",
|
|
104
|
+
description: "Runs beads end-to-end",
|
|
105
|
+
tools: ["bash", "view", "rg", "glob", "task", "read_agent"],
|
|
106
|
+
},
|
|
107
|
+
sections: {
|
|
108
|
+
polling: "## PI polling\nPoll with task + read_agent.",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
113
|
+
|
|
114
|
+
assert.equal(agents.length, 1);
|
|
115
|
+
const ralph = agents[0];
|
|
116
|
+
assert.equal(ralph.id, "ralph");
|
|
117
|
+
assert.equal(ralph.name, "agent-cortex:ralph");
|
|
118
|
+
assert.equal(ralph.description, "Runs beads end-to-end");
|
|
119
|
+
// Canonical tool names translated to PI names via token-map (glob→find, rg→grep, view→read).
|
|
120
|
+
assert.deepEqual(ralph.tools, ["bash", "find", "grep", "read", "read_agent", "task"]);
|
|
121
|
+
// SECTION token resolved from pi/<section>.md; TOOL token substituted.
|
|
122
|
+
assert.match(ralph.prompt, /Use task to spawn subagents\./);
|
|
123
|
+
assert.match(ralph.prompt, /Poll with task \+ read_agent\./);
|
|
124
|
+
assert.doesNotMatch(ralph.prompt, /\{\{/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("substitutes TOOL / PATH tokens against token-map.json using the real plugin root", () => {
|
|
128
|
+
const fx = track(makeFixture());
|
|
129
|
+
writeComposableAgent(fx, "plan", {
|
|
130
|
+
body: TOKEN_PROSE,
|
|
131
|
+
frontmatter: { name: "agent-cortex:plan", description: "Planner", tools: [] },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
135
|
+
assert.equal(agents.length, 1);
|
|
136
|
+
const plan = agents[0];
|
|
137
|
+
|
|
138
|
+
// {{TOOL:bash}} → bash.
|
|
139
|
+
assert.match(plan.prompt, /Run with bash\./);
|
|
140
|
+
// {{TOOL:ask_user}} → null for pi — token dropped (dangling prose is the author's
|
|
141
|
+
// responsibility per token-map.README), not left as literal token syntax.
|
|
142
|
+
assert.doesNotMatch(plan.prompt, /\{\{TOOL:ask_user\}\}/);
|
|
143
|
+
// Relative {{PATH:...}} resolves against the actual package root (dir of agents/).
|
|
144
|
+
assert.match(plan.prompt, new RegExp(`Read the plan skill at ${escapeRegExp(fx.dir)}/skills/workflow/plan/SKILL\\.md\\.`));
|
|
145
|
+
// Named {{PATH:agents_dir}} resolves to <root>/agents.
|
|
146
|
+
assert.match(plan.prompt, new RegExp(`Agents live in ${escapeRegExp(fx.agentsDir)}\\.`));
|
|
147
|
+
assert.doesNotMatch(plan.prompt, /\{\{/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("drops null-mapped tools (ask_user, skill) from the PI tool set", () => {
|
|
151
|
+
const fx = track(makeFixture());
|
|
152
|
+
writeComposableAgent(fx, "plan", {
|
|
153
|
+
frontmatter: {
|
|
154
|
+
name: "agent-cortex:plan",
|
|
155
|
+
description: "Planner",
|
|
156
|
+
tools: ["bash", "ask_user", "skill", "web_fetch"],
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
161
|
+
assert.equal(agents.length, 1);
|
|
162
|
+
// ask_user → null (omitted), skill → null (omitted), web_fetch → fetch_content.
|
|
163
|
+
assert.deepEqual(agents[0].tools, ["bash", "fetch_content"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("uses the built-in fallback map when token-map.json is unavailable", () => {
|
|
167
|
+
const fx = track(makeFixture());
|
|
168
|
+
writeComposableAgent(fx, "legacy", {
|
|
169
|
+
frontmatter: {
|
|
170
|
+
name: "agent-cortex:legacy",
|
|
171
|
+
description: "No token map",
|
|
172
|
+
tools: ["view", "glob", "ask_user"],
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const agents = discoverAgents(fx.agentsDir);
|
|
177
|
+
assert.equal(agents.length, 1);
|
|
178
|
+
// Copilot→PI fallback: view→read, glob→find+ls; unmapped ask_user passes through.
|
|
179
|
+
assert.deepEqual(agents[0].tools, ["ask_user", "find", "ls", "read"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("returns an empty list for a missing agents directory", () => {
|
|
183
|
+
const fx = track(makeFixture());
|
|
184
|
+
assert.deepEqual(discoverAgents(join(fx.dir, "does-not-exist"), REAL_TOKEN_MAP), []);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns an empty list when no agents are present", () => {
|
|
188
|
+
const fx = track(makeFixture());
|
|
189
|
+
mkdirSync(fx.agentsDir, { recursive: true });
|
|
190
|
+
assert.deepEqual(discoverAgents(fx.agentsDir, REAL_TOKEN_MAP), []);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ─── Flat fallback ────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
describe("discoverAgents (flat fallback)", () => {
|
|
197
|
+
it("prefers a composable directory over its flat file for the same agent", () => {
|
|
198
|
+
const fx = track(makeFixture());
|
|
199
|
+
writeComposableAgent(fx, "ralph", { body: "Composable body." });
|
|
200
|
+
writeFlatAgent(fx, "ralph", FLAT_FRONTMATTER("ralph", '["bash", "view"]'));
|
|
201
|
+
|
|
202
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
203
|
+
|
|
204
|
+
assert.equal(agents.length, 1);
|
|
205
|
+
assert.equal(agents[0].id, "ralph");
|
|
206
|
+
assert.match(agents[0].prompt, /Composable body\./);
|
|
207
|
+
assert.doesNotMatch(agents[0].prompt, /Flat body/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("falls back to flat *.agent.md for agents without a composable directory", () => {
|
|
211
|
+
const fx = track(makeFixture());
|
|
212
|
+
writeFlatAgent(fx, "strategy", FLAT_FRONTMATTER("strategy", '["bash", "view", "web_fetch"]'));
|
|
213
|
+
|
|
214
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
215
|
+
|
|
216
|
+
assert.equal(agents.length, 1);
|
|
217
|
+
const strategy = agents[0];
|
|
218
|
+
assert.equal(strategy.id, "strategy");
|
|
219
|
+
assert.equal(strategy.name, "agent-cortex:strategy");
|
|
220
|
+
assert.equal(strategy.description, "Flat description of strategy");
|
|
221
|
+
// view→read, web_fetch→fetch_content via token-map.
|
|
222
|
+
assert.deepEqual(strategy.tools, ["bash", "fetch_content", "read"]);
|
|
223
|
+
assert.match(strategy.prompt, /Flat body of strategy\./);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ─── loadTokenMap ────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe("loadTokenMap", () => {
|
|
230
|
+
it("reads the real token-map.json at the package root", () => {
|
|
231
|
+
assert.ok(REAL_TOKEN_MAP);
|
|
232
|
+
assert.equal(typeof REAL_TOKEN_MAP?.tools?.bash?.pi, "string");
|
|
233
|
+
// ask_user has no PI equivalent per the contract — must stay null.
|
|
234
|
+
assert.equal(REAL_TOKEN_MAP?.tools?.ask_user?.pi, null);
|
|
235
|
+
assert.ok(REAL_TOKEN_MAP?.paths?.agents_dir);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
function escapeRegExp(s: string): string {
|
|
240
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
241
|
+
}
|