@hone-ai/cli 1.23.0 → 1.25.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 +120 -6
- package/lib/editor-stub.js +44 -0
- package/lib/mcp-tools.js +77 -0
- package/lib/overlay-transport.js +40 -0
- package/lib/pipeline-config.js +62 -0
- package/package.json +1 -1
package/hone-cli.js
CHANGED
|
@@ -2377,16 +2377,32 @@ program
|
|
|
2377
2377
|
console.log(' Migrate any REPO-SPECIFIC edits then `rm -rf .github/agents/`');
|
|
2378
2378
|
}
|
|
2379
2379
|
|
|
2380
|
+
// HC-COMM-010 P3: in editor-mode repos, EDITOR-side agents sync as thin
|
|
2381
|
+
// stubs — their instructions are composed server-side and delivered
|
|
2382
|
+
// ephemerally at runtime, so the full prompt never persists to disk.
|
|
2383
|
+
// Server-mode repos are UNTOUCHED (full sync — daily editor-chat use of
|
|
2384
|
+
// these agents is a feature, not a leak to close).
|
|
2385
|
+
const { isEditorStubbedAgent, editorStubContent } = require('./lib/editor-stub');
|
|
2386
|
+
const { readExecutionMode } = require('./lib/pipeline-config');
|
|
2387
|
+
const syncExecutionMode = readExecutionMode(process.cwd());
|
|
2388
|
+
|
|
2380
2389
|
let synced = 0;
|
|
2381
2390
|
for (const agent of agents) {
|
|
2382
2391
|
try {
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2392
|
+
let content;
|
|
2393
|
+
if (isEditorStubbedAgent(agent, syncExecutionMode)) {
|
|
2394
|
+
content = editorStubContent(agent);
|
|
2395
|
+
console.log(` → ${agent}: editor mode — syncing runtime stub (full prompt served ephemerally)`);
|
|
2396
|
+
} else {
|
|
2397
|
+
const r = await client.get(`/prompts/${agent}`, {
|
|
2398
|
+
responseType: 'text',
|
|
2399
|
+
headers: { Accept: 'text/markdown' },
|
|
2400
|
+
});
|
|
2401
|
+
content = r.data;
|
|
2402
|
+
}
|
|
2387
2403
|
const dest = path.join(agentsDir, `${agent}.agent.md`);
|
|
2388
2404
|
const rel = `.claude/agents/${agent}.agent.md`;
|
|
2389
|
-
const result = safeWrite(dest, rel,
|
|
2405
|
+
const result = safeWrite(dest, rel, content);
|
|
2390
2406
|
if (result !== 'skipped') {
|
|
2391
2407
|
console.log(` ✓ ${rel}`);
|
|
2392
2408
|
synced++;
|
|
@@ -4486,7 +4502,7 @@ program
|
|
|
4486
4502
|
program
|
|
4487
4503
|
.command('classify-story [issueId]')
|
|
4488
4504
|
.description('Classify a GitHub issue by SDLC track. Extracts inputs from issue body/labels heuristically; reads other inputs from --input-file or interactive prompt. Outputs JSON.')
|
|
4489
|
-
.option('--input-file <path>', 'YAML file with all
|
|
4505
|
+
.option('--input-file <path>', 'YAML file with all 10 classifyStory inputs (skips GitHub fetch + interactive)')
|
|
4490
4506
|
.option('--interactive', 'prompt for the 5 judgment fields not heuristically extractable')
|
|
4491
4507
|
.option('--pretty', 'human-readable output (default: --json)')
|
|
4492
4508
|
.option('--repo-root <path>', 'repo root (default: process.cwd())')
|
|
@@ -5455,6 +5471,61 @@ program
|
|
|
5455
5471
|
}
|
|
5456
5472
|
});
|
|
5457
5473
|
|
|
5474
|
+
program
|
|
5475
|
+
.command('step-instruction <runId>')
|
|
5476
|
+
.description('Fetch the editor-mode instruction for a run parked awaiting_editor (HC-COMM-010 B3)')
|
|
5477
|
+
.option('--format <fmt>', 'Output format: pretty | json', 'pretty')
|
|
5478
|
+
.action(async (runId, opts) => {
|
|
5479
|
+
const config = getConfig();
|
|
5480
|
+
const client = api(config);
|
|
5481
|
+
try {
|
|
5482
|
+
const { data } = await client.get(`/orchestrate/${encodeURIComponent(runId)}/step-instruction`);
|
|
5483
|
+
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
5484
|
+
console.log(`Step: ${data.stepKey} (${data.agent})`);
|
|
5485
|
+
console.log(`Contract: ${data.contractVersion}`);
|
|
5486
|
+
console.log('─'.repeat(60));
|
|
5487
|
+
console.log(data.instruction);
|
|
5488
|
+
console.log('─'.repeat(60));
|
|
5489
|
+
console.log(`Submit with: hone submit-step ${runId} ${data.stepKey} --file <output.txt>`);
|
|
5490
|
+
} catch (e) {
|
|
5491
|
+
const st = e.response?.status;
|
|
5492
|
+
if (st === 409) console.error(`No step awaiting the editor: ${e.response.data?.error || ''} (run status: ${e.response.data?.runStatus || '?'})`);
|
|
5493
|
+
else if (st === 403) console.error(`${e.response.data?.error}: ${e.response.data?.remediation || ''}`);
|
|
5494
|
+
else console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
5495
|
+
process.exit(1);
|
|
5496
|
+
}
|
|
5497
|
+
});
|
|
5498
|
+
|
|
5499
|
+
program
|
|
5500
|
+
.command('submit-step <runId> <stepKey>')
|
|
5501
|
+
.description('Submit editor-executed output for an awaiting_editor step (HC-COMM-010 B3)')
|
|
5502
|
+
.option('--file <path>', 'Read the step output from this file')
|
|
5503
|
+
.option('--contract <version>', 'Instruction contract version (from step-instruction)', '1')
|
|
5504
|
+
.option('--format <fmt>', 'Output format: pretty | json', 'pretty')
|
|
5505
|
+
.action(async (runId, stepKey, opts) => {
|
|
5506
|
+
const config = getConfig();
|
|
5507
|
+
const client = api(config);
|
|
5508
|
+
try {
|
|
5509
|
+
if (!opts.file) { console.error('Provide the output via --file <path> (the text your editor produced).'); process.exit(1); }
|
|
5510
|
+
const fs = require('fs');
|
|
5511
|
+
const output = fs.readFileSync(opts.file, 'utf8');
|
|
5512
|
+
const { data } = await client.post(
|
|
5513
|
+
`/orchestrate/${encodeURIComponent(runId)}/step/${encodeURIComponent(stepKey)}/editor-output`,
|
|
5514
|
+
{ contractVersion: opts.contract, output }
|
|
5515
|
+
);
|
|
5516
|
+
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
5517
|
+
console.log(`Step ${data.stepKey} completed (executed_by: ${data.executedBy}); gate: ${data.gateResult}.`);
|
|
5518
|
+
console.log(`Approve when reviewed: hone run-story ${runId} --approve ${data.stepKey}`);
|
|
5519
|
+
console.log('Verification is unchanged — check-patch/verify-patch judge this output, not trust.');
|
|
5520
|
+
} catch (e) {
|
|
5521
|
+
const st = e.response?.status;
|
|
5522
|
+
if (st === 409) console.error(`Rejected: ${e.response.data?.error}`);
|
|
5523
|
+
else if (st === 413) console.error(`Rejected: ${e.response.data?.error}`);
|
|
5524
|
+
else console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
5525
|
+
process.exit(1);
|
|
5526
|
+
}
|
|
5527
|
+
});
|
|
5528
|
+
|
|
5458
5529
|
program
|
|
5459
5530
|
.command('run-story <storyId>')
|
|
5460
5531
|
.description('Run the SDLC pipeline for a story via the orchestrator')
|
|
@@ -5463,6 +5534,7 @@ program
|
|
|
5463
5534
|
.option('--branch <name>', 'Git branch (default: current)')
|
|
5464
5535
|
.option('--status', 'Show status of the latest workflow run for this story')
|
|
5465
5536
|
.option('--kill', 'Kill the latest workflow run for this story')
|
|
5537
|
+
.option('--axes <axes>', 'Comma-separated classifier axes for this run (e.g. shared-chokepoint) — composition input (HC-COMM-010 C0)')
|
|
5466
5538
|
.option('--approve <step>', 'Approve a paused gate (e.g., --approve step_0)')
|
|
5467
5539
|
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
5468
5540
|
.option('--poll-interval <s>', 'Poll interval in seconds', '5')
|
|
@@ -5823,6 +5895,48 @@ program
|
|
|
5823
5895
|
orchestrateConfig.ci_local_command = 'make ci';
|
|
5824
5896
|
}
|
|
5825
5897
|
|
|
5898
|
+
// HC-COMM-010 B0.5 (memo §12 A3): stamp execution.mode into the run config
|
|
5899
|
+
// at CREATION — the server reads it from run.config, making the mode
|
|
5900
|
+
// immutable per run (a mid-flight .pipeline-config.yml flip cannot change
|
|
5901
|
+
// a running run). Absent/old CLIs → the server defaults to 'server'.
|
|
5902
|
+
// Inert until B1: the server ignores the key today.
|
|
5903
|
+
try {
|
|
5904
|
+
const { readExecutionMode, EDITOR_MODE_CONSENT_NOTE } = require('./lib/pipeline-config');
|
|
5905
|
+
const executionMode = readExecutionMode(process.cwd());
|
|
5906
|
+
orchestrateConfig.execution_mode = executionMode;
|
|
5907
|
+
if (executionMode === 'editor') {
|
|
5908
|
+
// §2a: the consent note is shown at the moment editor mode is USED.
|
|
5909
|
+
console.log(` → execution mode: editor (opt-in)`);
|
|
5910
|
+
console.log(` ${EDITOR_MODE_CONSENT_NOTE}`);
|
|
5911
|
+
}
|
|
5912
|
+
} catch (e) {
|
|
5913
|
+
const msg = (e && e.message) ? e.message.split('\n')[0] : String(e);
|
|
5914
|
+
console.warn(` ⚠ execution mode read failed (non-fatal, defaulting to server): ${msg}`);
|
|
5915
|
+
orchestrateConfig.execution_mode = 'server';
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
// HC-COMM-010 C0: composition transports — INERT until C1 reads them.
|
|
5919
|
+
// Overlays: .hone-local/ is adopter disk the server can never read
|
|
5920
|
+
// (the cross-validation finding), so raw texts travel in run config —
|
|
5921
|
+
// the codebaseFiles pattern, size/count-capped, degrade-to-empty.
|
|
5922
|
+
// Axes: explicit --axes flag for now; automated wiring from classify
|
|
5923
|
+
// runs is future work (documented in the sprint plan).
|
|
5924
|
+
try {
|
|
5925
|
+
const { readRuleOverlays } = require('./lib/overlay-transport');
|
|
5926
|
+
const ov = readRuleOverlays(process.cwd());
|
|
5927
|
+
if (ov.overlays.length > 0) {
|
|
5928
|
+
orchestrateConfig.rule_overlays = ov.overlays;
|
|
5929
|
+
console.log(` → bundled ${ov.overlays.length} rule overlay(s) for server-side composition`);
|
|
5930
|
+
}
|
|
5931
|
+
for (const skip of ov.skipped) console.warn(` ⚠ overlay skipped: ${skip}`);
|
|
5932
|
+
} catch (e) {
|
|
5933
|
+
console.warn(` ⚠ overlay read failed (non-fatal): ${(e && e.message || String(e)).split('\n')[0]}`);
|
|
5934
|
+
}
|
|
5935
|
+
if (opts.axes) {
|
|
5936
|
+
const axes = String(opts.axes).split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
|
|
5937
|
+
if (axes.length > 0) orchestrateConfig.classifier_axes = axes;
|
|
5938
|
+
}
|
|
5939
|
+
|
|
5826
5940
|
// HC-019n-followup-28: resolve the E2E spec-generation mode (step_3b /
|
|
5827
5941
|
// Playwright). Precedence mirrors ci.gate: per-run flag > .pipeline-config.yml
|
|
5828
5942
|
// `e2e.spec_generation` > default `auto`. `auto` = the orchestrator runs
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* editor-stub.js — HC-COMM-010 P3: thin-stub content for editor-side agents.
|
|
4
|
+
*
|
|
5
|
+
* When a repo opts into `execution.mode: editor`, the agents of the server's
|
|
6
|
+
* EDITOR_SIDE_STEPS are DELIVERED AT RUNTIME (ephemeral, via
|
|
7
|
+
* `hone step-instruction` / the hone_step_instruction MCP tool) — so `hone
|
|
8
|
+
* sync` ships a stub instead of persisting the full prompt to disk. This
|
|
9
|
+
* closes the dominant prompt-exposure leak for editor-mode repos (memo §5 /
|
|
10
|
+
* analysis G1) WITHOUT touching server-mode repos, whose adopters use the
|
|
11
|
+
* full synced prompts for daily editor-chat work.
|
|
12
|
+
*
|
|
13
|
+
* EDITOR_STUBBED_AGENTS must stay equal to the agents of the server's
|
|
14
|
+
* EDITOR_SIDE_STEPS (orchestrator.js) — pinned by a regression test that
|
|
15
|
+
* derives the truth from workflow-dag, since the two packages share no lib.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const EDITOR_STUBBED_AGENTS = Object.freeze(['code-builder']);
|
|
19
|
+
|
|
20
|
+
function isEditorStubbedAgent(agent, executionMode) {
|
|
21
|
+
return executionMode === 'editor' && EDITOR_STUBBED_AGENTS.includes(agent);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function editorStubContent(agent) {
|
|
25
|
+
return [
|
|
26
|
+
`# ${agent} — served at runtime by Hone (editor mode)`,
|
|
27
|
+
'',
|
|
28
|
+
'This repo runs with `execution.mode: editor`, so this agent\'s pipeline',
|
|
29
|
+
'instruction is COMPOSED SERVER-SIDE and delivered ephemerally at',
|
|
30
|
+
'invocation time — it is not stored on disk.',
|
|
31
|
+
'',
|
|
32
|
+
'To execute a parked step:',
|
|
33
|
+
' 1. `hone step-instruction <runId>` (or the hone_step_instruction MCP tool)',
|
|
34
|
+
' 2. run the returned instruction in your editor',
|
|
35
|
+
' 3. `hone submit-step <runId> <stepKey> --file <output>`',
|
|
36
|
+
'',
|
|
37
|
+
'Output is judged by real verification (check-patch / verify-patch),',
|
|
38
|
+
'never taken on trust. Switch `execution.mode` back to `server` and',
|
|
39
|
+
're-run `hone sync` to restore the full local prompt.',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { EDITOR_STUBBED_AGENTS, isEditorStubbedAgent, editorStubContent };
|
package/lib/mcp-tools.js
CHANGED
|
@@ -131,6 +131,58 @@ async function killWorkflow({ runId, cwd } = {}) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// hone_step_instruction — fetch the editor-mode instruction for an
|
|
135
|
+
// awaiting_editor run (HC-COMM-010 B3). THE ephemeral delivery: the composed
|
|
136
|
+
// instruction lands in the editor's context via this tool result and is never
|
|
137
|
+
// written to disk by any Hone code.
|
|
138
|
+
async function stepInstruction({ runId, cwd } = {}) {
|
|
139
|
+
if (!runId) return { isError: true, detail: 'runId is required' };
|
|
140
|
+
const r = await runHone(['step-instruction', String(runId), '--format', 'json'], { cwd });
|
|
141
|
+
if (r.error) return { isError: true, detail: r.error };
|
|
142
|
+
const data = parseLastJson(r.stdout);
|
|
143
|
+
return {
|
|
144
|
+
isError: !data,
|
|
145
|
+
instruction: data ? data.instruction : null,
|
|
146
|
+
stepKey: data ? data.stepKey : null,
|
|
147
|
+
contractVersion: data ? data.contractVersion : null,
|
|
148
|
+
exitCode: r.exitCode,
|
|
149
|
+
detail: data
|
|
150
|
+
? `instruction for ${data.stepKey} (contract ${data.contractVersion}) — execute it and call hone_submit_step with the complete output`
|
|
151
|
+
: (r.stderr || r.stdout || 'could not fetch instruction').slice(0, 500),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// hone_submit_step — submit editor-executed output (HC-COMM-010 B3). Output
|
|
156
|
+
// travels via a temp file (1MB-class payloads must not ride argv); the file
|
|
157
|
+
// is best-effort deleted after the shell-out.
|
|
158
|
+
async function submitStep({ runId, stepKey, output, contractVersion = '1', cwd } = {}) {
|
|
159
|
+
if (!runId) return { isError: true, detail: 'runId is required' };
|
|
160
|
+
if (!stepKey) return { isError: true, detail: 'stepKey is required' };
|
|
161
|
+
if (typeof output !== 'string' || output.trim() === '') {
|
|
162
|
+
return { isError: true, detail: 'output is required (the complete step output text)' };
|
|
163
|
+
}
|
|
164
|
+
const tmp = path.join(os.tmpdir(), `hone-editor-output-${Date.now()}-${process.pid}.txt`);
|
|
165
|
+
try {
|
|
166
|
+
fsSync.writeFileSync(tmp, output, 'utf8');
|
|
167
|
+
const r = await runHone(
|
|
168
|
+
['submit-step', String(runId), String(stepKey), '--file', tmp, '--contract', String(contractVersion), '--format', 'json'],
|
|
169
|
+
{ cwd });
|
|
170
|
+
if (r.error) return { isError: true, detail: r.error };
|
|
171
|
+
const data = parseLastJson(r.stdout);
|
|
172
|
+
return {
|
|
173
|
+
isError: !r.ok,
|
|
174
|
+
submitted: r.ok,
|
|
175
|
+
gateResult: data ? data.gateResult : null,
|
|
176
|
+
exitCode: r.exitCode,
|
|
177
|
+
detail: data
|
|
178
|
+
? `step ${data.stepKey} completed (executed_by: ${data.executedBy}); human gate is next`
|
|
179
|
+
: (r.stderr || r.stdout || 'submission failed').slice(0, 500),
|
|
180
|
+
};
|
|
181
|
+
} finally {
|
|
182
|
+
try { fsSync.unlinkSync(tmp); } catch { /* best-effort cleanup */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
// hone_status — read run status (readOnly). GET /orchestrate/:id.
|
|
135
187
|
async function status({ runId, cwd } = {}) {
|
|
136
188
|
if (!runId) return { isError: true, detail: 'runId is required' };
|
|
@@ -343,6 +395,29 @@ const TOOLS = [
|
|
|
343
395
|
inputSchema: { type: 'object', properties: { runId: { type: 'string' } }, required: ['runId'] },
|
|
344
396
|
handler: killWorkflow,
|
|
345
397
|
},
|
|
398
|
+
{
|
|
399
|
+
name: 'hone_step_instruction',
|
|
400
|
+
description: 'Fetch the editor-mode instruction for a Hone run that is awaiting the editor (execution.mode: editor). Read-only. Execute the returned instruction in THIS session, then call hone_submit_step with the complete output.',
|
|
401
|
+
readOnly: true,
|
|
402
|
+
inputSchema: { type: 'object', properties: { runId: { type: 'string' } }, required: ['runId'] },
|
|
403
|
+
handler: stepInstruction,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'hone_submit_step',
|
|
407
|
+
description: "Submit the editor-executed output for a Hone step that was fetched via hone_step_instruction. The output enters Hone's NORMAL gate + verification flow (judged by real test execution, not trust).",
|
|
408
|
+
readOnly: false,
|
|
409
|
+
inputSchema: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {
|
|
412
|
+
runId: { type: 'string' },
|
|
413
|
+
stepKey: { type: 'string' },
|
|
414
|
+
output: { type: 'string' },
|
|
415
|
+
contractVersion: { type: 'string', default: '1' },
|
|
416
|
+
},
|
|
417
|
+
required: ['runId', 'stepKey', 'output'],
|
|
418
|
+
},
|
|
419
|
+
handler: submitStep,
|
|
420
|
+
},
|
|
346
421
|
{
|
|
347
422
|
name: 'hone_doctor',
|
|
348
423
|
description: 'Self-check the Hone setup (read-only): is a token configured, and is the `hone` CLI installed + reachable? Run this first if other tools fail.',
|
|
@@ -353,6 +428,8 @@ const TOOLS = [
|
|
|
353
428
|
];
|
|
354
429
|
|
|
355
430
|
module.exports = {
|
|
431
|
+
stepInstruction,
|
|
432
|
+
submitStep,
|
|
356
433
|
listWorkflows,
|
|
357
434
|
killWorkflow,
|
|
358
435
|
runStory, status, verifyPatch, checkPatch, showStep, approve, emitPr, verifyPr, agentEval, derive, sync, doctor,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* overlay-transport.js — HC-COMM-010 C0: read adopter rule-overlays for
|
|
4
|
+
* transport to the server at run creation.
|
|
5
|
+
*
|
|
6
|
+
* `.hone-local/rule-overlays/*.md` lives on ADOPTER DISK — the server can
|
|
7
|
+
* never read it (the cross-validation finding that refuted "merge overlays
|
|
8
|
+
* server-side" as originally sketched). So the CLI bundles the raw overlay
|
|
9
|
+
* texts into orchestrateConfig, exactly the codebaseFiles/§12-A3 transport
|
|
10
|
+
* pattern. Parsing (front-matter, insertion targets) happens SERVER-side in
|
|
11
|
+
* C1 via the mergeOverlays machinery — this reader stays deliberately dumb:
|
|
12
|
+
* raw {name, content} pairs, size/count-capped, degrade-to-empty on every
|
|
13
|
+
* failure path.
|
|
14
|
+
*/
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_MAX_FILES = 10;
|
|
19
|
+
const DEFAULT_MAX_TOTAL_BYTES = 50_000; // mirrors the codebaseFiles budget
|
|
20
|
+
|
|
21
|
+
function readRuleOverlays(repoRoot, { maxFiles = DEFAULT_MAX_FILES, maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES } = {}) {
|
|
22
|
+
const result = { overlays: [], skipped: [] };
|
|
23
|
+
if (!repoRoot || typeof repoRoot !== 'string') return result;
|
|
24
|
+
const dir = path.join(repoRoot, '.hone-local', 'rule-overlays');
|
|
25
|
+
let names;
|
|
26
|
+
try { names = fs.readdirSync(dir).filter(n => n.endsWith('.md')).sort(); } catch { return result; }
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
if (result.overlays.length >= maxFiles) { result.skipped.push(`${name} (file cap ${maxFiles})`); continue; }
|
|
30
|
+
let content;
|
|
31
|
+
try { content = fs.readFileSync(path.join(dir, name), 'utf8'); } catch { result.skipped.push(`${name} (unreadable)`); continue; }
|
|
32
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
33
|
+
if (total + bytes > maxTotalBytes) { result.skipped.push(`${name} (byte budget ${maxTotalBytes})`); continue; }
|
|
34
|
+
total += bytes;
|
|
35
|
+
result.overlays.push({ name, content });
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { readRuleOverlays, DEFAULT_MAX_FILES, DEFAULT_MAX_TOTAL_BYTES };
|
package/lib/pipeline-config.js
CHANGED
|
@@ -205,6 +205,63 @@ function readE2eSpecMode(repoRoot) {
|
|
|
205
205
|
return DEFAULT_E2E_SPEC_MODE;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// ── HC-COMM-010 (Track B / B0): execution mode ────────────────────────────────
|
|
209
|
+
// Which side EXECUTES the generation steps:
|
|
210
|
+
// server (default) → the Hone server runs every agent (BYO key / metered);
|
|
211
|
+
// editor → (feature ships with B1-B4) generation steps run in the
|
|
212
|
+
// adopter's editor AI via ephemeral server-composed
|
|
213
|
+
// instructions. The knob is reserved NOW (memo §2a) so
|
|
214
|
+
// adopters never migrate config to get editor mode later.
|
|
215
|
+
// Default-safe: missing/malformed → 'server'.
|
|
216
|
+
const EXECUTION_MODES = ['server', 'editor'];
|
|
217
|
+
const DEFAULT_EXECUTION_MODE = 'server';
|
|
218
|
+
|
|
219
|
+
// Memo §2a consent note — shown verbatim whenever `editor` is selected.
|
|
220
|
+
// Neutral own-use consent; supports but does not replace the ToS position.
|
|
221
|
+
const EDITOR_MODE_CONSENT_NOTE =
|
|
222
|
+
'Editor mode runs pipeline steps in your own editor AI on whatever plan ' +
|
|
223
|
+
"you've authorized there. By enabling it you confirm you are using your own " +
|
|
224
|
+
'authorized plan for your own development work, and that you are responsible ' +
|
|
225
|
+
'for ensuring your plan and usage comply with your agreement with your AI ' +
|
|
226
|
+
'provider. Hone does not select or provision your plan.';
|
|
227
|
+
|
|
228
|
+
/** Normalize an execution.mode value, defaulting to server. */
|
|
229
|
+
function normalizeExecutionMode(v) {
|
|
230
|
+
const s = String(v == null ? '' : v).toLowerCase().trim();
|
|
231
|
+
return EXECUTION_MODES.includes(s) ? s : DEFAULT_EXECUTION_MODE;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Read execution.mode from .pipeline-config.yml's `execution:` block.
|
|
236
|
+
* Same candidate order + graceful degradation as readE2eSpecMode.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} repoRoot
|
|
239
|
+
* @returns {'server'|'editor'} default 'server' when config is missing/malformed.
|
|
240
|
+
*/
|
|
241
|
+
function readExecutionMode(repoRoot) {
|
|
242
|
+
if (!repoRoot || typeof repoRoot !== 'string') return DEFAULT_EXECUTION_MODE;
|
|
243
|
+
const candidates = [
|
|
244
|
+
path.join(repoRoot, '.pipeline-config.yml'),
|
|
245
|
+
path.join(repoRoot, '.github/.pipeline-config.yml'),
|
|
246
|
+
];
|
|
247
|
+
for (const p of candidates) {
|
|
248
|
+
if (!fs.existsSync(p)) continue;
|
|
249
|
+
let raw;
|
|
250
|
+
try { raw = fs.readFileSync(p, 'utf8'); } catch { continue; }
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
const yaml = require('js-yaml');
|
|
254
|
+
parsed = yaml.load(raw);
|
|
255
|
+
} catch { continue; }
|
|
256
|
+
if (!parsed || typeof parsed !== 'object') continue;
|
|
257
|
+
const block = parsed.execution;
|
|
258
|
+
if (!block || typeof block !== 'object') continue;
|
|
259
|
+
if (block.mode === undefined) continue;
|
|
260
|
+
return normalizeExecutionMode(block.mode);
|
|
261
|
+
}
|
|
262
|
+
return DEFAULT_EXECUTION_MODE;
|
|
263
|
+
}
|
|
264
|
+
|
|
208
265
|
// ── HC-019n-followup-34: closed-loop verification gate ────────────────────────
|
|
209
266
|
// The CLI verbs (verify-patch/verify-pr) EXECUTE and report a verdict to the
|
|
210
267
|
// server. `verification.gate` controls what the server does with it:
|
|
@@ -360,6 +417,11 @@ function readAgentEvalConfig(repoRoot) {
|
|
|
360
417
|
}
|
|
361
418
|
|
|
362
419
|
module.exports = {
|
|
420
|
+
EXECUTION_MODES,
|
|
421
|
+
DEFAULT_EXECUTION_MODE,
|
|
422
|
+
EDITOR_MODE_CONSENT_NOTE,
|
|
423
|
+
normalizeExecutionMode,
|
|
424
|
+
readExecutionMode,
|
|
363
425
|
readStoryClassifierConfig,
|
|
364
426
|
KNOWN_THRESHOLD_KEYS,
|
|
365
427
|
readCIGateConfig,
|