dual-brain 7.1.2 → 7.1.4
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/bin/dual-brain.mjs +38 -28
- package/mcp-server/index.mjs +1 -1
- package/package.json +44 -4
- package/src/decide.mjs +32 -0
- package/src/index.mjs +1 -1
- package/src/profile.mjs +7 -4
- package/src/session.mjs +50 -10
- package/src/tui.mjs +10 -1
- package/hooks/agent-fleet.mjs +0 -659
- package/hooks/context-guard.mjs +0 -468
- package/hooks/dag-scheduler.mjs +0 -1249
- package/hooks/head-guard.sh +0 -41
- package/hooks/hook-dispatch.mjs +0 -254
- package/hooks/ledger-analysis.mjs +0 -337
- package/hooks/parallelism-scaler.mjs +0 -572
- package/hooks/quality-tiers.mjs +0 -642
- package/src/test.mjs +0 -1374
package/hooks/quality-tiers.mjs
DELETED
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* quality-tiers.mjs — Risk-based tiered quality gate for the Dual-Brain Orchestrator.
|
|
4
|
-
*
|
|
5
|
-
* Replaces the single quality-gate.mjs review-everything approach with a
|
|
6
|
-
* three-tier system that preserves head (Opus) bandwidth for genuinely
|
|
7
|
-
* high-risk work.
|
|
8
|
-
*
|
|
9
|
-
* Tiers:
|
|
10
|
-
* auto-pass — low risk: tests + lint only, no agent review
|
|
11
|
-
* peer-review — medium risk: tests + lint + sonnet agent review dispatch
|
|
12
|
-
* head-review — high/critical risk: tests + lint + peer + head summary
|
|
13
|
-
*
|
|
14
|
-
* Exports:
|
|
15
|
-
* classifyQualityTier(task)
|
|
16
|
-
* runAutoPass(task, options)
|
|
17
|
-
* runPeerReview(task, autoPassResult, options)
|
|
18
|
-
* runHeadReview(task, peerResult, options)
|
|
19
|
-
* runQualityPipeline(task, options)
|
|
20
|
-
* getQualityStats(manifest)
|
|
21
|
-
*
|
|
22
|
-
* CLI:
|
|
23
|
-
* node hooks/quality-tiers.mjs --classify '{"riskLevel":"medium",...}'
|
|
24
|
-
* node hooks/quality-tiers.mjs --stats <manifestId>
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { spawnSync } from 'child_process';
|
|
28
|
-
import { existsSync, readFileSync } from 'fs';
|
|
29
|
-
import { dirname, join, resolve } from 'path';
|
|
30
|
-
import { fileURLToPath } from 'url';
|
|
31
|
-
|
|
32
|
-
import { classifyRisk } from './risk-classifier.mjs';
|
|
33
|
-
|
|
34
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
-
const ROOT_DIR = join(__dirname, '..');
|
|
36
|
-
const MANIFEST_DIR = join(ROOT_DIR, '.dualbrain', 'manifests');
|
|
37
|
-
const ORCHESTRATOR_CONFIG = join(ROOT_DIR, 'orchestrator.json');
|
|
38
|
-
|
|
39
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
const LEVEL_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
42
|
-
|
|
43
|
-
const TIER_LABELS = {
|
|
44
|
-
'auto-pass': 'auto-pass',
|
|
45
|
-
'peer-review': 'peer-review',
|
|
46
|
-
'head-review': 'head-review',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/** Auth/security/billing path patterns that force head-review regardless of risk label */
|
|
50
|
-
const SENSITIVE_PATH_PATTERNS = [
|
|
51
|
-
/\b(auth|credential|secret|\.env|token|password|encrypt|certificate|\.pem|\.key|oauth|jwt)\b/i,
|
|
52
|
-
/\b(billing|payment|invoice|subscription|charge)\b/i,
|
|
53
|
-
/\b(security|permission|policy|role|access[-_]?control)\b/i,
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
/** Paths that indicate shared utilities (nudge toward peer-review) */
|
|
57
|
-
const SHARED_UTIL_PATTERNS = [
|
|
58
|
-
/\b(util[s]?|lib\/|shared\/|common\/|helper[s]?|middleware)\b/i,
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
function safeJsonParse(raw, fallback = null) {
|
|
64
|
-
try { return JSON.parse(raw); } catch { return fallback; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isoNow() {
|
|
68
|
-
return new Date().toISOString();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function touchesSensitivePaths(paths) {
|
|
72
|
-
return (paths || []).some(p =>
|
|
73
|
-
SENSITIVE_PATH_PATTERNS.some(rx => rx.test(p)),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function touchesSharedUtils(paths) {
|
|
78
|
-
return (paths || []).some(p =>
|
|
79
|
-
SHARED_UTIL_PATTERNS.some(rx => rx.test(p)),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function loadConfig() {
|
|
84
|
-
try {
|
|
85
|
-
return safeJsonParse(readFileSync(ORCHESTRATOR_CONFIG, 'utf8'), {});
|
|
86
|
-
} catch {
|
|
87
|
-
return {};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function runCmd(cmd, args, opts = {}) {
|
|
92
|
-
try {
|
|
93
|
-
const result = spawnSync(cmd, args, {
|
|
94
|
-
encoding: 'utf8',
|
|
95
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
-
timeout: opts.timeout || 60_000,
|
|
97
|
-
cwd: opts.cwd || ROOT_DIR,
|
|
98
|
-
});
|
|
99
|
-
return {
|
|
100
|
-
ok: result.status === 0,
|
|
101
|
-
stdout: result.stdout || '',
|
|
102
|
-
stderr: result.stderr || '',
|
|
103
|
-
status: result.status,
|
|
104
|
-
};
|
|
105
|
-
} catch (err) {
|
|
106
|
-
return { ok: false, stdout: '', stderr: err?.message || String(err), status: -1 };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function trimText(value, max = 200) {
|
|
111
|
-
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
112
|
-
if (text.length <= max) return text;
|
|
113
|
-
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ─── classifyQualityTier ──────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Determine which quality tier a completed task requires.
|
|
120
|
-
*
|
|
121
|
-
* @param {object} task
|
|
122
|
-
* @param {string} task.riskLevel — 'low' | 'medium' | 'high' | 'critical'
|
|
123
|
-
* @param {string} task.tier — 'search' | 'execute' | 'think'
|
|
124
|
-
* @param {string[]} task.owns — files the task exclusively owns
|
|
125
|
-
* @param {string[]} task.reads — files the task reads (shared)
|
|
126
|
-
* @param {object} task.result — prior execution result (optional)
|
|
127
|
-
* @param {number} task.fileCount — number of files changed (optional)
|
|
128
|
-
* @returns {'auto-pass'|'peer-review'|'head-review'}
|
|
129
|
-
*/
|
|
130
|
-
function classifyQualityTier(task) {
|
|
131
|
-
const {
|
|
132
|
-
riskLevel = 'low',
|
|
133
|
-
tier = 'execute',
|
|
134
|
-
owns = [],
|
|
135
|
-
reads = [],
|
|
136
|
-
result = {},
|
|
137
|
-
fileCount,
|
|
138
|
-
} = task;
|
|
139
|
-
|
|
140
|
-
const allFiles = [...(owns || []), ...(reads || [])];
|
|
141
|
-
const effectiveFileCount = fileCount ?? allFiles.length;
|
|
142
|
-
const riskIdx = LEVEL_ORDER[riskLevel] ?? 0;
|
|
143
|
-
|
|
144
|
-
// ── Immediate head-review triggers ──────────────────────────────────────────
|
|
145
|
-
if (riskIdx >= LEVEL_ORDER['high']) return 'head-review';
|
|
146
|
-
if (effectiveFileCount >= 4) return 'head-review';
|
|
147
|
-
if (touchesSensitivePaths(allFiles)) return 'head-review';
|
|
148
|
-
// think-tier tasks always warrant at least a peer look
|
|
149
|
-
if (tier === 'think' && riskIdx >= LEVEL_ORDER['medium']) return 'head-review';
|
|
150
|
-
|
|
151
|
-
// ── Peer-review triggers ─────────────────────────────────────────────────────
|
|
152
|
-
if (riskIdx >= LEVEL_ORDER['medium']) return 'peer-review';
|
|
153
|
-
if (effectiveFileCount >= 2 && effectiveFileCount <= 3) return 'peer-review';
|
|
154
|
-
if (touchesSharedUtils(allFiles)) return 'peer-review';
|
|
155
|
-
|
|
156
|
-
// ── Auto-pass (low risk, simple) ─────────────────────────────────────────────
|
|
157
|
-
return 'auto-pass';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ─── runAutoPass ──────────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Run automated checks: test runner + lint/typecheck.
|
|
164
|
-
* Does NOT invoke any review agent.
|
|
165
|
-
*
|
|
166
|
-
* @param {object} task
|
|
167
|
-
* @param {object} options
|
|
168
|
-
* @param {boolean} options.skipTests — skip test execution
|
|
169
|
-
* @param {boolean} options.autoEscalate — escalate tier on failure
|
|
170
|
-
* @returns {Promise<{tier, passed, checks, escalate}>}
|
|
171
|
-
*/
|
|
172
|
-
async function runAutoPass(task, options = {}) {
|
|
173
|
-
const { skipTests = false } = options;
|
|
174
|
-
const checks = [];
|
|
175
|
-
const config = loadConfig();
|
|
176
|
-
const allFiles = [...(task.owns || []), ...(task.reads || [])];
|
|
177
|
-
|
|
178
|
-
// ── Test check ───────────────────────────────────────────────────────────────
|
|
179
|
-
if (!skipTests) {
|
|
180
|
-
const testCmd = config?.quality_gate?.test_command || detectTestCommand();
|
|
181
|
-
if (testCmd) {
|
|
182
|
-
const [bin, ...args] = testCmd.split(/\s+/);
|
|
183
|
-
const testResult = runCmd(bin, args, { timeout: 90_000 });
|
|
184
|
-
checks.push({
|
|
185
|
-
name: 'tests',
|
|
186
|
-
passed: testResult.ok,
|
|
187
|
-
command: testCmd,
|
|
188
|
-
output: trimText(testResult.stdout || testResult.stderr, 300),
|
|
189
|
-
});
|
|
190
|
-
} else {
|
|
191
|
-
checks.push({ name: 'tests', passed: true, skipped: true, reason: 'no test command found' });
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
checks.push({ name: 'tests', passed: true, skipped: true, reason: 'skipTests=true' });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ── Lint/typecheck ───────────────────────────────────────────────────────────
|
|
198
|
-
const lintCmd = config?.quality_gate?.lint_command || detectLintCommand();
|
|
199
|
-
if (lintCmd) {
|
|
200
|
-
const [bin, ...args] = lintCmd.split(/\s+/);
|
|
201
|
-
const lintResult = runCmd(bin, args, { timeout: 60_000 });
|
|
202
|
-
checks.push({
|
|
203
|
-
name: 'lint',
|
|
204
|
-
passed: lintResult.ok,
|
|
205
|
-
command: lintCmd,
|
|
206
|
-
output: trimText(lintResult.stdout || lintResult.stderr, 200),
|
|
207
|
-
});
|
|
208
|
-
} else {
|
|
209
|
-
checks.push({ name: 'lint', passed: true, skipped: true, reason: 'no lint command configured' });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ── Ownership conflict check ──────────────────────────────────────────────────
|
|
213
|
-
const ownershipOk = checkOwnershipConflicts(task, allFiles);
|
|
214
|
-
checks.push({
|
|
215
|
-
name: 'ownership',
|
|
216
|
-
passed: ownershipOk.ok,
|
|
217
|
-
details: ownershipOk.details,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const allPassed = checks.every(c => c.passed);
|
|
221
|
-
const escalate = !allPassed;
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
tier: 'auto-pass',
|
|
225
|
-
passed: allPassed,
|
|
226
|
-
checks,
|
|
227
|
-
escalate,
|
|
228
|
-
timestamp: isoNow(),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ─── runPeerReview ────────────────────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Build a peer-review dispatch config for a sonnet agent.
|
|
236
|
-
* Does NOT execute the dispatch — returns the config for the caller to run.
|
|
237
|
-
*
|
|
238
|
-
* @param {object} task
|
|
239
|
-
* @param {object} autoPassResult — result from runAutoPass
|
|
240
|
-
* @param {object} options
|
|
241
|
-
* @param {boolean} options.autoEscalate
|
|
242
|
-
* @returns {Promise<{tier, passed, checks, peerFeedback, dispatchConfig, escalate}>}
|
|
243
|
-
*/
|
|
244
|
-
async function runPeerReview(task, autoPassResult, options = {}) {
|
|
245
|
-
const { autoEscalate = true } = options;
|
|
246
|
-
const checks = [...(autoPassResult?.checks || [])];
|
|
247
|
-
|
|
248
|
-
// Build a focused review prompt for the peer (sonnet) agent
|
|
249
|
-
const allFiles = [...(task.owns || []), ...(task.reads || [])];
|
|
250
|
-
const autoSummary = summarizeAutoPass(autoPassResult);
|
|
251
|
-
|
|
252
|
-
const reviewPrompt = buildPeerReviewPrompt({
|
|
253
|
-
task,
|
|
254
|
-
allFiles,
|
|
255
|
-
autoSummary,
|
|
256
|
-
riskLevel: task.riskLevel || 'medium',
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Dispatch config: caller executes this
|
|
260
|
-
const dispatchConfig = {
|
|
261
|
-
provider: 'claude',
|
|
262
|
-
model: 'sonnet',
|
|
263
|
-
tier: 'think',
|
|
264
|
-
prompt: reviewPrompt,
|
|
265
|
-
files: allFiles,
|
|
266
|
-
timeoutMs: 120_000,
|
|
267
|
-
returnStructured: true,
|
|
268
|
-
structuredInstructions: [
|
|
269
|
-
'Return JSON: { "concerns": string[], "verdict": "pass"|"escalate", "summary": string }',
|
|
270
|
-
'"concerns" lists specific issues found (empty array if none)',
|
|
271
|
-
'"verdict" is "escalate" if any concern warrants head review, otherwise "pass"',
|
|
272
|
-
'"summary" is 1-2 sentences compressed for head context (under 200 chars)',
|
|
273
|
-
].join('\n'),
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// Placeholder peer feedback — populated when caller executes dispatch
|
|
277
|
-
const peerFeedback = null;
|
|
278
|
-
const escalate = false; // caller sets this after executing dispatch
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
tier: 'peer-review',
|
|
282
|
-
passed: autoPassResult?.passed ?? true,
|
|
283
|
-
checks,
|
|
284
|
-
peerFeedback,
|
|
285
|
-
dispatchConfig,
|
|
286
|
-
escalate,
|
|
287
|
-
timestamp: isoNow(),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── runHeadReview ────────────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Build a structured review request for the head agent (Opus).
|
|
295
|
-
* Does NOT perform the review — returns the request summary for the head to act on.
|
|
296
|
-
*
|
|
297
|
-
* @param {object} task
|
|
298
|
-
* @param {object} peerResult — result from runPeerReview (with peerFeedback populated)
|
|
299
|
-
* @param {object} options
|
|
300
|
-
* @returns {Promise<{tier, passed, checks, peerFeedback, headVerdict, headReviewRequest}>}
|
|
301
|
-
*/
|
|
302
|
-
async function runHeadReview(task, peerResult, options = {}) {
|
|
303
|
-
const checks = [...(peerResult?.checks || [])];
|
|
304
|
-
const allFiles = [...(task.owns || []), ...(task.reads || [])];
|
|
305
|
-
|
|
306
|
-
// Compress all prior results for head context
|
|
307
|
-
const compressedChecks = compressPriorChecks(checks);
|
|
308
|
-
const peerSummary = peerResult?.peerFeedback
|
|
309
|
-
? trimText(String(peerResult.peerFeedback), 400)
|
|
310
|
-
: 'peer review not yet executed';
|
|
311
|
-
|
|
312
|
-
const headReviewRequest = {
|
|
313
|
-
task: trimText(task.description || task.intent || 'unknown task', 120),
|
|
314
|
-
riskLevel: task.riskLevel || 'high',
|
|
315
|
-
filesChanged: allFiles,
|
|
316
|
-
fileCount: allFiles.length,
|
|
317
|
-
automatedChecks: compressedChecks,
|
|
318
|
-
peerSummary,
|
|
319
|
-
focusAreas: buildHeadFocusAreas(task, peerResult),
|
|
320
|
-
instructions: [
|
|
321
|
-
'Review architecture alignment: does this fit the established patterns?',
|
|
322
|
-
'Security implications: any vectors opened even indirectly?',
|
|
323
|
-
'Cross-cutting concerns: does this affect other subsystems?',
|
|
324
|
-
'Return verdict: pass | issues_found | needs_rework, with specific findings.',
|
|
325
|
-
],
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
tier: 'head-review',
|
|
330
|
-
passed: false, // head sets this after review
|
|
331
|
-
checks,
|
|
332
|
-
peerFeedback: peerResult?.peerFeedback || null,
|
|
333
|
-
headVerdict: null, // populated after head reviews headReviewRequest
|
|
334
|
-
headReviewRequest,
|
|
335
|
-
timestamp: isoNow(),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ─── runQualityPipeline ───────────────────────────────────────────────────────
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Full quality pipeline: classify → auto-pass → [peer-review] → [head-review].
|
|
343
|
-
* Escalates automatically on failures when autoEscalate=true.
|
|
344
|
-
*
|
|
345
|
-
* @param {object} task
|
|
346
|
-
* @param {object} options
|
|
347
|
-
* @param {boolean} options.skipTests — skip test execution
|
|
348
|
-
* @param {boolean} options.strictMode — bump all tiers one level up
|
|
349
|
-
* @param {boolean} options.autoEscalate — auto-escalate on failures (default true)
|
|
350
|
-
* @returns {Promise<{tier, passed, checks, peerFeedback?, headVerdict?, headReviewRequest?, escalated}>}
|
|
351
|
-
*/
|
|
352
|
-
async function runQualityPipeline(task, options = {}) {
|
|
353
|
-
const { skipTests = false, strictMode = false, autoEscalate = true } = options;
|
|
354
|
-
|
|
355
|
-
// 1. Classify tier
|
|
356
|
-
let tier = classifyQualityTier(task);
|
|
357
|
-
|
|
358
|
-
// strictMode bumps everyone up one level
|
|
359
|
-
if (strictMode) {
|
|
360
|
-
tier = bumpTier(tier);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// 2. Always run auto-pass first (lowest common denominator)
|
|
364
|
-
const autoResult = await runAutoPass(task, { skipTests, autoEscalate });
|
|
365
|
-
|
|
366
|
-
// 3. Escalate from auto-pass if tests/lint failed
|
|
367
|
-
if (tier === 'auto-pass' && autoResult.escalate && autoEscalate) {
|
|
368
|
-
tier = 'peer-review';
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (tier === 'auto-pass') {
|
|
372
|
-
return { ...autoResult, escalated: false };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 4. Peer review
|
|
376
|
-
const peerResult = await runPeerReview(task, autoResult, { autoEscalate });
|
|
377
|
-
|
|
378
|
-
if (tier === 'peer-review') {
|
|
379
|
-
// Caller must execute peerResult.dispatchConfig and populate peerFeedback.
|
|
380
|
-
// We return the dispatch config so the caller can run it and re-call if escalation needed.
|
|
381
|
-
return { ...peerResult, escalated: tier !== classifyQualityTier(task) };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// 5. Head review (high/critical)
|
|
385
|
-
const headResult = await runHeadReview(task, peerResult, options);
|
|
386
|
-
return { ...headResult, escalated: tier !== classifyQualityTier(task) };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ─── getQualityStats ──────────────────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Aggregate quality tier results across a manifest.
|
|
393
|
-
*
|
|
394
|
-
* @param {object} manifest — wave-orchestrator manifest object, or a manifestId string
|
|
395
|
-
* @returns {object} stats
|
|
396
|
-
*/
|
|
397
|
-
function getQualityStats(manifest) {
|
|
398
|
-
// Accept either a manifest object or a raw manifestId string
|
|
399
|
-
if (typeof manifest === 'string') {
|
|
400
|
-
const path = join(MANIFEST_DIR, `${manifest}.json`);
|
|
401
|
-
if (!existsSync(path)) {
|
|
402
|
-
return { error: `Manifest not found: ${manifest}` };
|
|
403
|
-
}
|
|
404
|
-
manifest = safeJsonParse(readFileSync(path, 'utf8'), null);
|
|
405
|
-
if (!manifest) return { error: 'Manifest is unreadable' };
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const tasks = (manifest.waves || []).flatMap(w => w.tasks || []);
|
|
409
|
-
const stats = {
|
|
410
|
-
total: tasks.length,
|
|
411
|
-
autoPass: 0,
|
|
412
|
-
peerReview: 0,
|
|
413
|
-
headReview: 0,
|
|
414
|
-
escalated: 0,
|
|
415
|
-
failed: 0,
|
|
416
|
-
passed: 0,
|
|
417
|
-
byRisk: { low: 0, medium: 0, high: 0, critical: 0 },
|
|
418
|
-
tierBreakdown: {},
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
for (const task of tasks) {
|
|
422
|
-
const qr = task.qualityResult;
|
|
423
|
-
if (!qr) continue;
|
|
424
|
-
|
|
425
|
-
const t = qr.tier || 'auto-pass';
|
|
426
|
-
stats.tierBreakdown[t] = (stats.tierBreakdown[t] || 0) + 1;
|
|
427
|
-
|
|
428
|
-
if (t === 'auto-pass') stats.autoPass++;
|
|
429
|
-
if (t === 'peer-review') stats.peerReview++;
|
|
430
|
-
if (t === 'head-review') stats.headReview++;
|
|
431
|
-
if (qr.escalated) stats.escalated++;
|
|
432
|
-
if (qr.passed === false) stats.failed++;
|
|
433
|
-
if (qr.passed === true) stats.passed++;
|
|
434
|
-
|
|
435
|
-
const risk = task.riskLevel || 'low';
|
|
436
|
-
if (stats.byRisk[risk] !== undefined) stats.byRisk[risk]++;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
stats.headSavingsRate = stats.total > 0
|
|
440
|
-
? ((stats.autoPass + stats.peerReview) / stats.total).toFixed(2)
|
|
441
|
-
: '1.00';
|
|
442
|
-
|
|
443
|
-
return stats;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
447
|
-
|
|
448
|
-
function bumpTier(tier) {
|
|
449
|
-
if (tier === 'auto-pass') return 'peer-review';
|
|
450
|
-
if (tier === 'peer-review') return 'head-review';
|
|
451
|
-
return 'head-review';
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function detectTestCommand() {
|
|
455
|
-
if (existsSync(join(ROOT_DIR, 'package.json'))) {
|
|
456
|
-
const pkg = safeJsonParse(readFileSync(join(ROOT_DIR, 'package.json'), 'utf8'), {});
|
|
457
|
-
if (pkg?.scripts?.test) return 'npm test';
|
|
458
|
-
if (pkg?.scripts?.['test:ci']) return 'npm run test:ci';
|
|
459
|
-
}
|
|
460
|
-
if (existsSync(join(ROOT_DIR, 'Makefile'))) return 'make test';
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function detectLintCommand() {
|
|
465
|
-
if (existsSync(join(ROOT_DIR, 'package.json'))) {
|
|
466
|
-
const pkg = safeJsonParse(readFileSync(join(ROOT_DIR, 'package.json'), 'utf8'), {});
|
|
467
|
-
if (pkg?.scripts?.lint) return 'npm run lint';
|
|
468
|
-
if (pkg?.scripts?.typecheck) return 'npm run typecheck';
|
|
469
|
-
}
|
|
470
|
-
if (existsSync(join(ROOT_DIR, '.eslintrc.js')) || existsSync(join(ROOT_DIR, '.eslintrc.json'))) {
|
|
471
|
-
return 'npx eslint .';
|
|
472
|
-
}
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function checkOwnershipConflicts(task, allFiles) {
|
|
477
|
-
// Simple heuristic: flag if reads[] overlaps with owns[] from another task
|
|
478
|
-
// In a real manifest context the wave-orchestrator tracks this; here we just
|
|
479
|
-
// check if the same file appears in both owns and reads (self-conflict).
|
|
480
|
-
const owns = new Set(task.owns || []);
|
|
481
|
-
const reads = task.reads || [];
|
|
482
|
-
const conflicts = reads.filter(f => owns.has(f));
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
ok: conflicts.length === 0,
|
|
486
|
-
details: conflicts.length > 0
|
|
487
|
-
? `Files in both owns and reads: ${conflicts.join(', ')}`
|
|
488
|
-
: 'no conflicts',
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function summarizeAutoPass(autoPassResult) {
|
|
493
|
-
if (!autoPassResult) return 'no auto-pass results available';
|
|
494
|
-
const { checks = [], passed } = autoPassResult;
|
|
495
|
-
const lines = checks.map(c => {
|
|
496
|
-
if (c.skipped) return `${c.name}: skipped`;
|
|
497
|
-
return `${c.name}: ${c.passed ? 'pass' : 'FAIL'}${c.output ? ` — ${trimText(c.output, 80)}` : ''}`;
|
|
498
|
-
});
|
|
499
|
-
return `auto-pass: ${passed ? 'passed' : 'failed'}\n${lines.join('\n')}`;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function buildPeerReviewPrompt({ task, allFiles, autoSummary, riskLevel }) {
|
|
503
|
-
const description = task.description || task.intent || 'unknown task';
|
|
504
|
-
const owns = (task.owns || []).join(', ') || 'none';
|
|
505
|
-
const reads = (task.reads || []).join(', ') || 'none';
|
|
506
|
-
|
|
507
|
-
return [
|
|
508
|
-
`You are a peer code reviewer. Review the following completed task for correctness, edge cases, and unintended side effects.`,
|
|
509
|
-
``,
|
|
510
|
-
`## Task`,
|
|
511
|
-
`Description: ${trimText(description, 200)}`,
|
|
512
|
-
`Risk level: ${riskLevel}`,
|
|
513
|
-
`Files owned (edited): ${owns}`,
|
|
514
|
-
`Files read (referenced): ${reads}`,
|
|
515
|
-
``,
|
|
516
|
-
`## Automated Check Results`,
|
|
517
|
-
autoSummary,
|
|
518
|
-
``,
|
|
519
|
-
`## What to Check`,
|
|
520
|
-
`1. Correctness — does the implementation match the stated intent?`,
|
|
521
|
-
`2. Edge cases — what inputs or states could break this?`,
|
|
522
|
-
`3. Unintended side effects — does this affect other subsystems?`,
|
|
523
|
-
`4. Does the risk classification (${riskLevel}) seem accurate?`,
|
|
524
|
-
``,
|
|
525
|
-
`Return JSON only:`,
|
|
526
|
-
`{ "concerns": string[], "verdict": "pass"|"escalate", "summary": string }`,
|
|
527
|
-
`"verdict" must be "escalate" if any concern warrants head (Opus) review.`,
|
|
528
|
-
`"summary" must be under 200 chars for compression into head context.`,
|
|
529
|
-
].join('\n');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function compressPriorChecks(checks) {
|
|
533
|
-
return (checks || []).map(c => {
|
|
534
|
-
if (c.skipped) return `${c.name}: skipped`;
|
|
535
|
-
const status = c.passed ? 'pass' : 'FAIL';
|
|
536
|
-
const detail = c.output ? ` (${trimText(c.output, 60)})` : '';
|
|
537
|
-
return `${c.name}: ${status}${detail}`;
|
|
538
|
-
}).join('; ');
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function buildHeadFocusAreas(task, peerResult) {
|
|
542
|
-
const areas = [];
|
|
543
|
-
const riskLevel = task.riskLevel || 'high';
|
|
544
|
-
const allFiles = [...(task.owns || []), ...(task.reads || [])];
|
|
545
|
-
|
|
546
|
-
if (touchesSensitivePaths(allFiles)) {
|
|
547
|
-
areas.push('security — touches auth/credential/billing paths');
|
|
548
|
-
}
|
|
549
|
-
if (LEVEL_ORDER[riskLevel] >= LEVEL_ORDER['critical']) {
|
|
550
|
-
areas.push('critical risk — requires explicit approval before merge');
|
|
551
|
-
}
|
|
552
|
-
if (peerResult?.peerFeedback) {
|
|
553
|
-
const feedback = String(peerResult.peerFeedback);
|
|
554
|
-
if (/escalate|concern|issue|problem|risk/i.test(feedback)) {
|
|
555
|
-
areas.push(`peer flagged concerns: ${trimText(feedback, 120)}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if ((task.owns || []).length >= 4) {
|
|
559
|
-
areas.push(`large changeset: ${task.owns.length} files owned`);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return areas.length > 0 ? areas : ['standard high-risk review — architecture + security'];
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
566
|
-
|
|
567
|
-
if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
|
|
568
|
-
const args = process.argv.slice(2);
|
|
569
|
-
|
|
570
|
-
function exit(obj) {
|
|
571
|
-
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
572
|
-
process.exit(0);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function exitErr(msg) {
|
|
576
|
-
process.stderr.write(`error: ${msg}\n`);
|
|
577
|
-
process.exit(1);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const classifyIdx = args.indexOf('--classify');
|
|
581
|
-
const statsIdx = args.indexOf('--stats');
|
|
582
|
-
const pipelineIdx = args.indexOf('--pipeline');
|
|
583
|
-
const helpIdx = args.indexOf('--help');
|
|
584
|
-
|
|
585
|
-
if (helpIdx !== -1 || args.length === 0) {
|
|
586
|
-
process.stdout.write([
|
|
587
|
-
'Usage:',
|
|
588
|
-
' node hooks/quality-tiers.mjs --classify \'{"riskLevel":"medium","tier":"execute","owns":["src/utils.mjs"]}\'',
|
|
589
|
-
' node hooks/quality-tiers.mjs --stats <manifestId>',
|
|
590
|
-
' node hooks/quality-tiers.mjs --pipeline \'{"riskLevel":"low","owns":["src/foo.mjs"]}\' [--skip-tests] [--strict]',
|
|
591
|
-
'',
|
|
592
|
-
'Options:',
|
|
593
|
-
' --classify <json> Classify a task and return the quality tier',
|
|
594
|
-
' --stats <id> Aggregate quality stats from a manifest',
|
|
595
|
-
' --pipeline <json> Run the full quality pipeline for a task',
|
|
596
|
-
' --skip-tests Skip test execution in pipeline',
|
|
597
|
-
' --strict Bump all tiers one level up',
|
|
598
|
-
].join('\n') + '\n');
|
|
599
|
-
process.exit(0);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (classifyIdx !== -1) {
|
|
603
|
-
const raw = args[classifyIdx + 1];
|
|
604
|
-
if (!raw) exitErr('--classify requires a JSON argument');
|
|
605
|
-
const task = safeJsonParse(raw, null);
|
|
606
|
-
if (!task) exitErr('--classify argument is not valid JSON');
|
|
607
|
-
const tier = classifyQualityTier(task);
|
|
608
|
-
exit({ tier, task });
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (statsIdx !== -1) {
|
|
612
|
-
const manifestId = args[statsIdx + 1];
|
|
613
|
-
if (!manifestId || manifestId.startsWith('--')) exitErr('--stats requires a manifestId argument');
|
|
614
|
-
const stats = getQualityStats(manifestId);
|
|
615
|
-
exit(stats);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (pipelineIdx !== -1) {
|
|
619
|
-
const raw = args[pipelineIdx + 1];
|
|
620
|
-
if (!raw) exitErr('--pipeline requires a JSON argument');
|
|
621
|
-
const task = safeJsonParse(raw, null);
|
|
622
|
-
if (!task) exitErr('--pipeline argument is not valid JSON');
|
|
623
|
-
const skipTests = args.includes('--skip-tests');
|
|
624
|
-
const strictMode = args.includes('--strict');
|
|
625
|
-
runQualityPipeline(task, { skipTests, strictMode, autoEscalate: true })
|
|
626
|
-
.then(exit)
|
|
627
|
-
.catch(err => exitErr(err?.message || String(err)));
|
|
628
|
-
} else if (classifyIdx === -1 && statsIdx === -1) {
|
|
629
|
-
exitErr('Unknown command. Use --help for usage.');
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
634
|
-
|
|
635
|
-
export {
|
|
636
|
-
classifyQualityTier,
|
|
637
|
-
runAutoPass,
|
|
638
|
-
runPeerReview,
|
|
639
|
-
runHeadReview,
|
|
640
|
-
runQualityPipeline,
|
|
641
|
-
getQualityStats,
|
|
642
|
-
};
|