@jaybeeuu/agent-cortex 1.40.0 → 1.41.1

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 (74) hide show
  1. package/AGENTS.md +21 -8
  2. package/CHANGELOG.md +85 -0
  3. package/README.md +78 -38
  4. package/agents/README.md +5 -3
  5. package/bin/agent-cortex.mjs +36 -4
  6. package/bin/installers/claude.mjs +171 -21
  7. package/bin/installers/copilot.mjs +2 -17
  8. package/bin/installers/pi.mjs +261 -22
  9. package/claude/settings.json +4 -0
  10. package/claude-extras/scripts/statusline-command.sh +32 -4
  11. package/docs/claude-hooks.md +12 -0
  12. package/docs/ideas/ban-content-assertion-tests.md +73 -0
  13. package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
  14. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
  15. package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
  16. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
  17. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
  18. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
  19. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
  20. package/docs/ideas/extension-manifests.md +55 -14
  21. package/docs/ideas/improve-pi-system-prompt.md +5 -2
  22. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
  23. package/docs/ideas/record-idea-implementation-questions.md +68 -0
  24. package/docs/ideas/self-flushing-session.md +72 -0
  25. package/docs/ideas/state-in-beads-not-md.md +69 -0
  26. package/docs/ideas/task-session-context.local.md +95 -0
  27. package/docs/ideas/test-data-creation-helpers.md +92 -0
  28. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
  29. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
  30. package/docs/technical-direction/harness-config.md +161 -0
  31. package/docs/technical-direction/migration-execution-labels.md +60 -23
  32. package/extensions/agent-guardrails/README.md +45 -0
  33. package/extensions/agent-guardrails/guardrails.test.ts +42 -0
  34. package/extensions/agent-guardrails/guardrails.ts +66 -0
  35. package/extensions/agent-guardrails/index.ts +22 -0
  36. package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
  37. package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
  38. package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
  39. package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
  40. package/extensions/agent-guardrails/package.json +19 -0
  41. package/extensions/agent-guardrails/tsconfig.json +12 -0
  42. package/extensions/agent-modes/README.md +9 -3
  43. package/extensions/agent-modes/index.test.ts +208 -0
  44. package/extensions/agent-modes/index.ts +23 -6
  45. package/extensions/agent-modes/package.json +1 -1
  46. package/lib/cli.mjs +8 -2
  47. package/lib/pi-packages.mjs +169 -0
  48. package/package.json +5 -1
  49. package/pi/settings.json +23 -12
  50. package/plugin.json +1 -1
  51. package/scripts/lib/fs.mjs +31 -0
  52. package/skills/planning/classify-bead/REFERENCE.md +85 -0
  53. package/skills/planning/classify-bead/SKILL.md +32 -36
  54. package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
  55. package/skills/planning/create-task/REFERENCE.md +1 -1
  56. package/skills/planning/create-task/SKILL.md +20 -11
  57. package/skills/planning/create-task/scripts/create-chores.ts +4 -0
  58. package/skills/planning/write-a-prd/SKILL.md +2 -2
  59. package/skills/style/style-code/REFERENCE.md +32 -2
  60. package/skills/style/style-code/SKILL.md +9 -21
  61. package/skills/style/style-tests/EXAMPLES.md +548 -0
  62. package/skills/style/style-tests/SKILL.md +84 -33
  63. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  64. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  65. package/test/classify-bead.test.mjs +277 -0
  66. package/test/cli.test.mjs +55 -11
  67. package/test/create-chores.test.mjs +73 -0
  68. package/test/fs.test.mjs +76 -0
  69. package/test/helpers/fake-bd.mjs +67 -0
  70. package/test/installer.test.mjs +237 -4
  71. package/test/pi-installer.test.mjs +343 -4
  72. package/test/pi-packages.test.mjs +295 -0
  73. package/token-map.README.md +4 -2
  74. package/token-map.json +3 -3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: style-tests
3
- description: Defines this project's test-writing conventions — assertion strategy, mock discipline, and what to test. 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", or deciding "what to test".
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
@@ -9,6 +9,7 @@ description: Defines this project's test-writing conventions — assertion strat
9
9
 
10
10
  - Writing new tests, reviewing existing tests, or debugging a failing test suite.
11
11
  - Deciding between unit, integration, or e2e for a given behaviour.
12
+ - Deciding what test data to build and where.
12
13
  - Asked for a "test review", "what should I test", or "is this test well-written".
13
14
 
14
15
  ## When NOT to use
@@ -16,34 +17,81 @@ description: Defines this project's test-writing conventions — assertion strat
16
17
  - The red-green-refactor development loop — use `tdd` instead.
17
18
  - Code-level style and conventions — use `style-code`.
18
19
 
19
- ## Philosophy / rationale
20
+ ## Principles
20
21
 
21
- - **Test behaviour, not implementation.** A passing test should survive a refactor that does not change observable behaviour. If a test fails after a refactor that preserved behaviour, the test was wrong.
22
- - **Prefer sociable tests over mockery.** Favour integration tests over unit tests. Reserve unit tests for complex functional logic that benefits from tight isolation. Use e2e smoke tests to prove the system hangs together. This is the testing trophy, not the testing pyramid.
23
- - **Every test has a cost.** Time to write, run, and maintain. Only write a test when its value exceeds its weight. Each behaviour should be covered at least once — ideally only once.
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
+ - **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
+ - **Unit tests at module boundaries.** Exercise the surface of a deep module through its public API, never its internals.
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
+ - **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.
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.
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.
30
+ - **Deterministic and performant.** A failing or flaky test is investigated now. Deferring is acceptable only against a top-priority bead.
31
+ - **Balanced test trophy.** Unit tests support the integration tests that carry the higher coverage — not a pyramid base.
32
+ - **Tests are independent.** No shared mutable state, no order dependence; each test passes in isolation and in any order.
33
+ - **CI is the only truth.** Locally green never counts.
34
+ - **Don't re-test types or lint.** The compiler and linter already guarantee those; repeating them adds cost for no value.
35
+ - **Testing is not optional.** Every behaviour is tested.
36
+ - **Spec-by-test.** A behaviour is done when its test row is green — the plan's test rows are the spec that gates AFK implementation.
37
+
38
+ ## Content-assertion ban
39
+
40
+ **No test may assert the static contents of a file** — config, markdown, terraform, or any
41
+ snapshot of static content. Comparing a file against an expected string is a content inventory,
42
+ not behaviour proof; such a test belongs to neither the suite nor the PR.
43
+
44
+ Instead, assert the behaviour the file participates in: that key data was inserted into a
45
+ template, that a compiler converted a header correctly, or run a smoke test proving deployed
46
+ services are up and ready to serve traffic.
47
+
48
+ The line is **whether the assertion proves the behaviour under test**, not whether a file is
49
+ read. A parser's golden-output test — where the expected output is a product of the behaviour —
50
+ is legitimate. `readFile(config).includes("x")` is not.
51
+
52
+ ## Test-data construction
53
+
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.
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.
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_.
24
61
 
25
62
  ## Workflow
26
63
 
27
- 1. **Determine the test level.** Can you test this behaviour through the module's public API or interface? Prefer integration-level tests. Reserve unit tests for complex functional logic that genuinely benefits from tight isolation. Use a small number of e2e smoke tests 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.
28
65
 
29
- 2. **Decide what to mock.** Mock as little as possible. A useful mental model: mock what is **above** the module under test in the dependency tree, not below it. At the integration level, mock only genuine external systems. At e2e, mock almost nothing.
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.
30
67
 
31
68
  3. **Write the test structure.** Use a `describe` block per module or function; use `it` per behaviour. Name tests descriptively — the name should communicate what the system does, not how. The relationship between setup, action, and assertion should be self-evident.
32
69
 
33
- 4. **Set up data.** Inline what matters — any value that is asserted on or establishes the test condition must appear inline in the test body. Use factory/builder helpers for complex objects with sensible defaults, but any value that changes the test condition must be an explicit override, never buried in a default. Avoid `before`/`after` hooks for shared state; they are acceptable only for expensive, non-observable infrastructure setup (e.g. starting a server or database).
70
+ 4. **Set up data.** Inline what matters — any value that is asserted on or establishes the test condition must appear inline in the test body. Reuse via a small configurable factory (partial overrides, sensible defaults) colocated with the tests and named for what it builds — never a shared mutable top-level fixture. Avoid `before`/`after` hooks for shared state; they are acceptable only for expensive, non-observable infrastructure setup (e.g. starting a server or database).
34
71
 
