@jaybeeuu/agent-cortex 2.1.0 → 2.2.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/CHANGELOG.md +387 -0
- package/README.md +33 -13
- package/agents/README.md +22 -10
- package/agents/plan/agent.md +2 -2
- package/agents/ralph/agent.md +126 -88
- package/agents/ralph/claude/frontmatter.json +3 -3
- package/agents/ralph/claude/wake.md +46 -0
- package/agents/ralph/pi/{polling.md → wake.md} +35 -20
- package/agents/ralph-plan/agent.md +23 -11
- package/bin/agent-cortex.mjs +66 -4
- package/bin/installers/claude.mjs +182 -68
- package/bin/installers/pi.mjs +200 -24
- package/extensions/agent-guardrails/README.md +1 -1
- package/extensions/agent-guardrails/guardrails.ts +1 -1
- package/extensions/agent-guardrails/index.ts +1 -1
- package/extensions/agent-modes/discover.test.ts +2 -2
- package/hooks/claude/scripts/notify.mjs +8 -1
- package/lib/cli.mjs +91 -2
- package/package.json +27 -3
- package/scripts/check-changeset.mjs +4 -0
- package/scripts/check-no-sync.mjs +5 -1
- package/scripts/lib/compose-agent.mjs +69 -9
- package/scripts/lib/fs.mjs +10 -2
- package/scripts/lib/migrate.mjs +736 -0
- package/scripts/lib/release-gates.mjs +21 -1
- package/scripts/lib/skill-frontmatter.mjs +23 -6
- package/scripts/lib/skill-links.mjs +52 -0
- package/scripts/lib/verify-worker-tree.mjs +25 -0
- package/scripts/resolve-publish-command.mjs +4 -0
- package/skills/engineering/design-an-interface/SKILL.md +3 -3
- package/skills/engineering/grill-with-docs/SKILL.md +3 -5
- package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
- package/skills/engineering/tdd/SKILL.md +5 -7
- package/skills/engineering/technical-direction/SKILL.md +5 -5
- package/skills/planning/classify-bead/REFERENCE.md +3 -3
- package/skills/planning/classify-bead/SKILL.md +14 -9
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
- package/skills/planning/create-task/REFERENCE.md +52 -3
- package/skills/planning/create-task/SKILL.md +70 -26
- package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
- package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
- package/skills/planning/flesh-out-idea/SKILL.md +146 -0
- package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
- package/skills/planning/prd-to-tasks/SKILL.md +22 -16
- package/skills/planning/record-idea/FORMAT.md +66 -17
- package/skills/planning/record-idea/SKILL.md +48 -24
- package/skills/planning/request-refactor-plan/SKILL.md +4 -4
- package/skills/planning/spec-to-tickets/SKILL.md +5 -5
- package/skills/planning/write-a-prd/SKILL.md +8 -9
- package/skills/planning/write-a-ticket/SKILL.md +4 -6
- package/skills/productivity/bd-tool/SKILL.md +15 -10
- package/skills/productivity/grill-me/SKILL.md +5 -0
- package/skills/productivity/init-beads/SKILL.md +3 -3
- package/skills/productivity/migration/REFERENCE.md +227 -0
- package/skills/productivity/migration/SKILL.md +188 -0
- package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
- package/skills/productivity/write-a-skill/SKILL.md +5 -5
- package/skills/review/maintain-agent-docs/SKILL.md +5 -7
- package/skills/review/refactor-skill/SKILL.md +4 -3
- package/skills/review/review-security/SKILL.md +3 -5
- package/skills/style/style-code/SKILL.md +5 -10
- package/skills/style/style-comms/SKILL.md +5 -7
- package/skills/style/style-documentation/SKILL.md +5 -6
- package/skills/style/style-tests/SKILL.md +5 -7
- package/skills/workflow/git-workflow/SKILL.md +28 -4
- package/skills/workflow/hitl-collab/SKILL.md +4 -6
- package/skills/workflow/plan/SKILL.md +41 -30
- package/skills/workflow/ralph/REFERENCE.md +182 -51
- package/skills/workflow/ralph/SKILL.md +89 -50
- package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
- package/token-map.README.md +9 -4
- package/token-map.json +13 -6
- package/.agents/skills/style-versioning/SKILL.md +0 -172
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.github/workflows/ci.yml +0 -302
- package/.junie/guidelines.md +0 -87
- package/.junie/mcp/mcp.json +0 -10
- package/AGENTS.md +0 -153
- package/agents/ralph/claude/polling.md +0 -31
- package/agents-native/ralph.md +0 -203
- package/docs/claude-hooks.md +0 -144
- package/docs/commit-signing.md +0 -231
- package/docs/ideas/auto-model-selection.md +0 -75
- package/docs/ideas/ban-content-assertion-tests.md +0 -75
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
- package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
- package/docs/ideas/dedicated-review-tests-skill.md +0 -71
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
- package/docs/ideas/extension-manifests.md +0 -121
- package/docs/ideas/improve-pi-system-prompt.md +0 -43
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
- package/docs/ideas/model-selector.md +0 -40
- package/docs/ideas/pi-task-memory.md +0 -34
- package/docs/ideas/qa-extension.md +0 -43
- package/docs/ideas/record-idea-implementation-questions.md +0 -68
- package/docs/ideas/record-ideas-as-beads.md +0 -115
- package/docs/ideas/self-flushing-session.md +0 -72
- package/docs/ideas/state-in-beads-not-md.md +0 -69
- package/docs/ideas/subagent-visibility.md +0 -72
- package/docs/ideas/task-session-context.local.md +0 -95
- package/docs/ideas/task-session-context.md +0 -89
- package/docs/ideas/test-data-creation-helpers.md +0 -92
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
- package/docs/ideas/unified-ci-pipeline.md +0 -28
- package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
- package/docs/inspirations.md +0 -33
- package/docs/research/disable-model-invocation.md +0 -82
- package/docs/research/gas-city-research.md +0 -256
- package/docs/research/skill-design-research.md +0 -286
- package/docs/research/skill-improvements-analysis.md +0 -117
- package/docs/research/skill-patterns-research.md +0 -24
- package/docs/skills/skill-anatomy.md +0 -200
- package/docs/technical-direction/harness-config.md +0 -177
- package/docs/technical-direction/migration-execution-labels.md +0 -177
- package/docs/user-preferences.md +0 -17
- package/extensions/agent-guardrails/tsconfig.json +0 -12
- package/extensions/agent-modes/tsconfig.json +0 -12
- package/extensions/output-style/tsconfig.json +0 -12
- package/extensions/session-start/tsconfig.json +0 -12
- package/extensions/skill-stats/pnpm-lock.yaml +0 -329
- package/extensions/skill-stats/tsconfig.json +0 -12
- package/extensions/subagent/agents/planner.md +0 -37
- package/extensions/subagent/agents/reviewer.md +0 -35
- package/extensions/subagent/agents/scout.md +0 -50
- package/extensions/subagent/agents/worker.md +0 -24
- package/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/extensions/subagent/prompts/implement.md +0 -10
- package/extensions/subagent/prompts/scout-and-plan.md +0 -9
- package/extensions/subagent/tsconfig.json +0 -12
- package/pi/keybindings.json +0 -3
- package/pi/settings.json +0 -34
- package/pnpm-workspace.yaml +0 -14
- package/skills/planning/create-task/scripts/package.json +0 -14
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
- package/skills/planning/create-task/scripts/tsconfig.json +0 -12
- package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
- package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
- package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
- package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
- package/test/classify-bead.test.mjs +0 -277
- package/test/claude-installer.test.mjs +0 -308
- package/test/cli.test.mjs +0 -314
- package/test/create-chores.test.mjs +0 -85
- package/test/fake-bd.test.mjs +0 -72
- package/test/fs.test.mjs +0 -76
- package/test/helpers/fake-bd.mjs +0 -87
- package/test/helpers/fake-claude.mjs +0 -152
- package/test/helpers/fake-pi.mjs +0 -132
- package/test/installer.test.mjs +0 -806
- package/test/notify-hook.test.mjs +0 -63
- package/test/pending-gates.test.mjs +0 -147
- package/test/pi-installer.test.mjs +0 -785
- package/test/pi-register.test.mjs +0 -342
- package/test/pi-sweep.test.mjs +0 -619
- package/test/release-gates.test.mjs +0 -190
- package/test/skill-frontmatter.test.mjs +0 -137
- 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
|
-
}
|