@hone-ai/cli 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hone-cli.js CHANGED
@@ -362,10 +362,16 @@ program
362
362
  console.log('Running setup-ai-pipeline.sh v3.1...');
363
363
  console.log('');
364
364
 
365
+ // Auto-detect non-TTY (CI, piped, Claude Code) and add --non-interactive
366
+ const isNonInteractive = opts.nonInteractive || !process.stdin.isTTY;
367
+ if (isNonInteractive && !opts.nonInteractive) {
368
+ console.log(' (non-TTY detected — running in non-interactive mode)');
369
+ }
370
+
365
371
  const flags = [
366
372
  `--source "${path.join(tmpDir, 'enterprise-github')}"`,
367
- opts.dryRun ? '--dry-run' : '',
368
- opts.nonInteractive? '--non-interactive' : '',
373
+ opts.dryRun ? '--dry-run' : '',
374
+ isNonInteractive ? '--non-interactive' : '',
369
375
  ].filter(Boolean).join(' ');
370
376
 
371
377
  try {
@@ -4144,6 +4150,7 @@ program
4144
4150
  .option('--contracts', 'Run contract validation between pipeline agents')
4145
4151
  .option('--snapshot', 'Save current eval + contract results as regression baseline')
4146
4152
  .option('--regression', 'Compare current results against saved baseline (detect drift)')
4153
+ .option('--judge', 'Run LLM-as-judge scenarios (requires ANTHROPIC_API_KEY, costs tokens)')
4147
4154
  .action(async (opts) => {
4148
4155
  const path = require('path');
4149
4156
  const fs = require('fs');
@@ -4205,7 +4212,84 @@ program
4205
4212
  process.exit(results.failed > 0 ? 1 : 0);
4206
4213
  }
4207
4214
 
4208
- // Scenario evaluation mode
4215
+ // LLM-judge mode (HC-019i / #268)
4216
+ if (opts.judge) {
4217
+ const { loadScenarios, formatResults } = require('./lib/eval-runner');
4218
+ const { runJudgeScenario } = require('./lib/eval-llm-judge');
4219
+
4220
+ const apiKey = process.env.ANTHROPIC_API_KEY;
4221
+ if (!apiKey) {
4222
+ console.error('ANTHROPIC_API_KEY required for --judge mode. Set: export ANTHROPIC_API_KEY=sk-ant-...');
4223
+ process.exit(1);
4224
+ }
4225
+
4226
+ const scenarios = loadScenarios({
4227
+ evalDir, agent: opts.agent, tag: opts.tag, scenarioId: opts.scenario,
4228
+ readFile: (p) => fs.readFileSync(p, 'utf8'),
4229
+ listDir: (p) => fs.readdirSync(p), isDir: (p) => fs.statSync(p).isDirectory(),
4230
+ parseYaml: (text) => yaml.load(text),
4231
+ });
4232
+
4233
+ const judgeScenarios = scenarios.filter(s => s.grading?.mode === 'llm-judge');
4234
+ if (judgeScenarios.length === 0) {
4235
+ console.log('No llm-judge scenarios found. Add grading.mode: llm-judge to eval YAML files.');
4236
+ process.exit(0);
4237
+ }
4238
+
4239
+ // LLM call function using Anthropic API
4240
+ async function callLLM(systemPrompt, userPrompt) {
4241
+ const { data } = await axios.post('https://api.anthropic.com/v1/messages', {
4242
+ model: 'claude-sonnet-4-20250514',
4243
+ max_tokens: 2048,
4244
+ system: systemPrompt,
4245
+ messages: [{ role: 'user', content: userPrompt }],
4246
+ }, {
4247
+ headers: {
4248
+ 'x-api-key': apiKey,
4249
+ 'anthropic-version': '2023-06-01',
4250
+ 'content-type': 'application/json',
4251
+ },
4252
+ timeout: 60000,
4253
+ });
4254
+ return data.content?.[0]?.text || '';
4255
+ }
4256
+
4257
+ console.log(`Running ${judgeScenarios.length} LLM-judge scenario(s)...`);
4258
+ console.log('');
4259
+
4260
+ const results = [];
4261
+ for (const scenario of judgeScenarios) {
4262
+ const agentName = scenario.evalAgent || scenario.agent;
4263
+ const promptText = AGENT_PROMPTS[agentName];
4264
+ if (!promptText) {
4265
+ results.push({ id: scenario.id, agent: agentName, result: 'error',
4266
+ checks: 0, checks_passed: 0, failures: [{ type: 'missing_prompt', passed: false, detail: `agent "${agentName}" not found` }] });
4267
+ continue;
4268
+ }
4269
+ try {
4270
+ const result = await runJudgeScenario({ scenario, agentPrompt: promptText, callLLM });
4271
+ results.push(result);
4272
+ } catch (e) {
4273
+ results.push({ id: scenario.id, agent: agentName, result: 'error',
4274
+ checks: 0, checks_passed: 0, failures: [{ type: 'llm_error', passed: false, detail: e.message }] });
4275
+ }
4276
+
4277
+ if (opts.failFast && results[results.length - 1].result !== 'pass') break;
4278
+ }
4279
+
4280
+ const summary = {
4281
+ total: results.length,
4282
+ passed: results.filter(r => r.result === 'pass').length,
4283
+ failed: results.filter(r => r.result === 'fail').length,
4284
+ errors: results.filter(r => r.result === 'error').length,
4285
+ scenarios: results,
4286
+ };
4287
+
4288
+ console.log(formatResults(summary, opts.format));
4289
+ process.exit(summary.failed + summary.errors > 0 ? 1 : 0);
4290
+ }
4291
+
4292
+ // Scenario evaluation mode (deterministic)
4209
4293
  const { loadScenarios, runAllScenarios, formatResults } = require('./lib/eval-runner');
4210
4294
 
4211
4295
  if (!fs.existsSync(evalDir)) {
@@ -4429,6 +4513,216 @@ process.on('SIGINT', () => {
4429
4513
  process.exit(0);
4430
4514
  });
4431
4515
 
4516
+ // ── Release Review (pre-deployment holistic review) ──────────────────────────
4517
+ program
4518
+ .command('release-review')
4519
+ .description('Holistic code review of all changed files before deployment (runs Opus)')
4520
+ .option('--base <branch>', 'Base branch to diff against', 'main')
4521
+ .option('--format <fmt>', 'Output format: pretty or json', 'pretty')
4522
+ .option('--dry-run', 'Show what would be reviewed without calling the LLM', false)
4523
+ .option('--max-files <n>', 'Max source files to include in review', '40')
4524
+ .action(async (opts) => {
4525
+ const { execSync } = require('child_process');
4526
+ const fs = require('fs');
4527
+ const repoRoot = process.cwd();
4528
+
4529
+ // 1. Get changed files
4530
+ let changedFiles;
4531
+ try {
4532
+ const raw = execSync(`git diff --name-only origin/${opts.base}...HEAD`, { encoding: 'utf8', cwd: repoRoot });
4533
+ changedFiles = raw.trim().split('\n').filter(Boolean);
4534
+ } catch {
4535
+ try {
4536
+ const raw = execSync('git diff --name-only HEAD~10', { encoding: 'utf8', cwd: repoRoot });
4537
+ changedFiles = raw.trim().split('\n').filter(Boolean);
4538
+ } catch {
4539
+ console.error('Could not determine changed files. Run from a git repo.');
4540
+ process.exit(1);
4541
+ }
4542
+ }
4543
+
4544
+ if (changedFiles.length === 0) {
4545
+ console.log('No changed files found. Nothing to review.');
4546
+ process.exit(0);
4547
+ }
4548
+
4549
+ // 2. Filter to source files
4550
+ const sourceExts = ['.js', '.ts', '.py', '.go', '.java', '.rb', '.rs', '.sql', '.yml', '.yaml'];
4551
+ const sourceFiles = changedFiles.filter(f =>
4552
+ sourceExts.some(ext => f.endsWith(ext)) &&
4553
+ !f.includes('node_modules') && !f.includes('.test.') && !f.includes('/test/')
4554
+ );
4555
+
4556
+ const maxFiles = parseInt(opts.maxFiles, 10) || 40;
4557
+ const filesToReview = sourceFiles.slice(0, maxFiles);
4558
+
4559
+ console.log('');
4560
+ console.log('Hone AI — Production Review');
4561
+ console.log('================================');
4562
+ console.log(`Base: ${opts.base}`);
4563
+ console.log(`Changed files: ${changedFiles.length} total, ${sourceFiles.length} source, ${filesToReview.length} to review`);
4564
+ console.log('');
4565
+
4566
+ if (opts.dryRun) {
4567
+ console.log('Source files that would be reviewed:');
4568
+ for (const f of filesToReview) console.log(` ${f}`);
4569
+ if (sourceFiles.length > maxFiles) console.log(` ... and ${sourceFiles.length - maxFiles} more (increase --max-files)`);
4570
+ process.exit(0);
4571
+ }
4572
+
4573
+ // 3. Check for API key
4574
+ const apiKey = process.env.ANTHROPIC_API_KEY;
4575
+ if (!apiKey) {
4576
+ console.error('ANTHROPIC_API_KEY not set. Required for production review (Opus model).');
4577
+ console.error('Set it: export ANTHROPIC_API_KEY=sk-ant-...');
4578
+ process.exit(1);
4579
+ }
4580
+
4581
+ // 4. Build the diff content (truncated per-file to stay within context)
4582
+ let diffContent;
4583
+ try {
4584
+ diffContent = execSync(`git diff origin/${opts.base}...HEAD -- ${filesToReview.map(f => `'${f}'`).join(' ')}`, {
4585
+ encoding: 'utf8', cwd: repoRoot, maxBuffer: 10 * 1024 * 1024,
4586
+ });
4587
+ } catch {
4588
+ try {
4589
+ diffContent = execSync('git diff HEAD~10', { encoding: 'utf8', cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
4590
+ } catch (e) {
4591
+ console.error(`Could not generate diff: ${e.message}`);
4592
+ process.exit(1);
4593
+ }
4594
+ }
4595
+
4596
+ // Truncate if over 100k chars (~25k tokens) to stay within budget
4597
+ const MAX_DIFF_CHARS = 100000;
4598
+ if (diffContent.length > MAX_DIFF_CHARS) {
4599
+ diffContent = diffContent.slice(0, MAX_DIFF_CHARS) + '\n\n[... diff truncated at 100k chars ...]';
4600
+ }
4601
+
4602
+ // 5. Build the prompt
4603
+ const systemPrompt = [
4604
+ '# Production Reviewer — Pre-Deployment Gate',
4605
+ '',
4606
+ 'You are reviewing ALL changed files holistically before deployment.',
4607
+ 'Your job is to catch cross-file issues that per-story reviews miss.',
4608
+ '',
4609
+ '## Check each file for:',
4610
+ '1. SQL injection (parameterized queries?)',
4611
+ '2. Tenant isolation (org_id in every query?)',
4612
+ '3. Error handling (unhandled promises?)',
4613
+ '4. Race conditions (concurrent access?)',
4614
+ '5. Resource leaks (connections released?)',
4615
+ '6. Security (secrets exposed? auth correct?)',
4616
+ '7. Logic bugs (null handling? type coercion?)',
4617
+ '8. Dead code (built but never wired?)',
4618
+ '9. Performance (N+1? unbounded? large payloads?)',
4619
+ '',
4620
+ '## Check cross-file interactions:',
4621
+ '- Do modules wire together correctly?',
4622
+ '- Is auth middleware in correct order?',
4623
+ '- Are exported functions imported somewhere?',
4624
+ '- Do CLI messages reference options that exist?',
4625
+ '- Are DB constraints consistent with app validation?',
4626
+ '',
4627
+ '## Rate each finding: CRITICAL / HIGH / MEDIUM / LOW',
4628
+ '- CRITICAL: tenant isolation breach, data leak, security bypass',
4629
+ '- HIGH: race condition, dead safety code, UX broken',
4630
+ '- MEDIUM: performance waste, missing validation, resource leak',
4631
+ '- LOW: dead code, cosmetic',
4632
+ '',
4633
+ '## Output as JSON:',
4634
+ '```json',
4635
+ '{',
4636
+ ' "findings": [{ "severity": "...", "file": "...", "line": N, "issue": "...", "recommendation": "..." }],',
4637
+ ' "crossFileChecks": [{ "check": "...", "status": "ok|issue", "detail": "..." }],',
4638
+ ' "summary": { "critical": N, "high": N, "medium": N, "low": N },',
4639
+ ' "recommendation": "DEPLOY | FIX_FIRST | DO_NOT_DEPLOY"',
4640
+ '}',
4641
+ '```',
4642
+ ].join('\n');
4643
+
4644
+ const userPrompt = [
4645
+ `## Files changed (${filesToReview.length} source files):`,
4646
+ filesToReview.map(f => `- ${f}`).join('\n'),
4647
+ '',
4648
+ '## Full diff:',
4649
+ '```diff',
4650
+ diffContent,
4651
+ '```',
4652
+ '',
4653
+ 'Review ALL files holistically. Return findings as JSON.',
4654
+ ].join('\n');
4655
+
4656
+ console.log('Calling Anthropic API (claude-opus-4-20250514)...');
4657
+ console.log('');
4658
+
4659
+ // 6. Call Anthropic Messages API
4660
+ try {
4661
+ const { data } = await axios.post('https://api.anthropic.com/v1/messages', {
4662
+ model: 'claude-opus-4-20250514',
4663
+ max_tokens: 8192,
4664
+ system: systemPrompt,
4665
+ messages: [{ role: 'user', content: userPrompt }],
4666
+ }, {
4667
+ headers: {
4668
+ 'x-api-key': apiKey,
4669
+ 'anthropic-version': '2023-06-01',
4670
+ 'content-type': 'application/json',
4671
+ },
4672
+ timeout: 120000,
4673
+ });
4674
+
4675
+ const responseText = data.content?.[0]?.text || '';
4676
+
4677
+ // 7. Parse and display results
4678
+ if (opts.format === 'json') {
4679
+ // Try to extract JSON from response
4680
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
4681
+ if (jsonMatch) {
4682
+ try {
4683
+ const parsed = JSON.parse(jsonMatch[0]);
4684
+ console.log(JSON.stringify({
4685
+ base: opts.base,
4686
+ totalFiles: changedFiles.length,
4687
+ sourceFiles: sourceFiles.length,
4688
+ reviewedFiles: filesToReview.length,
4689
+ model: 'claude-opus-4-20250514',
4690
+ inputTokens: data.usage?.input_tokens || 0,
4691
+ outputTokens: data.usage?.output_tokens || 0,
4692
+ ...parsed,
4693
+ }, null, 2));
4694
+ } catch {
4695
+ console.log(JSON.stringify({ raw: responseText }, null, 2));
4696
+ }
4697
+ } else {
4698
+ console.log(JSON.stringify({ raw: responseText }, null, 2));
4699
+ }
4700
+ } else {
4701
+ console.log(responseText);
4702
+ }
4703
+
4704
+ // 8. Exit code based on findings
4705
+ const hasCritical = responseText.includes('"CRITICAL"') || responseText.includes('"critical"');
4706
+ const recommendation = responseText.includes('DO_NOT_DEPLOY');
4707
+ if (hasCritical || recommendation) {
4708
+ console.log('');
4709
+ console.log('CRITICAL issues found. Fix before deploying.');
4710
+ process.exit(1);
4711
+ }
4712
+ } catch (e) {
4713
+ const status = e.response?.status;
4714
+ const msg = e.response?.data?.error?.message || e.message;
4715
+ if (status === 401) {
4716
+ console.error('Invalid ANTHROPIC_API_KEY. Check your key and try again.');
4717
+ } else if (status === 429) {
4718
+ console.error('Rate limited by Anthropic API. Try again shortly.');
4719
+ } else {
4720
+ console.error(`Production review failed: ${msg}`);
4721
+ }
4722
+ process.exit(1);
4723
+ }
4724
+ });
4725
+
4432
4726
  // ── CLI setup ─────────────────────────────────────────────────────────────────
4433
4727
  program
4434
4728
  .name('hone')
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hone-ai/cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {