@jaybeeuu/agent-cortex 1.37.1 → 1.39.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 (135) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +9 -8
  2. package/.github/workflows/ci.yml +190 -8
  3. package/AGENTS.md +27 -6
  4. package/CHANGELOG.md +72 -0
  5. package/README.md +156 -39
  6. package/agents/README.md +49 -13
  7. package/agents/plan/agent.md +51 -0
  8. package/agents/plan/claude/frontmatter.json +6 -0
  9. package/agents/plan/copilot/frontmatter.json +6 -0
  10. package/agents/plan/pi/frontmatter.json +6 -0
  11. package/agents/plan.agent.md +3 -0
  12. package/agents/ralph/agent.md +262 -0
  13. package/agents/ralph/claude/frontmatter.json +6 -0
  14. package/agents/ralph/claude/polling.md +31 -0
  15. package/agents/ralph/copilot/frontmatter.json +6 -0
  16. package/agents/ralph/copilot/polling.md +62 -0
  17. package/agents/ralph/pi/frontmatter.json +6 -0
  18. package/agents/ralph/pi/polling.md +74 -0
  19. package/agents/ralph-plan/agent.md +114 -0
  20. package/agents/ralph-plan/claude/frontmatter.json +6 -0
  21. package/agents/ralph-plan/copilot/frontmatter.json +6 -0
  22. package/agents/ralph-plan/pi/frontmatter.json +6 -0
  23. package/agents/ralph-plan.agent.md +6 -3
  24. package/agents/ralph.agent.md +83 -57
  25. package/agents/strategy/agent.md +104 -0
  26. package/agents/strategy/claude/frontmatter.json +6 -0
  27. package/agents/strategy/copilot/frontmatter.json +6 -0
  28. package/agents/strategy/pi/frontmatter.json +6 -0
  29. package/agents/strategy.agent.md +3 -0
  30. package/bin/agent-cortex.mjs +76 -1
  31. package/bin/installers/claude.mjs +376 -0
  32. package/bin/installers/copilot.mjs +129 -0
  33. package/bin/installers/pi.mjs +197 -0
  34. package/claude/.claude-plugin/plugin.json +1 -1
  35. package/claude/agents/plan.md +3 -1
  36. package/claude/agents/ralph-plan.md +6 -4
  37. package/claude/agents/strategy.md +3 -1
  38. package/claude/hooks/scripts/notify.mjs +54 -0
  39. package/claude/hooks.json +15 -0
  40. package/docs/claude-hooks.md +115 -0
  41. package/docs/ideas/auto-model-selection.md +75 -0
  42. package/docs/ideas/extension-manifests.md +75 -0
  43. package/docs/ideas/improve-pi-system-prompt.md +34 -0
  44. package/docs/ideas/model-selector.md +40 -0
  45. package/docs/ideas/pi-task-memory.md +34 -0
  46. package/docs/ideas/qa-extension.md +43 -0
  47. package/docs/ideas/record-ideas-as-beads.md +115 -0
  48. package/docs/ideas/subagent-visibility.md +72 -0
  49. package/docs/ideas/task-session-context.md +89 -0
  50. package/docs/ideas/unified-ci-pipeline.md +28 -0
  51. package/docs/research/skill-design-research.md +4 -4
  52. package/docs/research/skill-improvements-analysis.md +1 -1
  53. package/docs/skills/skill-anatomy.md +47 -2
  54. package/docs/technical-direction/migration-execution-labels.md +132 -0
  55. package/extensions/agent-modes/README.md +54 -0
  56. package/extensions/agent-modes/discover.test.ts +241 -0
  57. package/extensions/agent-modes/discover.ts +458 -0
  58. package/extensions/agent-modes/index.ts +13 -224
  59. package/extensions/agent-modes/node_modules/.bin/pi +21 -0
  60. package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
  61. package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
  62. package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
  63. package/extensions/agent-modes/package.json +20 -0
  64. package/extensions/agent-modes/tsconfig.json +12 -0
  65. package/extensions/subagent/README.md +19 -0
  66. package/extensions/subagent/index.ts +131 -45
  67. package/extensions/subagent/node_modules/.bin/pi +21 -0
  68. package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
  69. package/extensions/subagent/node_modules/.bin/tsc +21 -0
  70. package/extensions/subagent/node_modules/.bin/tsserver +21 -0
  71. package/extensions/subagent/node_modules/.bin/tsx +21 -0
  72. package/extensions/subagent/package.json +23 -0
  73. package/extensions/subagent/tsconfig.json +12 -0
  74. package/extensions/subagent/wait.test.ts +202 -0
  75. package/extensions/subagent/wait.ts +118 -0
  76. package/hooks/claude/hooks.json +38 -0
  77. package/hooks/claude/scripts/notify.mjs +54 -0
  78. package/lib/cli.mjs +68 -3
  79. package/package.json +6 -4
  80. package/plugin.json +1 -1
  81. package/pnpm-workspace.yaml +1 -1
  82. package/scripts/build-copilot-agents.mjs +30 -0
  83. package/scripts/lib/compose-agent.mjs +241 -0
  84. package/skills/engineering/design-an-interface/SKILL.md +78 -58
  85. package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
  86. package/skills/engineering/grill-with-docs/SKILL.md +66 -63
  87. package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
  88. package/skills/engineering/technical-direction/FORMAT.md +38 -0
  89. package/skills/engineering/technical-direction/SKILL.md +70 -77
  90. package/skills/planning/classify-bead/SKILL.md +59 -35
  91. package/skills/planning/create-task/REFERENCE.md +40 -0
  92. package/skills/planning/create-task/SKILL.md +70 -66
  93. package/skills/planning/create-task/scripts/create-chores.ts +6 -1
  94. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
  95. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
  96. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
  97. package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
  98. package/skills/planning/prd-to-tasks/SKILL.md +86 -170
  99. package/skills/planning/record-idea/FORMAT.md +44 -0
  100. package/skills/planning/record-idea/SKILL.md +70 -53
  101. package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
  102. package/skills/planning/request-refactor-plan/SKILL.md +57 -46
  103. package/skills/planning/write-a-prd/FORMAT.md +54 -0
  104. package/skills/planning/write-a-prd/SKILL.md +49 -48
  105. package/skills/planning/write-a-ticket/SKILL.md +68 -53
  106. package/skills/productivity/handoff/SKILL.md +12 -0
  107. package/skills/productivity/init-beads/SKILL.md +48 -40
  108. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
  109. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
  110. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
  111. package/skills/productivity/write-a-skill/SKILL.md +2 -1
  112. package/skills/review/maintain-agent-docs/SKILL.md +71 -51
  113. package/skills/workflow/hitl-collab/SKILL.md +86 -17
  114. package/skills/workflow/ralph/REFERENCE.md +24 -19
  115. package/skills/workflow/ralph/SKILL.md +88 -74
  116. package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
  117. package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
  118. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
  119. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
  120. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
  121. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
  122. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
  123. package/skills/workflow/using-agent-skills/SKILL.md +122 -0
  124. package/test/claude-installer.test.mjs +248 -0
  125. package/test/cli.test.mjs +115 -2
  126. package/test/copilot-installer.test.mjs +298 -0
  127. package/test/installer.test.mjs +179 -0
  128. package/test/notify-hook.test.mjs +63 -0
  129. package/test/pi-installer.test.mjs +371 -0
  130. package/token-map.README.md +130 -0
  131. package/token-map.json +106 -0
  132. package/.github/workflows/ci.test.ts +0 -217
  133. package/.github/workflows/release.yml +0 -58
  134. package/scripts/build-claude-agents.mjs +0 -179
  135. package/scripts/publish-release.sh +0 -32
