@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
package/.beads/metadata.json
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { parse as parseYaml } from "yaml";
|
|
6
|
-
|
|
7
|
-
const ROOT = join(import.meta.dirname, "..", "..");
|
|
8
|
-
|
|
9
|
-
function loadYaml(relPath: string): Record<string, unknown> {
|
|
10
|
-
const fullPath = join(ROOT, relPath);
|
|
11
|
-
assert.ok(existsSync(fullPath), `${relPath} does not exist`);
|
|
12
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
13
|
-
return parseYaml(content) as Record<string, unknown>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function loadJson(relPath: string): Record<string, unknown> {
|
|
17
|
-
const fullPath = join(ROOT, relPath);
|
|
18
|
-
assert.ok(existsSync(fullPath), `${relPath} does not exist`);
|
|
19
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
20
|
-
return JSON.parse(content) as Record<string, unknown>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ─── CI Workflow ─────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
describe("CI workflow", () => {
|
|
26
|
-
const ci = loadYaml(".github/workflows/ci.yml") as Record<string, unknown>;
|
|
27
|
-
|
|
28
|
-
it("triggers on push to main and pull requests", () => {
|
|
29
|
-
const on = ci.on as Record<string, unknown>;
|
|
30
|
-
assert.ok(on.push, "has push trigger");
|
|
31
|
-
assert.ok("pull_request" in on, "has pull_request trigger");
|
|
32
|
-
|
|
33
|
-
const push = on.push as Record<string, unknown>;
|
|
34
|
-
assert.deepStrictEqual(push.branches, ["main"]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("has a job that runs on ubuntu-latest", () => {
|
|
38
|
-
const jobs = ci.jobs as Record<string, unknown>;
|
|
39
|
-
const jobNames = Object.keys(jobs);
|
|
40
|
-
assert.ok(jobNames.length > 0, "has at least one job");
|
|
41
|
-
|
|
42
|
-
const firstJob = jobs[jobNames[0]] as Record<string, unknown>;
|
|
43
|
-
assert.equal(firstJob["runs-on"], "ubuntu-latest");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("checks out code", () => {
|
|
47
|
-
const steps = getSteps(ci);
|
|
48
|
-
const checkout = steps.find(
|
|
49
|
-
(s: Record<string, unknown>) => s.name === "Checkout"
|
|
50
|
-
);
|
|
51
|
-
assert.ok(checkout, "has Checkout step");
|
|
52
|
-
const uses = (checkout as Record<string, unknown>).uses as string;
|
|
53
|
-
assert.ok(uses.startsWith("actions/checkout@"), "uses actions/checkout");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("sets up Node.js", () => {
|
|
57
|
-
const steps = getSteps(ci);
|
|
58
|
-
const setupNode = steps.find(
|
|
59
|
-
(s: Record<string, unknown>) => s.name === "Setup Node"
|
|
60
|
-
);
|
|
61
|
-
assert.ok(setupNode, "has Setup Node step");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("sets up pnpm", () => {
|
|
65
|
-
const steps = getSteps(ci);
|
|
66
|
-
const setupPnpm = steps.find(
|
|
67
|
-
(s: Record<string, unknown>) => s.name === "Setup pnpm"
|
|
68
|
-
);
|
|
69
|
-
assert.ok(setupPnpm, "has Setup pnpm step");
|
|
70
|
-
const uses = (setupPnpm as Record<string, unknown>).uses as string;
|
|
71
|
-
assert.ok(uses.startsWith("pnpm/action-setup@"), "uses pnpm/action-setup");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("installs dependencies", () => {
|
|
75
|
-
const steps = getSteps(ci);
|
|
76
|
-
const install = steps.find(
|
|
77
|
-
(s: Record<string, unknown>) => s.name === "Install dependencies"
|
|
78
|
-
);
|
|
79
|
-
assert.ok(install, "has Install dependencies step");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("runs lint", () => {
|
|
83
|
-
const steps = getSteps(ci);
|
|
84
|
-
const lint = steps.find(
|
|
85
|
-
(s: Record<string, unknown>) => s.name === "Lint"
|
|
86
|
-
);
|
|
87
|
-
assert.ok(lint, "has Lint step");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("runs tests", () => {
|
|
91
|
-
const steps = getSteps(ci);
|
|
92
|
-
const test = steps.find(
|
|
93
|
-
(s: Record<string, unknown>) => s.name === "Test"
|
|
94
|
-
);
|
|
95
|
-
assert.ok(test, "has Test step");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("runs build", () => {
|
|
99
|
-
const steps = getSteps(ci);
|
|
100
|
-
const build = steps.find(
|
|
101
|
-
(s: Record<string, unknown>) => s.name === "Build"
|
|
102
|
-
);
|
|
103
|
-
assert.ok(build, "has Build step");
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ─── Publish Workflow ────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
describe("Publish workflow", () => {
|
|
110
|
-
const publish = loadYaml(
|
|
111
|
-
".github/workflows/publish.yml"
|
|
112
|
-
) as Record<string, unknown>;
|
|
113
|
-
|
|
114
|
-
it("triggers on release published", () => {
|
|
115
|
-
const on = publish.on as Record<string, unknown>;
|
|
116
|
-
assert.ok(on.release, "has release trigger");
|
|
117
|
-
const release = on.release as Record<string, unknown>;
|
|
118
|
-
assert.deepStrictEqual(release.types, ["published"]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("has a job that runs on ubuntu-latest", () => {
|
|
122
|
-
const jobs = publish.jobs as Record<string, unknown>;
|
|
123
|
-
const jobNames = Object.keys(jobs);
|
|
124
|
-
assert.ok(jobNames.length > 0, "has at least one job");
|
|
125
|
-
|
|
126
|
-
const firstJob = jobs[jobNames[0]] as Record<string, unknown>;
|
|
127
|
-
assert.equal(firstJob["runs-on"], "ubuntu-latest");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("sets up Node 24 with setup-node v6 for OIDC", () => {
|
|
131
|
-
const steps = getSteps(publish);
|
|
132
|
-
const setupNode = steps.find(
|
|
133
|
-
(s: Record<string, unknown>) => s.name === "Setup Node"
|
|
134
|
-
);
|
|
135
|
-
assert.ok(setupNode, "has Setup Node step");
|
|
136
|
-
const uses = (setupNode as Record<string, unknown>).uses as string;
|
|
137
|
-
assert.ok(uses.startsWith("actions/setup-node@v6"), "uses setup-node v6");
|
|
138
|
-
const with_ = (setupNode as Record<string, unknown>).with as Record<
|
|
139
|
-
string,
|
|
140
|
-
unknown
|
|
141
|
-
>;
|
|
142
|
-
assert.equal(with_["node-version"], 24);
|
|
143
|
-
assert.equal(
|
|
144
|
-
with_["registry-url"],
|
|
145
|
-
undefined,
|
|
146
|
-
"no registry-url — OIDC auth needs no registry config"
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("packs the tarball then publishes with npm", () => {
|
|
151
|
-
const steps = getSteps(publish);
|
|
152
|
-
const publishStep = steps.find(
|
|
153
|
-
(s: Record<string, unknown>) => s.name === "Publish to npm"
|
|
154
|
-
);
|
|
155
|
-
assert.ok(publishStep, "has Publish to npm step");
|
|
156
|
-
const run = (publishStep as Record<string, unknown>).run as string;
|
|
157
|
-
assert.ok(run.includes("pnpm pack"), "packs with pnpm");
|
|
158
|
-
assert.ok(run.includes("npm publish"), "publishes with npm");
|
|
159
|
-
assert.ok(!run.includes("pnpm publish"), "does not publish via pnpm");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("grants contents: read and id-token: write permissions for OIDC", () => {
|
|
163
|
-
const jobs = publish.jobs as Record<string, unknown>;
|
|
164
|
-
const jobNames = Object.keys(jobs);
|
|
165
|
-
const job = jobs[jobNames[0]] as Record<string, unknown>;
|
|
166
|
-
const permissions = job.permissions as Record<string, unknown>;
|
|
167
|
-
assert.equal(permissions["contents"], "read");
|
|
168
|
-
assert.equal(permissions["id-token"], "write");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("has no NPM_TOKEN secret or NODE_AUTH_TOKEN auth", () => {
|
|
172
|
-
const content = readFileSync(
|
|
173
|
-
join(ROOT, ".github/workflows/publish.yml"),
|
|
174
|
-
"utf-8"
|
|
175
|
-
);
|
|
176
|
-
assert.ok(!content.includes("NPM_TOKEN"), "no NPM_TOKEN secret reference");
|
|
177
|
-
assert.ok(
|
|
178
|
-
!content.includes("NODE_AUTH_TOKEN"),
|
|
179
|
-
"no NODE_AUTH_TOKEN auth env"
|
|
180
|
-
);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// ─── Package.json ────────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
describe("package.json", () => {
|
|
187
|
-
const pkg = loadJson("package.json") as Record<string, unknown>;
|
|
188
|
-
|
|
189
|
-
it("is not private (publishable to npm)", () => {
|
|
190
|
-
assert.equal(pkg.private, undefined, "package is not private");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("has a build script", () => {
|
|
194
|
-
const scripts = pkg.scripts as Record<string, unknown>;
|
|
195
|
-
assert.ok(scripts.build, "has build script");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("has a lint script", () => {
|
|
199
|
-
const scripts = pkg.scripts as Record<string, unknown>;
|
|
200
|
-
assert.ok(scripts.lint, "has lint script");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("has publishConfig with public access", () => {
|
|
204
|
-
const publishConfig = pkg.publishConfig as Record<string, unknown>;
|
|
205
|
-
assert.ok(publishConfig, "has publishConfig");
|
|
206
|
-
assert.equal(publishConfig.access, "public");
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
211
|
-
|
|
212
|
-
function getSteps(workflow: Record<string, unknown>): Record<string, unknown>[] {
|
|
213
|
-
const jobs = workflow.jobs as Record<string, unknown>;
|
|
214
|
-
const jobNames = Object.keys(jobs);
|
|
215
|
-
const firstJob = jobs[jobNames[0]] as Record<string, unknown>;
|
|
216
|
-
return firstJob.steps as Record<string, unknown>[];
|
|
217
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
publish:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
permissions:
|
|
11
|
-
contents: read
|
|
12
|
-
id-token: write
|
|
13
|
-
steps:
|
|
14
|
-
- name: Checkout
|
|
15
|
-
uses: actions/checkout@v4
|
|
16
|
-
- name: Setup Node
|
|
17
|
-
uses: actions/setup-node@v6
|
|
18
|
-
with:
|
|
19
|
-
node-version: 24
|
|
20
|
-
- name: Setup pnpm
|
|
21
|
-
uses: pnpm/action-setup@v4
|
|
22
|
-
with:
|
|
23
|
-
version: 10
|
|
24
|
-
run_install: false
|
|
25
|
-
- name: Install dependencies
|
|
26
|
-
run: pnpm install --frozen-lockfile
|
|
27
|
-
- name: Lint
|
|
28
|
-
run: pnpm lint
|
|
29
|
-
- name: Test
|
|
30
|
-
run: pnpm test
|
|
31
|
-
- name: Build
|
|
32
|
-
run: pnpm build
|
|
33
|
-
- name: Publish to npm
|
|
34
|
-
run: |
|
|
35
|
-
TARBALL=$(pnpm pack | tail -n 1)
|
|
36
|
-
npm publish "$TARBALL"
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
// Builds the self-contained Claude Code plugin subtree at claude/ from the canonical
|
|
2
|
-
// Copilot sources. The Copilot files (agents/*.agent.md) and the grouped skills/ tree
|
|
3
|
-
// stay the single source of truth; the claude/ subtree is generated and committed.
|
|
4
|
-
// CI runs this and checks `git diff --exit-code claude/` to guarantee it is never stale.
|
|
5
|
-
//
|
|
6
|
-
// claude/agents/*.md generated from agents/*.agent.md (frontmatter + tools converted)
|
|
7
|
-
// claude/skills/<name> symlinks to the grouped skills/<group>/<name> dirs (single source)
|
|
8
|
-
//
|
|
9
|
-
// Claude only loads agents from the default ./agents/ dir of the plugin root (custom
|
|
10
|
-
// `agents` manifest paths are ignored), so the plugin root is claude/ — isolating it
|
|
11
|
-
// from the Copilot .agent.md files, which would otherwise load as broken agents.
|
|
12
|
-
//
|
|
13
|
-
// Run: pnpm build:claude (node scripts/build-claude-agents.mjs)
|
|
14
|
-
// Zero dependencies so it runs on the CI Node (20) and local Node alike.
|
|
15
|
-
|
|
16
|
-
import { readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, symlinkSync, statSync, copyFileSync, existsSync } from "node:fs";
|
|
17
|
-
import { join, dirname, relative } from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
-
|
|
20
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
-
const AGENT_SRC = join(ROOT, "agents");
|
|
22
|
-
const NATIVE_SRC = join(ROOT, "agents-native");
|
|
23
|
-
const SKILL_SRC = join(ROOT, "skills");
|
|
24
|
-
const AGENT_OUT = join(ROOT, "claude", "agents");
|
|
25
|
-
const SKILL_OUT = join(ROOT, "claude", "skills");
|
|
26
|
-
|
|
27
|
-
// Copilot tool name -> Claude tool name. `null` = drop (no Claude equivalent).
|
|
28
|
-
const TOOL_MAP = {
|
|
29
|
-
bash: "Bash",
|
|
30
|
-
view: "Read",
|
|
31
|
-
edit: "Edit",
|
|
32
|
-
create: "Write",
|
|
33
|
-
grep: "Grep",
|
|
34
|
-
rg: "Grep",
|
|
35
|
-
glob: "Glob",
|
|
36
|
-
ask_user: "AskUserQuestion",
|
|
37
|
-
web_fetch: "WebFetch",
|
|
38
|
-
skill: "Skill",
|
|
39
|
-
task: "Task",
|
|
40
|
-
read_agent: null, // Claude's Task returns results inline; no separate read step.
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Agents NOT transformed from the Copilot source. `ralph` is authored natively for Claude
|
|
44
|
-
// instead (agents-native/ralph.md) — its event-driven orchestration can't be produced by
|
|
45
|
-
// token substitution from the poll-loop Copilot source.
|
|
46
|
-
const DEFER = new Set(["ralph"]);
|
|
47
|
-
|
|
48
|
-
// Ralph-coupled Copilot skills that don't apply to the lean Claude Ralph (and carry dead
|
|
49
|
-
// ~/.copilot orchestration paths). Not symlinked into the Claude plugin.
|
|
50
|
-
const SKILL_EXCLUDE = new Set(["ralph", "run-pipeline-stage"]);
|
|
51
|
-
|
|
52
|
-
const COPILOT_PLUGIN_ROOT = "~/.copilot/installed-plugins/_direct/agent-cortex";
|
|
53
|
-
const CLAUDE_PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}";
|
|
54
|
-
const NAME_PREFIX = "agent-cortex:";
|
|
55
|
-
|
|
56
|
-
function parseFrontmatter(text, file) {
|
|
57
|
-
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
58
|
-
if (!m) throw new Error(`${file}: no YAML frontmatter found`);
|
|
59
|
-
return { fm: m[1], body: text.slice(m[0].length) };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function scalarLine(fm, key) {
|
|
63
|
-
const m = fm.match(new RegExp(`^${key}:[ \\t]*(.+?)[ \\t]*$`, "m"));
|
|
64
|
-
return m ? m[1] : undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function stripQuotes(s) {
|
|
68
|
-
return s === undefined ? undefined : s.replace(/^["']([\s\S]*)["']$/, "$1");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function mapTools(fm, file) {
|
|
72
|
-
const raw = scalarLine(fm, "tools");
|
|
73
|
-
if (!raw) throw new Error(`${file}: no tools field`);
|
|
74
|
-
let list;
|
|
75
|
-
try {
|
|
76
|
-
list = JSON.parse(raw);
|
|
77
|
-
} catch {
|
|
78
|
-
throw new Error(`${file}: tools is not a JSON array: ${raw}`);
|
|
79
|
-
}
|
|
80
|
-
const out = [];
|
|
81
|
-
for (const t of list) {
|
|
82
|
-
if (!(t in TOOL_MAP)) throw new Error(`${file}: unknown tool "${t}" — extend TOOL_MAP`);
|
|
83
|
-
const mapped = TOOL_MAP[t];
|
|
84
|
-
if (mapped && !out.includes(mapped)) out.push(mapped);
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Rewrite harness-specific tokens in the prose body: exact backticked tool names and
|
|
90
|
-
// the hardcoded Copilot plugin path. Only touches `\`token\`` so shell snippets like
|
|
91
|
-
// `bd create` inside code fences are untouched.
|
|
92
|
-
function transformBody(body) {
|
|
93
|
-
let out = body;
|
|
94
|
-
for (const [copilot, claude] of Object.entries(TOOL_MAP)) {
|
|
95
|
-
if (claude) out = out.split("`" + copilot + "`").join("`" + claude + "`");
|
|
96
|
-
}
|
|
97
|
-
return out.split(COPILOT_PLUGIN_ROOT).join(CLAUDE_PLUGIN_ROOT);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function buildAgents() {
|
|
101
|
-
rmSync(AGENT_OUT, { recursive: true, force: true });
|
|
102
|
-
mkdirSync(AGENT_OUT, { recursive: true });
|
|
103
|
-
|
|
104
|
-
const written = [];
|
|
105
|
-
const skipped = [];
|
|
106
|
-
for (const file of readdirSync(AGENT_SRC).filter((f) => f.endsWith(".agent.md")).sort()) {
|
|
107
|
-
const { fm, body } = parseFrontmatter(readFileSync(join(AGENT_SRC, file), "utf8"), file);
|
|
108
|
-
|
|
109
|
-
const rawName = stripQuotes(scalarLine(fm, "name"));
|
|
110
|
-
if (!rawName) throw new Error(`${file}: no name field`);
|
|
111
|
-
const slug = rawName.startsWith(NAME_PREFIX) ? rawName.slice(NAME_PREFIX.length) : rawName;
|
|
112
|
-
if (DEFER.has(slug)) {
|
|
113
|
-
skipped.push(slug);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const description = scalarLine(fm, "description");
|
|
118
|
-
if (!description) throw new Error(`${file}: no description field`);
|
|
119
|
-
|
|
120
|
-
const header =
|
|
121
|
-
`---\n` +
|
|
122
|
-
`# GENERATED from agents/${file} by scripts/build-claude-agents.mjs — DO NOT EDIT.\n` +
|
|
123
|
-
`name: ${slug}\n` +
|
|
124
|
-
`description: ${description}\n` +
|
|
125
|
-
`tools: ${mapTools(fm, file).join(", ")}\n` +
|
|
126
|
-
`---\n`;
|
|
127
|
-
const out = `${header}\n${transformBody(body).replace(/^\r?\n+/, "").replace(/\s*$/, "")}\n`;
|
|
128
|
-
writeFileSync(join(AGENT_OUT, `${slug}.md`), out);
|
|
129
|
-
written.push(slug);
|
|
130
|
-
}
|
|
131
|
-
console.log(`Generated ${written.length} Claude agent(s): ${written.join(", ")}`);
|
|
132
|
-
if (skipped.length) console.log(`Skipped Copilot source(s): ${skipped.join(", ")}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Copy hand-authored Claude-native agents into claude/agents/ verbatim, alongside the
|
|
136
|
-
// generated ones. These have no Copilot source (their bodies differ structurally per harness).
|
|
137
|
-
function copyNativeAgents() {
|
|
138
|
-
if (!existsSync(NATIVE_SRC)) return;
|
|
139
|
-
const copied = [];
|
|
140
|
-
for (const file of readdirSync(NATIVE_SRC).filter((f) => f.endsWith(".md")).sort()) {
|
|
141
|
-
copyFileSync(join(NATIVE_SRC, file), join(AGENT_OUT, file));
|
|
142
|
-
copied.push(file.replace(/\.md$/, ""));
|
|
143
|
-
}
|
|
144
|
-
if (copied.length) console.log(`Copied ${copied.length} native Claude agent(s): ${copied.join(", ")}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Symlink each grouped skill (skills/<group>/<name>/) into the flat claude/skills/ dir
|
|
148
|
-
// so Claude's one-level-deep skill scan discovers them without duplicating content.
|
|
149
|
-
function buildSkills() {
|
|
150
|
-
rmSync(SKILL_OUT, { recursive: true, force: true });
|
|
151
|
-
mkdirSync(SKILL_OUT, { recursive: true });
|
|
152
|
-
|
|
153
|
-
const byName = new Map();
|
|
154
|
-
for (const group of readdirSync(SKILL_SRC).sort()) {
|
|
155
|
-
const groupDir = join(SKILL_SRC, group);
|
|
156
|
-
if (!statSync(groupDir).isDirectory()) continue;
|
|
157
|
-
for (const name of readdirSync(groupDir).sort()) {
|
|
158
|
-
const skillDir = join(groupDir, name);
|
|
159
|
-
let hasSkill;
|
|
160
|
-
try {
|
|
161
|
-
hasSkill = statSync(join(skillDir, "SKILL.md")).isFile();
|
|
162
|
-
} catch {
|
|
163
|
-
hasSkill = false;
|
|
164
|
-
}
|
|
165
|
-
if (!hasSkill) continue;
|
|
166
|
-
if (SKILL_EXCLUDE.has(name)) continue;
|
|
167
|
-
if (byName.has(name)) {
|
|
168
|
-
throw new Error(`duplicate skill name "${name}" (${byName.get(name)} and ${group}) — names must be unique when flattened`);
|
|
169
|
-
}
|
|
170
|
-
byName.set(name, group);
|
|
171
|
-
symlinkSync(relative(SKILL_OUT, skillDir), join(SKILL_OUT, name));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
console.log(`Linked ${byName.size} skill(s) into claude/skills/`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
buildAgents();
|
|
178
|
-
copyNativeAgents();
|
|
179
|
-
buildSkills();
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
const SKILL_PATH = new URL("../SKILL.md", import.meta.url);
|
|
5
|
-
|
|
6
|
-
function readSkill(): string {
|
|
7
|
-
return readFileSync(SKILL_PATH, "utf-8");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe("using-agent-skills SKILL.md structure", () => {
|
|
11
|
-
const content = readSkill();
|
|
12
|
-
|
|
13
|
-
it("has required front-matter fields", () => {
|
|
14
|
-
expect(content).toMatch(/^---\n/);
|
|
15
|
-
expect(content).toMatch(/name:\s*using-agent-skills/);
|
|
16
|
-
expect(content).toMatch(/description:/);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("description is ≤ 1024 characters", () => {
|
|
20
|
-
const descMatch = content.match(/description:\s*(.+)/);
|
|
21
|
-
expect(descMatch).toBeTruthy();
|
|
22
|
-
if (descMatch) {
|
|
23
|
-
expect(descMatch[1].length).toBeLessThanOrEqual(1024);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("description includes 'Use when' trigger phrases", () => {
|
|
28
|
-
expect(content).toMatch(/Use when/i);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("has required 'When to use' section", () => {
|
|
32
|
-
expect(content).toMatch(/## When to use/);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("has required 'Workflow' section", () => {
|
|
36
|
-
expect(content).toMatch(/## Workflow/);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("has required 'Verification checklist' section", () => {
|
|
40
|
-
expect(content).toMatch(/## Verification checklist/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("has 'Core Operating Behaviors' section", () => {
|
|
44
|
-
expect(content).toMatch(/## Core Operating Behaviors/);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("has a skill routing decision tree", () => {
|
|
48
|
-
expect(content).toContain("Task arrives");
|
|
49
|
-
expect(content).toContain("→ ralph");
|
|
50
|
-
expect(content).toContain("→ tdd");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("references existing agent-cortex skills", () => {
|
|
54
|
-
const expectedSkills = [
|
|
55
|
-
"ralph",
|
|
56
|
-
"plan",
|
|
57
|
-
"tdd",
|
|
58
|
-
"bd-tool",
|
|
59
|
-
"git-workflow",
|
|
60
|
-
"review-security",
|
|
61
|
-
"style-code",
|
|
62
|
-
"style-tests",
|
|
63
|
-
];
|
|
64
|
-
for (const skill of expectedSkills) {
|
|
65
|
-
expect(content).toContain(skill);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("is under 150 lines", () => {
|
|
70
|
-
const lines = content.split("\n");
|
|
71
|
-
expect(lines.length).toBeLessThanOrEqual(150);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
File without changes
|
|
File without changes
|