@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.
Files changed (91) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +1 -1
  2. package/.github/workflows/ci.yml +47 -14
  3. package/AGENTS.md +35 -8
  4. package/CHANGELOG.md +60 -0
  5. package/README.md +174 -97
  6. package/agents/README.md +14 -11
  7. package/bin/agent-cortex.mjs +55 -10
  8. package/bin/installers/claude.mjs +674 -174
  9. package/bin/installers/copilot.mjs +13 -18
  10. package/bin/installers/pi.mjs +293 -51
  11. package/claude/settings.json +4 -0
  12. package/{claude → claude-extras}/scripts/statusline-command.sh +32 -4
  13. package/docs/claude-hooks.md +15 -3
  14. package/docs/ideas/ban-content-assertion-tests.md +73 -0
  15. package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
  16. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
  17. package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
  18. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
  19. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
  20. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
  21. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
  22. package/docs/ideas/extension-manifests.md +55 -14
  23. package/docs/ideas/improve-pi-system-prompt.md +5 -2
  24. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
  25. package/docs/ideas/record-idea-implementation-questions.md +68 -0
  26. package/docs/ideas/self-flushing-session.md +72 -0
  27. package/docs/ideas/state-in-beads-not-md.md +69 -0
  28. package/docs/ideas/task-session-context.local.md +95 -0
  29. package/docs/ideas/test-data-creation-helpers.md +92 -0
  30. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
  31. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
  32. package/docs/technical-direction/harness-config.md +161 -0
  33. package/docs/technical-direction/migration-execution-labels.md +60 -23
  34. package/extensions/agent-guardrails/README.md +45 -0
  35. package/extensions/agent-guardrails/guardrails.test.ts +42 -0
  36. package/extensions/agent-guardrails/guardrails.ts +66 -0
  37. package/extensions/agent-guardrails/index.ts +22 -0
  38. package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
  39. package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
  40. package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
  41. package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
  42. package/extensions/agent-guardrails/package.json +19 -0
  43. package/extensions/agent-guardrails/tsconfig.json +12 -0
  44. package/extensions/agent-modes/README.md +12 -5
  45. package/extensions/agent-modes/discover.test.ts +30 -7
  46. package/extensions/agent-modes/discover.ts +1 -1
  47. package/extensions/agent-modes/index.test.ts +208 -0
  48. package/extensions/agent-modes/index.ts +23 -6
  49. package/extensions/agent-modes/package.json +1 -1
  50. package/extensions/skill-stats/index.test.ts +3 -2
  51. package/lib/cli.mjs +30 -12
  52. package/lib/pi-packages.mjs +169 -0
  53. package/package.json +8 -4
  54. package/pi/settings.json +23 -12
  55. package/plugin.json +1 -1
  56. package/scripts/build-copilot-agents.mjs +1 -1
  57. package/scripts/check-no-sync.mjs +49 -0
  58. package/scripts/lib/compose-agent.mjs +32 -14
  59. package/scripts/lib/fs.mjs +31 -0
  60. package/skills/engineering/grill-with-docs/SKILL.md +1 -1
  61. package/skills/planning/classify-bead/REFERENCE.md +85 -0
  62. package/skills/planning/classify-bead/SKILL.md +32 -36
  63. package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
  64. package/skills/planning/create-task/SKILL.md +19 -10
  65. package/skills/planning/write-a-prd/SKILL.md +2 -2
  66. package/skills/style/style-code/REFERENCE.md +32 -2
  67. package/skills/style/style-code/SKILL.md +5 -21
  68. package/skills/style/style-tests/SKILL.md +69 -17
  69. package/skills/workflow/hitl-collab/SKILL.md +1 -1
  70. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  71. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  72. package/test/classify-bead.test.mjs +277 -0
  73. package/test/claude-installer.test.mjs +199 -139
  74. package/test/cli.test.mjs +95 -41
  75. package/test/copilot-installer.test.mjs +92 -82
  76. package/test/fs.test.mjs +76 -0
  77. package/test/helpers/fake-claude.mjs +152 -0
  78. package/test/installer.test.mjs +542 -57
  79. package/test/pi-installer.test.mjs +457 -102
  80. package/test/pi-packages.test.mjs +295 -0
  81. package/token-map.README.md +23 -12
  82. package/token-map.json +8 -7
  83. package/.claude-plugin/marketplace.json +0 -16
  84. package/claude/.claude-plugin/plugin.json +0 -11
  85. package/claude/agents/plan.md +0 -58
  86. package/claude/agents/ralph-plan.md +0 -121
  87. package/claude/agents/ralph.md +0 -193
  88. package/claude/agents/strategy.md +0 -111
  89. package/claude/hooks/scripts/notify.mjs +0 -54
  90. package/claude/hooks.json +0 -38
  91. /package/{claude → claude-extras}/.mcp.json +0 -0
