@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.
- package/AGENTS.md +21 -8
- package/CHANGELOG.md +48 -0
- package/README.md +78 -38
- package/agents/README.md +5 -3
- package/bin/agent-cortex.mjs +36 -4
- package/bin/installers/claude.mjs +171 -21
- package/bin/installers/copilot.mjs +2 -17
- package/bin/installers/pi.mjs +261 -22
- package/claude/settings.json +4 -0
- package/claude-extras/scripts/statusline-command.sh +32 -4
- package/docs/claude-hooks.md +12 -0
- package/docs/ideas/ban-content-assertion-tests.md +73 -0
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
- package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
- package/docs/ideas/extension-manifests.md +55 -14
- package/docs/ideas/improve-pi-system-prompt.md +5 -2
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
- package/docs/ideas/record-idea-implementation-questions.md +68 -0
- package/docs/ideas/self-flushing-session.md +72 -0
- package/docs/ideas/state-in-beads-not-md.md +69 -0
- package/docs/ideas/task-session-context.local.md +95 -0
- package/docs/ideas/test-data-creation-helpers.md +92 -0
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
- package/docs/technical-direction/harness-config.md +161 -0
- package/docs/technical-direction/migration-execution-labels.md +60 -23
- package/extensions/agent-guardrails/README.md +45 -0
- package/extensions/agent-guardrails/guardrails.test.ts +42 -0
- package/extensions/agent-guardrails/guardrails.ts +66 -0
- package/extensions/agent-guardrails/index.ts +22 -0
- package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
- package/extensions/agent-guardrails/package.json +19 -0
- package/extensions/agent-guardrails/tsconfig.json +12 -0
- package/extensions/agent-modes/README.md +9 -3
- package/extensions/agent-modes/index.test.ts +208 -0
- package/extensions/agent-modes/index.ts +23 -6
- package/extensions/agent-modes/package.json +1 -1
- package/lib/cli.mjs +8 -2
- package/lib/pi-packages.mjs +169 -0
- package/package.json +5 -1
- package/pi/settings.json +23 -12
- package/plugin.json +1 -1
- package/scripts/lib/fs.mjs +31 -0
- package/skills/planning/classify-bead/REFERENCE.md +85 -0
- package/skills/planning/classify-bead/SKILL.md +32 -36
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
- package/skills/planning/create-task/SKILL.md +19 -10
- package/skills/planning/write-a-prd/SKILL.md +2 -2
- package/skills/style/style-code/REFERENCE.md +32 -2
- package/skills/style/style-code/SKILL.md +5 -21
- package/skills/style/style-tests/SKILL.md +69 -17
- package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
- package/test/classify-bead.test.mjs +277 -0
- package/test/cli.test.mjs +55 -11
- package/test/fs.test.mjs +76 -0
- package/test/installer.test.mjs +237 -4
- package/test/pi-installer.test.mjs +343 -4
- package/test/pi-packages.test.mjs +295 -0
- package/token-map.README.md +4 -2
- 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
|
|
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
|
|
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
|
-
##
|
|
20
|
+
## Principles
|
|
20
21
|
|
|
21
|
-
- **Test behaviour, not implementation.** A
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
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
|
|
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.
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
###
|
|
130
|
+
### Test data
|
|
83
131
|
|
|
84
132
|
| Instead of… | Write… |
|
|
85
133
|
|---|---|
|
|
86
|
-
| `const user =
|
|
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
|
-
|
|
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(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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("
|
|
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
|
}
|