@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.
- package/AGENTS.md +21 -8
- package/CHANGELOG.md +85 -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/REFERENCE.md +1 -1
- package/skills/planning/create-task/SKILL.md +20 -11
- package/skills/planning/create-task/scripts/create-chores.ts +4 -0
- 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 +9 -21
- package/skills/style/style-tests/EXAMPLES.md +548 -0
- package/skills/style/style-tests/SKILL.md +84 -33
- 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/create-chores.test.mjs +73 -0
- package/test/fs.test.mjs +76 -0
- package/test/helpers/fake-bd.mjs +67 -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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
## Workflow
|
|
42
40
|
|
|
43
|
-
|
|
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
|
-
|
|
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. **
|
|
45
|
+
3. **Apply the rubric** — only when the classifier reports `escalate: true`. Read the full bead body:
|
|
50
46
|
|
|
51
|
-
**HITL** —
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
83
|
-
- The rubric is deterministic, so
|
|
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
|
-
- [ ]
|
|
89
|
-
- [ ]
|
|
90
|
-
- [ ]
|
|
91
|
-
- [ ]
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
50
|
+
- **AFK** — continue to step 4.
|
|
43
51
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
|
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` —
|
|
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
|
|
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` (
|
|
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
|
-
#
|
|
1
|
+
# Examples
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|