@@ -1,71 +1,71 @@
1
1
  // Tests for `agent-cortex install claude` runtime registration: after
2
- // regenerating the claude/ subtree, the plain install registers the plugin
3
- // with Claude Code by driving the `claude plugin` CLI (marketplace add →
4
- // install → marketplace update → plugin update). The real CLI is a genuine
5
- // external system, so these tests drive a fake `claudeBin` script that records
6
- // every invocation; the real repo's marketplace.json supplies the manifest.
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.
7
11
 
8
12
  import { describe, it } from "node:test";
9
13
  import assert from "node:assert/strict";
10
14
  import { execFile } from "node:child_process";
11
- import {
12
- mkdtempSync,
13
- mkdirSync,
14
- writeFileSync,
15
- rmSync,
16
- readFileSync,
17
- chmodSync,
18
- existsSync,
19
- } from "node:fs";
15
+ import { mkdtemp, mkdir, writeFile, rm, readFile, stat } from "node:fs/promises";
20
16
  import { tmpdir } from "node:os";
21
17
  import { fileURLToPath } from "node:url";
22
18
  import { dirname, join } from "node:path";
23
19
  import { registerClaude } from "../bin/installers/claude.mjs";
20
+ import { makeFakeClaude, registrationActions } from "./helpers/fake-claude.mjs";
24
21
 
25
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
23
  const ROOT = join(__dirname, "..");
27
24
  const CLI_PATH = join(ROOT, "bin", "agent-cortex.mjs");
28
25
 
29
- function runCli(args) {
26
+ function runCli(args, env = {}) {
30
27
  return new Promise((resolve) => {
31
- execFile(process.execPath, [CLI_PATH, ...args], (error, stdout, stderr) => {
32
- resolve({ exitCode: error ? error.code ?? 1 : 0, stdout, stderr });
33
- });
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
+ );
34
36
  });
35
37
  }
36
38
 
37
- // ─── Fake claude CLI ─────────────────────────────────────────────────────────
38
-
39
- /**
40
- * A fake `claude` binary recording every argv line to `log` (one line per
41
- * invocation, space-joined). Set `failWhen` to a substring: any invocation
42
- * whose argv contains it exits 1. Exits 0 otherwise.
43
- */
44
- function makeFakeClaude(dir, { failWhen = null } = {}) {
45
- const bin = join(dir, "claude");
46
- const log = join(dir, "calls.log");
47
- const script = [
48
- "#!/bin/sh",
49
- `echo "$*" >> "${log}"`,
50
- ...(failWhen
51
- ? [`if printf '%s' "$*" | grep -q -- "${failWhen}"; then exit 1; fi`]
52
- : []),
53
- "exit 0",
54
- ].join("\n");
55
- writeFileSync(bin, `${script}\n`);
56
- chmodSync(bin, 0o755);
57
- return { bin, log };
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
+ }
58
47
  }
59
48
 
60
49
  function makeTmp() {
61
- return mkdtempSync(join(tmpdir(), "agent-cortex-claude-installer-"));
50
+ return mkdtemp(join(tmpdir(), "agent-cortex-claude-installer-"));
62
51
  }
63
52
 
