@hone-ai/cli 1.32.0 → 1.34.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 +149 -70
- package/lib/auto-verify.js +21 -1
- package/lib/emit-pr.js +1 -0
- package/lib/headroom.js +63 -0
- package/lib/materialize-diff.js +157 -20
- package/lib/mcp-tools.js +64 -18
- package/lib/poll-decision.js +16 -1
- package/lib/run-report.js +147 -6
- package/lib/run-target.js +97 -0
- package/lib/tree-artifact.js +166 -0
- package/lib/verify-patch.js +88 -3
- package/package.json +1 -1
package/hone-cli.js
CHANGED
|
@@ -5522,11 +5522,16 @@ program
|
|
|
5522
5522
|
const m = Math.floor((Date.now() - new Date(ts).getTime()) / 60000);
|
|
5523
5523
|
return m < 60 ? `${m}m` : m < 1440 ? `${Math.floor(m / 60)}h` : `${Math.floor(m / 1440)}d`;
|
|
5524
5524
|
};
|
|
5525
|
-
|
|
5525
|
+
// A story ID cut to fit is marked as cut: pasted into `run-story <storyId>`, a bare prefix could name another story.
|
|
5526
|
+
// ASCII on purpose: U+2026 is ambiguous-width, and a terminal that draws it double-width shifts the row.
|
|
5527
|
+
const clip = (s, w) => (s.length > w ? `${s.slice(0, w - 3)}...` : s);
|
|
5528
|
+
// HC-GF-108: the FULL run ID. The footer says `hone run-story <runId> --kill`, and an 8-character
|
|
5529
|
+
// prefix is not a run ID; nor is one accepted as a target, since `12345678` is also a valid story ID.
|
|
5530
|
+
console.log(['RUN ID'.padEnd(36), 'STORY'.padEnd(22), 'STATUS'.padEnd(10), 'STEP'.padEnd(10), 'AGE'].join(' '));
|
|
5526
5531
|
for (const r of data.runs) {
|
|
5527
5532
|
console.log([
|
|
5528
|
-
String(r.id).
|
|
5529
|
-
String(r.story_id || '-')
|
|
5533
|
+
String(r.id).padEnd(36),
|
|
5534
|
+
clip(String(r.story_id || '-'), 21).padEnd(22),
|
|
5530
5535
|
String(r.status).padEnd(10),
|
|
5531
5536
|
String(r.current_step || '-').padEnd(10),
|
|
5532
5537
|
age(r.created_at),
|
|
@@ -5547,7 +5552,9 @@ program
|
|
|
5547
5552
|
const config = getConfig();
|
|
5548
5553
|
const client = api(config);
|
|
5549
5554
|
try {
|
|
5550
|
-
|
|
5555
|
+
// HC-GF-092: advertise what this CLI can honour. The server serves the highest it also
|
|
5556
|
+
// knows, so an older CLI — which sends nothing — keeps being served contract 1.
|
|
5557
|
+
const { data } = await client.get(`/orchestrate/${encodeURIComponent(runId)}/step-instruction?contracts=1,2`);
|
|
5551
5558
|
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
5552
5559
|
console.log(`Step: ${data.stepKey} (${data.agent})`);
|
|
5553
5560
|
console.log(`Contract: ${data.contractVersion}`);
|
|
@@ -5568,15 +5575,36 @@ program
|
|
|
5568
5575
|
.command('submit-step <runId> <stepKey>')
|
|
5569
5576
|
.description('Submit editor-executed output for an awaiting_editor step (HC-COMM-010 B3)')
|
|
5570
5577
|
.option('--file <path>', 'Read the step output from this file')
|
|
5578
|
+
.option('--from-tree', 'HC-GF-091: render the artifact from the working tree against HEAD instead of reading one file — the agent writes files with its own tools, so no single model response has to hold the whole build')
|
|
5579
|
+
.option('--test-report <text>', 'With --from-tree: the test-run report to carry verbatim. Never invented when absent.')
|
|
5571
5580
|
.option('--contract <version>', 'Instruction contract version (from step-instruction)', '1')
|
|
5572
5581
|
.option('--format <fmt>', 'Output format: pretty | json', 'pretty')
|
|
5573
5582
|
.action(async (runId, stepKey, opts) => {
|
|
5574
5583
|
const config = getConfig();
|
|
5575
5584
|
const client = api(config);
|
|
5576
5585
|
try {
|
|
5577
|
-
if (
|
|
5586
|
+
if (opts.file && opts.fromTree) { console.error('Use --file OR --from-tree, not both: they are two different claims about where the build is.'); process.exit(1); }
|
|
5587
|
+
// HC-GF-092: under contract 2 the instruction told the agent to WRITE the files with its own
|
|
5588
|
+
// tools. A hand-assembled --file artifact means the instruction and the submission disagree
|
|
5589
|
+
// about where the build is, and the server cannot see that — only this side can.
|
|
5590
|
+
if (String(opts.contract) === '2' && !opts.fromTree) {
|
|
5591
|
+
console.error('Contract 2 is the from-tree contract: the instruction told the agent to write the files with its own tools. Re-run with --from-tree, or fetch a contract 1 instruction if you really did assemble the output by hand.');
|
|
5592
|
+
process.exit(1);
|
|
5593
|
+
}
|
|
5594
|
+
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); }
|
|
5578
5595
|
const fs = require('fs');
|
|
5579
|
-
|
|
5596
|
+
let output;
|
|
5597
|
+
if (opts.fromTree) {
|
|
5598
|
+
const { renderFromTree } = require('./lib/tree-artifact');
|
|
5599
|
+
const rendered = renderFromTree({ repoRoot: process.cwd(), testReport: opts.testReport });
|
|
5600
|
+
output = rendered.text;
|
|
5601
|
+
// Say what was left out BEFORE the submission, not after: a file the tree has and the
|
|
5602
|
+
// artifact does not is a decision the operator has to be able to see.
|
|
5603
|
+
for (const s2 of rendered.skipped) console.error(`not included: ${s2.path} — ${s2.reason}`);
|
|
5604
|
+
console.error(`from-tree: ${rendered.files.length} file(s), ${rendered.deletes.length} deletion(s), ${rendered.skipped.length} skipped`);
|
|
5605
|
+
} else {
|
|
5606
|
+
output = fs.readFileSync(opts.file, 'utf8');
|
|
5607
|
+
}
|
|
5580
5608
|
const { data } = await client.post(
|
|
5581
5609
|
`/orchestrate/${encodeURIComponent(runId)}/step/${encodeURIComponent(stepKey)}/editor-output`,
|
|
5582
5610
|
{ contractVersion: opts.contract, output }
|
|
@@ -5598,17 +5626,18 @@ program
|
|
|
5598
5626
|
.command('run-story <storyId>')
|
|
5599
5627
|
.description('Run the SDLC pipeline for a story via the orchestrator')
|
|
5600
5628
|
.option('--mode <mode>', 'Execution mode: interactive or batch', 'interactive')
|
|
5601
|
-
.option('--repo <name>', 'Repository name override (default: directory name)')
|
|
5629
|
+
.option('--repo <name>', 'Repository name override (default: directory name). Also the repository a story ID is looked up in for --status, --kill and --approve')
|
|
5602
5630
|
.option('--branch <name>', 'Git branch (default: current)')
|
|
5603
|
-
.option('--status', 'Show status of
|
|
5604
|
-
.option('--kill', 'Kill
|
|
5631
|
+
.option('--status', 'Show status of a run: pass a run ID, or a story ID for that story\'s latest run in this repository')
|
|
5632
|
+
.option('--kill', 'Kill a run: pass a run ID, or a story ID for that story\'s latest run in this repository')
|
|
5605
5633
|
.option('--axes <axes>', 'Comma-separated classifier axes for this run (e.g. shared-chokepoint) — composition input (HC-COMM-010 C0)')
|
|
5606
|
-
.option('--approve <step>', 'Approve a paused gate (e.g., --approve step_0). Exit codes: 2 = paused at a human gate, 3 = parked for your editor (execution.mode: editor)')
|
|
5634
|
+
.option('--approve <step>', 'Approve a paused gate (e.g., --approve step_0) on a run ID, or on a story ID\'s latest run in this repository. Exit codes: 2 = paused at a human gate, 3 = parked for your editor (execution.mode: editor)')
|
|
5607
5635
|
.option('--no-verify', 'HC-GF-064: do not run verify-patch automatically when the run pauses at step_4')
|
|
5608
5636
|
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
5609
5637
|
.option('--poll-interval <s>', 'Poll interval in seconds', '5')
|
|
5610
5638
|
.option('--story-file <path>', 'HC-GF-010: read the story text from a local file. Skips the GitHub-issue and Hone-roadmap lookups entirely, so a repo with neither can still run the pipeline.')
|
|
5611
5639
|
.option('--description <text>', 'HC-GF-010: the story text, inline. Same effect as --story-file.')
|
|
5640
|
+
.option('--acknowledge-failed-verdict', 'Approve a step whose verification did NOT pass under verification.gate: enforce (HC-GF-074). The server refuses that approval by default and names the failing gates; this is for a human who has read them and still wants to proceed.')
|
|
5612
5641
|
.option('--acknowledge-abort', 'Approve a step the agent ABORTED (HC-GF-069). An abort means the agent found nothing it could review or build, so approving advances past a step that produced no usable result — read the reason first. Exists so a human can override, which is why the refusal is not simply a hard block.')
|
|
5613
5642
|
.option('--no-follow', 'With --approve: post the approval and exit immediately instead of following the run to its next decision point (HC-GF-070). The default FOLLOWS, because a caller told to poll does not — which is why HC-GF-064 auto-verify never fired outside a TTY.')
|
|
5614
5643
|
.option('--include-paths <paths>', 'Comma-separated list of source file paths to bundle into the codebase context (HC-019n-followup-12). Augments auto-detected paths from the issue body.')
|
|
@@ -5635,11 +5664,25 @@ program
|
|
|
5635
5664
|
}
|
|
5636
5665
|
} catch { /* never block a run on a banner */ }
|
|
5637
5666
|
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
const
|
|
5641
|
-
|
|
5642
|
-
|
|
5667
|
+
const { resolveRunTarget, isRunId } = require('./lib/run-target');
|
|
5668
|
+
const isUuid = isRunId(storyIdOrRunId);
|
|
5669
|
+
const repoName = opts.repo || path.basename(process.cwd());
|
|
5670
|
+
|
|
5671
|
+
// HC-GF-106: --status, --kill and --approve act on an EXISTING run, and all three resolve their
|
|
5672
|
+
// argument here. Each used to forward it verbatim (`runId || storyIdOrRunId`), so a story ID was
|
|
5673
|
+
// posted as a run ID and answered `Run not found` — while --help promised the story's latest run.
|
|
5674
|
+
// A story is scoped to `repoName`, the name the start path below records, so it resolves to a run
|
|
5675
|
+
// this checkout started. Never falls back to the argument: no run found means exit 1.
|
|
5676
|
+
const existingRunId = async (flag) => {
|
|
5677
|
+
const t = await resolveRunTarget({ client, idOrStory: storyIdOrRunId, repoName, flag });
|
|
5678
|
+
if (t.error) { console.error(t.error); process.exit(1); }
|
|
5679
|
+
if (t.source === 'story') {
|
|
5680
|
+
// stderr: stdout is what a `--status --format json` caller parses, so the note stays out of it.
|
|
5681
|
+
const at = t.run.current_step ? ` at ${t.run.current_step}` : '';
|
|
5682
|
+
console.error(`→ ${storyIdOrRunId}: latest run in ${repoName} is ${t.runId} (${t.run.status}${at})`);
|
|
5683
|
+
}
|
|
5684
|
+
return t.runId;
|
|
5685
|
+
};
|
|
5643
5686
|
|
|
5644
5687
|
// HC-GF-070: ONE poll loop, reachable from BOTH entry points.
|
|
5645
5688
|
//
|
|
@@ -5688,8 +5731,20 @@ program
|
|
|
5688
5731
|
process.exit(d.exitCode);
|
|
5689
5732
|
}
|
|
5690
5733
|
|
|
5691
|
-
if (d.outcome === POLL_OUTCOMES.HUMAN_GATE) {
|
|
5692
|
-
|
|
5734
|
+
if (d.outcome === POLL_OUTCOMES.HUMAN_GATE || d.outcome === POLL_OUTCOMES.EXHAUSTED) {
|
|
5735
|
+
// HC-GF-075: an exhausted step is a decision, not a gate on an artifact. Say what approving
|
|
5736
|
+
// does, and do NOT auto-verify — the attempt on record is the one the validator rejected,
|
|
5737
|
+
// and a verdict on it would read as a verdict on the build.
|
|
5738
|
+
const exhausted = d.outcome === POLL_OUTCOMES.EXHAUSTED;
|
|
5739
|
+
if (exhausted) {
|
|
5740
|
+
console.log(`⏸ Step ${d.stepKey} (${d.agent || 'unknown'}) used up its format retries and paused.`);
|
|
5741
|
+
console.log(` ${String(d.errorMessage || '').slice(0, 240)}`);
|
|
5742
|
+
console.log(` Inspect: hone show step ${pollRunId} ${d.stepKey}`);
|
|
5743
|
+
console.log(` Approving starts another cycle of ${d.stepKey} with a fresh format budget; it does not `
|
|
5744
|
+
+ 'accept an earlier attempt. guardrails.max_retries_per_step caps the cycles.');
|
|
5745
|
+
} else {
|
|
5746
|
+
console.log(`⏳ Step ${d.stepKey} (${d.agent || 'unknown'}) — awaiting approval`);
|
|
5747
|
+
}
|
|
5693
5748
|
|
|
5694
5749
|
// HC-GF-064: verify BEFORE asking anyone to approve. The gate this feeds had
|
|
5695
5750
|
// existed since HC-019n-followup-34/35 and never fired, because nothing ever
|
|
@@ -5698,8 +5753,8 @@ program
|
|
|
5698
5753
|
// cli/lib/auto-verify.js and docs/architecture/adr-verification-invocation.md).
|
|
5699
5754
|
// Never throws — a failure here must leave the operator exactly where they
|
|
5700
5755
|
// were, which is being asked to approve.
|
|
5701
|
-
try {
|
|
5702
|
-
const { decideAutoVerify, announce, shouldRepoll, alreadyVerified } = require('./lib/auto-verify');
|
|
5756
|
+
if (!exhausted) try {
|
|
5757
|
+
const { decideAutoVerify, announce, shouldRepoll, alreadyVerified, verifyOutcomeLine } = require('./lib/auto-verify');
|
|
5703
5758
|
const decision = decideAutoVerify({
|
|
5704
5759
|
stepKey: d.stepKey,
|
|
5705
5760
|
repoRoot: process.cwd(),
|
|
@@ -5721,9 +5776,8 @@ program
|
|
|
5721
5776
|
console.log(' → verified; enforce may have advanced the run. Re-checking...');
|
|
5722
5777
|
continue;
|
|
5723
5778
|
}
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
: ' → verification did not pass; the gate still needs you.');
|
|
5779
|
+
// HC-GF-008: keyed by verify-patch's exit code — "did not pass" only when a command ran and failed.
|
|
5780
|
+
console.log(verifyOutcomeLine(r.status, decision.gate));
|
|
5727
5781
|
}
|
|
5728
5782
|
} catch (e) {
|
|
5729
5783
|
console.log(` (auto-verify skipped: ${e.message})`);
|
|
@@ -5732,7 +5786,7 @@ program
|
|
|
5732
5786
|
if (process.stdin.isTTY) {
|
|
5733
5787
|
const readline = require('readline');
|
|
5734
5788
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5735
|
-
const answer = await new Promise(resolve => rl.question(' Approve? [y/n/kill] ', resolve));
|
|
5789
|
+
const answer = await new Promise(resolve => rl.question(exhausted ? ` Retry ${d.stepKey} with a fresh format budget? [y/n/kill] ` : ' Approve? [y/n/kill] ', resolve));
|
|
5736
5790
|
rl.close();
|
|
5737
5791
|
|
|
5738
5792
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
@@ -5757,7 +5811,9 @@ program
|
|
|
5757
5811
|
} else {
|
|
5758
5812
|
// Non-interactive (CI): exit 2 (paused, needs approval) — the contract
|
|
5759
5813
|
// adopter pipelines already branch on.
|
|
5760
|
-
console.log(
|
|
5814
|
+
console.log(exhausted
|
|
5815
|
+
? ` Non-interactive mode. Retry with: hone run-story ${pollRunId} --approve ${d.stepKey} (or --kill)`
|
|
5816
|
+
: ` Non-interactive mode. Approve with: hone run-story ${pollRunId} --approve ${d.stepKey}`);
|
|
5761
5817
|
process.exit(d.exitCode);
|
|
5762
5818
|
}
|
|
5763
5819
|
continue;
|
|
@@ -5769,8 +5825,8 @@ program
|
|
|
5769
5825
|
|
|
5770
5826
|
// Status mode
|
|
5771
5827
|
if (opts.status) {
|
|
5772
|
-
const statusId = runId || storyIdOrRunId;
|
|
5773
5828
|
try {
|
|
5829
|
+
const statusId = await existingRunId('--status');
|
|
5774
5830
|
const { data } = await client.get(`/orchestrate/${statusId}`);
|
|
5775
5831
|
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
5776
5832
|
console.log('');
|
|
@@ -5819,8 +5875,8 @@ program
|
|
|
5819
5875
|
|
|
5820
5876
|
// Kill mode
|
|
5821
5877
|
if (opts.kill) {
|
|
5822
|
-
const killId = runId || storyIdOrRunId;
|
|
5823
5878
|
try {
|
|
5879
|
+
const killId = await existingRunId('--kill');
|
|
5824
5880
|
const { data } = await client.post(`/orchestrate/${killId}/kill`, { reason: 'CLI kill' });
|
|
5825
5881
|
console.log(`Workflow ${data.runId} killed.`);
|
|
5826
5882
|
} catch (e) {
|
|
@@ -5832,10 +5888,12 @@ program
|
|
|
5832
5888
|
|
|
5833
5889
|
// Approve mode
|
|
5834
5890
|
if (opts.approve) {
|
|
5835
|
-
|
|
5891
|
+
let approveId;
|
|
5836
5892
|
try {
|
|
5893
|
+
approveId = await existingRunId(`--approve ${opts.approve}`);
|
|
5837
5894
|
await client.post(`/orchestrate/${approveId}/approve`,
|
|
5838
|
-
{ stepKey: opts.approve, acknowledgeAbort: opts.acknowledgeAbort === true
|
|
5895
|
+
{ stepKey: opts.approve, acknowledgeAbort: opts.acknowledgeAbort === true,
|
|
5896
|
+
acknowledgeFailedVerdict: opts.acknowledgeFailedVerdict === true });
|
|
5839
5897
|
console.log(`Gate ${opts.approve} approved. Workflow resuming.`);
|
|
5840
5898
|
} catch (e) {
|
|
5841
5899
|
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
@@ -5871,7 +5929,6 @@ program
|
|
|
5871
5929
|
process.exit(1);
|
|
5872
5930
|
}
|
|
5873
5931
|
|
|
5874
|
-
const repoName = opts.repo || path.basename(process.cwd());
|
|
5875
5932
|
let branch = opts.branch;
|
|
5876
5933
|
if (!branch) {
|
|
5877
5934
|
try { branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8' }).trim(); } catch { branch = null; }
|
|
@@ -7509,7 +7566,7 @@ showCmd
|
|
|
7509
7566
|
// to apply — structure is not applicability.
|
|
7510
7567
|
program
|
|
7511
7568
|
.command('check-patch <workflowId>')
|
|
7512
|
-
.description('Verify step_4\'s diff applies to
|
|
7569
|
+
.description('Verify step_4\'s diff applies to HEAD — the tree verify-patch and emit-pr apply it to (condition 5)')
|
|
7513
7570
|
.option('--step-key <key>', 'Step to check (default: step_4)', 'step_4')
|
|
7514
7571
|
.option('--attempt <n>', 'Specific attempt number (default: latest)')
|
|
7515
7572
|
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
@@ -7517,7 +7574,7 @@ program
|
|
|
7517
7574
|
const { execFileSync } = require('child_process');
|
|
7518
7575
|
const os = require('os');
|
|
7519
7576
|
const { extractDiff, interpretApplyCheck } = require('./lib/patch-apply');
|
|
7520
|
-
const { extractFileBlocks, buildDiff,
|
|
7577
|
+
const { extractFileBlocks, buildDiff, headMaterializeIO, MaterializeError, hasMaterializableWork } = require('./lib/materialize-diff');
|
|
7521
7578
|
const { gitEnv } = require('./lib/git-env');
|
|
7522
7579
|
const config = getConfig();
|
|
7523
7580
|
const client = api(config);
|
|
@@ -7551,10 +7608,10 @@ program
|
|
|
7551
7608
|
`○ ${opts.stepKey} declared NO CHANGES — nothing to apply.`);
|
|
7552
7609
|
return;
|
|
7553
7610
|
}
|
|
7554
|
-
if (
|
|
7611
|
+
if (hasMaterializableWork(blocks)) {
|
|
7555
7612
|
let built;
|
|
7556
7613
|
try {
|
|
7557
|
-
built = buildDiff(blocks,
|
|
7614
|
+
built = buildDiff(blocks, headMaterializeIO({ cwd: process.cwd() }));
|
|
7558
7615
|
} catch (e) {
|
|
7559
7616
|
if (e instanceof MaterializeError) {
|
|
7560
7617
|
emit({ ok: false, verdict: 'does_not_apply', reason: e.code, detail: e.message },
|
|
@@ -7567,8 +7624,9 @@ program
|
|
|
7567
7624
|
source = 'file-blocks';
|
|
7568
7625
|
noChanges = built.partCount === 0;
|
|
7569
7626
|
if (noChanges) {
|
|
7570
|
-
|
|
7571
|
-
|
|
7627
|
+
// HC-GF-077: identical to HEAD, not to the working copy — the editor's uncommitted edits are not HEAD.
|
|
7628
|
+
emit({ ok: true, verdict: 'already_in_head', source: 'file-blocks', detail: `HEAD already contains ${opts.stepKey}'s files` },
|
|
7629
|
+
`○ HEAD already contains ${opts.stepKey}'s files — nothing to apply.`);
|
|
7572
7630
|
return;
|
|
7573
7631
|
}
|
|
7574
7632
|
} else {
|
|
@@ -7587,16 +7645,25 @@ program
|
|
|
7587
7645
|
}
|
|
7588
7646
|
}
|
|
7589
7647
|
|
|
7590
|
-
// Write to a temp file and run git apply --check against
|
|
7648
|
+
// Write to a temp file and run git apply --check against HEAD. HC-GF-077: against an index read from HEAD,
|
|
7649
|
+
// not the working copy — an editor's uncommitted edits are exactly what emit-pr's branch will NOT contain.
|
|
7591
7650
|
const tmp = path.join(os.tmpdir(), `hone-check-patch-${Date.now()}.diff`);
|
|
7592
7651
|
let result;
|
|
7593
7652
|
try {
|
|
7594
7653
|
fs.writeFileSync(tmp, diff);
|
|
7595
7654
|
let code = 0, stderr = '';
|
|
7596
7655
|
try {
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7656
|
+
const index = path.join(os.tmpdir(), `hone-check-index-${process.pid}-${Date.now()}`);
|
|
7657
|
+
try {
|
|
7658
|
+
try {
|
|
7659
|
+
execFileSync('git', ['read-tree', `--index-output=${index}`, 'HEAD'], { cwd: process.cwd(), env: gitEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
7660
|
+
} catch { // no commit yet: every path is new, so check against the empty tree
|
|
7661
|
+
execFileSync('git', ['read-tree', '--empty', `--index-output=${index}`], { cwd: process.cwd(), env: gitEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
7662
|
+
}
|
|
7663
|
+
execFileSync('git', ['apply', '--check', '--cached', tmp], {
|
|
7664
|
+
cwd: process.cwd(), env: gitEnv({ GIT_INDEX_FILE: index }), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
7665
|
+
});
|
|
7666
|
+
} finally { try { fs.unlinkSync(index); } catch { /* best-effort */ } }
|
|
7600
7667
|
} catch (e) {
|
|
7601
7668
|
code = e.status || 1;
|
|
7602
7669
|
stderr = (e.stderr || '').toString();
|
|
@@ -7612,7 +7679,7 @@ program
|
|
|
7612
7679
|
} else {
|
|
7613
7680
|
emit({ ok: false, verdict: 'failed', source, detail: result.detail },
|
|
7614
7681
|
`✗ ${opts.stepKey} diff does NOT apply: ${result.detail}\n` +
|
|
7615
|
-
` The output is a well-formed diff but does not match
|
|
7682
|
+
` The output is a well-formed diff but does not match HEAD — ` +
|
|
7616
7683
|
`exactly the defect condition 5 catches that condition 4 cannot.`);
|
|
7617
7684
|
process.exit(2); // distinct exit: 2 = does-not-apply, 1 = no-diff/fetch
|
|
7618
7685
|
}
|
|
@@ -7640,8 +7707,8 @@ program
|
|
|
7640
7707
|
.action(async (workflowId, opts) => {
|
|
7641
7708
|
const { execFileSync, spawnSync } = require('child_process');
|
|
7642
7709
|
const os = require('os');
|
|
7643
|
-
const { extractFileBlocks, buildDiff,
|
|
7644
|
-
const {
|
|
7710
|
+
const { extractFileBlocks, buildDiff, headMaterializeIO, MaterializeError, hasMaterializableWork } = require('./lib/materialize-diff');
|
|
7711
|
+
const { resolveCommandDetailed, verdictFromRun, verifyCommandEnv, EXIT, resolveAuxGates, classifyGateRun } = require('./lib/verify-patch');
|
|
7645
7712
|
const { gitEnv } = require('./lib/git-env');
|
|
7646
7713
|
const config = getConfig();
|
|
7647
7714
|
const client = api(config);
|
|
@@ -7652,12 +7719,17 @@ program
|
|
|
7652
7719
|
};
|
|
7653
7720
|
|
|
7654
7721
|
// 1. Fetch step_4 output.
|
|
7722
|
+
// HC-GF-074: the attempt this run verified — reported with the verdict below.
|
|
7723
|
+
let verifiedAttempt;
|
|
7655
7724
|
let output, runConfig;
|
|
7656
7725
|
try {
|
|
7657
7726
|
const url = `/orchestrate/${workflowId}/step/${opts.stepKey}` +
|
|
7658
7727
|
(opts.attempt ? `?attempt=${opts.attempt}` : '');
|
|
7659
7728
|
const r = await client.get(url);
|
|
7660
7729
|
output = r.data?.output;
|
|
7730
|
+
// HC-GF-074: remember WHICH attempt this is. Without --attempt the server returns the latest, and the verdict
|
|
7731
|
+
// used to be reported with `attempt: null` — so no verdict on record could say which build it judged.
|
|
7732
|
+
verifiedAttempt = r.data?.attempt != null ? Number(r.data.attempt) : undefined;
|
|
7661
7733
|
// best-effort: pull the run's ci_local_command for the default
|
|
7662
7734
|
try {
|
|
7663
7735
|
const runR = await client.get(`/orchestrate/${workflowId}`);
|
|
@@ -7673,7 +7745,7 @@ program
|
|
|
7673
7745
|
// 2. Materialize the diff (shared with check-patch/emit-pr, followup-23/27).
|
|
7674
7746
|
// Condition 6 presupposes 5.
|
|
7675
7747
|
const blocks = extractFileBlocks(output);
|
|
7676
|
-
const hasWork =
|
|
7748
|
+
const hasWork = hasMaterializableWork(blocks);
|
|
7677
7749
|
if (blocks.noChanges || !hasWork) {
|
|
7678
7750
|
emit({ ok: false, verdict: 'no_diff', reason: 'no_changes_or_diff' },
|
|
7679
7751
|
`✗ ${opts.stepKey} produced no file changes to verify.`);
|
|
@@ -7681,7 +7753,7 @@ program
|
|
|
7681
7753
|
}
|
|
7682
7754
|
let diff;
|
|
7683
7755
|
try {
|
|
7684
|
-
diff = buildDiff(blocks,
|
|
7756
|
+
diff = buildDiff(blocks, headMaterializeIO({ cwd: process.cwd() })).diff;
|
|
7685
7757
|
} catch (e) {
|
|
7686
7758
|
if (e instanceof MaterializeError) {
|
|
7687
7759
|
emit({ ok: false, verdict: 'does_not_apply', reason: e.code, detail: e.message },
|
|
@@ -7690,17 +7762,17 @@ program
|
|
|
7690
7762
|
}
|
|
7691
7763
|
throw e;
|
|
7692
7764
|
}
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
}
|
|
7765
|
+
// HC-GF-077: an empty diff against HEAD means HEAD already contains the step's files. That is nothing to
|
|
7766
|
+
// APPLY, not nothing to VERIFY — the command still runs, on HEAD as it is. The old early return exited 0
|
|
7767
|
+
// without running anything, and auto-verify called that "verified and recorded".
|
|
7768
|
+
const alreadyInHead = !diff.trim();
|
|
7769
|
+
if (alreadyInHead && opts.format !== 'json') console.log(` ○ HEAD already contains ${opts.stepKey}'s files — verifying HEAD as it is`);
|
|
7698
7770
|
|
|
7699
7771
|
// 3. Isolated worktree from HEAD.
|
|
7700
7772
|
const wt = fs.mkdtempSync(path.join(os.tmpdir(), 'hone-verify-'));
|
|
7701
7773
|
const diffFile = path.join(os.tmpdir(), `hone-verify-${Date.now()}.diff`);
|
|
7702
7774
|
fs.writeFileSync(diffFile, diff);
|
|
7703
|
-
const command =
|
|
7775
|
+
const { command, source: commandSource } = resolveCommandDetailed(opts.command, runConfig?.ci_local_command, { cwd: process.cwd() }); // followup-7 tiers; HC-GF-008: the source says whether `make ci` was a guess
|
|
7704
7776
|
let cleanupDone = false;
|
|
7705
7777
|
const cleanup = () => {
|
|
7706
7778
|
if (cleanupDone || opts.keep) return;
|
|
@@ -7729,9 +7801,9 @@ program
|
|
|
7729
7801
|
execFileSync('git', ['worktree', 'add', '--detach', wt, 'HEAD'],
|
|
7730
7802
|
{ cwd: process.cwd(), env: gitEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
7731
7803
|
|
|
7732
|
-
// 4. Apply the diff INTO the worktree (real apply, isolated).
|
|
7804
|
+
// 4. Apply the diff INTO the worktree (real apply, isolated) — unless HEAD already holds it.
|
|
7733
7805
|
let applied = true, applyDetail = '';
|
|
7734
|
-
try {
|
|
7806
|
+
if (!alreadyInHead) try {
|
|
7735
7807
|
execFileSync('git', ['-C', wt, 'apply', diffFile], { env: gitEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
7736
7808
|
} catch (e) {
|
|
7737
7809
|
applied = false;
|
|
@@ -7757,12 +7829,14 @@ program
|
|
|
7757
7829
|
if (opts.format !== 'json') console.log(` running: ${command} (in isolated worktree, timeout ${opts.timeout}s)`);
|
|
7758
7830
|
const run = spawnSync(command, {
|
|
7759
7831
|
cwd: wt, shell: true, encoding: 'utf8',
|
|
7760
|
-
timeout: Number(opts.timeout) * 1000, env: gitEnv(),
|
|
7832
|
+
timeout: Number(opts.timeout) * 1000, env: verifyCommandEnv(gitEnv()),
|
|
7761
7833
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7762
7834
|
});
|
|
7763
7835
|
const timedOut = run.error && run.error.code === 'ETIMEDOUT';
|
|
7764
|
-
const v = verdictFromRun({ code: run.status, timedOut, stdout: run.stdout, stderr: run.stderr });
|
|
7765
|
-
|
|
7836
|
+
const v = verdictFromRun({ code: run.status, timedOut, stdout: run.stdout, stderr: run.stderr, command, source: commandSource });
|
|
7837
|
+
// HC-GF-077: nothing was applied when HEAD already held the step — say what the command actually ran on.
|
|
7838
|
+
if (alreadyInHead && v.verdict === 'pass') v.detail = "tests passed on HEAD, which already contains the step's files";
|
|
7839
|
+
const sym = v.verdict === 'pass' ? '✓' : v.verdict === 'timeout' ? '⏱' : v.verdict === 'unresolved' ? '○' : '✗';
|
|
7766
7840
|
|
|
7767
7841
|
// HC-GF-023: on a failure the operator needs the OUTPUT, not a quote from it.
|
|
7768
7842
|
// The full log is written OUTSIDE the worktree (cleanup removes the worktree)
|
|
@@ -7807,7 +7881,7 @@ program
|
|
|
7807
7881
|
for (const gate of resolveAuxGates({ configured: ciCfg, cwd: process.cwd() })) {
|
|
7808
7882
|
const gr = spawnSync(gate.command, {
|
|
7809
7883
|
cwd: wt, shell: true, encoding: 'utf8',
|
|
7810
|
-
timeout: Number(opts.timeout) * 1000, env: gitEnv(),
|
|
7884
|
+
timeout: Number(opts.timeout) * 1000, env: verifyCommandEnv(gitEnv()),
|
|
7811
7885
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7812
7886
|
});
|
|
7813
7887
|
const c = classifyGateRun({
|
|
@@ -7847,7 +7921,7 @@ program
|
|
|
7847
7921
|
verdict: reportVerdict,
|
|
7848
7922
|
exitCode,
|
|
7849
7923
|
detail: reportDetail,
|
|
7850
|
-
attempt: opts.attempt ? Number(opts.attempt) :
|
|
7924
|
+
attempt: opts.attempt ? Number(opts.attempt) : verifiedAttempt,
|
|
7851
7925
|
headSha,
|
|
7852
7926
|
gates: reportGates.length ? reportGates : undefined,
|
|
7853
7927
|
});
|
|
@@ -7921,8 +7995,8 @@ program
|
|
|
7921
7995
|
.action(async (workflowId, opts) => {
|
|
7922
7996
|
const { execFileSync, spawnSync } = require('child_process');
|
|
7923
7997
|
const os = require('os');
|
|
7924
|
-
const { extractFileBlocks, buildDiff,
|
|
7925
|
-
const {
|
|
7998
|
+
const { extractFileBlocks, buildDiff, headMaterializeIO, MaterializeError, hasMaterializableWork } = require('./lib/materialize-diff');
|
|
7999
|
+
const { resolveCommandDetailed, verdictFromRun, verifyCommandEnv } = require('./lib/verify-patch');
|
|
7926
8000
|
const {
|
|
7927
8001
|
EXIT, buildBranchName, buildCommitMessage, buildPrBody, decidePublish,
|
|
7928
8002
|
} = require('./lib/emit-pr');
|
|
@@ -7962,7 +8036,7 @@ program
|
|
|
7962
8036
|
// 2. Materialize the diff (shared with check-patch/verify-patch,
|
|
7963
8037
|
// followup-23/27). Condition 7 presupposes 5.
|
|
7964
8038
|
const blocks = extractFileBlocks(output);
|
|
7965
|
-
const hasWork =
|
|
8039
|
+
const hasWork = hasMaterializableWork(blocks);
|
|
7966
8040
|
if (blocks.noChanges || !hasWork) {
|
|
7967
8041
|
emit({ ok: false, verdict: 'no_diff', reason: 'no_changes_or_diff' },
|
|
7968
8042
|
`✗ ${opts.stepKey} produced no file changes to emit.`);
|
|
@@ -7970,7 +8044,7 @@ program
|
|
|
7970
8044
|
}
|
|
7971
8045
|
let diff;
|
|
7972
8046
|
try {
|
|
7973
|
-
diff = buildDiff(blocks,
|
|
8047
|
+
diff = buildDiff(blocks, headMaterializeIO({ cwd: process.cwd() })).diff;
|
|
7974
8048
|
} catch (e) {
|
|
7975
8049
|
if (e instanceof MaterializeError) {
|
|
7976
8050
|
emit({ ok: false, verdict: 'does_not_apply', reason: e.code, detail: e.message },
|
|
@@ -7980,8 +8054,9 @@ program
|
|
|
7980
8054
|
throw e;
|
|
7981
8055
|
}
|
|
7982
8056
|
if (!diff.trim()) {
|
|
7983
|
-
|
|
7984
|
-
|
|
8057
|
+
// HC-GF-077: empty against HEAD — the base branch already holds the change, so there is nothing to emit.
|
|
8058
|
+
emit({ ok: true, verdict: 'already_in_head', detail: `HEAD already contains ${opts.stepKey}'s files` },
|
|
8059
|
+
`○ HEAD already contains ${opts.stepKey}'s files — nothing to emit.`);
|
|
7985
8060
|
return;
|
|
7986
8061
|
}
|
|
7987
8062
|
|
|
@@ -8028,7 +8103,7 @@ program
|
|
|
8028
8103
|
const wt = fs.mkdtempSync(path.join(os.tmpdir(), 'hone-emit-'));
|
|
8029
8104
|
const diffFile = path.join(os.tmpdir(), `hone-emit-${Date.now()}.diff`);
|
|
8030
8105
|
fs.writeFileSync(diffFile, diff);
|
|
8031
|
-
const command =
|
|
8106
|
+
const { command, source: commandSource } = resolveCommandDetailed(opts.command, runConfig?.ci_local_command, { cwd: process.cwd() }); // followup-7 tiers; HC-GF-008: the source says whether `make ci` was a guess
|
|
8032
8107
|
// `worktree add -b` creates the branch ref BEFORE apply/verify. If we bail
|
|
8033
8108
|
// before a successful commit (apply fails, verify fails), that empty branch
|
|
8034
8109
|
// (pointing at HEAD) must be torn down too — else "a failing diff never
|
|
@@ -8086,19 +8161,21 @@ program
|
|
|
8086
8161
|
if (opts.format !== 'json') console.log(` verifying: ${command} (in isolated worktree, timeout ${opts.timeout}s)`);
|
|
8087
8162
|
const run = spawnSync(command, {
|
|
8088
8163
|
cwd: wt, shell: true, encoding: 'utf8',
|
|
8089
|
-
timeout: Number(opts.timeout) * 1000, env: gitEnv(),
|
|
8164
|
+
timeout: Number(opts.timeout) * 1000, env: verifyCommandEnv(gitEnv()),
|
|
8090
8165
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8091
8166
|
});
|
|
8092
8167
|
const timedOut = run.error && run.error.code === 'ETIMEDOUT';
|
|
8093
|
-
const v = verdictFromRun({ code: run.status, timedOut, stdout: run.stdout, stderr: run.stderr });
|
|
8168
|
+
const v = verdictFromRun({ code: run.status, timedOut, stdout: run.stdout, stderr: run.stderr, command, source: commandSource });
|
|
8094
8169
|
if (v.verdict === 'pass') {
|
|
8095
8170
|
verifyState = 'pass';
|
|
8096
8171
|
} else {
|
|
8097
8172
|
verifyOk = false;
|
|
8098
|
-
|
|
8173
|
+
// HC-GF-008: a command that never started is not a failed verify.
|
|
8174
|
+
const unresolved = v.verdict === 'unresolved';
|
|
8175
|
+
const sym = unresolved ? '○' : v.verdict === 'timeout' ? '⏱' : '✗';
|
|
8099
8176
|
emit({ ok: false, verdict: v.verdict, command, detail: v.detail },
|
|
8100
|
-
`${sym} verify failed, no branch emitted: ${v.detail}\n (command: ${command})`);
|
|
8101
|
-
exitCode = v.exitCode; // 3 tests-failed
|
|
8177
|
+
`${sym} ${unresolved ? 'verify could not run, no branch emitted' : 'verify failed, no branch emitted'}: ${v.detail}\n (command: ${command})`);
|
|
8178
|
+
exitCode = v.exitCode; // 3 tests-failed, 4 timeout, 7 unresolved
|
|
8102
8179
|
}
|
|
8103
8180
|
}
|
|
8104
8181
|
|
|
@@ -8258,7 +8335,9 @@ program
|
|
|
8258
8335
|
if (!command) { try { command = readCIGateConfig(repoRoot).local_command; } catch { command = 'make ci'; } }
|
|
8259
8336
|
command = command || 'make ci';
|
|
8260
8337
|
if (opts.format !== 'json') console.log(` running local CI: ${command} (timeout ${opts.timeout}s)`);
|
|
8261
|
-
|
|
8338
|
+
// HC-GF-008: the adopter's own command, without make state inherited from whoever ran hone.
|
|
8339
|
+
const { verifyCommandEnv } = require('./lib/verify-patch');
|
|
8340
|
+
const run = spawnSync(command, { cwd: repoRoot, shell: true, encoding: 'utf8', timeout: Number(opts.timeout) * 1000, stdio: ['ignore', 'inherit', 'inherit'], env: verifyCommandEnv(gitEnv()) });
|
|
8262
8341
|
local = { code: run.status == null ? 1 : run.status, timedOut: !!(run.error && run.error.code === 'ETIMEDOUT') };
|
|
8263
8342
|
}
|
|
8264
8343
|
|
package/lib/auto-verify.js
CHANGED
|
@@ -164,4 +164,24 @@ function verdictReportLine(verdict, gate) {
|
|
|
164
164
|
return ` → reported verdict '${verdict}' to the server (${effect})`;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
/**
|
|
168
|
+
* HC-GF-008 — the line run-story prints after auto-verify's `verify-patch` exits, keyed by that exit code.
|
|
169
|
+
*
|
|
170
|
+
* It said "verification did not pass" for every non-zero exit. On TICK-002 (run 5f1a7f75) that line sat
|
|
171
|
+
* directly under `✗ step_4 produced no file changes to verify.` — nothing had run, because the build's
|
|
172
|
+
* output had been discarded (HC-GF-075). Only a failing or timed-out command is a verification that did
|
|
173
|
+
* not pass; every other exit is a verification that did not happen, and says which.
|
|
174
|
+
*/
|
|
175
|
+
function verifyOutcomeLine(status, gate) {
|
|
176
|
+
const { EXIT } = require('./verify-patch');
|
|
177
|
+
switch (status) {
|
|
178
|
+
case EXIT.PASS: return ` → verified and recorded (gate: ${gate}). The gate still needs you.`;
|
|
179
|
+
case EXIT.TESTS_FAILED: case EXIT.TIMEOUT: return ' → verification did not pass; the gate still needs you.';
|
|
180
|
+
case EXIT.UNRESOLVED: return ' → verification could not run (see above) — nothing is known about this build; the gate still needs you.';
|
|
181
|
+
case EXIT.NO_DIFF: return ' → nothing was verified: no diff to apply, or the step output could not be fetched (see above); the gate still needs you.';
|
|
182
|
+
case EXIT.DOES_NOT_APPLY: return ' → nothing was verified: the diff did not apply (see above); the gate still needs you.';
|
|
183
|
+
default: return ` → verify-patch exited ${status} without a verdict (see above); the gate still needs you.`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { decideAutoVerify, announce, readVerificationAuto, shouldRepoll, alreadyVerified, verdictReportLine, verifyOutcomeLine, VERIFIABLE_STEP };
|
package/lib/emit-pr.js
CHANGED
|
@@ -34,6 +34,7 @@ const EXIT = {
|
|
|
34
34
|
TIMEOUT: 4, // --verify test command timed out
|
|
35
35
|
BRANCH_CONFLICT: 5, // local branch already exists (not overwritten)
|
|
36
36
|
PUBLISH_FAILED: 6, // push or gh pr create failed (no remote / no auth / rejected)
|
|
37
|
+
UNRESOLVED: 7, // --verify could not start the test command (HC-GF-008) — no commit made
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/**
|
package/lib/headroom.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* HC-GF-089 — how close a step came to its limits, said before it hits one.
|
|
4
|
+
*
|
|
5
|
+
* The largest local build used 793s of its 900s limit and nothing said so. Until now the only way to
|
|
6
|
+
* learn where a limit bites was to hit it: HC-GF-114 exists because the cut-off path had never fired
|
|
7
|
+
* in the entire run history, and reaching it took lowering a cap by hand. A run one slow story away
|
|
8
|
+
* from a timeout should say so while it still completes.
|
|
9
|
+
*
|
|
10
|
+
* ADVISORY by construction. This never blocks, never routes back and never changes a verdict — it
|
|
11
|
+
* prints. A signal that could fail a run would need a far higher bar than "approaching a limit", and
|
|
12
|
+
* a warning that reads like a rejection trains operators to ignore it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** HC-GF-080's per-message output cap — what the CLI child actually runs with. */
|
|
16
|
+
const TOKEN_CAP_PER_MESSAGE = 128_000;
|
|
17
|
+
|
|
18
|
+
/** Three quarters. Low enough to give a story's worth of warning, high enough not to fire routinely. */
|
|
19
|
+
const THRESHOLD = 0.75;
|
|
20
|
+
|
|
21
|
+
/** A ratio, or null when there is nothing to judge — a missing limit is not a limit of zero. */
|
|
22
|
+
function ratio(used, limit) {
|
|
23
|
+
const u = Number(used);
|
|
24
|
+
const l = Number(limit);
|
|
25
|
+
if (!Number.isFinite(u) || !Number.isFinite(l) || l <= 0 || u < 0) return null;
|
|
26
|
+
return u / l;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One line when a step passed THRESHOLD of its time or per-message token budget, else null.
|
|
31
|
+
* @param {object} generation HC-GF-079's generation report, as stored on the step row.
|
|
32
|
+
*/
|
|
33
|
+
function headroomLine(generation) {
|
|
34
|
+
const g = generation && typeof generation === 'object' ? generation : {};
|
|
35
|
+
|
|
36
|
+
const timeRatio = ratio(g.elapsed_ms, g.timeout_ms);
|
|
37
|
+
// The BUSIEST message decides: a build whose last message nearly filled the cap is at risk even
|
|
38
|
+
// when the earlier ones were small, and an average would hide exactly that.
|
|
39
|
+
const counts = (Array.isArray(g.output_tokens) ? g.output_tokens : [])
|
|
40
|
+
.map(Number).filter((n) => Number.isFinite(n) && n >= 0);
|
|
41
|
+
const busiest = counts.length ? Math.max(...counts) : null;
|
|
42
|
+
const tokenRatio = busiest === null ? null : ratio(busiest, TOKEN_CAP_PER_MESSAGE);
|
|
43
|
+
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (timeRatio !== null && timeRatio >= THRESHOLD) {
|
|
46
|
+
parts.push(`time ${Math.round(timeRatio * 100)}% `
|
|
47
|
+
+ `(${Math.round(Number(g.elapsed_ms) / 1000)}s of ${Math.round(Number(g.timeout_ms) / 1000)}s)`);
|
|
48
|
+
}
|
|
49
|
+
if (tokenRatio !== null && tokenRatio >= THRESHOLD) {
|
|
50
|
+
parts.push(`tokens ${Math.round(tokenRatio * 100)}% `
|
|
51
|
+
+ `(${busiest} of ${TOKEN_CAP_PER_MESSAGE} in one message)`);
|
|
52
|
+
}
|
|
53
|
+
if (!parts.length) return null;
|
|
54
|
+
// HC-GF-129: the threshold has no upper bound, so every ratio from 75% to infinity used to get
|
|
55
|
+
// the same reassurance. On run 692b0c29 a call that ran 550,017ms against a 550,000ms ceiling —
|
|
56
|
+
// it TIMED OUT, `complete: false`, the build was cut off — was reported as "not over it".
|
|
57
|
+
// At or past 1.0 the limit was reached, and saying otherwise is simply false.
|
|
58
|
+
const over = (timeRatio !== null && timeRatio >= 1) || (tokenRatio !== null && tokenRatio >= 1);
|
|
59
|
+
return `headroom: ${parts.join(', ')} — `
|
|
60
|
+
+ (over ? 'the limit was REACHED: this call was cut off' : 'close to the limit, not over it');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { headroomLine, TOKEN_CAP_PER_MESSAGE, THRESHOLD };
|