@jaybeeuu/agent-cortex 1.41.0 → 1.42.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 (71) hide show
  1. package/.github/workflows/ci.yml +5 -5
  2. package/CHANGELOG.md +60 -0
  3. package/agents/ralph-plan/agent.md +7 -3
  4. package/agents/ralph-plan.agent.md +7 -3
  5. package/claude-extras/scripts/statusline-command.sh +0 -0
  6. package/docs/ideas/ban-content-assertion-tests.md +3 -1
  7. package/docs/ideas/dedicated-review-tests-skill.md +71 -0
  8. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +71 -0
  9. package/package.json +5 -2
  10. package/plugin.json +1 -1
  11. package/scripts/install-instructions.sh +0 -0
  12. package/scripts/sync-plugin-version.sh +0 -0
  13. package/skills/engineering/grill-with-docs/SKILL.md +3 -2
  14. package/skills/engineering/improve-codebase-architecture/REFERENCE.md +5 -0
  15. package/skills/engineering/improve-codebase-architecture/SKILL.md +2 -1
  16. package/skills/planning/create-task/REFERENCE.md +1 -1
  17. package/skills/planning/create-task/SKILL.md +9 -4
  18. package/skills/planning/create-task/scripts/create-chores.ts +8 -1
  19. package/skills/planning/prd-to-tasks/REFERENCE.md +2 -2
  20. package/skills/planning/prd-to-tasks/SKILL.md +9 -4
  21. package/skills/planning/request-refactor-plan/FORMAT.md +5 -0
  22. package/skills/planning/request-refactor-plan/SKILL.md +3 -2
  23. package/skills/planning/spec-to-tickets/REFERENCE.md +82 -0
  24. package/skills/planning/spec-to-tickets/SKILL.md +143 -0
  25. package/skills/planning/write-a-prd/FORMAT.md +6 -0
  26. package/skills/planning/write-a-prd/SKILL.md +9 -6
  27. package/skills/planning/write-a-ticket/EXAMPLES.md +133 -0
  28. package/skills/planning/write-a-ticket/SKILL.md +77 -59
  29. package/skills/productivity/bd-tool/SKILL.md +27 -2
  30. package/skills/productivity/grill-me/SKILL.md +11 -0
  31. package/skills/review/review-security/scripts/install-gitleaks.sh +0 -0
  32. package/skills/style/style-code/SKILL.md +4 -0
  33. package/skills/style/style-tests/EXAMPLES.md +548 -0
  34. package/skills/style/style-tests/SKILL.md +34 -35
  35. package/skills/workflow/plan/SKILL.md +20 -8
  36. package/skills/workflow/ralph/REFERENCE.md +3 -1
  37. package/skills/workflow/run-pipeline-stage/SKILL.md +6 -1
  38. package/skills/workflow/using-agent-skills/SKILL.md +1 -0
  39. package/test/create-chores.test.mjs +85 -0
  40. package/test/fake-bd.test.mjs +72 -0
  41. package/test/helpers/fake-bd.mjs +87 -0
  42. package/extensions/agent-guardrails/node_modules/.bin/pi +0 -21
  43. package/extensions/agent-guardrails/node_modules/.bin/tsc +0 -21
  44. package/extensions/agent-guardrails/node_modules/.bin/tsserver +0 -21
  45. package/extensions/agent-guardrails/node_modules/.bin/tsx +0 -21
  46. package/extensions/agent-modes/node_modules/.bin/pi +0 -21
  47. package/extensions/agent-modes/node_modules/.bin/tsc +0 -21
  48. package/extensions/agent-modes/node_modules/.bin/tsserver +0 -21
  49. package/extensions/agent-modes/node_modules/.bin/tsx +0 -21
  50. package/extensions/skill-stats/node_modules/.bin/pi +0 -21
  51. package/extensions/skill-stats/node_modules/.bin/tsc +0 -21
  52. package/extensions/skill-stats/node_modules/.bin/tsserver +0 -21
  53. package/extensions/skill-stats/node_modules/.bin/tsx +0 -21
  54. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  55. package/extensions/subagent/node_modules/.bin/pi +0 -21
  56. package/extensions/subagent/node_modules/.bin/pi-ai +0 -21
  57. package/extensions/subagent/node_modules/.bin/tsc +0 -21
  58. package/extensions/subagent/node_modules/.bin/tsserver +0 -21
  59. package/extensions/subagent/node_modules/.bin/tsx +0 -21
  60. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +0 -21
  61. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +0 -21
  62. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +0 -21
  63. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  64. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +0 -21
  65. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +0 -21
  66. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +0 -21
  67. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  68. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +0 -21
  69. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +0 -21
  70. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +0 -21
  71. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: style-tests
