@hone-ai/cli 1.7.1 → 1.8.1
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/hone-cli.js +404 -0
- package/lib/eval-contracts.js +41 -0
- package/package.json +1 -1
package/hone-cli.js
CHANGED
|
@@ -4235,6 +4235,410 @@ program
|
|
|
4235
4235
|
process.exit(results.failed + results.errors > 0 ? 1 : 0);
|
|
4236
4236
|
});
|
|
4237
4237
|
|
|
4238
|
+
// ── HC-041: Run Story (Orchestrator) ─────────────────────────────────────────
|
|
4239
|
+
program
|
|
4240
|
+
.command('run-story <storyId>')
|
|
4241
|
+
.description('Run the SDLC pipeline for a story via the orchestrator')
|
|
4242
|
+
.option('--mode <mode>', 'Execution mode: interactive or batch', 'interactive')
|
|
4243
|
+
.option('--repo <name>', 'Repository name override (default: directory name)')
|
|
4244
|
+
.option('--branch <name>', 'Git branch (default: current)')
|
|
4245
|
+
.option('--status', 'Show status of the latest workflow run for this story')
|
|
4246
|
+
.option('--kill', 'Kill the latest workflow run for this story')
|
|
4247
|
+
.option('--approve <step>', 'Approve a paused gate (e.g., --approve step_0)')
|
|
4248
|
+
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
4249
|
+
.option('--poll-interval <s>', 'Poll interval in seconds', '5')
|
|
4250
|
+
.action(async (storyIdOrRunId, opts) => {
|
|
4251
|
+
const config = getConfig();
|
|
4252
|
+
const client = api(config);
|
|
4253
|
+
|
|
4254
|
+
// Resolve: if it looks like a UUID, use as runId directly.
|
|
4255
|
+
// Otherwise treat as storyId — will be used for POST /orchestrate body.
|
|
4256
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(storyIdOrRunId);
|
|
4257
|
+
const storyId = isUuid ? null : storyIdOrRunId;
|
|
4258
|
+
const runId = isUuid ? storyIdOrRunId : null;
|
|
4259
|
+
|
|
4260
|
+
// Status mode
|
|
4261
|
+
if (opts.status) {
|
|
4262
|
+
const statusId = runId || storyIdOrRunId;
|
|
4263
|
+
try {
|
|
4264
|
+
const { data } = await client.get(`/orchestrate/${statusId}`);
|
|
4265
|
+
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
4266
|
+
console.log('');
|
|
4267
|
+
console.log(`Workflow: ${data.runId}`);
|
|
4268
|
+
console.log(`Story: ${data.storyId}`);
|
|
4269
|
+
console.log(`Status: ${data.status}`);
|
|
4270
|
+
console.log(`Mode: ${data.mode}`);
|
|
4271
|
+
console.log(`Tokens: ${(data.totalTokens || 0).toLocaleString()}`);
|
|
4272
|
+
if (data.errorMessage) console.log(`Error: ${data.errorMessage}`);
|
|
4273
|
+
console.log('');
|
|
4274
|
+
console.log('Steps:');
|
|
4275
|
+
for (const s of (data.steps || [])) {
|
|
4276
|
+
const icon = s.status === 'completed' ? '✓' : s.status === 'running' ? '⏳' : s.status === 'failed' ? '✗' : '⬜';
|
|
4277
|
+
const gate = s.gateResult ? ` (${s.gateResult})` : '';
|
|
4278
|
+
console.log(` ${icon} ${s.stepKey} (${s.agent}) ${s.status}${gate}`);
|
|
4279
|
+
}
|
|
4280
|
+
if (data.awaitingApproval) {
|
|
4281
|
+
console.log('');
|
|
4282
|
+
console.log(`Awaiting approval at ${data.currentStep}.`);
|
|
4283
|
+
console.log(`Approve: hone run-story ${data.runId} --approve ${data.currentStep}`);
|
|
4284
|
+
}
|
|
4285
|
+
console.log('');
|
|
4286
|
+
} catch (e) {
|
|
4287
|
+
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
4288
|
+
process.exit(1);
|
|
4289
|
+
}
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
// Kill mode
|
|
4294
|
+
if (opts.kill) {
|
|
4295
|
+
const killId = runId || storyIdOrRunId;
|
|
4296
|
+
try {
|
|
4297
|
+
const { data } = await client.post(`/orchestrate/${killId}/kill`, { reason: 'CLI kill' });
|
|
4298
|
+
console.log(`Workflow ${data.runId} killed.`);
|
|
4299
|
+
} catch (e) {
|
|
4300
|
+
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
4301
|
+
process.exit(1);
|
|
4302
|
+
}
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
// Approve mode
|
|
4307
|
+
if (opts.approve) {
|
|
4308
|
+
const approveId = runId || storyIdOrRunId;
|
|
4309
|
+
try {
|
|
4310
|
+
const { data } = await client.post(`/orchestrate/${approveId}/approve`, { stepKey: opts.approve });
|
|
4311
|
+
console.log(`Gate ${opts.approve} approved. Workflow resuming.`);
|
|
4312
|
+
console.log(`Poll: hone run-story ${approveId} --status`);
|
|
4313
|
+
} catch (e) {
|
|
4314
|
+
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
4315
|
+
process.exit(1);
|
|
4316
|
+
}
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
// Start workflow (storyId required, runId not accepted for starting)
|
|
4321
|
+
if (isUuid) {
|
|
4322
|
+
console.error('Cannot start workflow with a runId. Use a story ID like HC-042.');
|
|
4323
|
+
console.error('To check status: hone run-story <runId> --status');
|
|
4324
|
+
process.exit(1);
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
const repoName = opts.repo || path.basename(process.cwd());
|
|
4328
|
+
let branch = opts.branch;
|
|
4329
|
+
if (!branch) {
|
|
4330
|
+
try { branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8' }).trim(); } catch { branch = null; }
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
try {
|
|
4334
|
+
const { data } = await client.post('/orchestrate', {
|
|
4335
|
+
storyId: storyIdOrRunId, repoName, branch, mode: opts.mode, config: {},
|
|
4336
|
+
});
|
|
4337
|
+
|
|
4338
|
+
console.log('');
|
|
4339
|
+
console.log(`Workflow started: ${data.runId}`);
|
|
4340
|
+
console.log(`Mode: ${data.mode} | Story: ${data.storyId}`);
|
|
4341
|
+
console.log(`Poll: ${data.pollUrl}`);
|
|
4342
|
+
console.log('');
|
|
4343
|
+
|
|
4344
|
+
// Poll loop
|
|
4345
|
+
const interval = parseInt(opts.pollInterval, 10) * 1000;
|
|
4346
|
+
while (true) {
|
|
4347
|
+
await new Promise(r => setTimeout(r, interval));
|
|
4348
|
+
|
|
4349
|
+
let status;
|
|
4350
|
+
try {
|
|
4351
|
+
const { data: pollData } = await client.get(`/orchestrate/${data.runId}`);
|
|
4352
|
+
status = pollData;
|
|
4353
|
+
} catch (e) {
|
|
4354
|
+
console.error(`Poll error: ${e.message}`);
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
if (status.status === 'completed') {
|
|
4359
|
+
console.log(`✓ Workflow completed! Total tokens: ${(status.totalTokens || 0).toLocaleString()}`);
|
|
4360
|
+
process.exit(0);
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
if (status.status === 'failed' || status.status === 'killed') {
|
|
4364
|
+
console.error(`✗ Workflow ${status.status}: ${status.errorMessage || 'unknown'}`);
|
|
4365
|
+
process.exit(1);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
if (status.status === 'paused' && status.awaitingApproval) {
|
|
4369
|
+
const step = status.steps.find(s => s.stepKey === status.currentStep);
|
|
4370
|
+
console.log(`⏳ Step ${status.currentStep} (${step?.agent || 'unknown'}) — awaiting approval`);
|
|
4371
|
+
|
|
4372
|
+
// Interactive prompt
|
|
4373
|
+
if (process.stdin.isTTY) {
|
|
4374
|
+
const readline = require('readline');
|
|
4375
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4376
|
+
const answer = await new Promise(resolve => {
|
|
4377
|
+
rl.question(' Approve? [y/n/kill] ', resolve);
|
|
4378
|
+
});
|
|
4379
|
+
rl.close();
|
|
4380
|
+
|
|
4381
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
4382
|
+
try {
|
|
4383
|
+
await client.post(`/orchestrate/${data.runId}/approve`, { stepKey: status.currentStep });
|
|
4384
|
+
console.log(' Approved — workflow resuming...');
|
|
4385
|
+
} catch (e) {
|
|
4386
|
+
console.error(` Approve failed: ${e.response?.data?.error || e.message}`);
|
|
4387
|
+
}
|
|
4388
|
+
} else if (answer.toLowerCase() === 'kill') {
|
|
4389
|
+
try {
|
|
4390
|
+
await client.post(`/orchestrate/${data.runId}/kill`, { reason: 'CLI kill' });
|
|
4391
|
+
console.log(' Workflow killed.');
|
|
4392
|
+
process.exit(0);
|
|
4393
|
+
} catch (e) {
|
|
4394
|
+
console.error(` Kill failed: ${e.message}`);
|
|
4395
|
+
}
|
|
4396
|
+
} else {
|
|
4397
|
+
console.log(` Skipped. Resume later: hone run-story ${data.runId} --status`);
|
|
4398
|
+
process.exit(0);
|
|
4399
|
+
}
|
|
4400
|
+
} else {
|
|
4401
|
+
// Non-interactive (CI): exit with code 2 (paused, needs approval)
|
|
4402
|
+
console.log(` Non-interactive mode. Approve with: hone run-story ${data.runId} --approve ${status.currentStep}`);
|
|
4403
|
+
process.exit(2);
|
|
4404
|
+
}
|
|
4405
|
+
continue;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
// Still running
|
|
4409
|
+
const currentStep = status.steps.find(s => s.status === 'running');
|
|
4410
|
+
if (currentStep) {
|
|
4411
|
+
process.stdout.write(` Running: ${currentStep.stepKey} (${currentStep.agent})...\r`);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
} catch (e) {
|
|
4415
|
+
if (e.response?.status === 429) {
|
|
4416
|
+
console.error(`Max concurrent workflows reached. ${e.response.data.error}`);
|
|
4417
|
+
} else {
|
|
4418
|
+
console.error(`Failed to start: ${e.response?.data?.error || e.message}`);
|
|
4419
|
+
}
|
|
4420
|
+
process.exit(1);
|
|
4421
|
+
}
|
|
4422
|
+
});
|
|
4423
|
+
|
|
4424
|
+
// SIGINT handler for run-story
|
|
4425
|
+
process.on('SIGINT', () => {
|
|
4426
|
+
console.log('\n\nWorkflow continues on the server.');
|
|
4427
|
+
console.log('Resume: hone run-story <storyId> --status');
|
|
4428
|
+
console.log('Kill: hone run-story <storyId> --kill');
|
|
4429
|
+
process.exit(0);
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4432
|
+
// ── Release Review (pre-deployment holistic review) ──────────────────────────
|
|
4433
|
+
program
|
|
4434
|
+
.command('release-review')
|
|
4435
|
+
.description('Holistic code review of all changed files before deployment (runs Opus)')
|
|
4436
|
+
.option('--base <branch>', 'Base branch to diff against', 'main')
|
|
4437
|
+
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
4438
|
+
.option('--dry-run', 'Show what would be reviewed without calling the LLM', false)
|
|
4439
|
+
.option('--max-files <n>', 'Max source files to include in review', '40')
|
|
4440
|
+
.action(async (opts) => {
|
|
4441
|
+
const { execSync } = require('child_process');
|
|
4442
|
+
const fs = require('fs');
|
|
4443
|
+
const repoRoot = process.cwd();
|
|
4444
|
+
|
|
4445
|
+
// 1. Get changed files
|
|
4446
|
+
let changedFiles;
|
|
4447
|
+
try {
|
|
4448
|
+
const raw = execSync(`git diff --name-only origin/${opts.base}...HEAD`, { encoding: 'utf8', cwd: repoRoot });
|
|
4449
|
+
changedFiles = raw.trim().split('\n').filter(Boolean);
|
|
4450
|
+
} catch {
|
|
4451
|
+
try {
|
|
4452
|
+
const raw = execSync('git diff --name-only HEAD~10', { encoding: 'utf8', cwd: repoRoot });
|
|
4453
|
+
changedFiles = raw.trim().split('\n').filter(Boolean);
|
|
4454
|
+
} catch {
|
|
4455
|
+
console.error('Could not determine changed files. Run from a git repo.');
|
|
4456
|
+
process.exit(1);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
if (changedFiles.length === 0) {
|
|
4461
|
+
console.log('No changed files found. Nothing to review.');
|
|
4462
|
+
process.exit(0);
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
// 2. Filter to source files
|
|
4466
|
+
const sourceExts = ['.js', '.ts', '.py', '.go', '.java', '.rb', '.rs', '.sql', '.yml', '.yaml'];
|
|
4467
|
+
const sourceFiles = changedFiles.filter(f =>
|
|
4468
|
+
sourceExts.some(ext => f.endsWith(ext)) &&
|
|
4469
|
+
!f.includes('node_modules') && !f.includes('.test.') && !f.includes('/test/')
|
|
4470
|
+
);
|
|
4471
|
+
|
|
4472
|
+
const maxFiles = parseInt(opts.maxFiles, 10) || 40;
|
|
4473
|
+
const filesToReview = sourceFiles.slice(0, maxFiles);
|
|
4474
|
+
|
|
4475
|
+
console.log('');
|
|
4476
|
+
console.log('Hone AI — Production Review');
|
|
4477
|
+
console.log('================================');
|
|
4478
|
+
console.log(`Base: ${opts.base}`);
|
|
4479
|
+
console.log(`Changed files: ${changedFiles.length} total, ${sourceFiles.length} source, ${filesToReview.length} to review`);
|
|
4480
|
+
console.log('');
|
|
4481
|
+
|
|
4482
|
+
if (opts.dryRun) {
|
|
4483
|
+
console.log('Source files that would be reviewed:');
|
|
4484
|
+
for (const f of filesToReview) console.log(` ${f}`);
|
|
4485
|
+
if (sourceFiles.length > maxFiles) console.log(` ... and ${sourceFiles.length - maxFiles} more (increase --max-files)`);
|
|
4486
|
+
process.exit(0);
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// 3. Check for API key
|
|
4490
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
4491
|
+
if (!apiKey) {
|
|
4492
|
+
console.error('ANTHROPIC_API_KEY not set. Required for production review (Opus model).');
|
|
4493
|
+
console.error('Set it: export ANTHROPIC_API_KEY=sk-ant-...');
|
|
4494
|
+
process.exit(1);
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
// 4. Build the diff content (truncated per-file to stay within context)
|
|
4498
|
+
let diffContent;
|
|
4499
|
+
try {
|
|
4500
|
+
diffContent = execSync(`git diff origin/${opts.base}...HEAD -- ${filesToReview.map(f => `'${f}'`).join(' ')}`, {
|
|
4501
|
+
encoding: 'utf8', cwd: repoRoot, maxBuffer: 10 * 1024 * 1024,
|
|
4502
|
+
});
|
|
4503
|
+
} catch {
|
|
4504
|
+
try {
|
|
4505
|
+
diffContent = execSync('git diff HEAD~10', { encoding: 'utf8', cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
4506
|
+
} catch (e) {
|
|
4507
|
+
console.error(`Could not generate diff: ${e.message}`);
|
|
4508
|
+
process.exit(1);
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
// Truncate if over 100k chars (~25k tokens) to stay within budget
|
|
4513
|
+
const MAX_DIFF_CHARS = 100000;
|
|
4514
|
+
if (diffContent.length > MAX_DIFF_CHARS) {
|
|
4515
|
+
diffContent = diffContent.slice(0, MAX_DIFF_CHARS) + '\n\n[... diff truncated at 100k chars ...]';
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
// 5. Build the prompt
|
|
4519
|
+
const systemPrompt = [
|
|
4520
|
+
'# Production Reviewer — Pre-Deployment Gate',
|
|
4521
|
+
'',
|
|
4522
|
+
'You are reviewing ALL changed files holistically before deployment.',
|
|
4523
|
+
'Your job is to catch cross-file issues that per-story reviews miss.',
|
|
4524
|
+
'',
|
|
4525
|
+
'## Check each file for:',
|
|
4526
|
+
'1. SQL injection (parameterized queries?)',
|
|
4527
|
+
'2. Tenant isolation (org_id in every query?)',
|
|
4528
|
+
'3. Error handling (unhandled promises?)',
|
|
4529
|
+
'4. Race conditions (concurrent access?)',
|
|
4530
|
+
'5. Resource leaks (connections released?)',
|
|
4531
|
+
'6. Security (secrets exposed? auth correct?)',
|
|
4532
|
+
'7. Logic bugs (null handling? type coercion?)',
|
|
4533
|
+
'8. Dead code (built but never wired?)',
|
|
4534
|
+
'9. Performance (N+1? unbounded? large payloads?)',
|
|
4535
|
+
'',
|
|
4536
|
+
'## Check cross-file interactions:',
|
|
4537
|
+
'- Do modules wire together correctly?',
|
|
4538
|
+
'- Is auth middleware in correct order?',
|
|
4539
|
+
'- Are exported functions imported somewhere?',
|
|
4540
|
+
'- Do CLI messages reference options that exist?',
|
|
4541
|
+
'- Are DB constraints consistent with app validation?',
|
|
4542
|
+
'',
|
|
4543
|
+
'## Rate each finding: CRITICAL / HIGH / MEDIUM / LOW',
|
|
4544
|
+
'- CRITICAL: tenant isolation breach, data leak, security bypass',
|
|
4545
|
+
'- HIGH: race condition, dead safety code, UX broken',
|
|
4546
|
+
'- MEDIUM: performance waste, missing validation, resource leak',
|
|
4547
|
+
'- LOW: dead code, cosmetic',
|
|
4548
|
+
'',
|
|
4549
|
+
'## Output as JSON:',
|
|
4550
|
+
'```json',
|
|
4551
|
+
'{',
|
|
4552
|
+
' "findings": [{ "severity": "...", "file": "...", "line": N, "issue": "...", "recommendation": "..." }],',
|
|
4553
|
+
' "crossFileChecks": [{ "check": "...", "status": "ok|issue", "detail": "..." }],',
|
|
4554
|
+
' "summary": { "critical": N, "high": N, "medium": N, "low": N },',
|
|
4555
|
+
' "recommendation": "DEPLOY | FIX_FIRST | DO_NOT_DEPLOY"',
|
|
4556
|
+
'}',
|
|
4557
|
+
'```',
|
|
4558
|
+
].join('\n');
|
|
4559
|
+
|
|
4560
|
+
const userPrompt = [
|
|
4561
|
+
`## Files changed (${filesToReview.length} source files):`,
|
|
4562
|
+
filesToReview.map(f => `- ${f}`).join('\n'),
|
|
4563
|
+
'',
|
|
4564
|
+
'## Full diff:',
|
|
4565
|
+
'```diff',
|
|
4566
|
+
diffContent,
|
|
4567
|
+
'```',
|
|
4568
|
+
'',
|
|
4569
|
+
'Review ALL files holistically. Return findings as JSON.',
|
|
4570
|
+
].join('\n');
|
|
4571
|
+
|
|
4572
|
+
console.log('Calling Anthropic API (claude-opus-4-20250514)...');
|
|
4573
|
+
console.log('');
|
|
4574
|
+
|
|
4575
|
+
// 6. Call Anthropic Messages API
|
|
4576
|
+
try {
|
|
4577
|
+
const { data } = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
4578
|
+
model: 'claude-opus-4-20250514',
|
|
4579
|
+
max_tokens: 8192,
|
|
4580
|
+
system: systemPrompt,
|
|
4581
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
4582
|
+
}, {
|
|
4583
|
+
headers: {
|
|
4584
|
+
'x-api-key': apiKey,
|
|
4585
|
+
'anthropic-version': '2023-06-01',
|
|
4586
|
+
'content-type': 'application/json',
|
|
4587
|
+
},
|
|
4588
|
+
timeout: 120000,
|
|
4589
|
+
});
|
|
4590
|
+
|
|
4591
|
+
const responseText = data.content?.[0]?.text || '';
|
|
4592
|
+
|
|
4593
|
+
// 7. Parse and display results
|
|
4594
|
+
if (opts.format === 'json') {
|
|
4595
|
+
// Try to extract JSON from response
|
|
4596
|
+
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
|
4597
|
+
if (jsonMatch) {
|
|
4598
|
+
try {
|
|
4599
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4600
|
+
console.log(JSON.stringify({
|
|
4601
|
+
base: opts.base,
|
|
4602
|
+
totalFiles: changedFiles.length,
|
|
4603
|
+
sourceFiles: sourceFiles.length,
|
|
4604
|
+
reviewedFiles: filesToReview.length,
|
|
4605
|
+
model: 'claude-opus-4-20250514',
|
|
4606
|
+
inputTokens: data.usage?.input_tokens || 0,
|
|
4607
|
+
outputTokens: data.usage?.output_tokens || 0,
|
|
4608
|
+
...parsed,
|
|
4609
|
+
}, null, 2));
|
|
4610
|
+
} catch {
|
|
4611
|
+
console.log(JSON.stringify({ raw: responseText }, null, 2));
|
|
4612
|
+
}
|
|
4613
|
+
} else {
|
|
4614
|
+
console.log(JSON.stringify({ raw: responseText }, null, 2));
|
|
4615
|
+
}
|
|
4616
|
+
} else {
|
|
4617
|
+
console.log(responseText);
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
// 8. Exit code based on findings
|
|
4621
|
+
const hasCritical = responseText.includes('"CRITICAL"') || responseText.includes('"critical"');
|
|
4622
|
+
const recommendation = responseText.includes('DO_NOT_DEPLOY');
|
|
4623
|
+
if (hasCritical || recommendation) {
|
|
4624
|
+
console.log('');
|
|
4625
|
+
console.log('CRITICAL issues found. Fix before deploying.');
|
|
4626
|
+
process.exit(1);
|
|
4627
|
+
}
|
|
4628
|
+
} catch (e) {
|
|
4629
|
+
const status = e.response?.status;
|
|
4630
|
+
const msg = e.response?.data?.error?.message || e.message;
|
|
4631
|
+
if (status === 401) {
|
|
4632
|
+
console.error('Invalid ANTHROPIC_API_KEY. Check your key and try again.');
|
|
4633
|
+
} else if (status === 429) {
|
|
4634
|
+
console.error('Rate limited by Anthropic API. Try again shortly.');
|
|
4635
|
+
} else {
|
|
4636
|
+
console.error(`Production review failed: ${msg}`);
|
|
4637
|
+
}
|
|
4638
|
+
process.exit(1);
|
|
4639
|
+
}
|
|
4640
|
+
});
|
|
4641
|
+
|
|
4238
4642
|
// ── CLI setup ─────────────────────────────────────────────────────────────────
|
|
4239
4643
|
program
|
|
4240
4644
|
.name('hone')
|
package/lib/eval-contracts.js
CHANGED
|
@@ -56,6 +56,32 @@ const PIPELINE_CONTRACTS = [
|
|
|
56
56
|
outputGate: 'step_3',
|
|
57
57
|
metadataField: 'step_3.gate_result',
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
agent: 'e2e-test-spec-writer',
|
|
61
|
+
step: '5a',
|
|
62
|
+
inputArtifact: 'step-4-implementation.md',
|
|
63
|
+
inputGate: 'step_4.gate_result',
|
|
64
|
+
outputArtifact: null,
|
|
65
|
+
outputGate: null,
|
|
66
|
+
metadataField: null,
|
|
67
|
+
extraChecks: [
|
|
68
|
+
{ text: 'Playwright', check: 'playwright', detail: 'generates Playwright specs' },
|
|
69
|
+
{ text: 'data-testid', check: 'data_testid', detail: 'uses data-testid selectors' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
agent: 'e2e-qa-spec-healer',
|
|
74
|
+
step: 'independent',
|
|
75
|
+
inputArtifact: null,
|
|
76
|
+
inputGate: null,
|
|
77
|
+
outputArtifact: null,
|
|
78
|
+
outputGate: null,
|
|
79
|
+
metadataField: null,
|
|
80
|
+
extraChecks: [
|
|
81
|
+
{ text: 'DIAGNOSIS', check: 'diagnosis', detail: 'provides diagnosis category' },
|
|
82
|
+
{ text: 'Application bug', check: 'app_bug_handling', detail: 'handles application bugs (skip + file bug)' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
59
85
|
{
|
|
60
86
|
agent: 'code-builder',
|
|
61
87
|
step: 4,
|
|
@@ -106,6 +132,21 @@ const PIPELINE_CONTRACTS = [
|
|
|
106
132
|
{ text: 'test_strategy', check: 'test_strategy', detail: 'includes test_strategy in plan' },
|
|
107
133
|
],
|
|
108
134
|
},
|
|
135
|
+
{
|
|
136
|
+
agent: 'release-reviewer',
|
|
137
|
+
step: 'independent',
|
|
138
|
+
inputArtifact: null,
|
|
139
|
+
inputGate: null,
|
|
140
|
+
outputArtifact: null,
|
|
141
|
+
outputGate: null,
|
|
142
|
+
metadataField: null,
|
|
143
|
+
extraChecks: [
|
|
144
|
+
{ text: 'CRITICAL', check: 'severity_critical', detail: 'defines CRITICAL severity level' },
|
|
145
|
+
{ text: 'cross-file', check: 'cross_file_review', detail: 'checks cross-file interactions' },
|
|
146
|
+
{ text: 'tenant', check: 'tenant_isolation', detail: 'checks tenant isolation' },
|
|
147
|
+
{ text: 'DEPLOY', check: 'deploy_recommendation', detail: 'provides deploy/no-deploy recommendation' },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
109
150
|
];
|
|
110
151
|
|
|
111
152
|
/**
|