@hone-ai/cli 1.38.0 → 1.38.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 +217 -201
- package/lib/bundle-paths.js +118 -18
- package/lib/doctor-admin-merge.js +11 -3
- package/lib/git-remote.js +103 -0
- package/lib/judge-provider.js +39 -34
- package/lib/mcp-tools.js +42 -1
- package/lib/run-context.js +4 -3
- package/lib/run-target.js +34 -2
- package/lib/story-provenance.js +19 -12
- package/package.json +1 -1
package/hone-cli.js
CHANGED
|
@@ -23,7 +23,7 @@ const fs = require('fs');
|
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const os = require('os');
|
|
25
25
|
const crypto = require('crypto');
|
|
26
|
-
const { execSync } = require('child_process');
|
|
26
|
+
const { execSync, execFileSync } = require('child_process');
|
|
27
27
|
|
|
28
28
|
// HC-GF-144: verdictEnv lives in cli/lib so it is importable and can be tested by BEHAVIOUR.
|
|
29
29
|
// A source-regex asserting the variable name matched this very comment and passed while the
|
|
@@ -39,7 +39,7 @@ const { parseReviewJSON } = require('./lib/parse-review-json');
|
|
|
39
39
|
const { resolveBaseRef, getMaxDiffChars } = require('./lib/release-review-config');
|
|
40
40
|
const { gitEnv } = require('./lib/git-env');
|
|
41
41
|
const { collectIncludePaths } = require('./lib/include-paths');
|
|
42
|
-
const { runArgsError, orchestratePath, apiPath } = require('./lib/run-target'); // HC-GF-270-followup-1, -2
|
|
42
|
+
const { runArgsError, orchestratePath, apiPath, storyDir: storyDirIn, isGitRev } = require('./lib/run-target'); // HC-GF-270-followup-1, -2, -3
|
|
43
43
|
const { normalizeQuestion, questionLines, gateQuestionLines, countPhrase } = require('./lib/operator-question'); // HC-GF-234
|
|
44
44
|
const program = new Command();
|
|
45
45
|
|
|
@@ -217,8 +217,8 @@ program
|
|
|
217
217
|
.option('--category <category>', 'Probe category (adversarial|faithfulness|safety|boundary|stateful)', 'adversarial')
|
|
218
218
|
.option('--target <name>', 'Run against a specific agent target (default: all configured)')
|
|
219
219
|
.option('--output <format>', 'Output format (text|json)', 'text')
|
|
220
|
-
.option('--judge', 'Add the
|
|
221
|
-
.option('--provider <name>', 'Judge provider:
|
|
220
|
+
.option('--judge', 'Add the NLI judge layer (faithfulness only): claims are checked for entailment against the probe context. Needs --provider claude (paid)')
|
|
221
|
+
.option('--provider <name>', 'Judge provider: claude (paid Sonnet, needs ANTHROPIC_API_KEY). gh-models was retired 2026-07-30; there is no default')
|
|
222
222
|
.option('--report', 'Report the agent-eval verdict to the run (advisory) — needs --workflow-id')
|
|
223
223
|
.option('--workflow-id <id>', 'The run id to report the verdict against (for --report)')
|
|
224
224
|
.option('--timeout <s>', 'Seconds to allow each probe (default 5 — raise it for packs that need a real generation)', '5')
|
|
@@ -922,11 +922,14 @@ program
|
|
|
922
922
|
console.log(' to switch to local-mode (real safety net at $0 CI cost).');
|
|
923
923
|
}
|
|
924
924
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
925
|
+
// HC-GF-270-followup-3: an argument list, not a shell string — the script path and the source directory
|
|
926
|
+
// reach bash as the arguments they are, whatever characters a temp path holds.
|
|
927
|
+
const setupArgs = [
|
|
928
|
+
scriptPath,
|
|
929
|
+
'--source', path.join(tmpDir, 'enterprise-github'),
|
|
930
|
+
...(opts.dryRun ? ['--dry-run'] : []),
|
|
931
|
+
...(isNonInteractive ? ['--non-interactive'] : []),
|
|
932
|
+
];
|
|
930
933
|
|
|
931
934
|
// HC-RC-004 pass-1 (HIGH-1): when source==='auto-detect', LEAVE
|
|
932
935
|
// CI_GATE unset so the bash script's existing detected_ci_default
|
|
@@ -941,7 +944,7 @@ program
|
|
|
941
944
|
// after detection (apply_stack_override) and refuses an unsupported value.
|
|
942
945
|
if (opts.stack) setupEnv.HONE_STACK_OVERRIDE = String(opts.stack).toLowerCase();
|
|
943
946
|
try {
|
|
944
|
-
|
|
947
|
+
execFileSync('bash', setupArgs, {
|
|
945
948
|
stdio: 'inherit',
|
|
946
949
|
cwd: process.cwd(),
|
|
947
950
|
env: setupEnv,
|
|
@@ -1332,7 +1335,12 @@ async function installTestFramework(repoRoot, opts) {
|
|
|
1332
1335
|
return installPythonTestFramework(repoRoot, detected, pkgs, opts);
|
|
1333
1336
|
}
|
|
1334
1337
|
|
|
1335
|
-
|
|
1338
|
+
// HC-GF-270-followup-3: the installer and its arguments as a list, run without a shell. npm, yarn and pnpm
|
|
1339
|
+
// are `.cmd` shims on Windows, which Node will only spawn through a shell — so these two calls (constant
|
|
1340
|
+
// arguments only) take one there and nowhere else. No `env`: an install keeps the ambient toolchain (HC-GF-144).
|
|
1341
|
+
const INSTALL_ARGV = { yarn: ['yarn', 'add', '-D'], pnpm: ['pnpm', 'add', '-D'], npm: ['npm', 'install', '-D'] };
|
|
1342
|
+
const [installBin, ...installPre] = INSTALL_ARGV[detected.pkgMgr] || INSTALL_ARGV.npm;
|
|
1343
|
+
const installShell = process.platform === 'win32';
|
|
1336
1344
|
|
|
1337
1345
|
// ── Unit test framework ────────────────────────────────
|
|
1338
1346
|
if (detected.testFw) {
|
|
@@ -1340,7 +1348,7 @@ async function installTestFramework(repoRoot, opts) {
|
|
|
1340
1348
|
} else {
|
|
1341
1349
|
console.log(` Stack: ${detected.stack} → Installing: ${pkgs.unit.join(', ')}`);
|
|
1342
1350
|
try {
|
|
1343
|
-
|
|
1351
|
+
execFileSync(installBin, [...installPre, ...pkgs.unit], { stdio: 'inherit', cwd: repoRoot, shell: installShell });
|
|
1344
1352
|
console.log(` ✓ Unit test framework installed`);
|
|
1345
1353
|
} catch (e) {
|
|
1346
1354
|
console.error(` ✗ Install failed: ${e.message}`);
|
|
@@ -1422,7 +1430,7 @@ async function installTestFramework(repoRoot, opts) {
|
|
|
1422
1430
|
} else {
|
|
1423
1431
|
console.log(` Installing Playwright...`);
|
|
1424
1432
|
try {
|
|
1425
|
-
|
|
1433
|
+
execFileSync(installBin, [...installPre, '@playwright/test'], { stdio: 'inherit', cwd: repoRoot, shell: installShell });
|
|
1426
1434
|
console.log(` ✓ @playwright/test installed`);
|
|
1427
1435
|
console.log(` Installing Chromium browser...`);
|
|
1428
1436
|
execSync('npx playwright install chromium', { stdio: 'inherit', cwd: repoRoot });
|
|
@@ -3228,6 +3236,7 @@ program
|
|
|
3228
3236
|
.option('--max-files <n>', 'Max source files for derive (default: 15)', '15')
|
|
3229
3237
|
.option('--skip-derive', 'Skip the derive step (setup + sync only)')
|
|
3230
3238
|
.action(async (opts) => {
|
|
3239
|
+
refuseBadRunArgs([{ label: '--max-files', kind: 'count', value: opts.maxFiles }]); // HC-GF-270-followup-3
|
|
3231
3240
|
const startTime = Date.now();
|
|
3232
3241
|
|
|
3233
3242
|
console.log('');
|
|
@@ -3248,10 +3257,8 @@ program
|
|
|
3248
3257
|
if (opts.e2e !== false) setupArgs.push('--e2e');
|
|
3249
3258
|
if (opts.noE2e) setupArgs.push('--no-e2e');
|
|
3250
3259
|
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
const setupCmd = `node "${cliPath}" ${setupArgs.join(' ')}`;
|
|
3254
|
-
ex(setupCmd, { stdio: 'inherit', cwd: process.cwd(), env: process.env });
|
|
3260
|
+
// HC-GF-270-followup-3: this CLI re-run by the node running it, with an argument list — no shell.
|
|
3261
|
+
execFileSync(process.execPath, [__filename, ...setupArgs], { stdio: 'inherit', cwd: process.cwd(), env: process.env });
|
|
3255
3262
|
} catch (e) {
|
|
3256
3263
|
console.error('Setup failed:', e.message);
|
|
3257
3264
|
process.exit(1);
|
|
@@ -3263,10 +3270,10 @@ program
|
|
|
3263
3270
|
console.log('━━━ Step 2/3: Derive Domain Skills ━━━━━━━━━━━━');
|
|
3264
3271
|
console.log('');
|
|
3265
3272
|
try {
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3273
|
+
// HC-GF-270-followup-3: `--max-files` was pasted into a shell string, so `--max-files '15;touch x'`
|
|
3274
|
+
// ran `touch x`. It is checked as a count when adopt starts, and reaches derive as one argument.
|
|
3275
|
+
execFileSync(process.execPath, [__filename, 'derive', '--max-files', String(opts.maxFiles)],
|
|
3276
|
+
{ stdio: 'inherit', cwd: process.cwd(), env: process.env, timeout: 600_000 });
|
|
3270
3277
|
} catch (e) {
|
|
3271
3278
|
console.warn('');
|
|
3272
3279
|
console.warn('⚠ Derive failed (non-fatal):', e.message?.slice(0, 100));
|
|
@@ -3283,9 +3290,7 @@ program
|
|
|
3283
3290
|
console.log('━━━ Step 3/3: Sync Prompts ━━━━━━━━━━━━━━━━━━━━');
|
|
3284
3291
|
console.log('');
|
|
3285
3292
|
try {
|
|
3286
|
-
|
|
3287
|
-
const cliPath = __filename;
|
|
3288
|
-
ex(`node "${cliPath}" sync`, { stdio: 'inherit', cwd: process.cwd(), env: process.env });
|
|
3293
|
+
execFileSync(process.execPath, [__filename, 'sync'], { stdio: 'inherit', cwd: process.cwd(), env: process.env });
|
|
3289
3294
|
} catch (e) {
|
|
3290
3295
|
console.warn('⚠ Sync failed:', e.message);
|
|
3291
3296
|
}
|
|
@@ -3295,12 +3300,10 @@ program
|
|
|
3295
3300
|
// mismatches surface immediately instead of silently breaking
|
|
3296
3301
|
// Gate 3 / Pizza Tracker on the first PR.
|
|
3297
3302
|
try {
|
|
3298
|
-
const { execSync: ex } = require('child_process');
|
|
3299
|
-
const cliPath = __filename;
|
|
3300
3303
|
console.log('');
|
|
3301
3304
|
console.log('━━━ Pattern detection (H-018) ━━━━━━━━━━━━━━━━━━');
|
|
3302
3305
|
console.log('');
|
|
3303
|
-
|
|
3306
|
+
execFileSync(process.execPath, [__filename, 'check-patterns'], { stdio: 'inherit', cwd: process.cwd(), env: process.env });
|
|
3304
3307
|
} catch (e) {
|
|
3305
3308
|
// Exit 1 from check-patterns means drift detected — the user
|
|
3306
3309
|
// already saw the table, so just note that adoption succeeded
|
|
@@ -4176,6 +4179,8 @@ metricsCmd
|
|
|
4176
4179
|
.description('Bootstrap .github/metrics/<STORY-ID>.yml from pipeline metadata + git + gh')
|
|
4177
4180
|
.option('--force', 'Overwrite an existing scorecard (default: preserve)')
|
|
4178
4181
|
.action(async (storyIdArg, opts) => {
|
|
4182
|
+
// HC-GF-270-followup-3: the id names a directory this reads and a file it writes; `../` must not leave them.
|
|
4183
|
+
refuseBadRunArgs([{ label: '<storyId>', kind: 'storyId', value: storyIdArg, optional: true }]);
|
|
4179
4184
|
const ps = require('./lib/pipeline-status');
|
|
4180
4185
|
const mc = require('./lib/metrics-collect');
|
|
4181
4186
|
const repoRoot = process.cwd();
|
|
@@ -4198,8 +4203,11 @@ metricsCmd
|
|
|
4198
4203
|
process.exit(1);
|
|
4199
4204
|
}
|
|
4200
4205
|
}
|
|
4206
|
+
// HC-GF-270-followup-3: an id read from the branch name gets the check a typed one got — the branch
|
|
4207
|
+
// pattern has no length cap, so storyDir would otherwise throw a stack trace instead of refusing.
|
|
4208
|
+
refuseBadRunArgs([{ label: 'the story id read from the branch name', kind: 'storyId', value: storyId }]);
|
|
4201
4209
|
|
|
4202
|
-
const storyDir =
|
|
4210
|
+
const storyDir = storyDirIn(pipelineRoot, storyId);
|
|
4203
4211
|
const metaPath = path.join(storyDir, 'metadata.yml');
|
|
4204
4212
|
if (!fs.existsSync(metaPath)) {
|
|
4205
4213
|
console.error(`No pipeline metadata for ${storyId} — checked ${metaPath}`);
|
|
@@ -4222,7 +4230,7 @@ metricsCmd
|
|
|
4222
4230
|
|
|
4223
4231
|
// Idempotency: preserve existing scorecard unless --force
|
|
4224
4232
|
fs.mkdirSync(metricsRoot, { recursive: true });
|
|
4225
|
-
const outPath =
|
|
4233
|
+
const outPath = `${storyDirIn(metricsRoot, storyId)}.yml`;
|
|
4226
4234
|
if (fs.existsSync(outPath) && !opts.force) {
|
|
4227
4235
|
console.error(`${outPath} already exists. Re-run with --force to overwrite.`);
|
|
4228
4236
|
console.error('(Default behavior: preserve any hand-written or agent-appended fields.)');
|
|
@@ -4236,17 +4244,28 @@ metricsCmd
|
|
|
4236
4244
|
} catch { /* leave undefined */ }
|
|
4237
4245
|
|
|
4238
4246
|
// Files changed — best-effort
|
|
4247
|
+
// HC-GF-270-followup-3: the base comes from metadata.yml — repository content — and reaches git as one
|
|
4248
|
+
// argument. A base git would read as an option (`--output=…` writes a file) is skipped, not run.
|
|
4239
4249
|
let filesChanged = [];
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4250
|
+
const base = String(parsed.base || 'develop');
|
|
4251
|
+
if (isGitRev(base)) {
|
|
4252
|
+
try {
|
|
4253
|
+
const diff = execFileSync('git', ['diff', '--name-only', `${base}...HEAD`], { encoding: 'utf8' });
|
|
4254
|
+
filesChanged = diff.split('\n').map(s => s.trim()).filter(Boolean);
|
|
4255
|
+
} catch { /* leave empty */ }
|
|
4256
|
+
} else {
|
|
4257
|
+
console.error(` ⚠ metadata.yml base ${JSON.stringify(base)} is not a git revision; files_changed left empty`);
|
|
4258
|
+
}
|
|
4244
4259
|
|
|
4245
4260
|
// PR info — best-effort, gh may be missing or no PR yet (E4/E5 graceful)
|
|
4246
4261
|
let prInfo = {};
|
|
4247
4262
|
try {
|
|
4248
4263
|
const branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8' }).trim();
|
|
4249
|
-
|
|
4264
|
+
// HC-GF-270-followup-3: `--head=<branch>` is one argument whatever the branch name holds (`;`, `$(` and `(`
|
|
4265
|
+
// are legal in one); stderr is dropped by stdio rather than by a shell redirect.
|
|
4266
|
+
// A network call: bounded like classify-story's gh call, so a hung gh leaves pr_number unset instead of hanging.
|
|
4267
|
+
const pr = execFileSync('gh', ['pr', 'list', `--head=${branch}`, '--json', 'number', '--jq', '.[0].number'],
|
|
4268
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 30_000 }).trim();
|
|
4250
4269
|
if (pr && /^\d+$/.test(pr)) prInfo = { pr_number: parseInt(pr, 10) };
|
|
4251
4270
|
} catch { /* leave empty — graceful */ }
|
|
4252
4271
|
|
|
@@ -5389,12 +5408,26 @@ program
|
|
|
5389
5408
|
.option('--artifact-dir <dir>', 'write the artifact here instead of .github/pipeline/<STORY-ID>/ (a CI gate must not leave files in the checkout)')
|
|
5390
5409
|
.option('--story-id <id>', 'override story id (defaults to extraction from current branch name)')
|
|
5391
5410
|
.action(async (cmdOpts) => {
|
|
5411
|
+
// HC-GF-270-followup-3: the story id names the artifact directory and the learnings file; the base reaches
|
|
5412
|
+
// git. Refused before anything runs — with exit 1 ("could not run"), because 2 here means a BLOCKER fired
|
|
5413
|
+
// (STEP_5B_BLOCKED) and verify-pr and the skill-audit gate read it that way.
|
|
5414
|
+
const badArg = runArgsError([
|
|
5415
|
+
{ label: '--story-id', kind: 'storyId', value: cmdOpts.storyId, optional: true },
|
|
5416
|
+
{ label: '--base', kind: 'gitRev', value: cmdOpts.base || 'develop' },
|
|
5417
|
+
]);
|
|
5418
|
+
if (badArg) {
|
|
5419
|
+
console.error(`step-5b: ${badArg}`);
|
|
5420
|
+
process.exit(1);
|
|
5421
|
+
}
|
|
5392
5422
|
const { runAssertions } = require('./lib/skill-assertions');
|
|
5393
5423
|
const { renderStep5bArtifact } = require('./lib/skill-audit-render');
|
|
5394
5424
|
const { extractStoryIdFromBranch } = require('./lib/pipeline-status');
|
|
5395
5425
|
const { parseLearningsFile } = require('./lib/learnings-parse');
|
|
5396
|
-
const { execSync } = require('node:child_process');
|
|
5426
|
+
const { execSync, execFileSync } = require('node:child_process');
|
|
5397
5427
|
const repoRoot = process.cwd();
|
|
5428
|
+
// HC-GF-270-followup-3: git reads with an argument list — the base is one argument, never shell text.
|
|
5429
|
+
const gitRead = (args) => execFileSync('git', args,
|
|
5430
|
+
{ cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
|
|
5398
5431
|
|
|
5399
5432
|
// 1. Resolve story id (CLI flag wins; else parse branch name)
|
|
5400
5433
|
let branchName = '';
|
|
@@ -5407,18 +5440,22 @@ program
|
|
|
5407
5440
|
console.error(`step-5b: cannot determine story id from branch "${branchName}". Pass --story-id explicitly. Skipping audit.`);
|
|
5408
5441
|
process.exit(0); // exit 0 — informational
|
|
5409
5442
|
}
|
|
5443
|
+
// HC-GF-270-followup-3: an id read from the branch name gets the check --story-id got (the pattern has no length cap).
|
|
5444
|
+
const derivedIdError = runArgsError([{ label: 'the story id read from the branch name', kind: 'storyId', value: storyId }]);
|
|
5445
|
+
if (derivedIdError) {
|
|
5446
|
+
console.error(`step-5b: ${derivedIdError}. Pass --story-id explicitly.`);
|
|
5447
|
+
process.exit(1);
|
|
5448
|
+
}
|
|
5410
5449
|
|
|
5411
5450
|
// 2. Capture PR diff (defensive — empty diff → no findings)
|
|
5412
5451
|
const baseBranch = cmdOpts.base || 'develop';
|
|
5413
5452
|
let diff = '';
|
|
5414
5453
|
try {
|
|
5415
|
-
diff =
|
|
5416
|
-
{ cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
|
|
5454
|
+
diff = gitRead(['diff', `origin/${baseBranch}...HEAD`]);
|
|
5417
5455
|
} catch {
|
|
5418
5456
|
// Fallback: no remote tracking — try local base
|
|
5419
5457
|
try {
|
|
5420
|
-
diff =
|
|
5421
|
-
{ cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
|
|
5458
|
+
diff = gitRead(['diff', `${baseBranch}...HEAD`]);
|
|
5422
5459
|
} catch { /* leave empty */ }
|
|
5423
5460
|
}
|
|
5424
5461
|
|
|
@@ -5427,8 +5464,7 @@ program
|
|
|
5427
5464
|
let commitMessages;
|
|
5428
5465
|
for (const range of [`origin/${baseBranch}..HEAD`, `${baseBranch}..HEAD`]) {
|
|
5429
5466
|
try {
|
|
5430
|
-
commitMessages =
|
|
5431
|
-
{ cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
|
|
5467
|
+
commitMessages = gitRead(['log', '--format=%B', range]);
|
|
5432
5468
|
break;
|
|
5433
5469
|
} catch { /* try the next range */ }
|
|
5434
5470
|
}
|
|
@@ -5482,7 +5518,9 @@ program
|
|
|
5482
5518
|
baseBranch,
|
|
5483
5519
|
referencedSkills,
|
|
5484
5520
|
});
|
|
5485
|
-
|
|
5521
|
+
// `--artifact-dir` is the deliberate escape hatch (a CI gate writes outside the checkout); the default is
|
|
5522
|
+
// the story's own directory, which a story id cannot leave (HC-GF-270-followup-3).
|
|
5523
|
+
const outDir = cmdOpts.artifactDir ? path.resolve(repoRoot, cmdOpts.artifactDir) : storyDirIn(path.join(repoRoot, '.github/pipeline'), storyId);
|
|
5486
5524
|
fs.mkdirSync(outDir, { recursive: true });
|
|
5487
5525
|
const outPath = path.join(outDir, 'step-5b-skill-audit.md');
|
|
5488
5526
|
fs.writeFileSync(outPath, artifact);
|
|
@@ -5566,8 +5604,8 @@ program
|
|
|
5566
5604
|
.option('--contracts', 'Run contract validation between pipeline agents')
|
|
5567
5605
|
.option('--snapshot', 'Save current eval + contract results as regression baseline')
|
|
5568
5606
|
.option('--regression', 'Compare current results against saved baseline (detect drift)')
|
|
5569
|
-
.option('--judge', 'Run LLM-as-judge scenarios
|
|
5570
|
-
.option('--provider <name>', 'LLM-judge provider:
|
|
5607
|
+
.option('--judge', 'Run LLM-as-judge scenarios. Needs --provider claude (paid); the free gh-models default was retired 2026-07-30')
|
|
5608
|
+
.option('--provider <name>', 'LLM-judge provider: claude (paid Sonnet, needs ANTHROPIC_API_KEY). There is no default')
|
|
5571
5609
|
.option('--evidence-mode <mode>', 'HC-RC-001 editor-LLM evidence transfer: "local" writes .hone/eval-evidence.json (signed); "off" disables (default)')
|
|
5572
5610
|
.action(async (opts) => {
|
|
5573
5611
|
const path = require('path');
|
|
@@ -5650,14 +5688,11 @@ program
|
|
|
5650
5688
|
process.exit(0);
|
|
5651
5689
|
}
|
|
5652
5690
|
|
|
5653
|
-
// HC-019n-followup-33
|
|
5654
|
-
//
|
|
5655
|
-
//
|
|
5656
|
-
//
|
|
5657
|
-
//
|
|
5658
|
-
// HC-COMM-011-followup-3: provider wiring extracted to lib/judge-provider.js
|
|
5659
|
-
// (shared with `hone agent-eval --judge`). Same zero-cost default (gh-models)
|
|
5660
|
-
// + Sonnet-not-Opus paid path; the retired dated Sonnet id must not appear
|
|
5691
|
+
// HC-019n-followup-33 gave the judge lane a free default (GitHub Models). HC-GF-340: GitHub
|
|
5692
|
+
// retired it on 2026-07-30, so there is no default now and no free path; the operator names
|
|
5693
|
+
// `--provider claude` (paid Sonnet) or gets one sentence saying why.
|
|
5694
|
+
// HC-COMM-011-followup-3: provider wiring lives in lib/judge-provider.js (shared with
|
|
5695
|
+
// `hone agent-eval --judge`); the retired dated Sonnet id must not appear
|
|
5661
5696
|
// (pinned by opus-4-8-upgrade.test.js).
|
|
5662
5697
|
const { buildJudgeCallLLM } = require('./lib/judge-provider');
|
|
5663
5698
|
const built = buildJudgeCallLLM(opts.provider, { axios });
|
|
@@ -5761,13 +5796,13 @@ program
|
|
|
5761
5796
|
// expected_output_format heuristics. Two systems, two different
|
|
5762
5797
|
// questions; coexist.
|
|
5763
5798
|
//
|
|
5764
|
-
//
|
|
5765
|
-
//
|
|
5766
|
-
//
|
|
5799
|
+
// HC-GF-340: there is no default provider. It used to be gh-models (free GH PAT inference), which
|
|
5800
|
+
// GitHub retired on 2026-07-30; the only one that works now is paid, and an adopter must not be
|
|
5801
|
+
// billed by a default they never chose. `--no-llm` (the dry run) needs none.
|
|
5767
5802
|
program
|
|
5768
5803
|
.command('skill-eval <skillName>')
|
|
5769
5804
|
.description('Run derived-skill eval scenarios against an LLM (HC-010d-followup-1)')
|
|
5770
|
-
.option('--provider <name>', 'LLM provider:
|
|
5805
|
+
.option('--provider <name>', 'LLM provider: claude-haiku (paid, needs ANTHROPIC_API_KEY). gh-models was retired 2026-07-30; there is no default')
|
|
5771
5806
|
.option('--scenario <id>', 'Run a single scenario by id (e.g., SF-DEV-EVAL-001)')
|
|
5772
5807
|
.option('--tag <tag>', 'Filter scenarios by HC-010c rule-id tag (e.g., SF-SEC-001)')
|
|
5773
5808
|
.option('--format <fmt>', 'Output format: pretty | json', 'pretty')
|
|
@@ -5836,62 +5871,26 @@ program
|
|
|
5836
5871
|
process.exit(0);
|
|
5837
5872
|
}
|
|
5838
5873
|
|
|
5839
|
-
// Provider wiring
|
|
5840
|
-
// future provider addition lives in one switch.
|
|
5874
|
+
// Provider wiring. Inline + per-provider so a future provider addition lives in one switch.
|
|
5841
5875
|
const axios = require('axios');
|
|
5842
|
-
//
|
|
5843
|
-
//
|
|
5844
|
-
// `cli/lib/release-review-config.js
|
|
5845
|
-
//
|
|
5846
|
-
// 32K leaves zero budget for scenario.input + JSON envelope, so
|
|
5847
|
-
// GH Models returns HTTP 400 and every scenario errors. 8000
|
|
5848
|
-
// chars ≈ ~2000 tokens for the system slot, leaving ~6000 tokens
|
|
5849
|
-
// for scenario.input + envelope — comfortable under GH Models'
|
|
5850
|
-
// cap while still preserving enough of the skill body to evaluate
|
|
5851
|
-
// adopter patterns.
|
|
5876
|
+
// 8000 chars (~2000 tokens) for the system slot leaves the rest of the request for
|
|
5877
|
+
// scenario.input + the JSON envelope. The cap was sized for GitHub Models' 8000-token
|
|
5878
|
+
// request-body limit (see `cli/lib/release-review-config.js`); it is kept so a skill body
|
|
5879
|
+
// that grew large cannot crowd out the scenario now that claude-haiku is the only provider.
|
|
5852
5880
|
const MAX_SKILL_PROMPT_CHARS = 8000;
|
|
5853
5881
|
let apiKey, modelLabel, callLLM;
|
|
5854
|
-
if (opts.provider === 'gh-models') {
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
// Exit 2 — operator config error, NOT a skill regression.
|
|
5860
|
-
// Pass-2 review caught: exit 1 collided with the CI-gate exit
|
|
5861
|
-
// code for eval failures, so a missing secret was reported
|
|
5862
|
-
// as "skill regression" by the CI gate. The convention from
|
|
5863
|
-
// the regression at line 172-188 is exit 2 for operator
|
|
5864
|
-
// errors, exit 1 for eval failures.
|
|
5865
|
-
process.exit(2);
|
|
5866
|
-
}
|
|
5867
|
-
modelLabel = 'openai/gpt-4.1';
|
|
5868
|
-
callLLM = async (systemPrompt, userPrompt) => {
|
|
5869
|
-
const { data } = await axios.post(
|
|
5870
|
-
'https://models.github.ai/inference/chat/completions',
|
|
5871
|
-
{
|
|
5872
|
-
model: modelLabel,
|
|
5873
|
-
messages: [
|
|
5874
|
-
{ role: 'system', content: systemPrompt.slice(0, MAX_SKILL_PROMPT_CHARS) },
|
|
5875
|
-
{ role: 'user', content: userPrompt },
|
|
5876
|
-
],
|
|
5877
|
-
max_tokens: 2048,
|
|
5878
|
-
},
|
|
5879
|
-
{
|
|
5880
|
-
headers: {
|
|
5881
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
5882
|
-
'Content-Type': 'application/json',
|
|
5883
|
-
},
|
|
5884
|
-
timeout: 120000,
|
|
5885
|
-
}
|
|
5886
|
-
);
|
|
5887
|
-
return data.choices?.[0]?.message?.content || '';
|
|
5888
|
-
};
|
|
5882
|
+
if (opts.provider === undefined || opts.provider === 'gh-models') {
|
|
5883
|
+
// HC-GF-340: refuse before any request. Exit 2 — operator config error, NOT a skill
|
|
5884
|
+
// regression (exit 1 is the CI-gate code for eval failures).
|
|
5885
|
+
console.error(`✗ ${require('./lib/judge-provider').providerRequiredMessage('--provider claude-haiku')}`);
|
|
5886
|
+
process.exit(2);
|
|
5889
5887
|
} else if (opts.provider === 'claude-haiku') {
|
|
5890
5888
|
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
5891
5889
|
if (!apiKey) {
|
|
5892
5890
|
console.error('✗ ANTHROPIC_API_KEY not set. Required for --provider claude-haiku.');
|
|
5893
5891
|
console.error(' Set: export ANTHROPIC_API_KEY=sk-ant-...');
|
|
5894
|
-
// Exit 2 — operator config error (
|
|
5892
|
+
// Exit 2 — operator config error, NOT a skill regression (exit 1 is the CI-gate code
|
|
5893
|
+
// for eval failures).
|
|
5895
5894
|
process.exit(2);
|
|
5896
5895
|
}
|
|
5897
5896
|
modelLabel = 'claude-haiku-4-5-20251001';
|
|
@@ -5909,10 +5908,16 @@ program
|
|
|
5909
5908
|
},
|
|
5910
5909
|
timeout: 120000,
|
|
5911
5910
|
});
|
|
5912
|
-
|
|
5911
|
+
const text = data.content?.[0]?.text;
|
|
5912
|
+
// An empty answer is a provider failure the runner records as an error, not a scenario
|
|
5913
|
+
// that failed its assertions.
|
|
5914
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
5915
|
+
throw new Error(`claude-haiku returned no text (stop_reason: ${data.stop_reason || 'unknown'})`);
|
|
5916
|
+
}
|
|
5917
|
+
return text;
|
|
5913
5918
|
};
|
|
5914
5919
|
} else {
|
|
5915
|
-
console.error(`✗ Invalid --provider: ${opts.provider}. Use '
|
|
5920
|
+
console.error(`✗ Invalid --provider: ${opts.provider}. Use 'claude-haiku'.`);
|
|
5916
5921
|
process.exit(2);
|
|
5917
5922
|
}
|
|
5918
5923
|
|
|
@@ -5945,10 +5950,9 @@ program
|
|
|
5945
5950
|
|
|
5946
5951
|
console.log(formatResults(summary, opts.format));
|
|
5947
5952
|
|
|
5948
|
-
// Exit 1 on any non-pass so CI (HC-019f-style gate) can wire this
|
|
5949
|
-
//
|
|
5950
|
-
//
|
|
5951
|
-
// zero adopter cost.
|
|
5953
|
+
// Exit 1 on any non-pass so CI (HC-019f-style gate) can wire this as a required check. It is
|
|
5954
|
+
// paid now (HC-GF-340: the free gh-models default was retired), so a CI gate needs an
|
|
5955
|
+
// ANTHROPIC_API_KEY secret and a decision to spend.
|
|
5952
5956
|
process.exit(summary.failed + summary.errors > 0 ? 1 : 0);
|
|
5953
5957
|
});
|
|
5954
5958
|
|
|
@@ -5997,6 +6001,7 @@ program
|
|
|
5997
6001
|
.description('Fetch the editor-mode instruction for a run parked awaiting_editor (HC-COMM-010 B3)')
|
|
5998
6002
|
.option('--format <fmt>', 'Output format: pretty | json', 'pretty')
|
|
5999
6003
|
.action(async (runId, opts) => {
|
|
6004
|
+
refuseBadRunArgs([{ label: '<runId>', kind: 'runId', value: runId }]); // HC-GF-270-followup-3
|
|
6000
6005
|
const config = getConfig();
|
|
6001
6006
|
const client = api(config);
|
|
6002
6007
|
try {
|
|
@@ -6028,6 +6033,10 @@ program
|
|
|
6028
6033
|
.option('--contract <version>', 'Instruction contract version (from step-instruction)', '1')
|
|
6029
6034
|
.option('--format <fmt>', 'Output format: pretty | json', 'pretty')
|
|
6030
6035
|
.action(async (runId, stepKey, opts) => {
|
|
6036
|
+
refuseBadRunArgs([ // HC-GF-270-followup-3: both become path segments of the submission
|
|
6037
|
+
{ label: '<runId>', kind: 'runId', value: runId },
|
|
6038
|
+
{ label: '<stepKey>', kind: 'stepKey', value: stepKey },
|
|
6039
|
+
]);
|
|
6031
6040
|
const config = getConfig();
|
|
6032
6041
|
const client = api(config);
|
|
6033
6042
|
try {
|
|
@@ -6082,6 +6091,19 @@ function runContextIo(apiConfig) {
|
|
|
6082
6091
|
const abs = path.resolve(cwd, rel);
|
|
6083
6092
|
return abs.startsWith(cwd + path.sep) ? abs : null;
|
|
6084
6093
|
};
|
|
6094
|
+
// HC-GF-283: where this checkout's issues live — host, owner, repo — resolved ONCE and shared by
|
|
6095
|
+
// both gh calls, so the text and the trust cannot come from different hosts. An SSH alias is
|
|
6096
|
+
// resolved with `ssh -G` (prints config, never connects). A failure rejects both calls: nothing is
|
|
6097
|
+
// fetched rather than gh falling back to its default host.
|
|
6098
|
+
let issueRepoP = null;
|
|
6099
|
+
const issueRepo = () => issueRepoP || (issueRepoP = (async () => {
|
|
6100
|
+
const { resolveRemote } = require('./lib/git-remote');
|
|
6101
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8', timeout: 10_000 }).trim();
|
|
6102
|
+
const { execFile } = require('child_process');
|
|
6103
|
+
return resolveRemote(remoteUrl, {
|
|
6104
|
+
sshConfig: async (alias) => (await require('util').promisify(execFile)('ssh', ['-G', alias], { encoding: 'utf8', timeout: 10_000 })).stdout,
|
|
6105
|
+
});
|
|
6106
|
+
})());
|
|
6085
6107
|
return {
|
|
6086
6108
|
// Every subprocess this adapter starts carries a timeout: a hung git or gh must fail open with a
|
|
6087
6109
|
// message, not stall the command (node-developer hard rule 5). The module turns each failure into
|
|
@@ -6110,15 +6132,15 @@ function runContextIo(apiConfig) {
|
|
|
6110
6132
|
return fs.readFileSync(abs, 'utf8');
|
|
6111
6133
|
},
|
|
6112
6134
|
// Async (execFile, no shell) so queue-stories can resolve several issues at once.
|
|
6135
|
+
// HC-GF-283: `--repo HOST/OWNER/REPO`. Without the host gh used its default one, so a GitHub
|
|
6136
|
+
// Enterprise remote behind an SSH alias read the issue from github.com.
|
|
6113
6137
|
fetchIssue: async (issueNumber) => {
|
|
6114
|
-
const
|
|
6115
|
-
const m = remoteUrl.match(/[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
6116
|
-
if (!m) throw new Error(`could not parse owner/repo from remote: ${remoteUrl}`);
|
|
6138
|
+
const { host, owner, repo } = await issueRepo();
|
|
6117
6139
|
const { execFile } = require('child_process');
|
|
6118
6140
|
let stdout;
|
|
6119
6141
|
try {
|
|
6120
6142
|
({ stdout } = await require('util').promisify(execFile)('gh',
|
|
6121
|
-
['issue', 'view', String(issueNumber), '--repo', `${
|
|
6143
|
+
['issue', 'view', String(issueNumber), '--repo', `${host}/${owner}/${repo}`, '--json', 'title,body,url'],
|
|
6122
6144
|
{ encoding: 'utf8', timeout: 30_000 }));
|
|
6123
6145
|
} catch (e) {
|
|
6124
6146
|
// execFile keeps gh's stderr on the error — its first line is the actionable part ("gh auth
|
|
@@ -6132,14 +6154,12 @@ function runContextIo(apiConfig) {
|
|
|
6132
6154
|
// HC-GF-270: who wrote the issue. run-context.js classifies a failure and story-provenance.js
|
|
6133
6155
|
// decides trust; this only fetches. execFile, not a shell string: owner/repo come from the remote.
|
|
6134
6156
|
fetchIssueProvenance: async (issueNumber) => {
|
|
6135
|
-
const
|
|
6136
|
-
const
|
|
6137
|
-
if (!m) throw new Error(`could not parse owner/repo from remote: ${remoteUrl}`);
|
|
6138
|
-
const where = { owner: m[1], repo: m[2], remoteUrl };
|
|
6157
|
+
const { host, owner, repo, remoteUrl } = await issueRepo();
|
|
6158
|
+
const where = { owner, repo, remoteUrl, host };
|
|
6139
6159
|
try {
|
|
6140
6160
|
const { execFile } = require('child_process');
|
|
6141
6161
|
const { stdout } = await require('util').promisify(execFile)('gh',
|
|
6142
|
-
['api', `repos/${
|
|
6162
|
+
['api', `repos/${owner}/${repo}/issues/${issueNumber}`, '--hostname', host],
|
|
6143
6163
|
{ encoding: 'utf8', timeout: 20_000 });
|
|
6144
6164
|
return { ...where, issue: JSON.parse(stdout) };
|
|
6145
6165
|
} catch (e) {
|
|
@@ -7208,12 +7228,12 @@ program
|
|
|
7208
7228
|
|
|
7209
7229
|
program
|
|
7210
7230
|
.command('release-review')
|
|
7211
|
-
.description('Holistic code review of all changed files before deployment (
|
|
7231
|
+
.description('Holistic code review of all changed files before deployment (needs --provider opus, paid; the free GH Models default was retired 2026-07-30)')
|
|
7212
7232
|
.option('--base <branch>', 'Base branch to diff against', 'main')
|
|
7213
7233
|
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
7214
7234
|
.option('--dry-run', 'Show what would be reviewed without calling the LLM', false)
|
|
7215
7235
|
.option('--max-files <n>', 'Max source files to include in review', '40')
|
|
7216
|
-
.option('--provider <name>', 'LLM provider:
|
|
7236
|
+
.option('--provider <name>', 'LLM provider: opus (paid, needs ANTHROPIC_API_KEY). gh-models was retired 2026-07-30; there is no default')
|
|
7217
7237
|
.option('--cache <mode>', 'HC-RC-002-followup-1 content-hash cache: on | off (default on)', 'on')
|
|
7218
7238
|
.action(async (opts) => {
|
|
7219
7239
|
const { execSync } = require('child_process');
|
|
@@ -7251,12 +7271,23 @@ program
|
|
|
7251
7271
|
// CI step passes `--base origin/main` and the previous code prefixed
|
|
7252
7272
|
// again → `origin/origin/main` → git rejected → silent fallback to
|
|
7253
7273
|
// `HEAD~10`. Both lanes reviewed the wrong diff in PR #313 first run.
|
|
7274
|
+
// HC-GF-270-followup-3: the base gets the check step-5b and verify-pr give theirs. resolveBaseRef prefixes
|
|
7275
|
+
// `origin/`, so an option-shaped base cannot reach git as an option — but git would fail on it and the
|
|
7276
|
+
// review would fall back to HEAD~10 without saying so.
|
|
7277
|
+
const badBase = runArgsError([{ label: '--base', kind: 'gitRev', value: opts.base }]);
|
|
7278
|
+
if (badBase) {
|
|
7279
|
+
console.error(badBase);
|
|
7280
|
+
emitStatusEnvelope('refused', { error: badBase });
|
|
7281
|
+
process.exit(2);
|
|
7282
|
+
}
|
|
7254
7283
|
const baseRef = resolveBaseRef(opts.base);
|
|
7255
7284
|
|
|
7256
7285
|
// 1. Get changed files
|
|
7257
7286
|
let changedFiles;
|
|
7258
7287
|
try {
|
|
7259
|
-
|
|
7288
|
+
// HC-GF-270-followup-3: argument lists, no shell. baseRef always starts `origin/` (resolveBaseRef), so it
|
|
7289
|
+
// cannot be read as an option.
|
|
7290
|
+
const raw = execFileSync('git', ['diff', '--name-only', `${baseRef}...HEAD`], { encoding: 'utf8', cwd: repoRoot, env: gitEnv() });
|
|
7260
7291
|
changedFiles = raw.trim().split('\n').filter(Boolean);
|
|
7261
7292
|
} catch {
|
|
7262
7293
|
try {
|
|
@@ -7299,34 +7330,32 @@ program
|
|
|
7299
7330
|
}
|
|
7300
7331
|
|
|
7301
7332
|
// 3. Provider validation + credential check
|
|
7302
|
-
|
|
7303
|
-
|
|
7333
|
+
// HC-GF-340: gh-models (the old default) is retired, and no provider is a default now, so an
|
|
7334
|
+
// unnamed or retired one is refused with the cause instead of calling an endpoint that
|
|
7335
|
+
// answers "OK" and reporting "empty response ... content filter, quota or model name".
|
|
7336
|
+
if (opts.provider === undefined || opts.provider === 'gh-models') {
|
|
7337
|
+
console.error(require('./lib/judge-provider').providerRequiredMessage('--provider opus'));
|
|
7338
|
+
process.exit(1);
|
|
7339
|
+
}
|
|
7340
|
+
if (opts.provider !== 'opus') {
|
|
7341
|
+
console.error(`Invalid --provider: ${opts.provider}. Use 'opus'.`);
|
|
7304
7342
|
process.exit(1);
|
|
7305
7343
|
}
|
|
7306
7344
|
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
console.error('In CI: GITHUB_TOKEN is auto-injected. Locally: export GITHUB_TOKEN=<your PAT>.');
|
|
7314
|
-
process.exit(1);
|
|
7315
|
-
}
|
|
7316
|
-
} else {
|
|
7317
|
-
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
7318
|
-
providerLabel = 'Anthropic Opus (claude-opus-4-8)';
|
|
7319
|
-
if (!apiKey) {
|
|
7320
|
-
console.error('ANTHROPIC_API_KEY not set. Required for --provider opus.');
|
|
7321
|
-
console.error('Set it: export ANTHROPIC_API_KEY=sk-ant-...');
|
|
7322
|
-
process.exit(1);
|
|
7323
|
-
}
|
|
7345
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
7346
|
+
const providerLabel = 'Anthropic Opus (claude-opus-4-8)';
|
|
7347
|
+
if (!apiKey) {
|
|
7348
|
+
console.error('ANTHROPIC_API_KEY not set. Required for --provider opus.');
|
|
7349
|
+
console.error('Set it: export ANTHROPIC_API_KEY=sk-ant-...');
|
|
7350
|
+
process.exit(1);
|
|
7324
7351
|
}
|
|
7325
7352
|
|
|
7326
7353
|
// 4. Build the diff content (truncated per-file to stay within context)
|
|
7327
7354
|
let diffContent;
|
|
7328
7355
|
try {
|
|
7329
|
-
|
|
7356
|
+
// HC-GF-270-followup-3: each changed file is ONE argument after `--`. They were wrapped in single quotes
|
|
7357
|
+
// inside a shell string, so a file a PR author named with a `'` in it ran as a command.
|
|
7358
|
+
diffContent = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--', ...filesToReview], {
|
|
7330
7359
|
encoding: 'utf8', cwd: repoRoot, env: gitEnv(), maxBuffer: 10 * 1024 * 1024,
|
|
7331
7360
|
});
|
|
7332
7361
|
} catch {
|
|
@@ -7338,9 +7367,7 @@ program
|
|
|
7338
7367
|
}
|
|
7339
7368
|
}
|
|
7340
7369
|
|
|
7341
|
-
// Provider-specific truncation
|
|
7342
|
-
// request-body cap via models.github.ai/inference — 100K chars overflows.
|
|
7343
|
-
// See cli/lib/release-review-config.js for the budget math.
|
|
7370
|
+
// Provider-specific truncation; see cli/lib/release-review-config.js for the budget math.
|
|
7344
7371
|
const MAX_DIFF_CHARS = getMaxDiffChars(opts.provider);
|
|
7345
7372
|
if (diffContent.length > MAX_DIFF_CHARS) {
|
|
7346
7373
|
diffContent = diffContent.slice(0, MAX_DIFF_CHARS) +
|
|
@@ -7423,9 +7450,7 @@ program
|
|
|
7423
7450
|
normalizeCacheFlag,
|
|
7424
7451
|
} = require('./lib/release-review-cache');
|
|
7425
7452
|
const cacheEnabled = normalizeCacheFlag(opts.cache);
|
|
7426
|
-
const modelForCache =
|
|
7427
|
-
? 'openai/gpt-4.1'
|
|
7428
|
-
: 'claude-opus-4-8';
|
|
7453
|
+
const modelForCache = 'claude-opus-4-8';
|
|
7429
7454
|
let cacheContentHash = null;
|
|
7430
7455
|
if (cacheEnabled) {
|
|
7431
7456
|
try {
|
|
@@ -7488,59 +7513,30 @@ program
|
|
|
7488
7513
|
}
|
|
7489
7514
|
|
|
7490
7515
|
// 6. Call LLM (provider-branched, HC-080a-spike)
|
|
7491
|
-
// max_tokens
|
|
7492
|
-
//
|
|
7493
|
-
// safe ceiling for openai/gpt-4.1 via GH Models; Opus supports more but
|
|
7494
|
-
// running it with 4096 keeps the comparison apples-to-apples.
|
|
7516
|
+
// max_tokens stays at 4096, the value the HC-080a-spike comparison with GPT-4.1 held constant,
|
|
7517
|
+
// so a review's output budget has not changed now that Opus is the only provider.
|
|
7495
7518
|
const MAX_OUTPUT_TOKENS = 4096;
|
|
7496
7519
|
const startedAt = Date.now();
|
|
7497
7520
|
try {
|
|
7498
7521
|
let responseText, inputTokens, outputTokens, modelLabel;
|
|
7499
7522
|
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
},
|
|
7518
|
-
timeout: 120000,
|
|
7519
|
-
}
|
|
7520
|
-
);
|
|
7521
|
-
responseText = data.choices?.[0]?.message?.content || '';
|
|
7522
|
-
inputTokens = data.usage?.prompt_tokens || 0;
|
|
7523
|
-
outputTokens = data.usage?.completion_tokens || 0;
|
|
7524
|
-
modelLabel = 'openai/gpt-4.1';
|
|
7525
|
-
} else {
|
|
7526
|
-
const { data } = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
7527
|
-
model: 'claude-opus-4-8',
|
|
7528
|
-
max_tokens: MAX_OUTPUT_TOKENS,
|
|
7529
|
-
system: systemPrompt,
|
|
7530
|
-
messages: [{ role: 'user', content: userPrompt }],
|
|
7531
|
-
}, {
|
|
7532
|
-
headers: {
|
|
7533
|
-
'x-api-key': apiKey,
|
|
7534
|
-
'anthropic-version': '2023-06-01',
|
|
7535
|
-
'content-type': 'application/json',
|
|
7536
|
-
},
|
|
7537
|
-
timeout: 120000,
|
|
7538
|
-
});
|
|
7539
|
-
responseText = data.content?.[0]?.text || '';
|
|
7540
|
-
inputTokens = data.usage?.input_tokens || 0;
|
|
7541
|
-
outputTokens = data.usage?.output_tokens || 0;
|
|
7542
|
-
modelLabel = 'claude-opus-4-8';
|
|
7543
|
-
}
|
|
7523
|
+
const { data } = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
7524
|
+
model: 'claude-opus-4-8',
|
|
7525
|
+
max_tokens: MAX_OUTPUT_TOKENS,
|
|
7526
|
+
system: systemPrompt,
|
|
7527
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
7528
|
+
}, {
|
|
7529
|
+
headers: {
|
|
7530
|
+
'x-api-key': apiKey,
|
|
7531
|
+
'anthropic-version': '2023-06-01',
|
|
7532
|
+
'content-type': 'application/json',
|
|
7533
|
+
},
|
|
7534
|
+
timeout: 120000,
|
|
7535
|
+
});
|
|
7536
|
+
responseText = data.content?.[0]?.text || '';
|
|
7537
|
+
inputTokens = data.usage?.input_tokens || 0;
|
|
7538
|
+
outputTokens = data.usage?.output_tokens || 0;
|
|
7539
|
+
modelLabel = 'claude-opus-4-8';
|
|
7544
7540
|
|
|
7545
7541
|
const elapsedMs = Date.now() - startedAt;
|
|
7546
7542
|
|
|
@@ -8447,6 +8443,13 @@ program
|
|
|
8447
8443
|
.option('--repo <name>', 'Repository name the batch was queued under (default: this directory\'s name)')
|
|
8448
8444
|
.action(async (batchId, opts) => {
|
|
8449
8445
|
const { followBatch, EXIT } = require('./lib/batch-follow');
|
|
8446
|
+
// HC-GF-270-followup-3: refused before the preflight git/gh calls and the config read — with EXIT.FATAL (1),
|
|
8447
|
+
// because 2 is EXIT.NEEDS_YOU, which a caller reads as "the batch needs a human".
|
|
8448
|
+
const badBatch = runArgsError([{ label: '<batchId>', kind: 'batchId', value: batchId }]);
|
|
8449
|
+
if (badBatch) {
|
|
8450
|
+
console.error(badBatch);
|
|
8451
|
+
process.exit(EXIT.FATAL);
|
|
8452
|
+
}
|
|
8450
8453
|
const { spawn, spawnSync } = require('child_process');
|
|
8451
8454
|
const positive = (raw, flag, unit) => {
|
|
8452
8455
|
const n = Number(raw);
|
|
@@ -8947,7 +8950,13 @@ program
|
|
|
8947
8950
|
.option('--format <fmt>', 'pretty or json', 'pretty')
|
|
8948
8951
|
.action(async (prOrBranch, opts) => {
|
|
8949
8952
|
// HC-GF-270-followup-1: a --workflow-id is checked before any gh call or CI command runs.
|
|
8950
|
-
|
|
8953
|
+
// HC-GF-270-followup-3: so are the story id, which names the directory step-5c-ci.md is written to, and the
|
|
8954
|
+
// base, which reaches git through step-5b.
|
|
8955
|
+
refuseBadRunArgs([
|
|
8956
|
+
{ label: '--workflow-id', kind: 'runId', value: opts.workflowId, optional: true },
|
|
8957
|
+
{ label: '--story-id', kind: 'storyId', value: opts.storyId, optional: true },
|
|
8958
|
+
{ label: '--base', kind: 'gitRev', value: opts.base || 'develop' },
|
|
8959
|
+
]);
|
|
8951
8960
|
const { execFileSync, spawnSync } = require('child_process');
|
|
8952
8961
|
// HC-GF-200: refuse a mistyped bound HERE, naming the flag. Left to node,
|
|
8953
8962
|
// a NaN becomes `RangeError: ... It must be an integer. Received NaN` from
|
|
@@ -8974,6 +8983,9 @@ program
|
|
|
8974
8983
|
let branch = '';
|
|
8975
8984
|
try { branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { /* */ }
|
|
8976
8985
|
const storyId = opts.storyId || extractStoryIdFromBranch(branch) || extractStoryIdFromBranch(prOrBranch) || 'unknown-story';
|
|
8986
|
+
// HC-GF-270-followup-3: an id read from the branch or the ref gets the check --story-id got (the pattern has no
|
|
8987
|
+
// length cap) — before the CI command runs, and so the artifact write below cannot throw.
|
|
8988
|
+
refuseBadRunArgs([{ label: 'the story id read from the branch name', kind: 'storyId', value: storyId }]);
|
|
8977
8989
|
|
|
8978
8990
|
// 2. CI gate mode: --mode > ci.gate config > github.
|
|
8979
8991
|
let configGate = 'github';
|
|
@@ -9030,12 +9042,16 @@ program
|
|
|
9030
9042
|
|
|
9031
9043
|
const verdict = ciGateVerdict({ mode, gh, local });
|
|
9032
9044
|
|
|
9033
|
-
// 5. Write the real step-5c-ci.md artifact.
|
|
9045
|
+
// 5. Write the real step-5c-ci.md artifact. Best-effort, but the report below names the file only if it was
|
|
9046
|
+
// written (HC-GF-270-followup-3: a failed write used to print the path anyway).
|
|
9047
|
+
let artifactLine = ` → .github/pipeline/${storyId}/step-5c-ci.md`;
|
|
9034
9048
|
try {
|
|
9035
|
-
const dir = path.join(repoRoot, '.github/pipeline', storyId);
|
|
9049
|
+
const dir = storyDirIn(path.join(repoRoot, '.github/pipeline'), storyId); // HC-GF-270-followup-3
|
|
9036
9050
|
fs.mkdirSync(dir, { recursive: true });
|
|
9037
9051
|
fs.writeFileSync(path.join(dir, 'step-5c-ci.md'), renderStep5cArtifact({ storyId, mode, prRef: prOrBranch, gh, local, verdict }));
|
|
9038
|
-
} catch {
|
|
9052
|
+
} catch (e) {
|
|
9053
|
+
artifactLine = ` ⚠ step-5c-ci.md not written: ${e.message}`;
|
|
9054
|
+
}
|
|
9039
9055
|
|
|
9040
9056
|
// 6. Closed loop (HC-019n-followup-36): report the CI-gate verdict to the
|
|
9041
9057
|
// server. This RECORDS against step_5c — which is a post-PR, CLI-produced
|
|
@@ -9072,7 +9088,7 @@ program
|
|
|
9072
9088
|
{ ok: exitCode === EXIT.PASS, exitCode, storyId, mode, skillAudit, ciGate: verdict, gh, local, reported },
|
|
9073
9089
|
`${sym} CI gate (${mode}): ${verdict.verdict} — ${verdict.detail}\n` +
|
|
9074
9090
|
` skill audit: ${opts.skipSkillAudit ? 'skipped' : skillAudit.blocked ? `✗ BLOCKER finding(s) — ${skillAudit.artifact}` : (skillAudit.artifact || skillAudit.error || 'done')}\n` +
|
|
9075
|
-
|
|
9091
|
+
artifactLine);
|
|
9076
9092
|
process.exit(exitCode);
|
|
9077
9093
|
});
|
|
9078
9094
|
|