35
- 5. **Write targeted assertions.** Prefer `toMatchObject` over asserting every property. Multiple assertions are fine when they all verify the same behaviour. Avoid asserting call counts when asserting call arguments already covers the behaviour.
72
+ 5. **Write targeted assertions.** Prefer `toMatchObject` over asserting every property. Multiple assertions are fine when they all verify the same behaviour. Avoid asserting call counts when asserting call arguments already covers the behaviour. Assert behaviour the file participates in, never its static contents.
36
73
 
37
74
  6. **Review against the checklist below.** Read the test as if you are seeing the codebase for the first time.
38
75
 
39
76
  ## Red Flags
40
77
 
41
- - **Mocking implementation details.** Hooking into internal module structure ties tests to code shape, not behaviour. Mock the module's public interface instead.
42
- - **Duplicating type or lint coverage.** Types and linting already provide those guarantees. Repeating them with assertions adds maintenance cost for zero value.
43
- - **Skipping flaky tests.** A flaky test is a broken test. Fix it or delete it. Skipping normalises unreliability across the suite.
44
- - **Sharing state via hooks.** Tests must be fully independent. `before`/`after` hooks that set up shared mutable state create order-dependent failures.
45
- - **`any` or unsafe casts in test code.** Test code is production code. Be strict with types — no `any`, no unsafe casts, no cutting corners.
46
- - **Skipping a failing test.** A failing test means behaviour broke. Investigate and fix it — do not paper over it.
78
+ | Red flag | Instead |
79
+ |---|---|
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 |
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 |
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 |
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 |
88
+ | Duplicating type or lint coverage | Delete the assertion — the compiler and linter already guarantee it |
89
+ | Skipping flaky or failing tests, or leaving `.only`/focus in | Fix or delete; a skipped or focused test is a blind spot |
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 |
94
+ | `any` or unsafe casts in test code | Proper types — test code is production code |
47
95
 
48
96
  ## Common Rationalizations
49
97
 
@@ -54,6 +102,9 @@ description: Defines this project's test-writing conventions — assertion strat
54
102
  | "I'll fix the flake later" | No you will not. Fix it now or delete it. |
55
103
  | "This test is too small for that pattern" | Small tests benefit from structure too. Consistency beats shortcuts. |
56
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. |
105
+ | "The config file must match exactly this content" | You are cataloguing a file, not proving behaviour. Assert what the code does with it. |
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. |
57
108
 
58
109
  ## Cross-skill references
59
110
 
@@ -61,37 +112,37 @@ description: Defines this project's test-writing conventions — assertion strat
61
112
  |---|---|
62
113
  | Red-green-refactor development loop | `tdd` |
63
114
  | Code-level style and conventions | `style-code` |
115
+ | Plan test rows that gate implementation | `plan` |
64
116
  | Secrets and credential scanning | `review-security` |
65
117
 
66
118
  ## Examples
67
119
 
68
- ### Mock boundaries
69
-
70
- | Instead of… | Write… |
71
- |---|---|
72
- | Mocking the internal database driver to verify a query was called | Testing through the repository interface with a test double at that boundary |
73
- | Mocking `fetch` in a unit test for an API client | Testing the client with a real HTTP stub that returns controlled responses |
74
-
75
- ### 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:
76
123
 
77
124
  | Instead of… | Write… |
78
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 |
79
128
  | `expect(result).toEqual(fullExpectedObject)` with every field asserted | `expect(result).toMatchObject({ status: "success", id: expect.any(String) })` |
80
- | `expect(mockSave).toHaveBeenCalledTimes(1)` and then `expect(mockSave).toHaveBeenCalledWith(data)` | Just the call-args assertion — count is redundant |
81
-
82
- ### Factory overrides
83
-
84
- | Instead of… | Write… |
85
- |---|---|
86
- | `const user = createUser();` where the test needs `isAdmin: true` but the default happens to set it | `const user = createUser({ isAdmin: true });` — explicit override visible in the test body |
129
+ | `const user = sharedUser; user.isAdmin = true;` mutating a module-scope fixture | `const user = createUser({ isAdmin: true });` — fresh per test |
87
130
 
88
131
  ## Verification checklist
89
132
 
90
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
91
137
  - [ ] No mocking of internal module details — only public interfaces
