@hone-ai/cli 1.39.0 → 1.41.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 +174 -53
- package/lib/anthropic-response.js +58 -0
- package/lib/architect-config.js +5 -0
- package/lib/batch-follow.js +253 -16
- package/lib/block-grammar.js +55 -0
- package/lib/branch-protection.js +141 -25
- package/lib/bundle-limits.js +10 -0
- package/lib/bundle-paths.js +20 -3
- package/lib/call-sites.js +209 -26
- package/lib/context-reserve.js +11 -8
- package/lib/judge-provider.js +20 -11
- package/lib/operator-question.js +9 -4
- package/lib/release-review-config.js +3 -2
- package/lib/required-checks.js +354 -0
- package/lib/review-diff.js +188 -0
- package/lib/reviewer-optin.js +115 -0
- package/lib/run-context.js +18 -41
- package/lib/run-report.js +94 -4
- package/lib/security-scanner.js +7 -4
- package/lib/tree-artifact.js +36 -3
- package/lib/verify-patch.js +122 -12
- package/package.json +1 -1
package/hone-cli.js
CHANGED
|
@@ -598,6 +598,10 @@ program
|
|
|
598
598
|
.option('--e2e', 'Also install Playwright E2E framework (use with --install-tests)')
|
|
599
599
|
.option('--no-e2e', 'Skip Playwright even when --install-tests is set')
|
|
600
600
|
.option('--no-branch-protection', 'Skip installing GitHub branch protection on the default branch (H-001)')
|
|
601
|
+
// HC-GF-337g: opt-in. The positive flag MUST be declared first — declared second, Commander
|
|
602
|
+
// defaults aiReviewer to true and every adopter gets the reviewer again (cli/lib/reviewer-optin.js).
|
|
603
|
+
.option('--ai-reviewer', 'Install the AI code reviewer workflow (runs on your own ANTHROPIC_API_KEY repo secret; carries the H-019 learnings gate)')
|
|
604
|
+
.option('--no-ai-reviewer', 'Do not install the AI code reviewer (the default)')
|
|
601
605
|
.option('--refresh', 'Re-scan platform metadata without re-running full setup (HC-013c)')
|
|
602
606
|
.option('--ci-gate <mode>', 'HC-RC-004: CI gating mode (github | local | mixed | none). Skips interactive prompt.')
|
|
603
607
|
.action(async (opts) => {
|
|
@@ -717,6 +721,15 @@ program
|
|
|
717
721
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
718
722
|
fs.mkdirSync(scriptsEntDir, { recursive: true });
|
|
719
723
|
|
|
724
|
+
// HC-GF-337g: the reviewer is opt-in. Its scripts are left out of the list without the flag, and
|
|
725
|
+
// that absence is the whole opt-out: setup-ai-pipeline.sh installs ai-review.yml either way (its
|
|
726
|
+
// other gates — secret scan, tracker, metrics check, E2E healer — are not the reviewer's), copies
|
|
727
|
+
// the script, the learnings gate and the ESM package.json only when the script arrived, stamps the
|
|
728
|
+
// workflow so its ai-review job runs only then, and generate_config records ai_review.installed
|
|
729
|
+
// from that same file.
|
|
730
|
+
const { shouldInstallReviewer, reviewerNotice, workflowReviewerState } = require('./lib/reviewer-optin');
|
|
731
|
+
const installReviewer = shouldInstallReviewer(opts);
|
|
732
|
+
|
|
720
733
|
// Build parallel fetch list
|
|
721
734
|
const fetches = [
|
|
722
735
|
// Agent stubs and skill files. HC-GF-136's gate found `key`, `promise` and `write` here
|
|
@@ -750,38 +763,42 @@ program
|
|
|
750
763
|
promise: client.get('/scripts/copilot-instructions').then(r => r.data),
|
|
751
764
|
write: (data) => fs.writeFileSync(path.join(enterpriseGH, 'copilot-instructions.md'), data),
|
|
752
765
|
},
|
|
753
|
-
// AI review workflow template
|
|
766
|
+
// AI review workflow template — installed with or without the reviewer (HC-GF-337g)
|
|
754
767
|
{
|
|
755
768
|
key: 'workflows/ai-review.yml',
|
|
756
769
|
promise: client.get('/scripts/workflow-template', { responseType: 'text', headers: { Accept: 'text/yaml' } }).then(r => r.data),
|
|
757
770
|
write: (data) => fs.writeFileSync(path.join(workflowsDir, 'ai-review.yml'), data),
|
|
758
771
|
},
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
772
|
+
// HC-GF-337g: the reviewer's three entries, requested only on `--ai-reviewer` — not fetched and
|
|
773
|
+
// discarded, so a repository that did not opt in makes no reviewer request at all.
|
|
774
|
+
...(installReviewer ? [
|
|
775
|
+
// AI reviewer script
|
|
776
|
+
{
|
|
777
|
+
key: 'scripts/ai-reviewer.js',
|
|
778
|
+
promise: client.get('/scripts/reviewer-script', { responseType: 'text', headers: { Accept: 'text/javascript' } }).then(r => r.data),
|
|
779
|
+
write: (data) => fs.writeFileSync(path.join(scriptsEntDir, 'ai-reviewer.js'), data),
|
|
780
|
+
},
|
|
781
|
+
// H-007: Learnings gate helper (imported by ai-reviewer at CI time).
|
|
782
|
+
// Both files are ESM. The .github/scripts/package.json (next entry below)
|
|
783
|
+
// declares ESM scope for this directory so node treats both .js files as
|
|
784
|
+
// ES modules regardless of the adopter's root package.json (see #16).
|
|
785
|
+
{
|
|
786
|
+
key: 'scripts/check-learnings-gate.js',
|
|
787
|
+
promise: client.get('/scripts/learnings-gate-script', { responseType: 'text', headers: { Accept: 'text/javascript' } }).then(r => r.data),
|
|
788
|
+
write: (data) => fs.writeFileSync(path.join(scriptsEntDir, 'check-learnings-gate.js'), data),
|
|
789
|
+
},
|
|
790
|
+
// H-007: Subdir package.json declares ESM scope for .github/scripts/.
|
|
791
|
+
// Both ai-reviewer.js and check-learnings-gate.js use `import` syntax;
|
|
792
|
+
// without this file, node treats them as CommonJS (the npm-init default)
|
|
793
|
+
// and the workflow fails with `Cannot use import statement outside a
|
|
794
|
+
// module`. Subdir package.json scope OVERRIDES the adopter's root —
|
|
795
|
+
// adopter's main project module semantics are unaffected. See #16.
|
|
796
|
+
{
|
|
797
|
+
key: 'scripts/package.json',
|
|
798
|
+
promise: Promise.resolve('{"type": "module"}\n'),
|
|
799
|
+
write: (data) => fs.writeFileSync(path.join(scriptsEntDir, 'package.json'), data),
|
|
800
|
+
},
|
|
801
|
+
] : []),
|
|
785
802
|
// H-004: Auto-promote workflow template (installed at setup time;
|
|
786
803
|
// adopters customize {{BRANCHES}} via .pipeline-config.yml's
|
|
787
804
|
// ci.trigger_branches setting via setup-ai-pipeline.sh substitution).
|
|
@@ -841,6 +858,12 @@ program
|
|
|
841
858
|
process.stdout.write(` ⚠ ${key} — ${result.reason?.message || 'failed'}\n`);
|
|
842
859
|
}
|
|
843
860
|
});
|
|
861
|
+
// HC-GF-337g: read both facts NOW — what was received (the file generate_config records from),
|
|
862
|
+
// and whether an earlier setup left a reviewer — and report them after the script has run (or,
|
|
863
|
+
// with --dry-run, as what it would do). No adopter file is deleted.
|
|
864
|
+
const reviewerReceived = fs.existsSync(path.join(scriptsEntDir, 'ai-reviewer.js'));
|
|
865
|
+
const reviewerAlreadyPresent = !installReviewer &&
|
|
866
|
+
fs.existsSync(path.join(process.cwd(), '.github', 'scripts', 'ai-reviewer.js'));
|
|
844
867
|
|
|
845
868
|
// 6. Download setup-ai-pipeline.sh, verify HMAC, write to tmp
|
|
846
869
|
let scriptContent;
|
|
@@ -957,6 +980,22 @@ program
|
|
|
957
980
|
process.exit(1);
|
|
958
981
|
}
|
|
959
982
|
|
|
983
|
+
// HC-GF-337g: after the script, so "installed" is never claimed before the installer ran. A dry
|
|
984
|
+
// run reports what it would do; a real run reports what is now in the repository — including the
|
|
985
|
+
// workflow, which an earlier setup may have left without the job that skips without a key.
|
|
986
|
+
const reviewerWorkflowPath = path.join(process.cwd(), '.github', 'workflows', 'ai-review.yml');
|
|
987
|
+
console.log(reviewerNotice(
|
|
988
|
+
opts.dryRun ? reviewerReceived
|
|
989
|
+
: reviewerReceived && fs.existsSync(path.join(process.cwd(), '.github', 'scripts', 'ai-reviewer.js')),
|
|
990
|
+
{
|
|
991
|
+
requested: installReviewer, alreadyPresent: reviewerAlreadyPresent, dryRun: !!opts.dryRun,
|
|
992
|
+
// A dry run writes nothing, so a missing workflow is "absent" only if its template did not arrive.
|
|
993
|
+
workflow: opts.dryRun && !fs.existsSync(reviewerWorkflowPath) && fs.existsSync(path.join(workflowsDir, 'ai-review.yml'))
|
|
994
|
+
? 'off'
|
|
995
|
+
: workflowReviewerState(fs.existsSync(reviewerWorkflowPath) ? fs.readFileSync(reviewerWorkflowPath, 'utf8') : null),
|
|
996
|
+
},
|
|
997
|
+
));
|
|
998
|
+
|
|
960
999
|
// HC-GF-320: every phase below writes (CLAUDE.md, config grounding, test
|
|
961
1000
|
// packages, GitHub branch protection). A dry run stops here and says what
|
|
962
1001
|
// they would do.
|
|
@@ -1129,17 +1168,51 @@ program
|
|
|
1129
1168
|
opts.branchProtection === false ||
|
|
1130
1169
|
/^(1|true|yes)$/i.test(process.env.HONE_SKIP_BRANCH_PROTECTION || '');
|
|
1131
1170
|
|
|
1171
|
+
// HC-GF-348: require only what this repository's workflows report, under the names read from them.
|
|
1172
|
+
// HC-GF-354: the served secret scan too, and only checks whose workflow is sure to run on pull requests
|
|
1173
|
+
// to the protected branch — which only `gh repo view` names, so the list is a function of the branch.
|
|
1174
|
+
// HC-GF-352: plus `ci.required_checks`, under the same two rules. Every name the workflows report is
|
|
1175
|
+
// passed too, so the legacy `ai-code-review` an earlier setup required is removed only when nothing
|
|
1176
|
+
// in the repository reports it. HC-GF-353: the review count is decided inside, from the writers.
|
|
1132
1177
|
const { installBranchProtection } = require('./lib/branch-protection');
|
|
1178
|
+
const { requiredChecksPlan, reportedChecksInDir } = require('./lib/required-checks');
|
|
1179
|
+
const protWorkflowsDir = path.join(process.cwd(), '.github', 'workflows');
|
|
1180
|
+
const protReviewerYml = path.join(protWorkflowsDir, 'ai-review.yml');
|
|
1181
|
+
const protReviewerText = fs.existsSync(protReviewerYml) ? fs.readFileSync(protReviewerYml, 'utf8') : null;
|
|
1182
|
+
const protConfigYml = path.join(process.cwd(), '.github', '.pipeline-config.yml');
|
|
1183
|
+
// Unreadable config: no configured checks, rather than a failed setup after everything was written.
|
|
1184
|
+
let protConfigText = null;
|
|
1185
|
+
try { protConfigText = fs.existsSync(protConfigYml) ? fs.readFileSync(protConfigYml, 'utf8') : null; } catch { protConfigText = null; }
|
|
1186
|
+
let protPlan = null;
|
|
1133
1187
|
const protResult = await installBranchProtection({
|
|
1134
1188
|
log: console.log,
|
|
1135
1189
|
opts: {
|
|
1136
1190
|
skip: skipBranchProtection,
|
|
1137
|
-
requiredChecks:
|
|
1191
|
+
requiredChecks: (branch) => {
|
|
1192
|
+
const plan = requiredChecksPlan({
|
|
1193
|
+
reviewerState: workflowReviewerState(protReviewerText),
|
|
1194
|
+
reviewerWorkflowText: protReviewerText,
|
|
1195
|
+
workflowsDir: protWorkflowsDir,
|
|
1196
|
+
configText: protConfigText,
|
|
1197
|
+
branch,
|
|
1198
|
+
});
|
|
1199
|
+
if (branch != null) protPlan = plan;
|
|
1200
|
+
return plan.required;
|
|
1201
|
+
},
|
|
1202
|
+
reportedChecks: reportedChecksInDir(protWorkflowsDir),
|
|
1138
1203
|
},
|
|
1139
1204
|
});
|
|
1140
1205
|
console.log('');
|
|
1141
1206
|
if (protResult.status === 'installed') {
|
|
1142
1207
|
console.log(` ✓ Branch protection installed on '${protResult.branch}'`);
|
|
1208
|
+
const req = protResult.requiredChecks || [];
|
|
1209
|
+
console.log(req.length
|
|
1210
|
+
? ` Required status checks: ${req.join(', ')}`
|
|
1211
|
+
: ' No status check is required.');
|
|
1212
|
+
console.log(` Required approving reviews: ${protResult.reviewCount} (${protResult.reviewReason})`);
|
|
1213
|
+
for (const d of (protPlan && protPlan.dropped) || []) {
|
|
1214
|
+
console.log(` Not required: ${d.name} — ${d.reason}`);
|
|
1215
|
+
}
|
|
1143
1216
|
} else if (protResult.status === 'skipped') {
|
|
1144
1217
|
console.log(` ⚠ Branch protection skipped: ${protResult.reason}`);
|
|
1145
1218
|
if (protResult.manualCommand) {
|
|
@@ -5823,13 +5896,12 @@ program
|
|
|
5823
5896
|
},
|
|
5824
5897
|
timeout: 120000,
|
|
5825
5898
|
});
|
|
5826
|
-
const text = data.content?.[0]?.text;
|
|
5827
5899
|
// An empty answer is a provider failure the runner records as an error, not a scenario
|
|
5828
|
-
// that failed its assertions.
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
return text;
|
|
5900
|
+
// that failed its assertions. HC-GF-337f: read the first TEXT block, not block 0, so a response
|
|
5901
|
+
// that opens with a thinking block is not an empty one; a refusal names its category.
|
|
5902
|
+
const r = require('./lib/anthropic-response').readAnthropicText(data, { label: 'claude-haiku', allowPartial: true }); // a cut-off answer fails its keywords
|
|
5903
|
+
if (!r.ok) throw new Error(r.message);
|
|
5904
|
+
return r.text;
|
|
5833
5905
|
};
|
|
5834
5906
|
} else {
|
|
5835
5907
|
console.error(`✗ Invalid --provider: ${opts.provider}. Use 'claude-haiku'.`);
|
|
@@ -5966,6 +6038,8 @@ program
|
|
|
5966
6038
|
if (!opts.file && !opts.fromTree) { console.error('Provide the output via --file <path> (the text your editor produced), or --from-tree to render it from the working tree.'); process.exit(1); }
|
|
5967
6039
|
const fs = require('fs');
|
|
5968
6040
|
let output;
|
|
6041
|
+
let reviewDiff = null;
|
|
6042
|
+
let warned = false;
|
|
5969
6043
|
if (opts.fromTree) {
|
|
5970
6044
|
const { renderFromTree } = require('./lib/tree-artifact');
|
|
5971
6045
|
const rendered = renderFromTree({ repoRoot: process.cwd(), testReport: opts.testReport });
|
|
@@ -5974,15 +6048,32 @@ program
|
|
|
5974
6048
|
// artifact does not is a decision the operator has to be able to see.
|
|
5975
6049
|
for (const s2 of rendered.skipped) console.error(`not included: ${s2.path} — ${s2.reason}`);
|
|
5976
6050
|
console.error(`from-tree: ${rendered.files.length} file(s), ${rendered.deletes.length} deletion(s), ${rendered.skipped.length} skipped`);
|
|
6051
|
+
// HC-GF-390: for a build, the base diff of each file emitted whole whose original the reviewers saw only in
|
|
6052
|
+
// windows. Sent beside the artifact, stored apart from it, and never a reason this submit fails.
|
|
6053
|
+
if (stepKey === 'step_4') {
|
|
6054
|
+
try {
|
|
6055
|
+
const { computeReviewDiff } = require('./lib/review-diff');
|
|
6056
|
+
reviewDiff = computeReviewDiff({ repoRoot: process.cwd(), rendered,
|
|
6057
|
+
warn: (line) => { warned = true; console.error(line); } });
|
|
6058
|
+
} catch (e) { reviewDiff = null; warned = true; console.error(`review diff not sent: ${e.message}`); }
|
|
6059
|
+
if (reviewDiff) {
|
|
6060
|
+
console.error(`review diff: ${reviewDiff.stats.files} file(s), ${reviewDiff.notes.length} note(s), against ${reviewDiff.headSha.slice(0, 12)}`);
|
|
6061
|
+
for (const n of reviewDiff.notes) console.error(` review diff note: ${n}`);
|
|
6062
|
+
} else {
|
|
6063
|
+
console.error(warned ? 'review diff: none (see above)' : 'review diff: none (no modified file over the bundle cap)');
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
5977
6066
|
} else {
|
|
5978
6067
|
output = fs.readFileSync(opts.file, 'utf8');
|
|
5979
6068
|
}
|
|
5980
6069
|
const { data } = await client.post(
|
|
5981
6070
|
orchestratePath([runId, 'step', stepKey, 'editor-output']),
|
|
5982
|
-
{ contractVersion: opts.contract, output
|
|
6071
|
+
{ contractVersion: opts.contract, output,
|
|
6072
|
+
...(reviewDiff ? { reviewDiff: require('./lib/review-diff').toWire(reviewDiff) } : {}) }
|
|
5983
6073
|
);
|
|
5984
6074
|
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
5985
6075
|
console.log(`Step ${data.stepKey} completed (executed_by: ${data.executedBy}); gate: ${data.gateResult}.`);
|
|
6076
|
+
if (data.reviewDiffIgnored) console.error(`review diff not stored: ${data.reviewDiffIgnored} (the reviewers see the files unverified, as before)`);
|
|
5986
6077
|
console.log(`Approve when reviewed: hone run-story ${runId} --approve ${data.stepKey}`);
|
|
5987
6078
|
console.log('Verification is unchanged — check-patch/verify-patch judge this output, not trust.');
|
|
5988
6079
|
} catch (e) {
|
|
@@ -6035,10 +6126,6 @@ function runContextIo(apiConfig) {
|
|
|
6035
6126
|
e2eSpecMode: () => pc.readE2eSpecMode(cwd),
|
|
6036
6127
|
verificationGate: () => pc.readVerificationGateConfig(cwd),
|
|
6037
6128
|
verificationGateSources: () => pc.readVerificationGateSources(cwd),
|
|
6038
|
-
readPlanText: () => {
|
|
6039
|
-
const planPath = path.join(cwd, '.github', 'EXECUTION_PLAN.yml');
|
|
6040
|
-
return fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf8') : '';
|
|
6041
|
-
},
|
|
6042
6129
|
exists: (rel) => { const abs = inRepo(rel); return !!abs && fs.existsSync(abs); },
|
|
6043
6130
|
isDirectory: (rel) => { const abs = inRepo(rel); try { return !!abs && fs.statSync(abs).isDirectory(); } catch { return false; } },
|
|
6044
6131
|
readFile: (rel) => {
|
|
@@ -6128,6 +6215,7 @@ program
|
|
|
6128
6215
|
.option('--include-paths <paths>', 'Source file paths to bundle into the codebase context (HC-019n-followup-12). Comma-separated, and the flag may be repeated; both forms accumulate. Augments auto-detected paths from the issue body.', collectIncludePaths, [])
|
|
6129
6216
|
.option('--e2e-specs', 'HC-019n-followup-28: force step_3b (Playwright spec generation) ON for this run (overrides e2e.spec_generation config)')
|
|
6130
6217
|
.option('--skip-e2e-specs', 'HC-019n-followup-28: force step_3b (Playwright spec generation) OFF for this run (overrides e2e.spec_generation config)')
|
|
6218
|
+
.option('--consult-architect', 'HC-GF-397: run the architect consult (step_0b) even when the groomer says architect_engaged: false. It can only add the consult, never skip it.')
|
|
6131
6219
|
.action(async (storyIdOrRunId, opts) => {
|
|
6132
6220
|
const config = getConfig();
|
|
6133
6221
|
const client = api(config);
|
|
@@ -6606,6 +6694,7 @@ program
|
|
|
6606
6694
|
explicitSource: opts.storyFile ? { kind: 'file', label: path.basename(String(opts.storyFile)) } : { kind: 'description' },
|
|
6607
6695
|
includePaths: opts.includePaths,
|
|
6608
6696
|
axes: opts.axes,
|
|
6697
|
+
consultArchitect: opts.consultArchitect,
|
|
6609
6698
|
});
|
|
6610
6699
|
printRunContextNotes(storyCtx.notes.pre);
|
|
6611
6700
|
printRunContextNotes(repoCtx.notes);
|
|
@@ -7266,7 +7355,7 @@ program
|
|
|
7266
7355
|
}
|
|
7267
7356
|
|
|
7268
7357
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
7269
|
-
const providerLabel = 'Anthropic Opus (claude-opus-
|
|
7358
|
+
const providerLabel = 'Anthropic Opus (claude-opus-5-5)';
|
|
7270
7359
|
if (!apiKey) {
|
|
7271
7360
|
console.error('ANTHROPIC_API_KEY not set. Required for --provider opus.');
|
|
7272
7361
|
console.error('Set it: export ANTHROPIC_API_KEY=sk-ant-...');
|
|
@@ -7376,7 +7465,7 @@ program
|
|
|
7376
7465
|
normalizeCacheFlag,
|
|
7377
7466
|
} = require('./lib/release-review-cache');
|
|
7378
7467
|
const cacheEnabled = normalizeCacheFlag(opts.cache);
|
|
7379
|
-
const modelForCache = 'claude-opus-
|
|
7468
|
+
const modelForCache = 'claude-opus-5-5';
|
|
7380
7469
|
let cacheContentHash = null;
|
|
7381
7470
|
if (cacheEnabled) {
|
|
7382
7471
|
try {
|
|
@@ -7439,15 +7528,23 @@ program
|
|
|
7439
7528
|
}
|
|
7440
7529
|
|
|
7441
7530
|
// 6. Call LLM (provider-branched, HC-080a-spike)
|
|
7442
|
-
//
|
|
7443
|
-
//
|
|
7444
|
-
|
|
7531
|
+
// HC-GF-337f: 16000, not the 4096 the HC-080a-spike comparison with GPT-4.1 held constant (that comparison
|
|
7532
|
+
// ended when Opus became the only provider). Claude Opus 5.5 always thinks, and thinking tokens share
|
|
7533
|
+
// max_tokens, so a small cap can be spent before any text is written. A 16000-token answer can take
|
|
7534
|
+
// longer than 120 s, so the request timeout is 300 s.
|
|
7535
|
+
const MAX_OUTPUT_TOKENS = 16000;
|
|
7445
7536
|
const startedAt = Date.now();
|
|
7446
7537
|
try {
|
|
7447
7538
|
let responseText, inputTokens, outputTokens, modelLabel;
|
|
7448
7539
|
|
|
7540
|
+
// Thinking and effort are two SIBLING fields: `thinking: {type: 'adaptive'}` and
|
|
7541
|
+
// `output_config: {effort: 'medium'}` (medium is this model's default; it is sent so the choice is
|
|
7542
|
+
// visible). Thinking cannot be disabled on this model (HTTP 400), and no sampling parameter or
|
|
7543
|
+
// forced tool_choice is sent: both are a 400 here as well.
|
|
7449
7544
|
const { data } = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
7450
|
-
model: 'claude-opus-
|
|
7545
|
+
model: 'claude-opus-5-5',
|
|
7546
|
+
thinking: { type: 'adaptive' },
|
|
7547
|
+
output_config: { effort: 'medium' },
|
|
7451
7548
|
max_tokens: MAX_OUTPUT_TOKENS,
|
|
7452
7549
|
system: systemPrompt,
|
|
7453
7550
|
messages: [{ role: 'user', content: userPrompt }],
|
|
@@ -7457,12 +7554,15 @@ program
|
|
|
7457
7554
|
'anthropic-version': '2023-06-01',
|
|
7458
7555
|
'content-type': 'application/json',
|
|
7459
7556
|
},
|
|
7460
|
-
timeout:
|
|
7557
|
+
timeout: 300000,
|
|
7461
7558
|
});
|
|
7462
|
-
|
|
7559
|
+
// The first TEXT block, never block 0: a response that opens with a thinking block is not an empty
|
|
7560
|
+
// one (the HC-GF-085 defect on this side). The classification is kept for the failure message.
|
|
7561
|
+
const read = require('./lib/anthropic-response').readAnthropicText(data, { label: `--provider ${opts.provider}` });
|
|
7562
|
+
responseText = read.ok ? read.text : '';
|
|
7463
7563
|
inputTokens = data.usage?.input_tokens || 0;
|
|
7464
7564
|
outputTokens = data.usage?.output_tokens || 0;
|
|
7465
|
-
modelLabel = 'claude-opus-
|
|
7565
|
+
modelLabel = 'claude-opus-5-5';
|
|
7466
7566
|
|
|
7467
7567
|
const elapsedMs = Date.now() - startedAt;
|
|
7468
7568
|
|
|
@@ -7470,16 +7570,22 @@ program
|
|
|
7470
7570
|
// choices/content (content-filter trip, soft quota limit, model alias
|
|
7471
7571
|
// typo). Without this guard the CLI would silently exit 0 and the CI
|
|
7472
7572
|
// artifact would look like a clean pass — a false-negative on the
|
|
7473
|
-
// entire review.
|
|
7573
|
+
// entire review. HC-GF-337f: the cause is the classified one (a refusal with its category, a
|
|
7574
|
+
// cut-off at max_tokens, or no text block), not a guess.
|
|
7474
7575
|
if (!responseText || responseText.trim().length === 0) {
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7576
|
+
// A review cut off at max_tokens is NOT a review: exit 0 here means "deploy", and a cut-off JSON
|
|
7577
|
+
// review can pass the loose CRITICAL / DO_NOT_DEPLOY match. It gets its own status. `kind` and
|
|
7578
|
+
// `category` are additive, so a refusal can be told from an empty response in the CI artifact.
|
|
7579
|
+
const truncated = read.kind === 'truncated';
|
|
7580
|
+
console.error(`${truncated ? 'Incomplete' : 'Empty'} response from --provider ${opts.provider}. ${read.message}`);
|
|
7581
|
+
emitStatusEnvelope(truncated ? 'truncated' : 'empty_response', {
|
|
7478
7582
|
resolvedBase: baseRef,
|
|
7479
7583
|
model: modelLabel,
|
|
7480
7584
|
inputTokens,
|
|
7481
7585
|
outputTokens,
|
|
7482
7586
|
elapsedMs: Date.now() - startedAt,
|
|
7587
|
+
kind: read.kind || null,
|
|
7588
|
+
category: read.category ?? null,
|
|
7483
7589
|
});
|
|
7484
7590
|
process.exit(1);
|
|
7485
7591
|
}
|
|
@@ -8358,11 +8464,16 @@ program
|
|
|
8358
8464
|
// The server cannot execute, so a batch queued at night reached step_4 and waited for a person to type
|
|
8359
8465
|
// check-patch, verify-patch and emit-pr. This is the I/O around cli/lib/batch-follow.js, which makes every
|
|
8360
8466
|
// decision. The verbs run as children of THIS CLI, one at a time, so the loop runs exactly what a person runs.
|
|
8467
|
+
// HC-GF-363: a step_4 attempt whose verify-patch verdict is a measured `fail` under verification.gate: enforce is sent back
|
|
8468
|
+
// to the builder with the failing tests as the reason (POST /orchestrate/:runId/reject), at most twice per run, counted from
|
|
8469
|
+
// the server's own rows; the third failure is named for a person.
|
|
8361
8470
|
// Exit: 0 the batch completed, 1 fatal (refused, unknown batch, unreachable), 2 something needs you (HC-GF-351: or every
|
|
8362
8471
|
// unfinished run is waiting on a person, named with its step and why), 3 deadline.
|
|
8363
8472
|
program
|
|
8364
8473
|
.command('follow-batch <batchId>')
|
|
8365
|
-
|
|
8474
|
+
// Short text for the top-level `hone --help` (HC-GF-363): the full text below is shown by `hone follow-batch --help`.
|
|
8475
|
+
.summary('Follow a queued batch: check and verify each step_4 patch, and emit each completed run (HC-GF-098). A run hard-paused for files it was not given is re-served once with them (HC-GF-351); every pause it cannot act on is named, and it exits 2 when only people are left to act')
|
|
8476
|
+
.description('Follow a queued batch: check and verify each step_4 patch, and emit each completed run (HC-GF-098). A run hard-paused for files it was not given is re-served once with them (HC-GF-351); every pause it cannot act on is named, and it exits 2 when only people are left to act. Under verification.gate: enforce, a step_4 attempt whose verify-patch verdict is a failed test run is rejected back to the builder with the failing tests (HC-GF-363), at most twice per run; the two rejects use step_4\'s retry budget (max_retries_per_step), and a run refused before you upgraded is rejected on the first poll')
|
|
8366
8477
|
.option('--poll-interval <sec>', 'Seconds between polls', '30')
|
|
8367
8478
|
.option('--deadline <min>', 'Stop after this many minutes, even if the batch is still running')
|
|
8368
8479
|
.option('--max-offline <min>', 'Give up once the server has been unreachable this long', '15')
|
|
@@ -8444,6 +8555,16 @@ program
|
|
|
8444
8555
|
if (!require('./lib/run-target').isRunId(runId)) throw new Error('not a run id');
|
|
8445
8556
|
await client.post(orchestratePath([runId, 'kill']), { reason });
|
|
8446
8557
|
},
|
|
8558
|
+
// HC-GF-363: send a step_4 attempt that failed verification under enforce back to the builder. The loop re-reads
|
|
8559
|
+
// the run and counts its earlier rejects from `fetchStep` (an attempt's stored reject reason) before it calls this.
|
|
8560
|
+
rejectRun: async (runId, body) => {
|
|
8561
|
+
if (!require('./lib/run-target').isRunId(runId)) throw new Error('not a run id');
|
|
8562
|
+
return (await client.post(orchestratePath([runId, 'reject']), body)).data;
|
|
8563
|
+
},
|
|
8564
|
+
fetchStep: async (runId, stepKey, attempt) => {
|
|
8565
|
+
if (!require('./lib/run-target').isRunId(runId)) throw new Error('not a run id');
|
|
8566
|
+
return (await client.get(orchestratePath([runId, 'step', stepKey], { attempt }))).data;
|
|
8567
|
+
},
|
|
8447
8568
|
// The consent and provenance rule run-story applies before it runs a patch (HC-GF-064, HC-GF-270).
|
|
8448
8569
|
verifyDecision: (run) => {
|
|
8449
8570
|
let assessment;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* HC-GF-337f — read an Anthropic Messages response without losing half of it.
|
|
4
|
+
*
|
|
5
|
+
* Every CLI path used to take `data.content[0].text`. Once a model can emit a thinking block
|
|
6
|
+
* FIRST, block 0 is `{type: 'thinking'}` and that expression is `undefined`: an empty review or
|
|
7
|
+
* verdict reported as a content filter or a quota trip. The HC-GF-085 defect, on the CLI side.
|
|
8
|
+
*
|
|
9
|
+
* Returns a discriminated result; the caller owns its own exit semantics (skill-eval throws inside
|
|
10
|
+
* callLLM, release-review emits a status envelope, the judge throws so the scenario is an error and
|
|
11
|
+
* not a failed criterion). Nothing here throws, so the outcomes can be asserted in one table.
|
|
12
|
+
*
|
|
13
|
+
* { ok: true, text, stopReason }
|
|
14
|
+
* { ok: false, kind: 'refusal', category, stopReason, message } // HTTP 200, stop_reason "refusal"
|
|
15
|
+
* { ok: false, kind: 'truncated', stopReason, partialText, message } // cut off at the cap; partialText is null when there is none
|
|
16
|
+
* { ok: false, kind: 'no_text', stopReason, message } // blocks, but none with text
|
|
17
|
+
*
|
|
18
|
+
* Order is part of the contract: a refusal is decided FIRST (it can arrive with a thinking block and
|
|
19
|
+
* no text, and must never read as `no_text`). Then `max_tokens`: a response cut off at the cap is
|
|
20
|
+
* `truncated` EVEN WHEN it carries text, because a cut-off answer can be well-formed and wrong (a review
|
|
21
|
+
* cut off before its verdict). A caller whose success means approval (release-review) therefore gets
|
|
22
|
+
* the safe answer by default. A caller for which a partial answer is harmless (a judge verdict that
|
|
23
|
+
* defaults to NO, a skill-eval answer that fails its keywords) opts in with `allowPartial: true` and
|
|
24
|
+
* then gets the text back as `ok`. The first non-blank text block wins otherwise.
|
|
25
|
+
*
|
|
26
|
+
* Every non-ok message names the label, `stop_reason:` and, when the API gave one, the refusal category.
|
|
27
|
+
* Messages begin `<label> returned no text (stop_reason: ...)` when there is no text at all: callers and
|
|
28
|
+
* tests match that prefix.
|
|
29
|
+
*
|
|
30
|
+
* ADOPTER SURFACE (HC-LC-001): this file ships in the published CLI. It is a pure classifier: no
|
|
31
|
+
* I/O, no local Claude binary.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} data the parsed response body
|
|
34
|
+
* @param {{label?: string, allowPartial?: boolean}} [opts] label: who is asking, so three call sites read
|
|
35
|
+
* differently in a log. allowPartial: accept text from a response cut off at max_tokens (default false).
|
|
36
|
+
*/
|
|
37
|
+
function readAnthropicText(data, { label, allowPartial = false } = {}) {
|
|
38
|
+
const who = label || 'the provider';
|
|
39
|
+
const stopReason = (data && data.stop_reason) || 'unknown';
|
|
40
|
+
const category = (data && data.stop_details && data.stop_details.category) || null;
|
|
41
|
+
const blocks = Array.isArray(data && data.content) ? data.content : [];
|
|
42
|
+
const cause = `stop_reason: ${stopReason}${category ? `, category: ${category}` : ''}`;
|
|
43
|
+
|
|
44
|
+
if (stopReason === 'refusal') {
|
|
45
|
+
return { ok: false, kind: 'refusal', category, stopReason, message: `${who} returned no text (${cause}): the model declined the request` };
|
|
46
|
+
}
|
|
47
|
+
const block = blocks.find((b) => b && b.type === 'text' && typeof b.text === 'string' && b.text.trim().length > 0);
|
|
48
|
+
if (stopReason === 'max_tokens' && !(block && allowPartial)) {
|
|
49
|
+
const message = block
|
|
50
|
+
? `${who} returned an incomplete answer (${cause}): it was cut off at max_tokens and may be missing its ending; raise max_tokens`
|
|
51
|
+
: `${who} returned no text (${cause}): the answer was cut off at max_tokens before any text, thinking included; raise max_tokens`;
|
|
52
|
+
return { ok: false, kind: 'truncated', stopReason, partialText: block ? block.text : null, message };
|
|
53
|
+
}
|
|
54
|
+
if (block) return { ok: true, text: block.text, stopReason };
|
|
55
|
+
return { ok: false, kind: 'no_text', stopReason, message: `${who} returned no text (${cause})` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { readAnthropicText };
|
package/lib/architect-config.js
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Pure helper — caller wraps fs.readFileSync + path resolution so the
|
|
22
22
|
* helper is unit-testable with stubs.
|
|
23
|
+
*
|
|
24
|
+
* HC-GF-030: NO PRODUCTION CODE CALLS THIS ANY MORE. `architect_consulted` and `checklist_b_completed`
|
|
25
|
+
* are server-owned (workflow-store.js SERVER_OWNED_CONFIG_KEYS; run-context.js SERVER_OWNED_KEYS): the
|
|
26
|
+
* server writes them when its step_0b consult is approved, and a plan file's claim is not evidence that
|
|
27
|
+
* a consult ran on THIS story. The module stays as the documented reader of the plan's shape.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
const path = require('node:path');
|