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.
@@ -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
- };