@jaybeeuu/agent-cortex 1.37.1 → 1.39.0
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 +9 -8
- package/.github/workflows/ci.yml +190 -8
- package/AGENTS.md +27 -6
- package/CHANGELOG.md +72 -0
- package/README.md +156 -39
- package/agents/README.md +49 -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 +6 -3
- 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 +76 -1
- package/bin/installers/claude.mjs +376 -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 +6 -4
- 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/auto-model-selection.md +75 -0
- package/docs/ideas/extension-manifests.md +75 -0
- package/docs/ideas/improve-pi-system-prompt.md +34 -0
- package/docs/ideas/model-selector.md +40 -0
- package/docs/ideas/pi-task-memory.md +34 -0
- package/docs/ideas/qa-extension.md +43 -0
- package/docs/ideas/record-ideas-as-beads.md +115 -0
- package/docs/ideas/subagent-visibility.md +72 -0
- package/docs/ideas/task-session-context.md +89 -0
- package/docs/ideas/unified-ci-pipeline.md +28 -0
- package/docs/research/skill-design-research.md +4 -4
- package/docs/research/skill-improvements-analysis.md +1 -1
- package/docs/skills/skill-anatomy.md +47 -2
- package/docs/technical-direction/migration-execution-labels.md +132 -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/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 +68 -3
- package/package.json +6 -4
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +1 -1
- package/scripts/build-copilot-agents.mjs +30 -0
- package/scripts/lib/compose-agent.mjs +241 -0
- package/skills/engineering/design-an-interface/SKILL.md +78 -58
- package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
- package/skills/engineering/grill-with-docs/SKILL.md +66 -63
- package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
- package/skills/engineering/technical-direction/FORMAT.md +38 -0
- package/skills/engineering/technical-direction/SKILL.md +70 -77
- package/skills/planning/classify-bead/SKILL.md +59 -35
- package/skills/planning/create-task/REFERENCE.md +40 -0
- package/skills/planning/create-task/SKILL.md +70 -66
- package/skills/planning/create-task/scripts/create-chores.ts +6 -1
- package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
- package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
- package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
- package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
- package/skills/planning/prd-to-tasks/SKILL.md +86 -170
- package/skills/planning/record-idea/FORMAT.md +44 -0
- package/skills/planning/record-idea/SKILL.md +70 -53
- package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
- package/skills/planning/request-refactor-plan/SKILL.md +57 -46
- package/skills/planning/write-a-prd/FORMAT.md +54 -0
- package/skills/planning/write-a-prd/SKILL.md +49 -48
- package/skills/planning/write-a-ticket/SKILL.md +68 -53
- package/skills/productivity/handoff/SKILL.md +12 -0
- package/skills/productivity/init-beads/SKILL.md +48 -40
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
- package/skills/productivity/write-a-skill/SKILL.md +2 -1
- package/skills/review/maintain-agent-docs/SKILL.md +71 -51
- package/skills/workflow/hitl-collab/SKILL.md +86 -17
- package/skills/workflow/ralph/REFERENCE.md +24 -19
- package/skills/workflow/ralph/SKILL.md +88 -74
- package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
- package/skills/workflow/using-agent-skills/SKILL.md +122 -0
- package/test/claude-installer.test.mjs +248 -0
- 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 +130 -0
- package/token-map.json +106 -0
- package/.github/workflows/ci.test.ts +0 -217
- package/.github/workflows/release.yml +0 -58
- package/scripts/build-claude-agents.mjs +0 -179
- package/scripts/publish-release.sh +0 -32
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Agent discovery for the agent-modes extension.
|
|
7
|
+
*
|
|
8
|
+
* Agents are read from the composable source format (`agents/<name>/agent.md` +
|
|
9
|
+
* `agents/<name>/pi/frontmatter.json`) — the canonical layout defined in
|
|
10
|
+
* agents/README.md. The PI system prompt is composed per the token-map.json
|
|
11
|
+
* contract:
|
|
12
|
+
*
|
|
13
|
+
* 1. `{{SECTION:name}}` is resolved from the agent's `pi/<name>.md` section file.
|
|
14
|
+
* 2. `{{TOOL:key}}` and `{{PATH:key}}` are substituted against the pi column of
|
|
15
|
+
* token-map.json, with the plugin root resolved from the actual extension
|
|
16
|
+
* location (installers must compute it from the real resolution, per the
|
|
17
|
+
* token-map notes — a hardcoded `~/.pi/...` literal would break checkout
|
|
18
|
+
* and symlinked installs).
|
|
19
|
+
*
|
|
20
|
+
* Flat `agents/*.agent.md` files are parsed as a fallback for agents that have
|
|
21
|
+
* no composable directory (they are generated output for Copilot, so they may
|
|
22
|
+
* exist for any agent). A composable directory always wins over its flat
|
|
23
|
+
* counterpart.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface AgentDef {
|
|
29
|
+
/** Lowercase id derived from the directory/file name (e.g. "ralph", "ralph-plan") */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Display name from frontmatter or prettified id */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Description from frontmatter */
|
|
34
|
+
description: string;
|
|
35
|
+
/** Colour for the status indicator */
|
|
36
|
+
color: ThemeColor;
|
|
37
|
+
/** PI tool names to enable (empty = all tools) */
|
|
38
|
+
tools: string[];
|
|
39
|
+
/** Full agent body (the system prompt to inject) */
|
|
40
|
+
prompt: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type HarnessId = "copilot" | "claude" | "pi";
|
|
44
|
+
|
|
45
|
+
export interface PathSpec {
|
|
46
|
+
base: string;
|
|
47
|
+
relative: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Shape of token-map.json (see the file at the package root). */
|
|
51
|
+
export interface TokenMap {
|
|
52
|
+
tools: Record<string, Record<HarnessId, string | null>>;
|
|
53
|
+
paths: Record<string, Record<HarnessId, string | PathSpec>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PI: HarnessId = "pi";
|
|
57
|
+
|
|
58
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Mapping from agent id → colour for the status indicator. */
|
|
61
|
+
const AGENT_COLORS: Record<string, ThemeColor> = {
|
|
62
|
+
ralph: "accent",
|
|
63
|
+
"ralph-plan": "warning",
|
|
64
|
+
strategy: "accent",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Modes with no tool restriction get the full default set. */
|
|
68
|
+
const DEFAULT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Copilot CLI tool names → PI built-in tool names. Fallback translation used
|
|
72
|
+
* when token-map.json is unavailable; token-map.json is the source of truth
|
|
73
|
+
* when present.
|
|
74
|
+
*/
|
|
75
|
+
const COPILOT_TO_PI: Record<string, string[]> = {
|
|
76
|
+
bash: ["bash"],
|
|
77
|
+
view: ["read"],
|
|
78
|
+
read: ["read"],
|
|
79
|
+
edit: ["edit"],
|
|
80
|
+
create: ["write"],
|
|
81
|
+
write: ["write"],
|
|
82
|
+
grep: ["grep"],
|
|
83
|
+
rg: ["grep", "find"],
|
|
84
|
+
glob: ["find", "ls"],
|
|
85
|
+
find: ["find"],
|
|
86
|
+
ls: ["ls"],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ─── Read helpers ────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function isFile(p: string): boolean {
|
|
92
|
+
try {
|
|
93
|
+
return statSync(p).isFile();
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Load token-map.json from the package root (sibling of the agents dir). */
|
|
100
|
+
export function loadTokenMap(agentsDir: string): TokenMap | null {
|
|
101
|
+
try {
|
|
102
|
+
const raw = readFileSync(join(agentsDir, "..", "token-map.json"), "utf-8");
|
|
103
|
+
const parsed = JSON.parse(raw) as Partial<TokenMap>;
|
|
104
|
+
return parsed.tools && parsed.paths ? (parsed as TokenMap) : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function prettifyId(id: string): string {
|
|
111
|
+
return id
|
|
112
|
+
.split("-")
|
|
113
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
114
|
+
.join(" ");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Tool translation ────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Translate a frontmatter tool list into PI tool names.
|
|
121
|
+
*
|
|
122
|
+
* With token-map.json: canonical keys are translated via the pi column; a null
|
|
123
|
+
* mapping means PI has no equivalent (ask_user, skill) and the entry is omitted;
|
|
124
|
+
* a name that is not a canonical key passes through untouched (it may be a
|
|
125
|
+
* native PI tool such as `fetch_content`). Without the map, the built-in
|
|
126
|
+
* COPILOT_TO_PI fallback applies and unknown names pass through.
|
|
127
|
+
*/
|
|
128
|
+
function translateTools(raw: unknown, tokenMap: TokenMap | null): string[] {
|
|
129
|
+
if (!Array.isArray(raw)) return [];
|
|
130
|
+
|
|
131
|
+
const out = new Set<string>();
|
|
132
|
+
for (const t of raw) {
|
|
133
|
+
if (typeof t !== "string" || t === "") continue;
|
|
134
|
+
|
|
135
|
+
if (tokenMap) {
|
|
136
|
+
const mapping = tokenMap.tools[t];
|
|
137
|
+
if (mapping === undefined) {
|
|
138
|
+
out.add(t); // not a canonical key — pass through (may be a native PI tool)
|
|
139
|
+
} else if (mapping[PI] === null) {
|
|
140
|
+
console.warn(`[agent-modes] Tool "${t}" has no PI equivalent — omitted from the tool set`);
|
|
141
|
+
} else {
|
|
142
|
+
out.add(mapping[PI] as string);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const fallback = COPILOT_TO_PI[t];
|
|
146
|
+
if (fallback) {
|
|
147
|
+
for (const f of fallback) out.add(f);
|
|
148
|
+
} else {
|
|
149
|
+
out.add(t);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return [...out].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── PATH token resolution ───────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a `{{PATH:...}}` argument for the pi harness.
|
|
160
|
+
*
|
|
161
|
+
* A named key resolves through token-map.json's `paths` table; the `plugin_root`
|
|
162
|
+
* base is always overridden with the actual package root (the directory
|
|
163
|
+
* containing `agents/`) because the map's literal only describes the default npm
|
|
164
|
+
* install layout. Anything that is not a named key is treated as a relative path
|
|
165
|
+
* under the package root.
|
|
166
|
+
*/
|
|
167
|
+
function resolvePathToken(arg: string, tokenMap: TokenMap | null, pluginRoot: string): string {
|
|
168
|
+
const entry = tokenMap?.paths?.[arg];
|
|
169
|
+
if (!entry) return join(pluginRoot, arg);
|
|
170
|
+
|
|
171
|
+
const spec = entry[PI];
|
|
172
|
+
if (typeof spec === "string") {
|
|
173
|
+
return arg === "plugin_root" ? pluginRoot : spec; // ${ENV} literals left verbatim
|
|
174
|
+
}
|
|
175
|
+
if (spec && typeof spec === "object" && typeof spec.base === "string" && typeof spec.relative === "string") {
|
|
176
|
+
const base = resolvePathToken(spec.base, tokenMap, pluginRoot);
|
|
177
|
+
return join(base, spec.relative);
|
|
178
|
+
}
|
|
179
|
+
console.warn(`[agent-modes] Unresolvable {{PATH:${arg}}} — token dropped`);
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Prompt composition ──────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compose the PI system prompt for a composable agent directory.
|
|
187
|
+
*
|
|
188
|
+
* Substitution order follows the token-map contract: SECTION, then TOOL, then
|
|
189
|
+
* PATH. Section files resolve from `<agentDir>/pi/<name>.md`; missing sections
|
|
190
|
+
* and null-mapped tools drop the token with a warning so the prompt never ships
|
|
191
|
+
* literal token syntax.
|
|
192
|
+
*/
|
|
193
|
+
function composePiPrompt(
|
|
194
|
+
agentDir: string,
|
|
195
|
+
id: string,
|
|
196
|
+
tokenMap: TokenMap | null,
|
|
197
|
+
pluginRoot: string,
|
|
198
|
+
): string {
|
|
199
|
+
let body = readFileSync(join(agentDir, "agent.md"), "utf-8").trim();
|
|
200
|
+
|
|
201
|
+
// 1. {{SECTION:name}} → content of pi/<name>.md
|
|
202
|
+
body = body.replace(/\{\{SECTION:([^}]+)\}\}/g, (_token, name: string) => {
|
|
203
|
+
const sectionPath = join(agentDir, "pi", `${name}.md`);
|
|
204
|
+
if (!isFile(sectionPath)) {
|
|
205
|
+
console.warn(`[agent-modes] Missing section "${name}" for agent "${id}" — token dropped`);
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
return readFileSync(sectionPath, "utf-8").trim();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// 2. {{TOOL:key}} → tools[key].pi (null mapping drops the token)
|
|
212
|
+
body = body.replace(/\{\{TOOL:([^}]+)\}\}/g, (_token, key: string) => {
|
|
213
|
+
const mapping = tokenMap?.tools?.[key];
|
|
214
|
+
if (mapping === undefined) {
|
|
215
|
+
console.warn(`[agent-modes] Unknown tool token {{TOOL:${key}}} in agent "${id}" — token dropped`);
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
if (mapping[PI] === null) {
|
|
219
|
+
console.warn(`[agent-modes] Tool token {{TOOL:${key}}} has no PI equivalent in agent "${id}" — token dropped`);
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
222
|
+
return mapping[PI] as string;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 3. {{PATH:key-or-relative}} → resolved against the actual plugin root
|
|
226
|
+
body = body.replace(/\{\{PATH:([^}]+)\}\}/g, (_token, arg: string) => resolvePathToken(arg, tokenMap, pluginRoot));
|
|
227
|
+
|
|
228
|
+
return body;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Composable directory reading ────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
interface Frontmatter {
|
|
234
|
+
name?: string;
|
|
235
|
+
description?: string;
|
|
236
|
+
tools?: string[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface ComposableFrontmatter extends Frontmatter {
|
|
240
|
+
name: string;
|
|
241
|
+
description: string;
|
|
242
|
+
tools: string[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseFrontmatterJson(raw: string, id: string): ComposableFrontmatter {
|
|
246
|
+
let fm: Record<string, unknown>;
|
|
247
|
+
try {
|
|
248
|
+
fm = JSON.parse(raw) as Record<string, unknown>;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.warn(`[agent-modes] Invalid frontmatter.json for agent "${id}": ${err}`);
|
|
251
|
+
return { name: prettifyId(id), description: "", tools: [] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
name: typeof fm.name === "string" && fm.name !== "" ? fm.name : prettifyId(id),
|
|
256
|
+
description: typeof fm.description === "string" ? fm.description : "",
|
|
257
|
+
tools: Array.isArray(fm.tools) ? (fm.tools.filter((t): t is string => typeof t === "string") as string[]) : [],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readComposableAgent(
|
|
262
|
+
agentDir: string,
|
|
263
|
+
id: string,
|
|
264
|
+
tokenMap: TokenMap | null,
|
|
265
|
+
pluginRoot: string,
|
|
266
|
+
): AgentDef | null {
|
|
267
|
+
try {
|
|
268
|
+
const fm = parseFrontmatterJson(readFileSync(join(agentDir, "pi", "frontmatter.json"), "utf-8"), id);
|
|
269
|
+
const tools = translateTools(fm.tools, tokenMap);
|
|
270
|
+
return {
|
|
271
|
+
id,
|
|
272
|
+
name: fm.name,
|
|
273
|
+
description: fm.description,
|
|
274
|
+
color: AGENT_COLORS[id] ?? "accent",
|
|
275
|
+
tools: tools.length > 0 ? tools : [...DEFAULT_TOOLS],
|
|
276
|
+
prompt: composePiPrompt(agentDir, id, tokenMap, pluginRoot),
|
|
277
|
+
};
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.warn(`[agent-modes] Failed to read composable agent "${id}": ${err}`);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Flat file parsing (transitional fallback) ───────────────────────────────
|
|
285
|
+
|
|
286
|
+
interface FlatParsed {
|
|
287
|
+
name?: string;
|
|
288
|
+
description?: string;
|
|
289
|
+
tools?: string[];
|
|
290
|
+
prompt: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function parseFlatFrontmatter(raw: string): FlatParsed {
|
|
294
|
+
const lines = raw.split(/\r?\n/);
|
|
295
|
+
if (lines[0] !== "---") return { prompt: raw };
|
|
296
|
+
|
|
297
|
+
// Find closing ---
|
|
298
|
+
let sep = -1;
|
|
299
|
+
for (let i = 1; i < lines.length; i++) {
|
|
300
|
+
if (lines[i].trim() === "---") {
|
|
301
|
+
sep = i;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (sep === -1) return { prompt: raw };
|
|
306
|
+
|
|
307
|
+
const yamlLines = lines.slice(1, sep);
|
|
308
|
+
const prompt = lines.slice(sep + 1).join("\n").trim();
|
|
309
|
+
|
|
310
|
+
let name: string | undefined;
|
|
311
|
+
let description: string | undefined;
|
|
312
|
+
let tools: string[] | undefined;
|
|
313
|
+
|
|
314
|
+
for (const line of yamlLines) {
|
|
315
|
+
const trimmed = line.trim();
|
|
316
|
+
const colonIdx = trimmed.indexOf(":");
|
|
317
|
+
if (colonIdx === -1) continue;
|
|
318
|
+
|
|
319
|
+
const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
|
320
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
321
|
+
|
|
322
|
+
if (key === "name") {
|
|
323
|
+
name = val.replace(/^["']|["']$/g, "");
|
|
324
|
+
} else if (key === "description") {
|
|
325
|
+
description = val.replace(/^["']|["']$/g, "");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Parse tools from the tools frontmatter.
|
|
330
|
+
// Supports two formats:
|
|
331
|
+
// tools: ["bash", "view"] (JSON array, single line)
|
|
332
|
+
// tools: (YAML list, subsequent lines)
|
|
333
|
+
// - bash
|
|
334
|
+
// - view
|
|
335
|
+
let inToolsBlock = false;
|
|
336
|
+
for (const line of yamlLines) {
|
|
337
|
+
const trimmed = line.trim();
|
|
338
|
+
|
|
339
|
+
// Check for inline JSON array: tools: ["foo", "bar"]
|
|
340
|
+
const colonIdx = trimmed.indexOf(":");
|
|
341
|
+
if (colonIdx !== -1) {
|
|
342
|
+
const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
|
343
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
344
|
+
if (key === "tools") {
|
|
345
|
+
if (val.startsWith("[")) {
|
|
346
|
+
try {
|
|
347
|
+
tools = JSON.parse(val);
|
|
348
|
+
} catch {
|
|
349
|
+
// Malformed JSON — fall through
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (val === "[]" || val === "") {
|
|
354
|
+
tools = [];
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// YAML block format: "tools:" on its own line, items on subsequent lines
|
|
361
|
+
if (trimmed === "tools:") {
|
|
362
|
+
inToolsBlock = true;
|
|
363
|
+
if (!tools) tools = [];
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (inToolsBlock) {
|
|
367
|
+
if (line.startsWith(" ") || line.startsWith("\t") || trimmed.startsWith("- ")) {
|
|
368
|
+
const item = trimmed.replace(/^- /, "").trim().replace(/^["']|["']$/g, "");
|
|
369
|
+
if (item) {
|
|
370
|
+
if (!tools) tools = [];
|
|
371
|
+
tools.push(item);
|
|
372
|
+
}
|
|
373
|
+
} else if (trimmed !== "") {
|
|
374
|
+
inToolsBlock = false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return { name, description, tools, prompt };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function readFlatAgent(filePath: string, tokenMap: TokenMap | null): AgentDef | null {
|
|
383
|
+
let raw: string;
|
|
384
|
+
try {
|
|
385
|
+
raw = readFileSync(filePath, "utf-8");
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.warn(`[agent-modes] Failed to read ${basename(filePath)}: ${err}`);
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const parsed = parseFlatFrontmatter(raw);
|
|
392
|
+
const id = basename(filePath).replace(/\.agent\.md$/i, "").toLowerCase();
|
|
393
|
+
const name = parsed.name || prettifyId(id);
|
|
394
|
+
const tools = translateTools(parsed.tools, tokenMap);
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
id,
|
|
398
|
+
name,
|
|
399
|
+
description: parsed.description || "",
|
|
400
|
+
color: AGENT_COLORS[id] ?? "accent",
|
|
401
|
+
tools: tools.length > 0 ? tools : [...DEFAULT_TOOLS],
|
|
402
|
+
prompt: parsed.prompt || `You are ${name}. ${parsed.description || ""}`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── Discovery ───────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Discover agents from the agents directory.
|
|
410
|
+
*
|
|
411
|
+
* Composable directories (`<name>/agent.md` + `<name>/pi/frontmatter.json`) are
|
|
412
|
+
* the primary source. Flat `*.agent.md` files (generated output for Copilot) are
|
|
413
|
+
* read only when no composable directory exists for that agent id. When
|
|
414
|
+
* `tokenMap` is omitted, token-map.json is loaded from the package root;
|
|
415
|
+
* missing/unreadable maps fall back to the built-in copilot→pi translation.
|
|
416
|
+
*/
|
|
417
|
+
export function discoverAgents(agentsDir: string, tokenMap: TokenMap | null = null): AgentDef[] {
|
|
418
|
+
if (!existsSync(agentsDir) || !statSync(agentsDir).isDirectory()) {
|
|
419
|
+
console.warn(`[agent-modes] Agents directory not found: ${agentsDir}`);
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const map = tokenMap ?? loadTokenMap(agentsDir);
|
|
424
|
+
const pluginRoot = join(agentsDir, "..");
|
|
425
|
+
|
|
426
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true }).sort((a, b) =>
|
|
427
|
+
a.name.localeCompare(b.name),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const agents: AgentDef[] = [];
|
|
431
|
+
const composableIds = new Set<string>();
|
|
432
|
+
|
|
433
|
+
// 1. Composable directory format
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
if (!entry.isDirectory()) continue;
|
|
436
|
+
const dir = join(agentsDir, entry.name);
|
|
437
|
+
if (!isFile(join(dir, "agent.md")) || !isFile(join(dir, "pi", "frontmatter.json"))) continue;
|
|
438
|
+
const id = entry.name.toLowerCase();
|
|
439
|
+
const def = readComposableAgent(dir, id, map, pluginRoot);
|
|
440
|
+
if (def) {
|
|
441
|
+
composableIds.add(id);
|
|
442
|
+
agents.push(def);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 2. Flat fallback for agents without a composable directory
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
if (!entry.isFile() || !entry.name.endsWith(".agent.md")) continue;
|
|
449
|
+
const id = entry.name.replace(/\.agent\.md$/i, "").toLowerCase();
|
|
450
|
+
if (composableIds.has(id)) continue; // composable wins
|
|
451
|
+
const def = readFlatAgent(join(agentsDir, entry.name), map);
|
|
452
|
+
if (def) agents.push(def);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return agents;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export { DEFAULT_TOOLS };
|