@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,588 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { resolve } from 'node:path';
3
-
4
- // ─── Types ────────────────────────────────────────────────────────────────────
5
-
6
- export type BeadStatus = 'open' | 'in_progress' | 'blocked' | 'closed' | 'deferred';
7
- export type Classification = 'afk' | 'hitl' | 'unknown';
8
- export type BeadType = 'task' | 'chore' | 'epic';
9
- export type Stage =
10
- | 'test-writing'
11
- | 'coding'
12
- | 'test-reviewing'
13
- | 'verifying'
14
- | 'reviewing'
15
- | 'fixing'
16
- | 'documenting'
17
- | 'code'
18
- | 'verify'
19
- | 'review'
20
- | 'document'
21
- | 'fix'
22
- | null;
23
-
24
- export interface Bead {
25
- id: string;
26
- title: string;
27
- status: BeadStatus;
28
- classification: Classification;
29
- stage: Stage;
30
- beadType: BeadType;
31
- parentId: string | null;
32
- epicId: string | null;
33
- blockedBy: string[];
34
- blocks: string[];
35
- descriptionSummary: string;
36
- }
37
-
38
- export type ExecFn = (cmd: string, cwd: string) => string;
39
-
40
- // ─── Data Fetch Layer ─────────────────────────────────────────────────────────
41
-
42
- const STATUS_CHAR_MAP: Record<string, BeadStatus> = {
43
- '○': 'open',
44
- '◐': 'in_progress',
45
- '●': 'blocked',
46
- '✓': 'closed',
47
- '❄': 'deferred',
48
- };
49
-
50
- export function parseListLine(
51
- line: string,
52
- ): { id: string; status: BeadStatus; title: string } | null {
53
- // Formats accepted:
54
- // {STATUS_CHAR} {ID} P{N} {TITLE} — current bd list output
55
- // {STATUS_CHAR} {ID} ● P{N} {TITLE} — older bd (● before P{N} is optional)
56
- const match = line.trim().match(/^([○◐●✓❄])\s+(\S+)\s+(?:●\s+)?P\d+\s+(.+)$/);
57
- if (!match) return null;
58
- const [, statusChar, id, title] = match;
59
- const status = STATUS_CHAR_MAP[statusChar];
60
- if (!status) return null;
61
- return { id, status, title: title.trim() };
62
- }
63
-
64
- export function parseBdList(
65
- output: string,
66
- ): Array<{ id: string; status: BeadStatus; title: string }> {
67
- return output
68
- .split('\n')
69
- .map((line) => parseListLine(line))
70
- .filter((r): r is NonNullable<typeof r> => r !== null);
71
- }
72
-
73
- const VALID_STAGES: ReadonlySet<Exclude<Stage, null>> = new Set([
74
- 'test-writing',
75
- 'coding',
76
- 'test-reviewing',
77
- 'verifying',
78
- 'reviewing',
79
- 'fixing',
80
- 'documenting',
81
- 'code',
82
- 'verify',
83
- 'review',
84
- 'document',
85
- 'fix',
86
- ]);
87
-
88
- export function parseBdShow(output: string): Omit<Bead, 'id' | 'title' | 'status'> {
89
- const labels: string[] = [];
90
- const blockedBy: string[] = [];
91
- const blocks: string[] = [];
92
- const descriptionLines: string[] = [];
93
- let epicId: string | null = null;
94
- let parentId: string | null = null;
95
- let beadType: BeadType = 'task';
96
- let inDescription = false;
97
- let inBlocks = false;
98
- let inDependsOn = false;
99
-
100
- for (const line of output.split('\n')) {
101
- const trimmed = line.trim();
102
-
103
- if (trimmed === 'DESCRIPTION') {
104
- inDescription = true;
105
- inBlocks = false;
106
- inDependsOn = false;
107
- continue;
108
- }
109
- if (trimmed === 'BLOCKS') {
110
- inBlocks = true;
111
- inDescription = false;
112
- inDependsOn = false;
113
- continue;
114
- }
115
- if (trimmed === 'DEPENDS ON') {
116
- inDependsOn = true;
117
- inBlocks = false;
118
- inDescription = false;
119
- continue;
120
- }
121
- if (trimmed.startsWith('LABELS:')) {
122
- inDescription = false;
123
- inBlocks = false;
124
- inDependsOn = false;
125
- const labelPart = trimmed.replace('LABELS:', '').trim();
126
- labels.push(...labelPart.split(',').map((l) => l.trim()).filter(Boolean));
127
- continue;
128
- }
129
- // Header line with Owner/Type/Parent fields (delimited by ·)
130
- if (/^(Owner:|Created:|Assignee:|Type:)/.test(trimmed)) {
131
- inDescription = false;
132
- inBlocks = false;
133
- inDependsOn = false;
134
- const typeMatch = trimmed.match(/Type:\s*(\S+)/);
135
- if (typeMatch) {
136
- const typeValue = typeMatch[1];
137
- if (typeValue === 'task' || typeValue === 'chore' || typeValue === 'epic') {
138
- beadType = typeValue;
139
- }
140
- }
141
- const parentMatch = trimmed.match(/Parent:\s*(\S+)/);
142
- if (parentMatch) parentId = parentMatch[1];
143
- }
144
-
145
- if (inBlocks && trimmed.startsWith('←')) {
146
- // ← ○ agent-cortex-6d4: Title ● P1
147
- const match = trimmed.match(/←\s+[○◐●✓❄]\s+(\S+):/);
148
- if (match) blocks.push(match[1]);
149
- } else if (inDependsOn && trimmed.startsWith('→')) {
150
- // → ◐ agent-cortex-wuo: Title ● P1
151
- const match = trimmed.match(/→\s+[○◐●✓❄]\s+(\S+):/);
152
- if (match) blockedBy.push(match[1]);
153
- } else if (inDescription && trimmed !== '' && descriptionLines.length < 3) {
154
- descriptionLines.push(trimmed);
155
- }
156
- }
157
-
158
- let classification: Classification = 'unknown';
159
- let stage: Stage = null;
160
-
161
- for (const label of labels) {
162
- if (label === 'implementation-type:afk') classification = 'afk';
163
- else if (label === 'implementation-type:hitl') classification = 'hitl';
164
- else if (label.startsWith('stage:')) {
165
- const stagePart = label.replace('stage:', '');
166
- if (VALID_STAGES.has(stagePart as Exclude<Stage, null>)) {
167
- stage = stagePart as Exclude<Stage, null>;
168
- }
169
- } else if (label.startsWith('epic:')) {
170
- epicId = label.slice('epic:'.length);
171
- }
172
- }
173
-
174
- return {
175
- classification,
176
- stage,
177
- beadType,
178
- parentId,
179
- epicId,
180
- blockedBy,
181
- blocks,
182
- descriptionSummary: descriptionLines.join(' ').substring(0, 120),
183
- };
184
- }
185
-
186
- export function fetchBeads(workspace: string, exec: ExecFn = defaultExec): Bead[] {
187
- const allOutput = exec('bd list', workspace);
188
- const closedOutput = exec('bd list --status=closed', workspace);
189
-
190
- const seen = new Set<string>();
191
- const listItems = [...parseBdList(allOutput), ...parseBdList(closedOutput)].filter((item) => {
192
- if (seen.has(item.id)) return false;
193
- seen.add(item.id);
194
- return true;
195
- });
196
-
197
- // Fetch child tasks for epics — bd list may omit children by default
198
- for (const item of listItems.filter((i) => i.title.includes('[epic]'))) {
199
- for (const child of parseBdList(exec(`bd children ${item.id}`, workspace))) {
200
- if (!seen.has(child.id)) {
201
- seen.add(child.id);
202
- listItems.push(child);
203
- }
204
- }
205
- }
206
-
207
- return listItems.map((item) => {
208
- const showOutput = exec(`bd show ${item.id}`, workspace);
209
- const details = parseBdShow(showOutput);
210
- return { id: item.id, title: item.title, status: item.status, ...details };
211
- });
212
- }
213
-
214
- function defaultExec(cmd: string, cwd: string): string {
215
- return execSync(cmd, { cwd, encoding: 'utf-8' });
216
- }
217
-
218
- // ─── Status Badge ─────────────────────────────────────────────────────────────
219
-
220
- export function buildStatusBadge(bead: Pick<Bead, 'status' | 'classification' | 'stage'>): string {
221
- let topLevel: string;
222
- switch (bead.status) {
223
- case 'in_progress':
224
- topLevel = '▶️';
225
- break;
226
- case 'open':
227
- topLevel = '⏳';
228
- break;
229
- case 'blocked':
230
- topLevel = '🔒';
231
- break;
232
- case 'closed':
233
- topLevel = '✅';
234
- break;
235
- case 'deferred':
236
- topLevel = '❌';
237
- break;
238
- }
239
-
240
- const classification =
241
- bead.classification === 'afk' ? '🤖' : bead.classification === 'hitl' ? '🙋' : '';
242
-
243
- let subStatus = '';
244
- if (bead.status === 'in_progress' && bead.stage) {
245
- switch (bead.stage) {
246
- case 'coding':
247
- case 'code':
248
- subStatus = '🔨';
249
- break;
250
- case 'reviewing':
251
- case 'review':
252
- subStatus = '👁';
253
- break;
254
- case 'fixing':
255
- case 'fix':
256
- subStatus = '🔧';
257
- break;
258
- case 'documenting':
259
- case 'document':
260
- subStatus = '📝';
261
- break;
262
- case 'verify':
263
- case 'verifying':
264
- subStatus = '🧪';
265
- break;
266
- }
267
- }
268
-
269
- return `${topLevel}${classification}${subStatus}`;
270
- }
271
-
272
- // ─── Render Layer ─────────────────────────────────────────────────────────────
273
-
274
- function mermaidId(id: string): string {
275
- return id.replace(/-/g, '_');
276
- }
277
-
278
- function truncate(s: string, max: number): string {
279
- return s.length > max ? s.substring(0, max - 3) + '...' : s;
280
- }
281
-
282
- function computeBfsDepths(incomplete: Bead[], incompleteIds: Set<string>): Map<string, number> {
283
- const depths = new Map<string, number>();
284
-
285
- const hasIncomingEdge = new Set<string>();
286
- for (const bead of incomplete) {
287
- for (const blockId of bead.blocks) {
288
- if (incompleteIds.has(blockId)) hasIncomingEdge.add(blockId);
289
- }
290
- }
291
-
292
- const beadMap = new Map(incomplete.map((b) => [b.id, b]));
293
- const queue: string[] = [];
294
- for (const bead of incomplete) {
295
- if (!hasIncomingEdge.has(bead.id)) {
296
- depths.set(bead.id, 0);
297
- queue.push(bead.id);
298
- }
299
- }
300
-
301
- let i = 0;
302
- while (i < queue.length) {
303
- const id = queue[i++];
304
- const depth = depths.get(id)!;
305
- const bead = beadMap.get(id);
306
- if (!bead) continue;
307
- for (const childId of bead.blocks) {
308
- if (incompleteIds.has(childId) && !depths.has(childId)) {
309
- depths.set(childId, depth + 1);
310
- queue.push(childId);
311
- }
312
- }
313
- }
314
-
315
- // Assign remaining nodes (cycles/disconnected) past the max depth
316
- const maxDepth = depths.size > 0 ? Math.max(...depths.values()) : -1;
317
- let nextDepth = maxDepth + 1;
318
- for (const bead of incomplete) {
319
- if (!depths.has(bead.id)) depths.set(bead.id, nextDepth);
320
- }
321
-
322
- return depths;
323
- }
324
-
325
- export function detectOrphanedBlocked(beads: Bead[]): Bead[] {
326
- const beadMap = new Map(beads.map((b) => [b.id, b]));
327
- return beads.filter((bead) => {
328
- if (bead.status !== 'blocked') return false;
329
- const hasUnresolved = bead.blockedBy.some((depId) => {
330
- const dep = beadMap.get(depId);
331
- return dep == null || dep.status !== 'closed';
332
- });
333
- return !hasUnresolved;
334
- });
335
- }
336
-
337
- const DEPTH_CUTOFF = 5;
338
-
339
- export function renderMermaidGraph(beads: Bead[]): string {
340
- const beadMap = new Map(beads.map((b) => [b.id, b]));
341
- const incomplete = beads.filter((b) => b.status !== 'closed');
342
-
343
- const incompleteIds = new Set(incomplete.map((b) => b.id));
344
- const depths = computeBfsDepths(incomplete, incompleteIds);
345
-
346
- // Build parent→children map for incomplete beads
347
- const parentChildMap = new Map<string, Bead[]>();
348
- const childIds = new Set<string>();
349
- for (const bead of incomplete) {
350
- if (bead.parentId && incompleteIds.has(bead.parentId)) {
351
- const children = parentChildMap.get(bead.parentId) ?? [];
352
- children.push(bead);
353
- parentChildMap.set(bead.parentId, children);
354
- childIds.add(bead.id);
355
- }
356
- }
357
-
358
- // Group by BFS depth
359
- const maxDepth = incomplete.length > 0 ? Math.max(...depths.values()) : -1;
360
- const byDepth: Bead[][] = Array.from({ length: maxDepth + 1 }, () => []);
361
- for (const bead of incomplete) {
362
- byDepth[depths.get(bead.id) ?? 0].push(bead);
363
- }
364
-
365
- // Apply per-level cutoff
366
- const visibleIds = new Set<string>();
367
- const collapsedIds = new Set<string>();
368
-
369
- interface SummaryNode {
370
- mid: string; // mermaid id, e.g. "summary_d0"
371
- depth: number;
372
- count: number;
373
- afkCount: number;
374
- hitlCount: number;
375
- childIds: string[];
376
- }
377
- const summaryNodes: SummaryNode[] = [];
378
-
379
- for (let d = 0; d < byDepth.length; d++) {
380
- const level = byDepth[d];
381
- if (level.length <= DEPTH_CUTOFF) {
382
- level.forEach((b) => visibleIds.add(b.id));
383
- } else {
384
- level.slice(0, DEPTH_CUTOFF).forEach((b) => visibleIds.add(b.id));
385
- const collapsed = level.slice(DEPTH_CUTOFF);
386
- collapsed.forEach((b) => collapsedIds.add(b.id));
387
- const afkCount = collapsed.filter((b) => b.classification === 'afk').length;
388
- const hitlCount = collapsed.filter((b) => b.classification === 'hitl').length;
389
- const snChildIds = [
390
- ...new Set(
391
- collapsed.flatMap((b) => b.blocks.filter((id) => incompleteIds.has(id))),
392
- ),
393
- ];
394
- summaryNodes.push({ mid: `summary_d${d}`, depth: d, count: collapsed.length, afkCount, hitlCount, childIds: snChildIds });
395
- }
396
- }
397
-
398
- // Group visible beads by epic (excluding children rendered in parent subgraphs)
399
- const epicGroups = new Map<string, Bead[]>();
400
- const potentialNoEpic: Bead[] = [];
401
- for (const bead of incomplete) {
402
- if (!visibleIds.has(bead.id)) continue;
403
- if (childIds.has(bead.id)) continue; // rendered inside parent subgraph
404
- if (bead.epicId) {
405
- const group = epicGroups.get(bead.epicId) ?? [];
406
- group.push(bead);
407
- epicGroups.set(bead.epicId, group);
408
- } else {
409
- potentialNoEpic.push(bead);
410
- }
411
- }
412
-
413
- // Epics with visible children are represented by their subgraph — skip them as standalone nodes
414
- const noEpic = potentialNoEpic.filter((bead) => !epicGroups.has(bead.id));
415
-
416
- const lines: string[] = ['```mermaid', 'graph TD'];
417
-
418
- const renderNode = (bead: Bead, indent: string): void => {
419
- const badge = buildStatusBadge(bead);
420
- const label = truncate(bead.title, 45);
421
- lines.push(`${indent}${mermaidId(bead.id)}["${badge} ${bead.id}<br/>${label}"]`);
422
- };
423
-
424
- // Render a parent bead as a subgraph with its children inside
425
- const renderParentSubgraph = (bead: Bead, indent: string): void => {
426
- const children = parentChildMap.get(bead.id) ?? [];
427
- const visibleChildren = children.filter((c) => visibleIds.has(c.id));
428
- if (visibleChildren.length === 0) {
429
- renderNode(bead, indent);
430
- return;
431
- }
432
- const badge = buildStatusBadge(bead);
433
- lines.push(`${indent}subgraph sg_${mermaidId(bead.id)}["${badge} ${bead.id}: ${truncate(bead.title, 40)}"]`);
434
- for (const child of visibleChildren) {
435
- renderNode(child, indent + ' ');
436
- }
437
- lines.push(`${indent}end`);
438
- };
439
-
440
- for (const [epicId, members] of epicGroups) {
441
- const epic = beadMap.get(epicId);
442
- lines.push(` subgraph sg_${mermaidId(epicId)}["📦 ${epic?.title ?? epicId}"]`);
443
- for (const bead of members) {
444
- if (parentChildMap.has(bead.id)) {
445
- renderParentSubgraph(bead, ' ');
446
- } else {
447
- renderNode(bead, ' ');
448
- }
449
- }
450
- lines.push(' end');
451
- }
452
-
453
- for (const bead of noEpic) {
454
- if (parentChildMap.has(bead.id)) {
455
- renderParentSubgraph(bead, ' ');
456
- } else {
457
- renderNode(bead, ' ');
458
- }
459
- }
460
-
461
- // Render summary nodes
462
- for (const sn of summaryNodes) {
463
- lines.push(` ${sn.mid}["+${sn.count} more (${sn.afkCount} 🤖, ${sn.hitlCount} 🙋)"]`);
464
- }
465
-
466
- // Helper: resolve a bead id to the mermaid id to use (may be a summary node)
467
- const resolveTargetMid = (targetId: string): string | null => {
468
- if (visibleIds.has(targetId)) return mermaidId(targetId);
469
- if (collapsedIds.has(targetId)) {
470
- const d = depths.get(targetId);
471
- if (d === undefined) return null;
472
- const sn = summaryNodes.find((s) => s.depth === d);
473
- return sn?.mid ?? null;
474
- }
475
- return null;
476
- };
477
-
478
- // Edges from visible beads (skip epics represented only as subgraphs — they have no node)
479
- for (const bead of incomplete) {
480
- if (!visibleIds.has(bead.id)) continue;
481
- if (epicGroups.has(bead.id)) continue;
482
- for (const blockId of bead.blocks) {
483
- const target = resolveTargetMid(blockId);
484
- if (target) lines.push(` ${mermaidId(bead.id)} --> ${target}`);
485
- }
486
- }
487
-
488
- // Edges from summary nodes to their children
489
- for (const sn of summaryNodes) {
490
- const emitted = new Set<string>();
491
- for (const snChildId of sn.childIds) {
492
- const target = resolveTargetMid(snChildId);
493
- if (target && !emitted.has(target)) {
494
- emitted.add(target);
495
- lines.push(` ${sn.mid} --> ${target}`);
496
- }
497
- }
498
- }
499
-
500
- lines.push('```');
501
- return lines.join('\n');
502
- }
503
-
504
- export function renderTaskTable(beads: Bead[]): string {
505
- const beadMap = new Map(beads.map((b) => [b.id, b]));
506
- const active = beads.filter((b) => b.status !== 'closed');
507
- const completed = beads.filter((b) => b.status === 'closed');
508
- const ordered = [...active, ...completed];
509
-
510
- if (ordered.length === 0) return '_No tasks._';
511
-
512
- const rows = ordered.map((bead) => {
513
- const badge = buildStatusBadge(bead);
514
- const blockedByStr =
515
- bead.blockedBy.length > 0
516
- ? bead.blockedBy
517
- .map((depId) => {
518
- const dep = beadMap.get(depId);
519
- return dep ? `${depId} ${buildStatusBadge(dep)}` : depId;
520
- })
521
- .join(', ')
522
- : '';
523
- return `| ${bead.id} | ${bead.title} | ${badge} | ${blockedByStr} | ${bead.descriptionSummary || '_no summary_'} |`;
524
- });
525
-
526
- return [
527
- '| Code | Title | Status | Blocked by | Summary |',
528
- '|------|-------|--------|------------|---------|',
529
- ...rows,
530
- ].join('\n');
531
- }
532
-
533
- export function renderOrphanedBlockedCallout(beads: Bead[]): string {
534
- const orphaned = detectOrphanedBlocked(beads);
535
- if (orphaned.length === 0) return '';
536
-
537
- const items = orphaned
538
- .map((b) => `> - **${b.id}**: ${b.title}`)
539
- .join('\n');
540
- return `> ⚠️ **Orphaned Blocked Beads** — marked blocked but have no unresolved dependencies:\n>\n${items}\n`;
541
- }
542
-
543
- export function renderMarkdown(beads: Bead[]): string {
544
- const parts: string[] = [];
545
-
546
- parts.push('## Dependency Graph\n');
547
- parts.push(renderMermaidGraph(beads));
548
- parts.push('');
549
-
550
- const callout = renderOrphanedBlockedCallout(beads);
551
- if (callout) {
552
- parts.push(callout);
553
- parts.push('');
554
- }
555
-
556
- parts.push('## Tasks\n');
557
- parts.push(renderTaskTable(beads));
558
- parts.push('');
559
-
560
- return parts.join('\n');
561
- }
562
-
563
- // ─── Main ─────────────────────────────────────────────────────────────────────
564
-
565
- function parseCliArgs(args: string[]): { workspace: string } {
566
- let workspace = process.cwd();
567
- for (let i = 0; i < args.length; i++) {
568
- if (args[i] === '--workspace' && args[i + 1]) {
569
- workspace = resolve(args[i + 1]);
570
- i++;
571
- }
572
- }
573
- return { workspace };
574
- }
575
-
576
- async function main(): Promise<void> {
577
- const { workspace } = parseCliArgs(process.argv.slice(2));
578
- const beads = fetchBeads(workspace);
579
- process.stdout.write(renderMarkdown(beads));
580
- }
581
-
582
- // Only run when executed directly, not when imported (e.g. by tests)
583
- if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'))) {
584
- main().catch((err: unknown) => {
585
- console.error(err);
586
- process.exit(1);
587
- });
588
- }
@@ -1,15 +0,0 @@
1
- {
2
- "name": "@agent-cortex/run-pipeline-stage-scripts",
3
- "version": "1.0.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "typecheck": "tsc --noEmit",
8
- "test": "node --test --import tsx/esm generate-progress.test.ts"
9
- },
10
- "devDependencies": {
11
- "@types/node": "^22.0.0",
12
- "tsx": "^4.0.0",
13
- "typescript": "^5.0.0"
14
- }
15
- }