@jaybeeuu/agent-cortex 2.0.0 → 2.2.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 (183) hide show
  1. package/CHANGELOG.md +437 -0
  2. package/README.md +46 -15
  3. package/agents/README.md +22 -10
  4. package/agents/plan/agent.md +2 -2
  5. package/agents/ralph/agent.md +128 -91
  6. package/agents/ralph/claude/frontmatter.json +3 -3
  7. package/agents/ralph/claude/wake.md +46 -0
  8. package/agents/ralph/pi/{polling.md → wake.md} +35 -20
  9. package/agents/ralph-plan/agent.md +23 -11
  10. package/bin/agent-cortex.mjs +66 -4
  11. package/bin/installers/claude.mjs +262 -77
  12. package/bin/installers/pi.mjs +200 -24
  13. package/extensions/agent-guardrails/README.md +1 -1
  14. package/extensions/agent-guardrails/guardrails.ts +1 -1
  15. package/extensions/agent-guardrails/index.ts +1 -1
  16. package/extensions/agent-modes/discover.test.ts +2 -2
  17. package/extensions/output-style/README.md +58 -0
  18. package/extensions/output-style/index.ts +23 -0
  19. package/extensions/output-style/output-style.md +43 -0
  20. package/extensions/output-style/output-style.test.ts +50 -0
  21. package/extensions/output-style/output-style.ts +32 -0
  22. package/extensions/output-style/package.json +19 -0
  23. package/hooks/claude/scripts/notify.mjs +8 -1
  24. package/lib/cli.mjs +91 -2
  25. package/package.json +29 -4
  26. package/scripts/check-changeset.mjs +4 -0
  27. package/scripts/check-no-sync.mjs +5 -1
  28. package/scripts/check-skill-frontmatter.mjs +21 -0
  29. package/scripts/lib/compose-agent.mjs +69 -9
  30. package/scripts/lib/fs.mjs +10 -2
  31. package/scripts/lib/migrate.mjs +736 -0
  32. package/scripts/lib/release-gates.mjs +21 -1
  33. package/scripts/lib/skill-frontmatter.mjs +152 -0
  34. package/scripts/lib/skill-links.mjs +52 -0
  35. package/scripts/lib/verify-worker-tree.mjs +25 -0
  36. package/scripts/resolve-publish-command.mjs +4 -0
  37. package/skills/engineering/design-an-interface/SKILL.md +3 -3
  38. package/skills/engineering/grill-with-docs/SKILL.md +3 -5
  39. package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
  40. package/skills/engineering/tdd/SKILL.md +5 -7
  41. package/skills/engineering/technical-direction/SKILL.md +5 -5
  42. package/skills/planning/classify-bead/REFERENCE.md +3 -3
  43. package/skills/planning/classify-bead/SKILL.md +14 -9
  44. package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
  45. package/skills/planning/create-task/REFERENCE.md +52 -3
  46. package/skills/planning/create-task/SKILL.md +70 -26
  47. package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
  48. package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
  49. package/skills/planning/flesh-out-idea/SKILL.md +146 -0
  50. package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
  51. package/skills/planning/prd-to-tasks/SKILL.md +22 -16
  52. package/skills/planning/record-idea/FORMAT.md +66 -17
  53. package/skills/planning/record-idea/SKILL.md +48 -24
  54. package/skills/planning/request-refactor-plan/SKILL.md +4 -4
  55. package/skills/planning/spec-to-tickets/SKILL.md +5 -5
  56. package/skills/planning/write-a-prd/SKILL.md +8 -9
  57. package/skills/planning/write-a-ticket/SKILL.md +4 -6
  58. package/skills/productivity/bd-tool/SKILL.md +15 -10
  59. package/skills/productivity/grill-me/SKILL.md +5 -0
  60. package/skills/productivity/init-beads/SKILL.md +3 -3
  61. package/skills/productivity/migration/REFERENCE.md +227 -0
  62. package/skills/productivity/migration/SKILL.md +188 -0
  63. package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
  64. package/skills/productivity/write-a-skill/SKILL.md +5 -5
  65. package/skills/review/maintain-agent-docs/SKILL.md +5 -7
  66. package/skills/review/refactor-skill/SKILL.md +4 -3
  67. package/skills/review/review-security/SKILL.md +3 -5
  68. package/skills/style/style-code/SKILL.md +5 -10
  69. package/skills/style/style-comms/SKILL.md +5 -7
  70. package/skills/style/style-documentation/SKILL.md +5 -6
  71. package/skills/style/style-tests/SKILL.md +5 -7
  72. package/skills/workflow/git-workflow/SKILL.md +28 -4
  73. package/skills/workflow/hitl-collab/SKILL.md +4 -6
  74. package/skills/workflow/plan/SKILL.md +41 -30
  75. package/skills/workflow/ralph/REFERENCE.md +209 -55
  76. package/skills/workflow/ralph/SKILL.md +89 -50
  77. package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
  78. package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
  79. package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
  80. package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
  81. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
  82. package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
  83. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
  84. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
  85. package/token-map.README.md +9 -4
  86. package/token-map.json +13 -6
  87. package/.agents/skills/style-versioning/SKILL.md +0 -172
  88. package/.changeset/README.md +0 -8
  89. package/.changeset/config.json +0 -11
  90. package/.github/workflows/ci.yml +0 -302
  91. package/.junie/guidelines.md +0 -87
  92. package/.junie/mcp/mcp.json +0 -10
  93. package/AGENTS.md +0 -151
  94. package/agents/ralph/claude/polling.md +0 -31
  95. package/agents-native/ralph.md +0 -193
  96. package/docs/claude-hooks.md +0 -128
  97. package/docs/commit-signing.md +0 -231
  98. package/docs/ideas/auto-model-selection.md +0 -75
  99. package/docs/ideas/ban-content-assertion-tests.md +0 -75
  100. package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
  101. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
  102. package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
  103. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
  104. package/docs/ideas/dedicated-review-tests-skill.md +0 -71
  105. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
  106. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
  107. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
  108. package/docs/ideas/extension-manifests.md +0 -121
  109. package/docs/ideas/improve-pi-system-prompt.md +0 -37
  110. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
  111. package/docs/ideas/model-selector.md +0 -40
  112. package/docs/ideas/pi-task-memory.md +0 -34
  113. package/docs/ideas/qa-extension.md +0 -43
  114. package/docs/ideas/record-idea-implementation-questions.md +0 -68
  115. package/docs/ideas/record-ideas-as-beads.md +0 -115
  116. package/docs/ideas/self-flushing-session.md +0 -72
  117. package/docs/ideas/state-in-beads-not-md.md +0 -69
  118. package/docs/ideas/subagent-visibility.md +0 -72
  119. package/docs/ideas/task-session-context.local.md +0 -95
  120. package/docs/ideas/task-session-context.md +0 -89
  121. package/docs/ideas/test-data-creation-helpers.md +0 -92
  122. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
  123. package/docs/ideas/unified-ci-pipeline.md +0 -28
  124. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
  125. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
  126. package/docs/inspirations.md +0 -33
  127. package/docs/research/disable-model-invocation.md +0 -82
  128. package/docs/research/gas-city-research.md +0 -256
  129. package/docs/research/skill-design-research.md +0 -286
  130. package/docs/research/skill-improvements-analysis.md +0 -117
  131. package/docs/research/skill-patterns-research.md +0 -24
  132. package/docs/skills/skill-anatomy.md +0 -197
  133. package/docs/technical-direction/harness-config.md +0 -177
  134. package/docs/technical-direction/migration-execution-labels.md +0 -177
  135. package/docs/user-preferences.md +0 -17
  136. package/extensions/agent-guardrails/tsconfig.json +0 -12
  137. package/extensions/agent-modes/tsconfig.json +0 -12
  138. package/extensions/session-start/tsconfig.json +0 -12
  139. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  140. package/extensions/skill-stats/tsconfig.json +0 -12
  141. package/extensions/subagent/agents/planner.md +0 -37
  142. package/extensions/subagent/agents/reviewer.md +0 -35
  143. package/extensions/subagent/agents/scout.md +0 -50
  144. package/extensions/subagent/agents/worker.md +0 -24
  145. package/extensions/subagent/prompts/implement-and-review.md +0 -10
  146. package/extensions/subagent/prompts/implement.md +0 -10
  147. package/extensions/subagent/prompts/scout-and-plan.md +0 -9
  148. package/extensions/subagent/tsconfig.json +0 -12
  149. package/pi/keybindings.json +0 -3
  150. package/pi/settings.json +0 -34
  151. package/pnpm-workspace.yaml +0 -14
  152. package/skills/planning/create-task/scripts/package.json +0 -14
  153. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  154. package/skills/planning/create-task/scripts/tsconfig.json +0 -12
  155. package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
  156. package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
  157. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  158. package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
  159. package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
  160. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
  161. package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
  162. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
  163. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
  164. package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
  165. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
  166. package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
  167. package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
  168. package/test/classify-bead.test.mjs +0 -277
  169. package/test/claude-installer.test.mjs +0 -308
  170. package/test/cli.test.mjs +0 -314
  171. package/test/create-chores.test.mjs +0 -85
  172. package/test/fake-bd.test.mjs +0 -72
  173. package/test/fs.test.mjs +0 -76
  174. package/test/helpers/fake-bd.mjs +0 -87
  175. package/test/helpers/fake-claude.mjs +0 -152
  176. package/test/helpers/fake-pi.mjs +0 -132
  177. package/test/installer.test.mjs +0 -664
  178. package/test/notify-hook.test.mjs +0 -63
  179. package/test/pi-installer.test.mjs +0 -785
  180. package/test/pi-register.test.mjs +0 -342
  181. package/test/pi-sweep.test.mjs +0 -619
  182. package/test/release-gates.test.mjs +0 -190
  183. package/test/verify-worker-tree.test.mjs +0 -120