3
- description: Defines this project's test-writing conventions — what to test, assertion strategy, mock discipline, the ban on static-content assertions, and how to construct test data. Use BEFORE writing or editing any test and when reviewing tests — consult it whenever you add or change test files rather than choosing an approach ad hoc. Also for a "test review", "mock strategy", "what should I test", or "is this test well-written".
3
+ description: Defines test-writing conventions — assertion strategy, mock discipline, the content-assertion ban, and test-data construction. Use before writing or reviewing any test, or for a "test review" or "what should I test".
4
4
  ---
5
5
 
6
6
  # Test Style
@@ -22,8 +22,9 @@ description: Defines this project's test-writing conventions — what to test, a
22
22
  - **Test behaviour, not implementation.** A test must survive a refactor that does not change observable behaviour. If it fails after such a refactor, the test was wrong.
23
23
  - **Mock last, at the edges.** Mock genuine external boundaries only. Never mock within the application/domain boundary; a real collaborator is almost always cheaper than a mock that lies.
24
24
  - **Unit tests at module boundaries.** Exercise the surface of a deep module through its public API, never its internals.
25
- - **Integration and e2e are realistic.** Exercise real service behaviour — AWS via LocalStack or TestContainers. AWS may not be the AUT, but it must still be exercised for certainty. Use a small number of smoke tests to prove the system hangs together.
25
+ - **Integration and e2e exercise real services.** Run the genuine collaborator — Postgres, Redis, a queue, object storage via TestContainers or LocalStack — never a fake that drifts from the real contract. Reserve e2e for a small number of smoke tests proving the system hangs together.
26
26
  - **No more tests than needed.** Every test earns its cost by pinpointing exactly where a failure lies. A test that cannot localise a fault adds maintenance weight without signal.
27
+ - **One behaviour per test.** A test should fail for exactly one reason; several unrelated assertions turn a failure into a debugging exercise.
27
28
  - **Test code is production code.** Linted, typechecked, properly typed — no `any`, no unsafe casts, no shortcuts in constructing data. Tolerate WETness where it leaves tests more descriptive.
28
29
  - **All setup and assertion data is visible inline.** Show exactly and only the values that explain the case; a helper with sensible defaults keeps it terse.
29
30
  - **Deterministic and performant.** A failing or flaky test is investigated now. Deferring is acceptable only against a top-priority bead.
@@ -50,14 +51,17 @@ is legitimate. `readFile(config).includes("x")` is not.
50
51
 
51
52
  ## Test-data construction
52
53
 
53
- - **Build data inline in the test** (or in a helper called from it) so the test body shows the shape of its own input.
54
+ - **Vital data is visible inline.** Any value that is asserted, derives the asserted state, or sets up the scenario under test appears in the test body — never hidden in a helper or a module-scope constant.
55
+ - **Incidental data is hidden.** Everything else takes an obvious default inside a factory, so the test body reads as only the values that matter.
54
56
  - **No shared mutable top-level fixture.** If data must be reused, produce it fresh per test via a function — a factory, not a constant. Module-scope constants are acceptable only when genuinely immutable and read-only.
