@jaybeeuu/agent-cortex 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +411 -0
  2. package/README.md +33 -13
  3. package/agents/README.md +22 -10
  4. package/agents/plan/agent.md +2 -2
  5. package/agents/ralph/agent.md +126 -88
  6. package/agents/ralph/claude/frontmatter.json +3 -3
  7. package/agents/ralph/claude/wake.md +46 -0
  8. package/agents/ralph/pi/{polling.md → wake.md} +35 -20
  9. package/agents/ralph-plan/agent.md +23 -11
  10. package/bin/agent-cortex.mjs +66 -4
  11. package/bin/installers/claude.mjs +182 -68
  12. package/bin/installers/pi.mjs +200 -24
  13. package/extensions/agent-guardrails/README.md +1 -1
  14. package/extensions/agent-guardrails/guardrails.ts +1 -1
  15. package/extensions/agent-guardrails/index.ts +1 -1
  16. package/extensions/agent-modes/discover.test.ts +2 -2
  17. package/extensions/output-style/README.md +1 -1
  18. package/extensions/output-style/index.ts +2 -2
  19. package/extensions/output-style/output-style.ts +2 -2
  20. package/hooks/claude/scripts/notify.mjs +8 -1
  21. package/lib/cli.mjs +91 -2
  22. package/package.json +27 -3
  23. package/scripts/check-changeset.mjs +4 -0
  24. package/scripts/check-no-sync.mjs +5 -1
  25. package/scripts/lib/compose-agent.mjs +69 -9
  26. package/scripts/lib/fs.mjs +10 -2
  27. package/scripts/lib/migrate.mjs +736 -0
  28. package/scripts/lib/release-gates.mjs +21 -1
  29. package/scripts/lib/skill-frontmatter.mjs +23 -6
  30. package/scripts/lib/skill-links.mjs +52 -0
  31. package/scripts/lib/verify-worker-tree.mjs +25 -0
  32. package/scripts/resolve-publish-command.mjs +4 -0
  33. package/skills/engineering/design-an-interface/SKILL.md +3 -3
  34. package/skills/engineering/grill-with-docs/SKILL.md +3 -5
  35. package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
  36. package/skills/engineering/tdd/SKILL.md +5 -7
  37. package/skills/engineering/technical-direction/SKILL.md +5 -5
  38. package/skills/planning/classify-bead/REFERENCE.md +3 -3
  39. package/skills/planning/classify-bead/SKILL.md +14 -9
  40. package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
  41. package/skills/planning/create-task/REFERENCE.md +52 -3
  42. package/skills/planning/create-task/SKILL.md +70 -26
  43. package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
  44. package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
  45. package/skills/planning/flesh-out-idea/SKILL.md +146 -0
  46. package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
  47. package/skills/planning/prd-to-tasks/SKILL.md +22 -16
  48. package/skills/planning/record-idea/FORMAT.md +66 -17
  49. package/skills/planning/record-idea/SKILL.md +48 -24
  50. package/skills/planning/request-refactor-plan/SKILL.md +4 -4
  51. package/skills/planning/spec-to-tickets/SKILL.md +5 -5
  52. package/skills/planning/write-a-prd/SKILL.md +8 -9
  53. package/skills/planning/write-a-ticket/SKILL.md +4 -6
  54. package/skills/productivity/bd-tool/SKILL.md +15 -10
  55. package/skills/productivity/grill-me/SKILL.md +5 -0
  56. package/skills/productivity/init-beads/SKILL.md +3 -3
  57. package/skills/productivity/migration/REFERENCE.md +227 -0
  58. package/skills/productivity/migration/SKILL.md +188 -0
  59. package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
  60. package/skills/productivity/write-a-skill/SKILL.md +5 -5
  61. package/skills/review/maintain-agent-docs/SKILL.md +5 -7
  62. package/skills/review/refactor-skill/SKILL.md +4 -3
  63. package/skills/review/review-security/SKILL.md +3 -5
  64. package/skills/style/style-code/SKILL.md +5 -10
  65. package/skills/style/style-comms/SKILL.md +5 -7
  66. package/skills/style/style-documentation/SKILL.md +5 -6
  67. package/skills/style/style-tests/SKILL.md +5 -7
  68. package/skills/workflow/git-workflow/SKILL.md +52 -26
  69. package/skills/workflow/hitl-collab/SKILL.md +4 -6
  70. package/skills/workflow/plan/SKILL.md +41 -30
  71. package/skills/workflow/ralph/REFERENCE.md +182 -51
  72. package/skills/workflow/ralph/SKILL.md +89 -50
  73. package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
  74. package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
  75. package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
  76. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
  77. package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
  78. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
  79. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
  80. package/token-map.README.md +9 -4
  81. package/token-map.json +13 -6
  82. package/.agents/skills/style-versioning/SKILL.md +0 -172
  83. package/.changeset/README.md +0 -8
  84. package/.changeset/config.json +0 -11
  85. package/.github/workflows/ci.yml +0 -302
  86. package/.junie/guidelines.md +0 -87
  87. package/.junie/mcp/mcp.json +0 -10
  88. package/AGENTS.md +0 -153
  89. package/agents/ralph/claude/polling.md +0 -31
  90. package/agents-native/ralph.md +0 -203
  91. package/docs/claude-hooks.md +0 -144
  92. package/docs/commit-signing.md +0 -231
  93. package/docs/ideas/auto-model-selection.md +0 -75
  94. package/docs/ideas/ban-content-assertion-tests.md +0 -75
  95. package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
  96. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
  97. package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
  98. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
  99. package/docs/ideas/dedicated-review-tests-skill.md +0 -71
  100. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
  101. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
  102. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
  103. package/docs/ideas/extension-manifests.md +0 -121
  104. package/docs/ideas/improve-pi-system-prompt.md +0 -43
  105. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
  106. package/docs/ideas/model-selector.md +0 -40
  107. package/docs/ideas/pi-task-memory.md +0 -34
  108. package/docs/ideas/qa-extension.md +0 -43
  109. package/docs/ideas/record-idea-implementation-questions.md +0 -68
  110. package/docs/ideas/record-ideas-as-beads.md +0 -115
  111. package/docs/ideas/self-flushing-session.md +0 -72
  112. package/docs/ideas/state-in-beads-not-md.md +0 -69
  113. package/docs/ideas/subagent-visibility.md +0 -72
  114. package/docs/ideas/task-session-context.local.md +0 -95
  115. package/docs/ideas/task-session-context.md +0 -89
  116. package/docs/ideas/test-data-creation-helpers.md +0 -92
  117. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
  118. package/docs/ideas/unified-ci-pipeline.md +0 -28
  119. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
  120. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
  121. package/docs/inspirations.md +0 -33
  122. package/docs/research/disable-model-invocation.md +0 -82
  123. package/docs/research/gas-city-research.md +0 -256
  124. package/docs/research/skill-design-research.md +0 -286
  125. package/docs/research/skill-improvements-analysis.md +0 -117
  126. package/docs/research/skill-patterns-research.md +0 -24
  127. package/docs/skills/skill-anatomy.md +0 -200
  128. package/docs/technical-direction/harness-config.md +0 -177
  129. package/docs/technical-direction/migration-execution-labels.md +0 -177
  130. package/docs/user-preferences.md +0 -17
  131. package/extensions/agent-guardrails/tsconfig.json +0 -12
  132. package/extensions/agent-modes/tsconfig.json +0 -12
  133. package/extensions/output-style/tsconfig.json +0 -12
  134. package/extensions/session-start/tsconfig.json +0 -12
  135. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  136. package/extensions/skill-stats/tsconfig.json +0 -12
  137. package/extensions/subagent/agents/planner.md +0 -37
  138. package/extensions/subagent/agents/reviewer.md +0 -35
  139. package/extensions/subagent/agents/scout.md +0 -50
  140. package/extensions/subagent/agents/worker.md +0 -24
  141. package/extensions/subagent/prompts/implement-and-review.md +0 -10
  142. package/extensions/subagent/prompts/implement.md +0 -10
  143. package/extensions/subagent/prompts/scout-and-plan.md +0 -9
  144. package/extensions/subagent/tsconfig.json +0 -12
  145. package/pi/keybindings.json +0 -3
  146. package/pi/settings.json +0 -34
  147. package/pnpm-workspace.yaml +0 -14
  148. package/skills/planning/create-task/scripts/package.json +0 -14
  149. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  150. package/skills/planning/create-task/scripts/tsconfig.json +0 -12
  151. package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
  152. package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
  153. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  154. package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
  155. package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
  156. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
  157. package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
  158. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
  159. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
  160. package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
  161. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
  162. package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
  163. package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
  164. package/test/classify-bead.test.mjs +0 -277
  165. package/test/claude-installer.test.mjs +0 -308
  166. package/test/cli.test.mjs +0 -314
  167. package/test/create-chores.test.mjs +0 -85
  168. package/test/fake-bd.test.mjs +0 -72
  169. package/test/fs.test.mjs +0 -76
  170. package/test/helpers/fake-bd.mjs +0 -87
  171. package/test/helpers/fake-claude.mjs +0 -152
  172. package/test/helpers/fake-pi.mjs +0 -132
  173. package/test/installer.test.mjs +0 -806
  174. package/test/notify-hook.test.mjs +0 -63
  175. package/test/pending-gates.test.mjs +0 -147
  176. package/test/pi-installer.test.mjs +0 -785
  177. package/test/pi-register.test.mjs +0 -342
  178. package/test/pi-sweep.test.mjs +0 -619
  179. package/test/release-gates.test.mjs +0 -190
  180. package/test/skill-frontmatter.test.mjs +0 -137
  181. package/test/verify-worker-tree.test.mjs +0 -120
