@jaybeeuu/agent-cortex 1.40.0 → 1.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/AGENTS.md +21 -8
  2. package/CHANGELOG.md +85 -0
  3. package/README.md +78 -38
  4. package/agents/README.md +5 -3
  5. package/bin/agent-cortex.mjs +36 -4
  6. package/bin/installers/claude.mjs +171 -21
  7. package/bin/installers/copilot.mjs +2 -17
  8. package/bin/installers/pi.mjs +261 -22
  9. package/claude/settings.json +4 -0
  10. package/claude-extras/scripts/statusline-command.sh +32 -4
  11. package/docs/claude-hooks.md +12 -0
  12. package/docs/ideas/ban-content-assertion-tests.md +73 -0
  13. package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
  14. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
  15. package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
  16. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
  17. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
  18. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
  19. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
  20. package/docs/ideas/extension-manifests.md +55 -14
  21. package/docs/ideas/improve-pi-system-prompt.md +5 -2
  22. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
  23. package/docs/ideas/record-idea-implementation-questions.md +68 -0
  24. package/docs/ideas/self-flushing-session.md +72 -0
  25. package/docs/ideas/state-in-beads-not-md.md +69 -0
  26. package/docs/ideas/task-session-context.local.md +95 -0
  27. package/docs/ideas/test-data-creation-helpers.md +92 -0
  28. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
  29. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
  30. package/docs/technical-direction/harness-config.md +161 -0
  31. package/docs/technical-direction/migration-execution-labels.md +60 -23
  32. package/extensions/agent-guardrails/README.md +45 -0
  33. package/extensions/agent-guardrails/guardrails.test.ts +42 -0
  34. package/extensions/agent-guardrails/guardrails.ts +66 -0
  35. package/extensions/agent-guardrails/index.ts +22 -0
  36. package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
  37. package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
  38. package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
  39. package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
  40. package/extensions/agent-guardrails/package.json +19 -0
  41. package/extensions/agent-guardrails/tsconfig.json +12 -0
  42. package/extensions/agent-modes/README.md +9 -3
  43. package/extensions/agent-modes/index.test.ts +208 -0
  44. package/extensions/agent-modes/index.ts +23 -6
  45. package/extensions/agent-modes/package.json +1 -1
  46. package/lib/cli.mjs +8 -2
  47. package/lib/pi-packages.mjs +169 -0
  48. package/package.json +5 -1
  49. package/pi/settings.json +23 -12
  50. package/plugin.json +1 -1
  51. package/scripts/lib/fs.mjs +31 -0
  52. package/skills/planning/classify-bead/REFERENCE.md +85 -0
  53. package/skills/planning/classify-bead/SKILL.md +32 -36
  54. package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
  55. package/skills/planning/create-task/REFERENCE.md +1 -1
  56. package/skills/planning/create-task/SKILL.md +20 -11
  57. package/skills/planning/create-task/scripts/create-chores.ts +4 -0
  58. package/skills/planning/write-a-prd/SKILL.md +2 -2
  59. package/skills/style/style-code/REFERENCE.md +32 -2
  60. package/skills/style/style-code/SKILL.md +9 -21
  61. package/skills/style/style-tests/EXAMPLES.md +548 -0
  62. package/skills/style/style-tests/SKILL.md +84 -33
  63. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  64. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  65. package/test/classify-bead.test.mjs +277 -0
  66. package/test/cli.test.mjs +55 -11
  67. package/test/create-chores.test.mjs +73 -0
  68. package/test/fs.test.mjs +76 -0
  69. package/test/helpers/fake-bd.mjs +67 -0
  70. package/test/installer.test.mjs +237 -4
  71. package/test/pi-installer.test.mjs +343 -4
  72. package/test/pi-packages.test.mjs +295 -0
  73. package/token-map.README.md +4 -2
  74. package/token-map.json +3 -3
@@ -0,0 +1,85 @@
1
+ # classify-bead reference
2
+
3
+ Detail behind `SKILL.md`. The executable `scripts/classify-bead.mjs` is the source of
4
+ truth for the deterministic tiers; this document mirrors it for verification.
5
+
6
+ ## Tier precedence
7
+
8
+ | Tier | `source` | Evidence | Applies the label? |
9
+ |---|---|---|---|
10
+ | 1 | `label` | An `implementation-type:afk` or `implementation-type:hitl` label is already present | no — it is already there |
11
+ | 2 | `type-field` | The legacy `## Type` section in the bead body declares `AFK`/`HITL` (block or inline form, case-insensitive) | yes |
12
+ | 3 | `heuristic` | A conservative natural-language signal (below) | yes |
13
+ | 4 | *(none)* | Nothing deterministic matched → `escalate: true`; a small/cheap model applies the rubric | after the model decides |
14
+
15
+ When both `implementation-type` labels are present, AFK wins — matching the
16
+ pre-existing skill wording, which checks `implementation-type:afk` first.
17
+
18
+ The classifier runs `bd show <id> --json` and `bd tag <id> <label>`; it never
19
+ re-reads prose from the rendered `bd show` output.
20
+
21
+ ## Heuristic signals
22
+
23
+ Heuristics only fire on explicit phrasing. They are checked in this order,
24
+ mirroring the rubric's "a human is required if any criterion is true" rule.
25
+
26
+ HITL (human required):
27
+
28
+ - `requires human`, `human sign-off|approval|judgement|decision`
29
+ - `manual step|action|intervention`
30
+ - `visually review|inspect|verify`
31
+ - `stakeholder sign-off|approval`
32
+ - `credentials|secrets setup|rotation|provisioning`
33
+ - `provisioning` within 40 characters of `infrastructure|infra|server|cluster`
34
+
35
+ AFK (explicitly no human):
36
+
37
+ - `no human action|input|review|intervention|judgement`
38
+
39
+ AFK (machine-checkable acceptance):
40
+
41
+ - `machine-checkable`
42
+ - `automated test|tests|check|checks|verification`
43
+
44
+ Anything else escalates. Widening this list trades model calls for
45
+ misclassification risk; only add phrasing that maps directly onto a rubric
46
+ criterion.
47
+
48
+ Matching is literal, not semantic, and the patterns are not negation-aware —
49
+ "no human approval is required" still matches the HITL tier. The error direction
50
+ is toward HITL, which is visible and correctable, so prefer unambiguous phrasing
51
+ over more patterns.
52
+
53
+ ## CLI
54
+
55
+ ```bash
56
+ node classify-bead.mjs <bead-id> [--dry-run] # resolve (and tag) one bead
57
+ node classify-bead.mjs --audit [--limit <n>] # measure the split over recent beads
58
+ ```
59
+
60
+ Run from the target project's directory — `bd` resolves its beads DB from cwd.
61
+ JSON goes to stdout:
62
+
63
+ - Classification: `{ id, classification, source, reason, escalate, applied, label }`
64
+ - Audit: `{ sampled, bySource, deterministic, needsRubric, deterministicRatio }`
65
+
66
+ Exit codes: `0` success (including `escalate`, which is a valid outcome), `2`
67
+ usage error, `1` `bd` failure. `BD_PATH` overrides the `bd` executable.
68
+
69
+ ## Measured split
70
+
71
+ This is the "measure first" number that sizes the win over spawning a subagent.
72
+ Reproduce it against the repo's beads DB:
73
+
74
+ ```bash
75
+ node <skill-scripts>/classify-bead.mjs --audit --limit 100
76
+ ```
77
+
78
+ Measured 2026-09-18 on `agent-cortex`: 78 of the newest 100 beads resolved
79
+ deterministically (77 from an existing label, 1 from a heuristic) and 22 needed
80
+ the rubric. That is the share of classifications that skip the model entirely
81
+ when the caller runs the classifier before spawning a subagent. The sample is
82
+ the newest 100 beads, so the ratio moves as beads are created — re-run the
83
+ command rather than quoting this snapshot. The deterministic rate is higher on
84
+ backlogs labelled at planning time; freshly created beads with neither a label
85
+ nor a `## Type` field lean on the heuristic tier.
@@ -5,7 +5,7 @@ description: Classify a bead as AFK or HITL and apply the implementation-type la
5
5
 
6
6
  # Classify Bead
7
7
 
8
- Determine whether a bead requires human action (HITL) or can be completed autonomously by an agent (AFK), then persist the result as an `implementation-type` label.
8
+ Determine whether a bead requires human action (HITL) or can be completed autonomously by an agent (AFK), then persist the result as an `implementation-type` label. A deterministic classifier settles the common cases; only genuine rubric judgement reaches a model.
9
9
 
10
10
  ## When to use
11
11
 
@@ -21,71 +21,67 @@ Determine whether a bead requires human action (HITL) or can be completed autono
21
21
 
22
22
  ## Invocation
23
23
 
24
- Run this skill as a background subagent via {{TOOL:task}} so bead content never loads into the calling agent's context. Classification is a rubric lookup, not code generation — a small, cheap model is sufficient. Pass the bead ID and the `bd prime` output; the subagent runs the workflow below and reports back the classification plus the `bd tag` command it applied.
24
+ Run the deterministic classifier first. It resolves the existing label, the legacy `## Type` field, and explicit heuristic signals without a model call:
25
25
 
26
- Only run inline when no task-spawning capability exists in the current environment.
26
+ ```bash
27
+ # Run from the TARGET project's directory — bd resolves its beads DB from cwd.
28
+ node <skill-scripts>/classify-bead.mjs <bead-id>
29
+ ```
27
30
 
28
- **Callers should check `bd label list <id>` themselves first.** If `implementation-type` is already present, skip spawning this skill entirely — spawning a subagent purely to re-read a label that already exists wastes a full agent call.
31
+ - Output carries `"classification": "afk" | "hitl"` → done. The classifier applied the label; report the classification. Do not spawn a subagent.
32
+ - Output carries `"escalate": true` → no deterministic signal. Spawn this skill as a background subagent via {{TOOL:task}} (a small, cheap model) with the bead ID and the `bd prime` output; the subagent applies the rubric below and tags the bead.
33
+ - `--dry-run` resolves without applying the label. `--audit [--limit <n>]` reports the deterministic-vs-rubric split over recent beads.
29
34
 
30
- ## Workflow
31
-
32
- 1. **Check for an existing label.**
33
-
34
- ```bash
35
- bd label list <id>
36
- ```
35
+ `<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. Only run inline when no task-spawning capability exists in the current environment.
37
36
 
38
- - `implementation-type:afk` present → classification is **AFK**. Stop here.
39
- - `implementation-type:hitl` present → classification is **HITL**. Stop here.
37
+ **Callers should run the classifier themselves before spawning this subagent.** If `implementation-type` is already present, or the classifier resolves the bead, there is no subagent call to make.
40
38
 
41
- 2. **Check the `## Type` field (back-compat).**
39
+ ## Workflow
42
40
 
43
- ```bash
44
- bd show <id>
45
- ```
41
+ 1. **Check for an existing label.** `bd label list <id>` — `implementation-type:afk` present → **AFK**; `implementation-type:hitl` present → **HITL**. Stop.
46
42
 
47
- Locate the `## Type` section in the bead body. `HITL` or `AFK` (case-insensitive) determines the classification; continue to step 4. This field predates the label system, so treat it as authoritative when present.
43
+ 2. **Run the deterministic classifier.** `node <skill-scripts>/classify-bead.mjs <id>`. A resolved classification is already tagged; stop and report it.
48
44
 
49
- 3. **Classify from first principles** — only when neither the label nor the `## Type` field is present. Read the full bead body and apply the rubric:
45
+ 3. **Apply the rubric** — only when the classifier reports `escalate: true`. Read the full bead body:
50
46
 
51
- **HITL** — the bead requires a human if at least one of the following is true:
47
+ **HITL** — a human is required if at least one of the following holds:
52
48
  - The outcome cannot be verified by an agent (e.g. visual review, stakeholder sign-off, UX judgement call).
53
49
  - A manual action only a human can perform is required (e.g. credential setup, secrets management, external service configuration, infrastructure provisioning outside the codebase).
54
50
  - A decision must be made that the agent cannot make unilaterally (e.g. architectural choice between equally valid options, regulatory or legal sign-off).
55
51
 
56
- **AFK** — the agent can implement, verify, and complete the task autonomously, and all acceptance criteria are machine-checkable.
57
-
58
- 4. **Apply the label.**
52
+ **AFK** — otherwise: the agent can implement, verify, and complete the task autonomously, and all acceptance criteria are machine-checkable.
59
53
 
60
- ```bash
61
- bd tag <id> implementation-type:afk # or implementation-type:hitl
62
- ```
54
+ 4. **Apply the label.** `bd tag <id> implementation-type:afk` (or `:hitl`). The classifier does this for the deterministic tiers; do it yourself only after step 3.
63
55
 
64
56
  5. **Return the classification.** Report `AFK` or `HITL` so the calling skill or agent can act on it.
65
57
 
58
+ Tier precedence (label → `## Type` → heuristics → rubric), the heuristic signal list, the CLI contract, and the measured deterministic split are in `REFERENCE.md`.
59
+
66
60
  ## Red Flags
67
61
 
68
- - Classifying a bead without the caller-side `bd label list` check — you either overwrite a recorded decision or waste an agent spawn re-reading a label that already exists.
62
+ - Classifying a bead without the caller-side `bd label list` check — you either overwrite a recorded decision or waste a call re-reading a label that already exists.
63
+ - Spawning the rubric subagent when the classifier returned a classification — the deterministic tier is the point.
69
64
  - Defaulting to HITL for a complex task whose acceptance criteria are machine-checkable — complexity alone is not a criterion.
70
- - Choosing a class without reading the bead body when neither the label nor `## Type` is present.
65
+ - Choosing a class without reading the bead body when the classifier escalated.
71
66
 
