@hone-ai/cli 1.34.0 → 1.35.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/bin/hone-mcp.js +1 -0
- package/hone-cli.js +1341 -795
- package/lib/agent-eval-pacing.js +102 -0
- package/lib/agent-eval-probes-boundary.js +0 -0
- package/lib/agent-eval-probes-stateful.js +72 -0
- package/lib/agent-eval-score.js +70 -0
- package/lib/agent-executor.js +26 -4
- package/lib/asset-fetch-list.js +72 -0
- package/lib/auto-verify.js +67 -3
- package/lib/batch-follow.js +333 -0
- package/lib/block-grammar.js +126 -0
- package/lib/bundle-context.js +43 -4
- package/lib/bundle-paths.js +698 -1
- package/lib/bundle-source-dirs.js +79 -0
- package/lib/doctor-bind-default.js +192 -18
- package/lib/doctor-local-run.js +139 -1
- package/lib/emit-pr.js +39 -6
- package/lib/git-helpers.js +19 -8
- package/lib/hard-pause-remedy.js +101 -0
- package/lib/include-paths.js +155 -0
- package/lib/learnings-parse.js +97 -7
- package/lib/materialize-diff.js +131 -73
- package/lib/mcp-tools.js +20 -14
- package/lib/pipeline-config.js +182 -0
- package/lib/private-temp.js +52 -0
- package/lib/promote-payload.js +77 -0
- package/lib/refresh-knowledge.js +15 -6
- package/lib/repo-tree.js +22 -3
- package/lib/run-context.js +698 -0
- package/lib/run-target.js +66 -1
- package/lib/sandbox-lifecycle.js +357 -0
- package/lib/schedule-cron.js +1 -1
- package/lib/skill-assertions.js +61 -7
- package/lib/skill-audit-render.js +1 -1
- package/lib/story-classifier-extract.js +16 -8
- package/lib/story-classifier.js +64 -0
- package/lib/story-compose.js +69 -0
- package/lib/story-line.js +40 -0
- package/lib/story-provenance.js +267 -0
- package/lib/timeout-option.js +42 -0
- package/lib/untrusted-code-env.js +51 -0
- package/lib/verdict-env.js +31 -0
- package/lib/verify-patch.js +74 -4
- package/lib/verify-pr.js +2 -1
- package/lib/worktree-provision.js +62 -2
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* agent-eval-pacing.js — HC-GF-199. Pace a calibration run from what the
|
|
4
|
+
* provider actually did.
|
|
5
|
+
*
|
|
6
|
+
* Measured 2026-09-19: three sequential calibration runs, no pacing of any kind.
|
|
7
|
+
* Run 1 completed 5 of 8; runs 2 and 3 completed NOTHING. Twenty-four
|
|
8
|
+
* consecutive generations on one subscription and the provider stopped
|
|
9
|
+
* answering, so the experiment measured provider availability rather than the
|
|
10
|
+
* judgment variance it was run to measure.
|
|
11
|
+
*
|
|
12
|
+
* WHY THIS IS NOT A SLEEP. A constant delay is fitted to one afternoon's limits.
|
|
13
|
+
* It goes stale silently, and the empty runs it then produces look like a
|
|
14
|
+
* regression in the thing under test rather than an outage in the instrument.
|
|
15
|
+
* The row for this story forbids it explicitly. Everything here is derived from
|
|
16
|
+
* observed outcomes: zero refusals means zero delay, and a provider that
|
|
17
|
+
* recovers is rewarded immediately.
|
|
18
|
+
*
|
|
19
|
+
* WHAT COUNTS AS A REFUSAL. Only `outcome: 'error'` — the probe did not
|
|
20
|
+
* complete. A graded WRONG answer (`outcome: 'fail'`) is a measurement, and
|
|
21
|
+
* backing off because the agent was wrong would slow a run that is working
|
|
22
|
+
* perfectly. That distinction is [[HC-GF-198]]'s, and it is load-bearing here.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Base unit of backoff. Doubling starts here; it is not a tuned wait. */
|
|
26
|
+
const BASE_DELAY_MS = 2_000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ceiling on a single wait. An unbounded backoff is the hang HC-CI-026 exists
|
|
30
|
+
* to stop, wearing a different hat — a run that waits forever reports nothing
|
|
31
|
+
* and looks identical to one still working.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_DELAY_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Consecutive refusals after which the run gives up.
|
|
37
|
+
*
|
|
38
|
+
* Grinding on produces a row of nulls, burns generations against a provider
|
|
39
|
+
* that has already said no, and makes a dead run look like a fast one. Stopping
|
|
40
|
+
* and SAYING SO is the honest outcome — the same rule as HC-CI-016: a run that
|
|
41
|
+
* could not run is not a result.
|
|
42
|
+
*/
|
|
43
|
+
const STOP_AFTER_CONSECUTIVE = 3;
|
|
44
|
+
|
|
45
|
+
/** Exponential backoff in the number of CONSECUTIVE refusals, capped. */
|
|
46
|
+
function nextDelayMs(consecutiveRefusals) {
|
|
47
|
+
if (!consecutiveRefusals || consecutiveRefusals < 1) return 0;
|
|
48
|
+
return Math.min(BASE_DELAY_MS * (2 ** (consecutiveRefusals - 1)), MAX_DELAY_MS);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Has the provider refused enough times in a row to call it stopped? */
|
|
52
|
+
function shouldStop(consecutiveRefusals) {
|
|
53
|
+
return consecutiveRefusals >= STOP_AFTER_CONSECUTIVE;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Bookkeeping over a run. `record` takes the scored probe result; `delayBeforeNext`
|
|
58
|
+
* says how long to wait before the next probe; `stopReason` is non-null once the
|
|
59
|
+
* provider should be treated as gone.
|
|
60
|
+
*/
|
|
61
|
+
function createPacer() {
|
|
62
|
+
let consecutive = 0;
|
|
63
|
+
let refusals = 0;
|
|
64
|
+
let waitedMs = 0;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
record(result) {
|
|
68
|
+
// Only a non-completion is a refusal. A wrong answer is data.
|
|
69
|
+
if (result && result.outcome === 'error') {
|
|
70
|
+
consecutive += 1;
|
|
71
|
+
refusals += 1;
|
|
72
|
+
} else {
|
|
73
|
+
consecutive = 0;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
delayBeforeNext() {
|
|
77
|
+
return nextDelayMs(consecutive);
|
|
78
|
+
},
|
|
79
|
+
noteWaited(ms) {
|
|
80
|
+
waitedMs += ms;
|
|
81
|
+
},
|
|
82
|
+
stopReason() {
|
|
83
|
+
return shouldStop(consecutive)
|
|
84
|
+
? `the provider refused ${consecutive} consecutive probes — stopping rather than `
|
|
85
|
+
+ 'reporting a run it did not complete'
|
|
86
|
+
: null;
|
|
87
|
+
},
|
|
88
|
+
/** What the run should SAY about its own pacing. A degraded run must not look fast. */
|
|
89
|
+
summary() {
|
|
90
|
+
return { refusals, consecutiveAtEnd: consecutive, waitedMs };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
createPacer,
|
|
97
|
+
nextDelayMs,
|
|
98
|
+
shouldStop,
|
|
99
|
+
BASE_DELAY_MS,
|
|
100
|
+
MAX_DELAY_MS,
|
|
101
|
+
STOP_AFTER_CONSECUTIVE,
|
|
102
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* agent-eval-probes-stateful.js — HC-GF-197.
|
|
4
|
+
*
|
|
5
|
+
* Calibration for the ONE judgment the sandbox chain depends on: does this story's acceptance need
|
|
6
|
+
* stored state? HC-GF-194 made step_0 record it as a required field, which fixed the MECHANISM —
|
|
7
|
+
* the question is now always asked and never silently defaulted. It did not fix the ANSWER: the
|
|
8
|
+
* same story came back true, false, true across three live runs.
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS IS NOT AN `evals/` SCENARIO. That harness grades PROMPT TEXT — `runScenario(scenario,
|
|
11
|
+
* promptText)`, "zero LLM tokens" — and its llm-judge mode judges the prompt too ("Does the prompt
|
|
12
|
+
* clearly define the agent's role?"). An eval here would assert the prompt MENTIONS
|
|
13
|
+
* `requires_stored_state`, which HC-GF-194's regression test already pins, and would say nothing
|
|
14
|
+
* about whether the groomer answers correctly. `agent-eval` is the surface that runs the agent and
|
|
15
|
+
* grades its OUTPUT, which is what calibration means.
|
|
16
|
+
*
|
|
17
|
+
* GROUND TRUTH IS THE OWNER'S, NOT MINE. Labelled 2026-09-19 against HC-GF-173's rule — "starting
|
|
18
|
+
* from an EMPTY datastore, could this acceptance be checked without inserting anything?" — because
|
|
19
|
+
* scoring the groomer against my own reading of the rule would be measuring the thing in question.
|
|
20
|
+
* HC-GF-126 is the case that matters: labelled TRUE, and a live run answered FALSE, so that run was
|
|
21
|
+
* WRONG rather than defensibly borderline.
|
|
22
|
+
*
|
|
23
|
+
* Each probe carries the story's ACCEPTANCE, which is what the rule is about — not its title, and
|
|
24
|
+
* not its implementation notes, both of which mention datastores for stories that need none.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Matches the recorded answer, tolerating the bold and backticked renderings the harvest does. */
|
|
28
|
+
const ANSWERS = (v) => `requires_stored_state\`?\\**\\s*\`?\\s*:\\s*\\**\\s*${v}\\b`;
|
|
29
|
+
|
|
30
|
+
/** A story whose acceptance CANNOT be checked against an empty datastore. */
|
|
31
|
+
const needsState = (id, acceptance) => ({
|
|
32
|
+
id: `stateful-true-${id}`,
|
|
33
|
+
input: `Groom this story and produce your Routing block.\n\nStory ${id}\nAcceptance: ${acceptance}`,
|
|
34
|
+
// One grader, deliberately. An earlier draft added a second asserting the question was answered
|
|
35
|
+
// AT ALL — reasoning that a groomer emitting neither value would slip through. That is true of a
|
|
36
|
+
// `not_contains` check and false of this one: a positive regex for `true` already fails on
|
|
37
|
+
// silence and on `false`. Mutation confirmed it: deleting the second grader changed no outcome.
|
|
38
|
+
graders: [{ type: 'regex', pattern: ANSWERS('true'), flags: 'i' }],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** A story whose acceptance is checkable with the datastore empty — real inputs already exist. */
|
|
42
|
+
const needsNoState = (id, acceptance) => ({
|
|
43
|
+
id: `stateful-false-${id}`,
|
|
44
|
+
input: `Groom this story and produce your Routing block.\n\nStory ${id}\nAcceptance: ${acceptance}`,
|
|
45
|
+
graders: [{ type: 'regex', pattern: ANSWERS('false'), flags: 'i' }],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = [
|
|
49
|
+
// ── TRUE: a row must exist before the acceptance can be read ──
|
|
50
|
+
needsState('HC-GF-126',
|
|
51
|
+
'A fresh N-day trial reports exactly N with the DB clock forced ahead of the process clock, '
|
|
52
|
+
+ 'and still reports N with it behind.'),
|
|
53
|
+
needsState('HC-GF-195',
|
|
54
|
+
'The expiry verdict the route reports and the one the key resolver enforces come from the same '
|
|
55
|
+
+ 'clock, proven by a test that skews the process clock and shows both unchanged.'),
|
|
56
|
+
needsState('HC-GF-192',
|
|
57
|
+
"A stateful story's verification runs against the database the sandbox block declares, proven "
|
|
58
|
+
+ 'by the run itself rather than by configuration.'),
|
|
59
|
+
needsState('HC-GF-105',
|
|
60
|
+
'Two failures sharing a cause signature pause the batch; two with different causes do not.'),
|
|
61
|
+
|
|
62
|
+
// ── FALSE: a diff, a file or a command is real data — seeding there tests the fixture author ──
|
|
63
|
+
needsNoState('HC-GF-188',
|
|
64
|
+
'A story naming a symbol in an oversized file either receives that symbol\'s region, or the '
|
|
65
|
+
+ 'operator is told the named file was cut.'),
|
|
66
|
+
needsNoState('HC-GF-182',
|
|
67
|
+
'A new roadmap row whose premise greps docs/ without excluding the roadmap fails the gate.'),
|
|
68
|
+
needsNoState('HC-GF-186',
|
|
69
|
+
'`make ci` reports how many captured learnings are unpromoted.'),
|
|
70
|
+
needsNoState('HC-CI-024',
|
|
71
|
+
'An iteration run skips only the continuity and e2e gates, and says so.'),
|
|
72
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* agent-eval-score.js — HC-GF-198.
|
|
4
|
+
*
|
|
5
|
+
* A probe has THREE outcomes, not two. `agent-eval` scored `passed = !run.error && graders.every(…)`,
|
|
6
|
+
* so a probe that never answered was recorded identically to one that answered wrongly. Measured on
|
|
7
|
+
* HC-GF-197's first calibration: 8 probes, 3 timed out, and the reported figure was 4/8 where 4 of
|
|
8
|
+
* the 5 that COMPLETED were correct — understating the agent by 20 points and sending a reader
|
|
9
|
+
* after a correctness problem that was really a latency one.
|
|
10
|
+
*
|
|
11
|
+
* This is HC-GF-193 mirrored. There a suite that ran ZERO tests exited 0 and read as a pass; here an
|
|
12
|
+
* agent that answered NOTHING reads as a wrong answer. Both collapse "no result" into a verdict.
|
|
13
|
+
*
|
|
14
|
+
* A TIMEOUT IS STILL NOT OK. Excluding it from accuracy must not make it invisible: a target that
|
|
15
|
+
* times out on every probe would then report a clean run with no accuracy at all, which is the same
|
|
16
|
+
* defect a third time. It is reported as its own outcome, counted separately, and still fails the
|
|
17
|
+
* command's exit — visible, excluded from the score, never silently fine.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The outcome of one probe.
|
|
22
|
+
*
|
|
23
|
+
* @param {{error?: string|null}} run what the executor returned
|
|
24
|
+
* @param {Array<{passed: boolean}>} graders grader results (empty when the run errored)
|
|
25
|
+
* @returns {{outcome: 'pass'|'fail'|'error', passed: boolean, error: string|null}}
|
|
26
|
+
*/
|
|
27
|
+
function scoreProbe(run, graders = []) {
|
|
28
|
+
const error = (run && run.error) || null;
|
|
29
|
+
if (error) return { outcome: 'error', passed: false, error };
|
|
30
|
+
const passed = graders.length > 0 && graders.every((g) => g.passed);
|
|
31
|
+
return { outcome: passed ? 'pass' : 'fail', passed, error: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What a set of probe results means together.
|
|
36
|
+
*
|
|
37
|
+
* `accuracy` is over COMPLETED probes only and is null when none completed — a ratio with an empty
|
|
38
|
+
* denominator is not 0%, and printing 0% for "nothing ran" is the defect this file exists to fix.
|
|
39
|
+
*
|
|
40
|
+
* @param {Array<{outcome: string}>} results
|
|
41
|
+
* @returns {{total, completed, correct, errored, accuracy: string|null, ok: boolean}}
|
|
42
|
+
*/
|
|
43
|
+
function summarizeProbes(results = []) {
|
|
44
|
+
const total = results.length;
|
|
45
|
+
const errored = results.filter((r) => r.outcome === 'error').length;
|
|
46
|
+
const correct = results.filter((r) => r.outcome === 'pass').length;
|
|
47
|
+
const completed = total - errored;
|
|
48
|
+
return {
|
|
49
|
+
total,
|
|
50
|
+
completed,
|
|
51
|
+
correct,
|
|
52
|
+
errored,
|
|
53
|
+
accuracy: completed > 0 ? `${correct}/${completed}` : null,
|
|
54
|
+
// Any wrong answer OR any probe that never answered fails the command. A run nobody could
|
|
55
|
+
// score is not a run that passed.
|
|
56
|
+
ok: errored === 0 && correct === completed && total > 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One line a reader can act on, keeping the two failure kinds apart. */
|
|
61
|
+
function describeProbeSummary(s) {
|
|
62
|
+
const acc = s.accuracy === null
|
|
63
|
+
? 'no probe completed, so there is no accuracy to report'
|
|
64
|
+
: `${s.accuracy} of the probes that completed`;
|
|
65
|
+
return s.errored > 0
|
|
66
|
+
? `${acc} — ${s.errored} of ${s.total} never answered (timeout or spawn failure) and are NOT counted as wrong answers`
|
|
67
|
+
: `${acc}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { scoreProbe, summarizeProbes, describeProbeSummary };
|
package/lib/agent-executor.js
CHANGED
|
@@ -18,7 +18,7 @@ const { spawn } = require('child_process');
|
|
|
18
18
|
* @param {object} target - agent target config {name, invoke: {command}}
|
|
19
19
|
* @param {string} probeInput - probe input text to pass via stdin
|
|
20
20
|
* @param {object} opts - options {timeout: 5000}
|
|
21
|
-
* @returns {Promise<{output: string} | {error: string, exitCode?: number}>}
|
|
21
|
+
* @returns {Promise<{output: string} | {error: string, exitCode?: number, stderr?: string}>}
|
|
22
22
|
*/
|
|
23
23
|
async function runAgent(target, probeInput, opts = {}) {
|
|
24
24
|
if (!target || !target.invoke || !target.invoke.command) {
|
|
@@ -26,6 +26,13 @@ async function runAgent(target, probeInput, opts = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const timeout = opts.timeout ?? 5000;
|
|
29
|
+
// HC-GF-199: the child's stderr was captured into `errorBuffer` and NEVER
|
|
30
|
+
// READ — measured, two references in this file: one declaration, one append.
|
|
31
|
+
// The provider's own explanation of why it refused was collected and thrown
|
|
32
|
+
// away, which is precisely the signal honest backoff and diagnosis need.
|
|
33
|
+
// Bounded because a chatty failure must not blow up the JSON report.
|
|
34
|
+
const STDERR_CAP = 4096;
|
|
35
|
+
const tail = (s) => (s.length > STDERR_CAP ? s.slice(-STDERR_CAP) : s);
|
|
29
36
|
const command = target.invoke.command;
|
|
30
37
|
|
|
31
38
|
return new Promise((resolve) => {
|
|
@@ -46,6 +53,21 @@ async function runAgent(target, probeInput, opts = {}) {
|
|
|
46
53
|
child = spawn('sh', ['-c', command], {
|
|
47
54
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
55
|
detached: true,
|
|
56
|
+
// HC-GF-199: run where the CALLER says, not wherever the CLI happens to
|
|
57
|
+
// be. The calibration packs exercise the real story-groomer prompt,
|
|
58
|
+
// which instructs the agent to WRITE its artifacts — and it did, into
|
|
59
|
+
// this repository, for a story nobody groomed. A measurement must not
|
|
60
|
+
// mutate the thing it measures. Defaults to the current directory so
|
|
61
|
+
// every existing pack behaves exactly as before.
|
|
62
|
+
cwd: opts.cwd || process.cwd(),
|
|
63
|
+
// HC-GF-199: isolating the cwd breaks any target command that resolves a
|
|
64
|
+
// repo path RELATIVELY — and this repo's own did. Give every command an
|
|
65
|
+
// absolute anchor so it can load from the repo while writing to the
|
|
66
|
+
// sandbox. Measured: without this, the prompt load threw MODULE_NOT_FOUND,
|
|
67
|
+
// the shell group carried on, and the provider was called with an EMPTY
|
|
68
|
+
// system prompt — producing plausible output in the wrong format that the
|
|
69
|
+
// graders scored as a WRONG ANSWER, at full generation cost.
|
|
70
|
+
env: { ...process.env, HONE_REPO_ROOT: opts.repoRoot || process.cwd() },
|
|
49
71
|
});
|
|
50
72
|
} catch (e) {
|
|
51
73
|
// Spawn failed (e.g., sh not found — unlikely but possible)
|
|
@@ -99,19 +121,19 @@ async function runAgent(target, probeInput, opts = {}) {
|
|
|
99
121
|
|
|
100
122
|
// If we timed out, return timeout error
|
|
101
123
|
if (timedOut) {
|
|
102
|
-
return resolve({ error: 'timeout', exitCode: null });
|
|
124
|
+
return resolve({ error: 'timeout', exitCode: null, stderr: tail(errorBuffer) });
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
// If process was killed by a signal (e.g., SIGSEGV = signal 11 → exit code 139)
|
|
106
128
|
if (signal) {
|
|
107
129
|
const signalCode = signal === 'SIGSEGV' ? 11 : (signal === 'SIGKILL' ? 9 : 0);
|
|
108
130
|
const exitCodeForSignal = 128 + signalCode;
|
|
109
|
-
return resolve({ error: 'process crashed', exitCode: exitCodeForSignal });
|
|
131
|
+
return resolve({ error: 'process crashed', exitCode: exitCodeForSignal, stderr: tail(errorBuffer) });
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
// Non-zero exit code
|
|
113
135
|
if (exitCode !== 0) {
|
|
114
|
-
return resolve({ error: 'command failed', exitCode });
|
|
136
|
+
return resolve({ error: 'command failed', exitCode, stderr: tail(errorBuffer) });
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
// Success
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* asset-fetch-list.js — the agent-stub and skill files `hone setup` pulls from the server.
|
|
4
|
+
*
|
|
5
|
+
* Extracted by HC-GF-143's batch: HC-GF-136's gate found `key`, `promise` and `write` computed
|
|
6
|
+
* inline and asserted by nothing. `key` is what the operator sees when a fetch fails, and `write`
|
|
7
|
+
* decides where the bytes land — a wrong path installs an adopter's agent into the wrong directory
|
|
8
|
+
* and the run reports success.
|
|
9
|
+
*
|
|
10
|
+
* Pure: the caller injects the fetch and the two writers, so the mapping is testable without a
|
|
11
|
+
* network or a filesystem.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} args
|
|
16
|
+
* @param {string[]} args.agents agent names
|
|
17
|
+
* @param {string[]} args.skills skill names
|
|
18
|
+
* @param {(url: string) => Promise<any>} args.get
|
|
19
|
+
* @param {(agent: string, data: any) => void} args.writeAgent
|
|
20
|
+
* @param {(skill: string, data: any) => void} args.writeSkill
|
|
21
|
+
* @returns {Array<{key: string, promise: Promise<any>, write: (data: any) => void}>}
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The derived skills in a `/skills/:repo/derived` response — or nothing.
|
|
25
|
+
*
|
|
26
|
+
* HC-GF-184: a named function rather than an inline ternary, because the inline version was
|
|
27
|
+
* effectively untestable. A non-array `skills` (say the string "nope") iterates as CHARACTERS, and
|
|
28
|
+
* the per-item shape check rejects each one, so the wrong container and the right one produce the
|
|
29
|
+
* same visible result — a mutation of the container check survived every test. Returning the list
|
|
30
|
+
* makes the decision observable on its own.
|
|
31
|
+
*/
|
|
32
|
+
function derivedSkillsFrom(data) {
|
|
33
|
+
if (!data || !Array.isArray(data.skills)) return [];
|
|
34
|
+
return data.skills.filter(
|
|
35
|
+
s => s && typeof s.skill_name === 'string' && typeof s.content === 'string');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assetFetchList({ agents = [], skills = [], derivedRepo = null, get, writeAgent, writeSkill }) {
|
|
39
|
+
return [
|
|
40
|
+
...agents.map(agent => ({
|
|
41
|
+
key: `${agent}.agent.md`,
|
|
42
|
+
promise: get(`/scripts/agent-stub/${agent}`),
|
|
43
|
+
write: (data) => writeAgent(agent, data),
|
|
44
|
+
})),
|
|
45
|
+
...skills.map(skill => ({
|
|
46
|
+
key: `skills/${skill}/SKILL.md`,
|
|
47
|
+
promise: get(`/skills/${skill}`),
|
|
48
|
+
write: (data) => writeSkill(skill, data),
|
|
49
|
+
})),
|
|
50
|
+
// ── HC-GF-184: the per-repo DERIVED set ────────────────────────────────────────────────────
|
|
51
|
+
// The list above is hand-typed, so it names the eight enterprise baselines and can never name a
|
|
52
|
+
// derived skill. Measured 2026-09-18: `node-developer` and `node-architect` had sat in
|
|
53
|
+
// skill_snapshots since the previous day while `.github/skills/` held the baselines only — the
|
|
54
|
+
// pipeline could read them, the adopter's editor could not, and `last_derived` read as "in use".
|
|
55
|
+
//
|
|
56
|
+
// ADDITIVE ON PURPOSE. The three copies of that list (here, the derive parser, the step
|
|
57
|
+
// declarations) are a real chokepoint, but unifying them would change what every existing
|
|
58
|
+
// adopter receives. This adds one request that returns `{skills: []}` for a repo that has never
|
|
59
|
+
// derived, so setup behaves exactly as before for them.
|
|
60
|
+
...(derivedRepo ? [{
|
|
61
|
+
key: `skills/<derived for ${derivedRepo}>`,
|
|
62
|
+
promise: get(`/skills/${derivedRepo}/derived`),
|
|
63
|
+
// Tolerant by construction: this runs mid-setup, AFTER the baseline has landed, so a shape
|
|
64
|
+
// change at the endpoint must not abort an adopter's install half-written.
|
|
65
|
+
write: (data) => {
|
|
66
|
+
for (const s of derivedSkillsFrom(data)) writeSkill(s.skill_name, s.content);
|
|
67
|
+
},
|
|
68
|
+
}] : []),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { assetFetchList, derivedSkillsFrom };
|
package/lib/auto-verify.js
CHANGED
|
@@ -50,13 +50,35 @@ const VERIFIABLE_STEP = 'step_4';
|
|
|
50
50
|
* how to stop it — the surprise is the real cost of this feature, and an unexplained
|
|
51
51
|
* surprise is the version that gets it reverted.
|
|
52
52
|
*/
|
|
53
|
-
function decideAutoVerify({ stepKey, repoRoot, noVerifyFlag = false, env = process.env } = {}) {
|
|
53
|
+
function decideAutoVerify({ stepKey, repoRoot, noVerifyFlag = false, env = process.env, assessment } = {}) {
|
|
54
54
|
const gate = safeGate(repoRoot);
|
|
55
55
|
const off = (reason) => ({ run: false, reason, command: null, gate });
|
|
56
56
|
|
|
57
57
|
if (noVerifyFlag) return off('--no-verify was passed');
|
|
58
58
|
if (String(env.HONE_NO_AUTO_VERIFY || '') === '1') return off('HONE_NO_AUTO_VERIFY=1');
|
|
59
59
|
|
|
60
|
+
// ── HC-GF-270: consent to run YOUR tests is not consent to run a STRANGER'S code ──────
|
|
61
|
+
//
|
|
62
|
+
// `ci.local_command` is the adopter saying how to test this repo. It is not the adopter
|
|
63
|
+
// saying "execute whatever patch the pipeline produced from whatever text it was given",
|
|
64
|
+
// and until now this function had no input about where that text came from: a story
|
|
65
|
+
// fetched from a GitHub issue by a non-collaborator reached the provisioning install
|
|
66
|
+
// (which runs the PATCHED manifest's `postinstall`) as the operator, before anyone read it.
|
|
67
|
+
//
|
|
68
|
+
// FAILS CLOSED, and deliberately BEFORE the stepKey check: a caller that forgot to assess
|
|
69
|
+
// must produce a pause, never a bypass. The assessment is the caller's, not ours, so that
|
|
70
|
+
// deleting this check and deleting the trusted set in story-provenance.js are two
|
|
71
|
+
// separately-mutatable layers (Checklist B item 6) — the reason trust is not derived here.
|
|
72
|
+
if (!assessment || typeof assessment !== 'object') {
|
|
73
|
+
const reason = 'no story provenance assessment was supplied — refusing to execute this '
|
|
74
|
+
+ 'patch until the caller says where its story text came from';
|
|
75
|
+
return { ...off(reason), untrusted: { label: 'unassessed', reason } };
|
|
76
|
+
}
|
|
77
|
+
if (!assessment.trusted) {
|
|
78
|
+
const reason = assessment.reason || 'this run\'s story text came from a source Hone does not trust';
|
|
79
|
+
return { ...off(reason), untrusted: { label: assessment.label || 'untrusted', reason } };
|
|
80
|
+
}
|
|
81
|
+
|
|
60
82
|
// Only step_4. A verdict for any other step has no gate to inform, so running an
|
|
61
83
|
// adopter's suite there would spend their time for nothing.
|
|
62
84
|
if (stepKey !== VERIFIABLE_STEP) {
|
|
@@ -100,7 +122,9 @@ function readYamlBlock(repoRoot, key) {
|
|
|
100
122
|
|
|
101
123
|
function safeCi(repoRoot) {
|
|
102
124
|
try { return readCIConfigDetails(repoRoot); }
|
|
103
|
-
|
|
125
|
+
// readCIConfigDetails catches its own read and parse failures, so this is belt-and-braces: if it
|
|
126
|
+
// ever throws, an empty object reads as "no ci.local_command", which is no consent (HC-GF-270).
|
|
127
|
+
catch { return {}; }
|
|
104
128
|
}
|
|
105
129
|
function safeGate(repoRoot) {
|
|
106
130
|
try { return readVerificationGateConfig(repoRoot) || 'advisory'; }
|
|
@@ -116,6 +140,39 @@ function announce(decision, runId) {
|
|
|
116
140
|
].join('\n');
|
|
117
141
|
}
|
|
118
142
|
|
|
143
|
+
/**
|
|
144
|
+
* HC-GF-270 — what the operator sees INSTEAD of a verification.
|
|
145
|
+
*
|
|
146
|
+
* A pause the operator cannot explain is indistinguishable from a broken pipeline, and the
|
|
147
|
+
* fix for that is not fewer pauses, it is a pause that names the issue, the author, the
|
|
148
|
+
* association that disqualified it, and the two commands that move forward: read the patch,
|
|
149
|
+
* then run it having read it. `--trust-source` is on the SECOND line on purpose — an
|
|
150
|
+
* operator who copies the first line has read the diff.
|
|
151
|
+
*/
|
|
152
|
+
function provenanceNotice(assessment, { runId, source = {}, stepKey = 'step_4' } = {}) {
|
|
153
|
+
const src = (source && typeof source === 'object' && !Array.isArray(source)) ? source : {};
|
|
154
|
+
// HC-GF-270 code review (LOW-6): one shape. `ref` is {owner,repo,number} or null for every
|
|
155
|
+
// kind, and the human-readable name of a non-issue source lives in `label` — so this reads
|
|
156
|
+
// the same two fields whatever kind produced the run, and a new kind cannot break it.
|
|
157
|
+
const r = src.ref;
|
|
158
|
+
const ref = (r && typeof r === 'object' && !Array.isArray(r) && r.owner && r.repo && r.number != null)
|
|
159
|
+
? `${r.owner}/${r.repo}#${r.number}`
|
|
160
|
+
: (typeof src.label === 'string' && src.label.trim() !== '' ? src.label.trim() : 'unknown');
|
|
161
|
+
return [
|
|
162
|
+
'',
|
|
163
|
+
' ⏸ NEEDS YOU — auto-verify did NOT run: this run\'s story text came from a source Hone does not trust.',
|
|
164
|
+
` source: ${ref}${src.html_url ? ` (${src.html_url})` : ''}`,
|
|
165
|
+
` author: ${src.author || 'unknown'} author_association: ${src.association || 'unknown'}`,
|
|
166
|
+
` reason: ${(assessment && assessment.reason) || 'untrusted'}`,
|
|
167
|
+
'',
|
|
168
|
+
' Verifying would run this patch — the provisioning install FIRST — as you, with your',
|
|
169
|
+
' $HOME and your network. Nothing has been executed.',
|
|
170
|
+
` 1. read it: hone show step ${runId} ${stepKey}`,
|
|
171
|
+
` 2. run it: hone verify-patch ${runId} --report --trust-source`,
|
|
172
|
+
'',
|
|
173
|
+
].join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
119
176
|
|
|
120
177
|
/**
|
|
121
178
|
* After a verdict is posted, should the caller RE-POLL (the run may have moved) or fall
|
|
@@ -180,8 +237,15 @@ function verifyOutcomeLine(status, gate) {
|
|
|
180
237
|
case EXIT.UNRESOLVED: return ' → verification could not run (see above) — nothing is known about this build; the gate still needs you.';
|
|
181
238
|
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
239
|
case EXIT.DOES_NOT_APPLY: return ' → nothing was verified: the diff did not apply (see above); the gate still needs you.';
|
|
240
|
+
// HC-GF-270 code review (INFO): the one arm of the exit-code contract that was extended in
|
|
241
|
+
// verify-patch.js/emit-pr.js and not here. Unreachable from the poll loop today — it only
|
|
242
|
+
// spawns when the assessment is trusted — but an exit code that means "refused" must never
|
|
243
|
+
// render as "exited N without a verdict", which reads as a crash. The map is the contract;
|
|
244
|
+
// a hole in it is a defect waiting for the second caller.
|
|
245
|
+
case EXIT.UNTRUSTED_SOURCE: return ' → nothing was verified: this run\'s story text came from a source Hone '
|
|
246
|
+
+ 'does not trust (see above). Read the patch, then re-run with --trust-source; the gate still needs you.';
|
|
183
247
|
default: return ` → verify-patch exited ${status} without a verdict (see above); the gate still needs you.`;
|
|
184
248
|
}
|
|
185
249
|
}
|
|
186
250
|
|
|
187
|
-
module.exports = { decideAutoVerify, announce, readVerificationAuto, shouldRepoll, alreadyVerified, verdictReportLine, verifyOutcomeLine, VERIFIABLE_STEP };
|
|
251
|
+
module.exports = { decideAutoVerify, announce, provenanceNotice, readVerificationAuto, shouldRepoll, alreadyVerified, verdictReportLine, verifyOutcomeLine, VERIFIABLE_STEP };
|