55
- - **Prefer a small configurable helper** — a factory taking partial overrides with defaults for the rest — over repeated full literals. Build the helper only when reuse justifies it; a single test's data stays a plain inline object.
56
- - **Name and locate the helper conventionally** so the next agent finds it — colocated with the tests, named for what it builds.
57
+ - **Prefer a small configurable factory.** Optional partial overrides over sensible defaults beat repeated full literals. Build it when reuse justifies it; a single test's data stays a plain inline object.
58
+ - **Name and locate the factory conventionally.** Colocate it with the tests and name it for what it builds: `createNameOfType`.
59
+
60
+ Worked examples for each rule are in `EXAMPLES.md` under _Test data_.
57
61
 
58
62
  ## Workflow
59
63
 
60
- 1. **Determine the test level.** Can you test this behaviour through the module's public API or interface? Prefer integration-level tests (the trophy's higher coverage). Reserve unit tests for complex functional logic that genuinely benefits from tight isolation, always at the module boundary. Use a small number of e2e smoke tests — with real services via LocalStack/TestContainers — to prove the system hangs together.
64
+ 1. **Determine the test level.** A unit proves logic in isolation, integration proves your code against a real collaborator across one seam, and e2e proves the assembled system on a user-visible journey. Can you test this behaviour through the module's public API or interface? Prefer integration-level tests (the trophy's higher coverage). Reserve unit tests for complex functional logic that genuinely benefits from tight isolation, always at the module boundary. Use a small number of e2e smoke tests to prove the parts are wired together — not to re-prove rules already covered below.
61
65
 
62
66
  2. **Decide what to mock.** Mock as little as possible, and only at genuine external boundaries. A useful mental model: mock what is **above** the module under test in the dependency tree, not below it. At e2e, mock almost nothing.
63
67
 
@@ -75,11 +79,18 @@ is legitimate. `readFile(config).includes("x")` is not.
75
79
  |---|---|
76
80
  | Asserting static file content — an expected config/markdown/terraform string, or a snapshot of static content | Assert the behaviour the file participates in: data inserted into a template, a header compiled correctly, a deployed service ready to serve traffic |
77
81
  | Shared mutable top-level fixture reused across tests | Build fresh data per test via a factory; a module-scope constant only when genuinely immutable and read-only |
78
- | Repeated full literals for the same shape | A small configurable helper taking partial overrides with sensible defaults |
82
+ | Repeated full literals for the same shape | A small configurable factory taking partial overrides with sensible defaults |
83
+ | A test with no assertion, or one that cannot fail (`expect(result).toBeDefined()`) | Assert the observable contract — a specific value the code must produce |
84
+ | Computing the expected value with the implementation's own logic | Hard-code the expected value from a known-good case; the test must not share the implementation's bug |
79
85
  | Mocking implementation details or internal module structure | Mock the genuine external boundary and test through the public interface |
86
+ | An "integration" test that mocks the collaborator it is meant to integrate with | Run the real collaborator (TestContainers/LocalStack); a fake drifts from the real contract |
87
+ | E2E re-testing business rules already covered by unit tests | Spend e2e on wiring, migrations, and the journey — keep it to a few smoke tests |
80
88
  | Duplicating type or lint coverage | Delete the assertion — the compiler and linter already guarantee it |
81
- | Skipping flaky or failing tests | Fix or delete; a skipped test is a blind spot |
89
+ | Skipping flaky or failing tests, or leaving `.only`/focus in | Fix or delete; a skipped or focused test is a blind spot |
82
90
  | Sharing state via `before`/`after` hooks | Independent tests; hooks only for expensive, non-observable infrastructure |
91
+ | Sleeping or polling for async work to finish | Await the real completion signal; inject a clock for time-dependent logic |
92
+ | An unawaited promise in a test | `await` every async interaction so a rejection fails the test |
93
+ | Asserting log or console output | Assert the returned value or emitted event |
83
94
  | `any` or unsafe casts in test code | Proper types — test code is production code |
84
95
 
85
96
  ## Common Rationalizations
@@ -93,6 +104,7 @@ is legitimate. `readFile(config).includes("x")` is not.
93
104
  | "I'll skip this failing test and come back" | A skipped test is a blind spot. The failure is telling you something — listen to it. |
94
105
  | "The config file must match exactly this content" | You are cataloguing a file, not proving behaviour. Assert what the code does with it. |
