@jaybeeuu/agent-cortex 1.39.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 (91) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +1 -1
  2. package/.github/workflows/ci.yml +47 -14
  3. package/AGENTS.md +35 -8
  4. package/CHANGELOG.md +60 -0
  5. package/README.md +174 -97
  6. package/agents/README.md +14 -11
  7. package/bin/agent-cortex.mjs +55 -10
  8. package/bin/installers/claude.mjs +674 -174
  9. package/bin/installers/copilot.mjs +13 -18
  10. package/bin/installers/pi.mjs +293 -51
  11. package/claude/settings.json +4 -0
  12. package/{claude → claude-extras}/scripts/statusline-command.sh +32 -4
  13. package/docs/claude-hooks.md +15 -3
  14. package/docs/ideas/ban-content-assertion-tests.md +73 -0
  15. package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
  16. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
  17. package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
  18. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
  19. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
  20. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
  21. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
  22. package/docs/ideas/extension-manifests.md +55 -14
  23. package/docs/ideas/improve-pi-system-prompt.md +5 -2
  24. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
  25. package/docs/ideas/record-idea-implementation-questions.md +68 -0
  26. package/docs/ideas/self-flushing-session.md +72 -0
  27. package/docs/ideas/state-in-beads-not-md.md +69 -0
  28. package/docs/ideas/task-session-context.local.md +95 -0
  29. package/docs/ideas/test-data-creation-helpers.md +92 -0
  30. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
  31. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
  32. package/docs/technical-direction/harness-config.md +161 -0
  33. package/docs/technical-direction/migration-execution-labels.md +60 -23
  34. package/extensions/agent-guardrails/README.md +45 -0
  35. package/extensions/agent-guardrails/guardrails.test.ts +42 -0
  36. package/extensions/agent-guardrails/guardrails.ts +66 -0
  37. package/extensions/agent-guardrails/index.ts +22 -0
  38. package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
  39. package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
  40. package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
  41. package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
  42. package/extensions/agent-guardrails/package.json +19 -0
  43. package/extensions/agent-guardrails/tsconfig.json +12 -0
  44. package/extensions/agent-modes/README.md +12 -5
  45. package/extensions/agent-modes/discover.test.ts +30 -7
  46. package/extensions/agent-modes/discover.ts +1 -1
  47. package/extensions/agent-modes/index.test.ts +208 -0
  48. package/extensions/agent-modes/index.ts +23 -6
  49. package/extensions/agent-modes/package.json +1 -1
  50. package/extensions/skill-stats/index.test.ts +3 -2
  51. package/lib/cli.mjs +30 -12
  52. package/lib/pi-packages.mjs +169 -0
  53. package/package.json +8 -4
  54. package/pi/settings.json +23 -12
  55. package/plugin.json +1 -1
  56. package/scripts/build-copilot-agents.mjs +1 -1
  57. package/scripts/check-no-sync.mjs +49 -0
  58. package/scripts/lib/compose-agent.mjs +32 -14
  59. package/scripts/lib/fs.mjs +31 -0
  60. package/skills/engineering/grill-with-docs/SKILL.md +1 -1
  61. package/skills/planning/classify-bead/REFERENCE.md +85 -0
  62. package/skills/planning/classify-bead/SKILL.md +32 -36
  63. package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
  64. package/skills/planning/create-task/SKILL.md +19 -10
  65. package/skills/planning/write-a-prd/SKILL.md +2 -2
  66. package/skills/style/style-code/REFERENCE.md +32 -2
  67. package/skills/style/style-code/SKILL.md +5 -21
  68. package/skills/style/style-tests/SKILL.md +69 -17
  69. package/skills/workflow/hitl-collab/SKILL.md +1 -1
  70. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  71. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  72. package/test/classify-bead.test.mjs +277 -0
  73. package/test/claude-installer.test.mjs +199 -139
  74. package/test/cli.test.mjs +95 -41
  75. package/test/copilot-installer.test.mjs +92 -82
  76. package/test/fs.test.mjs +76 -0
  77. package/test/helpers/fake-claude.mjs +152 -0
  78. package/test/installer.test.mjs +542 -57
  79. package/test/pi-installer.test.mjs +457 -102
  80. package/test/pi-packages.test.mjs +295 -0
  81. package/token-map.README.md +23 -12
  82. package/token-map.json +8 -7
  83. package/.claude-plugin/marketplace.json +0 -16
  84. package/claude/.claude-plugin/plugin.json +0 -11
  85. package/claude/agents/plan.md +0 -58
  86. package/claude/agents/ralph-plan.md +0 -121
  87. package/claude/agents/ralph.md +0 -193
  88. package/claude/agents/strategy.md +0 -111
  89. package/claude/hooks/scripts/notify.mjs +0 -54
  90. package/claude/hooks.json +0 -38
  91. /package/{claude → claude-extras}/.mcp.json +0 -0
@@ -0,0 +1,292 @@
1
+ // Deterministic bead classifier — resolves an AFK/HITL classification from
2
+ // evidence already present on a bead before any model judgement is spent.
3
+ //
4
+ // Tier order (see skills/planning/classify-bead/SKILL.md):
5
+ // 1. implementation-type label — an existing decision is authoritative
6
+ // 2. legacy ## Type field — back-compat with pre-label beads
7
+ // 3. conservative heuristics — explicit natural-language signals only
8
+ // 4. escalate — hand to the rubric (small/cheap model)
9
+ //
10
+ // Zero dependencies: run with `node classify-bead.mjs <bead-id>`.
11
+
12
+ import { execFile } from 'node:child_process';
13
+ import { promisify } from 'node:util';
14
+ import { pathToFileURL } from 'node:url';
15
+
16
+ /** @typedef {'afk' | 'hitl'} Classification */
17
+ /** @typedef {'label' | 'type-field' | 'heuristic'} ResolutionSource */
18
+
19
+ /**
20
+ * @typedef {object} Resolution
21
+ * @property {Classification | null} classification Resolved class, or null when the rubric must decide.
22
+ * @property {ResolutionSource | null} source Which tier produced the resolution.
23
+ * @property {string} reason Human-readable justification for the resolution.
24
+ * @property {boolean} escalate True when model judgement is required (tier 4).
25
+ */
26
+
27
+ const AFK_LABEL = 'implementation-type:afk';
28
+ const HITL_LABEL = 'implementation-type:hitl';
29
+
30
+ // Heuristic signals are deliberately conservative: they only fire on explicit
31
+ // natural-language phrasing that maps directly onto the rubric's criteria.
32
+ // Anything short of that escalates to the rubric rather than guessing.
33
+
34
+ // Rubric: a human is required to perform or verify the work.
35
+ const HITL_SIGNALS = [
36
+ /\brequires? (a )?human\b/i,
37
+ /\bhuman (sign-?off|approval|judgement|decision)\b/i,
38
+ /\bmanual (step|action|intervention)\b/i,
39
+ /\bvisual(ly)? (review|inspect|verify)\b/i,
40
+ /\bstakeholder (sign-?off|approval)\b/i,
41
+ /\b(credential|secret)s? (setup|rotation|provisioning)\b/i,
42
+ /\bprovision(ing)?\b[^.]{0,40}\b(infrastructure|infra|server|cluster)\b/i,
43
+ ];
44
+
45
+ // Rubric: the bead explicitly rules out human involvement.
46
+ const AFK_EXPLICIT_SIGNALS = [
47
+ /\bno human (action|input|review|intervention|judgement)\b/i,
48
+ ];
49
+
50
+ // Rubric: acceptance criteria an agent can check without a human.
51
+ const AFK_SIGNALS = [
52
+ /\bmachine-checkable\b/i,
53
+ /\bautomated (test|tests|check|checks|verification)\b/i,
54
+ ];
55
+
56
+ /** @param {RegExp[]} patterns @param {string} text */
57
+ function matchesAny(patterns, text) {
58
+ return patterns.some((pattern) => pattern.test(text));
59
+ }
60
+
61
+ /**
62
+ * @param {Classification} classification
63
+ * @param {ResolutionSource} source
64
+ * @param {string} reason
65
+ * @returns {Resolution}
66
+ */
67
+ function resolved(classification, source, reason) {
68
+ return { classification, source, reason, escalate: false };
69
+ }
70
+
71
+ /** @param {string} reason @returns {Resolution} */
72
+ function needsRubric(reason) {
73
+ return { classification: null, source: null, reason, escalate: true };
74
+ }
75
+
76
+ /**
77
+ * Read the legacy `## Type` section out of a bead description.
78
+ *
79
+ * Accepts both the block form (`## Type` then a following `AFK`/`HITL` line)
80
+ * and the inline form (`## Type: AFK`). Returns null when the section is absent
81
+ * or carries an unrelated value.
82
+ *
83
+ * @param {string} description
84
+ * @returns {Classification | null}
85
+ */
86
+ function extractTypeField(description) {
87
+ const inline = description.match(/^[ \t]*#{1,6}[ \t]*Type[ \t]*:[ \t]*(AFK|HITL)\b/im);
88
+ if (inline) return /** @type {Classification} */ (inline[1].toLowerCase());
89
+
90
+ const block = description.match(/^[ \t]*#{1,6}[ \t]*Type[ \t]*\r?\n+[ \t]*(AFK|HITL)[ \t]*(?:\r?\n|$)/im);
91
+ if (block) return /** @type {Classification} */ (block[1].toLowerCase());
92
+
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Resolve a bead's implementation type from the evidence it already carries,
98
+ * without spending a model call.
99
+ *
100
+ * @param {{ labels?: string[], description?: string }} bead
101
+ * @returns {Resolution}
102
+ */
103
+ export function resolveClassification({ labels = [], description = '' }) {
104
+ if (labels.includes(AFK_LABEL)) return resolved('afk', 'label', `${AFK_LABEL} label already present`);
105
+ if (labels.includes(HITL_LABEL)) return resolved('hitl', 'label', `${HITL_LABEL} label already present`);
106
+
107
+ const typeField = extractTypeField(description);
108
+ if (typeField) return resolved(typeField, 'type-field', `legacy ## Type field declares ${typeField.toUpperCase()}`);
109
+
110
+ // Conservative heuristics: a HITL signal wins over an AFK one, mirroring the
111
+ // rubric's "a human is required if any criterion is true" rule.
112
+ if (matchesAny(HITL_SIGNALS, description)) {
113
+ return resolved('hitl', 'heuristic', 'description states a human-required action or verification');
114
+ }
115
+ if (matchesAny(AFK_EXPLICIT_SIGNALS, description)) {
116
+ return resolved('afk', 'heuristic', 'description explicitly rules out human involvement');
117
+ }
118
+ if (matchesAny(AFK_SIGNALS, description)) {
119
+ return resolved('afk', 'heuristic', 'description states machine-checkable acceptance criteria');
120
+ }
121
+
122
+ return needsRubric('no deterministic signal; rubric judgement required');
123
+ }
124
+
125
+ /** @param {Classification} classification */
126
+ function labelFor(classification) {
127
+ return classification === 'afk' ? AFK_LABEL : HITL_LABEL;
128
+ }
129
+
130
+ /** @typedef {{ label: number, typeField: number, heuristic: number, rubric: number }} SourceCounts */
131
+
132
+ /**
133
+ * @typedef {object} AuditSummary
134
+ * @property {number} sampled
135
+ * @property {SourceCounts} bySource
136
+ * @property {number} deterministic Beads resolved without a model call.
137
+ * @property {number} needsRubric Beads that would escalate to the rubric.
138
+ * @property {number} deterministicRatio deterministic / sampled (0 when empty).
139
+ */
140
+
141
+ /**
142
+ * Measure what share of a bead sample the deterministic tiers resolve. This is
143
+ * the "measure first" number that sizes the win over spawning a subagent.
144
+ *
145
+ * @param {Array<{ labels?: string[], description?: string }>} beads
146
+ * @returns {AuditSummary}
147
+ */
148
+ export function auditBeads(beads) {
149
+ /** @type {SourceCounts} */
150
+ const bySource = { label: 0, typeField: 0, heuristic: 0, rubric: 0 };
151
+
152
+ for (const bead of beads) {
153
+ const { source } = resolveClassification(bead);
154
+ if (source === 'label') bySource.label += 1;
155
+ else if (source === 'type-field') bySource.typeField += 1;
156
+ else if (source === 'heuristic') bySource.heuristic += 1;
157
+ else bySource.rubric += 1;
158
+ }
159
+
160
+ const deterministic = bySource.label + bySource.typeField + bySource.heuristic;
161
+ return {
162
+ sampled: beads.length,
163
+ bySource,
164
+ deterministic,
165
+ needsRubric: bySource.rubric,
166
+ deterministicRatio: beads.length === 0 ? 0 : deterministic / beads.length,
167
+ };
168
+ }
169
+
170
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
171
+
172
+ const USAGE =
173
+ 'Usage: node classify-bead.mjs <bead-id> [--dry-run]\n' +
174
+ ' node classify-bead.mjs --audit [--limit <n>]';
175
+
176
+ const DEFAULT_AUDIT_LIMIT = 100;
177
+
178
+ // BD_PATH env override lets machines where bd is not on PATH pin a location.
179
+ const BD_PATH = process.env.BD_PATH || 'bd';
180
+ const execFileAsync = promisify(execFile);
181
+
182
+ class UsageError extends Error {}
183
+
184
+ /**
185
+ * @param {string[]} argv
186
+ * @returns {{ beadId: string | null, audit: boolean, limit: number, dryRun: boolean, help: boolean }}
187
+ */
188
+ function parseArgs(argv) {
189
+ const options = {
190
+ beadId: /** @type {string | null} */ (null),
191
+ audit: false,
192
+ limit: DEFAULT_AUDIT_LIMIT,
193
+ dryRun: false,
194
+ help: false,
195
+ };
196
+
197
+ for (let i = 0; i < argv.length; i++) {
198
+ const arg = argv[i];
199
+ if (arg === '--audit') {
200
+ options.audit = true;
201
+ } else if (arg === '--dry-run') {
202
+ options.dryRun = true;
203
+ } else if (arg === '--help' || arg === '-h') {
204
+ options.help = true;
205
+ } else if (arg === '--limit') {
206
+ const value = Number.parseInt(argv[++i] ?? '', 10);
207
+ if (!Number.isInteger(value) || value <= 0) throw new UsageError('--limit requires a positive integer');
208
+ options.limit = value;
209
+ } else if (arg.startsWith('-')) {
210
+ throw new UsageError(`unknown option: ${arg}`);
211
+ } else if (options.beadId) {
212
+ throw new UsageError(`unexpected argument: ${arg}`);
213
+ } else {
214
+ options.beadId = arg;
215
+ }
216
+ }
217
+
218
+ return options;
219
+ }
220
+
221
+ /** @type {(args: string[]) => Promise<string>} */
222
+ async function defaultExec(args) {
223
+ const { stdout } = await execFileAsync(BD_PATH, args, { encoding: 'utf-8' });
224
+ return stdout;
225
+ }
226
+
227
+ /**
228
+ * @param {object} options
229
+ * @param {string[]} options.argv CLI arguments (without node/script).
230
+ * @param {(args: string[]) => Promise<string>} [options.exec] bd runner; injectable for tests.
231
+ * @param {(chunk: string) => void} [options.write] stdout sink; injectable for tests.
232
+ * @returns {Promise<number>} process exit code.
233
+ */
234
+ export async function runCli({ argv, exec = defaultExec, write = (chunk) => process.stdout.write(chunk) }) {
235
+ const options = parseArgs(argv);
236
+
237
+ if (options.help) {
238
+ write(`${USAGE}\n`);
239
+ return 0;
240
+ }
241
+
242
+ if (options.audit) {
243
+ const beads = JSON.parse(
244
+ await exec(['list', '--all', '--json', '--limit', String(options.limit), '--sort', 'created']),
245
+ );
246
+ write(`${JSON.stringify(auditBeads(beads), null, 2)}\n`);
247
+ return 0;
248
+ }
249
+
250
+ if (!options.beadId) {
251
+ write(`${USAGE}\n`);
252
+ return 2;
253
+ }
254
+
255
+ const [bead] = JSON.parse(await exec(['show', options.beadId, '--json']));
256
+ if (!bead) throw new Error(`bead not found: ${options.beadId}`);
257
+
258
+ const resolution = resolveClassification(bead);
259
+ const label = resolution.classification ? labelFor(resolution.classification) : null;
260
+ const applied = label !== null && resolution.source !== 'label' && !options.dryRun;
261
+
262
+ if (applied) await exec(['tag', options.beadId, /** @type {string} */ (label)]);
263
+
264
+ write(
265
+ `${JSON.stringify(
266
+ {
267
+ id: options.beadId,
268
+ classification: resolution.classification,
269
+ source: resolution.source,
270
+ reason: resolution.reason,
271
+ escalate: resolution.escalate,
272
+ applied,
273
+ label,
274
+ },
275
+ null,
276
+ 2,
277
+ )}\n`,
278
+ );
279
+ return 0;
280
+ }
281
+
282
+ // Only run the CLI when invoked directly, so tests can import the pure tiers.
283
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
284
+ runCli({ argv: process.argv.slice(2) })
285
+ .then((code) => {
286
+ process.exitCode = code;
287
+ })
288
+ .catch((error) => {
289
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
290
+ process.exitCode = error instanceof UsageError ? 2 : 1;
291
+ });
292
+ }
@@ -33,15 +33,23 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
33
33
  bd dep add <new-id> <parent> --type parent-child
34
34
  ```
35
35
 
36
- 2. **Check for an existing classification label.** Run `bd label list <id>`. If `implementation-type` is already present, skip step 3 — re-classifying a labelled bead wastes a subagent call.
36
+ 2. **Classify deterministically.** Run the `classify-bead` classifier — it resolves an existing label, the legacy `## Type` field, and explicit heuristic signals without a model call, and applies the label itself when it resolves:
37
37
 
38
- 3. **Classify the bead.** Delegate to the `classify-bead` skill as a subagent via {{TOOL:task}} — do not classify inline. It returns `AFK` or `HITL` and applies the `implementation-type` label.
38
+ ```bash
39
+ # Run from the TARGET project's directory — bd resolves its beads DB from cwd.
40
+ node <skill-scripts>/../../classify-bead/scripts/classify-bead.mjs <new-id>
41
+ ```
42
+
43
+ - `"classification": "afk" | "hitl"` → the label is applied; branch on that class in step 3.
44
+ - `"escalate": true` → delegate to the `classify-bead` skill as a subagent via {{TOOL:task}}; it applies the rubric and tags the bead.
45
+
46
+ `<skill-scripts>` is the absolute path to this skill's own `scripts/` directory, resolved from wherever this skill was loaded. Sibling skills sit alongside this skill's directory in every harness, so `../../classify-bead/scripts/` resolves to the classifier.
39
47
 
40
- 4. **Branch on classification.**
48
+ 3. **Branch on classification.**
41
49
  - **HITL** — stop here. Report the bead ID and classification to the caller. Do not create pipeline chores: a human drives the work, so the chore tree does not apply.
42
- - **AFK** — continue to step 5.
50
+ - **AFK** — continue to step 4.
43
51
 
44
- 5. **Expand into pipeline stage chores.** Run the `create-chores` script, which reads this skill's `pipeline.json` and deterministically creates all stage chores plus the PR gate bead in one shot:
52
+ 4. **Expand into pipeline stage chores.** Run the `create-chores` script, which reads this skill's `pipeline.json` and deterministically creates all stage chores plus the PR gate bead in one shot:
45
53
 
46
54
  ```bash
47
55
  # Run from the TARGET project's directory — bd resolves its beads DB from cwd;
@@ -53,12 +61,13 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
53
61
 
54
62
  `<skill-scripts>` is the absolute path to this skill's own `scripts/` directory, resolved from wherever this skill was loaded — it varies by harness and install location, so never hardcode one harness's path.
55
63
 
56
- 6. **Report.** Return the parent bead ID, the classification (`AFK`), and the created child bead IDs from the script output (including `featurePrReview`).
64
+ 5. **Report.** Return the parent bead ID, the classification (`AFK`), and the created child bead IDs from the script output (including `featurePrReview`).
57
65
 
58
66
  ## Red Flags
59
67
 
60
68
  - **Running the script from the wrong directory.** Chores land in whatever `.beads` tree `bd` resolves from cwd — the target project's root is the only safe cwd.
61
- - **Classifying inline.** The `classify-bead` rubric is a subagent's job (with a label short-circuit); inlining yours means re-reading bead content and drifting from the rubric.
69
+ - **Spawning the `classify-bead` subagent when the classifier already resolved the bead.** The deterministic script is the short-circuit; a subagent is only for `escalate: true`.
70
+ - **Skipping the classifier and classifying by hand.** The rubric is a subagent's job; a hand-rolled guess drifts from it.
62
71
  - **Expanding an HITL bead.** If classification is HITL, stop — creating a chore tree for human-driven work is wrong.
63
72
  - **Hardcoding `<skill-scripts>`.** The install path differs per harness (plugin cache, installed-plugins, dev checkout); resolve it at runtime.
64
73
 
@@ -66,7 +75,7 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
66
75
 
67
76
  | Rationalization | Rebuttal |
68
77
  |---|---|
69
- | "I know this task is AFK — I'll skip the classify-bead call" | Classification is a rubric check against the full bead body; assumptions masked by familiarity cause mislabelled work to hit the wrong queue. |
78
+ | "I know this task is AFK — I'll skip the classifier" | The classifier resolves the label, `## Type`, and explicit signals in one command; skipping it leaves the bead unlabelled or forces a needless subagent. |
70
79
  | "I'll create the chores by hand instead of running the script" | The script is the single source of truth for titles, labels, dep chains, and the PR gate. Hand-created chores drift silently. |
71
80
  | "The pipeline is overkill for this small task" | If the work is worth tracking at all, the stage chore tree is what lets ralph execute and gate it. Not ready for that? Use `record-idea`. |
72
81
 
@@ -77,7 +86,7 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
77
86
 
78
87
  ## Cross-skill references
79
88
 
80
- - `classify-bead` — classify the new bead via subagent (step 3).
89
+ - `classify-bead` — deterministic classifier first, rubric subagent only on escalation (step 2).
81
90
 
82
91
  ## Examples
83
92
 
@@ -93,7 +102,7 @@ The full bead-property contract (titles, labels, dependencies per chore and for
93
102
 
94
103
  - [ ] Parent bead exists with the correct title, description, and priority.
95
104
  - [ ] `parent-child` dependency added when a parent epic was supplied.
96
- - [ ] `implementation-type` label present on the parent bead (`bd label list <id>`).
105
+ - [ ] `implementation-type` label present on the parent bead (`bd label list <id>`) — resolved by the classifier or, on escalation, the rubric subagent.
97
106
  - [ ] HITL path: no pipeline chores created, caller told the classification. AFK path: script ran from the target project's cwd and its JSON contains every stage ID plus `featurePrReview`.
98
107
  - [ ] Chore titles/labels/dependencies match the contract (`[<parent-id>] <stage title>`, `stage:<id>`, `parent-child` to parent, `blocks` per `dependsOn`), verified against `REFERENCE.md`.
99
108
  - [ ] PR gate task exists with `implementation-type:hitl` + `lifecycle:feature-pr` labels and blocks the final document stage chore.
@@ -25,7 +25,7 @@ description: Create a PRD through user interview, codebase exploration, and modu
25
25
  3. **Interview relentlessly.** Walk down each branch of the design tree, resolving dependencies between decisions one by one, until the user's understanding and yours are identical. Every open question at this point becomes ambiguity in the PRD.
26
26
  4. **Sketch the modules.** List the major modules to build or modify, actively looking for deep modules — ones that encapsulate rich behaviour behind a simple, stable interface testable in isolation. Confirm the list with the user, plus which modules they want tests written for.
27
27
  5. **Write the PRD.** Render the template in `FORMAT.md` (Problem Statement, Solution, User Stories, Implementation Decisions, Testing Decisions, Out of Scope, Further Notes). Make the user-story list extremely extensive so it covers all aspects of the feature.
28
- 6. **File the bead.** Create the epic with `bd create "<title>" --type epic --priority P1 --body-file -`, piping the rendered PRD to stdin, then run `classify-bead` (as a subagent, per its invocation instructions) to label it AFK or HITL.
28
+ 6. **File the bead.** Create the epic with `bd create "<title>" --type epic --priority P1 --body-file -`, piping the rendered PRD to stdin, then run the `classify-bead` classifier (`node <this-skill-dir>/../classify-bead/scripts/classify-bead.mjs <epic-id>`, run from the target project's cwd) to label it AFK or HITL. Escalate to the `classify-bead` subagent only when the classifier reports `escalate: true`.
29
29
 
30
30
  Run the steps in order. Skip a step only when the user confirms it is unnecessary (e.g. the codebase is already explored).
31
31
 
@@ -59,7 +59,7 @@ Run the steps in order. Skip a step only when the user confirms it is unnecessar
59
59
  - Use `record-idea` when the idea is not yet ready to build.
60
60
  - Use `write-a-ticket` when the spec targets an external tracker rather than a bead.
61
61
  - Run `prd-to-tasks` on the filed epic to break the PRD into an executable backlog.
62
- - Run `classify-bead` on the new epic — the repo labels every new bead AFK or HITL.
62
+ - Run the `classify-bead` classifier on the new epic — the repo labels every new bead AFK or HITL. Its `scripts/` directory sits next to this skill's directory in every harness, so `<this-skill-dir>/../classify-bead/scripts/classify-bead.mjs` resolves it. Escalate to its rubric subagent only when the classifier reports `escalate: true`.
63
63
 
64
64
  ## Examples
65
65
 
@@ -1,6 +1,36 @@
1
- # Additional Examples
1
+ # Examples
2
2
 
3
- Extended before/after pairs for less common style-code patterns.
3
+ Before/after pairs for `style-code`. The most common patterns come first; the rest are
4
+ extended pairs for less frequent cases.
5
+
6
+ ## Common patterns
7
+
8
+ ### Types at boundaries
9
+
10
+ | Instead of… | Write… |
11
+ |---|---|
12
+ | `function handleRequest(raw: any) { ... }` with no validation | `function handleRequest(raw: unknown): HandlerResult { ... }` validated at the ingress boundary |
13
+ | `const user = JSON.parse(raw)` with unchecked cast | `const user = parseUser(JSON.parse(raw))` validated with `parseUser` at the deserialisation edge |
14
+
15
+ ### Naming
16
+
17
+ | Instead of… | Write… |
18
+ |---|---|
19
+ | `function process(items) { ... }` | `function resolveOverdueAccounts(accounts: Account[]) { ... }` |
20
+ | `interface Config { ... }` | `interface ExportOptions { ... }` |
21
+
22
+ ### Scoping
23
+
24
+ | Instead of… | Write… |
25
+ |---|---|
26
+ | One commit fixing a bug, renaming a module, and adding an abstraction | One commit per concern, each reviewable on its own |
27
+
28
+ ### Stateful objects
29
+
30
+ | Instead of… | Write… |
31
+ |---|---|
32
+ | ```ts // factory closure hiding mutable state function createStepRegistry() { const steps = new Map<string, Step>(); return { register: (name, step) => steps.set(name, step), resolve: (name) => steps.get(name), }; } ``` | ```ts // a class names the state it owns class StepRegistry { #steps = new Map<string, Step>(); register(name: string, step: Step): void { this.#steps.set(name, step); } resolve(name: string): Step \| undefined { return this.#steps.get(name); } } ``` |
33
+ | ```ts class C { private steps = new Map<string, Step>(); } ``` (compile-time only — erases away) | ```ts class C { #steps = new Map<string, Step>(); } ``` (runtime-enforced native private field) |
4
34
 
5
35
  ## Validation and error handling
6
36
 
@@ -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