92
138
  - [ ] No `before`/`after` hooks for shared test state (infrastructure setup only)
93
- - [ ] Assertions are targeted — no over-asserting or redundant checks
94
- - [ ] No skipped or commented-out tests
95
- - [ ] No flaky tests — all deterministic
139
+ - [ ] Assertions are targeted — no over-asserting, no tautologies, no assertion-free tests
140
+ - [ ] No test asserts static file content (config, markdown, terraform, static snapshots) — assertions prove behaviour, not inventory
141
+ - [ ] Test data is built inline or via a fresh-per-test factory; no shared mutable top-level fixture
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
96
147
  - [ ] No `any` or unsafe casts in test code
97
148
  - [ ] No duplication of type or linting coverage
@@ -24,7 +24,7 @@ Stage beads (`stage:code`, `stage:verify`, `stage:review`, `stage:document`, `st
24
24
 
25
25
  1. Run `bd prime` and hold the output; subagents can run it themselves if they need context.
26
26
  2. If no bead was specified, run `bd ready` and ask the user which to work on.
27
- 3. Claim the bead with `bd update <id> --claim` — the bead, not the prompt, is the source of truth.
27
+ 3. Claim the bead with `bd update <id> --claim` — the bead, not the prompt, is the source of truth. Append a stage-start note immediately (see _Milestone Notes_).
28
28
  4. Read the bead's `stage:*` label — it names the stage to execute.
29
29
  5. Load the universal stage-runner prompt (`skills/workflow/run-pipeline-stage/prompts/stage-runner.md`), populate `<stage>`, and read the matching playbook (`skills/workflow/run-pipeline-stage/playbooks/<stage>.md`) for stage-specific rules.
30
30
  6. Tag the stage and regenerate the progress doc so pairing sessions stay current:
@@ -88,6 +88,16 @@ Log stage starts and completes, test/lint/build/security results, blockers, and
88
88
  mkdir -p .agent-cortex/ralph && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [bead-id] [stage] message" >> .agent-cortex/ralph/ralph-bead-id.log
89
89
  ```
90
90
 
91
+ ## Milestone Notes
92
+
93
+ The bead's notes are the recovery contract: if a session dies mid-stage, a fresh agent must resume from `bd show <id>` alone. Append a short checkpoint at stage start and at each milestone — never on every tool call.
94
+
95
+ ```bash
96
+ bd update <id> --append-notes "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [<stage>] <what changed / what's next>"
97
+ ```
98
+
99
+ `--append-notes` preserves the trail; plain `--notes` overwrites it. Record the start (stage, worktree, first action), milestones (completed chunks, tests green, files changed), decisions and rejected options, blocks, and discoveries. One or two lines per entry — a scan-friendly checkpoint trail, not a transcript. No new scratch files; beads stay the single source of truth.
100
+
91
101
  ## Report Format
92
102
 
93
103
  Every subagent prompt must end with the REPORT instruction:
@@ -113,6 +123,7 @@ Subagents report facts — do not ask them to suggest or predict the next step.
113
123
  - Composing a subagent prompt without the REPORT contract — the output cannot be routed.
114
124
  - Creating a feedback bead and then dispatching anyway — bypasses the fix-round accounting and parent-child dependency graph.
115
125
  - Skipping the playbook "because the stage is simple" — playbooks carry the per-stage discipline the pipeline relies on.
126
+ - Leaving bead notes to the end of the stage — a session that dies mid-stage then loses the whole trail.
116
127
 
117
128
  ## Common Rationalizations
118
129
 
@@ -145,5 +156,6 @@ Subagents report facts — do not ask them to suggest or predict the next step.
145
156
  - [ ] Correct playbook read and followed for the stage; stage skills invoked where the playbook says.
146
157
  - [ ] Composed prompt ends with the complete REPORT contract block.
147
158
  - [ ] Progress log lines written for every stage transition and key result.
159
+ - [ ] Milestone notes appended at stage start and each milestone with `--append-notes` (not `--notes`).
148
160
  - [ ] REPORT routed per the Dispatch Rules — next stage, feedback bead, or `bd close <id>`.
149
161
  - [ ] Feedback beads carry the full BLOCKING_ISSUES content and the parent-child dependency.
@@ -14,6 +14,18 @@ Run `bd prime` if you need project-level context (conventions, tech stack, goals
14
14
  ## Progress log
15
15
  Write progress to: `.agent-cortex/ralph/ralph-<bead-id>.log`
16
16
 
17
+ ## Milestone notes
18
+ The bead's notes are the recovery contract — a fresh session must be able to resume from
19
+ `bd show <id>` alone. Append a short checkpoint before starting and at each milestone:
20
+
21
+ ```bash
22
+ bd update <id> --append-notes "[<ISO-timestamp>] [<stage>] <what changed / what's next>"
23
+ ```
24
+
25
+ Use `--append-notes` — plain `--notes` overwrites the trail. Record start, milestones, decisions
26
+ (with rejected options), blocks, and discoveries. One or two lines per entry: a checkpoint trail,
27
+ not a transcript. The REPORT below remains the authoritative stage outcome.
28
+
17
29
  ## Instructions
18
30
  1. Read and follow the playbook file listed above.
19
31
  2. Treat the bead as the source-of-truth specification (not a prompt script).
@@ -0,0 +1,277 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { access } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { resolveClassification, auditBeads, runCli } from '../skills/planning/classify-bead/scripts/classify-bead.mjs';
8
+
9
+ const testDir = dirname(fileURLToPath(import.meta.url));
10
+
11
+ describe('resolveClassification', () => {
12
+ it('resolves AFK from an existing implementation-type:afk label', () => {
13
+ const result = resolveClassification({
14
+ labels: ['implementation-type:afk'],
15
+ description: 'Do a thing.',
16
+ });
17
+
18
+ assert.equal(result.classification, 'afk');
19
+ assert.equal(result.source, 'label');
20
+ assert.equal(result.escalate, false);
21
+ });
22
+
23
+ it('resolves HITL from an existing implementation-type:hitl label', () => {
24
+ const result = resolveClassification({
25
+ labels: ['implementation-type:hitl'],
26
+ description: 'Provision the staging cluster.',
27
+ });
28
+
29
+ assert.equal(result.classification, 'hitl');
30
+ assert.equal(result.source, 'label');
31
+ assert.equal(result.escalate, false);
32
+ });
33
+
34
+ it('prefers AFK when both implementation-type labels are present', () => {
35
+ const result = resolveClassification({
36
+ labels: ['implementation-type:hitl', 'implementation-type:afk'],
37
+ description: 'Ambiguous bead.',
38
+ });
39
+
40
+ assert.equal(result.classification, 'afk');
41
+ });
42
+
43
+ it('resolves AFK from a legacy ## Type block when no label is present', () => {
44
+ const result = resolveClassification({
45
+ labels: [],
46
+ description: 'Add the export API.\n\n## Type\n\nAFK\n\n## Design\n\nSomething.',
47
+ });
48
+
49
+ assert.equal(result.classification, 'afk');
50
+ assert.equal(result.source, 'type-field');
51
+ assert.equal(result.escalate, false);
52
+ });
53
+
54
+ it('resolves HITL from a case-insensitive ## type block', () => {
55
+ const result = resolveClassification({
56
+ labels: [],
57
+ description: 'Rotate the signing key.\n\n## type\n\nhitl',
58
+ });
59
+
60
+ assert.equal(result.classification, 'hitl');
61
+ assert.equal(result.source, 'type-field');
62
+ });
63
+
64
+ it('resolves an inline ## Type: AFK field', () => {
65
+ const result = resolveClassification({
66
+ labels: [],
67
+ description: 'Tidy the logs.\n\n## Type: AFK\n',
68
+ });
69
+
70
+ assert.equal(result.classification, 'afk');
71
+ assert.equal(result.source, 'type-field');
72
+ });
73
+
74
+ it('prefers the label over a conflicting ## Type field', () => {
75
+ const result = resolveClassification({
76
+ labels: ['implementation-type:afk'],
77
+ description: 'Conflicting bead.\n\n## Type\n\nHITL',
78
+ });
79
+
80
+ assert.equal(result.classification, 'afk');
81
+ assert.equal(result.source, 'label');
82
+ });
83
+
84
+ it('escalates when the ## Type field carries no AFK/HITL value', () => {
85
+ const result = resolveClassification({
86
+ labels: [],
87
+ description: 'A feature.\n\n## Type\n\nFeature',
88
+ });
89
+
90
+ assert.equal(result.classification, null);
91
+ assert.equal(result.escalate, true);
92
+ });
93
+
94
+ it('resolves AFK from explicit no-human phrasing', () => {
95
+ const result = resolveClassification({
96
+ labels: [],
97
+ description: 'Rename the column; no human review required.',
98
+ });
99
+
100
+ assert.equal(result.classification, 'afk');
101
+ assert.equal(result.source, 'heuristic');
102
+ });
103
+
104
+ it('resolves HITL from an explicit human sign-off requirement', () => {
105
+ const result = resolveClassification({
106
+ labels: [],
107
+ description: 'Ship the redesign once it has stakeholder sign-off.',
108
+ });
109
+
110
+ assert.equal(result.classification, 'hitl');
111
+ assert.equal(result.source, 'heuristic');
112
+ });
113
+
114
+ it('lets a HITL signal outrank a generic automated-tests signal', () => {
115
+ const result = resolveClassification({
116
+ labels: [],
117
+ description: 'Has automated tests, but the release requires human approval.',
118
+ });
119
+
120
+ assert.equal(result.classification, 'hitl');
121
+ });
122
+
123
+ it('resolves AFK when the acceptance criteria are automated tests', () => {
124
+ const result = resolveClassification({
125
+ labels: [],
126
+ description: 'Cover the parser with automated tests.',
127
+ });
128
+
129
+ assert.equal(result.classification, 'afk');
130
+ });
131
+
132
+ it('escalates when no deterministic signal is present', () => {
133
+ const result = resolveClassification({
134
+ labels: [],
135
+ description: 'Rework the billing domain model to support proration.',
136
+ });
137
+
138
+ assert.equal(result.classification, null);
139
+ assert.equal(result.source, null);
140
+ assert.equal(result.escalate, true);
141
+ });
142
+
143
+ it('prefers the ## Type field over heuristic signals', () => {
144
+ const result = resolveClassification({
145
+ labels: [],
146
+ description: '## Type\n\nHITL\n\nCover it with automated tests.',
147
+ });
148
+
149
+ assert.equal(result.classification, 'hitl');
150
+ assert.equal(result.source, 'type-field');
151
+ });
152
+ });
153
+
154
+ describe('auditBeads', () => {
155
+ const sample = [
156
+ { labels: ['implementation-type:afk'], description: '' },
157
+ { labels: ['implementation-type:hitl'], description: '' },
158
+ { labels: [], description: '## Type\n\nAFK' },
159
+ { labels: [], description: 'Rework the billing domain model.' },
160
+ ];
161
+
162
+ it('counts each resolution source and the deterministic share', () => {
163
+ const summary = auditBeads(sample);
164
+
165
+ assert.deepEqual(summary.bySource, { label: 2, typeField: 1, heuristic: 0, rubric: 1 });
166
+ assert.equal(summary.deterministic, 3);
167
+ assert.equal(summary.needsRubric, 1);
168
+ assert.equal(summary.deterministicRatio, 0.75);
169
+ });
170
+
171
+ it('reports a zero ratio for an empty sample', () => {
172
+ assert.equal(auditBeads([]).deterministicRatio, 0);
173
+ });
174
+ });
175
+
176
+ describe('runCli', () => {
177
+ function makeExec(beads) {
178
+ const calls = [];
179
+ const exec = async (args) => {
180
+ calls.push(args);
181
+ if (args[0] === 'show') return JSON.stringify(beads);
182
+ if (args[0] === 'list') return JSON.stringify(beads);
183
+ if (args[0] === 'tag') return '';
184
+ throw new Error(`unexpected bd call: ${args.join(' ')}`);
185
+ };
186
+ return { exec, calls };
187
+ }
188
+
189
+ function capture() {
190
+ let output = '';
191
+ return { write: (chunk) => { output += chunk; }, read: () => JSON.parse(output) };
192
+ }
193
+
194
+ it('applies the label when the ## Type field resolves the classification', async () => {
195
+ const { exec, calls } = makeExec([{ id: 'proj-1', labels: [], description: '## Type\n\nAFK' }]);
196
+ const out = capture();
197
+
198
+ const code = await runCli({ argv: ['proj-1'], exec, write: out.write });
199
+
200
+ assert.equal(code, 0);
201
+ assert.deepEqual(calls, [
202
+ ['show', 'proj-1', '--json'],
203
+ ['tag', 'proj-1', 'implementation-type:afk'],
204
+ ]);
205
+ assert.deepEqual(out.read(), {
206
+ id: 'proj-1',
207
+ classification: 'afk',
208
+ source: 'type-field',
209
+ reason: 'legacy ## Type field declares AFK',
210
+ escalate: false,
211
+ applied: true,
212
+ label: 'implementation-type:afk',
213
+ });
214
+ });
215
+
216
+ it('does not re-apply a label that is already present', async () => {
217
+ const { exec, calls } = makeExec([{ id: 'proj-2', labels: ['implementation-type:hitl'], description: '' }]);
218
+ const out = capture();
219
+
220
+ const code = await runCli({ argv: ['proj-2'], exec, write: out.write });
221
+
222
+ assert.equal(code, 0);
223
+ assert.deepEqual(calls, [['show', 'proj-2', '--json']]);
224
+ assert.equal(out.read().applied, false);
225
+ assert.equal(out.read().classification, 'hitl');
226
+ });
227
+
228
+ it('escalates without applying a label when no deterministic signal is present', async () => {
229
+ const { exec, calls } = makeExec([{ id: 'proj-3', labels: [], description: 'Rework the billing model.' }]);
230
+ const out = capture();
231
+
232
+ const code = await runCli({ argv: ['proj-3'], exec, write: out.write });
233
+
234
+ assert.equal(code, 0);
235
+ assert.deepEqual(calls, [['show', 'proj-3', '--json']]);
236
+ assert.equal(out.read().escalate, true);
237
+ assert.equal(out.read().label, null);
238
+ });
239
+
240
+ it('resolves without applying a label under --dry-run', async () => {
241
+ const { exec, calls } = makeExec([{ id: 'proj-4', labels: [], description: '## Type\n\nHITL' }]);
242
+ const out = capture();
243
+
244
+ const code = await runCli({ argv: ['proj-4', '--dry-run'], exec, write: out.write });
245
+
246
+ assert.equal(code, 0);
247
+ assert.deepEqual(calls, [['show', 'proj-4', '--json']]);
248
+ assert.equal(out.read().classification, 'hitl');
249
+ assert.equal(out.read().applied, false);
250
+ });
251
+
252
+ it('reports the deterministic split in --audit mode', async () => {
253
+ const beads = [
254
+ { labels: ['implementation-type:afk'], description: '' },
255
+ { labels: [], description: 'Rework the billing model.' },
256
+ ];
257
+ const { exec, calls } = makeExec(beads);
258
+ const out = capture();
259
+
260
+ const code = await runCli({ argv: ['--audit', '--limit', '2'], exec, write: out.write });
261
+
262
+ assert.equal(code, 0);
263
+ assert.deepEqual(calls, [['list', '--all', '--json', '--limit', '2', '--sort', 'created']]);
264
+ assert.equal(out.read().deterministic, 1);
265
+ assert.equal(out.read().needsRubric, 1);
266
+ });
267
+ });
268
+
269
+ describe('classifier wiring', () => {
270
+ it('exposes the classifier at the sibling path callers are told to use', async () => {
271
+ // create-task/SKILL.md resolves <skill-scripts>/../../classify-bead/scripts/classify-bead.mjs.
272
+ const createTaskScripts = resolve(testDir, '../skills/planning/create-task/scripts');
273
+ const classifier = resolve(createTaskScripts, '../../classify-bead/scripts/classify-bead.mjs');
274
+
275
+ await assert.doesNotReject(access(classifier));
276
+ });
277
+ });
package/test/cli.test.mjs CHANGED
@@ -20,15 +20,21 @@ async function pathExists(p) {
20
20
  }
21
21
  }
22
22
 
23
- function runCli(args) {
23
+ /** Run the CLI; optionally pass a custom env (e.g. a fake HOME). */
24
+ function runCli(args, env = {}) {
24
25
  return new Promise((resolve) => {
25
- execFile(process.execPath, [CLI_PATH, ...args], (error, stdout, stderr) => {
26
- resolve({
27
- exitCode: error ? error.code ?? 1 : 0,
28
- stdout: stdout,
29
- stderr: stderr,
30
- });
31
- });
26
+ execFile(
27
+ process.execPath,
28
+ [CLI_PATH, ...args],
29
+ { env: { ...process.env, ...env } },
30
+ (error, stdout, stderr) => {
31
+ resolve({
32
+ exitCode: error ? error.code ?? 1 : 0,
33
+ stdout: stdout,
34
+ stderr: stderr,
35
+ });
36
+ },
37
+ );
32
38
  });
33
39
  }
34
40
 
@@ -117,6 +123,11 @@ describe("parseArgs", () => {
117
123
  assert.deepStrictEqual(result, { command: "install", harness: "pi", pluginRoot: "/tmp/plugin" });
118
124
  });
119
125
 
126
+ it("parses --no-provision for install", async () => {
127
+ const result = parseArgs(["install", "pi", "--no-provision"]);
128
+ assert.deepStrictEqual(result, { command: "install", harness: "pi", noProvision: true });
129
+ });
130
+
120
131
  it("combines multiple install options", async () => {
121
132
  const result = parseArgs(["install", "pi", "--dry-run", "--output", "/tmp/out", "--plugin-root", "/tmp/plugin"]);
122
133
  assert.deepStrictEqual(result, {
@@ -214,8 +225,21 @@ describe("CLI integration", () => {
214
225
  assert.ok(stdout.includes("claude"));
215
226
  });
216
227
 
217
- it("exits 0 for install pi", async () => {
218
- const { exitCode, stdout } = await runCli(["install", "pi"]);
228
+ it("exits 0 for install pi (into a temp output dir)", async () => {
229
+ const outDir = await mkdtemp(join(tmpdir(), "cli-pi-default-"));
230
+ try {
231
+ const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
232
+ assert.equal(exitCode, 0);
233
+ assert.ok(stdout.includes("pi"));
234
+ } finally {
235
+ await rm(outDir, { recursive: true, force: true });
236
+ }
237
+ });
238
+
239
+ it("exits 0 for install pi --no-provision", async () => {
240
+ // --no-provision keeps this hermetic: without it the CLI would install the
241
+ // third-party packages declared in the package manifest.
242
+ const { exitCode, stdout } = await runCli(["install", "pi", "--no-provision"]);
219
243
  assert.equal(exitCode, 0);
220
244
  assert.ok(stdout.includes("pi"));
221
245
  });
@@ -245,7 +269,24 @@ describe("CLI integration", () => {
245
269
  assert.ok(stdout.includes("dry-run"));
246
270
  });
247
271
 
248
- it("exits 0 for install pi --output <tmp> and writes agent files", async () => {
272
+ it("install pi --dry-run lists the pi packages it would install", async () => {
273
+ // Fake HOME keeps the pi user scope hermetic: both manifest packages are
274
+ // missing, so the plan contains exactly the declared sources.
275
+ const fakeHome = await mkdtemp(join(tmpdir(), "cli-pi-home-"));
276
+ try {
277
+ const { exitCode, stdout } = await runCli(["install", "pi", "--dry-run"], { HOME: fakeHome });
278
+ assert.equal(exitCode, 0);
279
+ assert.ok(stdout.includes("Would install pi package npm:pi-questions"), "planned pi-questions shown");
280
+ assert.ok(
281
+ stdout.includes("Would install pi package npm:pi-web-access@0.10.7"),
282
+ "planned pi-web-access shown",
283
+ );
284
+ } finally {
285
+ await rm(fakeHome, { recursive: true, force: true });
286
+ }
287
+ });
288
+
289
+ it("exits 0 for install pi --output <tmp> and writes agent and config files", async () => {
249
290
  const outDir = await mkdtemp(join(tmpdir(), "cli-pi-install-"));
250
291
  try {
251
292
  const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
@@ -253,6 +294,9 @@ describe("CLI integration", () => {
253
294
  assert.ok(stdout.includes("ralph.agent.md"));
254
295
  const ralph = await readFile(join(outDir, "agents", "ralph.agent.md"), "utf-8");
255
296
  assert.ok(ralph.includes("agent-cortex:ralph"));
297
+ const settings = JSON.parse(await readFile(join(outDir, "settings.json"), "utf-8"));
298
+ assert.ok(Array.isArray(settings.packages));
299
+ assert.ok(await readFile(join(outDir, "keybindings.json"), "utf-8"));
256
300
  } finally {
257
301
  await rm(outDir, { recursive: true, force: true });
258
302
  }