95
106
  | "A shared fixture saves setup time" | It couples tests and hides which value drives the case. Build data fresh per test. |
107
+ | "Asserting every field is more thorough" | It makes the test break for reasons the case does not care about. Assert the fields that prove the behaviour. |
96
108
 
97
109
  ## Cross-skill references
98
110
 
@@ -105,45 +117,32 @@ is legitimate. `readFile(config).includes("x")` is not.
105
117
 
106
118
  ## Examples
107
119
 
108
- ### Behaviour over content
109
-
110
- | Instead of… | Write… |
111
- |---|---|
112
- | Reading a generated config and asserting it equals an expected string | Assert the value the code inserted into the template |
113
- | Snapshotting a markdown or terraform file's contents | Assert the behaviour the renderer/compiler produced |
114
- | Asserting a deploy artefact's static bytes | A smoke test that the deployed service is up and ready to serve traffic |
115
-
116
- ### Mock boundaries
117
-
118
- | Instead of… | Write… |
119
- |---|---|
120
- | Mocking the internal database driver to verify a query was called | Testing through the repository interface with a test double at that boundary |
121
- | Mocking `fetch` in a unit test for an API client | Testing the client with a real HTTP stub that returns controlled responses |
122
-
123
- ### Targeted assertions
120
+ The full `❌ Instead` / `✅ Write` catalogue lives in `EXAMPLES.md`: behaviour over
121
+ implementation, test data, mocking, integration and e2e, targeted assertions, structure and
122
+ naming, determinism and async, isolation, and content assertions. Headline pairs:
124
123
 
125
124
  | Instead of… | Write… |
126
125
  |---|---|
126
+ | Mocking the internal database driver to verify a query was called | Testing through the repository interface with a real in-memory implementation |
127
+ | Snapshotting a markdown or terraform file's contents | Asserting the behaviour the renderer/compiler produced |
127
128
  | `expect(result).toEqual(fullExpectedObject)` with every field asserted | `expect(result).toMatchObject({ status: "success", id: expect.any(String) })` |
128
- | `expect(mockSave).toHaveBeenCalledTimes(1)` and then `expect(mockSave).toHaveBeenCalledWith(data)` | Just the call-args assertion — count is redundant |
129
-
130
- ### Test data
131
-
132
- | Instead of… | Write… |
133
- |---|---|
134
129
  | `const user = sharedUser; user.isAdmin = true;` mutating a module-scope fixture | `const user = createUser({ isAdmin: true });` — fresh per test |
135
- | Spelling the full object literal in every test | `createUser({ isAdmin: true })` — a helper with defaults and explicit overrides |
136
130
 
137
131
  ## Verification checklist
138
132
 
139
133
  - [ ] Test verifies behaviour, not implementation structure
134
+ - [ ] Integration tests exercise the real collaborator — no mocking the engine you are integrating with
135
+ - [ ] E2E smoke tests prove wiring, migrations, and the journey — not business rules covered lower down
136
+ - [ ] Each test fails for exactly one reason, and its name states the behaviour
140
137
  - [ ] No mocking of internal module details — only public interfaces
141
138
  - [ ] No `before`/`after` hooks for shared test state (infrastructure setup only)
142
- - [ ] Assertions are targeted — no over-asserting or redundant checks
139
+ - [ ] Assertions are targeted — no over-asserting, no tautologies, no assertion-free tests
143
140
  - [ ] No test asserts static file content (config, markdown, terraform, static snapshots) — assertions prove behaviour, not inventory
144
141
  - [ ] Test data is built inline or via a fresh-per-test factory; no shared mutable top-level fixture
145
- - [ ] Reused test data uses a small configurable helper rather than repeated full literals
146
- - [ ] No skipped or commented-out tests
147
- - [ ] No flaky tests — all deterministic
142
+ - [ ] Vital data is visible inline; incidental data is defaulted in a factory
143
+ - [ ] Reused test data uses a small configurable factory rather than repeated full literals
144
+ - [ ] No skipped, focused, or commented-out tests
145
+ - [ ] No flaky tests — all deterministic, no sleeps or real clocks
146
+ - [ ] Every async interaction is awaited
148
147
  - [ ] No `any` or unsafe casts in test code
