agent-rules-init 0.6.2 → 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,7 +3,9 @@ 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 {
8
10
  /** @deprecated Project metadata is no longer requested interactively. */
9
11
  promptFn?: PromptFn;
@@ -22,6 +24,12 @@ export interface RunCliOptions {
22
24
  assistant?: AssistantId;
23
25
  /** Model forwarded verbatim to the assistant CLI; its default when omitted. */
24
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;
25
33
  /** Receives the rendered files before they are written (or planned). */
26
34
  onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
27
35
  /** Receives the deterministic generation fingerprint before optional enrichment. */
@@ -32,39 +40,11 @@ export interface RunCliOptions {
32
40
  onConfigWarnings?: (warnings: readonly string[]) => void;
33
41
  /** Receives the facts extracted from the repo (including canonical commands). */
34
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;
35
47
  }
36
48
  export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
37
- export interface CliRunOptions {
38
- lang?: Lang;
39
- dryRun?: true;
40
- force?: true;
41
- apply?: true;
42
- check?: true;
43
- json?: true;
44
- nonInteractive?: true;
45
- enrich?: true;
46
- assistant?: AssistantId;
47
- model?: string;
48
- }
49
- export type CliAction = ({
50
- kind: "run";
51
- } & CliRunOptions) | {
52
- kind: "help";
53
- } | {
54
- kind: "version";
55
- } | {
56
- kind: "invalid-lang";
57
- value: string;
58
- } | {
59
- kind: "invalid-assistant";
60
- value: string;
61
- } | {
62
- kind: "missing-value";
63
- flag: string;
64
- } | {
65
- kind: "unknown";
66
- flag: string;
67
- };
68
- export declare function resolveCliAction(argv: string[]): CliAction;
69
49
  export declare function getVersion(): string;
70
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";
@@ -11,73 +12,48 @@ 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
14
  import { hasInteractiveTty, } from "./core/prompt-engine.js";
14
- import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment, defaultExecFn, } from "./core/llm-bridge.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
31
  const ui = UI[lang];
80
- 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);
81
57
  const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
82
58
  // Missing framework/tooling signals are valid facts, not questions the user should
83
59
  // have to answer. Keep the low-confidence `none` value and render conservative,
@@ -88,7 +64,7 @@ export async function runCli(rootPath, options = {}) {
88
64
  options.onFacts?.(facts);
89
65
  const ctx = { facts, signals };
90
66
  const entries = detections.map((detection) => {
91
- const pack = ALL_PACKS.find((p) => p.id === detection.packId);
67
+ const pack = findPack(detection.packId);
92
68
  return { detection, ruleSet: pack.rules(detection, lang, ctx) };
93
69
  });
94
70
  const files = [];
@@ -105,7 +81,7 @@ export async function runCli(rootPath, options = {}) {
105
81
  });
106
82
  files.push({ path: "GEMINI.generated.md", content: renderGeminiMd(entries, facts, lang) });
107
83
  for (const detection of detections) {
108
- const pack = ALL_PACKS.find((p) => p.id === detection.packId);
84
+ const pack = findPack(detection.packId);
109
85
  for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
110
86
  files.push(file);
111
87
  }
@@ -130,6 +106,7 @@ export async function runCli(rootPath, options = {}) {
130
106
  }
131
107
  const baselineHash = hashGeneratedFiles(files);
132
108
  options.onBaselineHash?.(baselineHash);
109
+ let acceptedEnrichmentState;
133
110
  const wantsEnrich = (options.enrich ?? config.enrich) === true;
134
111
  const canOfferEnrich = !options.nonInteractive && hasInteractiveTty();
135
112
  if (!options.skipLlm && !config.noAi && (wantsEnrich || canOfferEnrich)) {
@@ -153,30 +130,77 @@ export async function runCli(rootPath, options = {}) {
153
130
  proceed = (await clack.confirm({ message: ui.enrichConfirm(chosenAssistant) })) === true;
154
131
  }
155
132
  if (proceed) {
156
- const estimate = estimateEnrichment(files);
157
- if (estimate.batches > 1)
158
- notify(ui.enrichLargeInput(estimate.characters, estimate.batches));
159
- const spinner = canOfferEnrich ? clack.spinner() : undefined;
160
- if (spinner)
161
- spinner.start(ui.enrichWorking(chosenAssistant));
162
- else
163
- notify(ui.enrichWorking(chosenAssistant));
164
- const enriched = await enrichFilesWithAssistant(chosenAssistant, files, {
165
- execFn,
166
- lang,
167
- cwd: rootPath,
168
- mustKeep: facts.canonical.map((c) => c.command),
169
- existingDocs: readExistingDocs(rootPath),
170
- model: options.model ?? config.model,
171
- onMetrics: options.onEnrichMetrics,
172
- });
173
- const changed = enriched.some((file, i) => file.content !== files[i].content);
174
- const outcome = changed ? ui.enrichDone : ui.enrichKept;
175
- if (spinner)
176
- spinner.stop(outcome);
177
- else
178
- notify(outcome);
179
- 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
+ }
180
204
  }