@@ -1,682 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- parseListLine,
5
- parseBdList,
6
- parseBdShow,
7
- buildStatusBadge,
8
- detectOrphanedBlocked,
9
- renderMermaidGraph,
10
- renderTaskTable,
11
- renderOrphanedBlockedCallout,
12
- fetchBeads,
13
- type Bead,
14
- } from './generate-progress.ts';
15
-
16
- // ─── Fixtures ─────────────────────────────────────────────────────────────────
17
-
18
- function makeBead(overrides: Partial<Bead> = {}): Bead {
19
- return {
20
- id: 'proj-abc',
21
- title: 'Do something',
22
- status: 'open',
23
- classification: 'afk',
24
- stage: null,
25
- beadType: 'task',
26
- parentId: null,
27
- epicId: null,
28
- blockedBy: [],
29
- blocks: [],
30
- descriptionSummary: 'Some summary',
31
- ...overrides,
32
- };
33
- }
34
-
35
- const BD_LIST_OUTPUT = `○ proj-aaa ● P1 First task
36
- ◐ proj-bbb ● P2 Second task
37
- ● proj-ccc ● P1 Third task
38
- ✓ proj-ddd ● P3 Completed task
39
- ❄ proj-eee ● P2 Deferred task
40
-
41
- --------------------------------------------------------------------------------
42
- Total: 5 issues (2 open, 1 in progress)
43
-
44
- Status: ○ open ◐ in_progress ● blocked ✓ closed ❄ deferred`;
45
-
46
- const BD_SHOW_AFK_CODING = `◐ proj-bbb · Second task [● P2 · IN_PROGRESS]
47
- Owner: Jane · Assignee: Jane · Type: task
48
- Created: 2026-01-01 · Updated: 2026-01-02
49
-
50
- DESCRIPTION
51
- Implements the second task with some detail about what it does.
52
- More detail here.
53
-
54
- LABELS: implementation-type:afk, stage:coding
55
-
56
- BLOCKS
57
- ← ○ proj-ccc: Third task ● P1
58
-
59
- DEPENDS ON
60
- → ○ proj-aaa: First task ● P1`;
61
-
62
- const BD_SHOW_HITL = `○ proj-aaa · First task [● P1 · OPEN]
63
- Owner: Jane · Type: task
64
- Created: 2026-01-01 · Updated: 2026-01-01
65
-
66
- DESCRIPTION
67
- Configure secrets manually.
68
-
69
- LABELS: implementation-type:hitl
70
-
71
- `;
72
-
73
- const BD_SHOW_NO_LABELS = `○ proj-ccc · Third task [● P1 · BLOCKED]
74
- Owner: Jane · Type: task
75
- Created: 2026-01-01 · Updated: 2026-01-01
76
-
77
- DESCRIPTION
78
- Blocked task.
79
-
80
- DEPENDS ON
81
- → ◐ proj-bbb: Second task ● P2`;
82
-
83
- const BD_SHOW_CHORE_WITH_PARENT = `◐ proj-bbb-code · Code stage [● P2 · IN_PROGRESS]
84
- Owner: Jane · Assignee: Jane · Type: chore · Parent: proj-bbb
85
- Created: 2026-01-01 · Updated: 2026-01-02
86
-
87
- DESCRIPTION
88
- Coding stage for second task.
89
-
90
- LABELS: implementation-type:afk, stage:code
91
- `;
92
-
93
- const BD_SHOW_CHORE_VERIFY = `○ proj-bbb-verify · Verify stage [● P2 · OPEN]
94
- Owner: Jane · Type: chore · Parent: proj-bbb
95
- Created: 2026-01-01 · Updated: 2026-01-02
96
-
97
- DESCRIPTION
98
- Verify stage for second task.
99
-
100
- LABELS: implementation-type:afk, stage:verify
101
-
102
- DEPENDS ON
103
- → ◐ proj-bbb-code: Code stage ● P2`;
104
-
105
- const BD_SHOW_CHORE_REVIEW = `○ proj-bbb-review · Review stage [● P2 · OPEN]
106
- Owner: Jane · Type: chore · Parent: proj-bbb
107
- Created: 2026-01-01 · Updated: 2026-01-02
108
-
109
- DESCRIPTION
110
- Review stage for second task.
111
-
112
- LABELS: implementation-type:afk, stage:review
113
-
114
- DEPENDS ON
115
- → ○ proj-bbb-verify: Verify stage ● P2`;
116
-
117
- // ─── Behavior 1: parse bd list line ──────────────────────────────────────────
118
-
119
- describe('parseListLine', () => {
120
- it('parses an open bead', () => {
121
- const result = parseListLine('○ proj-aaa ● P1 First task');
122
- assert.deepEqual(result, { id: 'proj-aaa', status: 'open', title: 'First task' });
123
- });
124
-
125
- it('parses an in-progress bead', () => {
126
- const result = parseListLine('◐ proj-bbb ● P2 Second task');
127
- assert.deepEqual(result, { id: 'proj-bbb', status: 'in_progress', title: 'Second task' });
128
- });
129
-
130
- it('parses a blocked bead', () => {
131
- const result = parseListLine('● proj-ccc ● P1 Third task');
132
- assert.deepEqual(result, { id: 'proj-ccc', status: 'blocked', title: 'Third task' });
133
- });
134
-
135
- it('parses a closed bead', () => {
136
- const result = parseListLine('✓ proj-ddd ● P3 Completed task');
137
- assert.deepEqual(result, { id: 'proj-ddd', status: 'closed', title: 'Completed task' });
138
- });
139
-
140
- it('parses a deferred bead', () => {
141
- const result = parseListLine('❄ proj-eee ● P2 Deferred task');
142
- assert.deepEqual(result, { id: 'proj-eee', status: 'deferred', title: 'Deferred task' });
143
- });
144
-
145
- it('parses current bd list format without the priority marker column', () => {
146
- // Current bd emits `{STATUS} {ID} P{N} {TITLE}`; the `●` before P{N} is legacy.
147
- const result = parseListLine('◐ proj-bbb P2 Second task');
148
- assert.deepEqual(result, { id: 'proj-bbb', status: 'in_progress', title: 'Second task' });
149
- });
150
-
151
- it('returns null for separator/summary lines', () => {
152
- assert.equal(parseListLine('---'), null);
153
- assert.equal(parseListLine('Total: 5 issues'), null);
154
- assert.equal(parseListLine(''), null);
155
- assert.equal(parseListLine('Status: ○ open'), null);
156
- });
157
-
158
- it('handles leading whitespace', () => {
159
- const result = parseListLine(' ○ proj-aaa ● P1 First task');
160
- assert.deepEqual(result, { id: 'proj-aaa', status: 'open', title: 'First task' });
161
- });
162
- });
163
-
164
- // ─── Behavior 2: parse bd list output (multiple lines) ───────────────────────
165
-
166
- describe('parseBdList', () => {
167
- it('extracts all bead entries from full bd list output', () => {
168
- const results = parseBdList(BD_LIST_OUTPUT);
169
- assert.equal(results.length, 5);
170
- assert.deepEqual(results[0], { id: 'proj-aaa', status: 'open', title: 'First task' });
171
- assert.deepEqual(results[1], { id: 'proj-bbb', status: 'in_progress', title: 'Second task' });
172
- assert.deepEqual(results[2], { id: 'proj-ccc', status: 'blocked', title: 'Third task' });
173
- assert.deepEqual(results[3], { id: 'proj-ddd', status: 'closed', title: 'Completed task' });
174
- assert.deepEqual(results[4], { id: 'proj-eee', status: 'deferred', title: 'Deferred task' });
175
- });
176
-
177
- it('returns empty array for empty output', () => {
178
- assert.deepEqual(parseBdList('No issues found.'), []);
179
- });
180
- });
181
-
182
- // ─── Behavior 3: parse bd show output ────────────────────────────────────────
183
-
184
- describe('parseBdShow', () => {
185
- it('extracts afk classification and coding stage', () => {
186
- const result = parseBdShow(BD_SHOW_AFK_CODING);
187
- assert.equal(result.classification, 'afk');
188
- assert.equal(result.stage, 'coding');
189
- });
190
-
191
- it('extracts hitl classification', () => {
192
- const result = parseBdShow(BD_SHOW_HITL);
193
- assert.equal(result.classification, 'hitl');
194
- assert.equal(result.stage, null);
195
- });
196
-
197
- it('defaults to unknown classification when no label', () => {
198
- const result = parseBdShow(BD_SHOW_NO_LABELS);
199
- assert.equal(result.classification, 'unknown');
200
- assert.equal(result.stage, null);
201
- });
202
-
203
- it('extracts blocks list', () => {
204
- const result = parseBdShow(BD_SHOW_AFK_CODING);
205
- assert.deepEqual(result.blocks, ['proj-ccc']);
206
- });
207
-
208
- it('extracts blockedBy (depends on) list', () => {
209
- const result = parseBdShow(BD_SHOW_AFK_CODING);
210
- assert.deepEqual(result.blockedBy, ['proj-aaa']);
211
- });
212
-
213
- it('extracts blockedBy from show with only depends-on section', () => {
214
- const result = parseBdShow(BD_SHOW_NO_LABELS);
215
- assert.deepEqual(result.blockedBy, ['proj-bbb']);
216
- assert.deepEqual(result.blocks, []);
217
- });
218
-
219
- it('extracts description summary (first lines)', () => {
220
- const result = parseBdShow(BD_SHOW_AFK_CODING);
221
- assert.ok(result.descriptionSummary.includes('Implements the second task'));
222
- });
223
-
224
- it('handles all stage values', () => {
225
- for (const stage of ['reviewing', 'fixing', 'documenting'] as const) {
226
- const output = `LABELS: implementation-type:afk, stage:${stage}`;
227
- const result = parseBdShow(output);
228
- assert.equal(result.stage, stage);
229
- }
230
- });
231
-
232
- it('parses epicId from epic: label', () => {
233
- const output = `○ proj-child · Child task [● P2 · OPEN]
234
- Owner: Jane · Type: task
235
- Created: 2026-01-01 · Updated: 2026-01-01
236
-
237
- DESCRIPTION
238
- Child task description.
239
-
240
- LABELS: implementation-type:afk, epic:proj-epic`;
241
- const result = parseBdShow(output);
242
- assert.equal(result.epicId, 'proj-epic');
243
- });
244
-
245
- it('leaves epicId null when no epic: label', () => {
246
- const result = parseBdShow(BD_SHOW_HITL);
247
- assert.equal(result.epicId, null);
248
- });
249
-
250
- it('extracts parentId from header line', () => {
251
- const result = parseBdShow(BD_SHOW_CHORE_WITH_PARENT);
252
- assert.equal(result.parentId, 'proj-bbb');
253
- });
254
-
255
- it('extracts beadType from header line', () => {
256
- const result = parseBdShow(BD_SHOW_CHORE_WITH_PARENT);
257
- assert.equal(result.beadType, 'chore');
258
- });
259
-
260
- it('defaults beadType to task and parentId to null', () => {
261
- const result = parseBdShow(BD_SHOW_AFK_CODING);
262
- assert.equal(result.beadType, 'task');
263
- assert.equal(result.parentId, null);
264
- });
265
-
266
- it('handles new stage values (code, verify, review, document, fix)', () => {
267
- for (const stage of ['code', 'verify', 'review', 'document', 'fix'] as const) {
268
- const output = `LABELS: implementation-type:afk, stage:${stage}`;
269
- const result = parseBdShow(output);
270
- assert.equal(result.stage, stage);
271
- }
272
- });
273
- });
274
-
275
- // ─── Behavior 4: build status badge ──────────────────────────────────────────
276
-
277
- describe('buildStatusBadge', () => {
278
- it('open afk bead → ⏳🤖', () => {
279
- const bead = makeBead({ status: 'open', classification: 'afk', stage: null });
280
- assert.equal(buildStatusBadge(bead), '⏳🤖');
281
- });
282
-
283
- it('in_progress afk bead with coding stage → ▶️🤖🔨', () => {
284
- const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'coding' });
285
- assert.equal(buildStatusBadge(bead), '▶️🤖🔨');
286
- });
287
-
288
- it('in_progress hitl reviewing → ▶️🙋👁', () => {
289
- const bead = makeBead({ status: 'in_progress', classification: 'hitl', stage: 'reviewing' });
290
- assert.equal(buildStatusBadge(bead), '▶️🙋👁');
291
- });
292
-
293
- it('blocked unknown → 🔒', () => {
294
- const bead = makeBead({ status: 'blocked', classification: 'unknown', stage: null });
295
- assert.equal(buildStatusBadge(bead), '🔒');
296
- });
297
-
298
- it('closed afk → ✅🤖', () => {
299
- const bead = makeBead({ status: 'closed', classification: 'afk', stage: null });
300
- assert.equal(buildStatusBadge(bead), '✅🤖');
301
- });
302
-
303
- it('deferred hitl → ❌🙋', () => {
304
- const bead = makeBead({ status: 'deferred', classification: 'hitl', stage: null });
305
- assert.equal(buildStatusBadge(bead), '❌🙋');
306
- });
307
-
308
- it('sub-status not shown when not in_progress', () => {
309
- // Even if stage is set, sub-status only shows for in_progress
310
- const bead = makeBead({ status: 'open', classification: 'afk', stage: 'coding' });
311
- assert.equal(buildStatusBadge(bead), '⏳🤖');
312
- });
313
-
314
- it('in_progress with new stage code → ▶️🤖🔨', () => {
315
- const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'code' });
316
- assert.equal(buildStatusBadge(bead), '▶️🤖🔨');
317
- });
318
-
319
- it('in_progress with new stage verify → ▶️🤖🧪', () => {
320
- const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'verify' });
321
- assert.equal(buildStatusBadge(bead), '▶️🤖🧪');
322
- });
323
-
324
- it('in_progress with new stage review → ▶️🤖👁', () => {
325
- const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'review' });
326
- assert.equal(buildStatusBadge(bead), '▶️🤖👁');
327
- });
328
-
329
- it('in_progress with new stage document → ▶️🤖📝', () => {
330
- const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'document' });
331
- assert.equal(buildStatusBadge(bead), '▶️🤖📝');
332
- });
333
-
334
- it('in_progress with new stage fix → ▶️🤖🔧', () => {
335
- const bead = makeBead({ status: 'in_progress', classification: 'afk', stage: 'fix' });
336
- assert.equal(buildStatusBadge(bead), '▶️🤖🔧');
337
- });
338
- });
339
-
340
- // ─── Behavior 5: detect orphaned blocked beads ────────────────────────────────
341
-
342
- describe('detectOrphanedBlocked', () => {
343
- it('returns empty when no blocked beads', () => {
344
- const beads = [makeBead({ status: 'open' }), makeBead({ id: 'proj-b', status: 'closed' })];
345
- assert.deepEqual(detectOrphanedBlocked(beads), []);
346
- });
347
-
348
- it('returns blocked bead when all its deps are closed', () => {
349
- const beads: Bead[] = [
350
- makeBead({ id: 'proj-a', status: 'closed' }),
351
- makeBead({ id: 'proj-b', status: 'blocked', blockedBy: ['proj-a'] }),
352
- ];
353
- const orphaned = detectOrphanedBlocked(beads);
354
- assert.equal(orphaned.length, 1);
355
- assert.equal(orphaned[0].id, 'proj-b');
356
- });
357
-
358
- it('does not return blocked bead when some deps are still open', () => {
359
- const beads: Bead[] = [
360
- makeBead({ id: 'proj-a', status: 'open' }),
361
- makeBead({ id: 'proj-b', status: 'blocked', blockedBy: ['proj-a'] }),
362
- ];
363
- assert.deepEqual(detectOrphanedBlocked(beads), []);
364
- });
365
-
366
- it('returns blocked bead with no blockedBy entries (no tracked deps)', () => {
367
- const beads: Bead[] = [makeBead({ id: 'proj-b', status: 'blocked', blockedBy: [] })];
368
- const orphaned = detectOrphanedBlocked(beads);
369
- assert.equal(orphaned.length, 1);
370
- });
371
- });
372
-
373
- // ─── Behavior 6: render Mermaid graph ────────────────────────────────────────
374
-
375
- describe('renderMermaidGraph', () => {
376
- it('opens and closes mermaid code block', () => {
377
- const output = renderMermaidGraph([]);
378
- assert.ok(output.startsWith('```mermaid\ngraph TD'));
379
- assert.ok(output.endsWith('```'));
380
- });
381
-
382
- it('includes a node for each incomplete bead', () => {
383
- const beads = [
384
- makeBead({ id: 'proj-aaa', title: 'Task A', status: 'open' }),
385
- makeBead({ id: 'proj-bbb', title: 'Task B', status: 'in_progress' }),
386
- ];
387
- const output = renderMermaidGraph(beads);
388
- assert.ok(output.includes('proj_aaa'));
389
- assert.ok(output.includes('proj_bbb'));
390
- });
391
-
392
- it('excludes closed beads from graph', () => {
393
- const beads = [
394
- makeBead({ id: 'proj-aaa', status: 'open' }),
395
- makeBead({ id: 'proj-bbb', status: 'closed' }),
396
- ];
397
- const output = renderMermaidGraph(beads);
398
- assert.ok(output.includes('proj_aaa'));
399
- assert.ok(!output.includes('proj_bbb'));
400
- });
401
-
402
- it('renders dependency edges between incomplete beads', () => {
403
- const beads: Bead[] = [
404
- makeBead({ id: 'proj-aaa', status: 'open', blocks: ['proj-bbb'] }),
405
- makeBead({ id: 'proj-bbb', status: 'open', blockedBy: ['proj-aaa'] }),
406
- ];
407
- const output = renderMermaidGraph(beads);
408
- assert.ok(output.includes('proj_aaa --> proj_bbb'));
409
- });
410
-
411
- it('does not render edge to closed bead', () => {
412
- const beads: Bead[] = [
413
- makeBead({ id: 'proj-aaa', status: 'open', blocks: ['proj-bbb'] }),
414
- makeBead({ id: 'proj-bbb', status: 'closed' }),
415
- ];
416
- const output = renderMermaidGraph(beads);
417
- assert.ok(!output.includes('proj_aaa --> proj_bbb'));
418
- });
419
-
420
- it('wraps beads with epicId in a subgraph', () => {
421
- const beads: Bead[] = [
422
- makeBead({ id: 'proj-epic', title: 'The Epic', status: 'open' }),
423
- makeBead({ id: 'proj-child', title: 'Child task', status: 'open', epicId: 'proj-epic' }),
424
- ];
425
- const output = renderMermaidGraph(beads);
426
- assert.ok(output.includes('subgraph sg_proj_epic'));
427
- assert.ok(output.includes('proj_child'));
428
- // Epic with visible children is represented by its subgraph — no standalone node
429
- assert.ok(!output.includes('\n proj_epic['));
430
- });
431
-
432
- it('renders epic as standalone node when it has no visible children', () => {
433
- const beads: Bead[] = [
434
- makeBead({ id: 'proj-epic', title: '[epic] The Epic', status: 'open' }),
435
- ];
436
- const output = renderMermaidGraph(beads);
437
- assert.ok(output.includes('proj_epic'));
438
- assert.ok(!output.includes('subgraph'));
439
- });
440
-
441
- it('collapses nodes beyond 5 at the same depth into a summary node', () => {
442
- // 6 independent root nodes (no deps), all at depth 0
443
- const beads: Bead[] = [
444
- makeBead({ id: 'proj-a1', status: 'open', classification: 'afk' }),
445
- makeBead({ id: 'proj-a2', status: 'open', classification: 'afk' }),
446
- makeBead({ id: 'proj-a3', status: 'open', classification: 'hitl' }),
447
- makeBead({ id: 'proj-a4', status: 'open', classification: 'afk' }),
448
- makeBead({ id: 'proj-a5', status: 'open', classification: 'afk' }),
449
- makeBead({ id: 'proj-a6', status: 'open', classification: 'hitl' }),
450
- ];
451
- const output = renderMermaidGraph(beads);
452
- // First 5 rendered normally
453
- assert.ok(output.includes('proj_a1'));
454
- assert.ok(output.includes('proj_a2'));
455
- assert.ok(output.includes('proj_a3'));
456
- assert.ok(output.includes('proj_a4'));
457
- assert.ok(output.includes('proj_a5'));
458
- // 6th collapsed into summary; proj_a6 should not appear as standalone node
459
- assert.ok(!output.includes('proj_a6'));
460
- // Summary node: 1 collapsed, 0 afk, 1 hitl
461
- assert.ok(output.includes('+1 more (0 🤖, 1 🙋)'));
462
- });
463
-
464
- it('summary node inherits edges to children of collapsed nodes', () => {
465
- // 6 root nodes; the 6th (proj-a6) blocks proj-child
466
- const beads: Bead[] = [
467
- makeBead({ id: 'proj-a1', status: 'open', classification: 'afk' }),
468
- makeBead({ id: 'proj-a2', status: 'open', classification: 'afk' }),
469
- makeBead({ id: 'proj-a3', status: 'open', classification: 'afk' }),
470
- makeBead({ id: 'proj-a4', status: 'open', classification: 'afk' }),
471
- makeBead({ id: 'proj-a5', status: 'open', classification: 'afk' }),
472
- makeBead({ id: 'proj-a6', status: 'open', classification: 'afk', blocks: ['proj-child'] }),
473
- makeBead({ id: 'proj-child', status: 'open', classification: 'afk', blockedBy: ['proj-a6'] }),
474
- ];
475
- const output = renderMermaidGraph(beads);
476
- // proj-child is at depth 1 → visible
477
- assert.ok(output.includes('proj_child'));
478
- // Summary node should edge to proj-child
479
- assert.ok(output.includes('summary_d0 --> proj_child'));
480
- });
481
-
482
- it('renders parent task as subgraph containing stage children', () => {
483
- const beads: Bead[] = [
484
- makeBead({ id: 'proj-parent', title: 'Parent task', status: 'in_progress', beadType: 'task' }),
485
- makeBead({ id: 'proj-child1', title: 'Code', status: 'closed', beadType: 'chore', parentId: 'proj-parent', stage: 'code' }),
486
- makeBead({ id: 'proj-child2', title: 'Review', status: 'in_progress', beadType: 'chore', parentId: 'proj-parent', stage: 'review' }),
487
- ];
488
- const output = renderMermaidGraph(beads);
489
- // Parent rendered as subgraph (children are incomplete, so proj-child2 is visible)
490
- assert.ok(output.includes('subgraph sg_proj_parent'));
491
- // Child2 (in_progress) appears inside subgraph
492
- assert.ok(output.includes('proj_child2'));
493
- // Closed child1 excluded from graph entirely
494
- assert.ok(!output.includes('proj_child1'));
495
- assert.ok(output.includes('end'));
496
- });
497
-
498
- it('renders parent as normal node when all children are closed', () => {
499
- const beads: Bead[] = [
500
- makeBead({ id: 'proj-parent', title: 'Parent', status: 'in_progress', beadType: 'task' }),
501
- makeBead({ id: 'proj-child1', title: 'Code', status: 'closed', beadType: 'chore', parentId: 'proj-parent' }),
502
- ];
503
- const output = renderMermaidGraph(beads);
504
- // No subgraph since only child is closed (incomplete children = 0)
505
- assert.ok(!output.includes('subgraph sg_proj_parent'));
506
- // Parent rendered as a normal node
507
- assert.ok(output.includes('proj_parent'));
508
- });
509
-
510
- it('renders edges between stage children inside parent subgraph', () => {
511
- const beads: Bead[] = [
512
- makeBead({ id: 'proj-parent', title: 'Parent', status: 'in_progress', beadType: 'task' }),
513
- makeBead({ id: 'proj-code', title: 'Code', status: 'in_progress', beadType: 'chore', parentId: 'proj-parent', stage: 'code', blocks: ['proj-review'] }),
514
- makeBead({ id: 'proj-review', title: 'Review', status: 'open', beadType: 'chore', parentId: 'proj-parent', stage: 'review', blockedBy: ['proj-code'] }),
515
- ];
516
- const output = renderMermaidGraph(beads);
517
- assert.ok(output.includes('proj_code --> proj_review'));
518
- });
519
- });
520
-
521
- // ─── Behavior 7: render task table ───────────────────────────────────────────
522
-
523
- describe('renderTaskTable', () => {
524
- it('returns placeholder when no beads', () => {
525
- const output = renderTaskTable([]);
526
- assert.equal(output, '_No tasks._');
527
- });
528
-
529
- it('includes table headers with Summary column', () => {
530
- const beads = [makeBead({ status: 'open' })];
531
- const output = renderTaskTable(beads);
532
- assert.ok(output.includes('| Code | Title | Status | Blocked by | Summary |'));
533
- });
534
-
535
- it('includes bead id, title, badge, and summary in row', () => {
536
- const bead = makeBead({
537
- id: 'proj-abc',
538
- title: 'My task',
539
- status: 'open',
540
- classification: 'afk',
541
- descriptionSummary: 'Does something useful',
542
- });
543
- const output = renderTaskTable([bead]);
544
- assert.ok(output.includes('proj-abc'));
545
- assert.ok(output.includes('My task'));
546
- assert.ok(output.includes('⏳🤖'));
547
- assert.ok(output.includes('Does something useful'));
548
- });
549
-
550
- it('shows blocker id and badge in blocked-by column', () => {
551
- const beads: Bead[] = [
552
- makeBead({ id: 'proj-a', status: 'open', classification: 'afk' }),
553
- makeBead({ id: 'proj-b', status: 'blocked', classification: 'hitl', blockedBy: ['proj-a'] }),
554
- ];
555
- const output = renderTaskTable(beads);
556
- assert.ok(output.includes('proj-a ⏳🤖'));
557
- });
558
-
559
- it('shows closed beads after active beads', () => {
560
- const beads: Bead[] = [
561
- makeBead({ id: 'proj-active', status: 'open', descriptionSummary: 'active work' }),
562
- makeBead({ id: 'proj-done', status: 'closed', descriptionSummary: 'done work' }),
563
- ];
564
- const output = renderTaskTable(beads);
565
- const activePos = output.indexOf('proj-active');
566
- const donePos = output.indexOf('proj-done');
567
- assert.ok(activePos < donePos, 'active bead should appear before closed bead');
568
- });
569
-
570
- it('includes both active and closed beads in the same table', () => {
571
- const beads: Bead[] = [
572
- makeBead({ id: 'proj-active', status: 'open' }),
573
- makeBead({ id: 'proj-done', status: 'closed' }),
574
- ];
575
- const output = renderTaskTable(beads);
576
- assert.ok(output.includes('proj-active'));
577
- assert.ok(output.includes('proj-done'));
578
- });
579
-
580
- it('uses no summary placeholder when descriptionSummary is empty', () => {
581
- const bead = makeBead({ status: 'closed', descriptionSummary: '' });
582
- const output = renderTaskTable([bead]);
583
- assert.ok(output.includes('_no summary_'));
584
- });
585
- });
586
-
587
- // ─── Behavior 9: orphaned blocked callout ────────────────────────────────────
588
-
589
- describe('renderOrphanedBlockedCallout', () => {
590
- it('returns empty string when no orphaned beads', () => {
591
- const beads = [makeBead({ status: 'open' })];
592
- assert.equal(renderOrphanedBlockedCallout(beads), '');
593
- });
594
-
595
- it('includes warning heading and bead id', () => {
596
- const beads: Bead[] = [
597
- makeBead({ id: 'proj-a', status: 'closed' }),
598
- makeBead({ id: 'proj-b', status: 'blocked', blockedBy: ['proj-a'] }),
599
- ];
600
- const output = renderOrphanedBlockedCallout(beads);
601
- assert.ok(output.includes('Orphaned Blocked Beads'));
602
- assert.ok(output.includes('proj-b'));
603
- });
604
- });
605
-
606
- // ─── Behavior 10: fetchBeads (data fetch integration) ────────────────────────
607
-
608
- describe('fetchBeads', () => {
609
- const mockExec = (cmd: string, _cwd: string): string => {
610
- if (cmd === 'bd list') return BD_LIST_OUTPUT;
611
- if (cmd === 'bd list --status=closed') return '✓ proj-ddd ● P3 Completed task\n';
612
- if (cmd === 'bd show proj-aaa') return BD_SHOW_HITL;
613
- if (cmd === 'bd show proj-bbb') return BD_SHOW_AFK_CODING;
614
- if (cmd === 'bd show proj-ccc') return BD_SHOW_NO_LABELS;
615
- if (cmd === 'bd show proj-ddd') return '✓ proj-ddd · Completed task\nLABELS: implementation-type:afk\n';
616
- if (cmd === 'bd show proj-eee') return '❄ proj-eee · Deferred task\nLABELS: implementation-type:afk\n';
617
- // No epics in BD_LIST_OUTPUT so no bd children calls expected
618
- return '';
619
- };
620
-
621
- it('fetches and assembles all beads without duplicates', () => {
622
- const beads = fetchBeads('/workspace', mockExec);
623
- const ids = beads.map((b) => b.id);
624
- // proj-ddd appears in both bd list and bd list --status=closed, should be deduplicated
625
- assert.equal(ids.filter((id) => id === 'proj-ddd').length, 1);
626
- assert.equal(beads.length, 5);
627
- });
628
-
629
- it('assembles correct classification from bd show', () => {
630
- const beads = fetchBeads('/workspace', mockExec);
631
- const bbb = beads.find((b) => b.id === 'proj-bbb');
632
- assert.equal(bbb?.classification, 'afk');
633
- assert.equal(bbb?.stage, 'coding');
634
- });
635
-
636
- it('assembles blockedBy and blocks from bd show', () => {
637
- const beads = fetchBeads('/workspace', mockExec);
638
- const bbb = beads.find((b) => b.id === 'proj-bbb');
639
- assert.deepEqual(bbb?.blockedBy, ['proj-aaa']);
640
- assert.deepEqual(bbb?.blocks, ['proj-ccc']);
641
- });
642
-
643
- it('fetches child beads for epics via bd children', () => {
644
- const epicListOutput = '○ proj-epic ● P1 [epic] Auth\n';
645
- const childrenOutput = '○ proj-child1 ● P2 Sign in\n○ proj-child2 ● P2 Sign out\n';
646
- const mockExecWithEpic = (cmd: string, _cwd: string): string => {
647
- if (cmd === 'bd list') return epicListOutput;
648
- if (cmd === 'bd list --status=closed') return '';
649
- if (cmd === 'bd children proj-epic') return childrenOutput;
650
- if (cmd === 'bd show proj-epic') return '○ proj-epic · [epic] Auth\nLABELS: implementation-type:hitl\n';
651
- if (cmd === 'bd show proj-child1') return '○ proj-child1 · Sign in\nLABELS: implementation-type:afk, epic:proj-epic\n';
652
- if (cmd === 'bd show proj-child2') return '○ proj-child2 · Sign out\nLABELS: implementation-type:afk, epic:proj-epic\n';
653
- return '';
654
- };
655
-
656
- const beads = fetchBeads('/workspace', mockExecWithEpic);
657
- const ids = beads.map((b) => b.id);
658
- assert.ok(ids.includes('proj-epic'));
659
- assert.ok(ids.includes('proj-child1'));
660
- assert.ok(ids.includes('proj-child2'));
661
- assert.equal(beads.length, 3);
662
-
663
- const child1 = beads.find((b) => b.id === 'proj-child1');
664
- assert.equal(child1?.epicId, 'proj-epic');
665
- });
666
-
667
- it('deduplicates children that also appear in bd list', () => {
668
- const epicListOutput = '○ proj-epic ● P1 [epic] Auth\n○ proj-child1 ● P2 Sign in\n';
669
- const childrenOutput = '○ proj-child1 ● P2 Sign in\n';
670
- const mockExecDedup = (cmd: string, _cwd: string): string => {
671
- if (cmd === 'bd list') return epicListOutput;
672
- if (cmd === 'bd list --status=closed') return '';
673
- if (cmd === 'bd children proj-epic') return childrenOutput;
674
- if (cmd === 'bd show proj-epic') return '○ proj-epic · [epic] Auth\nLABELS: implementation-type:hitl\n';
675
- if (cmd === 'bd show proj-child1') return '○ proj-child1 · Sign in\nLABELS: implementation-type:afk, epic:proj-epic\n';
676
- return '';
677
- };
678
-
679
- const beads = fetchBeads('/workspace', mockExecDedup);
680
- assert.equal(beads.filter((b) => b.id === 'proj-child1').length, 1);
681
- });
682
- });