149
148
  - [ ] No duplication of type or linting coverage
@@ -47,6 +47,10 @@ Establish what needs to be built through investigation and collaborative design.
47
47
  - Technical constraints and trade-offs
48
48
  - How each change will be verified
49
49
 
50
+ The epic's required sections — `## Success Criteria` for the PRD epic — are the interview's target:
51
+ it is complete only when every required section can be filled from agreed answers. Never invent
52
+ section content; surface assumptions and confirm them with the user.
53
+
50
54
  4. Sketch the major modules that will be built or modified. Look for opportunities to extract deep modules — ones that encapsulate significant behaviour behind a simple, testable interface.
51
55
 
52
56
  5. Draft a PRD using the template from `write-a-prd`. Include:
@@ -60,8 +64,9 @@ Establish what needs to be built through investigation and collaborative design.
60
64
  7. Create an epic bead from the approved PRD:
61
65
  ```bash
62
66
  bd create "<Feature Title> — PRD" --type epic \
63
- --description "<rendered PRD as markdown>" --priority p2
67
+ --description "<rendered PRD as markdown>" --priority p2 --validate
64
68
  bd tag <epic-id> workflow:plan
69
+ bd lint <epic-id>
65
70
  ```
66
71
 
67
72
  #### Phase 1 gate
@@ -70,7 +75,7 @@ Establish what needs to be built through investigation and collaborative design.
70
75
  - [ ] Design decisions resolved through interview
71
76
  - [ ] Modules sketched and reviewed with the user
72
77
  - [ ] PRD drafted and approved by the user
73
- - [ ] Epic bead created from approved PRD
78
+ - [ ] Epic bead created from approved PRD and lints clean (`bd lint <epic-id>`)
74
79
 
75
80
  ### Phase 2 — Break into tasks
76
81
 
@@ -82,13 +87,14 @@ Take the approved PRD and produce a set of epics and tasks using tracer-bullet v
82
87
  - **Title**: short descriptive name
83
88
  - **User stories covered**: which PRD stories this phase addresses
84
89
  - **What to build**: end-to-end behaviour description
85
- - **Acceptance criteria**: verifiable outcomes
90
+ - **Success criteria**: verifiable outcomes — the epic's required `## Success Criteria`
86
91
 
87
92
  10. If the work is small enough for a single epic (one phase, a few tasks), skip epics and proceed directly to Phase 3. A single epic with a few tasks dispatches through ralph more efficiently than creating unnecessary epic hierarchy. If the work has multiple distinct phases, create epics:
88
93
  ```bash
89
94
  bd create "<Phase Title>" --type epic \
90
- --description "<epic body with scope, criteria, dependencies>" --priority <same as parent>
95
+ --description "<epic body with scope, success criteria, dependencies>" --priority <same as parent> --validate
91
96
  bd tag <epic-id> workflow:plan
97
+ bd lint <epic-id>
92
98
  bd dep add <epic-id> <parent-epic-id> --type parent-child
93
99
  ```
94
100
 
@@ -146,8 +152,13 @@ Deliver a handoff bead and instructions for running ralph.
146
152
  - **Multiple independent epics**: create a parent meta-epic that groups them:
147
153
  ```bash
148
154
  bd create "<Feature Title> — Implementation" --type epic \
149
- --description "Meta-epic grouping all phases for execution. See child epics for detail." \
150
- --priority <same>
155
+ --description "Meta-epic grouping all phases for execution. See child epics for detail.
156
+
157
+ ## Success Criteria
158
+
159
+ - [ ] All child epics complete and the feature verified end-to-end." \
160
+ --priority <same> --validate
161
+ bd lint <meta-epic>
151
162
  bd dep add <epic-1> <meta-epic> --type parent-child
152
163
  bd dep add <epic-2> <meta-epic> --type parent-child
153
164
  ```
@@ -187,13 +198,14 @@ Deliver a handoff bead and instructions for running ralph.
187
198
  #### Phase 4 gate
188
199
 
