@jaybeeuu/agent-cortex 1.39.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/style-versioning/SKILL.md +1 -1
- package/.github/workflows/ci.yml +47 -14
- package/AGENTS.md +35 -8
- package/CHANGELOG.md +60 -0
- package/README.md +174 -97
- package/agents/README.md +14 -11
- package/bin/agent-cortex.mjs +55 -10
- package/bin/installers/claude.mjs +674 -174
- package/bin/installers/copilot.mjs +13 -18
- package/bin/installers/pi.mjs +293 -51
- package/claude/settings.json +4 -0
- package/{claude → claude-extras}/scripts/statusline-command.sh +32 -4
- package/docs/claude-hooks.md +15 -3
- package/docs/ideas/ban-content-assertion-tests.md +73 -0
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
- package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
- package/docs/ideas/extension-manifests.md +55 -14
- package/docs/ideas/improve-pi-system-prompt.md +5 -2
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
- package/docs/ideas/record-idea-implementation-questions.md +68 -0
- package/docs/ideas/self-flushing-session.md +72 -0
- package/docs/ideas/state-in-beads-not-md.md +69 -0
- package/docs/ideas/task-session-context.local.md +95 -0
- package/docs/ideas/test-data-creation-helpers.md +92 -0
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
- package/docs/technical-direction/harness-config.md +161 -0
- package/docs/technical-direction/migration-execution-labels.md +60 -23
- package/extensions/agent-guardrails/README.md +45 -0
- package/extensions/agent-guardrails/guardrails.test.ts +42 -0
- package/extensions/agent-guardrails/guardrails.ts +66 -0
- package/extensions/agent-guardrails/index.ts +22 -0
- package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
- package/extensions/agent-guardrails/package.json +19 -0
- package/extensions/agent-guardrails/tsconfig.json +12 -0
- package/extensions/agent-modes/README.md +12 -5
- package/extensions/agent-modes/discover.test.ts +30 -7
- package/extensions/agent-modes/discover.ts +1 -1
- package/extensions/agent-modes/index.test.ts +208 -0
- package/extensions/agent-modes/index.ts +23 -6
- package/extensions/agent-modes/package.json +1 -1
- package/extensions/skill-stats/index.test.ts +3 -2
- package/lib/cli.mjs +30 -12
- package/lib/pi-packages.mjs +169 -0
- package/package.json +8 -4
- package/pi/settings.json +23 -12
- package/plugin.json +1 -1
- package/scripts/build-copilot-agents.mjs +1 -1
- package/scripts/check-no-sync.mjs +49 -0
- package/scripts/lib/compose-agent.mjs +32 -14
- package/scripts/lib/fs.mjs +31 -0
- package/skills/engineering/grill-with-docs/SKILL.md +1 -1
- package/skills/planning/classify-bead/REFERENCE.md +85 -0
- package/skills/planning/classify-bead/SKILL.md +32 -36
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
- package/skills/planning/create-task/SKILL.md +19 -10
- package/skills/planning/write-a-prd/SKILL.md +2 -2
- package/skills/style/style-code/REFERENCE.md +32 -2
- package/skills/style/style-code/SKILL.md +5 -21
- package/skills/style/style-tests/SKILL.md +69 -17
- package/skills/workflow/hitl-collab/SKILL.md +1 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
- package/test/classify-bead.test.mjs +277 -0
- package/test/claude-installer.test.mjs +199 -139
- package/test/cli.test.mjs +95 -41
- package/test/copilot-installer.test.mjs +92 -82
- package/test/fs.test.mjs +76 -0
- package/test/helpers/fake-claude.mjs +152 -0
- package/test/installer.test.mjs +542 -57
- package/test/pi-installer.test.mjs +457 -102
- package/test/pi-packages.test.mjs +295 -0
- package/token-map.README.md +23 -12
- package/token-map.json +8 -7
- package/.claude-plugin/marketplace.json +0 -16
- package/claude/.claude-plugin/plugin.json +0 -11
- package/claude/agents/plan.md +0 -58
- package/claude/agents/ralph-plan.md +0 -121
- package/claude/agents/ralph.md +0 -193
- package/claude/agents/strategy.md +0 -111
- package/claude/hooks/scripts/notify.mjs +0 -54
- package/claude/hooks.json +0 -38
- /package/{claude → claude-extras}/.mcp.json +0 -0
package/test/cli.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 {
|
|
4
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
@@ -10,104 +10,125 @@ import { parseArgs, buildHelpText, validateHarness } from "../lib/cli.mjs";
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const CLI_PATH = join(__dirname, "..", "bin", "agent-cortex.mjs");
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** True when a path exists (stat succeeds). */
|
|
14
|
+
async function pathExists(p) {
|
|
15
|
+
try {
|
|
16
|
+
await stat(p);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Run the CLI; optionally pass a custom env (e.g. a fake HOME). */
|
|
24
|
+
function runCli(args, env = {}) {
|
|
14
25
|
return new Promise((resolve) => {
|
|
15
|
-
execFile(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
execFile(
|
|
27
|
+
process.execPath,
|
|
28
|
+
[CLI_PATH, ...args],
|
|
29
|
+
{ env: { ...process.env, ...env } },
|
|
30
|
+
(error, stdout, stderr) => {
|
|
31
|
+
resolve({
|
|
32
|
+
exitCode: error ? error.code ?? 1 : 0,
|
|
33
|
+
stdout: stdout,
|
|
34
|
+
stderr: stderr,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
);
|
|
22
38
|
});
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
describe("parseArgs", () => {
|
|
26
|
-
it("returns { command: 'help' } when no arguments given", () => {
|
|
42
|
+
it("returns { command: 'help' } when no arguments given", async () => {
|
|
27
43
|
const result = parseArgs([]);
|
|
28
44
|
assert.deepStrictEqual(result, { command: "help" });
|
|
29
45
|
});
|
|
30
46
|
|
|
31
|
-
it("returns { command: 'help' } for --help flag", () => {
|
|
47
|
+
it("returns { command: 'help' } for --help flag", async () => {
|
|
32
48
|
const result = parseArgs(["--help"]);
|
|
33
49
|
assert.deepStrictEqual(result, { command: "help" });
|
|
34
50
|
});
|
|
35
51
|
|
|
36
|
-
it("returns { command: 'help' } for -h flag", () => {
|
|
52
|
+
it("returns { command: 'help' } for -h flag", async () => {
|
|
37
53
|
const result = parseArgs(["-h"]);
|
|
38
54
|
assert.deepStrictEqual(result, { command: "help" });
|
|
39
55
|
});
|
|
40
56
|
|
|
41
|
-
it("returns { command: 'install', harness: 'copilot' } for install copilot", () => {
|
|
57
|
+
it("returns { command: 'install', harness: 'copilot' } for install copilot", async () => {
|
|
42
58
|
const result = parseArgs(["install", "copilot"]);
|
|
43
59
|
assert.deepStrictEqual(result, { command: "install", harness: "copilot" });
|
|
44
60
|
});
|
|
45
61
|
|
|
46
|
-
it("returns { command: 'install', harness: 'claude' } for install claude", () => {
|
|
62
|
+
it("returns { command: 'install', harness: 'claude' } for install claude", async () => {
|
|
47
63
|
const result = parseArgs(["install", "claude"]);
|
|
48
64
|
assert.deepStrictEqual(result, { command: "install", harness: "claude" });
|
|
49
65
|
});
|
|
50
66
|
|
|
51
|
-
it("returns { command: 'install', harness: 'pi' } for install pi", () => {
|
|
67
|
+
it("returns { command: 'install', harness: 'pi' } for install pi", async () => {
|
|
52
68
|
const result = parseArgs(["install", "pi"]);
|
|
53
69
|
assert.deepStrictEqual(result, { command: "install", harness: "pi" });
|
|
54
70
|
});
|
|
55
71
|
|
|
56
|
-
it("returns { command: 'install', harness: null } when install has no harness", () => {
|
|
72
|
+
it("returns { command: 'install', harness: null } when install has no harness", async () => {
|
|
57
73
|
const result = parseArgs(["install"]);
|
|
58
74
|
assert.deepStrictEqual(result, { command: "install", harness: null });
|
|
59
75
|
});
|
|
60
76
|
|
|
61
|
-
it("returns output for install claude --output <dir>", () => {
|
|
77
|
+
it("returns output for install claude --output <dir>", async () => {
|
|
62
78
|
const result = parseArgs(["install", "claude", "--output", "/tmp/x"]);
|
|
63
79
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
|
|
64
80
|
});
|
|
65
81
|
|
|
66
|
-
it("returns output for install claude --output=<dir>", () => {
|
|
82
|
+
it("returns output for install claude --output=<dir>", async () => {
|
|
67
83
|
const result = parseArgs(["install", "claude", "--output=/tmp/x"]);
|
|
68
84
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
|
|
69
85
|
});
|
|
70
86
|
|
|
71
|
-
it("returns dryRun for install claude --dry-run", () => {
|
|
87
|
+
it("returns dryRun for install claude --dry-run", async () => {
|
|
72
88
|
const result = parseArgs(["install", "claude", "--dry-run"]);
|
|
73
89
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", dryRun: true });
|
|
74
90
|
});
|
|
75
91
|
|
|
76
|
-
it("combines --dry-run with --output", () => {
|
|
92
|
+
it("combines --dry-run with --output", async () => {
|
|
77
93
|
const result = parseArgs(["install", "claude", "--output", "/tmp/x", "--dry-run"]);
|
|
78
94
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x", dryRun: true });
|
|
79
95
|
});
|
|
80
96
|
|
|
81
|
-
it("reports an unknown option for claude", () => {
|
|
97
|
+
it("reports an unknown option for claude", async () => {
|
|
82
98
|
const result = parseArgs(["install", "claude", "--bogus"]);
|
|
83
99
|
assert.equal(result.command, "install");
|
|
84
100
|
assert.equal(result.harness, "claude");
|
|
85
101
|
assert.ok(result.optionError.includes("--bogus"));
|
|
86
102
|
});
|
|
87
103
|
|
|
88
|
-
it("reports a missing value for --output for claude", () => {
|
|
104
|
+
it("reports a missing value for --output for claude", async () => {
|
|
89
105
|
const result = parseArgs(["install", "claude", "--output"]);
|
|
90
106
|
assert.equal(result.command, "install");
|
|
91
107
|
assert.equal(result.harness, "claude");
|
|
92
108
|
assert.ok(result.optionError.includes("--output"));
|
|
93
109
|
});
|
|
94
110
|
|
|
95
|
-
it("parses --dry-run for install", () => {
|
|
111
|
+
it("parses --dry-run for install", async () => {
|
|
96
112
|
const result = parseArgs(["install", "pi", "--dry-run"]);
|
|
97
113
|
assert.deepStrictEqual(result, { command: "install", harness: "pi", dryRun: true });
|
|
98
114
|
});
|
|
99
115
|
|
|
100
|
-
it("parses --output <dir> for install", () => {
|
|
116
|
+
it("parses --output <dir> for install", async () => {
|
|
101
117
|
const result = parseArgs(["install", "pi", "--output", "/tmp/out"]);
|
|
102
118
|
assert.deepStrictEqual(result, { command: "install", harness: "pi", output: "/tmp/out" });
|
|
103
119
|
});
|
|
104
120
|
|
|
105
|
-
it("parses --plugin-root <dir> for install", () => {
|
|
121
|
+
it("parses --plugin-root <dir> for install", async () => {
|
|
106
122
|
const result = parseArgs(["install", "pi", "--plugin-root", "/tmp/plugin"]);
|
|
107
123
|
assert.deepStrictEqual(result, { command: "install", harness: "pi", pluginRoot: "/tmp/plugin" });
|
|
108
124
|
});
|
|
109
125
|
|
|
110
|
-
it("
|
|
126
|
+
it("parses --no-provision for install", async () => {
|
|
127
|
+
const result = parseArgs(["install", "pi", "--no-provision"]);
|
|
128
|
+
assert.deepStrictEqual(result, { command: "install", harness: "pi", noProvision: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("combines multiple install options", async () => {
|
|
111
132
|
const result = parseArgs(["install", "pi", "--dry-run", "--output", "/tmp/out", "--plugin-root", "/tmp/plugin"]);
|
|
112
133
|
assert.deepStrictEqual(result, {
|
|
113
134
|
command: "install",
|
|
@@ -118,32 +139,32 @@ describe("parseArgs", () => {
|
|
|
118
139
|
});
|
|
119
140
|
});
|
|
120
141
|
|
|
121
|
-
it("reports a missing value for --output", () => {
|
|
142
|
+
it("reports a missing value for --output", async () => {
|
|
122
143
|
const result = parseArgs(["install", "pi", "--output"]);
|
|
123
144
|
assert.equal(result.command, "install");
|
|
124
145
|
assert.equal(result.harness, "pi");
|
|
125
146
|
assert.ok(result.optionError.includes("--output"));
|
|
126
147
|
});
|
|
127
148
|
|
|
128
|
-
it("reports an unknown option", () => {
|
|
149
|
+
it("reports an unknown option", async () => {
|
|
129
150
|
const result = parseArgs(["install", "pi", "--bogus"]);
|
|
130
151
|
assert.equal(result.command, "install");
|
|
131
152
|
assert.equal(result.harness, "pi");
|
|
132
153
|
assert.ok(result.optionError.includes("--bogus"));
|
|
133
154
|
});
|
|
134
155
|
|
|
135
|
-
it("keeps the old shape when no options are given", () => {
|
|
156
|
+
it("keeps the old shape when no options are given", async () => {
|
|
136
157
|
assert.deepStrictEqual(parseArgs(["install", "pi"]), { command: "install", harness: "pi" });
|
|
137
158
|
});
|
|
138
159
|
|
|
139
|
-
it("returns { command: 'unknown', name: 'foo' } for unknown command", () => {
|
|
160
|
+
it("returns { command: 'unknown', name: 'foo' } for unknown command", async () => {
|
|
140
161
|
const result = parseArgs(["foo"]);
|
|
141
162
|
assert.deepStrictEqual(result, { command: "unknown", name: "foo" });
|
|
142
163
|
});
|
|
143
164
|
});
|
|
144
165
|
|
|
145
166
|
describe("buildHelpText", () => {
|
|
146
|
-
it("returns a string containing usage info", () => {
|
|
167
|
+
it("returns a string containing usage info", async () => {
|
|
147
168
|
const text = buildHelpText();
|
|
148
169
|
assert.ok(typeof text === "string");
|
|
149
170
|
assert.ok(text.includes("agent-cortex"));
|
|
@@ -152,19 +173,19 @@ describe("buildHelpText", () => {
|
|
|
152
173
|
});
|
|
153
174
|
|
|
154
175
|
describe("validateHarness", () => {
|
|
155
|
-
it("returns ok for supported harnesses", () => {
|
|
176
|
+
it("returns ok for supported harnesses", async () => {
|
|
156
177
|
for (const h of ["copilot", "claude", "pi"]) {
|
|
157
178
|
assert.deepStrictEqual(validateHarness(h), { ok: true });
|
|
158
179
|
}
|
|
159
180
|
});
|
|
160
181
|
|
|
161
|
-
it("returns error when harness is null", () => {
|
|
182
|
+
it("returns error when harness is null", async () => {
|
|
162
183
|
const result = validateHarness(null);
|
|
163
184
|
assert.equal(result.ok, false);
|
|
164
185
|
assert.ok(result.error.includes("Missing harness"));
|
|
165
186
|
});
|
|
166
187
|
|
|
167
|
-
it("returns error for unknown harness", () => {
|
|
188
|
+
it("returns error for unknown harness", async () => {
|
|
168
189
|
const result = validateHarness("docker");
|
|
169
190
|
assert.equal(result.ok, false);
|
|
170
191
|
assert.ok(result.error.includes("Unknown harness"));
|
|
@@ -198,14 +219,27 @@ describe("CLI integration", () => {
|
|
|
198
219
|
});
|
|
199
220
|
|
|
200
221
|
it("exits 0 for install claude (into a temp output dir)", async () => {
|
|
201
|
-
const dir =
|
|
222
|
+
const dir = await mkdtemp(join(tmpdir(), "agent-cortex-cli-"));
|
|
202
223
|
const { exitCode, stdout } = await runCli(["install", "claude", "--output", dir]);
|
|
203
224
|
assert.equal(exitCode, 0);
|
|
204
225
|
assert.ok(stdout.includes("claude"));
|
|
205
226
|
});
|
|
206
227
|
|
|
207
|
-
it("exits 0 for install pi", async () => {
|
|
208
|
-
const
|
|
228
|
+
it("exits 0 for install pi (into a temp output dir)", async () => {
|
|
229
|
+
const outDir = await mkdtemp(join(tmpdir(), "cli-pi-default-"));
|
|
230
|
+
try {
|
|
231
|
+
const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
|
|
232
|
+
assert.equal(exitCode, 0);
|
|
233
|
+
assert.ok(stdout.includes("pi"));
|
|
234
|
+
} finally {
|
|
235
|
+
await rm(outDir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("exits 0 for install pi --no-provision", async () => {
|
|
240
|
+
// --no-provision keeps this hermetic: without it the CLI would install the
|
|
241
|
+
// third-party packages declared in the package manifest.
|
|
242
|
+
const { exitCode, stdout } = await runCli(["install", "pi", "--no-provision"]);
|
|
209
243
|
assert.equal(exitCode, 0);
|
|
210
244
|
assert.ok(stdout.includes("pi"));
|
|
211
245
|
});
|
|
@@ -235,16 +269,36 @@ describe("CLI integration", () => {
|
|
|
235
269
|
assert.ok(stdout.includes("dry-run"));
|
|
236
270
|
});
|
|
237
271
|
|
|
238
|
-
it("
|
|
239
|
-
|
|
272
|
+
it("install pi --dry-run lists the pi packages it would install", async () => {
|
|
273
|
+
// Fake HOME keeps the pi user scope hermetic: both manifest packages are
|
|
274
|
+
// missing, so the plan contains exactly the declared sources.
|
|
275
|
+
const fakeHome = await mkdtemp(join(tmpdir(), "cli-pi-home-"));
|
|
276
|
+
try {
|
|
277
|
+
const { exitCode, stdout } = await runCli(["install", "pi", "--dry-run"], { HOME: fakeHome });
|
|
278
|
+
assert.equal(exitCode, 0);
|
|
279
|
+
assert.ok(stdout.includes("Would install pi package npm:pi-questions"), "planned pi-questions shown");
|
|
280
|
+
assert.ok(
|
|
281
|
+
stdout.includes("Would install pi package npm:pi-web-access@0.10.7"),
|
|
282
|
+
"planned pi-web-access shown",
|
|
283
|
+
);
|
|
284
|
+
} finally {
|
|
285
|
+
await rm(fakeHome, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("exits 0 for install pi --output <tmp> and writes agent and config files", async () => {
|
|
290
|
+
const outDir = await mkdtemp(join(tmpdir(), "cli-pi-install-"));
|
|
240
291
|
try {
|
|
241
292
|
const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
|
|
242
293
|
assert.equal(exitCode, 0);
|
|
243
294
|
assert.ok(stdout.includes("ralph.agent.md"));
|
|
244
|
-
const ralph =
|
|
295
|
+
const ralph = await readFile(join(outDir, "agents", "ralph.agent.md"), "utf-8");
|
|
245
296
|
assert.ok(ralph.includes("agent-cortex:ralph"));
|
|
297
|
+
const settings = JSON.parse(await readFile(join(outDir, "settings.json"), "utf-8"));
|
|
298
|
+
assert.ok(Array.isArray(settings.packages));
|
|
299
|
+
assert.ok(await readFile(join(outDir, "keybindings.json"), "utf-8"));
|
|
246
300
|
} finally {
|
|
247
|
-
|
|
301
|
+
await rm(outDir, { recursive: true, force: true });
|
|
248
302
|
}
|
|
249
303
|
});
|
|
250
304
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
|
-
import {
|
|
8
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile, readdir, stat } from "node:fs/promises";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { join, dirname } from "node:path";
|
|
@@ -15,6 +15,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
15
15
|
const ROOT = join(__dirname, "..");
|
|
16
16
|
const CLI_PATH = join(ROOT, "bin", "agent-cortex.mjs");
|
|
17
17
|
|
|
18
|
+
/** True when a path exists (stat succeeds). */
|
|
19
|
+
async function pathExists(p) {
|
|
20
|
+
try {
|
|
21
|
+
await stat(p);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
function runCli(args) {
|
|
19
29
|
return new Promise((resolve) => {
|
|
20
30
|
execFile(process.execPath, [CLI_PATH, ...args], (error, stdout, stderr) => {
|
|
@@ -43,28 +53,28 @@ function tokenMap() {
|
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
function makeFixture() {
|
|
47
|
-
const root =
|
|
56
|
+
async function makeFixture() {
|
|
57
|
+
const root = await mkdtemp(join(tmpdir(), "copilot-installer-"));
|
|
48
58
|
return {
|
|
49
59
|
root,
|
|
50
60
|
output: join(root, "generated"),
|
|
51
|
-
cleanup: () =>
|
|
61
|
+
cleanup: async () => rm(root, { recursive: true, force: true }),
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
function writeFixture(fx, relPath, content) {
|
|
65
|
+
async function writeFixture(fx, relPath, content) {
|
|
56
66
|
const p = join(fx.root, relPath);
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
await mkdir(dirname(p), { recursive: true });
|
|
68
|
+
await writeFile(p, content);
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
/** Seed a minimal but realistic package tree (agents + token-map). */
|
|
62
|
-
function seedCopilotPackage(fx, opts = {}) {
|
|
72
|
+
async function seedCopilotPackage(fx, opts = {}) {
|
|
63
73
|
const { agents = {} } = opts;
|
|
64
|
-
writeFixture(fx, "token-map.json", JSON.stringify(tokenMap()));
|
|
74
|
+
await writeFixture(fx, "token-map.json", JSON.stringify(tokenMap()));
|
|
65
75
|
for (const [name, def] of Object.entries(agents)) {
|
|
66
|
-
writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
|
|
67
|
-
writeFixture(
|
|
76
|
+
await writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
|
|
77
|
+
await writeFixture(
|
|
68
78
|
fx,
|
|
69
79
|
`agents/${name}/copilot/frontmatter.json`,
|
|
70
80
|
JSON.stringify(
|
|
@@ -77,34 +87,34 @@ function seedCopilotPackage(fx, opts = {}) {
|
|
|
77
87
|
),
|
|
78
88
|
);
|
|
79
89
|
for (const [section, content] of Object.entries(def.sections ?? {})) {
|
|
80
|
-
writeFixture(fx, `agents/${name}/copilot/${section}.md`, content);
|
|
90
|
+
await writeFixture(fx, `agents/${name}/copilot/${section}.md`, content);
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/** Deterministic snapshot of a directory: relative paths + byte content. */
|
|
86
|
-
function snapshot(dir) {
|
|
96
|
+
async function snapshot(dir) {
|
|
87
97
|
const entries = [];
|
|
88
|
-
const walk = (p) => {
|
|
89
|
-
for (const e of
|
|
98
|
+
const walk = async (p) => {
|
|
99
|
+
for (const e of (await readdir(p, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
90
100
|
const full = join(p, e.name);
|
|
91
101
|
if (e.isDirectory()) {
|
|
92
102
|
entries.push([e.name, "dir"]);
|
|
93
|
-
walk(full);
|
|
94
|
-
} else entries.push([e.name, "file",
|
|
103
|
+
await walk(full);
|
|
104
|
+
} else entries.push([e.name, "file", await readFile(full, "utf-8")]);
|
|
95
105
|
}
|
|
96
106
|
};
|
|
97
|
-
walk(dir);
|
|
107
|
+
await walk(dir);
|
|
98
108
|
return entries;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
// ─── Agent composition ───────────────────────────────────────────────────────
|
|
102
112
|
|
|
103
113
|
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();
|
|
114
|
+
it("writes one flat <name>.agent.md per composable agent dir with copilot frontmatter and no leftover tokens", async () => {
|
|
115
|
+
const fx = await makeFixture();
|
|
106
116
|
try {
|
|
107
|
-
seedCopilotPackage(fx, {
|
|
117
|
+
await seedCopilotPackage(fx, {
|
|
108
118
|
agents: {
|
|
109
119
|
alpha: {
|
|
110
120
|
body: "# alpha\nUse {{TOOL:view}} to read.\n{{SECTION:polling}}\nRead {{PATH:agents_dir}}.",
|
|
@@ -119,11 +129,11 @@ describe("installCopilot — agent composition", () => {
|
|
|
119
129
|
},
|
|
120
130
|
});
|
|
121
131
|
|
|
122
|
-
const result = installCopilot({ root: fx.root, output: fx.output });
|
|
132
|
+
const result = await installCopilot({ root: fx.root, output: fx.output });
|
|
123
133
|
|
|
124
134
|
const file = join(fx.output, "alpha.agent.md");
|
|
125
|
-
assert.ok(
|
|
126
|
-
const content =
|
|
135
|
+
assert.ok(await pathExists(file), "flat agent file written");
|
|
136
|
+
const content = await readFile(file, "utf-8");
|
|
127
137
|
assert.ok(content.startsWith("---\n"), "yaml frontmatter");
|
|
128
138
|
assert.ok(content.includes("# GENERATED from agents/alpha/ by scripts/build-copilot-agents.mjs — DO NOT EDIT."));
|
|
129
139
|
// Committed flat-file field order: description, name, tools (YAML array), argument-hint.
|
|
@@ -138,64 +148,64 @@ describe("installCopilot — agent composition", () => {
|
|
|
138
148
|
assert.deepEqual(result.agents, ["alpha"]);
|
|
139
149
|
assert.equal(result.output, fx.output);
|
|
140
150
|
} finally {
|
|
141
|
-
fx.cleanup();
|
|
151
|
+
await fx.cleanup();
|
|
142
152
|
}
|
|
143
153
|
});
|
|
144
154
|
|
|
145
|
-
it("skips non-composable entries in agents/ and never clobbers composable sources", () => {
|
|
146
|
-
const fx = makeFixture();
|
|
155
|
+
it("skips non-composable entries in agents/ and never clobbers composable sources", async () => {
|
|
156
|
+
const fx = await makeFixture();
|
|
147
157
|
try {
|
|
148
|
-
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
149
|
-
writeFixture(fx, "agents/README.md", "# agents dir docs\n");
|
|
158
|
+
await seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
159
|
+
await writeFixture(fx, "agents/README.md", "# agents dir docs\n");
|
|
150
160
|
|
|
151
|
-
const result = installCopilot({ root: fx.root, output: fx.output });
|
|
161
|
+
const result = await installCopilot({ root: fx.root, output: fx.output });
|
|
152
162
|
|
|
153
|
-
assert.deepEqual(
|
|
163
|
+
assert.deepEqual((await readdir(fx.output)).sort(), ["alpha.agent.md"], "no README, no subdirectories written");
|
|
154
164
|
assert.deepEqual(result.agents, ["alpha"]);
|
|
155
165
|
// The composable source dir next to the generated file stays untouched.
|
|
156
|
-
assert.ok(
|
|
157
|
-
assert.ok(
|
|
166
|
+
assert.ok(await pathExists(join(fx.root, "agents", "alpha", "agent.md")), "composable source preserved");
|
|
167
|
+
assert.ok(await pathExists(join(fx.root, "agents", "alpha", "copilot", "frontmatter.json")), "harness dir preserved");
|
|
158
168
|
} finally {
|
|
159
|
-
fx.cleanup();
|
|
169
|
+
await fx.cleanup();
|
|
160
170
|
}
|
|
161
171
|
});
|
|
162
172
|
|
|
163
|
-
it("throws when a composable dir lacks copilot/frontmatter.json", () => {
|
|
164
|
-
const fx = makeFixture();
|
|
173
|
+
it("throws when a composable dir lacks copilot/frontmatter.json", async () => {
|
|
174
|
+
const fx = await makeFixture();
|
|
165
175
|
try {
|
|
166
|
-
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
167
|
-
|
|
168
|
-
assert.
|
|
169
|
-
|
|
176
|
+
await seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
177
|
+
await rm(join(fx.root, "agents", "alpha", "copilot"), { recursive: true, force: true });
|
|
178
|
+
await assert.rejects(
|
|
179
|
+
installCopilot({ root: fx.root, output: fx.output }),
|
|
170
180
|
/without copilot\/frontmatter\.json/,
|
|
171
181
|
);
|
|
172
182
|
} finally {
|
|
173
|
-
fx.cleanup();
|
|
183
|
+
await fx.cleanup();
|
|
174
184
|
}
|
|
175
185
|
});
|
|
176
186
|
|
|
177
|
-
it("defaults to the plugin-scanned agents/ dir of the package root (plugin.json \"agents\": \"agents/\")", () => {
|
|
178
|
-
const fx = makeFixture();
|
|
187
|
+
it("defaults to the plugin-scanned agents/ dir of the package root (plugin.json \"agents\": \"agents/\")", async () => {
|
|
188
|
+
const fx = await makeFixture();
|
|
179
189
|
try {
|
|
180
|
-
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
190
|
+
await seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
181
191
|
|
|
182
|
-
const result = installCopilot({ root: fx.root });
|
|
192
|
+
const result = await installCopilot({ root: fx.root });
|
|
183
193
|
|
|
184
194
|
assert.equal(result.output, join(fx.root, "agents"), "default output is the dir plugin.json scans");
|
|
185
|
-
assert.ok(
|
|
186
|
-
assert.ok(
|
|
195
|
+
assert.ok(await pathExists(join(fx.root, "agents", "alpha.agent.md")), "flat file lands next to the composable dir");
|
|
196
|
+
assert.ok(await pathExists(join(fx.root, "agents", "alpha", "agent.md")), "composable source untouched");
|
|
187
197
|
} finally {
|
|
188
|
-
fx.cleanup();
|
|
198
|
+
await fx.cleanup();
|
|
189
199
|
}
|
|
190
200
|
});
|
|
191
201
|
|
|
192
|
-
it("throws on an unknown tool token", () => {
|
|
193
|
-
const fx = makeFixture();
|
|
202
|
+
it("throws on an unknown tool token", async () => {
|
|
203
|
+
const fx = await makeFixture();
|
|
194
204
|
try {
|
|
195
|
-
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
|
|
196
|
-
assert.
|
|
205
|
+
await seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
|
|
206
|
+
await assert.rejects(installCopilot({ root: fx.root, output: fx.output }), /unknown tool token {{TOOL:nope}}/);
|
|
197
207
|
} finally {
|
|
198
|
-
fx.cleanup();
|
|
208
|
+
await fx.cleanup();
|
|
199
209
|
}
|
|
200
210
|
});
|
|
201
211
|
});
|
|
@@ -203,37 +213,37 @@ describe("installCopilot — agent composition", () => {
|
|
|
203
213
|
// ─── Output / dry-run ────────────────────────────────────────────────────────
|
|
204
214
|
|
|
205
215
|
describe("installCopilot — output & dry-run", () => {
|
|
206
|
-
it("dry-run reports the plan without writing anything", () => {
|
|
207
|
-
const fx = makeFixture();
|
|
216
|
+
it("dry-run reports the plan without writing anything", async () => {
|
|
217
|
+
const fx = await makeFixture();
|
|
208
218
|
try {
|
|
209
|
-
seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
219
|
+
await seedCopilotPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
210
220
|
|
|
211
|
-
const result = installCopilot({ root: fx.root, output: fx.output, dryRun: true });
|
|
221
|
+
const result = await installCopilot({ root: fx.root, output: fx.output, dryRun: true });
|
|
212
222
|
|
|
213
223
|
assert.equal(result.dryRun, true);
|
|
214
|
-
assert.equal(
|
|
224
|
+
assert.equal(await pathExists(fx.output), false, "no output dir created in dry-run");
|
|
215
225
|
assert.deepEqual(result.agents, ["alpha"], "planned files still reported");
|
|
216
226
|
} finally {
|
|
217
|
-
fx.cleanup();
|
|
227
|
+
await fx.cleanup();
|
|
218
228
|
}
|
|
219
229
|
});
|
|
220
230
|
|
|
221
|
-
it("is deterministic across repeated installs into the same output", () => {
|
|
222
|
-
const fx = makeFixture();
|
|
231
|
+
it("is deterministic across repeated installs into the same output", async () => {
|
|
232
|
+
const fx = await makeFixture();
|
|
223
233
|
try {
|
|
224
|
-
seedCopilotPackage(fx, {
|
|
234
|
+
await seedCopilotPackage(fx, {
|
|
225
235
|
agents: {
|
|
226
236
|
alpha: { body: "# alpha\n{{SECTION:polling}}", sections: { polling: "Poll with {{TOOL:task}}." } },
|
|
227
237
|
beta: { body: "# beta" },
|
|
228
238
|
},
|
|
229
239
|
});
|
|
230
240
|
|
|
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);
|
|
241
|
+
await installCopilot({ root: fx.root, output: fx.output });
|
|
242
|
+
const first = await snapshot(fx.output);
|
|
243
|
+
await installCopilot({ root: fx.root, output: fx.output });
|
|
244
|
+
assert.deepEqual(await snapshot(fx.output), first);
|
|
235
245
|
} finally {
|
|
236
|
-
fx.cleanup();
|
|
246
|
+
await fx.cleanup();
|
|
237
247
|
}
|
|
238
248
|
});
|
|
239
249
|
});
|
|
@@ -241,19 +251,19 @@ describe("installCopilot — output & dry-run", () => {
|
|
|
241
251
|
// ─── Integration against the real repo ───────────────────────────────────────
|
|
242
252
|
|
|
243
253
|
describe("installCopilot — real repo integration", () => {
|
|
244
|
-
it("regenerates byte-identical copies of the committed agents/*.agent.md files", () => {
|
|
245
|
-
const fx = makeFixture();
|
|
254
|
+
it("regenerates byte-identical copies of the committed agents/*.agent.md files", async () => {
|
|
255
|
+
const fx = await makeFixture();
|
|
246
256
|
try {
|
|
247
|
-
const result = installCopilot({ root: ROOT, output: fx.output });
|
|
257
|
+
const result = await installCopilot({ root: ROOT, output: fx.output });
|
|
248
258
|
|
|
249
259
|
assert.deepEqual(result.agents.sort(), ["plan", "ralph", "ralph-plan", "strategy"]);
|
|
250
260
|
for (const name of result.agents) {
|
|
251
|
-
const generated =
|
|
252
|
-
const committed =
|
|
261
|
+
const generated = await readFile(join(fx.output, `${name}.agent.md`), "utf-8");
|
|
262
|
+
const committed = await readFile(join(ROOT, "agents", `${name}.agent.md`), "utf-8");
|
|
253
263
|
assert.equal(generated, committed, `${name}.agent.md must match the committed flat file (CI drift check)`);
|
|
254
264
|
}
|
|
255
265
|
} finally {
|
|
256
|
-
fx.cleanup();
|
|
266
|
+
await fx.cleanup();
|
|
257
267
|
}
|
|
258
268
|
});
|
|
259
269
|
});
|
|
@@ -262,37 +272,37 @@ describe("installCopilot — real repo integration", () => {
|
|
|
262
272
|
|
|
263
273
|
describe("CLI integration", () => {
|
|
264
274
|
it("exits 0 for install copilot --output <tmp> and writes the flat files", async () => {
|
|
265
|
-
const fx = makeFixture();
|
|
275
|
+
const fx = await makeFixture();
|
|
266
276
|
try {
|
|
267
277
|
const { exitCode, stdout } = await runCli(["install", "copilot", "--output", fx.output]);
|
|
268
278
|
assert.equal(exitCode, 0);
|
|
269
279
|
assert.ok(stdout.includes("copilot"), "prints the harness name");
|
|
270
280
|
assert.ok(stdout.includes("Generated"), "prints the generation summary");
|
|
271
|
-
assert.ok(
|
|
272
|
-
assert.ok(
|
|
281
|
+
assert.ok(await pathExists(join(fx.output, "ralph.agent.md")), "agent files written");
|
|
282
|
+
assert.ok(await pathExists(join(fx.output, "plan.agent.md")));
|
|
273
283
|
} finally {
|
|
274
|
-
fx.cleanup();
|
|
284
|
+
await fx.cleanup();
|
|
275
285
|
}
|
|
276
286
|
});
|
|
277
287
|
|
|
278
288
|
it("exits 0 for install copilot --dry-run and writes nothing", async () => {
|
|
279
|
-
const fx = makeFixture();
|
|
289
|
+
const fx = await makeFixture();
|
|
280
290
|
try {
|
|
281
291
|
const { exitCode, stdout } = await runCli(["install", "copilot", "--output", fx.output, "--dry-run"]);
|
|
282
292
|
assert.equal(exitCode, 0);
|
|
283
293
|
assert.ok(/dry-run/i.test(stdout));
|
|
284
|
-
assert.equal(
|
|
294
|
+
assert.equal(await pathExists(fx.output), false, "no files written in dry-run");
|
|
285
295
|
} finally {
|
|
286
|
-
fx.cleanup();
|
|
296
|
+
await fx.cleanup();
|
|
287
297
|
}
|
|
288
298
|
});
|
|
289
299
|
|
|
290
300
|
it("exits 0 for install copilot (default output) without touching committed files", async () => {
|
|
291
|
-
const before =
|
|
301
|
+
const before = await readFile(join(ROOT, "agents", "ralph.agent.md"), "utf-8");
|
|
292
302
|
const { exitCode } = await runCli(["install", "copilot"]);
|
|
293
303
|
assert.equal(exitCode, 0);
|
|
294
304
|
// Default output regenerates the shared agents/ dir in place — output must
|
|
295
305
|
// stay byte-identical to what is committed.
|
|
296
|
-
assert.equal(
|
|
306
|
+
assert.equal(await readFile(join(ROOT, "agents", "ralph.agent.md"), "utf-8"), before);
|
|
297
307
|
});
|
|
298
308
|
});
|