agent-rules-init 0.6.0 → 0.6.1
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/LICENSE +1 -1
- package/README.md +6 -2
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +55 -41
- package/dist/core/activation.d.ts +12 -0
- package/dist/core/activation.js +104 -0
- package/dist/core/check-state.d.ts +18 -0
- package/dist/core/check-state.js +42 -0
- package/dist/core/i18n.d.ts +14 -0
- package/dist/core/i18n.js +22 -8
- package/dist/core/llm-bridge.d.ts +15 -0
- package/dist/core/llm-bridge.js +62 -4
- package/dist/core/scanner.js +1 -0
- package/package.json +8 -7
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 agent-rules-init contributors
|
|
3
|
+
Copyright (c) 2026 Rafael Camacho Marchena and agent-rules-init contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -8,15 +8,19 @@ npx agent-rules-init
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
-
All output is written with a `.generated` suffix and existing files are never overwritten. Review
|
|
11
|
+
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
12
|
|
|
13
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
14
|
|
|
15
|
-
Automation is supported through `--dry-run`, `--force`, `--check`, `--json` and `--non-interactive`. Repository defaults, enrichment assistant/model and per-project overrides can be stored in `.agent-rules-init.yml`.
|
|
15
|
+
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
|
+
|
|
17
|
+
Use `--apply` after review to activate staging files; replaced finals are backed up under `.agent-rules-init/backups/`.
|
|
16
18
|
|
|
17
19
|
Complete generations store a git-ignored hash receipt so `--check` can verify deterministic or enriched active files without rerunning an assistant. Activated final files take precedence over staging during checks.
|
|
18
20
|
|
|
19
21
|
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
|
+
|
|
23
|
+
Enrichment also reports batch, retry, input-size, fallback and duration metrics.
|
|
20
24
|
|
|
21
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.
|
|
22
26
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type GeneratedFile, type WriteResult } from "./core/writer.js";
|
|
|
2
2
|
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
|
-
import { type AssistantId, type ExecFn } from "./core/llm-bridge.js";
|
|
5
|
+
import { type AssistantId, type EnrichMetrics, type ExecFn } from "./core/llm-bridge.js";
|
|
6
6
|
import type { RepoFacts } from "./core/types.js";
|
|
7
7
|
export interface RunCliOptions {
|
|
8
8
|
promptFn?: PromptFn;
|
|
@@ -25,6 +25,7 @@ export interface RunCliOptions {
|
|
|
25
25
|
onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
|
|
26
26
|
/** Receives the deterministic generation fingerprint before optional enrichment. */
|
|
27
27
|
onBaselineHash?: (hash: string) => void;
|
|
28
|
+
onEnrichMetrics?: (metrics: EnrichMetrics) => void;
|
|
28
29
|
/** Preloaded repository configuration; loaded from disk when omitted. */
|
|
29
30
|
config?: AgentRulesConfig;
|
|
30
31
|
onConfigWarnings?: (warnings: readonly string[]) => void;
|
|
@@ -36,6 +37,7 @@ export interface CliRunOptions {
|
|
|
36
37
|
lang?: Lang;
|
|
37
38
|
dryRun?: true;
|
|
38
39
|
force?: true;
|
|
40
|
+
apply?: true;
|
|
39
41
|
check?: true;
|
|
40
42
|
json?: true;
|
|
41
43
|
nonInteractive?: true;
|
package/dist/cli.js
CHANGED
|
@@ -11,8 +11,10 @@ import { loadConfig } from "./core/config.js";
|
|
|
11
11
|
import { applyProjectExcludes, buildPackageUnits } from "./core/project-units.js";
|
|
12
12
|
import { renderProjectUnitAgents } from "./core/project-unit-output.js";
|
|
13
13
|
import { collectLowConfidenceQuestions, askQuestions, applyAnswers, makeDefaultPromptFn, hasInteractiveTty, } from "./core/prompt-engine.js";
|
|
14
|
-
import { detectAvailableAssistants, enrichFilesWithAssistant, defaultExecFn, } from "./core/llm-bridge.js";
|
|
15
|
-
import {
|
|
14
|
+
import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment, defaultExecFn, } from "./core/llm-bridge.js";
|
|
15
|
+
import { hashGeneratedFiles, makeGenerationState, writeGenerationState, } from "./core/generation-state.js";
|
|
16
|
+
import { evaluateGenerationCheck } from "./core/check-state.js";
|
|
17
|
+
import { applyGeneratedFiles } from "./core/activation.js";
|
|
16
18
|
import { jsTsPack } from "./packs/js-ts.js";
|
|
17
19
|
import { pythonPack } from "./packs/python.js";
|
|
18
20
|
import { javaPack } from "./packs/java.js";
|
|
@@ -150,6 +152,9 @@ export async function runCli(rootPath, options = {}) {
|
|
|
150
152
|
proceed = (await clack.confirm({ message: ui.enrichConfirm(chosenAssistant) })) === true;
|
|
151
153
|
}
|
|
152
154
|
if (proceed) {
|
|
155
|
+
const estimate = estimateEnrichment(files);
|
|
156
|
+
if (estimate.batches > 1)
|
|
157
|
+
notify(ui.enrichLargeInput(estimate.characters, estimate.batches));
|
|
153
158
|
const spinner = canOfferEnrich ? clack.spinner() : undefined;
|
|
154
159
|
if (spinner)
|
|
155
160
|
spinner.start(ui.enrichWorking(chosenAssistant));
|
|
@@ -162,6 +167,7 @@ export async function runCli(rootPath, options = {}) {
|
|
|
162
167
|
mustKeep: facts.canonical.map((c) => c.command),
|
|
163
168
|
existingDocs: readExistingDocs(rootPath),
|
|
164
169
|
model: options.model ?? config.model,
|
|
170
|
+
onMetrics: options.onEnrichMetrics,
|
|
165
171
|
});
|
|
166
172
|
const changed = enriched.some((file, i) => file.content !== files[i].content);
|
|
167
173
|
const outcome = changed ? ui.enrichDone : ui.enrichKept;
|
|
@@ -217,6 +223,10 @@ export function resolveCliAction(argv) {
|
|
|
217
223
|
options.force = true;
|
|
218
224
|
continue;
|
|
219
225
|
}
|
|
226
|
+
if (arg === "--apply") {
|
|
227
|
+
options.apply = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
220
230
|
if (arg === "--check") {
|
|
221
231
|
options.check = true;
|
|
222
232
|
continue;
|
|
@@ -294,7 +304,7 @@ export async function main() {
|
|
|
294
304
|
}
|
|
295
305
|
const machineOutput = action.json === true;
|
|
296
306
|
const planningOnly = action.dryRun === true || action.check === true;
|
|
297
|
-
const nonInteractive = action.nonInteractive === true || machineOutput || planningOnly;
|
|
307
|
+
const nonInteractive = action.nonInteractive === true || machineOutput || planningOnly || action.apply === true;
|
|
298
308
|
// Enrichment output is non-deterministic, so --check (freshness comparison) must stay
|
|
299
309
|
// on the deterministic baseline.
|
|
300
310
|
if (!machineOutput)
|
|
@@ -304,13 +314,17 @@ export async function main() {
|
|
|
304
314
|
}
|
|
305
315
|
try {
|
|
306
316
|
const loadedConfig = loadConfig(process.cwd());
|
|
307
|
-
const
|
|
317
|
+
const apply = action.apply === true && action.check !== true && action.dryRun !== true;
|
|
318
|
+
const configuredEnrich = (action.enrich ?? loadedConfig.config.enrich) === true;
|
|
319
|
+
const enrich = configuredEnrich && action.check !== true && !(apply && action.force !== true);
|
|
308
320
|
const lang = action.lang ?? loadedConfig.config.lang ?? defaultLang;
|
|
309
321
|
ui = UI[lang];
|
|
310
322
|
if (action.enrich === true && action.check === true)
|
|
311
323
|
console.warn(ui.enrichIgnoredWithCheck);
|
|
312
324
|
if (action.force === true && action.check === true)
|
|
313
325
|
console.warn(ui.forceIgnoredWithCheck);
|
|
326
|
+
if (action.apply === true && !apply)
|
|
327
|
+
console.warn(ui.applyIgnoredWithPlanning);
|
|
314
328
|
if (!machineOutput) {
|
|
315
329
|
for (const warning of loadedConfig.warnings)
|
|
316
330
|
console.warn(warning);
|
|
@@ -318,10 +332,11 @@ export async function main() {
|
|
|
318
332
|
let generatedFiles = [];
|
|
319
333
|
let repoFacts;
|
|
320
334
|
let baselineHash;
|
|
335
|
+
let enrichMetrics;
|
|
321
336
|
const results = await runCli(process.cwd(), {
|
|
322
337
|
lang,
|
|
323
338
|
config: loadedConfig.config,
|
|
324
|
-
dryRun: planningOnly,
|
|
339
|
+
dryRun: planningOnly || (apply && action.force !== true),
|
|
325
340
|
force: action.force === true && action.check !== true,
|
|
326
341
|
nonInteractive,
|
|
327
342
|
skipLlm: nonInteractive && !enrich,
|
|
@@ -334,59 +349,43 @@ export async function main() {
|
|
|
334
349
|
onBaselineHash: (hash) => {
|
|
335
350
|
baselineHash = hash;
|
|
336
351
|
},
|
|
352
|
+
onEnrichMetrics: (metrics) => {
|
|
353
|
+
enrichMetrics = metrics;
|
|
354
|
+
},
|
|
337
355
|
onFacts: (facts) => {
|
|
338
356
|
repoFacts = facts;
|
|
339
357
|
},
|
|
340
358
|
});
|
|
359
|
+
let activationResults = [];
|
|
360
|
+
if (apply) {
|
|
361
|
+
if (!baselineHash)
|
|
362
|
+
throw new Error("generation baseline was not produced");
|
|
363
|
+
activationResults = applyGeneratedFiles(process.cwd(), generatedFiles, baselineHash);
|
|
364
|
+
}
|
|
341
365
|
const written = results.filter((r) => r.status === "written");
|
|
342
366
|
const overwritten = results.filter((r) => r.status === "overwritten");
|
|
343
367
|
const changed = [...written, ...overwritten];
|
|
344
368
|
const failures = results.filter((r) => r.status === "error");
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const activePath = file.path.replace(".generated", "");
|
|
350
|
-
const stagingAbsolute = path.join(process.cwd(), file.path);
|
|
351
|
-
const activeAbsolute = path.join(process.cwd(), activePath);
|
|
352
|
-
const stagingExists = fs.existsSync(stagingAbsolute);
|
|
353
|
-
const activeExists = fs.existsSync(activeAbsolute);
|
|
354
|
-
const effectivePath = activeExists ? activePath : stagingExists ? file.path : undefined;
|
|
355
|
-
const effectiveAbsolute = activeExists ? activeAbsolute : stagingExists ? stagingAbsolute : undefined;
|
|
356
|
-
const expectedHash = baselineMatches ? generationState?.outputHashes[file.path] : undefined;
|
|
357
|
-
const current = effectiveAbsolute
|
|
358
|
-
? expectedHash
|
|
359
|
-
? hashContent(fs.readFileSync(effectiveAbsolute, "utf8")) === expectedHash
|
|
360
|
-
: fs.readFileSync(effectiveAbsolute, "utf8") === file.content
|
|
361
|
-
: false;
|
|
362
|
-
return { generatedPath: file.path, activePath, stagingExists, activeExists, effectivePath, current };
|
|
363
|
-
})
|
|
364
|
-
: [];
|
|
365
|
-
const missing = action.check
|
|
366
|
-
? generatedFiles.filter((file) => !fileStates.find((state) => state.generatedPath === file.path)?.effectivePath)
|
|
367
|
-
: [];
|
|
368
|
-
const outdated = action.check
|
|
369
|
-
? generationState && !baselineMatches
|
|
370
|
-
? [...generatedFiles]
|
|
371
|
-
: generatedFiles.filter((file) => {
|
|
372
|
-
const state = fileStates.find((candidate) => candidate.generatedPath === file.path);
|
|
373
|
-
return state?.effectivePath !== undefined && !state.current;
|
|
374
|
-
})
|
|
375
|
-
: [];
|
|
369
|
+
const check = action.check
|
|
370
|
+
? evaluateGenerationCheck(process.cwd(), generatedFiles, baselineHash)
|
|
371
|
+
: { baselineMatches: undefined, recordedBaselineHash: undefined, fileStates: [], missing: [], outdated: [] };
|
|
372
|
+
const { fileStates, missing, outdated } = check;
|
|
376
373
|
const checkIssues = missing.length + outdated.length;
|
|
377
374
|
if (machineOutput) {
|
|
378
375
|
const contentByPath = new Map(generatedFiles.map((file) => [file.path, file.content]));
|
|
379
376
|
console.log(JSON.stringify({
|
|
380
|
-
mode: action.check ? "check" : action.dryRun ? "dry-run" : "write",
|
|
377
|
+
mode: action.check ? "check" : action.dryRun ? "dry-run" : apply ? "apply" : "write",
|
|
381
378
|
configWarnings: loadedConfig.warnings,
|
|
382
379
|
facts: repoFacts,
|
|
383
380
|
wouldCreate: action.check ? missing.length : written.length,
|
|
384
381
|
missing: missing.map((file) => file.path),
|
|
385
382
|
outdated: outdated.map((file) => file.path),
|
|
386
|
-
baselineCurrent:
|
|
383
|
+
baselineCurrent: check.baselineMatches,
|
|
387
384
|
baselineHash,
|
|
388
|
-
recordedBaselineHash:
|
|
385
|
+
recordedBaselineHash: check.recordedBaselineHash,
|
|
389
386
|
fileStates,
|
|
387
|
+
activationResults,
|
|
388
|
+
enrichMetrics,
|
|
390
389
|
results: results.map((result) => ({
|
|
391
390
|
...result,
|
|
392
391
|
...(planningOnly ? { content: contentByPath.get(result.path) } : {}),
|
|
@@ -401,6 +400,16 @@ export async function main() {
|
|
|
401
400
|
console.log(`\n--- ${file.path} (${label}) ---\n${file.content}`);
|
|
402
401
|
}
|
|
403
402
|
}
|
|
403
|
+
else if (apply) {
|
|
404
|
+
for (const result of activationResults) {
|
|
405
|
+
if (result.status === "applied")
|
|
406
|
+
clack.log.success(ui.fileApplied(result.activePath, result.backupPath));
|
|
407
|
+
else if (result.status === "unchanged")
|
|
408
|
+
clack.log.info(ui.fileAlreadyApplied(result.activePath));
|
|
409
|
+
else if (result.status === "error")
|
|
410
|
+
clack.log.warn(`${result.activePath}: ${result.error}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
404
413
|
else if (!action.check)
|
|
405
414
|
for (const result of results) {
|
|
406
415
|
if (result.status === "written" || result.status === "overwritten") {
|
|
@@ -413,6 +422,8 @@ export async function main() {
|
|
|
413
422
|
clack.log.warn(`${result.path}: ${result.error}`);
|
|
414
423
|
}
|
|
415
424
|
}
|
|
425
|
+
if (!machineOutput && enrichMetrics)
|
|
426
|
+
clack.log.info(ui.enrichMetrics(enrichMetrics));
|
|
416
427
|
if (!machineOutput && action.check) {
|
|
417
428
|
console.log(checkIssues > 0
|
|
418
429
|
? ui.checkSummary(missing.length, outdated.length)
|
|
@@ -421,20 +432,23 @@ export async function main() {
|
|
|
421
432
|
else if (!machineOutput && action.dryRun) {
|
|
422
433
|
console.log(`\n${ui.dryRunSummary(changed.length)}`);
|
|
423
434
|
}
|
|
435
|
+
else if (!machineOutput && apply) {
|
|
436
|
+
clack.outro(ui.outroApplied);
|
|
437
|
+
}
|
|
424
438
|
else if (!machineOutput && changed.length > 0) {
|
|
425
439
|
clack.outro(ui.outroWritten);
|
|
426
440
|
}
|
|
427
441
|
else if (!machineOutput) {
|
|
428
442
|
clack.outro(ui.outroNothing);
|
|
429
443
|
}
|
|
430
|
-
if (failures.length > 0 || (action.check && checkIssues > 0)) {
|
|
444
|
+
if (failures.length > 0 || activationResults.some((result) => result.status === "error") || (action.check && checkIssues > 0)) {
|
|
431
445
|
process.exitCode = 1;
|
|
432
446
|
}
|
|
433
447
|
}
|
|
434
448
|
catch (err) {
|
|
435
449
|
const message = ui.unexpectedError(err.message);
|
|
436
450
|
if (machineOutput) {
|
|
437
|
-
console.log(JSON.stringify({ mode: action.check ? "check" : action.dryRun ? "dry-run" : "write", error: message }));
|
|
451
|
+
console.log(JSON.stringify({ mode: action.check ? "check" : action.dryRun ? "dry-run" : action.apply ? "apply" : "write", error: message }));
|
|
438
452
|
}
|
|
439
453
|
else {
|
|
440
454
|
clack.log.error(message);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GeneratedFile } from "./writer.js";
|
|
2
|
+
export interface ActivationResult {
|
|
3
|
+
generatedPath: string;
|
|
4
|
+
activePath: string;
|
|
5
|
+
status: "applied" | "unchanged" | "skipped" | "error";
|
|
6
|
+
backupPath?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class ActivationError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function applyGeneratedFiles(rootPath: string, files: readonly GeneratedFile[], baselineHash: string, now?: Date): ActivationResult[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { activePathFor } from "./check-state.js";
|
|
4
|
+
import { hashContent, loadGenerationState, writeGenerationState, } from "./generation-state.js";
|
|
5
|
+
const BACKUP_ROOT = ".agent-rules-init/backups";
|
|
6
|
+
export class ActivationError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ActivationError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function assertSafeRegularFile(filePath, label) {
|
|
13
|
+
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
|
|
14
|
+
throw new ActivationError(`refusing to use a symbolic link as ${label}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function safeResolve(rootPath, relativePath) {
|
|
18
|
+
const absolute = path.resolve(rootPath, relativePath);
|
|
19
|
+
const fromRoot = path.relative(path.resolve(rootPath), absolute);
|
|
20
|
+
if (fromRoot.startsWith("..") || path.isAbsolute(fromRoot)) {
|
|
21
|
+
throw new ActivationError("refusing to activate a path outside the repository root");
|
|
22
|
+
}
|
|
23
|
+
return absolute;
|
|
24
|
+
}
|
|
25
|
+
function assertParentInside(realRoot, absolutePath) {
|
|
26
|
+
const realParent = fs.realpathSync(path.dirname(absolutePath));
|
|
27
|
+
const fromRoot = path.relative(realRoot, realParent);
|
|
28
|
+
if (fromRoot.startsWith("..") || path.isAbsolute(fromRoot)) {
|
|
29
|
+
throw new ActivationError("refusing to write through a directory outside the repository root");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function availableBackupPath(rootPath, preferredPath) {
|
|
33
|
+
if (!fs.existsSync(path.resolve(rootPath, preferredPath)))
|
|
34
|
+
return preferredPath;
|
|
35
|
+
let suffix = 1;
|
|
36
|
+
while (fs.existsSync(path.resolve(rootPath, `${preferredPath}.${suffix}`)))
|
|
37
|
+
suffix++;
|
|
38
|
+
return `${preferredPath}.${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
export function applyGeneratedFiles(rootPath, files, baselineHash, now = new Date()) {
|
|
41
|
+
const state = loadGenerationState(rootPath);
|
|
42
|
+
if (!state)
|
|
43
|
+
throw new ActivationError("no generation receipt found; run the generator first");
|
|
44
|
+
if (state.baselineHash !== baselineHash) {
|
|
45
|
+
throw new ActivationError("the repository changed since generation; run with --force before --apply");
|
|
46
|
+
}
|
|
47
|
+
const backupStamp = now.toISOString().replace(/[:.]/g, "-");
|
|
48
|
+
const realRoot = fs.realpathSync(rootPath);
|
|
49
|
+
const results = [];
|
|
50
|
+
let wroteBackupIgnore = false;
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const activePath = activePathFor(file.path);
|
|
53
|
+
const stagingAbsolute = safeResolve(rootPath, file.path);
|
|
54
|
+
const activeAbsolute = safeResolve(rootPath, activePath);
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(stagingAbsolute)) {
|
|
57
|
+
results.push({ generatedPath: file.path, activePath, status: "skipped" });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
assertSafeRegularFile(stagingAbsolute, "generated staging file");
|
|
61
|
+
assertSafeRegularFile(activeAbsolute, "activated file");
|
|
62
|
+
const stagingContent = fs.readFileSync(stagingAbsolute, "utf8");
|
|
63
|
+
const activeContent = fs.existsSync(activeAbsolute)
|
|
64
|
+
? fs.readFileSync(activeAbsolute, "utf8")
|
|
65
|
+
: undefined;
|
|
66
|
+
if (activeContent === stagingContent) {
|
|
67
|
+
state.outputHashes[file.path] = hashContent(stagingContent);
|
|
68
|
+
results.push({ generatedPath: file.path, activePath, status: "unchanged" });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let backupPath;
|
|
72
|
+
if (activeContent !== undefined) {
|
|
73
|
+
backupPath = availableBackupPath(rootPath, path.join(BACKUP_ROOT, backupStamp, activePath).split(path.sep).join("/"));
|
|
74
|
+
const backupAbsolute = safeResolve(rootPath, backupPath);
|
|
75
|
+
fs.mkdirSync(path.dirname(backupAbsolute), { recursive: true });
|
|
76
|
+
assertParentInside(realRoot, backupAbsolute);
|
|
77
|
+
fs.copyFileSync(activeAbsolute, backupAbsolute, fs.constants.COPYFILE_EXCL);
|
|
78
|
+
if (!wroteBackupIgnore) {
|
|
79
|
+
const metadataRoot = path.join(rootPath, ".agent-rules-init");
|
|
80
|
+
fs.mkdirSync(metadataRoot, { recursive: true });
|
|
81
|
+
const ignorePath = path.join(metadataRoot, ".gitignore");
|
|
82
|
+
if (!fs.existsSync(ignorePath))
|
|
83
|
+
fs.writeFileSync(ignorePath, "*\n");
|
|
84
|
+
wroteBackupIgnore = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
fs.mkdirSync(path.dirname(activeAbsolute), { recursive: true });
|
|
88
|
+
assertParentInside(realRoot, activeAbsolute);
|
|
89
|
+
fs.writeFileSync(activeAbsolute, stagingContent);
|
|
90
|
+
state.outputHashes[file.path] = hashContent(stagingContent);
|
|
91
|
+
results.push({ generatedPath: file.path, activePath, status: "applied", backupPath });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
results.push({
|
|
95
|
+
generatedPath: file.path,
|
|
96
|
+
activePath,
|
|
97
|
+
status: "error",
|
|
98
|
+
error: error.message,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
writeGenerationState(rootPath, state);
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GeneratedFile } from "./writer.js";
|
|
2
|
+
export interface GeneratedFileState {
|
|
3
|
+
generatedPath: string;
|
|
4
|
+
activePath: string;
|
|
5
|
+
stagingExists: boolean;
|
|
6
|
+
activeExists: boolean;
|
|
7
|
+
effectivePath?: string;
|
|
8
|
+
current: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface GenerationCheck {
|
|
11
|
+
baselineMatches?: boolean;
|
|
12
|
+
recordedBaselineHash?: string;
|
|
13
|
+
fileStates: GeneratedFileState[];
|
|
14
|
+
missing: GeneratedFile[];
|
|
15
|
+
outdated: GeneratedFile[];
|
|
16
|
+
}
|
|
17
|
+
export declare function activePathFor(generatedPath: string): string;
|
|
18
|
+
export declare function evaluateGenerationCheck(rootPath: string, files: readonly GeneratedFile[], baselineHash: string | undefined): GenerationCheck;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { hashContent, loadGenerationState } from "./generation-state.js";
|
|
4
|
+
export function activePathFor(generatedPath) {
|
|
5
|
+
return generatedPath.replace(".generated", "");
|
|
6
|
+
}
|
|
7
|
+
export function evaluateGenerationCheck(rootPath, files, baselineHash) {
|
|
8
|
+
const generationState = loadGenerationState(rootPath);
|
|
9
|
+
const baselineMatches = generationState
|
|
10
|
+
? generationState.baselineHash === baselineHash
|
|
11
|
+
: undefined;
|
|
12
|
+
const fileStates = files.map((file) => {
|
|
13
|
+
const activePath = activePathFor(file.path);
|
|
14
|
+
const stagingAbsolute = path.join(rootPath, file.path);
|
|
15
|
+
const activeAbsolute = path.join(rootPath, activePath);
|
|
16
|
+
const stagingExists = fs.existsSync(stagingAbsolute);
|
|
17
|
+
const activeExists = fs.existsSync(activeAbsolute);
|
|
18
|
+
const effectivePath = activeExists ? activePath : stagingExists ? file.path : undefined;
|
|
19
|
+
const effectiveAbsolute = activeExists ? activeAbsolute : stagingExists ? stagingAbsolute : undefined;
|
|
20
|
+
const expectedHash = baselineMatches ? generationState?.outputHashes[file.path] : undefined;
|
|
21
|
+
const current = effectiveAbsolute
|
|
22
|
+
? expectedHash
|
|
23
|
+
? hashContent(fs.readFileSync(effectiveAbsolute, "utf8")) === expectedHash
|
|
24
|
+
: fs.readFileSync(effectiveAbsolute, "utf8") === file.content
|
|
25
|
+
: false;
|
|
26
|
+
return { generatedPath: file.path, activePath, stagingExists, activeExists, effectivePath, current };
|
|
27
|
+
});
|
|
28
|
+
const missing = files.filter((file) => !fileStates.find((state) => state.generatedPath === file.path)?.effectivePath);
|
|
29
|
+
const outdated = generationState && baselineMatches === false
|
|
30
|
+
? [...files]
|
|
31
|
+
: files.filter((file) => {
|
|
32
|
+
const state = fileStates.find((candidate) => candidate.generatedPath === file.path);
|
|
33
|
+
return state?.effectivePath !== undefined && !state.current;
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
baselineMatches,
|
|
37
|
+
recordedBaselineHash: generationState?.baselineHash,
|
|
38
|
+
fileStates,
|
|
39
|
+
missing,
|
|
40
|
+
outdated,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/core/i18n.d.ts
CHANGED
|
@@ -32,10 +32,13 @@ export interface UiTexts {
|
|
|
32
32
|
missingFlagValue: (flag: string) => string;
|
|
33
33
|
enrichIgnoredWithCheck: string;
|
|
34
34
|
forceIgnoredWithCheck: string;
|
|
35
|
+
applyIgnoredWithPlanning: string;
|
|
35
36
|
dryRunFileLabel: (status: "written" | "overwritten" | "skipped" | "error" | undefined) => string;
|
|
36
37
|
dryRunSummary: (changed: number) => string;
|
|
37
38
|
checkSummary: (missing: number, outdated: number) => string;
|
|
38
39
|
checkOk: string;
|
|
40
|
+
fileApplied: (path: string, backupPath?: string) => string;
|
|
41
|
+
fileAlreadyApplied: (path: string) => string;
|
|
39
42
|
assistantNotAvailable: (assistant: string) => string;
|
|
40
43
|
noTtyWarning: string;
|
|
41
44
|
skippedQuestion: (message: string) => string;
|
|
@@ -48,10 +51,21 @@ export interface UiTexts {
|
|
|
48
51
|
enrichNoAssistant: string;
|
|
49
52
|
enrichEvidenceDropped: (paths: readonly string[]) => string;
|
|
50
53
|
enrichRetrying: (assistant: string) => string;
|
|
54
|
+
enrichMetrics: (metrics: {
|
|
55
|
+
assistant: string;
|
|
56
|
+
model?: string;
|
|
57
|
+
batches: number;
|
|
58
|
+
attempts: number;
|
|
59
|
+
fallbackBatches: number;
|
|
60
|
+
inputChars: number;
|
|
61
|
+
durationMs: number;
|
|
62
|
+
}) => string;
|
|
63
|
+
enrichLargeInput: (characters: number, batches: number) => string;
|
|
51
64
|
enrichPrompt: (filesJson: string, mustKeep: readonly string[], existingDocsJson?: string) => string;
|
|
52
65
|
fileSkipped: (path: string) => string;
|
|
53
66
|
outroWritten: string;
|
|
54
67
|
outroNothing: string;
|
|
68
|
+
outroApplied: string;
|
|
55
69
|
unexpectedError: (message: string) => string;
|
|
56
70
|
cancelled: string;
|
|
57
71
|
dirNotes: Record<string, string>;
|
package/dist/core/i18n.js
CHANGED
|
@@ -81,10 +81,11 @@ Uso:
|
|
|
81
81
|
npx agent-rules-init --version muestra la versión
|
|
82
82
|
|
|
83
83
|
Los archivos se crean siempre con sufijo .generated y nunca sobrescriben nada existente:
|
|
84
|
-
revisa su contenido y
|
|
84
|
+
revisa su contenido y ejecuta --apply para activarlos con backup seguro.`,
|
|
85
85
|
automationUsage: `Automatización:
|
|
86
86
|
--dry-run renderiza y muestra archivos sin escribir
|
|
87
87
|
--force regenera solo archivos *.generated.*; nunca sobrescribe los finales
|
|
88
|
+
--apply activa los archivos generados; guarda backup de los finales reemplazados
|
|
88
89
|
--check falla si los archivos generados o activados faltan o están obsoletos; nunca escribe
|
|
89
90
|
--json emite un único resultado JSON legible por máquinas
|
|
90
91
|
--non-interactive omite preguntas y la oferta de enriquecimiento con IA
|
|
@@ -97,10 +98,13 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
97
98
|
missingFlagValue: (flag) => `La opción ${flag} requiere un valor.`,
|
|
98
99
|
enrichIgnoredWithCheck: "--enrich se ignora con --check.",
|
|
99
100
|
forceIgnoredWithCheck: "--force se ignora con --check.",
|
|
101
|
+
applyIgnoredWithPlanning: "--apply se ignora con --check o --dry-run.",
|
|
100
102
|
dryRunFileLabel: (status) => status === "written" ? "se crearía" : status === "overwritten" ? "se actualizaría" : "ya existe",
|
|
101
103
|
dryRunSummary: (changed) => `${changed} archivo(s) se crearían o actualizarían.`,
|
|
102
104
|
checkSummary: (missing, outdated) => `${missing} archivo(s) ausentes; ${outdated} archivo(s) obsoletos.`,
|
|
103
105
|
checkOk: "Los archivos generados o activados están presentes y actualizados.",
|
|
106
|
+
fileApplied: (path, backupPath) => backupPath ? `${path} activado (backup: ${backupPath}).` : `${path} activado.`,
|
|
107
|
+
fileAlreadyApplied: (path) => `${path}: ya estaba actualizado.`,
|
|
104
108
|
assistantNotAvailable: (assistant) => `Se pidió ${assistant} con --assistant pero no está instalado; se conserva la versión generada.`,
|
|
105
109
|
noTtyWarning: "No se detectó una terminal interactiva (esto pasa a veces en Git Bash en Windows). " +
|
|
106
110
|
"Continuando sin preguntas ni oferta de enriquecimiento con IA; se usarán los valores detectados.",
|
|
@@ -114,6 +118,10 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
114
118
|
enrichNoAssistant: "Se pidió --enrich pero no se encontró ningún asistente (claude o codex) instalado; se conserva la versión generada.",
|
|
115
119
|
enrichEvidenceDropped: (paths) => `Se descartaron afirmaciones del enriquecimiento porque su evidencia citada no existe en el repo: ${paths.join(", ")}`,
|
|
116
120
|
enrichRetrying: (assistant) => `La respuesta de ${assistant} no pasó la validación; se reintenta una vez…`,
|
|
121
|
+
enrichMetrics: (metrics) => `Enriquecimiento: ${metrics.assistant}${metrics.model ? `/${metrics.model}` : ""}, ${metrics.batches} lote(s), ` +
|
|
122
|
+
`${metrics.attempts} intento(s), ${metrics.inputChars} caracteres enviados, ${metrics.fallbackBatches} fallback(s), ` +
|
|
123
|
+
`${(metrics.durationMs / 1000).toFixed(1)} s.`,
|
|
124
|
+
enrichLargeInput: (characters, batches) => `El enriquecimiento enviará aproximadamente ${characters} caracteres en ${batches} procesos; revisa el modelo elegido y su coste.`,
|
|
117
125
|
enrichPrompt: (filesJson, mustKeep, existingDocsJson) => "Estás ejecutándote en la raíz de un repositorio. Los siguientes archivos de instrucciones para agentes de IA " +
|
|
118
126
|
"se generaron solo a partir de manifiestos, CI y configuración, por lo que algunas secciones (convenciones, arquitectura, prompts) son genéricas.\n" +
|
|
119
127
|
"Primero investiga el repositorio real con tus herramientas de lectura: configuración de estilo (linter, formatter, pre-commit), " +
|
|
@@ -135,10 +143,9 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
135
143
|
"Devuelve únicamente un array JSON válido con exactamente los mismos objetos path/content y en el mismo orden, sin bloque de código ni comentarios. " +
|
|
136
144
|
`Entrada JSON:\n${filesJson}`,
|
|
137
145
|
fileSkipped: (path) => `${path}: ya existía, se conserva sin cambios.`,
|
|
138
|
-
outroWritten: "Revisa los archivos *.generated.* y
|
|
139
|
-
'".generated" (ej. "CLAUDE.generated.md" → "CLAUDE.md") para activarlos — ' +
|
|
140
|
-
"tu asistente de IA solo lee el nombre final, no el generado.",
|
|
146
|
+
outroWritten: "Revisa los archivos *.generated.* y ejecuta `npx agent-rules-init --apply` para activarlos con backup seguro.",
|
|
141
147
|
outroNothing: "No se generó ningún archivo nuevo.",
|
|
148
|
+
outroApplied: "Archivos revisados activados. Los asistentes ya pueden leer los nombres finales.",
|
|
142
149
|
unexpectedError: (message) => `Fallo inesperado: ${message}`,
|
|
143
150
|
cancelled: "Operación cancelada.",
|
|
144
151
|
dirNotes: {
|
|
@@ -193,10 +200,11 @@ Usage:
|
|
|
193
200
|
npx agent-rules-init --version show the version
|
|
194
201
|
|
|
195
202
|
Files are always created with the .generated suffix and never overwrite anything:
|
|
196
|
-
review their content and
|
|
203
|
+
review their content and run --apply to activate them with safe backups.`,
|
|
197
204
|
automationUsage: `Automation:
|
|
198
205
|
--dry-run render and print files without writing
|
|
199
206
|
--force refresh only *.generated.* files; never overwrite activated final files
|
|
207
|
+
--apply activate generated files; back up any replaced final files
|
|
200
208
|
--check fail when generated or activated files are missing/outdated; never write
|
|
201
209
|
--json emit a single machine-readable JSON result
|
|
202
210
|
--non-interactive skip questions and the AI-enrichment offer
|
|
@@ -209,10 +217,13 @@ review their content and drop the suffix to activate them.`,
|
|
|
209
217
|
missingFlagValue: (flag) => `The ${flag} option requires a value.`,
|
|
210
218
|
enrichIgnoredWithCheck: "--enrich is ignored with --check.",
|
|
211
219
|
forceIgnoredWithCheck: "--force is ignored with --check.",
|
|
220
|
+
applyIgnoredWithPlanning: "--apply is ignored with --check or --dry-run.",
|
|
212
221
|
dryRunFileLabel: (status) => status === "written" ? "would create" : status === "overwritten" ? "would update" : "exists",
|
|
213
222
|
dryRunSummary: (changed) => `${changed} file(s) would be created or updated.`,
|
|
214
223
|
checkSummary: (missing, outdated) => `${missing} file(s) missing; ${outdated} file(s) outdated.`,
|
|
215
224
|
checkOk: "Generated or activated files are present and up to date.",
|
|
225
|
+
fileApplied: (path, backupPath) => backupPath ? `${path} activated (backup: ${backupPath}).` : `${path} activated.`,
|
|
226
|
+
fileAlreadyApplied: (path) => `${path}: already up to date.`,
|
|
216
227
|
assistantNotAvailable: (assistant) => `${assistant} was requested with --assistant but is not installed; keeping the generated version.`,
|
|
217
228
|
noTtyWarning: "No interactive terminal detected (this sometimes happens in Git Bash on Windows). " +
|
|
218
229
|
"Continuing without questions or the AI-enrichment offer; detected values will be used.",
|
|
@@ -226,6 +237,10 @@ review their content and drop the suffix to activate them.`,
|
|
|
226
237
|
enrichNoAssistant: "--enrich was requested but no installed assistant (claude or codex) was found; keeping the generated version.",
|
|
227
238
|
enrichEvidenceDropped: (paths) => `Dropped enrichment claims because their cited evidence does not exist in the repo: ${paths.join(", ")}`,
|
|
228
239
|
enrichRetrying: (assistant) => `${assistant}'s response failed validation; retrying once…`,
|
|
240
|
+
enrichMetrics: (metrics) => `Enrichment: ${metrics.assistant}${metrics.model ? `/${metrics.model}` : ""}, ${metrics.batches} batch(es), ` +
|
|
241
|
+
`${metrics.attempts} attempt(s), ${metrics.inputChars} characters sent, ${metrics.fallbackBatches} fallback(s), ` +
|
|
242
|
+
`${(metrics.durationMs / 1000).toFixed(1)} s.`,
|
|
243
|
+
enrichLargeInput: (characters, batches) => `Enrichment will send approximately ${characters} characters across ${batches} processes; review the chosen model and its cost.`,
|
|
229
244
|
enrichPrompt: (filesJson, mustKeep, existingDocsJson) => "You are running at the root of a repository. The following instruction files for AI agents were generated " +
|
|
230
245
|
"from manifests, CI and configuration only, so some sections (conventions, architecture, prompts) are generic.\n" +
|
|
231
246
|
"First investigate the actual repository with your read tools: style configuration (linter, formatter, pre-commit), " +
|
|
@@ -247,10 +262,9 @@ review their content and drop the suffix to activate them.`,
|
|
|
247
262
|
"Return only a valid JSON array with exactly the same path/content objects in the same order, without a code fence or commentary. " +
|
|
248
263
|
`Input JSON:\n${filesJson}`,
|
|
249
264
|
fileSkipped: (path) => `${path}: already existed, left unchanged.`,
|
|
250
|
-
outroWritten: "Review the *.generated.* files
|
|
251
|
-
'".generated" suffix (e.g. "CLAUDE.generated.md" → "CLAUDE.md") to activate them — ' +
|
|
252
|
-
"your AI assistant only reads the final name, not the generated one.",
|
|
265
|
+
outroWritten: "Review the *.generated.* files, then run `npx agent-rules-init --apply` to activate them with safe backups.",
|
|
253
266
|
outroNothing: "No new files were generated.",
|
|
267
|
+
outroApplied: "Reviewed files activated. Assistants can now read the final names.",
|
|
254
268
|
unexpectedError: (message) => `Unexpected failure: ${message}`,
|
|
255
269
|
cancelled: "Operation cancelled.",
|
|
256
270
|
dirNotes: {
|
|
@@ -19,6 +19,21 @@ export interface EnrichOptions {
|
|
|
19
19
|
existingDocs?: readonly GeneratedFile[];
|
|
20
20
|
/** Model identifier forwarded verbatim to the assistant CLI; its default when omitted. */
|
|
21
21
|
model?: string;
|
|
22
|
+
onMetrics?: (metrics: EnrichMetrics) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function estimateEnrichment(files: readonly GeneratedFile[]): {
|
|
25
|
+
characters: number;
|
|
26
|
+
batches: number;
|
|
27
|
+
};
|
|
28
|
+
export interface EnrichMetrics {
|
|
29
|
+
assistant: AssistantId;
|
|
30
|
+
model?: string;
|
|
31
|
+
batches: number;
|
|
32
|
+
attempts: number;
|
|
33
|
+
fallbackBatches: number;
|
|
34
|
+
inputChars: number;
|
|
35
|
+
outputChars: number;
|
|
36
|
+
durationMs: number;
|
|
22
37
|
}
|
|
23
38
|
/**
|
|
24
39
|
* Asks the assistant to investigate the repository it runs in and rewrite the generated
|
package/dist/core/llm-bridge.js
CHANGED
|
@@ -128,6 +128,9 @@ function parseAssistantBatch(stdout, originals) {
|
|
|
128
128
|
return { path: originals[index].path, content: entry.content };
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
+
export function estimateEnrichment(files) {
|
|
132
|
+
return { characters: JSON.stringify(files).length, batches: makeBatches(files).length };
|
|
133
|
+
}
|
|
131
134
|
// The assistant is free-form: it may drop the one command CI actually runs and invent a
|
|
132
135
|
// plausible replacement. Any lost must-keep command invalidates the whole batch.
|
|
133
136
|
function assertMustKeepSurvive(enriched, originals, mustKeep) {
|
|
@@ -139,7 +142,37 @@ function assertMustKeepSurvive(enriched, originals, mustKeep) {
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
}
|
|
145
|
+
const DANGEROUS_INSTRUCTION = /(?:rm\s+-rf|git\s+reset\s+--hard|curl[^\n|]*\|\s*(?:sh|bash)|invoke-expression|chmod\s+777|sudo\s+)/i;
|
|
146
|
+
const NEGATED_DANGER = /(?:never|do not|don't|must not|avoid|nunca|no ejecutes|no ejecutar|evita)/i;
|
|
147
|
+
function assertNoNewDangerousInstructions(enriched, originals) {
|
|
148
|
+
const originalText = originals.map((file) => file.content).join("\n");
|
|
149
|
+
for (const line of enriched.flatMap((file) => file.content.split("\n"))) {
|
|
150
|
+
const dangerous = line.match(DANGEROUS_INSTRUCTION)?.[0];
|
|
151
|
+
if (dangerous && !NEGATED_DANGER.test(line) && !originalText.includes(dangerous)) {
|
|
152
|
+
throw new Error(`assistant introduced a dangerous instruction: ${dangerous}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function assertNewBulletClaimsCiteEvidence(enriched, originals) {
|
|
157
|
+
for (let index = 0; index < enriched.length; index++) {
|
|
158
|
+
const originalLines = new Set(originals[index].content.split("\n").map((line) => line.trim()));
|
|
159
|
+
for (const line of enriched[index].content.split("\n")) {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
if (/^[-*] /.test(trimmed) && !originalLines.has(trimmed) && !HAS_EVIDENCE_GROUP.test(trimmed)) {
|
|
162
|
+
throw new Error("assistant introduced a bullet claim without cited evidence");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function assistantFailureDetail(assistant, error) {
|
|
168
|
+
const message = error.message;
|
|
169
|
+
if (/unknown (?:option|argument)|unrecognized (?:option|argument)|unexpected argument/i.test(message)) {
|
|
170
|
+
return `${assistant} does not support the required read-only safety flags; update the assistant CLI`;
|
|
171
|
+
}
|
|
172
|
+
return message;
|
|
173
|
+
}
|
|
142
174
|
const EVIDENCE_GROUP = /\((?:evidencia|evidence):([^)]*)\)/gi;
|
|
175
|
+
const HAS_EVIDENCE_GROUP = /\((?:evidencia|evidence):[^)]*\)/i;
|
|
143
176
|
// A checkable citation is a plain relative path; tokens with spaces, URLs or globs are
|
|
144
177
|
// left alone rather than guessed at.
|
|
145
178
|
const PATH_TOKEN = /^[\w.@~-]+(?:[\\/][\w.@~-]+)*[\\/]?$/;
|
|
@@ -181,20 +214,32 @@ function dropUnverifiableClaims(files, rootPath) {
|
|
|
181
214
|
* process; only very large outputs are split. Falls back to the originals per batch.
|
|
182
215
|
*/
|
|
183
216
|
export async function enrichFilesWithAssistant(assistant, files, options = {}) {
|
|
184
|
-
const { execFn = defaultExecFn, lang = "es", cwd, mustKeep = [], existingDocs = [], model } = options;
|
|
217
|
+
const { execFn = defaultExecFn, lang = "es", cwd, mustKeep = [], existingDocs = [], model, onMetrics } = options;
|
|
185
218
|
const existingDocsJson = existingDocs.length > 0 ? JSON.stringify(existingDocs) : undefined;
|
|
186
219
|
const enriched = [];
|
|
187
|
-
|
|
220
|
+
const batches = makeBatches(files);
|
|
221
|
+
const startedAt = Date.now();
|
|
222
|
+
let attempts = 0;
|
|
223
|
+
let fallbackBatches = 0;
|
|
224
|
+
let inputChars = 0;
|
|
225
|
+
let outputChars = 0;
|
|
226
|
+
for (const batch of batches) {
|
|
188
227
|
const input = UI[lang].enrichPrompt(JSON.stringify(batch), mustKeep, existingDocsJson);
|
|
228
|
+
let attemptInput = input;
|
|
189
229
|
// Contract violations (invalid JSON, changed paths, dropped commands) are stochastic,
|
|
190
230
|
// especially with smaller models; one bounded retry recovers most of them.
|
|
191
231
|
let attemptsLeft = 2;
|
|
192
232
|
while (attemptsLeft > 0) {
|
|
193
233
|
attemptsLeft--;
|
|
234
|
+
attempts++;
|
|
235
|
+
inputChars += attemptInput.length;
|
|
194
236
|
try {
|
|
195
|
-
const result = await execFn(assistant, printArgs(assistant, model),
|
|
237
|
+
const result = await execFn(assistant, printArgs(assistant, model), attemptInput, cwd);
|
|
238
|
+
outputChars += result.stdout.length;
|
|
196
239
|
let parsed = parseAssistantBatch(result.stdout, batch);
|
|
197
240
|
assertMustKeepSurvive(parsed, batch, mustKeep);
|
|
241
|
+
assertNoNewDangerousInstructions(parsed, batch);
|
|
242
|
+
assertNewBulletClaimsCiteEvidence(parsed, batch);
|
|
198
243
|
if (cwd) {
|
|
199
244
|
const verified = dropUnverifiableClaims(parsed, cwd);
|
|
200
245
|
if (verified.missing.length > 0)
|
|
@@ -206,14 +251,27 @@ export async function enrichFilesWithAssistant(assistant, files, options = {}) {
|
|
|
206
251
|
}
|
|
207
252
|
catch (err) {
|
|
208
253
|
if (attemptsLeft > 0) {
|
|
254
|
+
const detail = assistantFailureDetail(assistant, err);
|
|
255
|
+
attemptInput = `${input}\n\nYour previous response was rejected: ${detail}. Correct that exact problem and return the required JSON array only.`;
|
|
209
256
|
console.warn(UI[lang].enrichRetrying(assistant));
|
|
210
257
|
}
|
|
211
258
|
else {
|
|
212
|
-
console.warn(UI[lang].enrichFailed(assistant, err
|
|
259
|
+
console.warn(UI[lang].enrichFailed(assistant, assistantFailureDetail(assistant, err)));
|
|
260
|
+
fallbackBatches++;
|
|
213
261
|
enriched.push(...batch);
|
|
214
262
|
}
|
|
215
263
|
}
|
|
216
264
|
}
|
|
217
265
|
}
|
|
266
|
+
onMetrics?.({
|
|
267
|
+
assistant,
|
|
268
|
+
model,
|
|
269
|
+
batches: batches.length,
|
|
270
|
+
attempts,
|
|
271
|
+
fallbackBatches,
|
|
272
|
+
inputChars,
|
|
273
|
+
outputChars,
|
|
274
|
+
durationMs: Date.now() - startedAt,
|
|
275
|
+
});
|
|
218
276
|
return enriched;
|
|
219
277
|
}
|
package/dist/core/scanner.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-rules-init",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Generates CLAUDE.md, AGENTS.md, copilot-instructions.md and prompt files from what your repo actually is.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,12 +35,13 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsc -p tsconfig.json",
|
|
40
|
-
"test": "vitest run",
|
|
41
|
-
"lint": "tsc -p tsconfig.json --noEmit",
|
|
42
|
-
"
|
|
43
|
-
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
42
|
+
"test:enrich": "npm run build && node scripts/smoke-enrichment.mjs",
|
|
43
|
+
"prepublishOnly": "npm run lint && npm test && npm run build"
|
|
44
|
+
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@clack/prompts": "^0.9.1",
|
|
46
47
|
"yaml": "^2.9.0"
|