@@ -1,85 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { execFile } from "node:child_process";
4
- import { mkdtemp, readFile, rm } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
- import { fileURLToPath } from "node:url";
7
- import { dirname, join } from "node:path";
8
-
9
- import { makeFakeBd } from "./helpers/fake-bd.mjs";
10
-
11
- const testDir = dirname(fileURLToPath(import.meta.url));
12
- const CREATE_CHORES = join(testDir, "..", "skills", "planning", "create-task", "scripts", "create-chores.ts");
13
-
14
- /** Run create-chores.ts against the fake `bd` with a parent that already
15
- * carries implementation-type:afk — the label `bd create --parent` inherits.
16
- * @param {string[]} parentLabels Labels seeded on the parent bead
17
- * @returns {Promise<{ ids: Record<string, string>, beads: Array<{ id: string, title: string, labels: string[] }> }>} */
18
- async function createChoresAgainstFakeBd(parentLabels) {
19
- const dir = await mkdtemp(join(tmpdir(), "create-chores-"));
20
- const { bin, state } = await makeFakeBd(dir, {
21
- beads: [{ id: "parent-1", title: "Feature", labels: parentLabels }],
22
- });
23
-
24
- try {
25
- const { exitCode, stdout, stderr } = await runScript(
26
- ["--parent", "parent-1"],
27
- { BD_PATH: bin, FAKE_BD_STATE: state },
28
- );
29
- assert.equal(exitCode, 0, stderr);
30
-
31
- const ids = JSON.parse(stdout);
32
- const { beads } = JSON.parse(await readFile(state, "utf8"));
33
- return { ids, beads };
34
- } finally {
35
- await rm(dir, { recursive: true, force: true });
36
- }
37
- }
38
-
39
- /** Run create-chores.ts with the given env overrides. */
40
- function runScript(args, env) {
41
- return new Promise((resolve) => {
42
- execFile(
43
- process.execPath,
44
- [CREATE_CHORES, ...args],
45
- { env: { ...process.env, ...env } },
46
- (error, stdout, stderr) => {
47
- resolve({ exitCode: error ? error.code ?? 1 : 0, stdout, stderr });
48
- },
49
- );
50
- });
51
- }
52
-
53
- describe("create-chores", () => {
54
- it("gives the PR gate exactly its declared labels, without the parent's inherited implementation-type:afk", async () => {
55
- const { ids, beads } = await createChoresAgainstFakeBd(["implementation-type:afk"]);
56
-
57
- const gate = beads.find((b) => b.id === ids.featurePrReview);
58
- assert.deepEqual(
59
- [...gate.labels].sort(),
60
- ["implementation-type:hitl", "lifecycle:feature-pr"],
61
- );
62
- });
63
-
64
- it("still lets stage chores inherit implementation-type:afk from the parent", async () => {
65
- const { ids, beads } = await createChoresAgainstFakeBd(["implementation-type:afk"]);
66
-
67
- const codeChore = beads.find((b) => b.id === ids.code);
68
- assert.deepEqual(
69
- [...codeChore.labels].sort(),
70
- ["implementation-type:afk", "stage:code"],
71
- );
72
- });
73
-
74
- it("validates the PR gate task but not the stage chores", async () => {
75
- const { ids, beads } = await createChoresAgainstFakeBd(["implementation-type:afk"]);
76
-
77
- const gate = beads.find((b) => b.id === ids.featurePrReview);
78
- assert.equal(gate.type, "task");
79
- assert.equal(gate.validated, true);
80
- assert.match(gate.description, /## Acceptance Criteria/);
81
-
82
- const codeChore = beads.find((b) => b.id === ids.code);
83
- assert.equal(codeChore.validated, false);
84
- });
85
- });
@@ -1,72 +0,0 @@
1
- // Tests for the stateful fake `bd` helper's `--validate` model.
2
- // The fake mirrors `bd create --validate`: a substantive bead whose description lacks the
3
- // required section(s) for its type is rejected; chores have no requirements.
4
-
5
- import { describe, it } from "node:test";
6
- import assert from "node:assert/strict";
7
- import { execFile } from "node:child_process";
8
- import { mkdtemp, rm } from "node:fs/promises";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
-
12
- import { makeFakeBd } from "./helpers/fake-bd.mjs";
13
-
14
- /** Run the fake `bd` binary with the given args. */
15
- function runFake(bin, state, args) {
16
- return new Promise((resolve) => {
17
- execFile(bin, args, { env: { ...process.env, FAKE_BD_STATE: state } }, (error, stdout, stderr) => {
18
- resolve({ exitCode: error ? error.code ?? 1 : 0, stdout, stderr });
19
- });
20
- });
21
- }
22
-
23
- /** Create a fresh fake workspace, run `fn`, then remove it. */
24
- async function withFake(fn) {
25
- const dir = await mkdtemp(join(tmpdir(), "fake-bd-"));
26
- try {
27
- const { bin, state } = await makeFakeBd(dir, { beads: [] });
28
- await fn(bin, state);
29
- } finally {
30
- await rm(dir, { recursive: true, force: true });
31
- }
32
- }
33
-
34
- describe("fake bd --validate", () => {
35
- it("rejects a section-less task", async () => {
36
- await withFake(async (bin, state) => {
37
- const { exitCode, stderr } = await runFake(bin, state, ["create", "empty", "--type", "task", "--validate"]);
38
- assert.equal(exitCode, 1);
39
- assert.match(stderr, /Acceptance Criteria/);
40
- });
41
- });
42
-
43
- it("accepts a task carrying ## Acceptance Criteria (case-insensitive)", async () => {
44
- await withFake(async (bin, state) => {
45
- const { exitCode } = await runFake(bin, state, [
46
- "create", "ok", "--type", "task",
47
- "--description", "## Acceptance criteria\n- [ ] done",
48
- "--validate",
49
- ]);
50
- assert.equal(exitCode, 0);
51
- });
52
- });
53
-
54
- it("requires ## Success Criteria for an epic", async () => {
55
- await withFake(async (bin, state) => {
56
- const { exitCode, stderr } = await runFake(bin, state, [
57
- "create", "ep", "--type", "epic",
58
- "--description", "## Acceptance Criteria\n- [ ] x",
59
- "--validate",
60
- ]);
61
- assert.equal(exitCode, 1);
62
- assert.match(stderr, /Success Criteria/);
63
- });
64
- });
65
-
66
- it("does not require sections for a chore", async () => {
67
- await withFake(async (bin, state) => {
68
- const { exitCode } = await runFake(bin, state, ["create", "c", "--type", "chore", "--validate"]);
69
- assert.equal(exitCode, 0);
70
- });
71
- });
72
- });
package/test/fs.test.mjs DELETED
@@ -1,76 +0,0 @@
1
- // Unit tests for scripts/lib/fs.mjs — the shared async stat wrappers. The
2
- // helpers are exercised through their public API only; each case creates its
3
- // own throwaway fixture under os.tmpdir().
4
-
5
- import { describe, it } from "node:test";
6
- import assert from "node:assert/strict";
7
- import { mkdtemp, writeFile, rm } from "node:fs/promises";
8
- import { tmpdir } from "node:os";
9
- import { join } from "node:path";
10
- import { isDirectory, isFile } from "../scripts/lib/fs.mjs";
11
-
12
- async function makeTmp() {
13
- return await mkdtemp(join(tmpdir(), "agent-cortex-fs-"));
14
- }
15
-
16
- describe("isFile", () => {
17
- it("is true for an existing regular file", async () => {
18
- const dir = await makeTmp();
19
- try {
20
- const file = join(dir, "file.txt");
21
- await writeFile(file, "hello");
22
- assert.equal(await isFile(file), true);
23
- } finally {
24
- await rm(dir, { recursive: true, force: true });
25
- }
26
- });
27
-
28
- it("is false for a directory", async () => {
29
- const dir = await makeTmp();
30
- try {
31
- assert.equal(await isFile(dir), false);
32
- } finally {
33
- await rm(dir, { recursive: true, force: true });
34
- }
35
- });
36
-
37
- it("is false for a missing path", async () => {
38
- const dir = await makeTmp();
39
- try {
40
- assert.equal(await isFile(join(dir, "nope.txt")), false);
41
- } finally {
42
- await rm(dir, { recursive: true, force: true });
43
- }
44
- });
45
- });
46
-
47
- describe("isDirectory", () => {
48
- it("is true for an existing directory", async () => {
49
- const dir = await makeTmp();
50
- try {
51
- assert.equal(await isDirectory(dir), true);
52
- } finally {
53
- await rm(dir, { recursive: true, force: true });
54
- }
55
- });
56
-
57
- it("is false for a regular file", async () => {
58
- const dir = await makeTmp();
59
- try {
60
- const file = join(dir, "file.txt");
61
- await writeFile(file, "hello");
62
- assert.equal(await isDirectory(file), false);
63
- } finally {
64
- await rm(dir, { recursive: true, force: true });
65
- }
66
- });
67
-
68
- it("is false for a missing path", async () => {
69
- const dir = await makeTmp();
70
- try {
71
- assert.equal(await isDirectory(join(dir, "nope")), false);
72
- } finally {
73
- await rm(dir, { recursive: true, force: true });
74
- }
75
- });
76
- });
@@ -1,87 +0,0 @@
1
- // Shared test helper: a stateful fake `bd` binary for create-chores.ts.
2
- // `bd` is a genuine external system (its real database must never be touched
3
- // by tests), so the script is driven against this fake via the BD_PATH
4
- // override. The fake reproduces the real behaviours the tests depend on —
5
- // `bd create --parent` label inheritance (unless `--no-inherit-labels` is
6
- // passed) and `bd create --validate` section enforcement — and records every
7
- // created bead to a JSON state file the test can read back.
8
- //
9
- // State lives in `state.json` next to the binary; the test seeds it with the
10
- // parent bead and reads the children back after the script runs. Written async
11
- // throughout — the repo's check-no-sync lint scans test/ (including helpers).
12
-
13
- import { chmod, writeFile } from "node:fs/promises";
14
- import { join } from "node:path";
15
-
16
- const FAKE_BD_SCRIPT = [
17
- "#!/usr/bin/env node",
18
- "// Stateful fake for `bd create` (test/helpers/fake-bd.mjs).",
19
- "const fs = require('node:fs/promises');",
20
- "",
21
- "const stateFile = process.env.FAKE_BD_STATE;",
22
- "const VALUE_FLAGS = new Set(['--type', '--description', '--priority', '--labels', '--parent']);",
23
- "const BOOL_FLAGS = new Set(['--silent', '--no-inherit-labels', '--validate']);",
24
- "",
25
- "(async () => {",
26
- " const argv = process.argv.slice(2);",
27
- " if (argv[0] === 'dep') process.exit(0);",
28
- " if (argv[0] !== 'create') process.exit(1);",
29
- "",
30
- " const title = argv[1];",
31
- " const flags = {};",
32
- " const bools = new Set();",
33
- " for (let i = 2; i < argv.length; i++) {",
34
- " const arg = argv[i];",
35
- " if (BOOL_FLAGS.has(arg)) { bools.add(arg); continue; }",
36
- " if (VALUE_FLAGS.has(arg)) { flags[arg] = argv[++i]; continue; }",
37
- " }",
38
- "",
39
- " const state = JSON.parse(await fs.readFile(stateFile, 'utf8'));",
40
- "",
41
- " // Model `bd create --validate`: reject a substantive bead whose description lacks the",
42
- " // required section(s) for its type. Chores have no requirements and always pass.",
43
- " if (bools.has('--validate')) {",
44
- " const type = flags['--type'] || 'task';",
45
- " const required = {",
46
- " task: ['## Acceptance Criteria'],",
47
- " feature: ['## Acceptance Criteria'],",
48
- " bug: ['## Steps to Reproduce', '## Acceptance Criteria'],",
49
- " epic: ['## Success Criteria'],",
50
- " chore: [],",
51
- " }[type] || ['## Acceptance Criteria'];",
52
- " const description = flags['--description'] || '';",
53
- " const missing = required.filter((section) => !description.toLowerCase().includes(section.toLowerCase()));",
54
- " if (missing.length > 0) {",
55
- " console.error('missing required sections for ' + type + ': ' + missing.join(', '));",
56
- " process.exit(1);",
57
- " }",
58
- " }",
59
- "",
60
- " const explicit = (flags['--labels'] || '').split(',').filter(Boolean);",
61
- " const parent = state.beads.find((b) => b.id === flags['--parent']);",
62
- " const inherited = parent && !bools.has('--no-inherit-labels') ? parent.labels : [];",
63
- " const labels = [...new Set([...inherited, ...explicit])];",
64
- "",
65
- " const id = 'bd-' + state.nextId++;",
66
- " state.beads.push({ id, title, labels, parent: flags['--parent'] ?? null, type: flags['--type'] || 'task', description: flags['--description'] || '', validated: bools.has('--validate') });",
67
- " await fs.writeFile(stateFile, JSON.stringify(state));",
68
- " process.stdout.write(id + '\\n');",
69
- "})().catch((err) => {",
70
- " console.error(err);",
71
- " process.exit(1);",
72
- "});",
73
- ].join("\n");
74
-
75
- /** Create a stateful fake `bd` binary in `dir`.
76
- * @param {string} dir Directory to hold the binary and its state.json
77
- * @param {{ beads: Array<{ id: string, title: string, labels: string[] }> }} seed
78
- * Pre-existing beads (the parent) the fake inherits labels from
79
- * @returns {Promise<{ bin: string, state: string }>} */
80
- export async function makeFakeBd(dir, { beads }) {
81
- const bin = join(dir, "bd");
82
- const state = join(dir, "state.json");
83
- await writeFile(bin, FAKE_BD_SCRIPT + "\n");
84
- await chmod(bin, 0o755);
85
- await writeFile(state, JSON.stringify({ beads, nextId: 1 }));
86
- return { bin, state };
87
- }
@@ -1,152 +0,0 @@
1
- // Shared test helper: a stateful fake `claude` binary for the `claude plugin`
2
- // CLI. The real CLI is a genuine external system (and its home config must
3
- // never be touched by tests), so tests drive this fake instead. It records
4
- // every invocation to `calls.log` (argv, space-joined, one line each) and
5
- // simulates just enough real behaviour for idempotency assertions:
6
- //
7
- // plugin --help → exit 0 (availability probe)
8
- // plugin marketplace list --json → registered marketplaces as JSON
9
- // plugin marketplace add <root> → registers <root> (name read from its
10
- // .claude-plugin/marketplace.json)
11
- // plugin marketplace update <m> → exit 0
12
- // plugin list --json → installed plugins as JSON
13
- // plugin install <id> -y → records id at the version published by
14
- // the marketplace source's plugin.json
15
- // plugin update <name> -y → re-reads the version from the source
16
- //
17
- // State lives in `state.json` next to the binary, so a test can re-run the CLI
18
- // against the same fake and observe the update path. `failWhen` makes any
19
- // invocation whose argv contains the substring exit 1 (probe/command failures).
20
- //
21
- // Written async throughout (the fake is a child process and the helper's own
22
- // I/O is test code) — the repo's check-no-sync lint scans test/ for *Sync.
23
-
24
- import { chmod, writeFile } from "node:fs/promises";
25
- import { join } from "node:path";
26
-
27
- const FAKE_CLAUDE_SCRIPT = [
28
- "#!/usr/bin/env node",
29
- "// Stateful fake for the `claude plugin` CLI (test/helpers/fake-claude.mjs).",
30
- "const fs = require('node:fs/promises');",
31
- "const path = require('node:path');",
32
- "",
33
- "const dir = __dirname;",
34
- "const stateFile = path.join(dir, 'state.json');",
35
- "const logFile = path.join(dir, 'calls.log');",
36
- "const failWhenFile = path.join(dir, 'failWhen.txt');",
37
- "",
38
- "(async () => {",
39
- " const argv = process.argv.slice(2);",
40
- " const joined = argv.join(' ');",
41
- " await fs.appendFile(logFile, joined + '\\n');",
42
- "",
43
- " const failWhen = await fs.readFile(failWhenFile, 'utf8').catch(() => null);",
44
- " if (failWhen && joined.includes(failWhen)) process.exit(1);",
45
- "",
46
- " if (argv[0] !== 'plugin') process.exit(0);",
47
- "",
48
- " const readState = async () => {",
49
- " try {",
50
- " return JSON.parse(await fs.readFile(stateFile, 'utf8'));",
51
- " } catch {",
52
- " return { markets: [], installed: [] };",
53
- " }",
54
- " };",
55
- " const writeState = (state) => fs.writeFile(stateFile, JSON.stringify(state));",
56
- "",
57
- " // Real claude resolves the plugin version from the marketplace source's",
58
- " // plugin.json; the fake mirrors that by reading <source>/claude/.claude-plugin/plugin.json.",
59
- " const pluginJsonVersion = async (marketPath) => {",
60
- " try {",
61
- " return JSON.parse(await fs.readFile(path.join(marketPath, 'claude', '.claude-plugin', 'plugin.json'), 'utf8')).version;",
62
- " } catch {",
63
- " return undefined;",
64
- " }",
65
- " };",
66
- " const marketplaceName = async (source) => {",
67
- " try {",
68
- " return JSON.parse(await fs.readFile(path.join(source, '.claude-plugin', 'marketplace.json'), 'utf8')).name;",
69
- " } catch {",
70
- " return undefined;",
71
- " }",
72
- " };",
73
- "",
74
- " const state = await readState();",
75
- " const [sub, ...rest] = argv.slice(1);",
76
- "",
77
- " if (sub === '--help') process.exit(0);",
78
- "",
79
- " if (sub === 'marketplace') {",
80
- " const [op, ...opArgs] = rest;",
81
- " if (op === 'list') {",
82
- " console.log(JSON.stringify(state.markets));",
83
- " process.exit(0);",
84
- " }",
85
- " if (op === 'add') {",
86
- " const source = path.resolve(opArgs[0]);",
87
- " const name = (await marketplaceName(source)) ?? 'market-' + state.markets.length;",
88
- " if (!state.markets.some((m) => m.name === name)) state.markets.push({ name, path: source });",
89
- " await writeState(state);",
90
- " process.exit(0);",
91
- " }",
92
- " if (op === 'update') process.exit(0);",
93
- " process.exit(0);",
94
- " }",
95
- "",
96
- " if (sub === 'list') {",
97
- " console.log(JSON.stringify(state.installed));",
98
- " process.exit(0);",
99
- " }",
100
- "",
101
- " if (sub === 'install') {",
102
- " const id = rest[0];",
103
- " if (!state.installed.some((i) => i.id === id)) {",
104
- " const [name, market] = id.split('@');",
105
- " const m = state.markets.find((x) => x.name === market);",
106
- " state.installed.push({ id, version: m ? (await pluginJsonVersion(m.path)) ?? '0.0.0' : '0.0.0' });",
107
- " await writeState(state);",
108
- " }",
109
- " process.exit(0);",
110
- " }",
111
- "",
112
- " if (sub === 'update') {",
113
- " const name = rest[0];",
114
- " const entry = state.installed.find((i) => i.id.startsWith(name + '@'));",
115
- " if (entry) {",
116
- " const m = state.markets.find((x) => x.name === entry.id.split('@')[1]);",
117
- " const v = m ? await pluginJsonVersion(m.path) : undefined;",
118
- " if (v) entry.version = v;",
119
- " await writeState(state);",
120
- " }",
121
- " process.exit(0);",
122
- " }",
123
- "",
124
- " process.exit(0);",
125
- "})().catch((err) => {",
126
- " console.error(err);",
127
- " process.exit(1);",
128
- "});",
129
- ].join("\n");
130
-
131
- /** Create a stateful fake `claude` binary in `dir`.
132
- * @param {string} dir Directory to hold bin, calls.log, state.json
133
- * @param {{ failWhen?: string|null }} [options] Invocations whose argv contains
134
- * failWhen exit 1
135
- * @returns {Promise<{ bin: string, log: string, state: string }>} */
136
- export async function makeFakeClaude(dir, { failWhen = null } = {}) {
137
- const bin = join(dir, "claude");
138
- const log = join(dir, "calls.log");
139
- const state = join(dir, "state.json");
140
- await writeFile(bin, FAKE_CLAUDE_SCRIPT + "\n");
141
- await chmod(bin, 0o755);
142
- await writeFile(state, JSON.stringify({ markets: [], installed: [] }));
143
- if (failWhen) await writeFile(join(dir, "failWhen.txt"), failWhen);
144
- return { bin, log, state };
145
- }
146
-
147
- /** Registration actions from a fake call log: probes and state queries dropped.
148
- * @param {string[]} lines Raw call-log lines
149
- * @returns {string[]} Action lines (marketplace add/update, install, update) */
150
- export function registrationActions(lines) {
151
- return lines.filter((c) => !c.startsWith("plugin --help") && !c.includes("--json"));
152
- }
@@ -1,132 +0,0 @@
1
- // Shared test helper: a stateful fake `pi` binary for pi's package commands.
2
- // The real CLI is a genuine external system (and its user settings must never
3
- // be touched by tests), so tests drive this fake instead. It records every
4
- // invocation to `calls.log` (argv, space-joined, one line each) and simulates
5
- // just enough real behaviour to assert registration state:
6
- //
7
- // --version → exit 0 (availability probe)
8
- // list → configured package sources
9
- // install <source> → adds <source> to settings.json `packages`, persisted
10
- // relative to the settings file's directory (what real
11
- // pi does) unless an entry already resolves to it
12
- // remove <source> → drops the matching entry; exit 1 when none matches
13
- //
14
- // Local sources are resolved the way pi resolves them: relative to the
15
- // directory holding settings.json. `failWhen` makes any invocation whose argv
16
- // contains the substring exit 1 (command failures).
17
- //
18
- // Written async throughout (the fake is a child process and the helper's own
19
- // I/O is test code) — the repo's check-no-sync lint scans test/ for *Sync.
20
-
21
- import { chmod, writeFile } from "node:fs/promises";
22
- import { join } from "node:path";
23
-
24
- const FAKE_PI_SCRIPT = [
25
- "#!/usr/bin/env node",
26
- "// Stateful fake for the `pi` package CLI (test/helpers/fake-pi.mjs).",
27
- "const fs = require('node:fs/promises');",
28
- "const path = require('node:path');",
29
- "const os = require('node:os');",
30
- "",
31
- "const dir = __dirname;",
32
- "const settingsFile = path.join(dir, 'settings.json');",
33
- "const logFile = path.join(dir, 'calls.log');",
34
- "const failWhenFile = path.join(dir, 'failWhen.txt');",
35
- "",
36
- "const expand = (p) => (p === '~' ? os.homedir() : p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p);",
37
- "const sourceOf = (entry) => (typeof entry === 'string' ? entry : entry && entry.source);",
38
- "// pi resolves a configured local source against the settings file's directory.",
39
- "const resolvedSource = (entry) => path.resolve(dir, expand(sourceOf(entry)));",
40
- "const resolvedArgument = (arg) => path.resolve(dir, expand(arg));",
41
- "",
42
- "(async () => {",
43
- " const argv = process.argv.slice(2);",
44
- " const joined = argv.join(' ');",
45
- " await fs.appendFile(logFile, joined + '\\n');",
46
- "",
47
- " const failWhen = await fs.readFile(failWhenFile, 'utf8').catch(() => null);",
48
- " if (failWhen && joined.includes(failWhen)) process.exit(1);",
49
- "",
50
- " if (argv[0] === '--version' || argv[0] === '-v') {",
51
- " console.log('1.4.0');",
52
- " process.exit(0);",
53
- " }",
54
- "",
55
- " const readSettings = async () => {",
56
- " try {",
57
- " return JSON.parse(await fs.readFile(settingsFile, 'utf8'));",
58
- " } catch {",
59
- " return {};",
60
- " }",
61
- " };",
62
- " const writePackages = async (settings, packages) =>",
63
- " fs.writeFile(settingsFile, JSON.stringify({ ...settings, packages }, null, 2) + '\\n');",
64
- "",
65
- " const [command, argument] = argv;",
66
- " const settings = await readSettings();",
67
- " const packages = settings.packages ?? [];",
68
- "",
69
- " if (command === 'list') {",
70
- " if (packages.length === 0) {",
71
- " console.log('No packages installed.');",
72
- " process.exit(0);",
73
- " }",
74
- " console.log('User packages:');",
75
- " for (const entry of packages) console.log(' ' + sourceOf(entry));",
76
- " process.exit(0);",
77
- " }",
78
- "",
79
- " if (command === 'install') {",
80
- " const target = resolvedArgument(argument);",
81
- " const persisted = path.relative(dir, target) || '.';",
82
- " const index = packages.findIndex((entry) => resolvedSource(entry) === target);",
83
- " if (index === -1) {",
84
- " packages.push(persisted);",
85
- " } else if (typeof packages[index] === 'string') {",
86
- " packages[index] = persisted;",
87
- " } else {",
88
- " packages[index] = { ...packages[index], source: persisted };",
89
- " }",
90
- " await writePackages(settings, packages);",
91
- " console.log('Installed ' + argument);",
92
- " process.exit(0);",
93
- " }",
94
- "",
95
- " if (command === 'remove') {",
96
- " const target = resolvedArgument(argument);",
97
- " const next = packages.filter((entry) => resolvedSource(entry) !== target);",
98
- " if (next.length === packages.length) {",
99
- " console.error('No matching package found for ' + argument);",
100
- " process.exit(1);",
101
- " }",
102
- " await writePackages(settings, next);",
103
- " console.log('Removed ' + argument);",
104
- " process.exit(0);",
105
- " }",
106
- "",
107
- " process.exit(0);",
108
- "})().catch((err) => {",
109
- " console.error(err);",
110
- " process.exit(1);",
111
- "});",
112
- ].join("\n");
113
-
114
- /** Create a stateful fake `pi` binary in `dir`.
115
- *
116
- * The binary's own directory doubles as pi's settings directory, so a local
117
- * package source is persisted relative to it exactly as real pi does.
118
- * @param {string} dir Directory to hold bin, calls.log, settings.json
119
- * @param {{ failWhen?: string|null, packages?: unknown[] }} [options]
120
- * failWhen: invocations whose argv contains the substring exit 1;
121
- * packages: initial `packages` array for settings.json
122
- * @returns {Promise<{ bin: string, log: string, settingsPath: string }>} */
123
- export async function makeFakePi(dir, { failWhen = null, packages = [] } = {}) {
124
- const bin = join(dir, "pi");
125
- const log = join(dir, "calls.log");
126
- const settingsPath = join(dir, "settings.json");
127
- await writeFile(bin, FAKE_PI_SCRIPT + "\n");
128
- await chmod(bin, 0o755);
129
- await writeFile(settingsPath, `${JSON.stringify({ packages }, null, 2)}\n`);
130
- if (failWhen) await writeFile(join(dir, "failWhen.txt"), failWhen);
131
- return { bin, log, settingsPath };
132
- }