@jaybeeuu/agent-cortex 1.37.1 → 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.
Files changed (92) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +9 -8
  2. package/.github/workflows/ci.yml +190 -8
  3. package/AGENTS.md +25 -6
  4. package/CHANGELOG.md +42 -0
  5. package/README.md +132 -33
  6. package/agents/README.md +47 -13
  7. package/agents/plan/agent.md +51 -0
  8. package/agents/plan/claude/frontmatter.json +6 -0
  9. package/agents/plan/copilot/frontmatter.json +6 -0
  10. package/agents/plan/pi/frontmatter.json +6 -0
  11. package/agents/plan.agent.md +3 -0
  12. package/agents/ralph/agent.md +262 -0
  13. package/agents/ralph/claude/frontmatter.json +6 -0
  14. package/agents/ralph/claude/polling.md +31 -0
  15. package/agents/ralph/copilot/frontmatter.json +6 -0
  16. package/agents/ralph/copilot/polling.md +62 -0
  17. package/agents/ralph/pi/frontmatter.json +6 -0
  18. package/agents/ralph/pi/polling.md +74 -0
  19. package/agents/ralph-plan/agent.md +114 -0
  20. package/agents/ralph-plan/claude/frontmatter.json +6 -0
  21. package/agents/ralph-plan/copilot/frontmatter.json +6 -0
  22. package/agents/ralph-plan/pi/frontmatter.json +6 -0
  23. package/agents/ralph-plan.agent.md +5 -2
  24. package/agents/ralph.agent.md +83 -57
  25. package/agents/strategy/agent.md +104 -0
  26. package/agents/strategy/claude/frontmatter.json +6 -0
  27. package/agents/strategy/copilot/frontmatter.json +6 -0
  28. package/agents/strategy/pi/frontmatter.json +6 -0
  29. package/agents/strategy.agent.md +3 -0
  30. package/bin/agent-cortex.mjs +68 -1
  31. package/bin/installers/claude.mjs +262 -0
  32. package/bin/installers/copilot.mjs +129 -0
  33. package/bin/installers/pi.mjs +197 -0
  34. package/claude/.claude-plugin/plugin.json +1 -1
  35. package/claude/agents/plan.md +3 -1
  36. package/claude/agents/ralph-plan.md +5 -3
  37. package/claude/agents/strategy.md +3 -1
  38. package/claude/hooks/scripts/notify.mjs +54 -0
  39. package/claude/hooks.json +15 -0
  40. package/docs/claude-hooks.md +115 -0
  41. package/docs/ideas/auto-model-selection.md +75 -0
  42. package/docs/ideas/improve-pi-system-prompt.md +34 -0
  43. package/docs/ideas/model-selector.md +40 -0
  44. package/docs/ideas/pi-task-memory.md +34 -0
  45. package/docs/ideas/qa-extension.md +43 -0
  46. package/docs/ideas/unified-ci-pipeline.md +28 -0
  47. package/docs/skills/skill-anatomy.md +9 -0
  48. package/extensions/agent-modes/README.md +54 -0
  49. package/extensions/agent-modes/discover.test.ts +241 -0
  50. package/extensions/agent-modes/discover.ts +458 -0
  51. package/extensions/agent-modes/index.ts +13 -224
  52. package/extensions/agent-modes/node_modules/.bin/pi +21 -0
  53. package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
  54. package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
  55. package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
  56. package/extensions/agent-modes/package.json +20 -0
  57. package/extensions/agent-modes/tsconfig.json +12 -0
  58. package/extensions/subagent/README.md +19 -0
  59. package/extensions/subagent/index.ts +131 -45
  60. package/extensions/subagent/node_modules/.bin/pi +21 -0
  61. package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
  62. package/extensions/subagent/node_modules/.bin/tsc +21 -0
  63. package/extensions/subagent/node_modules/.bin/tsserver +21 -0
  64. package/extensions/subagent/node_modules/.bin/tsx +21 -0
  65. package/extensions/subagent/package.json +23 -0
  66. package/extensions/subagent/tsconfig.json +12 -0
  67. package/extensions/subagent/wait.test.ts +202 -0
  68. package/extensions/subagent/wait.ts +118 -0
  69. package/hooks/claude/hooks.json +38 -0
  70. package/hooks/claude/scripts/notify.mjs +54 -0
  71. package/lib/cli.mjs +62 -3
  72. package/package.json +6 -4
  73. package/plugin.json +1 -1
  74. package/scripts/build-copilot-agents.mjs +30 -0
  75. package/scripts/lib/compose-agent.mjs +241 -0
  76. package/skills/engineering/design-an-interface/SKILL.md +1 -1
  77. package/skills/engineering/improve-codebase-architecture/SKILL.md +1 -1
  78. package/skills/workflow/ralph/REFERENCE.md +2 -2
  79. package/skills/workflow/ralph/SKILL.md +6 -6
  80. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
  81. package/skills/workflow/using-agent-skills/SKILL.md +122 -0
  82. package/test/cli.test.mjs +115 -2
  83. package/test/copilot-installer.test.mjs +298 -0
  84. package/test/installer.test.mjs +179 -0
  85. package/test/notify-hook.test.mjs +63 -0
  86. package/test/pi-installer.test.mjs +371 -0
  87. package/token-map.README.md +128 -0
  88. package/token-map.json +106 -0
  89. package/.github/workflows/ci.test.ts +0 -217
  90. package/.github/workflows/release.yml +0 -58
  91. package/scripts/build-claude-agents.mjs +0 -179
  92. package/scripts/publish-release.sh +0 -32
