@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.
Files changed (46) hide show
  1. package/bin/hone-mcp.js +1 -0
  2. package/hone-cli.js +1341 -795
  3. package/lib/agent-eval-pacing.js +102 -0
  4. package/lib/agent-eval-probes-boundary.js +0 -0
  5. package/lib/agent-eval-probes-stateful.js +72 -0
  6. package/lib/agent-eval-score.js +70 -0
  7. package/lib/agent-executor.js +26 -4
  8. package/lib/asset-fetch-list.js +72 -0
  9. package/lib/auto-verify.js +67 -3
  10. package/lib/batch-follow.js +333 -0
  11. package/lib/block-grammar.js +126 -0
  12. package/lib/bundle-context.js +43 -4
  13. package/lib/bundle-paths.js +698 -1
  14. package/lib/bundle-source-dirs.js +79 -0
  15. package/lib/doctor-bind-default.js +192 -18
  16. package/lib/doctor-local-run.js +139 -1
  17. package/lib/emit-pr.js +39 -6
  18. package/lib/git-helpers.js +19 -8
  19. package/lib/hard-pause-remedy.js +101 -0
  20. package/lib/include-paths.js +155 -0
  21. package/lib/learnings-parse.js +97 -7
  22. package/lib/materialize-diff.js +131 -73
  23. package/lib/mcp-tools.js +20 -14
  24. package/lib/pipeline-config.js +182 -0
  25. package/lib/private-temp.js +52 -0
  26. package/lib/promote-payload.js +77 -0
  27. package/lib/refresh-knowledge.js +15 -6
  28. package/lib/repo-tree.js +22 -3
  29. package/lib/run-context.js +698 -0
  30. package/lib/run-target.js +66 -1
  31. package/lib/sandbox-lifecycle.js +357 -0
  32. package/lib/schedule-cron.js +1 -1
  33. package/lib/skill-assertions.js +61 -7
  34. package/lib/skill-audit-render.js +1 -1
  35. package/lib/story-classifier-extract.js +16 -8
  36. package/lib/story-classifier.js +64 -0
  37. package/lib/story-compose.js +69 -0
  38. package/lib/story-line.js +40 -0
  39. package/lib/story-provenance.js +267 -0
  40. package/lib/timeout-option.js +42 -0
  41. package/lib/untrusted-code-env.js +51 -0
  42. package/lib/verdict-env.js +31 -0
  43. package/lib/verify-patch.js +74 -4
  44. package/lib/verify-pr.js +2 -1
  45. package/lib/worktree-provision.js +62 -2
  46. package/package.json +1 -1
@@ -18,6 +18,8 @@
18
18
  * implemented" — the existing tests may not cover the new behaviour.
19
19
  */
20
20
 
21
+ const { withoutHoneVars } = require('./untrusted-code-env');
22
+
21
23
  /** Exit codes, matching the check-patch convention so scripts can branch. */
22
24
  const EXIT = {
23
25
  PASS: 0, // tests passed on the applied diff
@@ -28,6 +30,16 @@ const EXIT = {
28
30
  // HC-GF-008: the command itself could not start, so nothing about the patch is known. Not 5 or 6:
29
31
  // emit-pr re-exits with these codes, and those two are its BRANCH_CONFLICT and PUBLISH_FAILED.
30
32
  UNRESOLVED: 7,
33
+ // HC-GF-201: the environment was incomplete, so nothing was observed about the diff.
34
+ // Distinct from TESTS_FAILED on purpose — emit-pr re-exits these codes, and a run that
35
+ // could not run is not a result (HC-CI-016).
36
+ NOT_RUN: 8,
37
+ // HC-GF-270: this run's story text came from a source Hone does not trust, so the patch was
38
+ // never executed and NO verdict was reported. A refusal, not a failure — nothing is known
39
+ // about the diff. Pass --trust-source to run it having read it.
40
+ // MUST stay numerically identical to emit-pr.js's EXIT.UNTRUSTED_SOURCE: emit-pr re-exits
41
+ // these codes, so the two maps are byte-compatible by contract.
42
+ UNTRUSTED_SOURCE: 9,
31
43
  };
32
44
 
33
45
  /**
@@ -53,6 +65,16 @@ function _npmTestScript(cwd) {
53
65
  return (typeof t === 'string' && t.trim() && !/no test specified/i.test(t)) ? 'npm test' : null;
54
66
  } catch { return null; }
55
67
  }
68
+ /**
69
+ * HC-GF-223: the sources that are a DECLARATION rather than this resolver's own guess. Overriding a
70
+ * declared command narrows the evidence; overriding the terminal `make ci` guess supplies evidence
71
+ * where there was none, and must not be penalised (HC-GF-008 named that tier a guess for the same
72
+ * reason).
73
+ */
74
+ const DECLARED_SOURCES = [
75
+ 'ci.local_command (config file)', 'run config', 'Makefile ci target', 'package.json test script',
76
+ ];
77
+
56
78
  /**
57
79
  * HC-COMM-010-followup-7: tiered resolution. Without `opts.cwd` this is the
58
80
  * original pure function (explicit → configured → `make ci`). With `opts.cwd`:
@@ -63,11 +85,21 @@ function _npmTestScript(cwd) {
63
85
  * 4. `make ci` if the Makefile has a ci target
64
86
  * 5. `npm test` if package.json has a real test script
65
87
  * 6. `make ci` (documented default; fails loudly)
66
- * @returns {{command:string, source:string}}
88
+ * @returns {{command:string, source:string, replaced?:{command:string,source:string}|null}}
89
+ * `replaced` (HC-GF-223) is what an explicit --command displaced, when that was a declaration.
67
90
  */
68
91
  function resolveCommandDetailed(explicit, configured, opts = {}) {
69
92
  const e = (typeof explicit === 'string' ? explicit : '').trim();
70
- if (e) return { command: e, source: '--command' };
93
+ if (e) {
94
+ // What `--command` replaced. An operator narrows this command because the configured one cannot
95
+ // run in the worktree verify-patch creates, and a subset that passes is not a verification of
96
+ // the change — so the caller needs to know what did NOT run, not just what did.
97
+ const base = resolveCommandDetailed(null, configured, opts);
98
+ const replaced = DECLARED_SOURCES.includes(base.source) && base.command !== e
99
+ ? { command: base.command, source: base.source }
100
+ : null;
101
+ return { command: e, source: '--command', replaced };
102
+ }
71
103
  const cwd = opts && opts.cwd;
72
104
  if (cwd) {
73
105
  try {
@@ -145,10 +177,15 @@ function commandAbsence(command, { code, stdout = '', stderr = '' } = {}) {
145
177
  * enforce approves the gate.
146
178
  * The verified command is the adopter's own top-level invocation: level 0, no inherited flags. Returns a
147
179
  * copy; the caller's environment is untouched.
180
+ *
181
+ * HC-GF-269: and none of Hone's own variables. The command runs the builder's patch and tests, so it
182
+ * gets no `HONE_*` credential or endpoint (see untrusted-code-env.js). The scrub runs LAST, after the
183
+ * caller has merged in whatever it publishes on purpose (HC-GF-192's `plan.env`, HC-GF-193's
184
+ * `plan.controlEnv`) — so a sandbox cannot be used to hand a Hone credential to generated code either.
148
185
  */
149
186
  const INHERITED_MAKE_VARS = ['MAKELEVEL', 'MAKEFLAGS', 'GNUMAKEFLAGS'];
150
187
  function verifyCommandEnv(base = process.env) {
151
- const env = { ...base };
188
+ const env = withoutHoneVars(base);
152
189
  for (const key of INHERITED_MAKE_VARS) delete env[key];
153
190
  return env;
154
191
  }
@@ -162,11 +199,43 @@ function verifyCommandEnv(base = process.env) {
162
199
  * @param {{ code: number|null, timedOut: boolean, stdout?: string, stderr?: string, command?: string, source?: string }} run
163
200
  * @returns {{ verdict: 'pass'|'fail'|'timeout'|'unresolved', exitCode: number, detail: string }}
164
201
  */
165
- function verdictFromRun(run) {
202
+ function verdictFromRun(run, provisioning = null) {
203
+ // HC-GF-201: provisioning state is an INPUT to the verdict, not a side note. The
204
+ // run that exposed this printed `provisioning: linked node_modules` honestly and
205
+ // then blamed the patch for the roots it had not linked, because this function had
206
+ // no parameter for it and the `ok:false` was discarded before the verdict was computed.
207
+ if (provisioning && provisioning.ok === false) {
208
+ const roots = (provisioning.unprovisioned || []).map((u) => u.root).join(', ');
209
+ return {
210
+ verdict: 'not_run',
211
+ exitCode: EXIT.NOT_RUN,
212
+ detail: `environment incomplete — could not provision: ${roots}. `
213
+ + 'Nothing was observed about this change.',
214
+ };
215
+ }
166
216
  if (run.timedOut) {
167
217
  return { verdict: 'timeout', exitCode: EXIT.TIMEOUT, detail: 'test command exceeded the timeout' };
168
218
  }
169
219
  if (run.code === 0) {
220
+ // HC-GF-223: a pass under an operator-supplied command is evidence about a SUBSET. Measured on
221
+ // run `437246a7`: `--command "node --test <3 files>"` reported `pass` to the server and the same
222
+ // diff then failed `make ci` with 6 failures, from Redis state the subset could not see. The
223
+ // enforce gate (HC-GF-099) was satisfied by a verification that examined a third of the surface.
224
+ //
225
+ // The EXIT CODE stays PASS on purpose: the tests that ran did pass, and changing what exit 0
226
+ // means would silently change the contract for every adopter and every chained command (the same
227
+ // argument HC-GF-026 makes for lint). The VERDICT is what the server gates on, and both readers
228
+ // — `orchestrate.js` (`isGreen = verdict === 'pass'`) and `verification-refusal.js` — treat any
229
+ // other value as not green, so naming it here is what closes the false pass.
230
+ if (run.replaced && run.replaced.command) {
231
+ return {
232
+ verdict: 'pass_narrowed',
233
+ exitCode: EXIT.PASS,
234
+ detail: `tests passed under an operator-supplied command, but \`${run.replaced.command}\` `
235
+ + `(${run.replaced.source}) did not run — this is evidence about a subset, not a `
236
+ + 'verification of the change',
237
+ };
238
+ }
170
239
  return { verdict: 'pass', exitCode: EXIT.PASS, detail: 'tests passed on the applied diff' };
171
240
  }
172
241
  const absent = commandAbsence(run.command, run);
@@ -348,6 +417,7 @@ function classifyGateRun(run) {
348
417
  }
349
418
 
350
419
  module.exports = {
420
+ DECLARED_SOURCES,
351
421
  EXIT, resolveCommand, resolveCommandDetailed, DEFAULT_COMMAND,
352
422
  AUX_GATES, AUX_DETECT, resolveAuxGates, classifyGateRun,
353
423
  verdictFromRun, commandAbsence, verifyCommandEnv, lastMeaningfulLine, summarizeFailure, FAILURE_LINE_RE,
package/lib/verify-pr.js CHANGED
@@ -21,11 +21,12 @@
21
21
 
22
22
  /** Exit codes, extending the check/verify-patch convention. */
23
23
  const EXIT = {
24
- PASS: 0, // all required CI gates green (skill audit is informational)
24
+ PASS: 0, // all required CI gates green, and no BLOCKER skill assertion fired
25
25
  NO_PR: 1, // could not resolve a PR / branch to verify
26
26
  GATE_RED: 2, // a required CI gate is failing
27
27
  GATE_PENDING: 3, // checks did not reach a terminal state within the timeout
28
28
  ERROR: 4, // gh / command execution error
29
+ SKILL_BLOCKER: 5, // CI passed, but a BLOCKER skill assertion fired in `hone step-5b` (HC-GF-227)
29
30
  };
30
31
 
31
32
  const GATE_MODES = ['github', 'local', 'both', 'none'];
@@ -13,6 +13,7 @@
13
13
  */
14
14
  const fs = require('node:fs');
15
15
  const path = require('node:path');
16
+ const { withoutHoneVars } = require('./untrusted-code-env');
16
17
 
17
18
  const LOCKFILES = [
18
19
  ['package-lock.json', 'npm ci --no-audit --no-fund'],
@@ -104,7 +105,7 @@ function planProvision({ worktreeDir, parentDir, installCommand = null, fsMod =
104
105
  }
105
106
 
106
107
  /** @returns {{ok:boolean, did:'none'|'link'|'install', summary:string, command?:string}} */
107
- function provisionWorktree(plan, { worktreeDir, exec, fsMod = fs }) {
108
+ function provisionWorktree(plan, { worktreeDir, exec, fsMod = fs, env = process.env }) {
108
109
  if (!plan || plan.kind === 'none') return { ok: true, did: 'none', summary: `skipped (${plan ? plan.reason : 'no plan'})` };
109
110
  // HC-GF-024: recognised-but-unautomated. ok:true keeps the fail-open contract (the
110
111
  // test command still runs), but the summary names the manifest so a later
@@ -120,14 +121,73 @@ function provisionWorktree(plan, { worktreeDir, exec, fsMod = fs }) {
120
121
  } catch (e) { return { ok: false, did: 'link', summary: `link failed (${e.message}) — tests run unprovisioned` }; }
121
122
  }
122
123
  try {
123
- const run = exec(plan.command, { cwd: worktreeDir, shell: true, encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'pipe'] });
124
+ // HC-GF-269: the install runs the PATCHED manifest, so a `postinstall` step_4 wrote runs here —
125
+ // before the test command, and whether or not it passes. It keeps the operator's toolchain
126
+ // (proxy, registry, virtualenv: HC-GF-144) and loses only Hone's own `HONE_*` variables.
127
+ const run = exec(plan.command, { cwd: worktreeDir, shell: true, encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'pipe'], env: withoutHoneVars(env) });
124
128
  const ok = run && run.status === 0;
125
129
  const tail = ok ? '' : ` — ${String((run && (run.stderr || run.stdout)) || '').trim().split('\n').filter(Boolean).pop() || 'install failed'}`;
126
130
  return { ok, did: 'install', command: plan.command, summary: `${ok ? 'ran' : 'FAILED'} \`${plan.command}\` (${plan.reason})${tail}` };
127
131
  } catch (e) { return { ok: false, did: 'install', command: plan.command, summary: `install threw (${e.message}) — tests run unprovisioned` }; }
128
132
  }
129
133
 
134
+
135
+ /**
136
+ * Every directory in `dir` that owns a package manifest — HC-GF-201.
137
+ *
138
+ * `planProvision` tests exactly one directory, and this repo has three roots:
139
+ * `node_modules`, `cli/node_modules`, `server/node_modules`. The two it skipped are
140
+ * where `ci.unit` runs (`node --test cli/lib/*.test.js`), so a worktree built by
141
+ * `verify-patch` ran those tests against absent modules and the resulting
142
+ * `Cannot find module` lines were counted as the PATCH failing tests.
143
+ *
144
+ * Bounded depth on purpose: a manifest inside `node_modules` is a dependency's, not
145
+ * a root of the repo under test.
146
+ */
147
+ function discoverPackageRoots(dir, { fsMod = fs, maxDepth = 3 } = {}) {
148
+ const SKIP = new Set(['node_modules', '.git', '.hg', 'dist', 'build', 'coverage', 'vendor']);
149
+ const out = [];
150
+ const walk = (rel, depth) => {
151
+ const abs = rel === '.' ? dir : path.join(dir, rel);
152
+ let entries = [];
153
+ try { entries = fsMod.readdirSync(abs, { withFileTypes: true }); } catch { return; }
154
+ if (entries.some((e) => e.isFile() && e.name === 'package.json')) out.push(rel);
155
+ if (depth >= maxDepth) return;
156
+ for (const e of entries) {
157
+ if (!e.isDirectory() || SKIP.has(e.name) || e.name.startsWith('.')) continue;
158
+ walk(rel === '.' ? e.name : path.join(rel, e.name), depth + 1);
159
+ }
160
+ };
161
+ walk('.', 0);
162
+ return out;
163
+ }
164
+
165
+ /**
166
+ * Provision every discovered root, reporting the ones we MEANT to provision and could not.
167
+ *
168
+ * The distinction is load-bearing and was named in this story's step_1 plan: `kind:'none'`
169
+ * (no manifest) and `kind:'unsupported'` (Python/Go/Rust — deliberately not automated) mean
170
+ * there is nothing to do, NOT that provisioning failed. Counting them would make every
171
+ * non-Node adopter's every run report NOT RUN forever.
172
+ */
173
+ function provisionAllRoots(entries, { provision = provisionWorktree } = {}) {
174
+ const results = [];
175
+ const unprovisioned = [];
176
+ for (const { root, plan, worktreeDir, exec } of entries) {
177
+ if (plan.kind === 'none' || plan.kind === 'unsupported') {
178
+ results.push({ root, did: plan.kind, summary: plan.reason || plan.kind });
179
+ continue;
180
+ }
181
+ const r = provision(plan, { worktreeDir, exec, root });
182
+ results.push({ root, did: r.did || 'failed', summary: r.summary });
183
+ if (!r.ok) unprovisioned.push({ root, reason: r.summary || 'provisioning failed' });
184
+ }
185
+ return { ok: unprovisioned.length === 0, results, unprovisioned };
186
+ }
187
+
130
188
  module.exports = {
189
+ discoverPackageRoots,
190
+ provisionAllRoots,
131
191
  planProvision, provisionWorktree,
132
192
  LOCKFILES, RECOGNISED_MANIFESTS, ALL_MANIFEST_NAMES, INSTALL_TIMEOUT_MS,
133
193
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hone-ai/cli",
3
- "version": "1.34.0",
3
+ "version": "1.35.1",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {