@hone-ai/cli 1.19.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.
@@ -15,7 +15,9 @@ const yaml = require('js-yaml');
15
15
 
16
16
  // ── Canonical step file naming convention (H-009/A2) ───────────────
17
17
  // Verified across .github/pipeline/H-001/, H-014/, H-029/, H-035/, H-076/.
18
- // step_3b is conditional (manual E2E mode only).
18
+ // step_3b is conditional. HC-019n-followup-28 gave it an activation path:
19
+ // run-story enables it (config.enableConditional) when e2e.spec_generation != 'never';
20
+ // the orchestrator then runs it when the story requires an E2E plan, else skips it.
19
21
  // step_5b is conditional (SA-002 skill audit, present when story consults skills).
20
22
  // step_5c is conditional (H-076 CI gate, present after PR creation).
21
23
  //
@@ -83,7 +85,19 @@ const CONDITIONAL_STEPS = new Set(STEPS.filter(s => s.conditional).map(s => s.ke
83
85
  // get gobbled. The adopter-facing setup-ai-pipeline.sh default stays as
84
86
  // the H-005-canonical `[A-Z]+[-_][A-Za-z0-9]+` — pinned by
85
87
  // tests/regression/H-005-pizza-tracker-non-jira-regex.test.js.
86
- const STORY_ID_PATTERN = /(E[0-9]+-[A-Z][A-Za-z0-9]*(?:-followup-[0-9]+[a-z]?(?=[-_]|$))?|[A-Z]+[-_][A-Za-z0-9]+(?:-followup-[0-9]+[a-z]?(?=[-_]|$))?)/;
88
+ // HC-CI-005: the middle alternative is NEW. `[A-Z]+[-_][A-Za-z0-9]+` stops at
89
+ // the first segment, so a letters-letters-digits id read as only its first two
90
+ // parts: HC-CI-003 -> "HC-CI", HC-COMM-007 -> "HC-COMM", HC-RC-002 -> "HC-RC".
91
+ // That silently broke the HC-019b-followup-1 F1 architect-config lookup, which
92
+ // keys on the extracted id — a miss looks identical to "no architect needed".
93
+ //
94
+ // The new alternative is deliberately NARROW: letters, letters, then DIGITS.
95
+ // A generic trailing `(?:-[A-Za-z0-9]+)*` would be greedy and swallow branch
96
+ // prose — `HC-052-add-uuid-validation` would extract "HC-052-add". Verified:
97
+ // with this alternative that branch still yields "HC-052".
98
+ // Order matters — regex alternation is first-match-wins, so this must precede
99
+ // the general form or the general form shadows it.
100
+ const STORY_ID_PATTERN = /(E[0-9]+-[A-Z][A-Za-z0-9]*(?:-followup-[0-9]+[a-z]?(?=[-_]|$))?|[A-Z]+[-_][A-Z]+[-_][0-9]+[a-z]?(?:-followup-[0-9]+[a-z]?(?=[-_]|$))?|[A-Z]+[-_][A-Za-z0-9]+(?:-followup-[0-9]+[a-z]?(?=[-_]|$))?)/;
87
101
 
88
102
  // ─────────────────────────────────────────────────────────────────────
89
103
  // extractStoryIdFromBranch — pure regex on branch name
@@ -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 };
@@ -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.19.0",
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.6.0",
28
+ "axios": "^1.20.0",
26
29
  "commander": "^11.0.0",
27
- "js-yaml": "^4.1.0"
30
+ "js-yaml": "^4.3.2"
28
31
  },
29
32
  "engines": {
30
33
  "node": ">=18.0.0"