package/token-map.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "agent-cortex token map",
3
+ "description": "Central mapping of tool names, paths, and agent naming conventions across the copilot, claude, and pi harnesses. Single source of truth for install-time token substitution. See token-map.README.md for the design rationale.",
4
+ "version": 1,
5
+ "harnesses": ["copilot", "claude", "pi"],
6
+ "token_format": {
7
+ "convention": "{{TYPE:NAME}}",
8
+ "decision": "Adopt the {{TOOL:name}} / {{PATH:name}} / {{SECTION:name}} grammar already standardised by ADR #63 (agents/README.md) rather than a dotted {{tool.name}} form. The uppercase type prefix disambiguates token kinds at a glance, keeps tool/path/section names in one namespace collision-free, and matches the token grammar skills are already written against.",
9
+ "types": {
10
+ "TOOL": "{{TOOL:<canonical tool key>}} — replaced at install time with the bound harness's tool name (see tools).",
11
+ "PATH": "{{PATH:<path key or relative path>}} — replaced at install time with the harness-resolved path (see paths).",
12
+ "SECTION": "{{SECTION:<name>}} — replaced by the agent composer with <harness>/<name>.md section content. Not a global substitution; out of scope for token-map.json (see contract)."
13
+ }
14
+ },
15
+ "tools": {
16
+ "bash": { "copilot": "bash", "claude": "Bash", "pi": "bash" },
17
+ "view": { "copilot": "view", "claude": "Read", "pi": "read" },
18
+ "edit": { "copilot": "edit", "claude": "Edit", "pi": "edit" },
19
+ "create": { "copilot": "create", "claude": "Write", "pi": "write" },
20
+ "grep": { "copilot": "grep", "claude": "Grep", "pi": "grep" },
21
+ "rg": { "copilot": "rg", "claude": "Grep", "pi": "grep" },
22
+ "glob": { "copilot": "glob", "claude": "Glob", "pi": "find" },
23
+ "ask_user": { "copilot": "ask_user", "claude": "AskUserQuestion", "pi": null },
24
+ "web_fetch": { "copilot": "web_fetch", "claude": "WebFetch", "pi": "fetch_content" },
25
+ "skill": { "copilot": "skill", "claude": "Skill", "pi": null },
26
+ "task": { "copilot": "task", "claude": "Task", "pi": "task" },
27
+ "read_agent": { "copilot": "read_agent", "claude": null, "pi": "read_agent" },
28
+ "wait_for_agents": { "copilot": null, "claude": null, "pi": "wait_for_agents" }
29
+ },
30
+ "paths": {
31
+ "plugin_root": {
32
+ "copilot": "~/.copilot/installed-plugins/_direct/agent-cortex",
33
+ "claude": "${CLAUDE_PLUGIN_ROOT}",
34
+ "pi": "~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex"
35
+ },
36
+ "agents_dir": {
37
+ "copilot": { "base": "plugin_root", "relative": "agents" },
38
+ "claude": { "base": "plugin_root", "relative": "agents" },
39
+ "pi": { "base": "plugin_root", "relative": "agents" }
40
+ },
41
+ "skills_dir": {
42
+ "copilot": { "base": "plugin_root", "relative": "skills" },
43
+ "claude": { "base": "plugin_root", "relative": "skills" },
44
+ "pi": { "base": "plugin_root", "relative": "skills" }
45
+ }
46
+ },
47
+ "agents": {
48
+ "prefix": {
49
+ "copilot": "agent-cortex:",
50
+ "claude": "",
51
+ "pi": "agent-cortex:"
52
+ },
53
+ "deferred": {
54
+ "ralph": ["claude"]
55
+ },
56
+ "sources": {
57
+ "copilot": "agents/<name>/ composable dirs (canonical) — composed to agents/*.agent.md by scripts/build-copilot-agents.mjs",
58
+ "pi": "agents/<name>/ composable dirs (canonical) — composed at runtime by the agent-modes extension",
59
+ "claude": "agents/<name>/ composable dirs (canonical) — composed by bin/installers/claude.mjs (install-time generator; pnpm build:claude aliases it), plus agents-native/*.md for deferred agents"
60
+ }
61
+ },
62
+ "notes": {
63
+ "tools": {
64
+ "rg": "Copilot alias for grep. Claude and pi collapse it onto their single search tool (Grep / grep) — same collapse rule as the build script.",
65
+ "glob": "Claude has a dedicated Glob tool. pi's built-in pattern-search tool is find (hypa_find when the pi-hypa extension is installed).",
66
+ "ask_user": "Copilot (ask_user) and Claude (AskUserQuestion) prompt the user mid-run. pi intentionally ships no permission-popup/ask tool (per pi docs 'it intentionally does not include permission popups'), so the mapping is null.",
67
+ "web_fetch": "pi's fetch_content comes from the pi-web-access package (declared in pi/settings.json).",
68
+ "skill": "Copilot (skill) and Claude (Skill) expose skills as a tool. pi discovers skills but the agent loads them via read and /skill:<name> commands — there is no dedicated tool, so the mapping is null.",
69
+ "read_agent": "Claude's Task returns results inline — there is no separate read step, so read_agent drops on claude (null). Copilot uses the task + read_agent poll loop; pi blocks on wait_for_agents and keeps read_agent as a fallback.",
70
+ "wait_for_agents": "PI-only blocking wait over background subagent runs (the task tool): returns the results of completed agents and STILL RUNNING for the rest. Copilot has no equivalent tool (its task + read_agent poll loop stays) and Claude's Task delivers results inline with no wait step — both null."
71
+ },
72
+ "paths": {
73
+ "pi": "plugin_root defaults to the npm-package install layout (~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex). Local-checkout installs (pi/settings.json packages entry referencing a local src path) or symlinked installs differ — installers must compute plugin_root from the actual resolution and can override it."
74
+ },
75
+ "agents": {
76
+ "prefix": "Claude strips the agent-cortex: name prefix during conversion (NAME_PREFIX in bin/installers/claude.mjs): copilot/pi agents are agent-cortex:<slug>, claude agents are <slug>.",
77
+ "deferred": "ralph has no generated claude counterpart — it is authored natively as agents-native/ralph.md (its event-driven orchestration cannot be produced by section composition from the polling copilot source)."
78
+ }
79
+ },
80
+ "contract": {
81
+ "version": 1,
82
+ "summary": "Installers substitute {{TOOL:...}}, {{PATH:...}}, and {{SECTION:...}} tokens in generated harness files using this map. This contract is the single reference installers must implement; token-map.json is the data those rules operate on.",
83
+ "harness_ids": ["copilot", "claude", "pi"],
84
+ "binding": "The harness id is the same value the installer is invoked with (agent-cortex install <harness>). Each harness reads its own column from the tools, paths, and agents sections.",
85
+ "substitution_order": ["SECTION", "TOOL", "PATH"],
86
+ "rules": [
87
+ "TOOL: replace {{TOOL:<key>}} with tools.<key>.<harness>. Canonical keys are exact, case-sensitive strings (the copilot tool names, which are the authoring names in agents/<name>/agent.md).",
88
+ "TOOL null: a null value means the harness has no equivalent. In prose, remove the token (and emit a warning). In a tool-list context (frontmatter tools: [...]), omit the entry. Do not substitute an empty string.",
89
+ "TOOL unknown: a {{TOOL:<key>}} with no entry in tools is a hard error — the map must be extended. This mirrors the generators (scripts/build-copilot-agents.mjs, bin/installers/claude.mjs), which throw on unknown tools.",
90
+ "PATH named key: replace {{PATH:<key>}} with the resolved value of paths.<key>.<harness>.",
91
+ "PATH relative: replace {{PATH:<relative>}} with plugin_root + '/' + <relative> (e.g. {{PATH:skills/workflow/plan/SKILL.md}}).",
92
+ "PATH resolution: a value may be a literal string, an ${ENV_VAR} token, or a {base, relative} spec. Resolve {base, relative} by joining relative onto the resolved base. Leave ${ENV_VAR} values verbatim — the harness expands them at runtime (claude's ${CLAUDE_PLUGIN_ROOT}).",
93
+ "PATH unknown base: a {base, relative} spec whose base is not a key in paths is a hard error.",
94
+ "SECTION: {{SECTION:<name>}} is resolved by the agent composer from <agents_dir>/<harness>/<name>.md; token-map.json does not substitute it. Installers must leave SECTION tokens untouched for the composer.",
95
+ "Contract version: installers must reject token-map.json versions higher than the contract version they implement.",
96
+ "Case sensitivity: all token arguments and map keys are case-sensitive."
97
+ ],
98
+ "error_handling": {
99
+ "unknown_tool": "throw — the map must stay complete",
100
+ "unknown_path_base": "throw — the map must stay complete",
101
+ "null_tool_in_prose": "warn and drop the token",
102
+ "null_tool_in_tool_list": "omit the entry from the array",
103
+ "unsupported_version": "throw with the supported version range"
104
+ }
105
+ }
106
+ }
@@ -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,58 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- concurrency: ${{ github.workflow }}-${{ github.ref }}
9
-
10
- jobs:
11
- release:
12
- name: Release
13
- runs-on: ubuntu-latest
14
- permissions:
15
- contents: write
16
- pull-requests: write
17
- id-token: write
18
- steps:
19
- - name: Checkout
20
- uses: actions/checkout@v4
21
- with:
22
- fetch-depth: 0
23
-
24
- - name: Setup Node
25
- uses: actions/setup-node@v6
26
- with:
27
- node-version: 24
28
-
29
- - name: Setup pnpm
30
- uses: pnpm/action-setup@v4
31
- with:
32
- version: 10
33
-
34
- - name: Install dependencies
35
- run: pnpm install --frozen-lockfile
36
-
37
- # changesets/action is used ONLY to version packages and open/update
38
- # the "Version Packages" PR. We deliberately do NOT pass a publish
39
- # script here - changesets/action does not propagate the OIDC env vars
40
- # (ACTIONS_ID_TOKEN_REQUEST_URL / _TOKEN) to the spawned npm process,
41
- # so npm publish would fail with ENEEDAUTH. Publishing happens in a
42
- # separate top-level step below where env vars flow through cleanly.
43
- - name: Create Version Packages PR
44
- id: changesets
45
- uses: changesets/action@v1
46
- with:
47
- version: pnpm version-packages
48
- commit: "chore: version packages"
49
- title: "chore: version packages"
50
- env:
51
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52
-
53
- # When there are no changesets (i.e. the "Version Packages" PR was
54
- # merged), changesets/action does nothing - so we publish directly.
55
- # NOTE: @v1 (v1.9.0) exposes the output as 'hasChangesets' (camelCase).
56
- - name: Publish to npm
57
- if: steps.changesets.outputs.hasChangesets == 'false'
58
- run: bash scripts/publish-release.sh
@@ -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,32 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Custom publish script for changesets that uses pnpm pack + npm publish
3
- # This is needed because changeset publish doesn't properly handle OIDC
4
-
5
- set -euo pipefail
6
-
7
- # Check if there are any packages to publish
8
- VERSION=$(node -p "require('./package.json').version")
9
- PACKAGE_NAME=$(node -p "require('./package.json').name")
10
-
11
- echo "Publishing $PACKAGE_NAME@$VERSION"
12
-
13
- # Check if this version is already published
14
- if npm view "$PACKAGE_NAME@$VERSION" version 2>/dev/null; then
15
- echo "Version $VERSION is already published, skipping"
16
- exit 0
17
- fi
18
-
19
- # Pack the tarball
20
- TARBALL=$(pnpm pack | tail -n 1)
21
- echo "Created tarball: $TARBALL"
22
-
23
- # Publish using npm with OIDC trusted publishing.
24
- # NOTE: provenance is disabled because this repo is private - npm's
25
- # trusted publishing auto-enables provenance, but sigstore only supports
26
- # public repos. The masked error would be ENEEDAUTH.
27
- npm publish "$TARBALL" --access public --no-provenance
28
-
29
- echo "✓ Published $PACKAGE_NAME@$VERSION"
30
-
31
- # Create git tag
32
- git tag -a "v$VERSION" -m "Release $PACKAGE_NAME@$VERSION" || echo "Tag already exists"