64
- /** A minimal marketplace tree (as seen in the real repo) at a scratch path. */
65
- function makeMarketplaceRoot({ name = "jaybeeuu", plugin = "agent-cortex" } = {}) {
66
- const root = mkdtempSync(join(tmpdir(), "agent-cortex-marketplace-"));
67
- mkdirSync(join(root, ".claude-plugin"), { recursive: true });
68
- writeFileSync(
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
69
  join(root, ".claude-plugin", "marketplace.json"),
70
70
  JSON.stringify({
71
71
  name,
@@ -73,150 +73,210 @@ function makeMarketplaceRoot({ name = "jaybeeuu", plugin = "agent-cortex" } = {}
73
73
  plugins: [{ name: plugin, source: "./claude", description: "test plugin" }],
74
74
  }),
75
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
+ );
76
81
  return root;
77
82
  }
78
83
 
79
84
  // ─── registerClaude ──────────────────────────────────────────────────────────
80
85
 
81
86
  describe("registerClaude", () => {
82
- it("drives claude plugin marketplace add → install → marketplace update → plugin update", () => {
83
- const tmp = makeTmp();
84
- const root = makeMarketplaceRoot();
85
- const fake = makeFakeClaude(tmp);
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);
86
91
  try {
87
- const result = registerClaude({ root, claudeBin: fake.bin });
92
+ const result = await registerClaude({ root, claudeBin: fake.bin });
88
93
  assert.equal(result.marketplace, "jaybeeuu");
89
94
  assert.equal(result.plugin, "agent-cortex");
90
95
  assert.equal(result.dryRun, false);
91
-
92
- const calls = readFileSync(fake.log, "utf-8").trim().split("\n");
93
- // The availability probe (`plugin --help`) is a spawn, but the registration
94
- // surface is the four commands below — assert those, in order.
95
- const registration = calls.filter((c) => !c.startsWith("plugin --help"));
96
- assert.deepEqual(registration, [
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), [
97
104
  `plugin marketplace add ${root}`,
98
105
  "plugin install agent-cortex@jaybeeuu -y",
99
- "plugin marketplace update jaybeeuu",
100
- "plugin update agent-cortex -y",
101
106
  ]);
102
107
  } finally {
103
- rmSync(tmp, { recursive: true, force: true });
104
- rmSync(root, { recursive: true, force: true });
108
+ await rm(tmp, { recursive: true, force: true });
109
+ await rm(root, { recursive: true, force: true });
105
110
  }
106
111
  });
107
112
 
108
- it("derives marketplace and plugin names from the manifest", () => {
109
- const tmp = makeTmp();
110
- const root = makeMarketplaceRoot({ name: "scratchmkt", plugin: "scratchplug" });
111
- const fake = makeFakeClaude(tmp);
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);
112
117
  try {
113
- const result = registerClaude({ root, claudeBin: fake.bin });
118
+ const result = await registerClaude({ root, claudeBin: fake.bin });
114
119
  assert.equal(result.marketplace, "scratchmkt");
115
120
  assert.equal(result.plugin, "scratchplug");
116
- const calls = readFileSync(fake.log, "utf-8").trim().split("\n");
117
- const registration = calls.filter((c) => !c.startsWith("plugin --help"));
118
- assert.ok(registration.some((c) => c === "plugin install scratchplug@scratchmkt -y"));
119
- assert.ok(registration.some((c) => c === "plugin update scratchplug -y"));
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")));
120
125
  } finally {
121
- rmSync(tmp, { recursive: true, force: true });
122
- rmSync(root, { recursive: true, force: true });
126
+ await rm(tmp, { recursive: true, force: true });
127
+ await rm(root, { recursive: true, force: true });
123
128
  }
124
129
  });
125
130
 
126
- it("--dry-run plans the commands without spawning claude at all", () => {
127
- const tmp = makeTmp();
128
- const root = makeMarketplaceRoot();
129
- const fake = makeFakeClaude(tmp);
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);
130
135
  try {
131
- const result = registerClaude({ root, claudeBin: fake.bin, dryRun: true });
132
- assert.equal(result.dryRun, true);
133
- assert.deepEqual(
134
- result.commands,
135
- [
136
- ["plugin", "marketplace", "add", root],
137
- ["plugin", "install", "agent-cortex@jaybeeuu", "-y"],
138
- ["plugin", "marketplace", "update", "jaybeeuu"],
139
- ["plugin", "update", "agent-cortex", "-y"],
140
- ],
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" }),
141
177
  );
142
- // No registration spawns happened at all in dry-run mode.
143
- assert.equal(existsSync(fake.log), false);
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"]);
144
192
  } finally {
145
- rmSync(tmp, { recursive: true, force: true });
146
- rmSync(root, { recursive: true, force: true });
193
+ await rm(tmp, { recursive: true, force: true });
194
+ await rm(root, { recursive: true, force: true });
147
195
  }
148
196
  });
149
197
 
150
- it("defaults the claude binary to `claude` from PATH (dry-run only — no spawn)", () => {
151
- const root = makeMarketplaceRoot();
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);
152
202
  try {
153
- const result = registerClaude({ root, dryRun: true });
154
- assert.equal(result.commands.length, 4);
155
- assert.ok(result.commands[0][0] === "plugin");
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);
156
210
  } finally {
157
- rmSync(root, { recursive: true, force: true });
211
+ await rm(tmp, { recursive: true, force: true });
212
+ await rm(root, { recursive: true, force: true });
158
213
  }
159
214
  });
160
215
 
