create-byan-agent 2.9.4 → 2.9.6
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/install/bin/byan-cleanup.js +156 -0
- package/install/bin/byan-kanban.js +159 -0
- package/install/bin/byan-ledger.js +45 -0
- package/install/bin/create-byan-agent-v2.js +15 -1
- package/install/lib/cleanup/detector.js +154 -0
- package/install/lib/cleanup/executor.js +72 -0
- package/install/lib/staging-consent.js +149 -0
- package/install/lib/subagent-generator.js +208 -0
- package/install/lib/token-ledger.js +131 -0
- package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
- package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
- package/install/templates/.claude/agents/bmad-byan.md +152 -0
- package/install/templates/.claude/agents/bmad-carmack.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
- package/install/templates/.claude/agents/bmad-claude.md +26 -0
- package/install/templates/.claude/agents/bmad-codex.md +26 -0
- package/install/templates/.claude/agents/bmad-compliance.md +68 -0
- package/install/templates/.claude/agents/bmad-drawio.md +25 -0
- package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
- package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
- package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
- package/install/templates/.claude/agents/bmad-hermes.md +59 -0
- package/install/templates/.claude/agents/bmad-marc.md +25 -0
- package/install/templates/.claude/agents/bmad-patnote.md +26 -0
- package/install/templates/.claude/agents/bmad-rachid.md +25 -0
- package/install/templates/.claude/agents/bmad-tao.md +14 -0
- package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
- package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
- package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
- package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
- package/install/templates/.claude/hooks/fd-response-check.js +92 -0
- package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
- package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
- package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
- package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
- package/install/templates/.claude/hooks/tool-transparency.js +4 -0
- package/install/templates/.claude/settings.json +27 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
- package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
- package/install/templates/.githooks/pre-commit +75 -0
- package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
- package/install/templates/.github/extensions/byan-staging/package.json +8 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
- package/install/templates/detector.js +154 -0
- package/package.json +6 -7
- package/src/loadbalancer/capability-matrix.js +157 -0
- package/src/loadbalancer/config.js +141 -0
- package/src/loadbalancer/graceful-degradation.js +212 -0
- package/src/loadbalancer/health-probe.js +151 -0
- package/src/loadbalancer/hooks/claude-hooks.js +53 -0
- package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
- package/src/loadbalancer/index.js +81 -0
- package/src/loadbalancer/loadbalancer.default.yaml +65 -0
- package/src/loadbalancer/loadbalancer.js +324 -0
- package/src/loadbalancer/mcp-server.js +304 -0
- package/src/loadbalancer/metrics.js +146 -0
- package/src/loadbalancer/native/claude-integration.js +64 -0
- package/src/loadbalancer/native/copilot-integration.js +59 -0
- package/src/loadbalancer/pressure-score.js +102 -0
- package/src/loadbalancer/providers/base-provider.js +80 -0
- package/src/loadbalancer/providers/byan-api-provider.js +132 -0
- package/src/loadbalancer/providers/claude-provider.js +113 -0
- package/src/loadbalancer/providers/copilot-provider.js +104 -0
- package/src/loadbalancer/rate-limit-tracker.js +216 -0
- package/src/loadbalancer/session-bridge.js +179 -0
- package/src/loadbalancer/state/db.js +211 -0
- package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
- package/src/loadbalancer/tools/index.js +123 -0
- package/src/loadbalancer/velocity-estimator.js +147 -0
- package/src/staging/staging.js +394 -0
- package/update-byan-agent/bin/update-byan-agent.js +27 -2
- package/API-BYAN-V2.md +0 -741
- package/BMAD-QUICK-REFERENCE.md +0 -370
- package/CHANGELOG-v2.1.0.md +0 -371
- package/MIGRATION-v2.0-to-v2.1.md +0 -430
- package/README-BYAN-V2.md +0 -446
- package/TEST-GUIDE-v2.3.2.md +0 -161
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kanban + stand-up registry for BYAN party-mode sessions.
|
|
3
|
+
*
|
|
4
|
+
* Kanban : _byan-output/party-mode-sessions/<session_id>/kanban.json
|
|
5
|
+
* columns : todo | doing | blocked | review | done
|
|
6
|
+
* cards : { id, title, assignee, priority, created_at, moved_at,
|
|
7
|
+
* column, blocker_reason? }
|
|
8
|
+
*
|
|
9
|
+
* Stand-up : _byan-output/party-mode-sessions/<session_id>/standup.jsonl
|
|
10
|
+
* entries : { agent, timestamp, did, blockers, next }
|
|
11
|
+
*
|
|
12
|
+
* Hermes watches stand-ups : an agent with 2+ consecutive "blocked"
|
|
13
|
+
* reports in the stand-up stream is flagged and their card is moved to
|
|
14
|
+
* `blocked` column in the kanban.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
const COLUMNS = ['todo', 'doing', 'blocked', 'review', 'done'];
|
|
21
|
+
|
|
22
|
+
function resolveRoot(projectRoot) {
|
|
23
|
+
return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sessionDir(projectRoot, sessionId) {
|
|
27
|
+
return path.join(
|
|
28
|
+
resolveRoot(projectRoot),
|
|
29
|
+
'_byan-output',
|
|
30
|
+
'party-mode-sessions',
|
|
31
|
+
sanitize(sessionId)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitize(id) {
|
|
36
|
+
return String(id || 'default').replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 80);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function kanbanPath(projectRoot, sessionId) {
|
|
40
|
+
return path.join(sessionDir(projectRoot, sessionId), 'kanban.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function standupPath(projectRoot, sessionId) {
|
|
44
|
+
return path.join(sessionDir(projectRoot, sessionId), 'standup.jsonl');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readKanban(projectRoot, sessionId) {
|
|
48
|
+
const p = kanbanPath(projectRoot, sessionId);
|
|
49
|
+
if (!fs.existsSync(p)) return null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeKanban(projectRoot, sessionId, board) {
|
|
58
|
+
const p = kanbanPath(projectRoot, sessionId);
|
|
59
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
60
|
+
fs.writeFileSync(p, JSON.stringify(board, null, 2));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function emptyBoard(sessionId, now) {
|
|
64
|
+
return {
|
|
65
|
+
session_id: sessionId,
|
|
66
|
+
created_at: now.toISOString(),
|
|
67
|
+
updated_at: now.toISOString(),
|
|
68
|
+
columns: COLUMNS.slice(),
|
|
69
|
+
cards: {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createBoard({ sessionId, projectRoot, now = new Date() } = {}) {
|
|
74
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
75
|
+
const existing = readKanban(projectRoot, sessionId);
|
|
76
|
+
if (existing) return existing;
|
|
77
|
+
const board = emptyBoard(sessionId, now);
|
|
78
|
+
writeKanban(projectRoot, sessionId, board);
|
|
79
|
+
return board;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function addCard({
|
|
83
|
+
sessionId,
|
|
84
|
+
card,
|
|
85
|
+
projectRoot,
|
|
86
|
+
now = new Date(),
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
89
|
+
if (!card || !card.id || !card.title) throw new Error('card.id and card.title required');
|
|
90
|
+
|
|
91
|
+
const board = readKanban(projectRoot, sessionId) || emptyBoard(sessionId, now);
|
|
92
|
+
if (board.cards[card.id]) throw new Error(`card ${card.id} already exists`);
|
|
93
|
+
|
|
94
|
+
board.cards[card.id] = {
|
|
95
|
+
id: card.id,
|
|
96
|
+
title: card.title,
|
|
97
|
+
assignee: card.assignee || null,
|
|
98
|
+
priority: card.priority || 'P2',
|
|
99
|
+
column: card.column || 'todo',
|
|
100
|
+
created_at: now.toISOString(),
|
|
101
|
+
moved_at: now.toISOString(),
|
|
102
|
+
blocker_reason: null,
|
|
103
|
+
};
|
|
104
|
+
board.updated_at = now.toISOString();
|
|
105
|
+
|
|
106
|
+
writeKanban(projectRoot, sessionId, board);
|
|
107
|
+
return board.cards[card.id];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function moveCard({
|
|
111
|
+
sessionId,
|
|
112
|
+
cardId,
|
|
113
|
+
toColumn,
|
|
114
|
+
blocker_reason,
|
|
115
|
+
projectRoot,
|
|
116
|
+
now = new Date(),
|
|
117
|
+
} = {}) {
|
|
118
|
+
if (!COLUMNS.includes(toColumn)) {
|
|
119
|
+
throw new Error(`toColumn must be one of ${COLUMNS.join(', ')}, got ${toColumn}`);
|
|
120
|
+
}
|
|
121
|
+
const board = readKanban(projectRoot, sessionId);
|
|
122
|
+
if (!board) throw new Error(`no kanban for session ${sessionId}`);
|
|
123
|
+
if (!board.cards[cardId]) throw new Error(`card ${cardId} not found`);
|
|
124
|
+
|
|
125
|
+
const card = board.cards[cardId];
|
|
126
|
+
card.column = toColumn;
|
|
127
|
+
card.moved_at = now.toISOString();
|
|
128
|
+
card.blocker_reason = toColumn === 'blocked' ? blocker_reason || 'unspecified' : null;
|
|
129
|
+
board.updated_at = now.toISOString();
|
|
130
|
+
|
|
131
|
+
writeKanban(projectRoot, sessionId, board);
|
|
132
|
+
return card;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function assignCard({
|
|
136
|
+
sessionId,
|
|
137
|
+
cardId,
|
|
138
|
+
assignee,
|
|
139
|
+
projectRoot,
|
|
140
|
+
now = new Date(),
|
|
141
|
+
} = {}) {
|
|
142
|
+
if (!assignee) throw new Error('assignee is required');
|
|
143
|
+
const board = readKanban(projectRoot, sessionId);
|
|
144
|
+
if (!board || !board.cards[cardId]) throw new Error(`card ${cardId} not found`);
|
|
145
|
+
board.cards[cardId].assignee = assignee;
|
|
146
|
+
board.cards[cardId].moved_at = now.toISOString();
|
|
147
|
+
board.updated_at = now.toISOString();
|
|
148
|
+
writeKanban(projectRoot, sessionId, board);
|
|
149
|
+
return board.cards[cardId];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getBoard({ sessionId, projectRoot } = {}) {
|
|
153
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
154
|
+
return readKanban(projectRoot, sessionId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function postStandup({
|
|
158
|
+
sessionId,
|
|
159
|
+
agent,
|
|
160
|
+
did,
|
|
161
|
+
blockers = [],
|
|
162
|
+
next,
|
|
163
|
+
projectRoot,
|
|
164
|
+
now = new Date(),
|
|
165
|
+
} = {}) {
|
|
166
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
167
|
+
if (!agent) throw new Error('agent is required');
|
|
168
|
+
|
|
169
|
+
const entry = {
|
|
170
|
+
agent,
|
|
171
|
+
timestamp: now.toISOString(),
|
|
172
|
+
did: did || '',
|
|
173
|
+
blockers: Array.isArray(blockers) ? blockers : [],
|
|
174
|
+
next: next || '',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const p = standupPath(projectRoot, sessionId);
|
|
178
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
179
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
180
|
+
return entry;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function readStandups({ sessionId, projectRoot, limit = 50 } = {}) {
|
|
184
|
+
const p = standupPath(projectRoot, sessionId);
|
|
185
|
+
if (!fs.existsSync(p)) return [];
|
|
186
|
+
const lines = fs.readFileSync(p, 'utf8').split('\n').filter(Boolean);
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
try {
|
|
190
|
+
out.push(JSON.parse(line));
|
|
191
|
+
} catch {
|
|
192
|
+
// skip malformed
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out.slice(-limit);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect agents with >= minStreak consecutive blocked stand-ups.
|
|
200
|
+
* Returns array of { agent, streak, lastAt }.
|
|
201
|
+
*/
|
|
202
|
+
export function detectBlockedStreaks({ sessionId, minStreak = 2, projectRoot } = {}) {
|
|
203
|
+
const standups = readStandups({ sessionId, projectRoot, limit: 500 });
|
|
204
|
+
const streaks = {};
|
|
205
|
+
const agentLast = {};
|
|
206
|
+
|
|
207
|
+
for (const entry of standups) {
|
|
208
|
+
const isBlocked = Array.isArray(entry.blockers) && entry.blockers.length > 0;
|
|
209
|
+
if (isBlocked) {
|
|
210
|
+
streaks[entry.agent] = (streaks[entry.agent] || 0) + 1;
|
|
211
|
+
} else {
|
|
212
|
+
streaks[entry.agent] = 0;
|
|
213
|
+
}
|
|
214
|
+
agentLast[entry.agent] = entry.timestamp;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const flagged = [];
|
|
218
|
+
for (const [agent, n] of Object.entries(streaks)) {
|
|
219
|
+
if (n >= minStreak) {
|
|
220
|
+
flagged.push({ agent, streak: n, lastAt: agentLast[agent] });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return flagged;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const KANBAN_COLUMNS = COLUMNS;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peer review registry for BYAN agents working in party-mode.
|
|
3
|
+
*
|
|
4
|
+
* Contract :
|
|
5
|
+
* - An agent producing an artefact (commit, file change, spec) opens a
|
|
6
|
+
* review request via requestReview(). The request is persisted at
|
|
7
|
+
* _byan-output/reviews/<task_id>.json.
|
|
8
|
+
* - Another agent (must be ≠ author) issues a verdict via
|
|
9
|
+
* recordVerdict() with { verdict: approve | changes | block, comments,
|
|
10
|
+
* must_fix }.
|
|
11
|
+
* - listPending() returns all unresolved requests. pickReviewer()
|
|
12
|
+
* returns an alternative agent from the roster distinct from the
|
|
13
|
+
* author.
|
|
14
|
+
*
|
|
15
|
+
* Enforces the "reviewer must differ from author" invariant inside
|
|
16
|
+
* recordVerdict() and throws if violated.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_ROSTER = [
|
|
23
|
+
'bmad-bmm-architect',
|
|
24
|
+
'bmad-bmm-dev',
|
|
25
|
+
'bmad-bmm-quinn',
|
|
26
|
+
'bmad-bmm-pm',
|
|
27
|
+
'bmad-bmm-sm',
|
|
28
|
+
'bmad-bmm-analyst',
|
|
29
|
+
'bmad-bmm-ux-designer',
|
|
30
|
+
'bmad-bmm-tech-writer',
|
|
31
|
+
'bmad-tea-tea',
|
|
32
|
+
'bmad-compliance',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function resolveRoot(projectRoot) {
|
|
36
|
+
return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function reviewsDir(projectRoot) {
|
|
40
|
+
return path.join(resolveRoot(projectRoot), '_byan-output', 'reviews');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function reviewPath(projectRoot, taskId) {
|
|
44
|
+
return path.join(reviewsDir(projectRoot), `${sanitizeId(taskId)}.json`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sanitizeId(id) {
|
|
48
|
+
return String(id).replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 80);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readReview(projectRoot, taskId) {
|
|
52
|
+
const p = reviewPath(projectRoot, taskId);
|
|
53
|
+
if (!fs.existsSync(p)) return null;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeReview(projectRoot, review) {
|
|
62
|
+
fs.mkdirSync(reviewsDir(projectRoot), { recursive: true });
|
|
63
|
+
fs.writeFileSync(reviewPath(projectRoot, review.task_id), JSON.stringify(review, null, 2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function requestReview({
|
|
67
|
+
task_id,
|
|
68
|
+
author,
|
|
69
|
+
artifact_paths = [],
|
|
70
|
+
description = '',
|
|
71
|
+
projectRoot,
|
|
72
|
+
now = new Date(),
|
|
73
|
+
} = {}) {
|
|
74
|
+
if (!task_id) throw new Error('task_id is required');
|
|
75
|
+
if (!author) throw new Error('author (agent name) is required');
|
|
76
|
+
|
|
77
|
+
const existing = readReview(projectRoot, task_id);
|
|
78
|
+
if (existing && existing.status === 'pending') {
|
|
79
|
+
throw new Error(`review for task ${task_id} already pending (author ${existing.author})`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const review = {
|
|
83
|
+
task_id,
|
|
84
|
+
author,
|
|
85
|
+
artifact_paths: Array.isArray(artifact_paths) ? artifact_paths : [],
|
|
86
|
+
description: String(description || ''),
|
|
87
|
+
status: 'pending',
|
|
88
|
+
verdicts: [],
|
|
89
|
+
created_at: now.toISOString(),
|
|
90
|
+
updated_at: now.toISOString(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
writeReview(projectRoot, review);
|
|
94
|
+
return review;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function recordVerdict({
|
|
98
|
+
task_id,
|
|
99
|
+
reviewer,
|
|
100
|
+
verdict,
|
|
101
|
+
comments = [],
|
|
102
|
+
must_fix = [],
|
|
103
|
+
projectRoot,
|
|
104
|
+
now = new Date(),
|
|
105
|
+
} = {}) {
|
|
106
|
+
if (!task_id) throw new Error('task_id is required');
|
|
107
|
+
if (!reviewer) throw new Error('reviewer (agent name) is required');
|
|
108
|
+
if (!['approve', 'changes', 'block'].includes(verdict)) {
|
|
109
|
+
throw new Error(`verdict must be approve | changes | block, got ${verdict}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const review = readReview(projectRoot, task_id);
|
|
113
|
+
if (!review) throw new Error(`no review found for task ${task_id} — call requestReview first`);
|
|
114
|
+
|
|
115
|
+
if (review.author === reviewer) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`reviewer (${reviewer}) cannot be the same as author (${review.author}). Pick a different agent.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
review.verdicts.push({
|
|
122
|
+
reviewer,
|
|
123
|
+
verdict,
|
|
124
|
+
comments: Array.isArray(comments) ? comments : [],
|
|
125
|
+
must_fix: Array.isArray(must_fix) ? must_fix : [],
|
|
126
|
+
at: now.toISOString(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (verdict === 'approve') review.status = 'approved';
|
|
130
|
+
else if (verdict === 'block') review.status = 'blocked';
|
|
131
|
+
else review.status = 'changes_requested';
|
|
132
|
+
|
|
133
|
+
review.updated_at = now.toISOString();
|
|
134
|
+
writeReview(projectRoot, review);
|
|
135
|
+
return review;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getReview({ task_id, projectRoot } = {}) {
|
|
139
|
+
if (!task_id) throw new Error('task_id is required');
|
|
140
|
+
return readReview(projectRoot, task_id);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function listPending({ projectRoot } = {}) {
|
|
144
|
+
const dir = reviewsDir(projectRoot);
|
|
145
|
+
if (!fs.existsSync(dir)) return [];
|
|
146
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const f of files) {
|
|
149
|
+
try {
|
|
150
|
+
const r = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
151
|
+
if (r.status === 'pending' || r.status === 'changes_requested') out.push(r);
|
|
152
|
+
} catch {
|
|
153
|
+
// skip malformed
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
out.sort((a, b) => (a.created_at > b.created_at ? -1 : 1));
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function pickReviewer({ author, preferredDomain, roster = DEFAULT_ROSTER } = {}) {
|
|
161
|
+
const domainPairs = {
|
|
162
|
+
dev: ['bmad-bmm-quinn', 'bmad-tea-tea'],
|
|
163
|
+
'bmm-dev': ['bmad-bmm-quinn', 'bmad-tea-tea'],
|
|
164
|
+
'bmad-bmm-dev': ['bmad-bmm-quinn', 'bmad-tea-tea'],
|
|
165
|
+
architect: ['bmad-tea-tea', 'bmad-bmm-quinn'],
|
|
166
|
+
'bmad-bmm-architect': ['bmad-tea-tea', 'bmad-bmm-quinn'],
|
|
167
|
+
pm: ['bmad-bmm-sm', 'bmad-bmm-analyst'],
|
|
168
|
+
'bmad-bmm-pm': ['bmad-bmm-sm', 'bmad-bmm-analyst'],
|
|
169
|
+
'ux-designer': ['bmad-bmm-pm', 'bmad-bmm-analyst'],
|
|
170
|
+
'bmad-bmm-ux-designer': ['bmad-bmm-pm', 'bmad-bmm-analyst'],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const keys = [preferredDomain, author].filter(Boolean);
|
|
174
|
+
for (const k of keys) {
|
|
175
|
+
const candidates = domainPairs[k] || [];
|
|
176
|
+
for (const c of candidates) {
|
|
177
|
+
if (c !== author) return c;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const r of roster) {
|
|
182
|
+
if (r !== author) return r;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const DEFAULT_AGENT_ROSTER = DEFAULT_ROSTER;
|