181
205
  }
182
206
  }
@@ -191,76 +215,10 @@ export async function runCli(rootPath, options = {}) {
191
215
  }
192
216
  const results = writeGeneratedFiles(rootPath, files, { force: options.force });
193
217
  const completeRefresh = results.every((result) => result.status === "written" || result.status === "overwritten");
194
- if (completeRefresh)
195
- writeGenerationState(rootPath, makeGenerationState(baselineHash, files));
196
- return results;
197
- }
198
- function isLang(value) {
199
- return value === "es" || value === "en";
200
- }
201
- function isAssistant(value) {
202
- return value === "claude" || value === "codex";
203
- }
204
- export function resolveCliAction(argv) {
205
- const options = {};
206
- for (let i = 0; i < argv.length; i++) {
207
- const arg = argv[i];
208
- if (arg === "--help" || arg === "-h")
209
- return { kind: "help" };
210
- if (arg === "--version" || arg === "-v")
211
- return { kind: "version" };
212
- if (arg === "--lang" || arg.startsWith("--lang=")) {
213
- const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
214
- if (!isLang(value))
215
- return { kind: "invalid-lang", value };
216
- options.lang = value;
217
- continue;
218
- }
219
- if (arg === "--dry-run") {
220
- options.dryRun = true;
221
- continue;
222
- }
223
- if (arg === "--force") {
224
- options.force = true;
225
- continue;
226
- }
227
- if (arg === "--apply") {
228
- options.apply = true;
229
- continue;
230
- }
231
- if (arg === "--check") {
232
- options.check = true;
233
- continue;
234
- }
235
- if (arg === "--json") {
236
- options.json = true;
237
- continue;
238
- }
239
- if (arg === "--non-interactive") {
240
- options.nonInteractive = true;
241
- continue;
242
- }
243
- if (arg === "--enrich") {
244
- options.enrich = true;
245
- continue;
246
- }
247
- if (arg === "--assistant" || arg.startsWith("--assistant=")) {
248
- const value = arg.startsWith("--assistant=") ? arg.slice("--assistant=".length) : argv[++i] ?? "";
249
- if (!isAssistant(value))
250
- return { kind: "invalid-assistant", value };
251
- options.assistant = value;
252
- continue;
253
- }
254
- if (arg === "--model" || arg.startsWith("--model=")) {
255
- const value = arg.startsWith("--model=") ? arg.slice("--model=".length) : argv[++i] ?? "";
256
- if (value === "")
257
- return { kind: "missing-value", flag: "--model" };
258
- options.model = value;
259
- continue;
260
- }
261
- return { kind: "unknown", flag: arg };
218
+ if (completeRefresh) {
219
+ writeGenerationState(rootPath, makeGenerationState(baselineHash, files, acceptedEnrichmentState));
262
220
  }
263
- return { kind: "run", ...options };
221
+ return results;
264
222
  }
265
223
  function usageWithAutomationOptions(ui) {
266
224
  return `${ui.usage}\n\n${ui.automationUsage}`;
@@ -298,6 +256,16 @@ export async function main() {
298
256
  process.exitCode = 1;
299
257
  return;
300
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
+ }
301
269
  if (action.kind === "missing-value") {
302
270
  console.error(`${ui.missingFlagValue(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
303
271
  process.exitCode = 1;
@@ -334,6 +302,8 @@ export async function main() {
334
302
  let repoFacts;
335
303
  let baselineHash;
336
304
  let enrichMetrics;
305
+ let scanWarnings = [];
306
+ let scanStats;
337
307
  const results = await runCli(process.cwd(), {
338
308
  lang,
339
309
  config: loadedConfig.config,
@@ -344,6 +314,9 @@ export async function main() {
344
314
  enrich,
345
315
  assistant: action.assistant ?? loadedConfig.config.assistant,
346
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,
347
320
  onGeneratedFiles: (files) => {
348
321
  generatedFiles = files;
349
322
  },
@@ -356,7 +329,17 @@ export async function main() {
356
329
  onFacts: (facts) => {
357
330
  repoFacts = facts;
358
331
  },
332
+ onScanWarnings: (warnings) => {
333
+ scanWarnings = warnings;
334
+ },
335
+ onScanStats: (stats) => {
336
+ scanStats = stats;
337
+ },
338
+ useScannerWorker: true,
359
339
  });
340
+ if (!machineOutput)
341
+ for (const warning of scanWarnings)
342
+ console.warn(warning);
360
343
  let activationResults = [];
361
344
  if (apply) {
362
345
  if (!baselineHash)
@@ -377,6 +360,8 @@ export async function main() {
377
360
  console.log(JSON.stringify({
378
361
  mode: action.check ? "check" : action.dryRun ? "dry-run" : apply ? "apply" : "write",
379
362
  configWarnings: loadedConfig.warnings,
363
+ scanWarnings,
364
+ scanStats,
380
365
  facts: repoFacts,
381
366
  wouldCreate: action.check ? missing.length : written.length,
382
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;