161
- it("fails with upgrade guidance when the claude binary lacks the plugin subcommand", () => {
162
- const tmp = makeTmp();
163
- const root = makeMarketplaceRoot();
164
- const fake = makeFakeClaude(tmp, { failWhen: "plugin --help" });
216
+ it("defaults the claude binary to `claude` from PATH (dry-run only — no spawn)", async () => {
217
+ const root = await makeMarketplaceRoot();
165
218
  try {
166
- assert.throws(
167
- () => registerClaude({ root, claudeBin: fake.bin }),
168
- /plugin.*subcommand.*update/i,
169
- );
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);
170
223
  } finally {
171
- rmSync(tmp, { recursive: true, force: true });
172
- rmSync(root, { recursive: true, force: true });
224
+ await rm(root, { recursive: true, force: true });
173
225
  }
174
226
  });
175
227
 
176
- it("fails, naming the failing command, when a claude plugin command errors", () => {
177
- const tmp = makeTmp();
178
- const root = makeMarketplaceRoot();
179
- const fake = makeFakeClaude(tmp, { failWhen: "plugin install" });
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" });
180
232
  try {
181
- assert.throws(
182
- () => registerClaude({ root, claudeBin: fake.bin }),
183
- /claude plugin install agent-cortex@jaybeeuu -y/,
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/,
184
244
  );
185
245
  } finally {
186
- rmSync(tmp, { recursive: true, force: true });
187
- rmSync(root, { recursive: true, force: true });
246
+ await rm(tmp, { recursive: true, force: true });
247
+ await rm(root, { recursive: true, force: true });
188
248
  }
189
249
  });
190
250
 
191
- it("fails when the checkout has no marketplace manifest", () => {
192
- const tmp = makeTmp();
193
- const root = mkdtempSync(join(tmpdir(), "agent-cortex-no-market-"));
194
- const fake = makeFakeClaude(tmp);
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" });
195
255
  try {
196
- assert.throws(
197
- () => registerClaude({ root, claudeBin: fake.bin }),
198
- /marketplace\.json/,
256
+ await assert.rejects(
257
+ registerClaude({ root, claudeBin: fake.bin }),
258
+ /claude plugin install agent-cortex@jaybeeuu -y/,
199
259
  );
200
- // The failed manifest read happens before any claude invocation.
201
- assert.equal(existsSync(fake.log), false);
202
260
  } finally {
203
- rmSync(tmp, { recursive: true, force: true });
204
- rmSync(root, { recursive: true, force: true });
261
+ await rm(tmp, { recursive: true, force: true });
262
+ await rm(root, { recursive: true, force: true });
205
263
  }
206
264
  });
207
265
 
208
- it("works against the real repo checkout and its marketplace manifest", () => {
209
- const tmp = makeTmp();
210
- const fake = makeFakeClaude(tmp);
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);
211
270
  try {
212
- const result = registerClaude({ root: ROOT, claudeBin: fake.bin });
213
- assert.equal(result.marketplace, "jaybeeuu");
214
- assert.equal(result.plugin, "agent-cortex");
215
- const calls = readFileSync(fake.log, "utf-8").trim().split("\n");
216
- const registration = calls.filter((c) => !c.startsWith("plugin --help"));
217
- assert.equal(registration.length, 4);
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);
218
277
  } finally {
219
- rmSync(tmp, { recursive: true, force: true });
278
+ await rm(tmp, { recursive: true, force: true });
279
+ await rm(root, { recursive: true, force: true });
220
280
  }
221
281
  });
222
282
  });
@@ -229,20 +289,20 @@ describe("install claude CLI: registration", () => {
229
289
  assert.equal(exitCode, 0);
230
290
  assert.ok(stdout.includes("would run: claude plugin marketplace add"));
231
291
  assert.ok(stdout.includes("claude plugin install agent-cortex@jaybeeuu"));
232
- assert.ok(stdout.includes("claude plugin marketplace update jaybeeuu"));
233
- assert.ok(stdout.includes("claude plugin update agent-cortex"));
292
+ assert.ok(stdout.includes("plugin marketplace update"));
293
+ assert.ok(stdout.includes("plugin update agent-cortex"));
234
294
  assert.ok(/dry-run/i.test(stdout));
235
295
  });
236
296
 
237
297
  it("--output <dir> generates only — no registration plan is printed", async () => {
238
- const out = mkdtempSync(join(tmpdir(), "agent-cortex-cli-out-"));
298
+ const out = await mkdtemp(join(tmpdir(), "agent-cortex-cli-out-"));
239
299
  try {
240
300
  const { exitCode, stdout } = await runCli(["install", "claude", "--output", out]);
241
301
  assert.equal(exitCode, 0);
242
302
  assert.doesNotMatch(stdout, /claude plugin/);
243
303
  assert.ok(stdout.includes("Generated"));
244
304
  } finally {
245
- rmSync(out, { recursive: true, force: true });
305
+ await rm(out, { recursive: true, force: true });
246
306
  }
247
307
  });
248
308
  });