@@ -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,122 @@
1
+ ---
2
+ name: using-agent-skills
3
+ description: Routes incoming work to the right skill and enforces core agent behaviors. Use when starting a session, deciding which skill applies, or when you need to check "which skill should I use?".
4
+ ---
5
+
6
+ # Using Agent Skills
7
+
8
+ ## When to use
9
+
10
+ - Starting a new session and need to orient on available workflows.
11
+ - A task arrives and you need to decide which skill applies.
12
+ - The user asks "which skill should I use for this?"
13
+ - You want to verify that core operating behaviors are being followed.
14
+
15
+ ## Workflow
16
+
17
+ When a task arrives, identify the development phase and apply the corresponding skill:
18
+
19
+ ```
20
+ Task arrives
21
+ │
22
+ ├── Don't know what to build yet? ──────→ grill-me
23
+ ├── Have a rough idea? ─────────────────→ record-idea
24
+ ├── New feature / PRD needed? ──────────→ write-a-prd
25
+ ├── PRD approved, need tasks? ──────────→ prd-to-tasks
26
+ ├── Need a single task tracked? ────────→ create-task
27
+ ├── Need to plan a feature end-to-end? ─→ plan
28
+ ├── Running beads / task execution? ────→ ralph
29
+ │ └── Single stage? ─────────────────→ run-pipeline-stage
30
+ ├── Implementing code? ────────────────→ tdd
31
+ │ ├── Designing an interface? ───────→ design-an-interface
32
+ │ └── Need style conventions? ───────→ style-code
33
+ ├── Reviewing code? ───────────────────→ style-code
34
+ │ ├── Security concerns? ────────────→ review-security
35
+ │ └── Reviewing tests? ──────────────→ style-tests
36
+ ├── Writing tests? ────────────────────→ style-tests
37
+ ├── Writing docs? ─────────────────────→ style-documentation
38
+ ├── Writing external comms? ───────────→ style-comms
39
+ ├── Stress-testing a plan? ────────────→ grill-with-docs
40
+ ├── Planning a refactor? ──────────────→ request-refactor-plan
41
+ ├── Evaluating architecture? ──────────→ technical-direction
42
+ ├── Finding refactor opportunities? ───→ improve-codebase-architecture
43
+ ├── Need task tracking? ───────────────→ bd-tool
44
+ ├── Need human review / HITL? ─────────→ hitl-collab
45
+ ├── Need a handoff doc? ───────────────→ handoff
46
+ ├── Writing a ticket for Jira? ────────→ write-a-ticket
47
+ ├── Creating a new skill? ─────────────→ write-a-skill
48
+ ├── Refactoring a skill? ──────────────→ refactor-skill
49
+ ├── Updating agent docs? ──────────────→ maintain-agent-docs
50
+ └── Setting up beads in a new project? → init-beads
51
+ ```
52
+
53
+ ## Core Operating Behaviors
54
+
55
+ These behaviors apply at all times, across all skills. They are non-negotiable.
56
+
57
+ ### 1. Surface Assumptions
58
+
59
+ Before implementing anything non-trivial, explicitly state your assumptions. Don't silently fill in ambiguous requirements — surface uncertainty early, it's cheaper than rework.
60
+
61
+ ### 2. Manage Confusion Actively
62
+
63
+ When you encounter inconsistencies, conflicting requirements, or unclear specifications: **STOP**. Name the specific confusion, present the tradeoff or ask the clarifying question, and wait for resolution before continuing.
64
+
65
+ ### 3. Push Back When Warranted
66
+
67
+ You are not a yes-machine. When an approach has clear problems, point out the issue directly, explain the concrete downside, propose an alternative, and accept the human's decision if they override with full information.
68
+
69
+ ### 4. Enforce Simplicity
70
+
71
+ Your natural tendency is to overcomplicate. Before finishing any implementation, ask: Can this be done in fewer lines? Are these abstractions earning their complexity? Prefer the boring, obvious solution.
72
+
73
+ ### 5. Maintain Scope Discipline
74
+
75
+ Touch only what you're asked to touch. Do not clean up orthogonal code, refactor adjacent systems, or add features not in the spec. Surgical precision, not unsolicited renovation.
76
+
77
+ ### 6. Verify, Don't Assume
78
+
79
+ A task is not complete until verification passes. "Seems right" is never sufficient — there must be evidence: passing tests, build output, or runtime data.
80
+
81
+ ### 7. Flag Persistent Failures
82
+
83
+ When a tool or command fails repeatedly, stop and flag it to the user for investigation. Don't paper over failures with workarounds — workarounds waste tokens and hide real problems. Describe what failed, how many times, and any obvious patterns. Let the user decide whether to investigate in a dedicated session. Inefficiencies and failures should be squashed, not hidden.
84
+
85
+ ### 8. Stay On Target
86
+
87
+ Stick to the task at hand. If you discover work that's outside the current scope — a tangential bug, an adjacent improvement, a missing doc — flag it to the user but don't pursue it. If the side quest blocks or slows your progress, stop and report it. Otherwise, note it and get back on track. Scope creep mid-task burns tokens and dilutes focus.
88
+
89
+ ## Lifecycle Sequence
90
+
91
+ For a complete feature, the typical skill sequence is:
92
+
93
+ 1. `grill-me` → Extract what the user actually wants
94
+ 2. `record-idea` → Refine vague ideas
95
+ 3. `write-a-prd` → Define what we're building
96
+ 4. `prd-to-tasks` → Break into verifiable chunks
97
+ 5. `plan` → Orchestrate the full flow end-to-end
98
+ 6. `ralph` → Execute all pending beads
99
+ 7. `tdd` → Prove each slice works
100
+ 8. `style-code` → Review before merge
101
+ 9. `review-security` → Check for secrets
102
+ 10. `git-workflow` → Clean commit history
103
+
104
+ Not every task needs every skill. A bug fix might only need: `tdd` → `style-code` → `review-security`.
105
+
106
+ ## Red Flags
107
+
108
+ - Starting work without checking which skill applies.
109
+ - Skipping verification because "it looks right."
110
+ - Making wrong assumptions without stating them.
111
+ - Overcomplicating code when a simple solution exists.
112
+ - Modifying code orthogonal to the task scope.
113
+ - Going on side-quests instead of staying on target.
114
+ - Going on side-quests instead of staying on target.
115
+ - Going on side-quests instead of staying on target.
116
+
117
+ ## Verification checklist
118
+
119
+ - [ ] Correct skill identified before starting work.
120
+ - [ ] Core operating behaviors followed throughout.
121
+ - [ ] Verification completed with evidence (tests, build, runtime).
122
+ - [ ] Scope limited to the task at hand.
@@ -0,0 +1,248 @@
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.
7
+
8
+ import { describe, it } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ 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";
20
+ import { tmpdir } from "node:os";
21
+ import { fileURLToPath } from "node:url";
22
+ import { dirname, join } from "node:path";
23
+ import { registerClaude } from "../bin/installers/claude.mjs";
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const ROOT = join(__dirname, "..");
27
+ const CLI_PATH = join(ROOT, "bin", "agent-cortex.mjs");
28
+
29
+ function runCli(args) {
30
+ 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
+ });
34
+ });
35
+ }
36
+
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 };
58
+ }
59
+
60
+ function makeTmp() {
61
+ return mkdtempSync(join(tmpdir(), "agent-cortex-claude-installer-"));
62
+ }
63
+
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(
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
+ return root;
77
+ }
78
+
79
+ // ─── registerClaude ──────────────────────────────────────────────────────────
80
+
81
+ 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);
86
+ try {
87
+ const result = registerClaude({ root, claudeBin: fake.bin });
88
+ assert.equal(result.marketplace, "jaybeeuu");
89
+ assert.equal(result.plugin, "agent-cortex");
90
+ 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, [
97
+ `plugin marketplace add ${root}`,
98
+ "plugin install agent-cortex@jaybeeuu -y",
99
+ "plugin marketplace update jaybeeuu",
100
+ "plugin update agent-cortex -y",
101
+ ]);
102
+ } finally {
103
+ rmSync(tmp, { recursive: true, force: true });
104
+ rmSync(root, { recursive: true, force: true });
105
+ }
106
+ });
107
+
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);
112
+ try {
113
+ const result = registerClaude({ root, claudeBin: fake.bin });
114
+ assert.equal(result.marketplace, "scratchmkt");
115
+ 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"));
120
+ } finally {
121
+ rmSync(tmp, { recursive: true, force: true });
122
+ rmSync(root, { recursive: true, force: true });
123
+ }
124
+ });
125
+
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);
130
+ 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
+ ],
141
+ );
142
+ // No registration spawns happened at all in dry-run mode.
143
+ assert.equal(existsSync(fake.log), false);
144
+ } finally {
145
+ rmSync(tmp, { recursive: true, force: true });
146
+ rmSync(root, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ it("defaults the claude binary to `claude` from PATH (dry-run only — no spawn)", () => {
151
+ const root = makeMarketplaceRoot();
152
+ try {
153
+ const result = registerClaude({ root, dryRun: true });
154
+ assert.equal(result.commands.length, 4);
155
+ assert.ok(result.commands[0][0] === "plugin");
156
+ } finally {
157
+ rmSync(root, { recursive: true, force: true });
158
+ }
159
+ });
160
+
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" });
165
+ try {
166
+ assert.throws(
167
+ () => registerClaude({ root, claudeBin: fake.bin }),
168
+ /plugin.*subcommand.*update/i,
169
+ );
170
+ } finally {
171
+ rmSync(tmp, { recursive: true, force: true });
172
+ rmSync(root, { recursive: true, force: true });
173
+ }
174
+ });
175
+
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" });
180
+ try {
181
+ assert.throws(
182
+ () => registerClaude({ root, claudeBin: fake.bin }),
183
+ /claude plugin install agent-cortex@jaybeeuu -y/,
184
+ );
185
+ } finally {
186
+ rmSync(tmp, { recursive: true, force: true });
187
+ rmSync(root, { recursive: true, force: true });
188
+ }
189
+ });
190
+
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);
195
+ try {
196
+ assert.throws(
197
+ () => registerClaude({ root, claudeBin: fake.bin }),
198
+ /marketplace\.json/,
199
+ );
200
+ // The failed manifest read happens before any claude invocation.
201
+ assert.equal(existsSync(fake.log), false);
202
+ } finally {
203
+ rmSync(tmp, { recursive: true, force: true });
204
+ rmSync(root, { recursive: true, force: true });
205
+ }
206
+ });
207
+
208
+ it("works against the real repo checkout and its marketplace manifest", () => {
209
+ const tmp = makeTmp();
210
+ const fake = makeFakeClaude(tmp);
211
+ 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);
218
+ } finally {
219
+ rmSync(tmp, { recursive: true, force: true });
220
+ }
221
+ });
222
+ });
223
+
224
+ // ─── CLI wiring ───────────────────────────────────────────────────────────────
225
+
226
+ describe("install claude CLI: registration", () => {
227
+ it("--dry-run without --output prints the registration plan and writes nothing", async () => {
228
+ const { exitCode, stdout } = await runCli(["install", "claude", "--dry-run"]);
229
+ assert.equal(exitCode, 0);
230
+ assert.ok(stdout.includes("would run: claude plugin marketplace add"));
231
+ 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"));
234
+ assert.ok(/dry-run/i.test(stdout));
235
+ });
236
+
237
+ it("--output <dir> generates only — no registration plan is printed", async () => {
238
+ const out = mkdtempSync(join(tmpdir(), "agent-cortex-cli-out-"));
239
+ try {
240
+ const { exitCode, stdout } = await runCli(["install", "claude", "--output", out]);
241
+ assert.equal(exitCode, 0);
242
+ assert.doesNotMatch(stdout, /claude plugin/);
243
+ assert.ok(stdout.includes("Generated"));
244
+ } finally {
245
+ rmSync(out, { recursive: true, force: true });
246
+ }
247
+ });
248
+ });
package/test/cli.test.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { execFile } from "node:child_process";
4
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { dirname, join } from "node:path";
6
8
  import { parseArgs, buildHelpText, validateHarness } from "../lib/cli.mjs";
