@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 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')
@@ -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.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {