@hone-ai/cli 1.18.0 → 1.20.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/bin/hone-mcp.js +9 -0
- package/hone-cli.js +2171 -82
- package/lib/agent-eval-judge.js +60 -0
- package/lib/agent-eval-probes-adversarial.js +45 -0
- package/lib/agent-eval-probes-boundary.js +0 -0
- package/lib/agent-eval-probes-faithfulness.js +59 -0
- package/lib/agent-eval-probes-safety.js +28 -0
- package/lib/agent-executor.js +139 -0
- package/lib/architect-config.js +121 -0
- package/lib/bundle-paths.js +141 -0
- package/lib/ci-gate-chooser.js +267 -0
- package/lib/doctor-admin-merge.js +3 -2
- package/lib/doctor-architecture.js +213 -0
- package/lib/emit-pr.js +167 -0
- package/lib/eval-evidence.js +213 -0
- package/lib/eval-graders.js +98 -1
- package/lib/git-env.js +94 -0
- package/lib/git-helpers.js +3 -2
- package/lib/judge-provider.js +63 -0
- package/lib/materialize-diff.js +345 -0
- package/lib/mcp-tools.js +310 -0
- package/lib/patch-apply.js +108 -0
- package/lib/pipeline-config.js +300 -0
- package/lib/pipeline-status.js +27 -2
- package/lib/refresh-knowledge.js +7 -0
- package/lib/release-review-cache.js +162 -0
- package/lib/release-review-config.js +4 -2
- package/lib/schedule-cron.js +141 -0
- package/lib/skill-eval-runner.js +667 -0
- package/lib/stack-paths.js +84 -6
- package/lib/story-classifier-extract.js +2 -1
- package/lib/verify-patch.js +78 -0
- package/lib/verify-pr.js +141 -0
- package/mcp-server.js +67 -0
- package/package.json +8 -5
package/lib/stack-paths.js
CHANGED
|
@@ -235,10 +235,72 @@ function parsePlatformConfigPaths(yamlText) {
|
|
|
235
235
|
return out;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Parse adopter-discovered `platform.metadata_types.config[].path` entries
|
|
240
|
+
* from a `.pipeline-config.yml` text. HC-018: closes the classifier loop
|
|
241
|
+
* by feeding setup-time-discovered CONFIG metadata locations into
|
|
242
|
+
* `classifyPathsForRepo` so adopter platforms (Salesforce, NetSuite, dbt,
|
|
243
|
+
* Terraform, ...) classify as CONFIG without needing to be hardcoded
|
|
244
|
+
* into STACK_PATH_TABLE.
|
|
245
|
+
*
|
|
246
|
+
* The YAML shape emitted by cli/lib/config-augment.js is:
|
|
247
|
+
* platform:
|
|
248
|
+
* metadata_types:
|
|
249
|
+
* config:
|
|
250
|
+
* - { type: customObjects, path: "force-app/main/default/objects/", count: 18 }
|
|
251
|
+
*
|
|
252
|
+
* Some discover paths are comma-joined multi-paths
|
|
253
|
+
* (e.g. "force-app/main/default/classes, force-app/main/default/triggers")
|
|
254
|
+
* — those are split into individual entries.
|
|
255
|
+
*
|
|
256
|
+
* Returned values are raw filesystem paths (NOT regex). The caller
|
|
257
|
+
* anchors + escapes them before unioning into `extraConfigPaths`.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} yamlText
|
|
260
|
+
* @returns {string[]} raw path strings discovered under metadata_types.config[]
|
|
261
|
+
*/
|
|
262
|
+
function parsePlatformMetadataTypesConfigPaths(yamlText) {
|
|
263
|
+
if (typeof yamlText !== 'string' || yamlText.length === 0) return [];
|
|
264
|
+
const blockMatch = yamlText.match(/^platform:\s*\n((?:[ \t]+.*\n?)*)/m);
|
|
265
|
+
if (!blockMatch) return [];
|
|
266
|
+
const block = blockMatch[1];
|
|
267
|
+
// metadata_types: <body> — body lines indented strictly more than the key
|
|
268
|
+
const mtMatch = block.match(/^([ \t]*)metadata_types:\s*\n((?:\1[ \t]+.*\n?)*)/m);
|
|
269
|
+
if (!mtMatch) return [];
|
|
270
|
+
const mtBody = mtMatch[2];
|
|
271
|
+
// config: <body> under metadata_types — same indent-bounded scan
|
|
272
|
+
const cfgMatch = mtBody.match(/^([ \t]*)config:\s*\n((?:\1[ \t]+.*\n?)*)/m);
|
|
273
|
+
if (!cfgMatch) return [];
|
|
274
|
+
const cfgBody = cfgMatch[2];
|
|
275
|
+
// Match each inline-mapping item: `- { ..., path: "VALUE", ... }`
|
|
276
|
+
// (also accepts single-quoted and bare-token paths)
|
|
277
|
+
const out = [];
|
|
278
|
+
const re = /-\s*\{[^}]*\bpath:\s*(?:"([^"]+)"|'([^']+)'|([^,}\s]+))/g;
|
|
279
|
+
let m;
|
|
280
|
+
while ((m = re.exec(cfgBody)) !== null) {
|
|
281
|
+
const raw = (m[1] || m[2] || m[3] || '').trim();
|
|
282
|
+
if (!raw) continue;
|
|
283
|
+
// Discover code sometimes joins multiple paths with ", " — split them.
|
|
284
|
+
for (const piece of raw.split(/\s*,\s*/)) {
|
|
285
|
+
const v = piece.trim();
|
|
286
|
+
if (v) out.push(v);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Escape a literal filesystem path so it can be used as a RegExp pattern.
|
|
294
|
+
*/
|
|
295
|
+
function escapeRegexLiteral(s) {
|
|
296
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
297
|
+
}
|
|
298
|
+
|
|
238
299
|
/**
|
|
239
300
|
* Higher-level classifier composing platform fingerprint detection +
|
|
240
|
-
* adopter-supplied `.pipeline-config.yml platform.config_paths` +
|
|
241
|
-
*
|
|
301
|
+
* adopter-supplied `.pipeline-config.yml platform.config_paths` +
|
|
302
|
+
* setup-discovered `platform.metadata_types.config[].path` (HC-018) +
|
|
303
|
+
* the pure `classifyPathsForStack` core.
|
|
242
304
|
*
|
|
243
305
|
* Pure-helper-with-injected-I/O shape (same as compliance-check.js):
|
|
244
306
|
* caller wraps `fs.existsSync` and `fs.readFileSync` so the helper can
|
|
@@ -249,7 +311,12 @@ function parsePlatformConfigPaths(yamlText) {
|
|
|
249
311
|
* @param {string} opts.repoRoot - absolute path to repo root
|
|
250
312
|
* @param {(relativePath: string) => boolean} [opts.fileExists] - filesystem check
|
|
251
313
|
* @param {(relativePath: string) => string|null} [opts.readFile] - file reader
|
|
252
|
-
* @returns {{
|
|
314
|
+
* @returns {{
|
|
315
|
+
* category: string,
|
|
316
|
+
* detectedPlatforms: string[],
|
|
317
|
+
* configPathsUsed: string[],
|
|
318
|
+
* metadataConfigPathsUsed: string[]
|
|
319
|
+
* }}
|
|
253
320
|
*/
|
|
254
321
|
function classifyPathsForRepo(opts = {}) {
|
|
255
322
|
const { paths, repoRoot, fileExists, readFile } = opts;
|
|
@@ -261,19 +328,28 @@ function classifyPathsForRepo(opts = {}) {
|
|
|
261
328
|
detectedPlatforms = detectPlatforms({ repoRoot, fileExists });
|
|
262
329
|
} catch { /* defensive — if the module is unavailable, treat as no platforms */ }
|
|
263
330
|
|
|
264
|
-
// Read adopter-supplied platform.config_paths
|
|
331
|
+
// Read adopter-supplied platform.config_paths + discovered metadata_types
|
|
332
|
+
// from .pipeline-config.yml (best-effort)
|
|
265
333
|
let configPathsUsed = [];
|
|
334
|
+
let metadataConfigPathsUsed = [];
|
|
266
335
|
try {
|
|
267
336
|
if (typeof readFile === 'function') {
|
|
268
337
|
const yamlText = readFile('.pipeline-config.yml');
|
|
269
338
|
if (typeof yamlText === 'string' && yamlText.length > 0) {
|
|
270
339
|
configPathsUsed = parsePlatformConfigPaths(yamlText);
|
|
340
|
+
metadataConfigPathsUsed = parsePlatformMetadataTypesConfigPaths(yamlText);
|
|
271
341
|
}
|
|
272
342
|
}
|
|
273
343
|
} catch { /* defensive — bad YAML / missing file is a no-op */ }
|
|
274
344
|
|
|
275
|
-
|
|
276
|
-
|
|
345
|
+
// metadata_types paths are raw filesystem prefixes — anchor + escape
|
|
346
|
+
// so a discovered `force-app/main/default/objects/` is matched as a
|
|
347
|
+
// path prefix, not as a free-form regex.
|
|
348
|
+
const metadataPatterns = metadataConfigPathsUsed.map(p => '^' + escapeRegexLiteral(p));
|
|
349
|
+
const extraConfigPaths = configPathsUsed.concat(metadataPatterns);
|
|
350
|
+
|
|
351
|
+
const category = classifyPathsForStack(paths, { extraConfigPaths });
|
|
352
|
+
return { category, detectedPlatforms, configPathsUsed, metadataConfigPathsUsed };
|
|
277
353
|
}
|
|
278
354
|
|
|
279
355
|
module.exports = {
|
|
@@ -282,4 +358,6 @@ module.exports = {
|
|
|
282
358
|
classifyPathsForStack,
|
|
283
359
|
classifyPathsForRepo,
|
|
284
360
|
parsePlatformConfigPaths,
|
|
361
|
+
parsePlatformMetadataTypesConfigPaths,
|
|
362
|
+
escapeRegexLiteral,
|
|
285
363
|
};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const { execSync } = require('node:child_process');
|
|
22
|
+
const { gitEnv } = require('./git-env');
|
|
22
23
|
const fs = require('node:fs');
|
|
23
24
|
const path = require('node:path');
|
|
24
25
|
|
|
@@ -181,7 +182,7 @@ function extractRecentlyModifiedOverlap(repoRoot, touchedFiles, windowDays) {
|
|
|
181
182
|
try {
|
|
182
183
|
const out = execSync(
|
|
183
184
|
`git log --since="${w} days ago" --format=%H -- ${JSON.stringify(rel)}`,
|
|
184
|
-
{ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
185
|
+
{ cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
185
186
|
).trim();
|
|
186
187
|
if (out) return true;
|
|
187
188
|
} catch {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* verify-patch.js — HC-019n-followup-24 (pipeline-recovery condition 6).
|
|
4
|
+
*
|
|
5
|
+
* Condition 5 proved step_4's diff APPLIES. Condition 6 runs the adopter's test
|
|
6
|
+
* command against the applied diff and reports pass/fail — the first ground
|
|
7
|
+
* truth in the pipeline that the generated code does not BREAK the suite.
|
|
8
|
+
*
|
|
9
|
+
* The command runs inside a throwaway git WORKTREE checked out from HEAD, so the
|
|
10
|
+
* adopter's real tree (including uncommitted work) is never touched. This module
|
|
11
|
+
* is the pure half — resolving the command and interpreting an exit code into a
|
|
12
|
+
* verdict. The worktree lifecycle and process exec live in the CLI command,
|
|
13
|
+
* calling verdictFromRun() with the result, so this stays I/O-free and
|
|
14
|
+
* unit-testable.
|
|
15
|
+
*
|
|
16
|
+
* See .github/pipeline/HC-019n-followup-24/architect.md. NOTE the honest limit:
|
|
17
|
+
* a passing suite means "does not regress", NOT "the story is correctly
|
|
18
|
+
* implemented" — the existing tests may not cover the new behaviour.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Exit codes, matching the check-patch convention so scripts can branch. */
|
|
22
|
+
const EXIT = {
|
|
23
|
+
PASS: 0, // tests passed on the applied diff
|
|
24
|
+
NO_DIFF: 1, // no diff / fetch failure (condition-5 precondition unmet)
|
|
25
|
+
DOES_NOT_APPLY: 2, // diff did not apply
|
|
26
|
+
TESTS_FAILED: 3, // diff applied, tests failed
|
|
27
|
+
TIMEOUT: 4, // the command exceeded --timeout
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve which command to run.
|
|
32
|
+
*
|
|
33
|
+
* Priority: an explicit --command wins; else the adopter's configured
|
|
34
|
+
* ci.local_command; else the documented default `make ci`. Never an empty
|
|
35
|
+
* string (an empty command would "succeed" vacuously and report a false pass).
|
|
36
|
+
*
|
|
37
|
+
* @param {string|undefined} explicit --command flag
|
|
38
|
+
* @param {string|undefined} configured ci_local_command from run config
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function resolveCommand(explicit, configured) {
|
|
42
|
+
const pick = (explicit && explicit.trim())
|
|
43
|
+
|| (configured && configured.trim())
|
|
44
|
+
|| 'make ci';
|
|
45
|
+
return pick;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Interpret a completed command run into a condition-6 verdict.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ code: number|null, timedOut: boolean, stdout?: string, stderr?: string }} run
|
|
52
|
+
* @returns {{ verdict: 'pass'|'fail'|'timeout', exitCode: number, detail: string }}
|
|
53
|
+
*/
|
|
54
|
+
function verdictFromRun(run) {
|
|
55
|
+
if (run.timedOut) {
|
|
56
|
+
return { verdict: 'timeout', exitCode: EXIT.TIMEOUT, detail: 'test command exceeded the timeout' };
|
|
57
|
+
}
|
|
58
|
+
if (run.code === 0) {
|
|
59
|
+
return { verdict: 'pass', exitCode: EXIT.PASS, detail: 'tests passed on the applied diff' };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
verdict: 'fail',
|
|
63
|
+
exitCode: EXIT.TESTS_FAILED,
|
|
64
|
+
detail: `tests failed (exit ${run.code})` + (lastMeaningfulLine(run) ? `: ${lastMeaningfulLine(run)}` : ''),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The most useful one-liner from a failed run's output, for the report. */
|
|
69
|
+
function lastMeaningfulLine({ stdout = '', stderr = '' }) {
|
|
70
|
+
const combined = `${stdout}\n${stderr}`.split('\n')
|
|
71
|
+
.map(l => l.trim())
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
// Prefer a line that names a failure; else the last non-empty line.
|
|
74
|
+
const failLine = combined.reverse().find(l => /fail|error|✗|not ok|assert/i.test(l));
|
|
75
|
+
return (failLine || combined[0] || '').slice(0, 200);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { EXIT, resolveCommand, verdictFromRun, lastMeaningfulLine };
|
package/lib/verify-pr.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* verify-pr.js — HC-019n-followup-32 (pipeline-standards G4 §2, G3 functional half).
|
|
4
|
+
*
|
|
5
|
+
* The POST-emit-pr verification the recovery deferred. `check-patch` (cond 5) →
|
|
6
|
+
* `verify-patch` (cond 6, runs the tests) → `emit-pr` (cond 7, opens the PR) →
|
|
7
|
+
* **`verify-pr`** (this): once a real PR exists, run the real skill audit and the
|
|
8
|
+
* real CI gate against it — the two artifacts (step-5b-skill-audit.md,
|
|
9
|
+
* step-5c-ci.md) the server DAG can only MODEL, never execute (see
|
|
10
|
+
* HC-019n-followup-31: those steps are serverExecutable:false).
|
|
11
|
+
*
|
|
12
|
+
* This module is the PURE half — CI-gate mode resolution, `gh pr checks`
|
|
13
|
+
* interpretation, and the verdict — I/O-free and unit-testable. The gh/make-ci
|
|
14
|
+
* execution + artifact writes live in the CLI command.
|
|
15
|
+
*
|
|
16
|
+
* Unlike the code-reviewer prompt's step_5c (which DIAGNOSES + auto-fixes CI in a
|
|
17
|
+
* loop), verify-pr only VERIFIES and REPORTS a verdict — a CLI command states the
|
|
18
|
+
* truth; the operator (or a later closed loop) acts on it. NOTE the honest limit:
|
|
19
|
+
* a green CI gate means the checks passed, not that the change is correct.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Exit codes, extending the check/verify-patch convention. */
|
|
23
|
+
const EXIT = {
|
|
24
|
+
PASS: 0, // all required CI gates green (skill audit is informational)
|
|
25
|
+
NO_PR: 1, // could not resolve a PR / branch to verify
|
|
26
|
+
GATE_RED: 2, // a required CI gate is failing
|
|
27
|
+
GATE_PENDING: 3, // checks did not reach a terminal state within the timeout
|
|
28
|
+
ERROR: 4, // gh / command execution error
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const GATE_MODES = ['github', 'local', 'both', 'none'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the CI-gate mode. Precedence: explicit --mode > config ci.gate >
|
|
35
|
+
* default 'github' (backward compat, matching the step_5c prompt contract).
|
|
36
|
+
*/
|
|
37
|
+
function resolveGateMode(explicit, configGate) {
|
|
38
|
+
const norm = (v) => {
|
|
39
|
+
const s = String(v == null ? '' : v).toLowerCase().trim();
|
|
40
|
+
return GATE_MODES.includes(s) ? s : null;
|
|
41
|
+
};
|
|
42
|
+
return norm(explicit) || norm(configGate) || 'github';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Interpret `gh pr checks <PR> --json name,state,bucket` output (an array).
|
|
47
|
+
* `bucket` is gh's own classification: pass | fail | pending | skipping | cancel.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<{name?:string, bucket?:string, state?:string, link?:string}>} rows
|
|
50
|
+
* @returns {{ total:number, passed:number, failed:number, pending:number,
|
|
51
|
+
* skipped:number, failures:string[], allTerminal:boolean, allGreen:boolean }}
|
|
52
|
+
*/
|
|
53
|
+
function interpretGhChecks(rows) {
|
|
54
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
55
|
+
let passed = 0, failed = 0, pending = 0, skipped = 0;
|
|
56
|
+
const failures = [];
|
|
57
|
+
for (const r of list) {
|
|
58
|
+
const bucket = String(r.bucket || r.state || '').toLowerCase();
|
|
59
|
+
if (bucket === 'pass') passed++;
|
|
60
|
+
else if (bucket === 'fail' || bucket === 'cancel') { failed++; failures.push(r.name || '(unnamed check)'); }
|
|
61
|
+
else if (bucket === 'skipping' || bucket === 'skipped') skipped++;
|
|
62
|
+
else pending++; // pending / queued / in_progress / unknown
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
total: list.length,
|
|
66
|
+
passed, failed, pending, skipped, failures,
|
|
67
|
+
allTerminal: pending === 0,
|
|
68
|
+
// Green = at least one check, none failed, none still pending.
|
|
69
|
+
allGreen: list.length > 0 && failed === 0 && pending === 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compute the overall CI-gate verdict from the per-mode results.
|
|
75
|
+
*
|
|
76
|
+
* @param {{ mode:string, gh?:object|null, local?:{code:number,timedOut:boolean}|null }} o
|
|
77
|
+
* @returns {{ verdict:'pass'|'fail'|'pending'|'disabled', exitCode:number, detail:string }}
|
|
78
|
+
*/
|
|
79
|
+
function ciGateVerdict({ mode, gh, local }) {
|
|
80
|
+
if (mode === 'none') {
|
|
81
|
+
return { verdict: 'disabled', exitCode: EXIT.PASS, detail: 'ci.gate=none — CI verification explicitly disabled (audit row only)' };
|
|
82
|
+
}
|
|
83
|
+
const parts = [];
|
|
84
|
+
let anyFail = false, anyPending = false;
|
|
85
|
+
|
|
86
|
+
if (mode === 'github' || mode === 'both') {
|
|
87
|
+
if (!gh) { anyFail = true; parts.push('github: could not read checks'); }
|
|
88
|
+
else if (gh.failed > 0) { anyFail = true; parts.push(`github: ${gh.failed}/${gh.total} failing (${gh.failures.join(', ')})`); }
|
|
89
|
+
else if (!gh.allTerminal) { anyPending = true; parts.push(`github: ${gh.pending} check(s) still pending`); }
|
|
90
|
+
else if (gh.total === 0) { anyPending = true; parts.push('github: no checks reported yet'); }
|
|
91
|
+
else parts.push(`github: ${gh.passed}/${gh.total} green`);
|
|
92
|
+
}
|
|
93
|
+
if (mode === 'local' || mode === 'both') {
|
|
94
|
+
if (!local) { anyFail = true; parts.push('local: command did not run'); }
|
|
95
|
+
else if (local.timedOut) { anyPending = true; parts.push('local: command timed out'); }
|
|
96
|
+
else if (local.code !== 0) { anyFail = true; parts.push(`local: \`make ci\` exit ${local.code}`); }
|
|
97
|
+
else parts.push('local: passed');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (anyFail) return { verdict: 'fail', exitCode: EXIT.GATE_RED, detail: parts.join(' | ') };
|
|
101
|
+
if (anyPending) return { verdict: 'pending', exitCode: EXIT.GATE_PENDING, detail: parts.join(' | ') };
|
|
102
|
+
return { verdict: 'pass', exitCode: EXIT.PASS, detail: parts.join(' | ') };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Render the step-5c-ci.md gate matrix with a truthful provenance column.
|
|
107
|
+
* @returns {string} markdown
|
|
108
|
+
*/
|
|
109
|
+
function renderStep5cArtifact({ storyId, mode, prRef, gh, local, verdict }) {
|
|
110
|
+
const lines = [];
|
|
111
|
+
lines.push(`# Step 5c — CI Gate Verification (${storyId || 'unknown-story'})`);
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push(`- **PR / branch:** ${prRef || '(unknown)'}`);
|
|
114
|
+
lines.push(`- **CI gate mode:** ${mode}`);
|
|
115
|
+
lines.push(`- **Verdict:** ${verdict.verdict.toUpperCase()} — ${verdict.detail}`);
|
|
116
|
+
lines.push(`- **Produced by:** \`hone verify-pr\` (real execution — NOT an LLM description; HC-019n-followup-32)`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('| Check | Result | Provenance |');
|
|
119
|
+
lines.push('|---|---|---|');
|
|
120
|
+
if (mode === 'none') {
|
|
121
|
+
lines.push('| (CI verification) | DISABLED | ci.gate=none |');
|
|
122
|
+
}
|
|
123
|
+
if ((mode === 'github' || mode === 'both') && gh) {
|
|
124
|
+
if (gh.total === 0) lines.push('| (github checks) | none reported | github |');
|
|
125
|
+
else {
|
|
126
|
+
lines.push(`| github checks | ${gh.passed} pass / ${gh.failed} fail / ${gh.pending} pending / ${gh.skipped} skip | github |`);
|
|
127
|
+
for (const f of gh.failures) lines.push(`| ❌ ${f} | FAIL | github |`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if ((mode === 'local' || mode === 'both')) {
|
|
131
|
+
if (!local) lines.push('| local `make ci` | did not run | local |');
|
|
132
|
+
else lines.push(`| local \`make ci\` | ${local.timedOut ? 'TIMEOUT' : local.code === 0 ? 'PASS' : `FAIL (exit ${local.code})`} | ${local.code === 0 ? 'OPERATOR-VERIFIED-LOCAL' : 'local'} |`);
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('> Honest limit: a green CI gate means the checks passed, not that the change is correct.');
|
|
136
|
+
return lines.join('\n') + '\n';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
EXIT, GATE_MODES, resolveGateMode, interpretGhChecks, ciGateVerdict, renderStep5cArtifact,
|
|
141
|
+
};
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* mcp-server.js — HC-COMM-012 (A1 P1): the Hone MCP server (stdio).
|
|
4
|
+
*
|
|
5
|
+
* A THIN bridge: it exposes the pipeline tool handlers (lib/mcp-tools.js, which shell
|
|
6
|
+
* out to the `hone` CLI) over the Model Context Protocol so Claude Code (and later
|
|
7
|
+
* any MCP editor) can drive the server-mode pipeline. It contains NO pipeline logic,
|
|
8
|
+
* NO prompts, NO agent chains — the moat stays server-side (architect memo HC-COMM-012).
|
|
9
|
+
*
|
|
10
|
+
* stdout is the MCP protocol channel — NEVER write logs to it; use stderr.
|
|
11
|
+
*/
|
|
12
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
13
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
14
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
15
|
+
const { TOOLS, readHoneConfig } = require('./lib/mcp-tools');
|
|
16
|
+
|
|
17
|
+
function buildServer() {
|
|
18
|
+
let version = '0.0.0';
|
|
19
|
+
try { version = require('./package.json').version; } catch { /* keep default */ }
|
|
20
|
+
const server = new Server({ name: 'hone', version }, { capabilities: { tools: {} } });
|
|
21
|
+
|
|
22
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
23
|
+
tools: TOOLS.map((t) => ({
|
|
24
|
+
name: t.name,
|
|
25
|
+
description: t.description,
|
|
26
|
+
inputSchema: t.inputSchema,
|
|
27
|
+
annotations: { readOnlyHint: !!t.readOnly },
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
32
|
+
const tool = TOOLS.find((t) => t.name === req.params.name);
|
|
33
|
+
if (!tool) {
|
|
34
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }], isError: true };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const result = await tool.handler(req.params.arguments || {});
|
|
38
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError: !!result.isError };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { content: [{ type: 'text', text: `Tool error: ${e && e.message ? e.message : String(e)}` }], isError: true };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return server;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function start() {
|
|
48
|
+
const server = buildServer();
|
|
49
|
+
const transport = new StdioServerTransport();
|
|
50
|
+
await server.connect(transport);
|
|
51
|
+
// Preflight (stderr only — stdout is the protocol channel). Editor-agnostic: this
|
|
52
|
+
// is the same signal in Claude Code, Cursor, Windsurf, or any MCP client.
|
|
53
|
+
const cfg = readHoneConfig();
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`hone MCP server ready — api=${cfg.apiUrl}, token=${cfg.hasToken ? 'present' : 'MISSING (run `hone init --token <t>`)'}, ${TOOLS.length} tools. ` +
|
|
56
|
+
`Run hone_doctor if calls fail.\n`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (require.main === module) {
|
|
61
|
+
start().catch((e) => {
|
|
62
|
+
process.stderr.write(`hone MCP server failed to start: ${e && e.message ? e.message : String(e)}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { buildServer, start };
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hone-ai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Hone AI — Enterprise SDLC Pipeline CLI",
|
|
5
5
|
"main": "hone-cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"hone": "./bin/hone.js"
|
|
7
|
+
"hone": "./bin/hone.js",
|
|
8
|
+
"hone-mcp": "./bin/hone-mcp.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin/",
|
|
11
12
|
"hone-cli.js",
|
|
12
13
|
"lib/",
|
|
13
14
|
"!lib/*.test.js",
|
|
14
|
-
"schema/"
|
|
15
|
+
"schema/",
|
|
16
|
+
"mcp-server.js"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"test": "echo \"No tests yet\" && exit 0",
|
|
@@ -21,10 +23,11 @@
|
|
|
21
23
|
"postinstall": "echo '\\n Hone AI CLI installed successfully.\\n Next: run `hone init --token <YOUR_TOKEN>` to configure.\\n Docs: https://github.com/subbareddyvani/hone-server\\n'"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
24
27
|
"ajv": "^8.20.0",
|
|
25
|
-
"axios": "^1.
|
|
28
|
+
"axios": "^1.20.0",
|
|
26
29
|
"commander": "^11.0.0",
|
|
27
|
-
"js-yaml": "^4.
|
|
30
|
+
"js-yaml": "^4.3.2"
|
|
28
31
|
},
|
|
29
32
|
"engines": {
|
|
30
33
|
"node": ">=18.0.0"
|