@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
|
@@ -10,7 +10,7 @@ Detailed procedures for the fleet orchestration workflow. See [SKILL.md](./SKILL
|
|
|
10
10
|
2. **Read its `stage:*` label** from `bd show <id>`.
|
|
11
11
|
3. **Load the universal stage runner prompt** from `skills/workflow/run-pipeline-stage/prompts/stage-runner.md`.
|
|
12
12
|
4. **Read the parent task context**: follow the `parent-child` dependency to the parent task bead, run `bd show <parent-id>` to get the full task description.
|
|
13
|
-
5. **Ensure branch + worktree exist for this parent task** (see _Feature branches and worktrees_ below), then note the worktree path — you'll pass it as `cwd` to
|
|
13
|
+
5. **Ensure branch + worktree exist for this parent task** (see _Feature branches and worktrees_ below), then note the worktree path — you'll pass it as `cwd` to {{TOOL:task}}.
|
|
14
14
|
6. **Fill in the prompt** — replace placeholders with:
|
|
15
15
|
- Stage from bead label (`stage:<stage>`) for `<stage>`
|
|
16
16
|
- `bd prime` output (held in memory from initialization)
|
|
@@ -18,7 +18,7 @@ Detailed procedures for the fleet orchestration workflow. See [SKILL.md](./SKILL
|
|
|
18
18
|
- For `fix` stage: `FILES_CHANGED` from the preceding stage's report (the required changes come from the fix bead's own description — read via `bd show <fix-id>`)
|
|
19
19
|
- For `verify`, `review`, `document` stages: `SUMMARY` and `FILES_CHANGED` from the preceding stage's report
|
|
20
20
|
- Bead ID and log file path (`.agent-cortex/ralph/ralph-<bead-id>.log`)
|
|
21
|
-
7. **Call the
|
|
21
|
+
7. **Call the {{TOOL:task}} tool** with the filled prompt as `prompt` and the worktree path as `cwd`. Store the returned agentId — you need it for step 8 and for log polling.
|
|
22
22
|
8. **Map** the agent ID to the bead ID in memory for lookup on completion.
|
|
23
23
|
|
|
24
24
|
---
|
|
@@ -35,7 +35,7 @@ Run once at startup:
|
|
|
35
35
|
- **Task or other type without an `implementation-type:*` label**: unexpected here — classification is a planning-stage concern already applied by `create-task`. Do not invoke `classify-bead`; treat as needs-refinement and note for the shutdown summary.
|
|
36
36
|
- **Task labelled `implementation-type:hitl`**: skip — record the bead ID for the **Pending Human Action** summary at shutdown.
|
|
37
37
|
7. For each ready chore bead (up to 5), **dispatch** it (see _Dispatching a chore bead_ in REFERENCE.md). Chores for a parent bead must run from that parent's worktree.
|
|
38
|
-
8. **If** any chore beads were dispatched in step 7, start the **poll timer
|
|
38
|
+
8. **If** any chore beads were dispatched in step 7, start the **poll timer** (Copilot only): run `sleep 120` as a background bash process and hold its shellId in memory. PI has no timer — keep a `wait_for_agents` call outstanding over every in-flight agent ID instead (see the Event Loop note). **Otherwise**, if HITL gate beads are pending (check `bd list -l lifecycle:feature-pr -l implementation-type:hitl` and `bd list -l awaiting-epic-pr-merge`), proceed to **HITL Pause** (see REFERENCE.md) immediately.
|
|
39
39
|
9. Regenerate `.agent-cortex/ralph/progress.md`:
|
|
40
40
|
```bash
|
|
41
41
|
# workspace must be the absolute path you cd'd into — never . or $(pwd)
|
|
@@ -47,7 +47,7 @@ Run once at startup:
|
|
|
47
47
|
|
|
48
48
|
## Event Loop
|
|
49
49
|
|
|
50
|
-
After initialization, wait for background agents or the poll timer to complete. On each completion notification:
|
|
50
|
+
After initialization, wait for background agents or the poll timer to complete. Completion detection is harness-specific: the `sleep 120` poll timer described below is the **Copilot** mechanism. **PI** replaces it with the blocking `wait_for_agents` tool — it returns as soon as any in-flight agent finishes, so each return plays the role of a poll-timer tick with its completed-agent result attached; there is no timer to start or kill on PI. On each completion notification:
|
|
51
51
|
|
|
52
52
|
### Poll timer completed
|
|
53
53
|
|
|
@@ -59,7 +59,7 @@ After initialization, wait for background agents or the poll timer to complete.
|
|
|
59
59
|
### Background agent completed
|
|
60
60
|
|
|
61
61
|
1. **Poll that bead's log** to flush any final lines.
|
|
62
|
-
2. **Read** the completed agent's full output with `read_agent
|
|
62
|
+
2. **Read** the completed agent's full output with {{TOOL:read_agent}} (on PI the completed result arrives in the `wait_for_agents` response; read_agent remains a fallback for a specific result).
|
|
63
63
|
3. **Identify the bead**: parse the `---REPORT---` block for `BEAD_ID` and `STAGE_COMPLETED`.
|
|
64
64
|
4. **Close** the chore bead: `bd close <bead-id>`.
|
|
65
65
|
5. **Handle stage outcome**:
|
|
@@ -90,10 +90,10 @@ After initialization, wait for background agents or the poll timer to complete.
|
|
|
90
90
|
- **Always** run in **foreground** (interactive) mode. If you find yourself executing as a background task, immediately surface a warning to the user and ask them to re-run you in foreground mode.
|
|
91
91
|
- **Never** write, edit, or create source code or documentation yourself.
|
|
92
92
|
- **Never** edit bead task files directly — only use `bd` commands.
|
|
93
|
-
- **ALWAYS call the
|
|
93
|
+
- **ALWAYS call the {{TOOL:task}} tool to spawn subagents** — never use {{TOOL:bash}}, never run stages inline, never use any other tool. This is your only spawning mechanism.
|
|
94
94
|
- **Always** derive orchestration state from beads — do not maintain a separate state file.
|
|
95
|
-
- **Always** include the full `bd prime` output verbatim in every
|
|
96
|
-
- **Always** keep the poll timer running — restart it immediately after it fires — **unless** the HITL pause condition is met (no chores in-flight, no `stage:*` chores ready, HITL gate beads pending), in which case proceed to **HITL Pause** (see REFERENCE.md) and stop instead.
|
|
95
|
+
- **Always** include the full `bd prime` output verbatim in every {{TOOL:task}} tool prompt.
|
|
96
|
+
- **Always** keep the poll timer running (Copilot) — restart it immediately after it fires — **unless** the HITL pause condition is met (no chores in-flight, no `stage:*` chores ready, HITL gate beads pending), in which case proceed to **HITL Pause** (see REFERENCE.md) and stop instead. On PI there is no timer to keep alive — always keep one `wait_for_agents` call outstanding while agents are in-flight.
|
|
97
97
|
- **Never** post empty poll updates to chat — only surface new log content.
|
|
98
98
|
- **Max 5** tasks in-flight at once.
|
|
99
99
|
- **Max fix rounds** per task as defined by `maxFixRounds` in `skills/planning/create-task/pipeline.json`.
|
|
@@ -50,8 +50,10 @@ const STATUS_CHAR_MAP: Record<string, BeadStatus> = {
|
|
|
50
50
|
export function parseListLine(
|
|
51
51
|
line: string,
|
|
52
52
|
): { id: string; status: BeadStatus; title: string } | null {
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// Formats accepted:
|
|
54
|
+
// {STATUS_CHAR} {ID} P{N} {TITLE} — current bd list output
|
|
55
|
+
// {STATUS_CHAR} {ID} ● P{N} {TITLE} — older bd (● before P{N} is optional)
|
|
56
|
+
const match = line.trim().match(/^([○◐●✓❄])\s+(\S+)\s+(?:●\s+)?P\d+\s+(.+)$/);
|
|
55
57
|
if (!match) return null;
|
|
56
58
|
const [, statusChar, id, title] = match;
|
|
57
59
|
const status = STATUS_CHAR_MAP[statusChar];
|
|
@@ -28,9 +28,11 @@ Task arrives
|
|
|
28
28
|
├── Running beads / task execution? ────→ ralph
|
|
29
29
|
│ └── Single stage? ─────────────────→ run-pipeline-stage
|
|
30
30
|
├── Implementing code? ────────────────→ tdd
|
|
31
|
-
│
|
|
31
|
+
│ ├── Designing an interface? ───────→ design-an-interface
|
|
32
|
+
│ └── Need style conventions? ───────→ style-code
|
|
32
33
|
├── Reviewing code? ───────────────────→ style-code
|
|
33
|
-
│
|
|
34
|
+
│ ├── Security concerns? ────────────→ review-security
|
|
35
|
+
│ └── Reviewing tests? ──────────────→ style-tests
|
|
34
36
|
├── Writing tests? ────────────────────→ style-tests
|
|
35
37
|
├── Writing docs? ─────────────────────→ style-documentation
|
|
36
38
|
├── Writing external comms? ───────────→ style-comms
|
|
@@ -76,6 +78,14 @@ Touch only what you're asked to touch. Do not clean up orthogonal code, refactor
|
|
|
76
78
|
|
|
77
79
|
A task is not complete until verification passes. "Seems right" is never sufficient — there must be evidence: passing tests, build output, or runtime data.
|
|
78
80
|
|
|
81
|
+
### 7. Flag Persistent Failures
|
|
82
|
+
|
|
83
|
+
When a tool or command fails repeatedly, stop and flag it to the user for investigation. Don't paper over failures with workarounds — workarounds waste tokens and hide real problems. Describe what failed, how many times, and any obvious patterns. Let the user decide whether to investigate in a dedicated session. Inefficiencies and failures should be squashed, not hidden.
|
|
84
|
+
|
|
85
|
+
### 8. Stay On Target
|
|
86
|
+
|
|
87
|
+
Stick to the task at hand. If you discover work that's outside the current scope — a tangential bug, an adjacent improvement, a missing doc — flag it to the user but don't pursue it. If the side quest blocks or slows your progress, stop and report it. Otherwise, note it and get back on track. Scope creep mid-task burns tokens and dilutes focus.
|
|
88
|
+
|
|
79
89
|
## Lifecycle Sequence
|
|
80
90
|
|
|
81
91
|
For a complete feature, the typical skill sequence is:
|
|
@@ -100,6 +110,9 @@ Not every task needs every skill. A bug fix might only need: `tdd` → `style-co
|
|
|
100
110
|
- Making wrong assumptions without stating them.
|
|
101
111
|
- Overcomplicating code when a simple solution exists.
|
|
102
112
|
- Modifying code orthogonal to the task scope.
|
|
113
|
+
- Going on side-quests instead of staying on target.
|
|
114
|
+
- Going on side-quests instead of staying on target.
|
|
115
|
+
- Going on side-quests instead of staying on target.
|
|
103
116
|
|
|
104
117
|
## Verification checklist
|
|
105
118
|
|
package/test/cli.test.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { dirname, join } from "node:path";
|
|
6
8
|
import { parseArgs, buildHelpText, validateHarness } from "../lib/cli.mjs";
|
|
@@ -56,6 +58,84 @@ describe("parseArgs", () => {
|
|
|
56
58
|
assert.deepStrictEqual(result, { command: "install", harness: null });
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
it("returns output for install claude --output <dir>", () => {
|
|
62
|
+
const result = parseArgs(["install", "claude", "--output", "/tmp/x"]);
|
|
63
|
+
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns output for install claude --output=<dir>", () => {
|
|
67
|
+
const result = parseArgs(["install", "claude", "--output=/tmp/x"]);
|
|
68
|
+
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns dryRun for install claude --dry-run", () => {
|
|
72
|
+
const result = parseArgs(["install", "claude", "--dry-run"]);
|
|
73
|
+
assert.deepStrictEqual(result, { command: "install", harness: "claude", dryRun: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("combines --dry-run with --output", () => {
|
|
77
|
+
const result = parseArgs(["install", "claude", "--output", "/tmp/x", "--dry-run"]);
|
|
78
|
+
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x", dryRun: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("reports an unknown option for claude", () => {
|
|
82
|
+
const result = parseArgs(["install", "claude", "--bogus"]);
|
|
83
|
+
assert.equal(result.command, "install");
|
|
84
|
+
assert.equal(result.harness, "claude");
|
|
85
|
+
assert.ok(result.optionError.includes("--bogus"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reports a missing value for --output for claude", () => {
|
|
89
|
+
const result = parseArgs(["install", "claude", "--output"]);
|
|
90
|
+
assert.equal(result.command, "install");
|
|
91
|
+
assert.equal(result.harness, "claude");
|
|
92
|
+
assert.ok(result.optionError.includes("--output"));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("parses --dry-run for install", () => {
|
|
96
|
+
const result = parseArgs(["install", "pi", "--dry-run"]);
|
|
97
|
+
assert.deepStrictEqual(result, { command: "install", harness: "pi", dryRun: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("parses --output <dir> for install", () => {
|
|
101
|
+
const result = parseArgs(["install", "pi", "--output", "/tmp/out"]);
|
|
102
|
+
assert.deepStrictEqual(result, { command: "install", harness: "pi", output: "/tmp/out" });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("parses --plugin-root <dir> for install", () => {
|
|
106
|
+
const result = parseArgs(["install", "pi", "--plugin-root", "/tmp/plugin"]);
|
|
107
|
+
assert.deepStrictEqual(result, { command: "install", harness: "pi", pluginRoot: "/tmp/plugin" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("combines multiple install options", () => {
|
|
111
|
+
const result = parseArgs(["install", "pi", "--dry-run", "--output", "/tmp/out", "--plugin-root", "/tmp/plugin"]);
|
|
112
|
+
assert.deepStrictEqual(result, {
|
|
113
|
+
command: "install",
|
|
114
|
+
harness: "pi",
|
|
115
|
+
dryRun: true,
|
|
116
|
+
output: "/tmp/out",
|
|
117
|
+
pluginRoot: "/tmp/plugin",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("reports a missing value for --output", () => {
|
|
122
|
+
const result = parseArgs(["install", "pi", "--output"]);
|
|
123
|
+
assert.equal(result.command, "install");
|
|
124
|
+
assert.equal(result.harness, "pi");
|
|
125
|
+
assert.ok(result.optionError.includes("--output"));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("reports an unknown option", () => {
|
|
129
|
+
const result = parseArgs(["install", "pi", "--bogus"]);
|
|
130
|
+
assert.equal(result.command, "install");
|
|
131
|
+
assert.equal(result.harness, "pi");
|
|
132
|
+
assert.ok(result.optionError.includes("--bogus"));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("keeps the old shape when no options are given", () => {
|
|
136
|
+
assert.deepStrictEqual(parseArgs(["install", "pi"]), { command: "install", harness: "pi" });
|
|
137
|
+
});
|
|
138
|
+
|
|
59
139
|
it("returns { command: 'unknown', name: 'foo' } for unknown command", () => {
|
|
60
140
|
const result = parseArgs(["foo"]);
|
|
61
141
|
assert.deepStrictEqual(result, { command: "unknown", name: "foo" });
|
|
@@ -117,8 +197,9 @@ describe("CLI integration", () => {
|
|
|
117
197
|
assert.ok(stdout.includes("copilot"));
|
|
118
198
|
});
|
|
119
199
|
|
|
120
|
-
it("exits 0 for install claude", async () => {
|
|
121
|
-
const
|
|
200
|
+
it("exits 0 for install claude (into a temp output dir)", async () => {
|
|
201
|
+
const dir = mkdtempSync(join(tmpdir(), "agent-cortex-cli-"));
|
|
202
|
+
const { exitCode, stdout } = await runCli(["install", "claude", "--output", dir]);
|
|
122
203
|
assert.equal(exitCode, 0);
|
|
123
204
|
assert.ok(stdout.includes("claude"));
|
|
124
205
|
});
|
|
@@ -135,6 +216,38 @@ describe("CLI integration", () => {
|
|
|
135
216
|
assert.ok(stderr.includes("Missing harness"));
|
|
136
217
|
});
|
|
137
218
|
|
|
219
|
+
it("exits 1 and prints error for install with an unknown option", async () => {
|
|
220
|
+
const { exitCode, stderr } = await runCli(["install", "pi", "--bogus"]);
|
|
221
|
+
assert.equal(exitCode, 1);
|
|
222
|
+
assert.ok(stderr.includes("--bogus"));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("exits 1 and prints error for install with a missing option value", async () => {
|
|
226
|
+
const { exitCode, stderr } = await runCli(["install", "pi", "--output"]);
|
|
227
|
+
assert.equal(exitCode, 1);
|
|
228
|
+
assert.ok(stderr.includes("--output"));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("exits 0 for install pi --dry-run and writes nothing", async () => {
|
|
232
|
+
const { exitCode, stdout } = await runCli(["install", "pi", "--dry-run"]);
|
|
233
|
+
assert.equal(exitCode, 0);
|
|
234
|
+
assert.ok(stdout.includes("ralph.agent.md"));
|
|
235
|
+
assert.ok(stdout.includes("dry-run"));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("exits 0 for install pi --output <tmp> and writes agent files", async () => {
|
|
239
|
+
const outDir = mkdtempSync(join(tmpdir(), "cli-pi-install-"));
|
|
240
|
+
try {
|
|
241
|
+
const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
|
|
242
|
+
assert.equal(exitCode, 0);
|
|
243
|
+
assert.ok(stdout.includes("ralph.agent.md"));
|
|
244
|
+
const ralph = readFileSync(join(outDir, "agents", "ralph.agent.md"), "utf-8");
|
|
245
|
+
assert.ok(ralph.includes("agent-cortex:ralph"));
|
|
246
|
+
} finally {
|
|
247
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
138
251
|
it("exits 1 and prints error for install with unknown harness", async () => {
|
|
139
252
|
const { exitCode, stderr } = await runCli(["install", "docker"]);
|
|
140
253
|
assert.equal(exitCode, 1);
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Integration tests for `agent-cortex install copilot`: the install-time
|
|
2
|
+
// generator for the Copilot CLI flat agent files (agents/*.agent.md). All tests
|
|
3
|
+
// install into mkdtemp dirs so the real repo agents/ is never touched.
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { installCopilot } from "../bin/installers/copilot.mjs";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = join(__dirname, "..");
|
|
16
|
+
const CLI_PATH = join(ROOT, "bin", "agent-cortex.mjs");
|
|
17
|
+
|
|
18
|
+
function runCli(args) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
execFile(process.execPath, [CLI_PATH, ...args], (error, stdout, stderr) => {
|
|
21
|
+
resolve({ exitCode: error ? error.code ?? 1 : 0, stdout, stderr });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function tokenMap() {
|
|
29
|
+
return {
|
|
30
|
+
name: "fixture token map",
|
|
31
|
+
version: 1,
|
|
32
|
+
harnesses: ["copilot"],
|
|
33
|
+
tools: {
|
|
34
|
+
bash: { copilot: "bash" },
|
|
35
|
+
view: { copilot: "view" },
|
|
36
|
+
task: { copilot: "task" },
|
|
37
|
+
ask_user: { copilot: "ask_user" },
|
|
38
|
+
},
|
|
39
|
+
paths: {
|
|
40
|
+
plugin_root: { copilot: "~/.copilot/installed-plugins/_direct/agent-cortex" },
|
|
41
|
+
agents_dir: { copilot: { base: "plugin_root", relative: "agents" } },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeFixture() {
|
|
47
|
+
const root = mkdtempSync(join(tmpdir(), "copilot-installer-"));
|
|
48
|
+
return {
|
|
49
|
+
root,
|
|
50
|
+
output: join(root, "generated"),
|
|
51
|
+
cleanup: () => rmSync(root, { recursive: true, force: true }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeFixture(fx, relPath, content) {
|
|
56
|
+
const p = join(fx.root, relPath);
|
|
57
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
58
|
+
writeFileSync(p, content);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Seed a minimal but realistic package tree (agents + token-map). */
|
|
62
|
+
function seedCopilotPackage(fx, opts = {}) {
|
|
63
|
+
const { agents = {} } = opts;
|
|
64
|
+
writeFixture(fx, "token-map.json", JSON.stringify(tokenMap()));
|
|
65
|
+
for (const [name, def] of Object.entries(agents)) {
|
|
66
|
+
writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
|
|
67
|
+
writeFixture(
|
|
68
|
+
fx,
|
|
69
|
+
`agents/${name}/copilot/frontmatter.json`,
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
def.frontmatter ?? {
|
|
72
|
+
name: `agent-cortex:${name}`,
|
|
73
|
+
description: `Description of ${name}`,
|
|
74
|
+
tools: ["bash", "view"],
|
|
75
|
+
argumentHint: `Hint for ${name}`,
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
for (const [section, content] of Object.entries(def.sections ?? {})) {
|
|
80
|
+
writeFixture(fx, `agents/${name}/copilot/${section}.md`, content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Deterministic snapshot of a directory: relative paths + byte content. */
|
|
86
|
+
function snapshot(dir) {
|
|
87
|
+
const entries = [];
|
|
88
|
+
const walk = (p) => {
|
|
89
|
+
for (const e of readdirSync(p, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
90
|
+
const full = join(p, e.name);
|
|
91
|
+
if (e.isDirectory()) {
|
|
92
|
+
entries.push([e.name, "dir"]);
|
|
93
|
+
walk(full);
|
|
94
|
+
} else entries.push([e.name, "file", readFileSync(full, "utf-8")]);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
walk(dir);
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Agent composition ───────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("installCopilot — agent composition", () => {
|
|
104
|
+
it("writes one flat <name>.agent.md per composable agent dir with copilot frontmatter and no leftover tokens", () => {
|
|
105
|
+
const fx = makeFixture();
|
|
106
|
+
try {
|
|
107
|
+
seedCopilotPackage(fx, {
|
|
108
|
+
agents: {
|
|
109
|
+
alpha: {
|
|
110
|
+
body: "# alpha\nUse {{TOOL:view}} to read.\n{{SECTION:polling}}\nRead {{PATH:agents_dir}}.",
|
|
111
|
+
sections: { polling: "Spawn with {{TOOL:task}}." },
|
|
112
|
+
frontmatter: {
|
|
113
|
+
name: "agent-cortex:alpha",
|
|
114
|
+
description: "Reads things.",
|
|
115
|
+
tools: ["bash", "view"],
|
|
116
|
+
argumentHint: "Read something",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = installCopilot({ root: fx.root, output: fx.output });
|
|
123
|
+
|
|
124
|
+
const file = join(fx.output, "alpha.agent.md");
|
|
125
|
+
assert.ok(existsSync(file), "flat agent file written");
|
|
126
|
+
const content = readFileSync(file, "utf-8");
|
|
127
|
+
assert.ok(content.startsWith("---\n"), "yaml frontmatter");
|
|
128
|
+
assert.ok(content.includes("# GENERATED from agents/alpha/ by scripts/build-copilot-agents.mjs — DO NOT EDIT."));
|
|
129
|
+
// Committed flat-file field order: description, name, tools (YAML array), argument-hint.
|
|
130
|
+
const order = ["description:", "name: \"agent-cortex:alpha\"", "tools: [", "argument-hint:"].map((s) => content.indexOf(s));
|
|
131
|
+
assert.ok(order.every((i) => i !== -1), "all frontmatter fields present");
|
|
132
|
+
assert.deepEqual([...order].sort((a, b) => a - b), order, "frontmatter fields in committed order");
|
|
133
|
+
assert.match(content, /tools: \["bash", "view"\]/, "copilot tools kept as a YAML array (no translation)");
|
|
134
|
+
assert.ok(content.includes("Use view to read."), "{{TOOL:view}} substituted");
|
|
135
|
+
assert.ok(content.includes("Spawn with task."), "{{SECTION:polling}} composed from copilot/polling.md");
|
|
136
|
+
assert.ok(!/{{/.test(content), "no literal tokens remain");
|
|
137
|
+
assert.equal(result.agents.length, 1);
|
|
138
|
+
assert.deepEqual(result.agents, ["alpha"]);
|
|
139
|
+
assert.equal(result.output, fx.output);
|
|
140
|
+
} finally {
|
|
141
|
+
fx.cleanup();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("skips non-composable entries in agents/ and never clobbers composable sources", () => {
|
|
146
|
+
const fx = makeFixture();
|
|
147
|
+
try {
|
|
148
|
+
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
149
|
+
writeFixture(fx, "agents/README.md", "# agents dir docs\n");
|
|
150
|
+
|
|
151
|
+
const result = installCopilot({ root: fx.root, output: fx.output });
|
|
152
|
+
|
|
153
|
+
assert.deepEqual(readdirSync(fx.output).sort(), ["alpha.agent.md"], "no README, no subdirectories written");
|
|
154
|
+
assert.deepEqual(result.agents, ["alpha"]);
|
|
155
|
+
// The composable source dir next to the generated file stays untouched.
|
|
156
|
+
assert.ok(existsSync(join(fx.root, "agents", "alpha", "agent.md")), "composable source preserved");
|
|
157
|
+
assert.ok(existsSync(join(fx.root, "agents", "alpha", "copilot", "frontmatter.json")), "harness dir preserved");
|
|
158
|
+
} finally {
|
|
159
|
+
fx.cleanup();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("throws when a composable dir lacks copilot/frontmatter.json", () => {
|
|
164
|
+
const fx = makeFixture();
|
|
165
|
+
try {
|
|
166
|
+
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
167
|
+
rmSync(join(fx.root, "agents", "alpha", "copilot"), { recursive: true, force: true });
|
|
168
|
+
assert.throws(
|
|
169
|
+
() => installCopilot({ root: fx.root, output: fx.output }),
|
|
170
|
+
/without copilot\/frontmatter\.json/,
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
fx.cleanup();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("defaults to the plugin-scanned agents/ dir of the package root (plugin.json \"agents\": \"agents/\")", () => {
|
|
178
|
+
const fx = makeFixture();
|
|
179
|
+
try {
|
|
180
|
+
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
181
|
+
|
|
182
|
+
const result = installCopilot({ root: fx.root });
|
|
183
|
+
|
|
184
|
+
assert.equal(result.output, join(fx.root, "agents"), "default output is the dir plugin.json scans");
|
|
185
|
+
assert.ok(existsSync(join(fx.root, "agents", "alpha.agent.md")), "flat file lands next to the composable dir");
|
|
186
|
+
assert.ok(existsSync(join(fx.root, "agents", "alpha", "agent.md")), "composable source untouched");
|
|
187
|
+
} finally {
|
|
188
|
+
fx.cleanup();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("throws on an unknown tool token", () => {
|
|
193
|
+
const fx = makeFixture();
|
|
194
|
+
try {
|
|
195
|
+
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
|
|
196
|
+
assert.throws(() => installCopilot({ root: fx.root, output: fx.output }), /unknown tool token {{TOOL:nope}}/);
|
|
197
|
+
} finally {
|
|
198
|
+
fx.cleanup();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ─── Output / dry-run ────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
describe("installCopilot — output & dry-run", () => {
|
|
206
|
+
it("dry-run reports the plan without writing anything", () => {
|
|
207
|
+
const fx = makeFixture();
|
|
208
|
+
try {
|
|
209
|
+
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
210
|
+
|
|
211
|
+
const result = installCopilot({ root: fx.root, output: fx.output, dryRun: true });
|
|
212
|
+
|
|
213
|
+
assert.equal(result.dryRun, true);
|
|
214
|
+
assert.equal(existsSync(fx.output), false, "no output dir created in dry-run");
|
|
215
|
+
assert.deepEqual(result.agents, ["alpha"], "planned files still reported");
|
|
216
|
+
} finally {
|
|
217
|
+
fx.cleanup();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("is deterministic across repeated installs into the same output", () => {
|
|
222
|
+
const fx = makeFixture();
|
|
223
|
+
try {
|
|
224
|
+
seedCopilotPackage(fx, {
|
|
225
|
+
agents: {
|
|
226
|
+
alpha: { body: "# alpha\n{{SECTION:polling}}", sections: { polling: "Poll with {{TOOL:task}}." } },
|
|
227
|
+
beta: { body: "# beta" },
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
installCopilot({ root: fx.root, output: fx.output });
|
|
232
|
+
const first = snapshot(fx.output);
|
|
233
|
+
installCopilot({ root: fx.root, output: fx.output });
|
|
234
|
+
assert.deepEqual(snapshot(fx.output), first);
|
|
235
|
+
} finally {
|
|
236
|
+
fx.cleanup();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── Integration against the real repo ───────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
describe("installCopilot — real repo integration", () => {
|
|
244
|
+
it("regenerates byte-identical copies of the committed agents/*.agent.md files", () => {
|
|
245
|
+
const fx = makeFixture();
|
|
246
|
+
try {
|
|
247
|
+
const result = installCopilot({ root: ROOT, output: fx.output });
|
|
248
|
+
|
|
249
|
+
assert.deepEqual(result.agents.sort(), ["plan", "ralph", "ralph-plan", "strategy"]);
|
|
250
|
+
for (const name of result.agents) {
|
|
251
|
+
const generated = readFileSync(join(fx.output, `${name}.agent.md`), "utf-8");
|
|
252
|
+
const committed = readFileSync(join(ROOT, "agents", `${name}.agent.md`), "utf-8");
|
|
253
|
+
assert.equal(generated, committed, `${name}.agent.md must match the committed flat file (CI drift check)`);
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
fx.cleanup();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ─── CLI wiring ──────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
describe("CLI integration", () => {
|
|
264
|
+
it("exits 0 for install copilot --output <tmp> and writes the flat files", async () => {
|
|
265
|
+
const fx = makeFixture();
|
|
266
|
+
try {
|
|
267
|
+
const { exitCode, stdout } = await runCli(["install", "copilot", "--output", fx.output]);
|
|
268
|
+
assert.equal(exitCode, 0);
|
|
269
|
+
assert.ok(stdout.includes("copilot"), "prints the harness name");
|
|
270
|
+
assert.ok(stdout.includes("Generated"), "prints the generation summary");
|
|
271
|
+
assert.ok(existsSync(join(fx.output, "ralph.agent.md")), "agent files written");
|
|
272
|
+
assert.ok(existsSync(join(fx.output, "plan.agent.md")));
|
|
273
|
+
} finally {
|
|
274
|
+
fx.cleanup();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("exits 0 for install copilot --dry-run and writes nothing", async () => {
|
|
279
|
+
const fx = makeFixture();
|
|
280
|
+
try {
|
|
281
|
+
const { exitCode, stdout } = await runCli(["install", "copilot", "--output", fx.output, "--dry-run"]);
|
|
282
|
+
assert.equal(exitCode, 0);
|
|
283
|
+
assert.ok(/dry-run/i.test(stdout));
|
|
284
|
+
assert.equal(existsSync(fx.output), false, "no files written in dry-run");
|
|
285
|
+
} finally {
|
|
286
|
+
fx.cleanup();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("exits 0 for install copilot (default output) without touching committed files", async () => {
|
|
291
|
+
const before = readFileSync(join(ROOT, "agents", "ralph.agent.md"), "utf-8");
|
|
292
|
+
const { exitCode } = await runCli(["install", "copilot"]);
|
|
293
|
+
assert.equal(exitCode, 0);
|
|
294
|
+
// Default output regenerates the shared agents/ dir in place — output must
|
|
295
|
+
// stay byte-identical to what is committed.
|
|
296
|
+
assert.equal(readFileSync(join(ROOT, "agents", "ralph.agent.md"), "utf-8"), before);
|
|
297
|
+
});
|
|
298
|
+
});
|