189
200
  - [ ] Handoff bead identified and tagged `workflow:ralph`
201
+ - [ ] Every handoff-path bead lints clean (`bd lint`)
190
202
  - [ ] Bead state pushed with `bd dolt push`
191
203
  - [ ] Handoff summary presented with the exact instruction to give an agent
192
204
 
193
205
  ## Phase-gate checklist
194
206
 
195
- - [ ] Phase 1: PRD drafted and approved, epic bead created
196
- - [ ] Phase 2: Task breakdown approved, epics created (if needed)
207
+ - [ ] Phase 1: PRD drafted and approved, epic bead created and validated
208
+ - [ ] Phase 2: Task breakdown approved, epics created (if needed) and validated
197
209
  - [ ] Phase 3: All tasks created, classified, expanded, tree presented
198
210
  - [ ] Phase 4: Handoff bead prepared, state pushed, summary delivered
199
211
 
@@ -79,7 +79,9 @@ When a verify or review stage fails:
79
79
  ```bash
80
80
  fix_id=$(bd create "[<parent-id>] Fix (round <N>)" --type chore \
81
81
  --priority <same as parent> \
82
- --description "<CHANGES_REQUESTED list or VERIFY_FAILURES list from the triggering REPORT>" -q)
82
+ --description "Requirements: see <parent-id> acceptance criteria (`bd show <parent-id>`).
83
+
84
+ <CHANGES_REQUESTED list or VERIFY_FAILURES list from the triggering REPORT>" -q)
83
85
  bd tag $fix_id stage:fix
84
86
  bd dep add $fix_id <parent-id> --type parent-child # fix-chore is child of parent
85
87
  ```
@@ -61,12 +61,17 @@ On stage failure, create a chore bead whose description carries the feedback so
61
61
 
62
62
  ```bash
63
63
  new_id=$(bd create "[<parent-id>] <title>" --type chore \
64
- --description "<feedback content from REPORT>" --priority <parent-priority> -q)
64
+ --description "Requirements: see <parent-id> acceptance criteria (`bd show <parent-id>`).
65
+
66
+ <feedback content from REPORT>" --priority <parent-priority> -q)
65
67
  bd tag $new_id stage:<next-stage>
66
68
  bd tag $new_id workflow:ralph
67
69
  bd dep add $new_id <parent-id> --type parent-child
68
70
  ```
69
71
 
72
+ Chore beads carry no required sections, so the feedback description must point at the
73
+ parent/requirements bead (`<parent-id>`) rather than restating its acceptance criteria.
74
+
70
75
  | Triggering outcome | Suggested title | Next stage tag | Description content |
71
76
  |---|---|---|---|
72
77
  | `OUTCOME: BLOCKED` from verify | `Fix: verify failures` | `stage:code` | Full BLOCKING_ISSUES list |
@@ -23,6 +23,7 @@ Task arrives
23
23
  ├── Have a rough idea? ─────────────────→ record-idea
24
24
  ├── New feature / PRD needed? ──────────→ write-a-prd
25
25
  ├── PRD approved, need tasks? ──────────→ prd-to-tasks
26
+ ├── Spec approved, need tickets? ───────→ spec-to-tickets
26
27
  ├── Need a single task tracked? ────────→ create-task
27
28
  ├── Need to plan a feature end-to-end? ─→ plan
28
29
  ├── Running beads / task execution? ────→ ralph
@@ -0,0 +1,85 @@
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
+ });
@@ -0,0 +1,72 @@
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
+ });
@@ -0,0 +1,87 @@
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,21 +0,0 @@
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/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.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/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.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/../@earendil-works/pi-coding-agent/dist/cli.js" "$@"
19
- else
20
- exec node "$basedir/../@earendil-works/pi-coding-agent/dist/cli.js" "$@"
21
- fi
@@ -1,21 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,21 +0,0 @@
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/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.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/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.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/../@earendil-works/pi-coding-agent/dist/cli.js" "$@"
19
- else
20
- exec node "$basedir/../@earendil-works/pi-coding-agent/dist/cli.js" "$@"
21
- fi
@@ -1,21 +0,0 @@
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