@hone-ai/cli 1.24.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 +104 -8
- package/lib/editor-park.js +43 -0
- package/lib/editor-stub.js +44 -0
- package/lib/mcp-config.js +41 -0
- package/lib/overlay-transport.js +40 -0
- package/package.json +1 -1
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');
|
|
@@ -2377,16 +2378,45 @@ program
|
|
|
2377
2378
|
console.log(' Migrate any REPO-SPECIFIC edits then `rm -rf .github/agents/`');
|
|
2378
2379
|
}
|
|
2379
2380
|
|
|
2381
|
+
// HC-COMM-010 P3: in editor-mode repos, EDITOR-side agents sync as thin
|
|
2382
|
+
// stubs — their instructions are composed server-side and delivered
|
|
2383
|
+
// ephemerally at runtime, so the full prompt never persists to disk.
|
|
2384
|
+
// Server-mode repos are UNTOUCHED (full sync — daily editor-chat use of
|
|
2385
|
+
// these agents is a feature, not a leak to close).
|
|
2386
|
+
const { isEditorStubbedAgent, editorStubContent } = require('./lib/editor-stub');
|
|
2387
|
+
const { readExecutionMode } = require('./lib/pipeline-config');
|
|
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
|
+
}
|
|
2402
|
+
|
|
2380
2403
|
let synced = 0;
|
|
2381
2404
|
for (const agent of agents) {
|
|
2382
2405
|
try {
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2406
|
+
let content;
|
|
2407
|
+
if (isEditorStubbedAgent(agent, syncExecutionMode)) {
|
|
2408
|
+
content = editorStubContent(agent);
|
|
2409
|
+
console.log(` → ${agent}: editor mode — syncing runtime stub (full prompt served ephemerally)`);
|
|
2410
|
+
} else {
|
|
2411
|
+
const r = await client.get(`/prompts/${agent}`, {
|
|
2412
|
+
responseType: 'text',
|
|
2413
|
+
headers: { Accept: 'text/markdown' },
|
|
2414
|
+
});
|
|
2415
|
+
content = r.data;
|
|
2416
|
+
}
|
|
2387
2417
|
const dest = path.join(agentsDir, `${agent}.agent.md`);
|
|
2388
2418
|
const rel = `.claude/agents/${agent}.agent.md`;
|
|
2389
|
-
const result = safeWrite(dest, rel,
|
|
2419
|
+
const result = safeWrite(dest, rel, content);
|
|
2390
2420
|
if (result !== 'skipped') {
|
|
2391
2421
|
console.log(` ✓ ${rel}`);
|
|
2392
2422
|
synced++;
|
|
@@ -5518,7 +5548,8 @@ program
|
|
|
5518
5548
|
.option('--branch <name>', 'Git branch (default: current)')
|
|
5519
5549
|
.option('--status', 'Show status of the latest workflow run for this story')
|
|
5520
5550
|
.option('--kill', 'Kill the latest workflow run for this story')
|
|
5521
|
-
.option('--
|
|
5551
|
+
.option('--axes <axes>', 'Comma-separated classifier axes for this run (e.g. shared-chokepoint) — composition input (HC-COMM-010 C0)')
|
|
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)')
|
|
5522
5553
|
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
5523
5554
|
.option('--poll-interval <s>', 'Poll interval in seconds', '5')
|
|
5524
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.')
|
|
@@ -5552,9 +5583,15 @@ program
|
|
|
5552
5583
|
for (const s of (data.steps || [])) {
|
|
5553
5584
|
const icon = s.status === 'completed' ? '✓' : s.status === 'running' ? '⏳' : s.status === 'failed' ? '✗' : '⬜';
|
|
5554
5585
|
const gate = s.gateResult ? ` (${s.gateResult})` : '';
|
|
5555
|
-
|
|
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}`);
|
|
5556
5590
|
}
|
|
5557
|
-
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) {
|
|
5558
5595
|
console.log('');
|
|
5559
5596
|
console.log(`Awaiting approval at ${data.currentStep}.`);
|
|
5560
5597
|
console.log(`Approve: hone run-story ${data.runId} --approve ${data.currentStep}`);
|
|
@@ -5860,6 +5897,37 @@ program
|
|
|
5860
5897
|
console.warn(' Fix: hone run-story <id> --include-paths path/one.js,path/two.js');
|
|
5861
5898
|
}
|
|
5862
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
|
+
|
|
5863
5931
|
// HC-101-followup-2: pass the adopter's CI gate config to the
|
|
5864
5932
|
// orchestrator so step_5c can branch (github / local / both / none).
|
|
5865
5933
|
// Defaults to gate=github + local_command='make ci' when the config
|
|
@@ -5898,6 +5966,28 @@ program
|
|
|
5898
5966
|
orchestrateConfig.execution_mode = 'server';
|
|
5899
5967
|
}
|
|
5900
5968
|
|
|
5969
|
+
// HC-COMM-010 C0: composition transports — INERT until C1 reads them.
|
|
5970
|
+
// Overlays: .hone-local/ is adopter disk the server can never read
|
|
5971
|
+
// (the cross-validation finding), so raw texts travel in run config —
|
|
5972
|
+
// the codebaseFiles pattern, size/count-capped, degrade-to-empty.
|
|
5973
|
+
// Axes: explicit --axes flag for now; automated wiring from classify
|
|
5974
|
+
// runs is future work (documented in the sprint plan).
|
|
5975
|
+
try {
|
|
5976
|
+
const { readRuleOverlays } = require('./lib/overlay-transport');
|
|
5977
|
+
const ov = readRuleOverlays(process.cwd());
|
|
5978
|
+
if (ov.overlays.length > 0) {
|
|
5979
|
+
orchestrateConfig.rule_overlays = ov.overlays;
|
|
5980
|
+
console.log(` → bundled ${ov.overlays.length} rule overlay(s) for server-side composition`);
|
|
5981
|
+
}
|
|
5982
|
+
for (const skip of ov.skipped) console.warn(` ⚠ overlay skipped: ${skip}`);
|
|
5983
|
+
} catch (e) {
|
|
5984
|
+
console.warn(` ⚠ overlay read failed (non-fatal): ${(e && e.message || String(e)).split('\n')[0]}`);
|
|
5985
|
+
}
|
|
5986
|
+
if (opts.axes) {
|
|
5987
|
+
const axes = String(opts.axes).split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
|
|
5988
|
+
if (axes.length > 0) orchestrateConfig.classifier_axes = axes;
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5901
5991
|
// HC-019n-followup-28: resolve the E2E spec-generation mode (step_3b /
|
|
5902
5992
|
// Playwright). Precedence mirrors ci.gate: per-run flag > .pipeline-config.yml
|
|
5903
5993
|
// `e2e.spec_generation` > default `auto`. `auto` = the orchestrator runs
|
|
@@ -6010,6 +6100,12 @@ program
|
|
|
6010
6100
|
|
|
6011
6101
|
if (status.status === 'paused' && status.awaitingApproval) {
|
|
6012
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
|
+
}
|
|
6013
6109
|
console.log(`⏳ Step ${status.currentStep} (${step?.agent || 'unknown'}) — awaiting approval`);
|
|
6014
6110
|
|
|
6015
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,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 };
|
|
@@ -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 };
|
|
@@ -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 };
|