@jaybeeuu/agent-cortex 1.38.1 → 1.40.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.
Files changed (88) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +1 -1
  2. package/.github/workflows/ci.yml +47 -14
  3. package/AGENTS.md +19 -3
  4. package/CHANGELOG.md +42 -0
  5. package/README.md +118 -63
  6. package/agents/README.md +13 -10
  7. package/agents/ralph-plan/agent.md +1 -1
  8. package/agents/ralph-plan.agent.md +1 -1
  9. package/bin/agent-cortex.mjs +29 -8
  10. package/bin/installers/claude.mjs +569 -105
  11. package/bin/installers/copilot.mjs +22 -12
  12. package/bin/installers/pi.mjs +37 -34
  13. package/docs/claude-hooks.md +3 -3
  14. package/docs/ideas/extension-manifests.md +75 -0
  15. package/docs/ideas/record-ideas-as-beads.md +115 -0
  16. package/docs/ideas/subagent-visibility.md +72 -0
  17. package/docs/ideas/task-session-context.md +89 -0
  18. package/docs/research/skill-design-research.md +4 -4
  19. package/docs/research/skill-improvements-analysis.md +1 -1
  20. package/docs/skills/skill-anatomy.md +38 -2
  21. package/docs/technical-direction/migration-execution-labels.md +132 -0
  22. package/extensions/agent-modes/README.md +3 -2
  23. package/extensions/agent-modes/discover.test.ts +30 -7
  24. package/extensions/agent-modes/discover.ts +1 -1
  25. package/extensions/skill-stats/index.test.ts +3 -2
  26. package/lib/cli.mjs +24 -6
  27. package/package.json +4 -4
  28. package/plugin.json +1 -1
  29. package/pnpm-workspace.yaml +1 -1
  30. package/scripts/build-copilot-agents.mjs +1 -1
  31. package/scripts/check-no-sync.mjs +49 -0
  32. package/scripts/lib/compose-agent.mjs +32 -14
  33. package/skills/engineering/design-an-interface/SKILL.md +78 -58
  34. package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
  35. package/skills/engineering/grill-with-docs/SKILL.md +66 -63
  36. package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
  37. package/skills/engineering/technical-direction/FORMAT.md +38 -0
  38. package/skills/engineering/technical-direction/SKILL.md +70 -77
  39. package/skills/planning/classify-bead/SKILL.md +59 -35
  40. package/skills/planning/create-task/REFERENCE.md +40 -0
  41. package/skills/planning/create-task/SKILL.md +70 -66
  42. package/skills/planning/create-task/scripts/create-chores.ts +6 -1
  43. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
  44. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
  45. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
  46. package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
  47. package/skills/planning/prd-to-tasks/SKILL.md +86 -170
  48. package/skills/planning/record-idea/FORMAT.md +44 -0
  49. package/skills/planning/record-idea/SKILL.md +70 -53
  50. package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
  51. package/skills/planning/request-refactor-plan/SKILL.md +57 -46
  52. package/skills/planning/write-a-prd/FORMAT.md +54 -0
  53. package/skills/planning/write-a-prd/SKILL.md +49 -48
  54. package/skills/planning/write-a-ticket/SKILL.md +68 -53
  55. package/skills/productivity/handoff/SKILL.md +12 -0
  56. package/skills/productivity/init-beads/SKILL.md +48 -40
  57. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
  58. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
  59. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
  60. package/skills/productivity/write-a-skill/SKILL.md +2 -1
  61. package/skills/review/maintain-agent-docs/SKILL.md +71 -51
  62. package/skills/workflow/hitl-collab/SKILL.md +86 -17
  63. package/skills/workflow/ralph/REFERENCE.md +22 -17
  64. package/skills/workflow/ralph/SKILL.md +88 -74
  65. package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
  66. package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
  67. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
  68. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
  69. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
  70. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
  71. package/test/claude-installer.test.mjs +308 -0
  72. package/test/cli.test.mjs +40 -30
  73. package/test/copilot-installer.test.mjs +92 -82
  74. package/test/helpers/fake-claude.mjs +152 -0
  75. package/test/installer.test.mjs +309 -57
  76. package/test/pi-installer.test.mjs +115 -99
  77. package/token-map.README.md +21 -10
  78. package/token-map.json +7 -6
  79. package/.claude-plugin/marketplace.json +0 -16
  80. package/claude/.claude-plugin/plugin.json +0 -11
  81. package/claude/agents/plan.md +0 -58
  82. package/claude/agents/ralph-plan.md +0 -121
  83. package/claude/agents/ralph.md +0 -193
  84. package/claude/agents/strategy.md +0 -111
  85. package/claude/hooks/scripts/notify.mjs +0 -54
  86. package/claude/hooks.json +0 -38
  87. /package/{claude → claude-extras}/.mcp.json +0 -0
  88. /package/{claude → claude-extras}/scripts/statusline-command.sh +0 -0
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../tsx/dist/cli.mjs" "$@"
21
+ fi
@@ -0,0 +1,308 @@
1
+ // Tests for `agent-cortex install claude` runtime registration: after
2
+ // materialising the plugin into the home install root, the plain install
3
+ // registers the plugin with Claude Code by driving the `claude plugin` CLI.
4
+ // Registration is
5
+ // idempotent by STATE, not by exit code: `marketplace list --json` decides
6
+ // add-vs-update and `plugin list --json` decides install-vs-update against the
7
+ // materialised version. The real CLI is a genuine external system (and its
8
+ // home config must never be touched), so these tests drive the stateful fake
9
+ // from test/helpers/fake-claude.mjs; fixture manifests (makeMarketplaceRoot)
10
+ // stand in for the home install root's generated marketplace manifest.
11
+
12
+ import { describe, it } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { execFile } from "node:child_process";
15
+ import { mkdtemp, mkdir, writeFile, rm, readFile, stat } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { fileURLToPath } from "node:url";
18
+ import { dirname, join } from "node:path";
19
+ import { registerClaude } from "../bin/installers/claude.mjs";
20
+ import { makeFakeClaude, registrationActions } from "./helpers/fake-claude.mjs";
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const ROOT = join(__dirname, "..");
24
+ const CLI_PATH = join(ROOT, "bin", "agent-cortex.mjs");
25
+
26
+ function runCli(args, env = {}) {
27
+ return new Promise((resolve) => {
28
+ execFile(
29
+ process.execPath,
30
+ [CLI_PATH, ...args],
31
+ { env: { ...process.env, ...env } },
32
+ (error, stdout, stderr) => {
33
+ resolve({ exitCode: error ? error.code ?? 1 : 0, stdout, stderr });
34
+ },
35
+ );
36
+ });
37
+ }
38
+
39
+ /** True when a path exists (stat succeeds). */
40
+ async function pathExists(p) {
41
+ try {
42
+ await stat(p);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function makeTmp() {
50
+ return mkdtemp(join(tmpdir(), "agent-cortex-claude-installer-"));
51
+ }
52
+
53
+ async function callLines(log) {
54
+ return (await readFile(log, "utf-8")).trim().split("\n").filter(Boolean);
55
+ }
56
+
57
+ /** Registration actions logged at/after line `since` (0 = whole log). */
58
+ async function actionsSince(log, since = 0) {
59
+ return registrationActions((await callLines(log)).slice(since));
60
+ }
61
+
62
+ /** A minimal marketplace tree (as seen at the real install root): the
63
+ * marketplace manifest exposing ./claude plus the materialised plugin.json the
64
+ * fake reads for the installed version. */
65
+ async function makeMarketplaceRoot({ name = "jaybeeuu", plugin = "agent-cortex", version = "1.0.0" } = {}) {
66
+ const root = await mkdtemp(join(tmpdir(), "agent-cortex-marketplace-"));
67
+ await mkdir(join(root, ".claude-plugin"), { recursive: true });
68
+ await writeFile(
69
+ join(root, ".claude-plugin", "marketplace.json"),
70
+ JSON.stringify({
71
+ name,
72
+ owner: { name: "tester" },
73
+ plugins: [{ name: plugin, source: "./claude", description: "test plugin" }],
74
+ }),
75
+ );
76
+ await mkdir(join(root, "claude", ".claude-plugin"), { recursive: true });
77
+ await writeFile(
78
+ join(root, "claude", ".claude-plugin", "plugin.json"),
79
+ JSON.stringify({ name: plugin, version, description: "test plugin" }),
80
+ );
81
+ return root;
82
+ }
83
+
84
+ // ─── registerClaude ──────────────────────────────────────────────────────────
85
+
86
+ describe("registerClaude", () => {
87
+ it("drives marketplace add → install on a fresh registration, skipping the market update/plugin update no-ops", async () => {
88
+ const tmp = await makeTmp();
89
+ const root = await makeMarketplaceRoot();
90
+ const fake = await makeFakeClaude(tmp);
91
+ try {
92
+ const result = await registerClaude({ root, claudeBin: fake.bin });
93
+ assert.equal(result.marketplace, "jaybeeuu");
94
+ assert.equal(result.plugin, "agent-cortex");
95
+ assert.equal(result.dryRun, false);
96
+ assert.equal(result.registered, true);
97
+ assert.deepEqual(result.commands, [
98
+ ["plugin", "marketplace", "add", root],
99
+ ["plugin", "install", "agent-cortex@jaybeeuu", "-y"],
100
+ ]);
101
+ // The availability probe (`plugin --help`) and the state queries
102
+ // (`marketplace list --json`, `plugin list --json`) are not actions.
103
+ assert.deepEqual(await actionsSince(fake.log), [
104
+ `plugin marketplace add ${root}`,
105
+ "plugin install agent-cortex@jaybeeuu -y",
106
+ ]);
107
+ } finally {
108
+ await rm(tmp, { recursive: true, force: true });
109
+ await rm(root, { recursive: true, force: true });
110
+ }
111
+ });
112
+
113
+ it("derives marketplace and plugin names from the manifest", async () => {
114
+ const tmp = await makeTmp();
115
+ const root = await makeMarketplaceRoot({ name: "scratchmkt", plugin: "scratchplug" });
116
+ const fake = await makeFakeClaude(tmp);
117
+ try {
118
+ const result = await registerClaude({ root, claudeBin: fake.bin });
119
+ assert.equal(result.marketplace, "scratchmkt");
120
+ assert.equal(result.plugin, "scratchplug");
121
+ const actions = await actionsSince(fake.log);
122
+ assert.ok(actions.some((c) => c === "plugin install scratchplug@scratchmkt -y"));
123
+ // Fresh registration has no update commands.
124
+ assert.ok(!actions.some((c) => c.startsWith("plugin update")));
125
+ } finally {
126
+ await rm(tmp, { recursive: true, force: true });
127
+ await rm(root, { recursive: true, force: true });
128
+ }
129
+ });
130
+
131
+ it("re-running at the same materialised version is the update path: marketplace update only, no re-add or double-install", async () => {
132
+ const tmp = await makeTmp();
133
+ const root = await makeMarketplaceRoot();
134
+ const fake = await makeFakeClaude(tmp);
135
+ try {
136
+ await registerClaude({ root, claudeBin: fake.bin });
137
+ assert.deepEqual(await actionsSince(fake.log), [
138
+ `plugin marketplace add ${root}`,
139
+ "plugin install agent-cortex@jaybeeuu -y",
140
+ ]);
141
+ const run1End = (await callLines(fake.log)).length;
142
+
143
+ const second = await registerClaude({ root, claudeBin: fake.bin });
144
+ assert.equal(second.registered, true);
145
+ // Second run: the marketplace is present so only the update path runs;
146
+ // the plugin is already at the materialised version so nothing installs.
147
+ assert.deepEqual(await actionsSince(fake.log, run1End), ["plugin marketplace update jaybeeuu"]);
148
+ assert.ok(second.skipped.some((s) => s.includes("already installed")));
149
+
150
+ // Across both runs the marketplace was added exactly once and the
151
+ // plugin installed exactly once.
152
+ const all = await actionsSince(fake.log);
153
+ assert.equal(all.filter((c) => c.startsWith("plugin marketplace add")).length, 1);
154
+ assert.equal(all.filter((c) => c === "plugin install agent-cortex@jaybeeuu -y").length, 1);
155
+ } finally {
156
+ await rm(tmp, { recursive: true, force: true });
157
+ await rm(root, { recursive: true, force: true });
158
+ }
159
+ });
160
+
161
+ it("runs plugin update when a newer version is materialised, refreshing the installed version", async () => {
162
+ const tmp = await makeTmp();
163
+ const root = await makeMarketplaceRoot({ version: "1.0.0" });
164
+ const fake = await makeFakeClaude(tmp);
165
+ try {
166
+ await registerClaude({ root, claudeBin: fake.bin });
167
+ assert.deepEqual(await actionsSince(fake.log), [
168
+ `plugin marketplace add ${root}`,
169
+ "plugin install agent-cortex@jaybeeuu -y",
170
+ ]);
171
+ const run1End = (await callLines(fake.log)).length;
172
+
173
+ // A new version is materialised into the source the marketplace exposes.
174
+ await writeFile(
175
+ join(root, "claude", ".claude-plugin", "plugin.json"),
176
+ JSON.stringify({ name: "agent-cortex", version: "2.0.0", description: "test plugin" }),
177
+ );
178
+
179
+ const updated = await registerClaude({ root, claudeBin: fake.bin });
180
+ assert.deepEqual(await actionsSince(fake.log, run1End), [
181
+ "plugin marketplace update jaybeeuu",
182
+ "plugin update agent-cortex -y",
183
+ ]);
184
+ assert.ok(updated.skipped.some((s) => s.includes("already installed")));
185
+ const state = JSON.parse(await readFile(fake.state, "utf-8"));
186
+ assert.equal(state.installed[0].version, "2.0.0", "plugin update refreshed the installed version");
187
+
188
+ // At the new version another run is back to update-path-only.
189
+ const run2End = (await callLines(fake.log)).length;
190
+ await registerClaude({ root, claudeBin: fake.bin });
191
+ assert.deepEqual(await actionsSince(fake.log, run2End), ["plugin marketplace update jaybeeuu"]);
192
+ } finally {
193
+ await rm(tmp, { recursive: true, force: true });
194
+ await rm(root, { recursive: true, force: true });
195
+ }
196
+ });
197
+
198
+ it("--dry-run plans the commands without spawning claude at all", async () => {
199
+ const tmp = await makeTmp();
200
+ const root = await makeMarketplaceRoot();
201
+ const fake = await makeFakeClaude(tmp);
202
+ try {
203
+ const result = await registerClaude({ root, claudeBin: fake.bin, dryRun: true });
204
+ assert.equal(result.dryRun, true);
205
+ assert.equal(result.registered, true);
206
+ // Dry-run cannot know live state without spawning — no commands decided.
207
+ assert.deepEqual(result.commands, []);
208
+ // No registration spawns happened at all in dry-run mode.
209
+ assert.equal(await pathExists(fake.log), false);
210
+ } finally {
211
+ await rm(tmp, { recursive: true, force: true });
212
+ await rm(root, { recursive: true, force: true });
213
+ }
214
+ });
215
+
216
+ it("defaults the claude binary to `claude` from PATH (dry-run only — no spawn)", async () => {
217
+ const root = await makeMarketplaceRoot();
218
+ try {
219
+ const result = await registerClaude({ root, dryRun: true });
220
+ assert.equal(result.marketplace, "jaybeeuu");
221
+ assert.equal(result.plugin, "agent-cortex");
222
+ assert.equal(result.dryRun, true);
223
+ } finally {
224
+ await rm(root, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ it("warns with manual commands when the claude binary lacks the plugin subcommand; --require-register fails the run", async () => {
229
+ const tmp = await makeTmp();
230
+ const root = await makeMarketplaceRoot();
231
+ const fake = await makeFakeClaude(tmp, { failWhen: "plugin --help" });
232
+ try {
233
+ const warnings = [];
234
+ const result = await registerClaude({ root, claudeBin: fake.bin, warn: (m) => warnings.push(m) });
235
+ assert.equal(result.registered, false);
236
+ const manual = warnings.join("\n");
237
+ assert.match(manual, /NOT registered/);
238
+ assert.match(manual, new RegExp(`claude plugin marketplace add ${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
239
+ assert.match(manual, /claude plugin install agent-cortex@jaybeeuu -y/);
240
+
241
+ await assert.rejects(
242
+ registerClaude({ root, claudeBin: fake.bin, requireRegister: true }),
243
+ /require-register/,
244
+ );
245
+ } finally {
246
+ await rm(tmp, { recursive: true, force: true });
247
+ await rm(root, { recursive: true, force: true });
248
+ }
249
+ });
250
+
251
+ it("fails, naming the failing command, when a claude plugin command errors", async () => {
252
+ const tmp = await makeTmp();
253
+ const root = await makeMarketplaceRoot();
254
+ const fake = await makeFakeClaude(tmp, { failWhen: "plugin install" });
255
+ try {
256
+ await assert.rejects(
257
+ registerClaude({ root, claudeBin: fake.bin }),
258
+ /claude plugin install agent-cortex@jaybeeuu -y/,
259
+ );
260
+ } finally {
261
+ await rm(tmp, { recursive: true, force: true });
262
+ await rm(root, { recursive: true, force: true });
263
+ }
264
+ });
265
+
266
+ it("fails when the checkout has no marketplace manifest", async () => {
267
+ const tmp = await makeTmp();
268
+ const root = await mkdtemp(join(tmpdir(), "agent-cortex-no-market-"));
269
+ const fake = await makeFakeClaude(tmp);
270
+ try {
271
+ await assert.rejects(
272
+ registerClaude({ root, claudeBin: fake.bin }),
273
+ /marketplace\.json/,
274
+ );
275
+ // The failed manifest read happens before any claude invocation.
276
+ assert.equal(await pathExists(fake.log), false);
277
+ } finally {
278
+ await rm(tmp, { recursive: true, force: true });
279
+ await rm(root, { recursive: true, force: true });
280
+ }
281
+ });
282
+ });
283
+
284
+ // ─── CLI wiring ───────────────────────────────────────────────────────────────
285
+
286
+ describe("install claude CLI: registration", () => {
287
+ it("--dry-run without --output prints the registration plan and writes nothing", async () => {
288
+ const { exitCode, stdout } = await runCli(["install", "claude", "--dry-run"]);
289
+ assert.equal(exitCode, 0);
290
+ assert.ok(stdout.includes("would run: claude plugin marketplace add"));
291
+ assert.ok(stdout.includes("claude plugin install agent-cortex@jaybeeuu"));
292
+ assert.ok(stdout.includes("plugin marketplace update"));
293
+ assert.ok(stdout.includes("plugin update agent-cortex"));
294
+ assert.ok(/dry-run/i.test(stdout));
295
+ });
296
+
297
+ it("--output <dir> generates only — no registration plan is printed", async () => {
298
+ const out = await mkdtemp(join(tmpdir(), "agent-cortex-cli-out-"));
299
+ try {
300
+ const { exitCode, stdout } = await runCli(["install", "claude", "--output", out]);
301
+ assert.equal(exitCode, 0);
302
+ assert.doesNotMatch(stdout, /claude plugin/);
303
+ assert.ok(stdout.includes("Generated"));
304
+ } finally {
305
+ await rm(out, { recursive: true, force: true });
306
+ }
307
+ });
308
+ });
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 { mkdtempSync, readFileSync, rmSync } from "node:fs";
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,6 +10,16 @@ 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
+ /** 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
+
13
23
  function runCli(args) {
14
24
  return new Promise((resolve) => {
15
25
  execFile(process.execPath, [CLI_PATH, ...args], (error, stdout, stderr) => {
@@ -23,91 +33,91 @@ function runCli(args) {
23
33
  }
24
34
 
25
35
  describe("parseArgs", () => {
26
- it("returns { command: 'help' } when no arguments given", () => {
36
+ it("returns { command: 'help' } when no arguments given", async () => {
27
37
  const result = parseArgs([]);
28
38
  assert.deepStrictEqual(result, { command: "help" });
29
39
  });
30
40
 
31
- it("returns { command: 'help' } for --help flag", () => {
41
+ it("returns { command: 'help' } for --help flag", async () => {
32
42
  const result = parseArgs(["--help"]);
33
43
  assert.deepStrictEqual(result, { command: "help" });
34
44
  });
35
45
 
36
- it("returns { command: 'help' } for -h flag", () => {
46
+ it("returns { command: 'help' } for -h flag", async () => {
37
47
  const result = parseArgs(["-h"]);
38
48
  assert.deepStrictEqual(result, { command: "help" });
39
49
  });
40
50
 
41
- it("returns { command: 'install', harness: 'copilot' } for install copilot", () => {
51
+ it("returns { command: 'install', harness: 'copilot' } for install copilot", async () => {
42
52
  const result = parseArgs(["install", "copilot"]);
43
53
  assert.deepStrictEqual(result, { command: "install", harness: "copilot" });
44
54
  });
45
55
 
46
- it("returns { command: 'install', harness: 'claude' } for install claude", () => {
56
+ it("returns { command: 'install', harness: 'claude' } for install claude", async () => {
47
57
  const result = parseArgs(["install", "claude"]);
48
58
  assert.deepStrictEqual(result, { command: "install", harness: "claude" });
49
59
  });
50
60
 
51
- it("returns { command: 'install', harness: 'pi' } for install pi", () => {
61
+ it("returns { command: 'install', harness: 'pi' } for install pi", async () => {
52
62
  const result = parseArgs(["install", "pi"]);
53
63
  assert.deepStrictEqual(result, { command: "install", harness: "pi" });
54
64
  });
55
65
 
56
- it("returns { command: 'install', harness: null } when install has no harness", () => {
66
+ it("returns { command: 'install', harness: null } when install has no harness", async () => {
57
67
  const result = parseArgs(["install"]);
58
68
  assert.deepStrictEqual(result, { command: "install", harness: null });
59
69
  });
60
70
 
61
- it("returns output for install claude --output <dir>", () => {
71
+ it("returns output for install claude --output <dir>", async () => {
62
72
  const result = parseArgs(["install", "claude", "--output", "/tmp/x"]);
63
73
  assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
64
74
  });
65
75
 
66
- it("returns output for install claude --output=<dir>", () => {
76
+ it("returns output for install claude --output=<dir>", async () => {
67
77
  const result = parseArgs(["install", "claude", "--output=/tmp/x"]);
68
78
  assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
69
79
  });
70
80
 
71
- it("returns dryRun for install claude --dry-run", () => {
81
+ it("returns dryRun for install claude --dry-run", async () => {
72
82
  const result = parseArgs(["install", "claude", "--dry-run"]);
73
83
  assert.deepStrictEqual(result, { command: "install", harness: "claude", dryRun: true });
74
84
  });
75
85
 
76
- it("combines --dry-run with --output", () => {
86
+ it("combines --dry-run with --output", async () => {
77
87
  const result = parseArgs(["install", "claude", "--output", "/tmp/x", "--dry-run"]);
78
88
  assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x", dryRun: true });
79
89
  });
80
90
 
81
- it("reports an unknown option for claude", () => {
91
+ it("reports an unknown option for claude", async () => {
82
92
  const result = parseArgs(["install", "claude", "--bogus"]);
83
93
  assert.equal(result.command, "install");
84
94
  assert.equal(result.harness, "claude");
85
95
  assert.ok(result.optionError.includes("--bogus"));
86
96
  });
87
97
 
88
- it("reports a missing value for --output for claude", () => {
98
+ it("reports a missing value for --output for claude", async () => {
89
99
  const result = parseArgs(["install", "claude", "--output"]);
90
100
  assert.equal(result.command, "install");
91
101
  assert.equal(result.harness, "claude");
92
102
  assert.ok(result.optionError.includes("--output"));
93
103
  });
94
104
 
95
- it("parses --dry-run for install", () => {
105
+ it("parses --dry-run for install", async () => {
96
106
  const result = parseArgs(["install", "pi", "--dry-run"]);
97
107
  assert.deepStrictEqual(result, { command: "install", harness: "pi", dryRun: true });
98
108
  });
99
109
 
100
- it("parses --output <dir> for install", () => {
110
+ it("parses --output <dir> for install", async () => {
101
111
  const result = parseArgs(["install", "pi", "--output", "/tmp/out"]);
102
112
  assert.deepStrictEqual(result, { command: "install", harness: "pi", output: "/tmp/out" });
103
113
  });
104
114
 
105
- it("parses --plugin-root <dir> for install", () => {
115
+ it("parses --plugin-root <dir> for install", async () => {
106
116
  const result = parseArgs(["install", "pi", "--plugin-root", "/tmp/plugin"]);
107
117
  assert.deepStrictEqual(result, { command: "install", harness: "pi", pluginRoot: "/tmp/plugin" });
108
118
  });
109
119
 
110
- it("combines multiple install options", () => {
120
+ it("combines multiple install options", async () => {
111
121
  const result = parseArgs(["install", "pi", "--dry-run", "--output", "/tmp/out", "--plugin-root", "/tmp/plugin"]);
112
122
  assert.deepStrictEqual(result, {
113
123
  command: "install",
@@ -118,32 +128,32 @@ describe("parseArgs", () => {
118
128
  });
119
129
  });
120
130
 
121
- it("reports a missing value for --output", () => {
131
+ it("reports a missing value for --output", async () => {
122
132
  const result = parseArgs(["install", "pi", "--output"]);
123
133
  assert.equal(result.command, "install");
124
134
  assert.equal(result.harness, "pi");
125
135
  assert.ok(result.optionError.includes("--output"));
126
136
  });
127
137
 
128
- it("reports an unknown option", () => {
138
+ it("reports an unknown option", async () => {
129
139
  const result = parseArgs(["install", "pi", "--bogus"]);
130
140
  assert.equal(result.command, "install");
131
141
  assert.equal(result.harness, "pi");
132
142
  assert.ok(result.optionError.includes("--bogus"));
133
143
  });
134
144
 
135
- it("keeps the old shape when no options are given", () => {
145
+ it("keeps the old shape when no options are given", async () => {
136
146
  assert.deepStrictEqual(parseArgs(["install", "pi"]), { command: "install", harness: "pi" });
137
147
  });
138
148
 
139
- it("returns { command: 'unknown', name: 'foo' } for unknown command", () => {
149
+ it("returns { command: 'unknown', name: 'foo' } for unknown command", async () => {
140
150
  const result = parseArgs(["foo"]);
141
151
  assert.deepStrictEqual(result, { command: "unknown", name: "foo" });
142
152
  });
143
153
  });
144
154
 
145
155
  describe("buildHelpText", () => {
146
- it("returns a string containing usage info", () => {
156
+ it("returns a string containing usage info", async () => {
147
157
  const text = buildHelpText();
148
158
  assert.ok(typeof text === "string");
149
159
  assert.ok(text.includes("agent-cortex"));
@@ -152,19 +162,19 @@ describe("buildHelpText", () => {
152
162
  });
153
163
 
154
164
  describe("validateHarness", () => {
155
- it("returns ok for supported harnesses", () => {
165
+ it("returns ok for supported harnesses", async () => {
156
166
  for (const h of ["copilot", "claude", "pi"]) {
157
167
  assert.deepStrictEqual(validateHarness(h), { ok: true });
158
168
  }
159
169
  });
160
170
 
161
- it("returns error when harness is null", () => {
171
+ it("returns error when harness is null", async () => {
162
172
  const result = validateHarness(null);
163
173
  assert.equal(result.ok, false);
164
174
  assert.ok(result.error.includes("Missing harness"));
165
175
  });
166
176
 
167
- it("returns error for unknown harness", () => {
177
+ it("returns error for unknown harness", async () => {
168
178
  const result = validateHarness("docker");
169
179
  assert.equal(result.ok, false);
170
180
  assert.ok(result.error.includes("Unknown harness"));
@@ -198,7 +208,7 @@ describe("CLI integration", () => {
198
208
  });
199
209
 
200
210
  it("exits 0 for install claude (into a temp output dir)", async () => {
201
- const dir = mkdtempSync(join(tmpdir(), "agent-cortex-cli-"));
211
+ const dir = await mkdtemp(join(tmpdir(), "agent-cortex-cli-"));
202
212
  const { exitCode, stdout } = await runCli(["install", "claude", "--output", dir]);
203
213
  assert.equal(exitCode, 0);
204
214
  assert.ok(stdout.includes("claude"));
@@ -236,15 +246,15 @@ describe("CLI integration", () => {
236
246
  });
237
247
 
238
248
  it("exits 0 for install pi --output <tmp> and writes agent files", async () => {
239
- const outDir = mkdtempSync(join(tmpdir(), "cli-pi-install-"));
249
+ const outDir = await mkdtemp(join(tmpdir(), "cli-pi-install-"));
240
250
  try {
241
251
  const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
242
252
  assert.equal(exitCode, 0);
243
253
  assert.ok(stdout.includes("ralph.agent.md"));
244
- const ralph = readFileSync(join(outDir, "agents", "ralph.agent.md"), "utf-8");
254
+ const ralph = await readFile(join(outDir, "agents", "ralph.agent.md"), "utf-8");
245
255
  assert.ok(ralph.includes("agent-cortex:ralph"));
246
256
  } finally {
247
- rmSync(outDir, { recursive: true, force: true });
257
+ await rm(outDir, { recursive: true, force: true });
248
258
  }
249
259
  });
250
260