@hone-ai/cli 1.25.0 → 1.26.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 CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  const { Command } = require('commander');
20
+ const { isEditorParked, editorParkedGuidance, currentStep, EDITOR_PARKED_EXIT_CODE } = require('./lib/editor-park'); // HC-COMM-010-followup-1
20
21
  const axios = require('axios');
21
22
  const fs = require('fs');
22
23
  const path = require('path');
@@ -2385,6 +2386,19 @@ program
2385
2386
  const { isEditorStubbedAgent, editorStubContent } = require('./lib/editor-stub');
2386
2387
  const { readExecutionMode } = require('./lib/pipeline-config');
2387
2388
  const syncExecutionMode = readExecutionMode(process.cwd());
2389
+ // HC-COMM-010-followup-3: in editor mode, register the Hone MCP server for
2390
+ // Claude Code by writing a project-scoped .mcp.json (merge, never clobber).
2391
+ // Replaces the manual `claude mcp add hone -- hone-mcp` runbook step.
2392
+ try {
2393
+ const { ensureClaudeMcpConfig, MCP_FILE, FALLBACK_LINE } = require('./lib/mcp-config');
2394
+ const mcp = ensureClaudeMcpConfig(process.cwd(), { mode: syncExecutionMode });
2395
+ if (mcp.action === 'written' || mcp.action === 'updated') console.log(` ✓ Registered the Hone MCP server for Claude Code in ${MCP_FILE} (commit it; Claude Code asks once to approve project servers)`);
2396
+ else if (mcp.action === 'skipped' && syncExecutionMode === 'editor') console.log(` ⚠ ${mcp.reason}`);
2397
+ else if (mcp.action === 'unchanged') console.log(` ✓ Hone MCP server already registered in ${MCP_FILE}`);
2398
+ void FALLBACK_LINE;
2399
+ } catch (e) {
2400
+ console.log(` ⚠ MCP registration skipped (non-fatal): ${e.message}`);
2401
+ }
2388
2402
 
2389
2403
  let synced = 0;
2390
2404
  for (const agent of agents) {
@@ -5535,7 +5549,7 @@ program
5535
5549
  .option('--status', 'Show status of the latest workflow run for this story')
5536
5550
  .option('--kill', 'Kill the latest workflow run for this story')
5537
5551
  .option('--axes <axes>', 'Comma-separated classifier axes for this run (e.g. shared-chokepoint) — composition input (HC-COMM-010 C0)')
5538
- .option('--approve <step>', 'Approve a paused gate (e.g., --approve step_0)')
5552
+ .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)')
5539
5553
  .option('--format <fmt>', 'Output format: pretty or json', 'pretty')
5540
5554
  .option('--poll-interval <s>', 'Poll interval in seconds', '5')
5541
5555
  .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.')
@@ -5569,9 +5583,15 @@ program
5569
5583
  for (const s of (data.steps || [])) {
5570
5584
  const icon = s.status === 'completed' ? '✓' : s.status === 'running' ? '⏳' : s.status === 'failed' ? '✗' : '⬜';
5571
5585
  const gate = s.gateResult ? ` (${s.gateResult})` : '';
5572
- console.log(` ${icon} ${s.stepKey} (${s.agent}) ${s.status}${gate}`);
5586
+ // HC-019n-followup-40: one line per step = its CURRENT state; a retried
5587
+ // step says so instead of listing a stale failed row above a completed one.
5588
+ const tries = (s.attempts || 1) > 1 ? ` [attempt ${s.attempt} of ${s.attempts}]` : '';
5589
+ console.log(` ${icon} ${s.stepKey} (${s.agent}) ${s.status}${gate}${tries}`);
5573
5590
  }
5574
- if (data.awaitingApproval) {
5591
+ if (data.awaitingApproval && isEditorParked(data)) {
5592
+ console.log('');
5593
+ console.log(editorParkedGuidance(data.runId, data.currentStep, currentStep(data)?.agent));
5594
+ } else if (data.awaitingApproval) {
5575
5595
  console.log('');
5576
5596
  console.log(`Awaiting approval at ${data.currentStep}.`);
5577
5597
  console.log(`Approve: hone run-story ${data.runId} --approve ${data.currentStep}`);
@@ -5877,6 +5897,37 @@ program
5877
5897
  console.warn(' Fix: hone run-story <id> --include-paths path/one.js,path/two.js');
5878
5898
  }
5879
5899
 
5900
+ // HC-COMM-010 axes wiring (CLI half): when --axes wasn't given, derive
5901
+ // the DETERMINISTIC axes from the extractors (SC-002) — security labels/
5902
+ // text, cross-repo markers, recently-modified overlap vs the bundled
5903
+ // paths. Judgment axes (shared-chokepoint etc.) come from the groomer at
5904
+ // step_0 and are harvested server-side. Fail-open; explicit --axes wins.
5905
+ if (!opts.axes) {
5906
+ try {
5907
+ const { classifyStory } = require('./lib/story-classifier');
5908
+ const {
5909
+ extractType, extractSecurityImplications, extractCrossRepoDependency, extractRecentlyModifiedOverlap,
5910
+ } = require('./lib/story-classifier-extract');
5911
+ const autoBody = issueBodyForFiles || '';
5912
+ const autoLabels = [];
5913
+ const touched = Array.isArray(orchestrateConfig.codebaseFiles)
5914
+ ? orchestrateConfig.codebaseFiles.map(f => f.path).filter(Boolean)
5915
+ : Object.keys(orchestrateConfig.codebaseFiles || {});
5916
+ const auto = classifyStory({
5917
+ type: extractType(autoLabels, autoBody, String(storyIdOrRunId)),
5918
+ has_security_implications: extractSecurityImplications(autoLabels, autoBody),
5919
+ cross_repo_dependency: extractCrossRepoDependency(autoBody),
5920
+ recently_modified_overlap: extractRecentlyModifiedOverlap(process.cwd(), touched, 14),
5921
+ });
5922
+ if (auto.architect.engage && auto.architect.axes.length > 0) {
5923
+ orchestrateConfig.classifier_axes = auto.architect.axes;
5924
+ console.log(` → auto-detected classifier axes: ${auto.architect.axes.join(', ')}`);
5925
+ }
5926
+ } catch (e) {
5927
+ console.warn(` ⚠ axes auto-detection failed (non-fatal): ${(e && e.message || String(e)).split('\n')[0]}`);
5928
+ }
5929
+ }
5930
+
5880
5931
  // HC-101-followup-2: pass the adopter's CI gate config to the
5881
5932
  // orchestrator so step_5c can branch (github / local / both / none).
5882
5933
  // Defaults to gate=github + local_command='make ci' when the config
@@ -6049,6 +6100,12 @@ program
6049
6100
 
6050
6101
  if (status.status === 'paused' && status.awaitingApproval) {
6051
6102
  const step = status.steps.find(s => s.stepKey === status.currentStep);
6103
+ // HC-COMM-010-followup-1: an editor-parked run is a gate the EDITOR
6104
+ // satisfies, not a human — never offer --approve here (B4 refuses it).
6105
+ if (isEditorParked(status)) {
6106
+ console.log(editorParkedGuidance(data.runId, status.currentStep, step?.agent));
6107
+ process.exit(EDITOR_PARKED_EXIT_CODE);
6108
+ }
6052
6109
  console.log(`⏳ Step ${status.currentStep} (${step?.agent || 'unknown'}) — awaiting approval`);
6053
6110
 
6054
6111
  // Interactive prompt
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+ /**
3
+ * editor-park.js — HC-COMM-010-followup-1: recognize a run that is PARKED FOR
4
+ * THE EDITOR (execution.mode: editor, step awaiting_editor) and say what to do.
5
+ *
6
+ * Why: the run-status endpoint reports an editor-parked run as
7
+ * `paused` + `awaitingApproval: true` (it IS at a gate, just not one a human
8
+ * approves) — so run-story's poll loop used to print "--approve" guidance,
9
+ * which the B4 guard then refuses. Detection keys on the CURRENT step's
10
+ * status, reliable since HC-019n-followup-40 collapsed steps[] to the latest
11
+ * attempt. Pure: no I/O, no process access — the CLI decides how to exit.
12
+ */
13
+ const EDITOR_PARKED_EXIT_CODE = 3; // distinct from 2 (paused at a human gate)
14
+
15
+ function currentStep(status) {
16
+ if (!status || !Array.isArray(status.steps)) return null;
17
+ return status.steps.find(s => s && s.stepKey === status.currentStep) || null;
18
+ }
19
+
20
+ /** @returns {boolean} true when the run is paused because the editor owes output */
21
+ function isEditorParked(status) {
22
+ if (!status || status.status !== 'paused') return false;
23
+ const step = currentStep(status);
24
+ return !!(step && step.status === 'awaiting_editor');
25
+ }
26
+
27
+ /**
28
+ * The parked-state copy (ux-copy pass, 2026-09-05): what happened, where it is,
29
+ * the two ways forward (editor MCP tools / CLI verbs), and the closing step.
30
+ */
31
+ function editorParkedGuidance(runId, stepKey, agent) {
32
+ const a = agent ? ` (${agent})` : '';
33
+ return [
34
+ `⏸ Parked for your editor at ${stepKey}${a} — this step runs in your editor AI, not on Hone.`,
35
+ ` In Claude Code: call hone_step_instruction, build the step there, then hone_submit_step.`,
36
+ ` From the CLI: hone step-instruction ${runId}`,
37
+ ` … build … then: hone submit-step ${runId} ${stepKey} --file out.md`,
38
+ ` When submitted: hone run-story ${runId} --approve ${stepKey}`,
39
+ ` Guide: docs/runbooks/editor-mode.md`,
40
+ ].join('\n');
41
+ }
42
+
43
+ module.exports = { EDITOR_PARKED_EXIT_CODE, isEditorParked, editorParkedGuidance, currentStep };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+ /**
3
+ * mcp-config.js — HC-COMM-010-followup-3: register the Hone MCP server for
4
+ * Claude Code by writing a PROJECT-scoped `.mcp.json` when the repo runs in
5
+ * editor mode. Replaces the manual `claude mcp add hone -- hone-mcp` step.
6
+ *
7
+ * Contract: merge, never clobber. Other servers in an existing `.mcp.json` are
8
+ * preserved; only the `hone` entry is added/refreshed; a malformed file is left
9
+ * untouched (fail-open with a reason). Pure over an injected repoRoot.
10
+ */
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const MCP_FILE = '.mcp.json';
15
+ const HONE_SERVER = Object.freeze({ command: 'hone-mcp', args: [] });
16
+ const FALLBACK_LINE = 'claude mcp add hone -- hone-mcp';
17
+
18
+ function ensureClaudeMcpConfig(repoRoot, { mode } = {}) {
19
+ if (mode !== 'editor') return { action: 'skipped', reason: 'execution.mode is not editor' };
20
+ const file = path.join(repoRoot, MCP_FILE);
21
+ let doc = { mcpServers: {} };
22
+ if (fs.existsSync(file)) {
23
+ try {
24
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
25
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
26
+ doc = parsed;
27
+ if (!doc.mcpServers || typeof doc.mcpServers !== 'object' || Array.isArray(doc.mcpServers)) doc.mcpServers = {};
28
+ } catch (e) {
29
+ return { action: 'skipped', reason: `${MCP_FILE} is not valid JSON (${e.message}); left untouched — register manually: ${FALLBACK_LINE}` };
30
+ }
31
+ }
32
+ const existing = doc.mcpServers.hone;
33
+ if (existing && existing.command === HONE_SERVER.command && JSON.stringify(existing.args || []) === '[]') {
34
+ return { action: 'unchanged', file };
35
+ }
36
+ doc.mcpServers.hone = { ...HONE_SERVER, args: [] };
37
+ fs.writeFileSync(file, JSON.stringify(doc, null, 2) + '\n');
38
+ return { action: existing ? 'updated' : 'written', file };
39
+ }
40
+
41
+ module.exports = { ensureClaudeMcpConfig, MCP_FILE, HONE_SERVER, FALLBACK_LINE };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hone-ai/cli",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {