@jaybeeuu/agent-cortex 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.beads/README.md +81 -0
- package/.beads/config.yaml +1 -0
- package/.beads/issues.jsonl +236 -0
- package/.beads/metadata.json +7 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.github/workflows/ci.test.ts +217 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +36 -0
- package/.junie/guidelines.md +87 -0
- package/.junie/mcp/mcp.json +10 -0
- package/AGENTS.md +102 -0
- package/CHANGELOG.md +705 -0
- package/README.md +209 -0
- package/agents/README.md +235 -0
- package/agents/plan.agent.md +56 -0
- package/agents/ralph-plan.agent.md +119 -0
- package/agents/ralph.agent.md +305 -0
- package/agents/strategy.agent.md +109 -0
- package/agents-native/ralph.md +193 -0
- package/bin/agent-cortex.mjs +27 -0
- package/claude/.claude-plugin/plugin.json +11 -0
- package/claude/.mcp.json +18 -0
- package/claude/agents/plan.md +56 -0
- package/claude/agents/ralph-plan.md +119 -0
- package/claude/agents/ralph.md +193 -0
- package/claude/agents/strategy.md +109 -0
- package/claude/hooks.json +23 -0
- package/claude/scripts/statusline-command.sh +124 -0
- package/docs/ideas/auto-model-selection.md +75 -0
- package/docs/ideas/improve-pi-system-prompt.md +34 -0
- package/docs/ideas/model-selector.md +40 -0
- package/docs/ideas/pi-task-memory.md +34 -0
- package/docs/ideas/qa-extension.md +43 -0
- package/docs/inspirations.md +33 -0
- package/docs/research/gas-city-research.md +256 -0
- package/docs/research/skill-design-research.md +286 -0
- package/docs/research/skill-improvements-analysis.md +117 -0
- package/docs/research/skill-patterns-research.md +24 -0
- package/docs/skills/skill-anatomy.md +149 -0
- package/docs/user-preferences.md +17 -0
- package/extensions/agent-modes/index.ts +507 -0
- package/extensions/auto-name.ts +107 -0
- package/extensions/context-loader.md +35 -0
- package/extensions/context-loader.ts +65 -0
- package/extensions/lib/tiny-model.ts +212 -0
- package/extensions/notify/index.ts +293 -0
- package/extensions/session-start.ts +44 -0
- package/extensions/skill-stats/README.md +74 -0
- package/extensions/skill-stats/context-loader.test.ts +107 -0
- package/extensions/skill-stats/discover.ts +119 -0
- package/extensions/skill-stats/format.ts +13 -0
- package/extensions/skill-stats/index.test.ts +452 -0
- package/extensions/skill-stats/index.ts +222 -0
- package/extensions/skill-stats/package.json +19 -0
- package/extensions/skill-stats/pnpm-lock.yaml +329 -0
- package/extensions/skill-stats/record.ts +48 -0
- package/extensions/skill-stats/store.ts +104 -0
- package/extensions/skill-stats/tsconfig.json +12 -0
- package/extensions/subagent/README.md +175 -0
- package/extensions/subagent/agents/planner.md +37 -0
- package/extensions/subagent/agents/reviewer.md +35 -0
- package/extensions/subagent/agents/scout.md +50 -0
- package/extensions/subagent/agents/worker.md +24 -0
- package/extensions/subagent/agents.ts +126 -0
- package/extensions/subagent/index.ts +1138 -0
- package/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/extensions/subagent/prompts/implement.md +10 -0
- package/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/hooks.json +10 -0
- package/lib/cli.mjs +37 -0
- package/package.json +39 -0
- package/pi/keybindings.json +3 -0
- package/pi/settings.json +29 -0
- package/plugin.json +35 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build-claude-agents.mjs +179 -0
- package/scripts/install-instructions.sh +82 -0
- package/skills/engineering/design-an-interface/SKILL.md +94 -0
- package/skills/engineering/grill-with-docs/SKILL.md +88 -0
- package/skills/engineering/improve-codebase-architecture/REFERENCE.md +78 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/engineering/tdd/SKILL.md +150 -0
- package/skills/engineering/tdd/deep-modules.md +33 -0
- package/skills/engineering/tdd/interface-design.md +31 -0
- package/skills/engineering/tdd/mocking.md +59 -0
- package/skills/engineering/tdd/refactoring.md +10 -0
- package/skills/engineering/tdd/tests.md +61 -0
- package/skills/engineering/technical-direction/SKILL.md +86 -0
- package/skills/planning/classify-bead/SKILL.md +67 -0
- package/skills/planning/create-task/SKILL.md +96 -0
- package/skills/planning/create-task/pipeline.json +33 -0
- package/skills/planning/create-task/scripts/create-chores.ts +112 -0
- package/skills/planning/create-task/scripts/package.json +14 -0
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
- package/skills/planning/create-task/scripts/tsconfig.json +12 -0
- package/skills/planning/create-task/templates/code.md +40 -0
- package/skills/planning/create-task/templates/document.md +40 -0
- package/skills/planning/create-task/templates/fix.md +27 -0
- package/skills/planning/create-task/templates/review.md +36 -0
- package/skills/planning/create-task/templates/verify.md +43 -0
- package/skills/planning/prd-to-tasks/SKILL.md +227 -0
- package/skills/planning/record-idea/SKILL.md +62 -0
- package/skills/planning/record-idea/scripts/new-idea.sh +89 -0
- package/skills/planning/request-refactor-plan/SKILL.md +73 -0
- package/skills/planning/write-a-prd/SKILL.md +74 -0
- package/skills/planning/write-a-ticket/SKILL.md +97 -0
- package/skills/productivity/bd-tool/SKILL.md +90 -0
- package/skills/productivity/grill-me/SKILL.md +10 -0
- package/skills/productivity/handoff/SKILL.md +15 -0
- package/skills/productivity/init-beads/SKILL.md +93 -0
- package/skills/productivity/teach/GLOSSARY-FORMAT.md +35 -0
- package/skills/productivity/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/skills/productivity/teach/MISSION-FORMAT.md +31 -0
- package/skills/productivity/teach/RESOURCES-FORMAT.md +32 -0
- package/skills/productivity/teach/SKILL.md +109 -0
- package/skills/productivity/validate-agent-dir/scripts/package.json +15 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +2 -0
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +19 -0
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +317 -0
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.ts +127 -0
- package/skills/productivity/write-a-skill/SKILL.md +74 -0
- package/skills/review/maintain-agent-docs/REFERENCE.md +246 -0
- package/skills/review/maintain-agent-docs/SKILL.md +83 -0
- package/skills/review/refactor-skill/EXAMPLES.md +20 -0
- package/skills/review/refactor-skill/SKILL.md +148 -0
- package/skills/review/review-security/SKILL.md +107 -0
- package/skills/review/review-security/scripts/install-gitleaks.sh +43 -0
- package/skills/style/style-code/REFERENCE.md +38 -0
- package/skills/style/style-code/SKILL.md +113 -0
- package/skills/style/style-comms/SKILL.md +93 -0
- package/skills/style/style-documentation/SKILL.md +112 -0
- package/skills/style/style-tests/SKILL.md +97 -0
- package/skills/workflow/git-workflow/SKILL.md +85 -0
- package/skills/workflow/hitl-collab/SKILL.md +29 -0
- package/skills/workflow/plan/SKILL.md +272 -0
- package/skills/workflow/ralph/REFERENCE.md +202 -0
- package/skills/workflow/ralph/SKILL.md +105 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +151 -0
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +17 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +18 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +17 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +19 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +14 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +20 -0
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +783 -0
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +586 -0
- package/skills/workflow/run-pipeline-stage/scripts/package.json +15 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +2 -0
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +12 -0
- package/skills/workflow/using-agent-skills/SKILL.md +109 -0
- package/skills/workflow/using-agent-skills/scripts/validate-skill-structure.test.ts +73 -0
- package/test/cli.test.mjs +149 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
parseListLine,
|
|
7
|
+
parseBdList,
|
|
8
|
+
parseBdShow,
|
|
9
|
+
buildStatusBadge,
|
|
10
|
+
detectOrphanedBlocked,
|
|
11
|
+
renderMermaidGraph,
|
|
12
|
+
renderTaskTable,
|
|
13
|
+
renderOrphanedBlockedCallout,
|
|
14
|
+
fetchBeads,
|
|
15
|
+
type Bead,
|
|
16
|
+
} from './generate-progress.ts';
|
|
17
|
+
|
|
18
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function makeBead(overrides: Partial<Bead> = {}): Bead {
|
|
21
|
+
return {
|
|
22
|
+
id: 'proj-abc',
|
|
23
|
+
title: 'Do something',
|
|
24
|
+
status: 'open',
|
|
25
|
+
classification: 'afk',
|
|
26
|
+
stage: null,
|
|
27
|
+
beadType: 'task',
|
|
28
|
+
parentId: null,
|
|
29
|
+
epicId: null,
|
|
30
|
+
blockedBy: [],
|
|
31
|
+
blocks: [],
|
|
32
|
+
descriptionSummary: 'Some summary',
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const BD_LIST_OUTPUT = `○ proj-aaa ● P1 First task
|
|
38
|
+
◐ proj-bbb ● P2 Second task
|
|
39
|
+
● proj-ccc ● P1 Third task
|
|
40
|
+
✓ proj-ddd ● P3 Completed task
|
|
41
|
+
❄ proj-eee ● P2 Deferred task
|
|
42
|
+
|
|
43
|
+
--------------------------------------------------------------------------------
|
|
44
|
+
Total: 5 issues (2 open, 1 in progress)
|
|
45
|
+
|
|
46
|
+
Status: ○ open ◐ in_progress ● blocked ✓ closed ❄ deferred`;
|
|
47
|
+
|
|
48
|
+
const BD_SHOW_AFK_CODING = `◐ proj-bbb · Second task [● P2 · IN_PROGRESS]
|
|
49
|
+
Owner: Jane · Assignee: Jane · Type: task
|
|
50
|
+
Created: 2026-01-01 · Updated: 2026-01-02
|
|
51
|
+
|
|
52
|
+
DESCRIPTION
|
|
53
|
+
Implements the second task with some detail about what it does.
|
|
54
|
+
More detail here.
|
|
55
|
+
|
|
56
|
+
LABELS: implementation-type:afk, stage:coding
|
|
57
|
+
|
|
58
|
+
BLOCKS
|
|
59
|
+
← ○ proj-ccc: Third task ● P1
|
|
60
|
+
|
|
61
|
+
DEPENDS ON
|
|
62
|
+
→ ○ proj-aaa: First task ● P1`;
|
|
63
|
+
|
|
64
|
+
const BD_SHOW_HITL = `○ proj-aaa · First task [● P1 · OPEN]
|
|
65
|
+
Owner: Jane · Type: task
|
|
66
|
+
Created: 2026-01-01 · Updated: 2026-01-01
|
|
67
|
+
|
|
68
|
+
DESCRIPTION
|
|
69
|
+
Configure secrets manually.
|
|
70
|
+
|
|
71
|
+
LABELS: implementation-type:hitl
|
|
72
|
+
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const BD_SHOW_NO_LABELS = `○ proj-ccc · Third task [● P1 · BLOCKED]
|
|
76
|
+
Owner: Jane · Type: task
|
|
77
|
+
Created: 2026-01-01 · Updated: 2026-01-01
|
|
78
|
+
|
|
79
|
+
DESCRIPTION
|
|
80
|
+
Blocked task.
|
|
81
|
+
|
|
82
|
+
DEPENDS ON
|
|
83
|
+
→ ◐ proj-bbb: Second task ● P2`;
|
|
84
|
+
|
|
85
|
+
const BD_SHOW_CHORE_WITH_PARENT = `◐ proj-bbb-code · Code stage [● P2 · IN_PROGRESS]
|
|
86
|
+
Owner: Jane · Assignee: Jane · Type: chore · Parent: proj-bbb
|
|
87
|
+
Created: 2026-01-01 · Updated: 2026-01-02
|
|
88
|
+
|
|
89
|
+
DESCRIPTION
|
|
90
|
+
Coding stage for second task.
|
|
91
|
+
|
|
92
|
+
LABELS: implementation-type:afk, stage:code
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const BD_SHOW_CHORE_VERIFY = `○ proj-bbb-verify · Verify stage [● P2 · OPEN]
|
|
96
|
+
Owner: Jane · Type: chore · Parent: proj-bbb
|
|
97
|
+
Created: 2026-01-01 · Updated: 2026-01-02
|
|
98
|
+
|
|
99
|
+
DESCRIPTION
|
|
100
|
+
Verify stage for second task.
|
|
101
|
+
|
|
102
|
+
LABELS: implementation-type:afk, stage:verify
|
|
103
|
+
|
|
104
|
+
DEPENDS ON
|
|
105
|
+
→ ◐ proj-bbb-code: Code stage ● P2`;
|
|
106
|
+
|
|
107
|
+
const BD_SHOW_CHORE_REVIEW = `○ proj-bbb-review · Review stage [● P2 · OPEN]
|
|
108
|
+
Owner: Jane · Type: chore · Parent: proj-bbb
|
|
109
|
+
Created: 2026-01-01 · Updated: 2026-01-02
|
|
110
|
+
|
|
111
|
+
DESCRIPTION
|
|
112
|
+
Review stage for second task.
|
|
113
|
+
|
|
114
|
+
LABELS: implementation-type:afk, stage:review
|
|
115
|
+
|
|
116
|
+
DEPENDS ON
|
|
117
|
+
→ ○ proj-bbb-verify: Verify stage ● P2`;
|
|
118
|
+
|
|
119
|
+
// ─── Behavior 1: parse bd list line ──────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe('parseListLine', () => {
|
|
122
|
+
it('parses an open bead', () => {
|
|
123
|
+
const result = parseListLine('○ proj-aaa ● P1 First task');
|
|
124
|
+
assert.deepEqual(result, { id: 'proj-aaa', status: 'open', title: 'First task' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('parses an in-progress bead', () => {
|
|
128
|
+
const result = parseListLine('◐ proj-bbb ● P2 Second task');
|
|
129
|
+
assert.deepEqual(result, { id: 'proj-bbb', status: 'in_progress', title: 'Second task' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('parses a blocked bead', () => {
|
|
133
|
+
const result = parseListLine('● proj-ccc ● P1 Third task');
|
|
134
|
+
assert.deepEqual(result, { id: 'proj-ccc', status: 'blocked', title: 'Third task' });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('parses a closed bead', () => {
|
|
138
|
+
const result = parseListLine('✓ proj-ddd ● P3 Completed task');
|
|
139
|
+
assert.deepEqual(result, { id: 'proj-ddd', status: 'closed', title: 'Completed task' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('parses a deferred bead', () => {
|
|
143
|
+
const result = parseListLine('❄ proj-eee ● P2 Deferred task');
|
|
144
|
+
assert.deepEqual(result, { id: 'proj-eee', status: 'deferred', title: 'Deferred task' });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns null for separator/summary lines', () => {
|
|
148
|
+
assert.equal(parseListLine('---'), null);
|
|
149
|
+
assert.equal(parseListLine('Total: 5 issues'), null);
|
|
150
|
+
assert.equal(parseListLine(''), null);
|
|
151
|
+
assert.equal(parseListLine('Status: ○ open'), null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('handles leading whitespace', () => {
|
|
155
|
+
const result = parseListLine(' ○ proj-aaa ● P1 First task');
|
|
156
|
+
assert.deepEqual(result, { id: 'proj-aaa', status: 'open', title: 'First task' });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── Behavior 2: parse bd list output (multiple lines) ───────────────────────
|
|
161
|
+
|
|
162
|
+
describe('parseBdList', () => {
|
|
163
|
+
it('extracts all bead entries from full bd list output', () => {
|
|
164
|
+
const results = parseBdList(BD_LIST_OUTPUT);
|
|
165
|
+
assert.equal(results.length, 5);
|
|
166
|
+
assert.deepEqual(results[0], { id: 'proj-aaa', status: 'open', title: 'First task' });
|
|
167
|
+
assert.deepEqual(results[1], { id: 'proj-bbb', status: 'in_progress', title: 'Second task' });
|
|
168
|
+
assert.deepEqual(results[2], { id: 'proj-ccc', status: 'blocked', title: 'Third task' });
|
|
169
|
+
assert.deepEqual(results[3], { id: 'proj-ddd', status: 'closed', title: 'Completed task' });
|
|
170
|
+
assert.deepEqual(results[4], { id: 'proj-eee', status: 'deferred', title: 'Deferred task' });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns empty array for empty output', () => {
|
|
174
|
+
assert.deepEqual(parseBdList('No issues found.'), []);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ─── Behavior 3: parse bd show output ────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe('parseBdShow', () => {
|
|
181
|
+
it('extracts afk classification and coding stage', () => {
|
|
182
|
+
const result = parseBdShow(BD_SHOW_AFK_CODING);
|
|
183
|
+
assert.equal(result.classification, 'afk');
|
|
184
|
+
assert.equal(result.stage, 'coding');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('extracts hitl classification', () => {
|
|
188
|
+
const result = parseBdShow(BD_SHOW_HITL);
|
|
189
|
+
assert.equal(result.classification, 'hitl');
|
|
190
|
+
assert.equal(result.stage, null);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('defaults to unknown classification when no label', () => {
|
|
194
|
+
const result = parseBdShow(BD_SHOW_NO_LABELS);
|
|
195
|
+
assert.equal(result.classification, 'unknown');
|
|
196
|
+
assert.equal(result.stage, null);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('extracts blocks list', () => {
|
|
200
|
+
const result = parseBdShow(BD_SHOW_AFK_CODING);
|
|
201
|
+
assert.deepEqual(result.blocks, ['proj-ccc']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('extracts blockedBy (depends on) list', () => {
|
|
205
|
+
const result = parseBdShow(BD_SHOW_AFK_CODING);
|
|
206
|
+
assert.deepEqual(result.blockedBy, ['proj-aaa']);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('extracts blockedBy from show with only depends-on section', () => {
|
|
210
|
+
const result = parseBdShow(BD_SHOW_NO_LABELS);
|
|
211
|
+
assert.deepEqual(result.blockedBy, ['proj-bbb']);
|
|
212
|
+
assert.deepEqual(result.blocks, []);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('extracts description summary (first lines)', () => {
|
|
216
|
+
const result = parseBdShow(BD_SHOW_AFK_CODING);
|
|
217
|
+
assert.ok(result.descriptionSummary.includes('Implements the second task'));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('handles all stage values', () => {
|
|
221
|
+
for (const stage of ['reviewing', 'fixing', 'documenting'] as const) {
|
|
222
|
+
const output = `LABELS: implementation-type:afk, stage:${stage}`;
|
|
223
|
+
const result = parseBdShow(output);
|
|
224
|
+
assert.equal(result.stage, stage);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('parses epicId from epic: label', () => {
|
|
229
|
+
const output = `○ proj-child · Child task [● P2 · OPEN]
|
|
230
|
+
Owner: Jane · Type: task
|
|
231
|
+
Created: 2026-01-01 · Updated: 2026-01-01
|
|
232
|
+
|
|
233
|
+
DESCRIPTION
|
|
234
|
+
Child task description.
|
|
235
|
+
|
|
236
|
+
LABELS: implementation-type:afk, epic:proj-epic`;
|
|
237
|
+
const result = parseBdShow(output);
|
|
238
|
+
assert.equal(result.epicId, 'proj-epic');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('leaves epicId null when no epic: label', () => {
|
|
242
|
+
const result = parseBdShow(BD_SHOW_HITL);
|
|
243
|
+
assert.equal(result.epicId, null);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('extracts parentId from header line', () => {
|
|
247
|
+
const result = parseBdShow(BD_SHOW_CHORE_WITH_PARENT);
|
|
248
|
+
assert.equal(result.parentId, 'proj-bbb');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('extracts beadType from header line', () => {
|
|
252
|
+
const result = parseBdShow(BD_SHOW_CHORE_WITH_PARENT);
|
|
253
|
+
assert.equal(result.beadType, 'chore');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('defaults beadType to task and parentId to null', () => {
|
|
257
|
+
const result = parseBdShow(BD_SHOW_AFK_CODING);
|
|
258
|
+
assert.equal(result.beadType, 'task');
|
|
259
|
+
assert.equal(result.parentId, null);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('handles new stage values (code, verify, review, document, fix)', () => {
|
|
263
|
+
for (const stage of ['code', 'verify', 'review', 'document', 'fix'] as const) {
|
|
264
|
+
const output = `LABELS: implementation-type:afk, stage:${stage}`;
|
|
265
|
+
const result = parseBdShow(output);
|
|
266
|
+
assert.equal(result.stage, stage);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ─── Behavior 4: build status badge ──────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
describe('buildStatusBadge', () => {
|
|
274
|
+
it('open afk bead → ⏳🤖', () => {
|
|
275
|
+
const bead = makeBead({ status: 'open', classification: 'afk', stage: null });
|
|
276
|
+
assert.equal(buildStatusBadge(bead), '⏳🤖');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('in_progress afk bead with coding stage → ▶️🤖🔨', () => {
|
|
280
|
+
const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'coding' });
|
|
281
|
+
assert.equal(buildStatusBadge(bead), '▶️🤖🔨');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('in_progress hitl reviewing → ▶️🙋👁', () => {
|
|
285
|
+
const bead = makeBead({ status: 'in_progress', classification: 'hitl', stage: 'reviewing' });
|
|
286
|
+
assert.equal(buildStatusBadge(bead), '▶️🙋👁');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('blocked unknown → 🔒', () => {
|
|
290
|
+
const bead = makeBead({ status: 'blocked', classification: 'unknown', stage: null });
|
|
291
|
+
assert.equal(buildStatusBadge(bead), '🔒');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('closed afk → ✅🤖', () => {
|
|
295
|
+
const bead = makeBead({ status: 'closed', classification: 'afk', stage: null });
|
|
296
|
+
assert.equal(buildStatusBadge(bead), '✅🤖');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('deferred hitl → ❌🙋', () => {
|
|
300
|
+
const bead = makeBead({ status: 'deferred', classification: 'hitl', stage: null });
|
|
301
|
+
assert.equal(buildStatusBadge(bead), '❌🙋');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('sub-status not shown when not in_progress', () => {
|
|
305
|
+
// Even if stage is set, sub-status only shows for in_progress
|
|
306
|
+
const bead = makeBead({ status: 'open', classification: 'afk', stage: 'coding' });
|
|
307
|
+
assert.equal(buildStatusBadge(bead), '⏳🤖');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('in_progress with new stage code → ▶️🤖🔨', () => {
|
|
311
|
+
const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'code' });
|
|
312
|
+
assert.equal(buildStatusBadge(bead), '▶️🤖🔨');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('in_progress with new stage verify → ▶️🤖🧪', () => {
|
|
316
|
+
const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'verify' });
|
|
317
|
+
assert.equal(buildStatusBadge(bead), '▶️🤖🧪');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('in_progress with new stage review → ▶️🤖👁', () => {
|
|
321
|
+
const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'review' });
|
|
322
|
+
assert.equal(buildStatusBadge(bead), '▶️🤖👁');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('in_progress with new stage document → ▶️🤖📝', () => {
|
|
326
|
+
const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'document' });
|
|
327
|
+
assert.equal(buildStatusBadge(bead), '▶️🤖📝');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('in_progress with new stage fix → ▶️🤖🔧', () => {
|
|
331
|
+
const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'fix' });
|
|
332
|
+
assert.equal(buildStatusBadge(bead), '▶️🤖🔧');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ─── Behavior 5: detect orphaned blocked beads ────────────────────────────────
|
|
337
|
+
|
|
338
|
+
describe('detectOrphanedBlocked', () => {
|
|
339
|
+
it('returns empty when no blocked beads', () => {
|
|
340
|
+
const beads = [makeBead({ status: 'open' }), makeBead({ id: 'proj-b', status: 'closed' })];
|
|
341
|
+
assert.deepEqual(detectOrphanedBlocked(beads), []);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('returns blocked bead when all its deps are closed', () => {
|
|
345
|
+
const beads: Bead[] = [
|
|
346
|
+
makeBead({ id: 'proj-a', status: 'closed' }),
|
|
347
|
+
makeBead({ id: 'proj-b', status: 'blocked', blockedBy: ['proj-a'] }),
|
|
348
|
+
];
|
|
349
|
+
const orphaned = detectOrphanedBlocked(beads);
|
|
350
|
+
assert.equal(orphaned.length, 1);
|
|
351
|
+
assert.equal(orphaned[0].id, 'proj-b');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('does not return blocked bead when some deps are still open', () => {
|
|
355
|
+
const beads: Bead[] = [
|
|
356
|
+
makeBead({ id: 'proj-a', status: 'open' }),
|
|
357
|
+
makeBead({ id: 'proj-b', status: 'blocked', blockedBy: ['proj-a'] }),
|
|
358
|
+
];
|
|
359
|
+
assert.deepEqual(detectOrphanedBlocked(beads), []);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('returns blocked bead with no blockedBy entries (no tracked deps)', () => {
|
|
363
|
+
const beads: Bead[] = [makeBead({ id: 'proj-b', status: 'blocked', blockedBy: [] })];
|
|
364
|
+
const orphaned = detectOrphanedBlocked(beads);
|
|
365
|
+
assert.equal(orphaned.length, 1);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ─── Behavior 6: render Mermaid graph ────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
describe('renderMermaidGraph', () => {
|
|
372
|
+
it('opens and closes mermaid code block', () => {
|
|
373
|
+
const output = renderMermaidGraph([]);
|
|
374
|
+
assert.ok(output.startsWith('```mermaid\ngraph TD'));
|
|
375
|
+
assert.ok(output.endsWith('```'));
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('includes a node for each incomplete bead', () => {
|
|
379
|
+
const beads = [
|
|
380
|
+
makeBead({ id: 'proj-aaa', title: 'Task A', status: 'open' }),
|
|
381
|
+
makeBead({ id: 'proj-bbb', title: 'Task B', status: 'in_progress' }),
|
|
382
|
+
];
|
|
383
|
+
const output = renderMermaidGraph(beads);
|
|
384
|
+
assert.ok(output.includes('proj_aaa'));
|
|
385
|
+
assert.ok(output.includes('proj_bbb'));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('excludes closed beads from graph', () => {
|
|
389
|
+
const beads = [
|
|
390
|
+
makeBead({ id: 'proj-aaa', status: 'open' }),
|
|
391
|
+
makeBead({ id: 'proj-bbb', status: 'closed' }),
|
|
392
|
+
];
|
|
393
|
+
const output = renderMermaidGraph(beads);
|
|
394
|
+
assert.ok(output.includes('proj_aaa'));
|
|
395
|
+
assert.ok(!output.includes('proj_bbb'));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('renders dependency edges between incomplete beads', () => {
|
|
399
|
+
const beads: Bead[] = [
|
|
400
|
+
makeBead({ id: 'proj-aaa', status: 'open', blocks: ['proj-bbb'] }),
|
|
401
|
+
makeBead({ id: 'proj-bbb', status: 'open', blockedBy: ['proj-aaa'] }),
|
|
402
|
+
];
|
|
403
|
+
const output = renderMermaidGraph(beads);
|
|
404
|
+
assert.ok(output.includes('proj_aaa --> proj_bbb'));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('does not render edge to closed bead', () => {
|
|
408
|
+
const beads: Bead[] = [
|
|
409
|
+
makeBead({ id: 'proj-aaa', status: 'open', blocks: ['proj-bbb'] }),
|
|
410
|
+
makeBead({ id: 'proj-bbb', status: 'closed' }),
|
|
411
|
+
];
|
|
412
|
+
const output = renderMermaidGraph(beads);
|
|
413
|
+
assert.ok(!output.includes('proj_aaa --> proj_bbb'));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('wraps beads with epicId in a subgraph', () => {
|
|
417
|
+
const beads: Bead[] = [
|
|
418
|
+
makeBead({ id: 'proj-epic', title: 'The Epic', status: 'open' }),
|
|
419
|
+
makeBead({ id: 'proj-child', title: 'Child task', status: 'open', epicId: 'proj-epic' }),
|
|
420
|
+
];
|
|
421
|
+
const output = renderMermaidGraph(beads);
|
|
422
|
+
assert.ok(output.includes('subgraph sg_proj_epic'));
|
|
423
|
+
assert.ok(output.includes('proj_child'));
|
|
424
|
+
// Epic with visible children is represented by its subgraph — no standalone node
|
|
425
|
+
assert.ok(!output.includes('\n proj_epic['));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('renders epic as standalone node when it has no visible children', () => {
|
|
429
|
+
const beads: Bead[] = [
|
|
430
|
+
makeBead({ id: 'proj-epic', title: '[epic] The Epic', status: 'open' }),
|
|
431
|
+
];
|
|
432
|
+
const output = renderMermaidGraph(beads);
|
|
433
|
+
assert.ok(output.includes('proj_epic'));
|
|
434
|
+
assert.ok(!output.includes('subgraph'));
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('collapses nodes beyond 5 at the same depth into a summary node', () => {
|
|
438
|
+
// 6 independent root nodes (no deps), all at depth 0
|
|
439
|
+
const beads: Bead[] = [
|
|
440
|
+
makeBead({ id: 'proj-a1', status: 'open', classification: 'afk' }),
|
|
441
|
+
makeBead({ id: 'proj-a2', status: 'open', classification: 'afk' }),
|
|
442
|
+
makeBead({ id: 'proj-a3', status: 'open', classification: 'hitl' }),
|
|
443
|
+
makeBead({ id: 'proj-a4', status: 'open', classification: 'afk' }),
|
|
444
|
+
makeBead({ id: 'proj-a5', status: 'open', classification: 'afk' }),
|
|
445
|
+
makeBead({ id: 'proj-a6', status: 'open', classification: 'hitl' }),
|
|
446
|
+
];
|
|
447
|
+
const output = renderMermaidGraph(beads);
|
|
448
|
+
// First 5 rendered normally
|
|
449
|
+
assert.ok(output.includes('proj_a1'));
|
|
450
|
+
assert.ok(output.includes('proj_a2'));
|
|
451
|
+
assert.ok(output.includes('proj_a3'));
|
|
452
|
+
assert.ok(output.includes('proj_a4'));
|
|
453
|
+
assert.ok(output.includes('proj_a5'));
|
|
454
|
+
// 6th collapsed into summary; proj_a6 should not appear as standalone node
|
|
455
|
+
assert.ok(!output.includes('proj_a6'));
|
|
456
|
+
// Summary node: 1 collapsed, 0 afk, 1 hitl
|
|
457
|
+
assert.ok(output.includes('+1 more (0 🤖, 1 🙋)'));
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('summary node inherits edges to children of collapsed nodes', () => {
|
|
461
|
+
// 6 root nodes; the 6th (proj-a6) blocks proj-child
|
|
462
|
+
const beads: Bead[] = [
|
|
463
|
+
makeBead({ id: 'proj-a1', status: 'open', classification: 'afk' }),
|
|
464
|
+
makeBead({ id: 'proj-a2', status: 'open', classification: 'afk' }),
|
|
465
|
+
makeBead({ id: 'proj-a3', status: 'open', classification: 'afk' }),
|
|
466
|
+
makeBead({ id: 'proj-a4', status: 'open', classification: 'afk' }),
|
|
467
|
+
makeBead({ id: 'proj-a5', status: 'open', classification: 'afk' }),
|
|
468
|
+
makeBead({ id: 'proj-a6', status: 'open', classification: 'afk', blocks: ['proj-child'] }),
|
|
469
|
+
makeBead({ id: 'proj-child', status: 'open', classification: 'afk', blockedBy: ['proj-a6'] }),
|
|
470
|
+
];
|
|
471
|
+
const output = renderMermaidGraph(beads);
|
|
472
|
+
// proj-child is at depth 1 → visible
|
|
473
|
+
assert.ok(output.includes('proj_child'));
|
|
474
|
+
// Summary node should edge to proj-child
|
|
475
|
+
assert.ok(output.includes('summary_d0 --> proj_child'));
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('renders parent task as subgraph containing stage children', () => {
|
|
479
|
+
const beads: Bead[] = [
|
|
480
|
+
makeBead({ id: 'proj-parent', title: 'Parent task', status: 'in_progress', beadType: 'task' }),
|
|
481
|
+
makeBead({ id: 'proj-child1', title: 'Code', status: 'closed', beadType: 'chore', parentId: 'proj-parent', stage: 'code' }),
|
|
482
|
+
makeBead({ id: 'proj-child2', title: 'Review', status: 'in_progress', beadType: 'chore', parentId: 'proj-parent', stage: 'review' }),
|
|
483
|
+
];
|
|
484
|
+
const output = renderMermaidGraph(beads);
|
|
485
|
+
// Parent rendered as subgraph (children are incomplete, so proj-child2 is visible)
|
|
486
|
+
assert.ok(output.includes('subgraph sg_proj_parent'));
|
|
487
|
+
// Child2 (in_progress) appears inside subgraph
|
|
488
|
+
assert.ok(output.includes('proj_child2'));
|
|
489
|
+
// Closed child1 excluded from graph entirely
|
|
490
|
+
assert.ok(!output.includes('proj_child1'));
|
|
491
|
+
assert.ok(output.includes('end'));
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('renders parent as normal node when all children are closed', () => {
|
|
495
|
+
const beads: Bead[] = [
|
|
496
|
+
makeBead({ id: 'proj-parent', title: 'Parent', status: 'in_progress', beadType: 'task' }),
|
|
497
|
+
makeBead({ id: 'proj-child1', title: 'Code', status: 'closed', beadType: 'chore', parentId: 'proj-parent' }),
|
|
498
|
+
];
|
|
499
|
+
const output = renderMermaidGraph(beads);
|
|
500
|
+
// No subgraph since only child is closed (incomplete children = 0)
|
|
501
|
+
assert.ok(!output.includes('subgraph sg_proj_parent'));
|
|
502
|
+
// Parent rendered as a normal node
|
|
503
|
+
assert.ok(output.includes('proj_parent'));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('renders edges between stage children inside parent subgraph', () => {
|
|
507
|
+
const beads: Bead[] = [
|
|
508
|
+
makeBead({ id: 'proj-parent', title: 'Parent', status: 'in_progress', beadType: 'task' }),
|
|
509
|
+
makeBead({ id: 'proj-code', title: 'Code', status: 'in_progress', beadType: 'chore', parentId: 'proj-parent', stage: 'code', blocks: ['proj-review'] }),
|
|
510
|
+
makeBead({ id: 'proj-review', title: 'Review', status: 'open', beadType: 'chore', parentId: 'proj-parent', stage: 'review', blockedBy: ['proj-code'] }),
|
|
511
|
+
];
|
|
512
|
+
const output = renderMermaidGraph(beads);
|
|
513
|
+
assert.ok(output.includes('proj_code --> proj_review'));
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ─── Behavior 7: render task table ───────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
describe('renderTaskTable', () => {
|
|
520
|
+
it('returns placeholder when no beads', () => {
|
|
521
|
+
const output = renderTaskTable([]);
|
|
522
|
+
assert.equal(output, '_No tasks._');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('includes table headers with Summary column', () => {
|
|
526
|
+
const beads = [makeBead({ status: 'open' })];
|
|
527
|
+
const output = renderTaskTable(beads);
|
|
528
|
+
assert.ok(output.includes('| Code | Title | Status | Blocked by | Summary |'));
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('includes bead id, title, badge, and summary in row', () => {
|
|
532
|
+
const bead = makeBead({
|
|
533
|
+
id: 'proj-abc',
|
|
534
|
+
title: 'My task',
|
|
535
|
+
status: 'open',
|
|
536
|
+
classification: 'afk',
|
|
537
|
+
descriptionSummary: 'Does something useful',
|
|
538
|
+
});
|
|
539
|
+
const output = renderTaskTable([bead]);
|
|
540
|
+
assert.ok(output.includes('proj-abc'));
|
|
541
|
+
assert.ok(output.includes('My task'));
|
|
542
|
+
assert.ok(output.includes('⏳🤖'));
|
|
543
|
+
assert.ok(output.includes('Does something useful'));
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('shows blocker id and badge in blocked-by column', () => {
|
|
547
|
+
const beads: Bead[] = [
|
|
548
|
+
makeBead({ id: 'proj-a', status: 'open', classification: 'afk' }),
|
|
549
|
+
makeBead({ id: 'proj-b', status: 'blocked', classification: 'hitl', blockedBy: ['proj-a'] }),
|
|
550
|
+
];
|
|
551
|
+
const output = renderTaskTable(beads);
|
|
552
|
+
assert.ok(output.includes('proj-a ⏳🤖'));
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('shows closed beads after active beads', () => {
|
|
556
|
+
const beads: Bead[] = [
|
|
557
|
+
makeBead({ id: 'proj-active', status: 'open', descriptionSummary: 'active work' }),
|
|
558
|
+
makeBead({ id: 'proj-done', status: 'closed', descriptionSummary: 'done work' }),
|
|
559
|
+
];
|
|
560
|
+
const output = renderTaskTable(beads);
|
|
561
|
+
const activePos = output.indexOf('proj-active');
|
|
562
|
+
const donePos = output.indexOf('proj-done');
|
|
563
|
+
assert.ok(activePos < donePos, 'active bead should appear before closed bead');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('includes both active and closed beads in the same table', () => {
|
|
567
|
+
const beads: Bead[] = [
|
|
568
|
+
makeBead({ id: 'proj-active', status: 'open' }),
|
|
569
|
+
makeBead({ id: 'proj-done', status: 'closed' }),
|
|
570
|
+
];
|
|
571
|
+
const output = renderTaskTable(beads);
|
|
572
|
+
assert.ok(output.includes('proj-active'));
|
|
573
|
+
assert.ok(output.includes('proj-done'));
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('uses no summary placeholder when descriptionSummary is empty', () => {
|
|
577
|
+
const bead = makeBead({ status: 'closed', descriptionSummary: '' });
|
|
578
|
+
const output = renderTaskTable([bead]);
|
|
579
|
+
assert.ok(output.includes('_no summary_'));
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// ─── Behavior 9: orphaned blocked callout ────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
describe('renderOrphanedBlockedCallout', () => {
|
|
586
|
+
it('returns empty string when no orphaned beads', () => {
|
|
587
|
+
const beads = [makeBead({ status: 'open' })];
|
|
588
|
+
assert.equal(renderOrphanedBlockedCallout(beads), '');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('includes warning heading and bead id', () => {
|
|
592
|
+
const beads: Bead[] = [
|
|
593
|
+
makeBead({ id: 'proj-a', status: 'closed' }),
|
|
594
|
+
makeBead({ id: 'proj-b', status: 'blocked', blockedBy: ['proj-a'] }),
|
|
595
|
+
];
|
|
596
|
+
const output = renderOrphanedBlockedCallout(beads);
|
|
597
|
+
assert.ok(output.includes('Orphaned Blocked Beads'));
|
|
598
|
+
assert.ok(output.includes('proj-b'));
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ─── Behavior 10: fetchBeads (data fetch integration) ────────────────────────
|
|
603
|
+
|
|
604
|
+
describe('fetchBeads', () => {
|
|
605
|
+
const mockExec = (cmd: string, _cwd: string): string => {
|
|
606
|
+
if (cmd === 'bd list') return BD_LIST_OUTPUT;
|
|
607
|
+
if (cmd === 'bd list --status=closed') return '✓ proj-ddd ● P3 Completed task\n';
|
|
608
|
+
if (cmd === 'bd show proj-aaa') return BD_SHOW_HITL;
|
|
609
|
+
if (cmd === 'bd show proj-bbb') return BD_SHOW_AFK_CODING;
|
|
610
|
+
if (cmd === 'bd show proj-ccc') return BD_SHOW_NO_LABELS;
|
|
611
|
+
if (cmd === 'bd show proj-ddd') return '✓ proj-ddd · Completed task\nLABELS: implementation-type:afk\n';
|
|
612
|
+
if (cmd === 'bd show proj-eee') return '❄ proj-eee · Deferred task\nLABELS: implementation-type:afk\n';
|
|
613
|
+
// No epics in BD_LIST_OUTPUT so no bd children calls expected
|
|
614
|
+
return '';
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
it('fetches and assembles all beads without duplicates', () => {
|
|
618
|
+
const beads = fetchBeads('/workspace', mockExec);
|
|
619
|
+
const ids = beads.map((b) => b.id);
|
|
620
|
+
// proj-ddd appears in both bd list and bd list --status=closed, should be deduplicated
|
|
621
|
+
assert.equal(ids.filter((id) => id === 'proj-ddd').length, 1);
|
|
622
|
+
assert.equal(beads.length, 5);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('assembles correct classification from bd show', () => {
|
|
626
|
+
const beads = fetchBeads('/workspace', mockExec);
|
|
627
|
+
const bbb = beads.find((b) => b.id === 'proj-bbb');
|
|
628
|
+
assert.equal(bbb?.classification, 'afk');
|
|
629
|
+
assert.equal(bbb?.stage, 'coding');
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('assembles blockedBy and blocks from bd show', () => {
|
|
633
|
+
const beads = fetchBeads('/workspace', mockExec);
|
|
634
|
+
const bbb = beads.find((b) => b.id === 'proj-bbb');
|
|
635
|
+
assert.deepEqual(bbb?.blockedBy, ['proj-aaa']);
|
|
636
|
+
assert.deepEqual(bbb?.blocks, ['proj-ccc']);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('fetches child beads for epics via bd children', () => {
|
|
640
|
+
const epicListOutput = '○ proj-epic ● P1 [epic] Auth\n';
|
|
641
|
+
const childrenOutput = '○ proj-child1 ● P2 Sign in\n○ proj-child2 ● P2 Sign out\n';
|
|
642
|
+
const mockExecWithEpic = (cmd: string, _cwd: string): string => {
|
|
643
|
+
if (cmd === 'bd list') return epicListOutput;
|
|
644
|
+
if (cmd === 'bd list --status=closed') return '';
|
|
645
|
+
if (cmd === 'bd children proj-epic') return childrenOutput;
|
|
646
|
+
if (cmd === 'bd show proj-epic') return '○ proj-epic · [epic] Auth\nLABELS: implementation-type:hitl\n';
|
|
647
|
+
if (cmd === 'bd show proj-child1') return '○ proj-child1 · Sign in\nLABELS: implementation-type:afk, epic:proj-epic\n';
|
|
648
|
+
if (cmd === 'bd show proj-child2') return '○ proj-child2 · Sign out\nLABELS: implementation-type:afk, epic:proj-epic\n';
|
|
649
|
+
return '';
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const beads = fetchBeads('/workspace', mockExecWithEpic);
|
|
653
|
+
const ids = beads.map((b) => b.id);
|
|
654
|
+
assert.ok(ids.includes('proj-epic'));
|
|
655
|
+
assert.ok(ids.includes('proj-child1'));
|
|
656
|
+
assert.ok(ids.includes('proj-child2'));
|
|
657
|
+
assert.equal(beads.length, 3);
|
|
658
|
+
|
|
659
|
+
const child1 = beads.find((b) => b.id === 'proj-child1');
|
|
660
|
+
assert.equal(child1?.epicId, 'proj-epic');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('deduplicates children that also appear in bd list', () => {
|
|
664
|
+
const epicListOutput = '○ proj-epic ● P1 [epic] Auth\n○ proj-child1 ● P2 Sign in\n';
|
|
665
|
+
const childrenOutput = '○ proj-child1 ● P2 Sign in\n';
|
|
666
|
+
const mockExecDedup = (cmd: string, _cwd: string): string => {
|
|
667
|
+
if (cmd === 'bd list') return epicListOutput;
|
|
668
|
+
if (cmd === 'bd list --status=closed') return '';
|
|
669
|
+
if (cmd === 'bd children proj-epic') return childrenOutput;
|
|
670
|
+
if (cmd === 'bd show proj-epic') return '○ proj-epic · [epic] Auth\nLABELS: implementation-type:hitl\n';
|
|
671
|
+
if (cmd === 'bd show proj-child1') return '○ proj-child1 · Sign in\nLABELS: implementation-type:afk, epic:proj-epic\n';
|
|
672
|
+
return '';
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const beads = fetchBeads('/workspace', mockExecDedup);
|
|
676
|
+
assert.equal(beads.filter((b) => b.id === 'proj-child1').length, 1);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// ─── Behavior 11: plugin session-start hook wiring ────────────────────────────
|
|
681
|
+
|
|
682
|
+
describe('plugin session-start hook wiring', () => {
|
|
683
|
+
const repoRoot = join(import.meta.dirname, '..', '..', '..');
|
|
684
|
+
|
|
685
|
+
it('declares a hooks config path in plugin.json', () => {
|
|
686
|
+
const pluginJsonRaw = readFileSync(join(repoRoot, 'plugin.json'), 'utf8');
|
|
687
|
+
const pluginJson = JSON.parse(pluginJsonRaw) as { hooks?: unknown };
|
|
688
|
+
assert.equal(pluginJson.hooks, 'hooks.json');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('defines a sessionStart prompt hook that injects baseline context', () => {
|
|
692
|
+
const hooksJsonRaw = readFileSync(join(repoRoot, 'hooks.json'), 'utf8');
|
|
693
|
+
const hooksJson = JSON.parse(hooksJsonRaw) as {
|
|
694
|
+
hooks?: { sessionStart?: Array<{ type?: unknown; prompt?: unknown }> };
|
|
695
|
+
};
|
|
696
|
+
assert.ok(Array.isArray(hooksJson.hooks?.sessionStart));
|
|
697
|
+
const sessionStartHooks = hooksJson.hooks?.sessionStart ?? [];
|
|
698
|
+
assert.ok(
|
|
699
|
+
sessionStartHooks.some(
|
|
700
|
+
(entry) => entry.type === 'prompt' && typeof entry.prompt === 'string' && entry.prompt.length > 0,
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// ─── Behavior 12: skill anatomy documentation contracts ───────────────────────
|
|
707
|
+
|
|
708
|
+
describe('skill anatomy documentation contracts', () => {
|
|
709
|
+
const repoRoot = join(import.meta.dirname, '..', '..', '..');
|
|
710
|
+
|
|
711
|
+
it('includes the canonical skill anatomy doc', () => {
|
|
712
|
+
const anatomyDoc = readFileSync(join(repoRoot, 'docs', 'skills', 'skill-anatomy.md'), 'utf8');
|
|
713
|
+
assert.ok(anatomyDoc.length > 0);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('documents canonical anatomy and an annotated template acceptance checklist', () => {
|
|
717
|
+
const anatomyDoc = readFileSync(join(repoRoot, 'docs', 'skills', 'skill-anatomy.md'), 'utf8');
|
|
718
|
+
assert.match(anatomyDoc, /canonical skill anatomy/i);
|
|
719
|
+
assert.match(anatomyDoc, /annotated template/i);
|
|
720
|
+
assert.match(anatomyDoc, /acceptance criteria|checklist/i);
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// ─── Behavior 13: skill anatomy expanded documentation requirements ────────────
|
|
725
|
+
|
|
726
|
+
describe('skill anatomy expanded documentation requirements', () => {
|
|
727
|
+
const repoRoot = join(import.meta.dirname, '..', '..', '..');
|
|
728
|
+
|
|
729
|
+
it('documents required facets for each optional section', () => {
|
|
730
|
+
const anatomyDoc = readFileSync(join(repoRoot, 'docs', 'skills', 'skill-anatomy.md'), 'utf8');
|
|
731
|
+
assert.match(anatomyDoc, /optional sections/i);
|
|
732
|
+
assert.match(anatomyDoc, /what it is/i);
|
|
733
|
+
assert.match(anatomyDoc, /example snippet/i);
|
|
734
|
+
assert.match(anatomyDoc, /when to include/i);
|
|
735
|
+
assert.match(anatomyDoc, /when to omit/i);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('requires placement of "When NOT to use" adjacent to "When to use"', () => {
|
|
739
|
+
const anatomyDoc = readFileSync(join(repoRoot, 'docs', 'skills', 'skill-anatomy.md'), 'utf8');
|
|
740
|
+
assert.match(anatomyDoc, /##\s+When to use[\s\S]{0,400}##\s+When NOT to use/i);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('keeps the full annotated SKILL.md template at the end of the document', () => {
|
|
744
|
+
const anatomyDoc = readFileSync(join(repoRoot, 'docs', 'skills', 'skill-anatomy.md'), 'utf8').trimEnd();
|
|
745
|
+
assert.match(anatomyDoc, /##\s+Annotated template[\s\S]*```md[\s\S]*```$/i);
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ─── Behavior 15: research doc promotion/linking contracts ─────────────────────
|
|
750
|
+
|
|
751
|
+
describe('research doc promotion/linking contracts', () => {
|
|
752
|
+
const repoRoot = join(import.meta.dirname, '..', '..', '..');
|
|
753
|
+
|
|
754
|
+
it('links each research doc from docs/inspirations.md and marks each as promoted', () => {
|
|
755
|
+
const inspirationsDoc = readFileSync(join(repoRoot, 'docs', 'inspirations.md'), 'utf8');
|
|
756
|
+
const researchDir = join(repoRoot, 'docs', 'research');
|
|
757
|
+
const researchFiles = readdirSync(researchDir).filter((fileName) => fileName.endsWith('.md'));
|
|
758
|
+
|
|
759
|
+
for (const researchFile of researchFiles) {
|
|
760
|
+
assert.match(inspirationsDoc, new RegExp(`\\(research\\/${researchFile.replace('.', '\\.')}\\)`));
|
|
761
|
+
const researchDoc = readFileSync(join(researchDir, researchFile), 'utf8');
|
|
762
|
+
assert.match(researchDoc, /promoted research note/i);
|
|
763
|
+
assert.match(researchDoc, /\[Inspirations\]\(\.\.\/inspirations\.md\)/i);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('does not keep promoted research docs in .agent-cortex/working-docs (true move, not copy)', () => {
|
|
768
|
+
const researchDir = join(repoRoot, 'docs', 'research');
|
|
769
|
+
const researchFiles = readdirSync(researchDir).filter((fileName) => fileName.endsWith('.md'));
|
|
770
|
+
const repoWorkingDocsDir = join(repoRoot, '.agent-cortex', 'working-docs');
|
|
771
|
+
const workingDocSearchRoots = [repoWorkingDocsDir, join(repoWorkingDocsDir, 'research')];
|
|
772
|
+
|
|
773
|
+
for (const workingDocsDir of workingDocSearchRoots) {
|
|
774
|
+
if (!existsSync(workingDocsDir)) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
for (const researchFile of researchFiles) {
|
|
779
|
+
assert.equal(existsSync(join(workingDocsDir, researchFile)), false);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
});
|