agent-rules-init 0.6.1 → 0.7.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/README.md CHANGED
@@ -1,17 +1,26 @@
1
- # agent-rules-init
2
-
1
+ # agent-rules-init
2
+
3
+ This is the publishable CLI package from the
4
+ [`agent-rules-init` repository](https://github.com/racama29/agent-rules-init).
5
+ The root README is the canonical source for the complete feature, configuration,
6
+ security and contribution documentation.
7
+
8
+ The operating model is intentionally simple: generate staging files, review them,
9
+ then activate them with `npx agent-rules-init --apply`. Existing active files are
10
+ backed up before replacement.
11
+
3
12
  Generate repository-specific instructions for Claude Code, Codex, GitHub Copilot, Cursor, Gemini CLI and Windsurf from the manifests and commands that already exist in your project.
4
-
5
- ```bash
6
- npx agent-rules-init
7
- ```
8
-
9
- The CLI detects JavaScript/TypeScript, Python, Java, PHP, Ruby, Go, Rust, C#/.NET, Kotlin, Swift, Dart/Flutter, C/C++, Elixir, Scala and R projects. It also understands mixed repositories and npm, pnpm, Yarn and Bun workspaces, generating package-scoped `AGENTS.generated.md` files.
10
-
13
+
14
+ ```bash
15
+ npx agent-rules-init
16
+ ```
17
+
18
+ The CLI detects JavaScript/TypeScript, Python, Java, PHP, Ruby, Go, Rust, C#/.NET, Kotlin, Swift, Dart/Flutter, C/C++, Elixir, Scala and R projects. It also understands mixed repositories and npm, pnpm, Yarn and Bun workspaces, generating package-scoped `AGENTS.generated.md` files.
19
+
11
20
  All output is written with a `.generated` suffix and existing files are never overwritten during generation. Review them and run `--apply` to activate with safe backups.
12
-
13
- Use `--lang es` or `--lang en` to select the output language. See the [full documentation](https://github.com/racama29/agent-rules-init#readme) for supported frameworks, generated files and contribution instructions.
14
-
21
+
22
+ Use `--lang es` or `--lang en` to select the output language. See the [full documentation](https://github.com/racama29/agent-rules-init#readme) for supported frameworks, generated files and contribution instructions.
23
+
15
24
  Automation is supported through `--dry-run`, `--force`, `--apply`, `--check`, `--json` and `--non-interactive`. Repository defaults, enrichment assistant/model and per-project overrides can be stored in `.agent-rules-init.yml`.
16
25
 
17
26
  Use `--apply` after review to activate staging files; replaced finals are backed up under `.agent-rules-init/backups/`.
@@ -20,10 +29,12 @@ Complete generations store a git-ignored hash receipt so `--check` can verify de
20
29
 
21
30
  AI enrichment runs assistants with read-only restrictions and falls back to deterministic output when invocation or validation fails. Review generated files before activation, especially when the repository did not come from a trusted source.
22
31
 
23
- Enrichment also reports batch, retry, input-size, fallback and duration metrics.
24
-
25
- Generated documents share an evidence-backed model but are not duplicates: Claude receives broad repository context, AGENTS receives operational commands and scope, and Copilot receives concise implementation conventions. Observed architecture and local conventions include their source files so specific claims can be audited.
26
-
27
- ## License
28
-
29
- MIT
32
+ Verified enrichment is reused when repository inputs, assistant/model and accepted staging hashes are unchanged. Use `--no-enrich-cache` for a deliberate fresh run, and bound latency with `--enrich-timeout <seconds>` plus `--enrich-retries <0..2>`.
33
+
34
+ Semantic validation rejects prompt-injection language, new sections, destructive instructions, commands absent from repository facts and evidence paths outside the repository. Metrics report cache hits, changed lines and security rejections in addition to batch, retry, input-size, fallback and duration data.
35
+
36
+ Generated documents share an evidence-backed model but are not duplicates: Claude receives broad repository context, AGENTS receives operational commands and scope, and Copilot receives concise implementation conventions. Observed architecture and local conventions include their source files so specific claims can be audited.
37
+
38
+ ## License
39
+
40
+ MIT
package/dist/bin.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from "./cli.js";
3
- main();
3
+ void main();
package/dist/cli.d.ts CHANGED
@@ -3,8 +3,11 @@ import { type Lang } from "./core/i18n.js";
3
3
  import { type AgentRulesConfig } from "./core/config.js";
4
4
  import { type PromptFn } from "./core/prompt-engine.js";
5
5
  import { type AssistantId, type EnrichMetrics, type ExecFn } from "./core/llm-bridge.js";
6
- import type { RepoFacts } from "./core/types.js";
6
+ import type { RepoFacts, RepoSignals } from "./core/types.js";
7
+ export { resolveCliAction } from "./core/cli-options.js";
8
+ export type { CliAction, CliRunOptions } from "./core/cli-options.js";
7
9
  export interface RunCliOptions {
10
+ /** @deprecated Project metadata is no longer requested interactively. */
8
11
  promptFn?: PromptFn;
9
12
  execFn?: ExecFn;
10
13
  skipLlm?: boolean;
@@ -21,6 +24,12 @@ export interface RunCliOptions {
21
24
  assistant?: AssistantId;
22
25
  /** Model forwarded verbatim to the assistant CLI; its default when omitted. */
23
26
  model?: string;
27
+ /** Per-assistant-attempt timeout in seconds. */
28
+ enrichTimeoutSeconds?: number;
29
+ /** Disable reuse of verified enriched staging for this invocation. */
30
+ noEnrichCache?: boolean;
31
+ /** Number of retries after the first assistant attempt (0..2). */
32
+ enrichRetries?: number;
24
33
  /** Receives the rendered files before they are written (or planned). */
25
34
  onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
26
35
  /** Receives the deterministic generation fingerprint before optional enrichment. */
@@ -31,39 +40,11 @@ export interface RunCliOptions {
31
40
  onConfigWarnings?: (warnings: readonly string[]) => void;
32
41
  /** Receives the facts extracted from the repo (including canonical commands). */
33
42
  onFacts?: (facts: RepoFacts) => void;
43
+ onScanWarnings?: (warnings: readonly string[]) => void;
44
+ onScanStats?: (stats: NonNullable<RepoSignals["scanStats"]>) => void;
45
+ /** Offload repository traversal to a worker thread. Enabled by the published binary. */
46
+ useScannerWorker?: boolean;
34
47
  }
35
48
  export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
36
- export interface CliRunOptions {
37
- lang?: Lang;
38
- dryRun?: true;
39
- force?: true;
40
- apply?: true;
41
- check?: true;
42
- json?: true;
43
- nonInteractive?: true;
44
- enrich?: true;
45
- assistant?: AssistantId;
46
- model?: string;
47
- }
48
- export type CliAction = ({
49
- kind: "run";
50
- } & CliRunOptions) | {
51
- kind: "help";
52
- } | {
53
- kind: "version";
54
- } | {
55
- kind: "invalid-lang";
56
- value: string;
57
- } | {
58
- kind: "invalid-assistant";
59
- value: string;
60
- } | {
61
- kind: "missing-value";
62
- flag: string;
63
- } | {
64
- kind: "unknown";
65
- flag: string;
66
- };
67
- export declare function resolveCliAction(argv: string[]): CliAction;
68
49
  export declare function getVersion(): string;
69
50
  export declare function main(): Promise<void>;
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import * as clack from "@clack/prompts";
5
5
  import { scanRepo } from "./core/scanner.js";
6
+ import { scanRepoInWorker } from "./core/scanner-async.js";
6
7
  import { writeGeneratedFiles } from "./core/writer.js";
7
8
  import { renderClaudeMd, renderAgentsMd, renderCopilotInstructions, renderCursorRules, renderGeminiMd, renderPromptFiles, } from "./core/templates.js";
8
9
  import { buildRepoFacts } from "./core/repo-facts.js";
@@ -10,84 +11,60 @@ import { UI, detectLang } from "./core/i18n.js";
10
11
  import { loadConfig } from "./core/config.js";
11
12
  import { applyProjectExcludes, buildPackageUnits } from "./core/project-units.js";
12
13
  import { renderProjectUnitAgents } from "./core/project-unit-output.js";
13
- import { collectLowConfidenceQuestions, askQuestions, applyAnswers, makeDefaultPromptFn, hasInteractiveTty, } from "./core/prompt-engine.js";
14
- import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment, defaultExecFn, } from "./core/llm-bridge.js";
14
+ import { hasInteractiveTty, } from "./core/prompt-engine.js";
15
+ import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment, createDefaultExecFn, DEFAULT_EXEC_TIMEOUT_MS, summarizeEnrichmentChanges, } from "./core/llm-bridge.js";
15
16
  import { hashGeneratedFiles, makeGenerationState, writeGenerationState, } from "./core/generation-state.js";
17
+ import { loadCachedEnrichment, makeEnrichmentState } from "./core/enrichment-cache.js";
16
18
  import { evaluateGenerationCheck } from "./core/check-state.js";
17
19
  import { applyGeneratedFiles } from "./core/activation.js";
18
- import { jsTsPack } from "./packs/js-ts.js";
19
- import { pythonPack } from "./packs/python.js";
20
- import { javaPack } from "./packs/java.js";
21
- import { phpPack } from "./packs/php.js";
22
- import { rubyPack } from "./packs/ruby.js";
23
- import { goPack } from "./packs/go.js";
24
- import { rustPack } from "./packs/rust.js";
25
- import { csharpPack } from "./packs/csharp.js";
26
- import { kotlinPack } from "./packs/kotlin.js";
27
- import { swiftPack } from "./packs/swift.js";
28
- import { dartPack } from "./packs/dart.js";
29
- import { cppPack } from "./packs/cpp.js";
30
- import { elixirPack } from "./packs/elixir.js";
31
- import { scalaPack } from "./packs/scala.js";
32
- import { rPack } from "./packs/r.js";
33
- const ALL_PACKS = [
34
- jsTsPack,
35
- pythonPack,
36
- javaPack,
37
- phpPack,
38
- rubyPack,
39
- goPack,
40
- rustPack,
41
- csharpPack,
42
- kotlinPack,
43
- swiftPack,
44
- dartPack,
45
- cppPack,
46
- elixirPack,
47
- scalaPack,
48
- rPack,
49
- ];
50
- // Final-name docs the tool itself would generate; when they already exist they carry the
51
- // team's hand-written intent, so enrichment must integrate them instead of ignoring them.
52
- const EXISTING_DOC_PATHS = [
53
- "CLAUDE.md",
54
- "AGENTS.md",
55
- "GEMINI.md",
56
- ".github/copilot-instructions.md",
57
- ".cursor/rules/repository.mdc",
58
- ];
59
- const MAX_EXISTING_DOC_CHARS = 20_000;
60
- function readExistingDocs(rootPath) {
61
- const docs = [];
62
- for (const relativePath of EXISTING_DOC_PATHS) {
63
- const absolutePath = path.join(rootPath, relativePath);
64
- if (!fs.existsSync(absolutePath))
65
- continue;
66
- const content = fs.readFileSync(absolutePath, "utf8");
67
- if (content.trim() === "")
68
- continue;
69
- docs.push({ path: relativePath, content: content.slice(0, MAX_EXISTING_DOC_CHARS) });
70
- }
71
- return docs;
72
- }
20
+ import { ALL_PACKS, findPack } from "./packs/index.js";
21
+ import { readExistingDocs } from "./core/existing-docs.js";
22
+ import { resolveCliAction } from "./core/cli-options.js";
23
+ export { resolveCliAction } from "./core/cli-options.js";
73
24
  export async function runCli(rootPath, options = {}) {
74
25
  const loadedConfig = options.config ? { config: options.config, warnings: [] } : loadConfig(rootPath);
75
26
  const config = loadedConfig.config;
76
27
  options.onConfigWarnings?.(loadedConfig.warnings);
77
- const execFn = options.execFn ?? defaultExecFn;
28
+ const timeoutSeconds = options.enrichTimeoutSeconds ?? config.enrichTimeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_MS / 1000;
29
+ const execFn = options.execFn ?? createDefaultExecFn(timeoutSeconds * 1000);
78
30
  const lang = options.lang ?? config.lang ?? detectLang();
79
- const promptFn = options.promptFn ?? makeDefaultPromptFn(lang);
80
31
  const ui = UI[lang];
81
- const signals = applyProjectExcludes(scanRepo(rootPath), config.exclude ?? []);
32
+ const scanOptions = {
33
+ maxDepth: config.scanMaxDepth,
34
+ maxFiles: config.scanMaxFiles,
35
+ };
36
+ let scanned;
37
+ if (options.useScannerWorker) {
38
+ try {
39
+ scanned = await scanRepoInWorker(rootPath, scanOptions, (config.scanWorkerTimeoutSeconds ?? 30) * 1_000);
40
+ }
41
+ catch (error) {
42
+ scanned = scanRepo(rootPath, scanOptions);
43
+ scanned.scanWarnings = [
44
+ ...(scanned.scanWarnings ?? []),
45
+ `Scanner worker unavailable; used synchronous fallback: ${error.message}`,
46
+ ];
47
+ if (scanned.scanStats)
48
+ scanned.scanStats.mode = "sync-fallback";
49
+ }
50
+ }
51
+ else
52
+ scanned = scanRepo(rootPath, scanOptions);
53
+ const signals = applyProjectExcludes(scanned, config.exclude ?? []);
54
+ options.onScanWarnings?.(signals.scanWarnings ?? []);
55
+ if (signals.scanStats)
56
+ options.onScanStats?.(signals.scanStats);
82
57
  const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
83
- const questions = collectLowConfidenceQuestions(rawDetections, lang);
84
- const answers = options.nonInteractive ? {} : await askQuestions(questions, promptFn);
85
- const detections = applyAnswers(rawDetections, answers);
58
+ // Missing framework/tooling signals are valid facts, not questions the user should
59
+ // have to answer. Keep the low-confidence `none` value and render conservative,
60
+ // framework-neutral guidance; explicit repository config can still override project
61
+ // metadata when a team wants to provide it.
62
+ const detections = rawDetections;
86
63
  const facts = buildRepoFacts(signals, lang);
87
64
  options.onFacts?.(facts);
88
65
  const ctx = { facts, signals };
89
66
  const entries = detections.map((detection) => {
90
- const pack = ALL_PACKS.find((p) => p.id === detection.packId);
67
+ const pack = findPack(detection.packId);
91
68
  return { detection, ruleSet: pack.rules(detection, lang, ctx) };
92
69
  });
93
70
  const files = [];
@@ -104,7 +81,7 @@ export async function runCli(rootPath, options = {}) {
104
81
  });
105
82
  files.push({ path: "GEMINI.generated.md", content: renderGeminiMd(entries, facts, lang) });
106
83
  for (const detection of detections) {
107
- const pack = ALL_PACKS.find((p) => p.id === detection.packId);
84
+ const pack = findPack(detection.packId);
108
85
  for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
109
86
  files.push(file);
110
87
  }
@@ -129,6 +106,7 @@ export async function runCli(rootPath, options = {}) {
129
106
  }
130
107
  const baselineHash = hashGeneratedFiles(files);
131
108
  options.onBaselineHash?.(baselineHash);
109
+ let acceptedEnrichmentState;
132
110
  const wantsEnrich = (options.enrich ?? config.enrich) === true;
133
111
  const canOfferEnrich = !options.nonInteractive && hasInteractiveTty();
134
112
  if (!options.skipLlm && !config.noAi && (wantsEnrich || canOfferEnrich)) {
@@ -152,30 +130,77 @@ export async function runCli(rootPath, options = {}) {
152
130
  proceed = (await clack.confirm({ message: ui.enrichConfirm(chosenAssistant) })) === true;
153
131
  }
154
132
  if (proceed) {
155
- const estimate = estimateEnrichment(files);
156
- if (estimate.batches > 1)
157
- notify(ui.enrichLargeInput(estimate.characters, estimate.batches));
158
- const spinner = canOfferEnrich ? clack.spinner() : undefined;
159
- if (spinner)
160
- spinner.start(ui.enrichWorking(chosenAssistant));
161
- else
162
- notify(ui.enrichWorking(chosenAssistant));
163
- const enriched = await enrichFilesWithAssistant(chosenAssistant, files, {
164
- execFn,
165
- lang,
166
- cwd: rootPath,
167
- mustKeep: facts.canonical.map((c) => c.command),
168
- existingDocs: readExistingDocs(rootPath),
169
- model: options.model ?? config.model,
170
- onMetrics: options.onEnrichMetrics,
171
- });
172
- const changed = enriched.some((file, i) => file.content !== files[i].content);
173
- const outcome = changed ? ui.enrichDone : ui.enrichKept;
174
- if (spinner)
175
- spinner.stop(outcome);
176
- else
177
- notify(outcome);
178
- files.splice(0, files.length, ...enriched);
133
+ const existingDocs = readExistingDocs(rootPath);
134
+ const model = options.model ?? config.model;
135
+ const requestedState = makeEnrichmentState(rootPath, signals.files, baselineHash, existingDocs, chosenAssistant, model);
136
+ const useCache = options.noEnrichCache !== true && config.enrichCache !== false;
137
+ const cached = useCache
138
+ ? loadCachedEnrichment(rootPath, baselineHash, files, requestedState)
139
+ : undefined;
140
+ if (cached) {
141
+ const changes = summarizeEnrichmentChanges(files, cached);
142
+ options.onEnrichMetrics?.({
143
+ assistant: chosenAssistant,
144
+ model,
145
+ batches: 0,
146
+ attempts: 0,
147
+ fallbackBatches: 0,
148
+ inputChars: 0,
149
+ outputChars: cached.reduce((sum, file) => sum + file.content.length, 0),
150
+ durationMs: 0,
151
+ cacheHit: true,
152
+ ...changes,
153
+ securityRejections: 0,
154
+ });
155
+ acceptedEnrichmentState = requestedState;
156
+ files.splice(0, files.length, ...cached);
157
+ notify(ui.enrichCacheHit);
158
+ }
159
+ else {
160
+ const estimate = estimateEnrichment(files);
161
+ if (estimate.batches > 1)
162
+ notify(ui.enrichLargeInput(estimate.characters, estimate.batches));
163
+ const spinner = canOfferEnrich ? clack.spinner() : undefined;
164
+ const retries = options.enrichRetries ?? config.enrichRetries ?? 1;
165
+ notify(ui.enrichBudget(timeoutSeconds, retries + 1));
166
+ if (spinner)
167
+ spinner.start(ui.enrichWorking(chosenAssistant));
168
+ else
169
+ notify(ui.enrichWorking(chosenAssistant));
170
+ let runMetrics;
171
+ const verifiedCommands = [...new Set([
172
+ ...facts.canonical.map((entry) => entry.command),
173
+ ...facts.commands.map((entry) => entry.invocation),
174
+ ...facts.ciCommands.map((entry) => entry.command),
175
+ ])];
176
+ const enriched = await enrichFilesWithAssistant(chosenAssistant, files, {
177
+ execFn,
178
+ lang,
179
+ cwd: rootPath,
180
+ mustKeep: verifiedCommands,
181
+ existingDocs,
182
+ model,
183
+ maxAttempts: retries + 1,
184
+ onMetrics: (metrics) => {
185
+ runMetrics = metrics;
186
+ options.onEnrichMetrics?.(metrics);
187
+ },
188
+ });
189
+ const changed = enriched.some((file, i) => file.content !== files[i].content);
190
+ const outcome = changed ? ui.enrichDone : ui.enrichKept;
191
+ if (spinner)
192
+ spinner.stop(outcome);
193
+ else
194
+ notify(outcome);
195
+ files.splice(0, files.length, ...enriched);
196
+ if (changed && runMetrics && runMetrics.fallbackBatches === 0) {
197
+ acceptedEnrichmentState = requestedState;
198
+ }
199
+ else if (!changed) {
200
+ // Never cache a deterministic fallback as a successful enrichment.
201
+ acceptedEnrichmentState = undefined;
202
+ }
203
+ }
179
204
  }
180
205
  }
181
206
  }
@@ -190,76 +215,10 @@ export async function runCli(rootPath, options = {}) {
190
215
  }
191
216
  const results = writeGeneratedFiles(rootPath, files, { force: options.force });
192
217
  const completeRefresh = results.every((result) => result.status === "written" || result.status === "overwritten");
193
- if (completeRefresh)
194
- writeGenerationState(rootPath, makeGenerationState(baselineHash, files));
195
- return results;
196
- }
197
- function isLang(value) {
198
- return value === "es" || value === "en";
199
- }
200
- function isAssistant(value) {
201
- return value === "claude" || value === "codex";
202
- }
203
- export function resolveCliAction(argv) {
204
- const options = {};
205
- for (let i = 0; i < argv.length; i++) {
206
- const arg = argv[i];
207
- if (arg === "--help" || arg === "-h")
208
- return { kind: "help" };
209
- if (arg === "--version" || arg === "-v")
210
- return { kind: "version" };
211
- if (arg === "--lang" || arg.startsWith("--lang=")) {
212
- const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
213
- if (!isLang(value))
214
- return { kind: "invalid-lang", value };
215
- options.lang = value;
216
- continue;
217
- }
218
- if (arg === "--dry-run") {
219
- options.dryRun = true;
220
- continue;
221
- }
222
- if (arg === "--force") {
223
- options.force = true;
224
- continue;
225
- }
226
- if (arg === "--apply") {
227
- options.apply = true;
228
- continue;
229
- }
230
- if (arg === "--check") {
231
- options.check = true;
232
- continue;
233
- }
234
- if (arg === "--json") {
235
- options.json = true;
236
- continue;
237
- }
238
- if (arg === "--non-interactive") {
239
- options.nonInteractive = true;
240
- continue;
241
- }
242
- if (arg === "--enrich") {
243
- options.enrich = true;
244
- continue;
245
- }
246
- if (arg === "--assistant" || arg.startsWith("--assistant=")) {
247
- const value = arg.startsWith("--assistant=") ? arg.slice("--assistant=".length) : argv[++i] ?? "";
248
- if (!isAssistant(value))
249
- return { kind: "invalid-assistant", value };
250
- options.assistant = value;
251
- continue;
252
- }
253
- if (arg === "--model" || arg.startsWith("--model=")) {
254
- const value = arg.startsWith("--model=") ? arg.slice("--model=".length) : argv[++i] ?? "";
255
- if (value === "")
256
- return { kind: "missing-value", flag: "--model" };
257
- options.model = value;
258
- continue;
259
- }
260
- return { kind: "unknown", flag: arg };
218
+ if (completeRefresh) {
219
+ writeGenerationState(rootPath, makeGenerationState(baselineHash, files, acceptedEnrichmentState));
261
220
  }
262
- return { kind: "run", ...options };
221
+ return results;
263
222
  }
264
223
  function usageWithAutomationOptions(ui) {
265
224
  return `${ui.usage}\n\n${ui.automationUsage}`;
@@ -297,6 +256,16 @@ export async function main() {
297
256
  process.exitCode = 1;
298
257
  return;
299
258
  }
259
+ if (action.kind === "invalid-timeout") {
260
+ console.error(`${ui.invalidTimeout(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
261
+ process.exitCode = 1;
262
+ return;
263
+ }
264
+ if (action.kind === "invalid-retries") {
265
+ console.error(`${ui.invalidRetries(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
266
+ process.exitCode = 1;
267
+ return;
268
+ }
300
269
  if (action.kind === "missing-value") {
301
270
  console.error(`${ui.missingFlagValue(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
302
271
  process.exitCode = 1;
@@ -333,6 +302,8 @@ export async function main() {
333
302
  let repoFacts;
334
303
  let baselineHash;
335
304
  let enrichMetrics;
305
+ let scanWarnings = [];
306
+ let scanStats;
336
307
  const results = await runCli(process.cwd(), {
337
308
  lang,
338
309
  config: loadedConfig.config,
@@ -343,6 +314,9 @@ export async function main() {
343
314
  enrich,
344
315
  assistant: action.assistant ?? loadedConfig.config.assistant,
345
316
  model: action.model ?? loadedConfig.config.model,
317
+ enrichTimeoutSeconds: action.enrichTimeoutSeconds ?? loadedConfig.config.enrichTimeoutSeconds,
318
+ noEnrichCache: action.noEnrichCache === true,
319
+ enrichRetries: action.enrichRetries ?? loadedConfig.config.enrichRetries,
346
320
  onGeneratedFiles: (files) => {
347
321
  generatedFiles = files;
348
322
  },
@@ -355,7 +329,17 @@ export async function main() {
355
329
  onFacts: (facts) => {
356
330
  repoFacts = facts;
357
331
  },
332
+ onScanWarnings: (warnings) => {
333
+ scanWarnings = warnings;
334
+ },
335
+ onScanStats: (stats) => {
336
+ scanStats = stats;
337
+ },
338
+ useScannerWorker: true,
358
339
  });
340
+ if (!machineOutput)
341
+ for (const warning of scanWarnings)
342
+ console.warn(warning);
359
343
  let activationResults = [];
360
344
  if (apply) {
361
345
  if (!baselineHash)
@@ -376,6 +360,8 @@ export async function main() {
376
360
  console.log(JSON.stringify({
377
361
  mode: action.check ? "check" : action.dryRun ? "dry-run" : apply ? "apply" : "write",
378
362
  configWarnings: loadedConfig.warnings,
363
+ scanWarnings,
364
+ scanStats,
379
365
  facts: repoFacts,
380
366
  wouldCreate: action.check ? missing.length : written.length,
381
367
  missing: missing.map((file) => file.path),
@@ -0,0 +1,43 @@
1
+ import type { Lang } from "./i18n.js";
2
+ import type { AssistantId } from "./llm-bridge.js";
3
+ export interface CliRunOptions {
4
+ lang?: Lang;
5
+ dryRun?: true;
6
+ force?: true;
7
+ apply?: true;
8
+ check?: true;
9
+ json?: true;
10
+ nonInteractive?: true;
11
+ enrich?: true;
12
+ assistant?: AssistantId;
13
+ model?: string;
14
+ enrichTimeoutSeconds?: number;
15
+ noEnrichCache?: true;
16
+ enrichRetries?: number;
17
+ }
18
+ export type CliAction = ({
19
+ kind: "run";
20
+ } & CliRunOptions) | {
21
+ kind: "help";
22
+ } | {
23
+ kind: "version";
24
+ } | {
25
+ kind: "invalid-lang";
26
+ value: string;
27
+ } | {
28
+ kind: "invalid-assistant";
29
+ value: string;
30
+ } | {
31
+ kind: "invalid-timeout";
32
+ value: string;
33
+ } | {
34
+ kind: "invalid-retries";
35
+ value: string;
36
+ } | {
37
+ kind: "missing-value";
38
+ flag: string;
39
+ } | {
40
+ kind: "unknown";
41
+ flag: string;
42
+ };
43
+ export declare function resolveCliAction(argv: string[]): CliAction;
@@ -0,0 +1,70 @@
1
+ function optionValue(argv, index, name) {
2
+ const argument = argv[index];
3
+ return argument.startsWith(`${name}=`)
4
+ ? [argument.slice(name.length + 1), index]
5
+ : [argv[index + 1] ?? "", index + 1];
6
+ }
7
+ export function resolveCliAction(argv) {
8
+ const options = {};
9
+ for (let index = 0; index < argv.length; index++) {
10
+ const argument = argv[index];
11
+ if (argument === "--help" || argument === "-h")
12
+ return { kind: "help" };
13
+ if (argument === "--version" || argument === "-v")
14
+ return { kind: "version" };
15
+ if (argument === "--lang" || argument.startsWith("--lang=")) {
16
+ const [value, consumed] = optionValue(argv, index, "--lang");
17
+ index = consumed;
18
+ if (value !== "es" && value !== "en")
19
+ return { kind: "invalid-lang", value };
20
+ options.lang = value;
21
+ continue;
22
+ }
23
+ if (argument === "--assistant" || argument.startsWith("--assistant=")) {
24
+ const [value, consumed] = optionValue(argv, index, "--assistant");
25
+ index = consumed;
26
+ if (value !== "claude" && value !== "codex")
27
+ return { kind: "invalid-assistant", value };
28
+ options.assistant = value;
29
+ continue;
30
+ }
31
+ if (argument === "--model" || argument.startsWith("--model=")) {
32
+ const [value, consumed] = optionValue(argv, index, "--model");
33
+ index = consumed;
34
+ if (!value)
35
+ return { kind: "missing-value", flag: "--model" };
36
+ options.model = value;
37
+ continue;
38
+ }
39
+ if (argument === "--enrich-timeout" || argument.startsWith("--enrich-timeout=")) {
40
+ const [value, consumed] = optionValue(argv, index, "--enrich-timeout");
41
+ index = consumed;
42
+ const parsed = Number(value);
43
+ if (!Number.isInteger(parsed) || parsed < 10 || parsed > 3600)
44
+ return { kind: "invalid-timeout", value };
45
+ options.enrichTimeoutSeconds = parsed;
46
+ continue;
47
+ }
48
+ if (argument === "--enrich-retries" || argument.startsWith("--enrich-retries=")) {
49
+ const [value, consumed] = optionValue(argv, index, "--enrich-retries");
50
+ index = consumed;
51
+ const parsed = Number(value);
52
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 2)
53
+ return { kind: "invalid-retries", value };
54
+ options.enrichRetries = parsed;
55
+ continue;
56
+ }
57
+ const booleanOptions = {
58
+ "--dry-run": "dryRun", "--force": "force", "--apply": "apply", "--check": "check",
59
+ "--json": "json", "--non-interactive": "nonInteractive", "--enrich": "enrich",
60
+ "--no-enrich-cache": "noEnrichCache",
61
+ };
62
+ const key = booleanOptions[argument];
63
+ if (key) {
64
+ Object.assign(options, { [key]: true });
65
+ continue;
66
+ }
67
+ return { kind: "unknown", flag: argument };
68
+ }
69
+ return { kind: "run", ...options };
70
+ }
@@ -13,6 +13,18 @@ export interface AgentRulesConfig {
13
13
  enrich?: boolean;
14
14
  assistant?: "claude" | "codex";
15
15
  model?: string;
16
+ /** Reuse verified enriched staging when repository inputs are unchanged. Defaults to true. */
17
+ enrichCache?: boolean;
18
+ /** Per-assistant-attempt timeout in seconds (10..3600). */
19
+ enrichTimeoutSeconds?: number;
20
+ /** Validation retries after the first attempt (0..2). */
21
+ enrichRetries?: number;
22
+ /** Maximum directory nesting inspected by the repository scanner (1..64). */
23
+ scanMaxDepth?: number;
24
+ /** Maximum files collected before stopping the repository scan (100..1,000,000). */
25
+ scanMaxFiles?: number;
26
+ /** Worker scan timeout before a synchronous compatibility fallback (1..300 seconds). */
27
+ scanWorkerTimeoutSeconds?: number;
16
28
  }
17
29
  export interface LoadedConfig {
18
30
  config: AgentRulesConfig;