@jaybeeuu/agent-cortex 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/AGENTS.md +21 -8
  2. package/CHANGELOG.md +48 -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/SKILL.md +19 -10
  56. package/skills/planning/write-a-prd/SKILL.md +2 -2
  57. package/skills/style/style-code/REFERENCE.md +32 -2
  58. package/skills/style/style-code/SKILL.md +5 -21
  59. package/skills/style/style-tests/SKILL.md +69 -17
  60. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  61. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  62. package/test/classify-bead.test.mjs +277 -0
  63. package/test/cli.test.mjs +55 -11
  64. package/test/fs.test.mjs +76 -0
  65. package/test/installer.test.mjs +237 -4
  66. package/test/pi-installer.test.mjs +343 -4
  67. package/test/pi-packages.test.mjs +295 -0
  68. package/token-map.README.md +4 -2
  69. package/token-map.json +3 -3
@@ -24,6 +24,7 @@ description: Defines this project's coding style, conventions, and architectural
24
24
  - **Validate at system edges, trust the type system within.** Inbound data from API ingress, deserialisation, or inter-service communication is untrusted until validated. Within application boundaries, trust the type system to enforce correct calls — don't re-validate at every internal module boundary.
25
25
  - **Prefer result objects for domain errors; reserve exceptions for emergencies.** Expected failures — validation errors, not-found, conflict — belong in a typed result the caller can handle. Thrown `Error` should signal something genuinely broken: a programming mistake, an invariant violation, or unrecoverable state.
26
26
  - **Deep modules beat shallow ones.** A tight public API with complexity hidden behind it is easier to use, test, and change than a flat sprawl of interdependent helpers.
27
+ - **Be a class when you are stateful.** When a module must hold mutable state and expose methods that transform or manage it, model it as a class with public methods and real private fields (`#name`) — not a factory function returning a closure of methods. Native `#private` is enforced at runtime; TypeScript's `private` modifier is compile-time only and erases away. This is orthogonal to the functional guidance elsewhere: small components, explicit props, and hooks still govern UI and pure transforms, but a stateful object is a class, and pretending otherwise is a functional idiom wrapped around an object.
27
28
  - **Abstraction must earn its keep, but local helpers are free.** An abstraction extracted into a shared module before its shape is proven nearly always guesses wrong. But extracting a local helper to clarify domain logic is always worthwhile — even at first use. The goal is readable code, not zero duplication.
28
29
 
29
30
  ## Workflow
@@ -62,6 +63,7 @@ description: Defines this project's coding style, conventions, and architectural
62
63
  | "The existing code already does it this way" | Existing patterns may predate this guide. Follow the guide unless changing it would inflate scope beyond the task. |
63
64
  | "I'll throw an Error for this validation failure" | Expected domain failures should be typed results the caller can handle. Reserve thrown `Error` for things that are truly broken. |
64
65
  | "I'll extract this into a shared util — it might be useful later" | Extract locally first if it clarifies the code. Promote to shared only when at least a third real caller emerges. Guessing the shape before then wastes time and creates coupling. |
66
+ | "A factory closure is more functional/pure" | State is state — a class names it. A closure over mutable state is an object with extra steps: the state is still there, the API is less discoverable, and nothing is gained. Use `#private` fields and public methods. |
65
67
 
66
68
  ## Cross-skill references
67
69
 
@@ -77,27 +79,8 @@ description: Defines this project's coding style, conventions, and architectural
77
79
 
78
80
  ## Examples
79
81
 
80
- See `REFERENCE.md` for the full set. The most common patterns:
81
-
82
- ### Types at boundaries
83
-
84
- | Instead of… | Write… |
85
- |---|---|
86
- | `function handleRequest(raw: any) { ... }` with no validation | `function handleRequest(raw: unknown): HandlerResult { ... }` validated at the ingress boundary |
87
- | `const user = JSON.parse(raw)` with unchecked cast | `const user = parseUser(JSON.parse(raw))` validated with `parseUser` at the deserialisation edge |
88
-
89
- ### Naming
90
-
91
- | Instead of… | Write… |
92
- |---|---|
93
- | `function process(items) { ... }` | `function resolveOverdueAccounts(accounts: Account[]) { ... }` |
94
- | `interface Config { ... }` | `interface ExportOptions { ... }` |
95
-
96
- ### Scoping
97
-
98
- | Instead of… | Write… |
99
- |---|---|
100
- | One commit fixing a bug, renaming a module, and adding an abstraction | One commit per concern, each reviewable on its own |
82
+ See `REFERENCE.md` for the full set — common patterns (types at boundaries, naming,
83
+ scoping) and the stateful-object-vs-factory pair.
101
84
 
102
85
  ## Verification checklist
103
86
 
@@ -109,5 +92,6 @@ See `REFERENCE.md` for the full set. The most common patterns:
109
92
  - [ ] Types are co-located with the module that owns them
110
93
  - [ ] Modules are split by ownership, not just for reshuffling code
111
94
  - [ ] State transitions are explicit and encapsulated behind a clear API