@@ -56,6 +58,84 @@ describe("parseArgs", () => {
56
58
  assert.deepStrictEqual(result, { command: "install", harness: null });
57
59
  });
58
60
 
61
+ it("returns output for install claude --output <dir>", () => {
62
+ const result = parseArgs(["install", "claude", "--output", "/tmp/x"]);
63
+ assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
64
+ });
65
+
66
+ it("returns output for install claude --output=<dir>", () => {
67
+ const result = parseArgs(["install", "claude", "--output=/tmp/x"]);
68
+ assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
69
+ });
70
+
71
+ it("returns dryRun for install claude --dry-run", () => {
72
+ const result = parseArgs(["install", "claude", "--dry-run"]);
73
+ assert.deepStrictEqual(result, { command: "install", harness: "claude", dryRun: true });
74
+ });
75
+
76
+ it("combines --dry-run with --output", () => {
77
+ const result = parseArgs(["install", "claude", "--output", "/tmp/x", "--dry-run"]);
78
+ assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x", dryRun: true });
79
+ });
80
+
81
+ it("reports an unknown option for claude", () => {
82
+ const result = parseArgs(["install", "claude", "--bogus"]);
83
+ assert.equal(result.command, "install");
84
+ assert.equal(result.harness, "claude");
85
+ assert.ok(result.optionError.includes("--bogus"));
86
+ });
87
+
88
+ it("reports a missing value for --output for claude", () => {
89
+ const result = parseArgs(["install", "claude", "--output"]);
90
+ assert.equal(result.command, "install");
91
+ assert.equal(result.harness, "claude");
92
+ assert.ok(result.optionError.includes("--output"));
93
+ });
94
+
95
+ it("parses --dry-run for install", () => {
96
+ const result = parseArgs(["install", "pi", "--dry-run"]);
97
+ assert.deepStrictEqual(result, { command: "install", harness: "pi", dryRun: true });
98
+ });
99
+
100
+ it("parses --output <dir> for install", () => {
101
+ const result = parseArgs(["install", "pi", "--output", "/tmp/out"]);
102
+ assert.deepStrictEqual(result, { command: "install", harness: "pi", output: "/tmp/out" });
103
+ });
104
+
105
+ it("parses --plugin-root <dir> for install", () => {
106
+ const result = parseArgs(["install", "pi", "--plugin-root", "/tmp/plugin"]);
107
+ assert.deepStrictEqual(result, { command: "install", harness: "pi", pluginRoot: "/tmp/plugin" });
108
+ });
109
+
110
+ it("combines multiple install options", () => {
111
+ const result = parseArgs(["install", "pi", "--dry-run", "--output", "/tmp/out", "--plugin-root", "/tmp/plugin"]);
112
+ assert.deepStrictEqual(result, {
113
+ command: "install",
114
+ harness: "pi",
115
+ dryRun: true,
116
+ output: "/tmp/out",
117
+ pluginRoot: "/tmp/plugin",
118
+ });
119
+ });
120
+
121
+ it("reports a missing value for --output", () => {
122
+ const result = parseArgs(["install", "pi", "--output"]);
123
+ assert.equal(result.command, "install");
124
+ assert.equal(result.harness, "pi");
125
+ assert.ok(result.optionError.includes("--output"));
126
+ });
127
+
128
+ it("reports an unknown option", () => {
129
+ const result = parseArgs(["install", "pi", "--bogus"]);
130
+ assert.equal(result.command, "install");
131
+ assert.equal(result.harness, "pi");
132
+ assert.ok(result.optionError.includes("--bogus"));
133
+ });
134
+
135
+ it("keeps the old shape when no options are given", () => {
136
+ assert.deepStrictEqual(parseArgs(["install", "pi"]), { command: "install", harness: "pi" });
137
+ });
138
+
59
139
  it("returns { command: 'unknown', name: 'foo' } for unknown command", () => {
60
140
  const result = parseArgs(["foo"]);
61
141
  assert.deepStrictEqual(result, { command: "unknown", name: "foo" });
@@ -117,8 +197,9 @@ describe("CLI integration", () => {
117
197
  assert.ok(stdout.includes("copilot"));
118
198
  });
119
199
 
120
- it("exits 0 for install claude", async () => {
121
- const { exitCode, stdout } = await runCli(["install", "claude"]);
200
+ it("exits 0 for install claude (into a temp output dir)", async () => {
201
+ const dir = mkdtempSync(join(tmpdir(), "agent-cortex-cli-"));
202
+ const { exitCode, stdout } = await runCli(["install", "claude", "--output", dir]);
122
203
  assert.equal(exitCode, 0);
123
204
  assert.ok(stdout.includes("claude"));
124
205
  });
@@ -135,6 +216,38 @@ describe("CLI integration", () => {
135
216
  assert.ok(stderr.includes("Missing harness"));
136
217
  });
137
218
 
219
+ it("exits 1 and prints error for install with an unknown option", async () => {
220
+ const { exitCode, stderr } = await runCli(["install", "pi", "--bogus"]);
221
+ assert.equal(exitCode, 1);
222
+ assert.ok(stderr.includes("--bogus"));
223
+ });
224
+
225
+ it("exits 1 and prints error for install with a missing option value", async () => {
226
+ const { exitCode, stderr } = await runCli(["install", "pi", "--output"]);
227
+ assert.equal(exitCode, 1);
228
+ assert.ok(stderr.includes("--output"));
229
+ });
230
+
231
+ it("exits 0 for install pi --dry-run and writes nothing", async () => {
232
+ const { exitCode, stdout } = await runCli(["install", "pi", "--dry-run"]);
233
+ assert.equal(exitCode, 0);
234
+ assert.ok(stdout.includes("ralph.agent.md"));
235
+ assert.ok(stdout.includes("dry-run"));
236
+ });
237
+
238
+ it("exits 0 for install pi --output <tmp> and writes agent files", async () => {
239
+ const outDir = mkdtempSync(join(tmpdir(), "cli-pi-install-"));
240
+ try {
241
+ const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
242
+ assert.equal(exitCode, 0);
243
+ assert.ok(stdout.includes("ralph.agent.md"));
244
+ const ralph = readFileSync(join(outDir, "agents", "ralph.agent.md"), "utf-8");
245
+ assert.ok(ralph.includes("agent-cortex:ralph"));
246
+ } finally {
247
+ rmSync(outDir, { recursive: true, force: true });
248
+ }
249
+ });
250
+
138
251
  it("exits 1 and prints error for install with unknown harness", async () => {
139
252
  const { exitCode, stderr } = await runCli(["install", "docker"]);
140
253
  assert.equal(exitCode, 1);