72
67
  ## Common Rationalizations
73
68
 
74
69
  | Rationalization | Rebuttal |
75
- |---|---|
76
70
  | "This task is complex, so it must be HITL" | Complexity is not a criterion. HITL applies only when a human is genuinely required to complete or verify. |
77
71
  | "I'll mark it HITL to be safe" | HITL consumes scarce human attention. Prefer AFK — ralph's review gates still catch agent mistakes. |
78
- | "The label is probably already there, I'll skip the check" | One `bd label list` command settles it. Skipping it either overwrites a decision or spawns an agent to re-read a label. |
72
+ | "The label is probably already there, I'll skip the check" | One `bd label list` command settles it, and the classifier checks it too. |
73
+ | "Heuristics are good enough for this one" | The classifier uses conservative, explicit signals only. If it escalated, read the body. |
79
74
 
80
75
  ## Philosophy / rationale
81
76
 
82
- - Classification gates automation against human attention: AFK beads feed ralph's autonomous pipeline, HITL beads are routed to a person. We prefer AFK because human time is the scarce resource, and later review gates still catch agent mistakes — escalate only when completion or verification genuinely requires a human.
83
- - The rubric is deterministic, so this skill is a lookup rather than deep reasoning — which is why a cheap subagent suffices and the step 1 early exit exists.
77
+ - Classification gates automation against human attention: AFK beads feed ralph's autonomous pipeline, HITL beads route to a person. Prefer AFK because human time is the scarce resource, and later review gates still catch agent mistakes.
78
+ - The rubric is deterministic, so most classifications are a lookup. The executable encodes that lookup once, and the model is reserved for beads whose evidence is genuinely ambiguous.
84
79
 
85
80
  ## Verification checklist
86
81
 
87
82
  - [ ] `bd label list <id>` was checked before any other step.
88
- - [ ] Classification followed the precedence order: existing label → `## Type` → first-principles rubric.
89
- - [ ] The full bead body was read before classifying from first principles (step 3).
90
- - [ ] `bd tag <id> implementation-type:<afk|hitl>` was applied.
91
- - [ ] The calling skill or agent received the classification in the report.
83
+ - [ ] The deterministic classifier ran before any rubric subagent.
84
+ - [ ] A rubric subagent ran only when the classifier reported `escalate: true`.
85
+ - [ ] The full bead body was read before classifying from first principles.
86
+ - [ ] `bd tag <id> implementation-type:<afk|hitl>` is present afterwards.
87
+ - [ ] The calling skill or agent received the classification in the report.
@@ -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
+ }
@@ -24,7 +24,7 @@ One per stage in `pipeline.json` (currently `code`, `verify`, `review`, `documen
24
24
 
25
25
  - **Type**: `task`
26
26
  - **Title**: `[<parent-id>] PR Review and Merge`
27
- - **Labels**: `implementation-type:hitl`, `lifecycle:feature-pr`
27
+ - **Labels**: `implementation-type:hitl`, `lifecycle:feature-pr` — exactly one `implementation-type` label. The gate declares its own labels, so the script passes `--no-inherit-labels` and it never picks up the parent's `implementation-type:afk`. Stage chores do **not** opt out; they keep inheriting `:afk`.
28
28
  - **Dependencies**: `parent-child` to the parent bead; `blocks` on the final document stage chore (`document`, falling back to the last stage in `pipeline.json`).
29
29
  - **Purpose**: human review of the agent PR (agent branch → feature branch) before the feature closes.
30
30
 
@@ -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,8 +102,8 @@ 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
- - [ ] PR gate task exists with `implementation-type:hitl` + `lifecycle:feature-pr` labels and blocks the final document stage chore.
108
+ - [ ] PR gate task exists and blocks the final document stage chore, carrying **exactly one** `implementation-type` label — `implementation-type:hitl` (never an inherited `implementation-type:afk` alongside it) — plus `lifecycle:feature-pr`.
100
109
  - [ ] Report returned the parent bead ID, classification, and all child bead IDs.
@@ -106,6 +106,10 @@ const prGateBeadId = run([
106
106
  '--priority', priority,
107
107
  '--labels', 'implementation-type:hitl,lifecycle:feature-pr',
108
108
  '--parent', parentId,
109
+ // The gate declares its own labels; opt out of bd's create-time parent-label
110
+ // inheritance so its explicit :hitl is never joined by an inherited :afk.
111
+ // Stage chores deliberately keep inheriting :afk -- do not add this there.
112
+ '--no-inherit-labels',
109
113
  '--silent',
110
114
  ]);
111
115
  run(['bd', 'dep', 'add', prGateBeadId, documentStageBeadId, '--type', 'blocks']);
@@ -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