95
+ - [ ] Stateful modules with methods are classes using `#private` fields, not factory closures
112
96
  - [ ] Tests and docs updated where behaviour changed
113
97
  - [ ] Staged changes scanned with `review-security` before committing
@@ -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 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".
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,70 @@ 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 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.
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
+ - **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
+ - **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
+ - **Deterministic and performant.** A failing or flaky test is investigated now. Deferring is acceptable only against a top-priority bead.
30
+ - **Balanced test trophy.** Unit tests support the integration tests that carry the higher coverage — not a pyramid base.
31
+ - **Tests are independent.** No shared mutable state, no order dependence; each test passes in isolation and in any order.
32
+ - **CI is the only truth.** Locally green never counts.
33
+ - **Don't re-test types or lint.** The compiler and linter already guarantee those; repeating them adds cost for no value.
34
+ - **Testing is not optional.** Every behaviour is tested.
35
+ - **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.
36
+
37
+ ## Content-assertion ban
38
+
39
+ **No test may assert the static contents of a file** — config, markdown, terraform, or any
40
+ snapshot of static content. Comparing a file against an expected string is a content inventory,
41
+ not behaviour proof; such a test belongs to neither the suite nor the PR.
42
+
43
+ Instead, assert the behaviour the file participates in: that key data was inserted into a
44
+ template, that a compiler converted a header correctly, or run a smoke test proving deployed
45
+ services are up and ready to serve traffic.
46
+
47
+ The line is **whether the assertion proves the behaviour under test**, not whether a file is
48
+ read. A parser's golden-output test — where the expected output is a product of the behaviour —
49
+ is legitimate. `readFile(config).includes("x")` is not.
50
+
51
+ ## Test-data construction
52
+
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
+ - **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.
24
57
 
25
58
  ## Workflow
26
59
 
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.
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.
28
61
 
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.
62
+ 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
63
 
31
64
  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
65
 
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).
66
+ 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
67
 
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.
68
+ 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
69
 
37
70
  6. **Review against the checklist below.** Read the test as if you are seeing the codebase for the first time.
38
71
 
39
72
  ## Red Flags
40
73
 
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.
74
+ | Red flag | Instead |
75
+ |---|---|
76
+ | 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
+ | 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 |
79
+ | Mocking implementation details or internal module structure | Mock the genuine external boundary and test through the public interface |
80
+ | 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 |
82
+ | Sharing state via `before`/`after` hooks | Independent tests; hooks only for expensive, non-observable infrastructure |
83
+ | `any` or unsafe casts in test code | Proper types — test code is production code |
47
84
 
48
85
  ## Common Rationalizations
49
86
 
@@ -54,6 +91,8 @@ description: Defines this project's test-writing conventions — assertion strat
54
91
  | "I'll fix the flake later" | No you will not. Fix it now or delete it. |
55
92
  | "This test is too small for that pattern" | Small tests benefit from structure too. Consistency beats shortcuts. |
56
93
  | "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
+ | "The config file must match exactly this content" | You are cataloguing a file, not proving behaviour. Assert what the code does with it. |
95
+ | "A shared fixture saves setup time" | It couples tests and hides which value drives the case. Build data fresh per test. |
57
96
 
58
97
  ## Cross-skill references
59
98
 
@@ -61,10 +100,19 @@ description: Defines this project's test-writing conventions — assertion strat
61
100
  |---|---|
62
101
  | Red-green-refactor development loop | `tdd` |
63
102
  | Code-level style and conventions | `style-code` |
103
+ | Plan test rows that gate implementation | `plan` |
64
104
  | Secrets and credential scanning | `review-security` |
65
105
 
66
106
  ## Examples
67
107
 
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
+
68
116
  ### Mock boundaries
69
117
 
70
118
  | Instead of… | Write… |
@@ -79,11 +127,12 @@ description: Defines this project's test-writing conventions — assertion strat
79
127
  | `expect(result).toEqual(fullExpectedObject)` with every field asserted | `expect(result).toMatchObject({ status: "success", id: expect.any(String) })` |
80
128
  | `expect(mockSave).toHaveBeenCalledTimes(1)` and then `expect(mockSave).toHaveBeenCalledWith(data)` | Just the call-args assertion — count is redundant |
81
129
 
82
- ### Factory overrides
130
+ ### Test data
83
131
 
84
132
  | Instead of… | Write… |
85
133
  |---|---|
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 |
134
+ | `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 |
87
136
 
88
137
  ## Verification checklist
89
138
 
@@ -91,6 +140,9 @@ description: Defines this project's test-writing conventions — assertion strat
91
140
  - [ ] No mocking of internal module details — only public interfaces
92
141
  - [ ] No `before`/`after` hooks for shared test state (infrastructure setup only)
93
142
  - [ ] Assertions are targeted — no over-asserting or redundant checks
143
+ - [ ] No test asserts static file content (config, markdown, terraform, static snapshots) — assertions prove behaviour, not inventory
144
+ - [ ] 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
94
146
  - [ ] No skipped or commented-out tests
95
147
  - [ ] No flaky tests — all deterministic
96
148
  - [ ] No `any` or unsafe casts in test code
@@ -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
  }