agent-rules-init 0.3.0 → 0.5.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.
@@ -0,0 +1,25 @@
1
+ import type { Lang } from "./i18n.js";
2
+ export interface ProjectConfig {
3
+ framework?: string;
4
+ testRunner?: string;
5
+ linter?: string;
6
+ packageManager?: string;
7
+ }
8
+ export interface AgentRulesConfig {
9
+ lang?: Lang;
10
+ exclude?: string[];
11
+ projects?: Record<string, ProjectConfig>;
12
+ noAi?: boolean;
13
+ }
14
+ export interface LoadedConfig {
15
+ config: AgentRulesConfig;
16
+ /** Absolute path to the selected config file, if one exists. */
17
+ sourcePath?: string;
18
+ warnings: string[];
19
+ }
20
+ export declare class ConfigError extends Error {
21
+ readonly configPath: string;
22
+ constructor(message: string, configPath: string, options?: ErrorOptions);
23
+ }
24
+ /** Loads and validates the optional repository-local agent-rules configuration. */
25
+ export declare function loadConfig(rootPath: string): LoadedConfig;
@@ -0,0 +1,125 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse } from "yaml";
4
+ const CONFIG_FILENAMES = [".agent-rules-init.yml", ".agent-rules-init.yaml"];
5
+ const ROOT_KEYS = new Set(["lang", "exclude", "projects", "noAi"]);
6
+ const PROJECT_KEYS = ["framework", "testRunner", "linter", "packageManager"];
7
+ const PROJECT_KEY_SET = new Set(PROJECT_KEYS);
8
+ export class ConfigError extends Error {
9
+ configPath;
10
+ constructor(message, configPath, options) {
11
+ super(message, options);
12
+ this.name = "ConfigError";
13
+ this.configPath = configPath;
14
+ }
15
+ }
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ function unknownKeyWarnings(value, allowed, location) {
20
+ return Object.keys(value)
21
+ .filter((key) => !allowed.has(key))
22
+ .map((key) => `Unknown configuration key \"${location}${key}\"; it was ignored.`);
23
+ }
24
+ function optionalNonEmptyString(value, location, warnings) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ if (typeof value !== "string" || value.trim().length === 0) {
28
+ warnings.push(`Configuration key \"${location}\" must be a non-empty string; it was ignored.`);
29
+ return undefined;
30
+ }
31
+ return value.trim();
32
+ }
33
+ function validateProjects(value, warnings) {
34
+ if (value === undefined)
35
+ return undefined;
36
+ if (!isRecord(value)) {
37
+ warnings.push('Configuration key "projects" must be an object keyed by project path; it was ignored.');
38
+ return undefined;
39
+ }
40
+ const projects = {};
41
+ for (const [projectPath, rawProject] of Object.entries(value)) {
42
+ if (projectPath.trim().length === 0) {
43
+ warnings.push("A project path must not be empty; that project was ignored.");
44
+ continue;
45
+ }
46
+ if (!isRecord(rawProject)) {
47
+ warnings.push(`Configuration key \"projects.${projectPath}\" must be an object; it was ignored.`);
48
+ continue;
49
+ }
50
+ warnings.push(...unknownKeyWarnings(rawProject, PROJECT_KEY_SET, `projects.${projectPath}.`));
51
+ const project = {};
52
+ for (const key of PROJECT_KEYS) {
53
+ const parsed = optionalNonEmptyString(rawProject[key], `projects.${projectPath}.${key}`, warnings);
54
+ if (parsed !== undefined)
55
+ project[key] = parsed;
56
+ }
57
+ projects[projectPath] = project;
58
+ }
59
+ return projects;
60
+ }
61
+ function validateConfig(value, configPath, warnings) {
62
+ // An empty YAML document is equivalent to an empty configuration.
63
+ if (value === null || value === undefined)
64
+ return {};
65
+ if (!isRecord(value)) {
66
+ throw new ConfigError(`Invalid configuration in ${configPath}: the YAML root must be an object.`, configPath);
67
+ }
68
+ warnings.push(...unknownKeyWarnings(value, ROOT_KEYS, ""));
69
+ const config = {};
70
+ if (value.lang !== undefined) {
71
+ if (value.lang === "es" || value.lang === "en")
72
+ config.lang = value.lang;
73
+ else
74
+ warnings.push('Configuration key "lang" must be "es" or "en"; it was ignored.');
75
+ }
76
+ if (value.exclude !== undefined) {
77
+ if (!Array.isArray(value.exclude)) {
78
+ warnings.push('Configuration key "exclude" must be an array of strings; it was ignored.');
79
+ }
80
+ else {
81
+ const exclude = value.exclude.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
82
+ if (exclude.length !== value.exclude.length) {
83
+ warnings.push('Configuration key "exclude" contains non-string or empty entries; they were ignored.');
84
+ }
85
+ config.exclude = exclude.map((entry) => entry.trim());
86
+ }
87
+ }
88
+ const projects = validateProjects(value.projects, warnings);
89
+ if (projects !== undefined)
90
+ config.projects = projects;
91
+ if (value.noAi !== undefined) {
92
+ if (typeof value.noAi === "boolean")
93
+ config.noAi = value.noAi;
94
+ else
95
+ warnings.push('Configuration key "noAi" must be a boolean; it was ignored.');
96
+ }
97
+ return config;
98
+ }
99
+ /** Loads and validates the optional repository-local agent-rules configuration. */
100
+ export function loadConfig(rootPath) {
101
+ const found = CONFIG_FILENAMES.map((filename) => path.resolve(rootPath, filename)).filter((candidate) => fs.existsSync(candidate));
102
+ if (found.length === 0)
103
+ return { config: {}, warnings: [] };
104
+ const sourcePath = found[0];
105
+ const warnings = [];
106
+ if (found.length > 1) {
107
+ warnings.push(`Both ${CONFIG_FILENAMES.join(" and ")} exist; ${path.basename(sourcePath)} takes precedence.`);
108
+ }
109
+ let raw;
110
+ try {
111
+ raw = fs.readFileSync(sourcePath, "utf8");
112
+ }
113
+ catch (error) {
114
+ throw new ConfigError(`Cannot read configuration file ${sourcePath}.`, sourcePath, { cause: error });
115
+ }
116
+ let parsed;
117
+ try {
118
+ parsed = parse(raw);
119
+ }
120
+ catch (error) {
121
+ const detail = error instanceof Error ? ` ${error.message}` : "";
122
+ throw new ConfigError(`Invalid YAML in ${sourcePath}.${detail}`, sourcePath, { cause: error });
123
+ }
124
+ return { config: validateConfig(parsed, sourcePath, warnings), sourcePath, warnings };
125
+ }
@@ -1,12 +1,10 @@
1
1
  export type Lang = "es" | "en";
2
2
  export declare function detectLang(): Lang;
3
3
  export declare function summarySentence(lang: Lang, language: string, framework?: string, parenthetical?: string): string;
4
- export declare function runTestsConvention(lang: Lang, cmd: string): string;
5
- export declare function reviewBody(lang: Lang, focus: string, framework: string): string;
4
+ export declare function runTestsConvention(lang: Lang, cmd?: string): string;
5
+ export declare function reviewBody(lang: Lang, focus: string, framework?: string): string;
6
6
  export declare function refactorBody(lang: Lang, extra?: string): string;
7
- export declare function testingBody(lang: Lang, runner: string): string;
8
- export declare function unknownRunnerLabel(lang: Lang): string;
9
- export declare function unknownFrameworkLabel(lang: Lang): string;
7
+ export declare function testingBody(lang: Lang, runner?: string): string;
10
8
  export interface UiTexts {
11
9
  generatedHeader: string;
12
10
  sections: {
@@ -15,6 +13,7 @@ export interface UiTexts {
15
13
  ci: string;
16
14
  conventions: string;
17
15
  architecture: string;
16
+ canonical: string;
18
17
  };
19
18
  andMore: (count: number, file?: string) => string;
20
19
  noStackFallback: string;
@@ -26,19 +25,31 @@ export interface UiTexts {
26
25
  packageManager: string;
27
26
  };
28
27
  usage: string;
28
+ automationUsage: string;
29
29
  unknownOption: (flag: string) => string;
30
30
  invalidLang: (value: string) => string;
31
+ invalidAssistant: (value: string) => string;
32
+ missingFlagValue: (flag: string) => string;
33
+ assistantNotAvailable: (assistant: string) => string;
31
34
  noTtyWarning: string;
32
35
  skippedQuestion: (message: string) => string;
33
- polishDetected: (assistant: string) => string;
34
- polishConfirm: (assistant: string) => string;
35
- polishFailed: (assistant: string, error: string) => string;
36
- polishPrompt: (content: string) => string;
36
+ enrichDetected: (assistant: string) => string;
37
+ enrichConfirm: (assistant: string) => string;
38
+ enrichWorking: (assistant: string) => string;
39
+ enrichDone: string;
40
+ enrichKept: string;
41
+ enrichFailed: (assistant: string, error: string) => string;
42
+ enrichNoAssistant: string;
43
+ enrichEvidenceDropped: (paths: readonly string[]) => string;
44
+ enrichRetrying: (assistant: string) => string;
45
+ enrichPrompt: (filesJson: string, mustKeep: readonly string[], existingDocsJson?: string) => string;
37
46
  fileSkipped: (path: string) => string;
38
47
  outroWritten: string;
39
48
  outroNothing: string;
40
49
  unexpectedError: (message: string) => string;
41
50
  cancelled: string;
42
51
  dirNotes: Record<string, string>;
52
+ testDirNote: string;
53
+ entrypointNote: string;
43
54
  }
44
55
  export declare const UI: Record<Lang, UiTexts>;
package/dist/core/i18n.js CHANGED
@@ -14,16 +14,26 @@ export function summarySentence(lang, language, framework, parenthetical) {
14
14
  return `${language} project${framework ? ` using ${framework}` : ""}${paren}.`;
15
15
  }
16
16
  export function runTestsConvention(lang, cmd) {
17
+ if (cmd) {
18
+ return lang === "es"
19
+ ? `Ejecuta los tests con ${cmd} antes de terminar una tarea.`
20
+ : `Run the tests with ${cmd} before finishing a task.`;
21
+ }
17
22
  return lang === "es"
18
- ? `Ejecuta los tests con ${cmd} antes de terminar una tarea.`
19
- : `Run the tests with ${cmd} before finishing a task.`;
23
+ ? "Ejecuta la suite de tests del repositorio antes de terminar una tarea."
24
+ : "Run the repository's test suite before finishing a task.";
20
25
  }
21
26
  export function reviewBody(lang, focus, framework) {
22
27
  // Con focus vacío la frase colapsa a "bugs y desviaciones" sin coma colgante.
23
28
  const focusPart = focus ? `, ${focus}` : "";
29
+ if (framework) {
30
+ return lang === "es"
31
+ ? `Revisa el diff actual buscando bugs${focusPart} y desviaciones de las convenciones de ${framework}. Señala solo problemas concretos con línea de archivo.`
32
+ : `Review the current diff looking for bugs${focusPart} and deviations from ${framework} conventions. Point out only concrete issues with file and line.`;
33
+ }
24
34
  return lang === "es"
25
- ? `Revisa el diff actual buscando bugs${focusPart} y desviaciones de las convenciones de ${framework}. Señala solo problemas concretos con línea de archivo.`
26
- : `Review the current diff looking for bugs${focusPart} and deviations from ${framework} conventions. Point out only concrete issues with file and line.`;
35
+ ? `Revisa el diff actual buscando bugs${focusPart}. Señala solo problemas concretos con línea de archivo.`
36
+ : `Review the current diff looking for bugs${focusPart}. Point out only concrete issues with file and line.`;
27
37
  }
28
38
  export function refactorBody(lang, extra) {
29
39
  const base = lang === "es"
@@ -32,15 +42,14 @@ export function refactorBody(lang, extra) {
32
42
  return extra ? `${base} ${extra}` : base;
33
43
  }
34
44
  export function testingBody(lang, runner) {
45
+ if (runner) {
46
+ return lang === "es"
47
+ ? `Escribe tests con ${runner} para el código señalado. Cubre el camino feliz y al menos un caso límite.`
48
+ : `Write tests with ${runner} for the highlighted code. Cover the happy path and at least one edge case.`;
49
+ }
35
50
  return lang === "es"
36
- ? `Escribe tests con ${runner} para el código señalado. Cubre el camino feliz y al menos un caso límite.`
37
- : `Write tests with ${runner} for the highlighted code. Cover the happy path and at least one edge case.`;
38
- }
39
- export function unknownRunnerLabel(lang) {
40
- return lang === "es" ? "el test runner del proyecto" : "the project's test runner";
41
- }
42
- export function unknownFrameworkLabel(lang) {
43
- return lang === "es" ? "el framework del proyecto" : "the project's framework";
51
+ ? "Escribe tests para el código señalado. Cubre el camino feliz y al menos un caso límite."
52
+ : "Write tests for the highlighted code. Cover the happy path and at least one edge case.";
44
53
  }
45
54
  export const UI = {
46
55
  es: {
@@ -51,6 +60,7 @@ export const UI = {
51
60
  ci: "Lo que ejecuta CI (GitHub Actions)",
52
61
  conventions: "Convenciones",
53
62
  architecture: "Arquitectura",
63
+ canonical: "Comandos canónicos",
54
64
  },
55
65
  andMore: (count, file) => (file ? `…y ${count} más en ${file}` : `…y ${count} más`),
56
66
  noStackFallback: "No se detectó ningún stack conocido. Completa este archivo manualmente.",
@@ -61,25 +71,60 @@ export const UI = {
61
71
  linter: "el linter",
62
72
  packageManager: "el gestor de paquetes",
63
73
  },
64
- usage: `agent-rules-init — genera CLAUDE.md, AGENTS.md, copilot-instructions y prompts de review/refactor/testing a partir del stack detectado en tu repo.
65
-
66
- Uso:
67
- npx agent-rules-init escanea el directorio actual y genera los archivos *.generated.*
68
- npx agent-rules-init --lang es fuerza el idioma del contenido (es|en); por defecto se detecta del sistema
69
- npx agent-rules-init --help muestra esta ayuda
70
- npx agent-rules-init --version muestra la versión
71
-
72
- Los archivos se crean siempre con sufijo .generated y nunca sobrescriben nada existente:
74
+ usage: `agent-rules-init — genera CLAUDE.md, AGENTS.md, copilot-instructions y prompts de review/refactor/testing a partir del stack detectado en tu repo.
75
+
76
+ Uso:
77
+ npx agent-rules-init escanea el directorio actual y genera los archivos *.generated.*
78
+ npx agent-rules-init --enrich además, usa tu claude/codex instalado para analizar el código y enriquecer el resultado
79
+ npx agent-rules-init --lang es fuerza el idioma del contenido (es|en); por defecto se detecta del sistema
80
+ npx agent-rules-init --help muestra esta ayuda
81
+ npx agent-rules-init --version muestra la versión
82
+
83
+ Los archivos se crean siempre con sufijo .generated y nunca sobrescriben nada existente:
73
84
  revisa su contenido y quita el sufijo para activarlos.`,
85
+ automationUsage: `Automatización:
86
+ --dry-run renderiza y muestra archivos sin escribir
87
+ --check termina con error si faltan archivos generados; nunca escribe
88
+ --json emite un único resultado JSON legible por máquinas
89
+ --non-interactive omite preguntas y la oferta de enriquecimiento con IA
90
+ --enrich fuerza el enriquecimiento con IA sin preguntar (también sin TTY; combinable con --non-interactive)
91
+ --assistant <id> elige el asistente para enriquecer: claude o codex (por defecto, el primero instalado)
92
+ --model <modelo> modelo a usar, pasado tal cual al asistente (p. ej. haiku, gpt-5.5); por defecto, el del asistente`,
74
93
  unknownOption: (flag) => `Opción no reconocida: ${flag}`,
75
94
  invalidLang: (value) => `Valor de --lang no válido: "${value}" (usa "es" o "en").`,
95
+ invalidAssistant: (value) => `Valor de --assistant no válido: "${value}" (usa "claude" o "codex").`,
96
+ missingFlagValue: (flag) => `La opción ${flag} requiere un valor.`,
97
+ assistantNotAvailable: (assistant) => `Se pidió ${assistant} con --assistant pero no está instalado; se conserva la versión generada.`,
76
98
  noTtyWarning: "No se detectó una terminal interactiva (esto pasa a veces en Git Bash en Windows). " +
77
- "Continuando sin preguntas ni oferta de pulido con IA; se usarán los valores detectados.",
99
+ "Continuando sin preguntas ni oferta de enriquecimiento con IA; se usarán los valores detectados.",
78
100
  skippedQuestion: (message) => `No se detectó una terminal interactiva; se omite la pregunta "${message}" y se usa el valor detectado.`,
79
- polishDetected: (assistant) => `${assistant} detectado — puede ayudar a pulir la redacción final.`,
80
- polishConfirm: (assistant) => `Se detectó ${assistant}. ¿Quieres que pula la redacción final?`,
81
- polishFailed: (assistant, error) => `No se pudo pulir el contenido con ${assistant}, se mantiene el original: ${error}`,
82
- polishPrompt: (content) => `Pule la redacción del siguiente documento de instrucciones para un agente de IA, sin cambiar su significado ni estructura. Devuelve únicamente el documento pulido, sin comentarios ni explicaciones adicionales:\n\n${content}`,
101
+ enrichDetected: (assistant) => `${assistant} detectado — puede analizar el código de este repo y sustituir las secciones genéricas por reglas específicas verificadas.`,
102
+ enrichConfirm: (assistant) => `¿Quieres que ${assistant} analice el repositorio y enriquezca los archivos generados? Usará tu instalación de ${assistant} y puede tardar unos minutos.`,
103
+ enrichWorking: (assistant) => `${assistant} está analizando el repositorio y enriqueciendo los archivos…`,
104
+ enrichDone: "Archivos enriquecidos con lo observado en el repositorio.",
105
+ enrichKept: "No se aplicó el enriquecimiento; se conserva la versión generada.",
106
+ enrichFailed: (assistant, error) => `No se pudo enriquecer el contenido con ${assistant}, se mantiene la versión generada: ${error}`,
107
+ enrichNoAssistant: "Se pidió --enrich pero no se encontró ningún asistente (claude o codex) instalado; se conserva la versión generada.",
108
+ enrichEvidenceDropped: (paths) => `Se descartaron afirmaciones del enriquecimiento porque su evidencia citada no existe en el repo: ${paths.join(", ")}`,
109
+ enrichRetrying: (assistant) => `La respuesta de ${assistant} no pasó la validación; se reintenta una vez…`,
110
+ enrichPrompt: (filesJson, mustKeep, existingDocsJson) => "Estás ejecutándote en la raíz de un repositorio. Los siguientes archivos de instrucciones para agentes de IA " +
111
+ "se generaron solo a partir de manifiestos, CI y configuración, por lo que algunas secciones (convenciones, arquitectura, prompts) son genéricas.\n" +
112
+ "Primero investiga el repositorio real con tus herramientas de lectura: configuración de estilo (linter, formatter, pre-commit), " +
113
+ "CONTRIBUTING/README, y el código fuente y los tests suficientes para entender sus convenciones y arquitectura reales.\n" +
114
+ "Después reescribe cada archivo sustituyendo o ampliando los consejos genéricos con reglas específicas y comprobables de este repositorio, " +
115
+ "citando la evidencia de cada afirmación nueva con el formato (evidencia: `ruta/del/archivo`); las rutas citadas se verificarán contra el repo. " +
116
+ "No inventes comandos, rutas ni APIs; no afirmes nada que no hayas comprobado. " +
117
+ "Conserva el idioma, el formato Markdown y las rutas de cada archivo.\n" +
118
+ (mustKeep.length > 0
119
+ ? `Conserva literalmente estos comandos, sin modificarlos: ${mustKeep.map((c) => `\`${c}\``).join(", ")}.\n`
120
+ : "") +
121
+ (existingDocsJson
122
+ ? "El repositorio ya contiene estos documentos de instrucciones mantenidos a mano; reflejan la intención del equipo. " +
123
+ "Integra sus reglas en los archivos generados correspondientes sin contradecirlas ni perderlas.\n" +
124
+ `Documentos existentes (JSON):\n${existingDocsJson}\n`
125
+ : "") +
126
+ "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. " +
127
+ `Entrada JSON:\n${filesJson}`,
83
128
  fileSkipped: (path) => `${path}: ya existía, se conserva sin cambios.`,
84
129
  outroWritten: "Revisa los archivos *.generated.* y, cuando estés conforme, quita el sufijo " +
85
130
  '".generated" (ej. "CLAUDE.generated.md" → "CLAUDE.md") para activarlos — ' +
@@ -107,6 +152,8 @@ revisa su contenido y quita el sufijo para activarlos.`,
107
152
  assets: "activos",
108
153
  config: "configuración",
109
154
  },
155
+ testDirNote: "tests",
156
+ entrypointNote: "punto de entrada",
110
157
  },
111
158
  en: {
112
159
  generatedHeader: "Generated by agent-rules-init from what was detected in this repo.",
@@ -116,6 +163,7 @@ revisa su contenido y quita el sufijo para activarlos.`,
116
163
  ci: "What CI runs (GitHub Actions)",
117
164
  conventions: "Conventions",
118
165
  architecture: "Architecture",
166
+ canonical: "Canonical commands",
119
167
  },
120
168
  andMore: (count, file) => (file ? `…and ${count} more in ${file}` : `…and ${count} more`),
121
169
  noStackFallback: "No known stack was detected. Fill in this file manually.",
@@ -126,25 +174,60 @@ revisa su contenido y quita el sufijo para activarlos.`,
126
174
  linter: "the linter",
127
175
  packageManager: "the package manager",
128
176
  },
129
- usage: `agent-rules-init — generates CLAUDE.md, AGENTS.md, copilot-instructions and review/refactor/testing prompts from the stack detected in your repo.
130
-
131
- Usage:
132
- npx agent-rules-init scan the current directory and generate the *.generated.* files
133
- npx agent-rules-init --lang en force the content language (es|en); defaults to the system locale
134
- npx agent-rules-init --help show this help
135
- npx agent-rules-init --version show the version
136
-
137
- Files are always created with the .generated suffix and never overwrite anything:
177
+ usage: `agent-rules-init — generates CLAUDE.md, AGENTS.md, copilot-instructions and review/refactor/testing prompts from the stack detected in your repo.
178
+
179
+ Usage:
180
+ npx agent-rules-init scan the current directory and generate the *.generated.* files
181
+ npx agent-rules-init --enrich additionally, use your installed claude/codex to analyze the code and enrich the output
182
+ npx agent-rules-init --lang en force the content language (es|en); defaults to the system locale
183
+ npx agent-rules-init --help show this help
184
+ npx agent-rules-init --version show the version
185
+
186
+ Files are always created with the .generated suffix and never overwrite anything:
138
187
  review their content and drop the suffix to activate them.`,
188
+ automationUsage: `Automation:
189
+ --dry-run render and print files without writing
190
+ --check exit non-zero if generated files are missing; never write
191
+ --json emit a single machine-readable JSON result
192
+ --non-interactive skip questions and the AI-enrichment offer
193
+ --enrich force AI enrichment without asking (works without a TTY; composable with --non-interactive)
194
+ --assistant <id> pick the enrichment assistant: claude or codex (defaults to the first one installed)
195
+ --model <model> model to use, forwarded verbatim to the assistant (e.g. haiku, gpt-5.5); defaults to the assistant's own`,
139
196
  unknownOption: (flag) => `Unknown option: ${flag}`,
140
197
  invalidLang: (value) => `Invalid --lang value: "${value}" (use "es" or "en").`,
198
+ invalidAssistant: (value) => `Invalid --assistant value: "${value}" (use "claude" or "codex").`,
199
+ missingFlagValue: (flag) => `The ${flag} option requires a value.`,
200
+ assistantNotAvailable: (assistant) => `${assistant} was requested with --assistant but is not installed; keeping the generated version.`,
141
201
  noTtyWarning: "No interactive terminal detected (this sometimes happens in Git Bash on Windows). " +
142
- "Continuing without questions or the AI-polish offer; detected values will be used.",
202
+ "Continuing without questions or the AI-enrichment offer; detected values will be used.",
143
203
  skippedQuestion: (message) => `No interactive terminal detected; skipping the question "${message}" and using the detected value.`,
144
- polishDetected: (assistant) => `${assistant} detected — it can help polish the final wording.`,
145
- polishConfirm: (assistant) => `${assistant} was detected. Do you want it to polish the final wording?`,
146
- polishFailed: (assistant, error) => `Couldn't polish the content with ${assistant}, keeping the original: ${error}`,
147
- polishPrompt: (content) => `Polish the wording of the following instructions document for an AI agent, without changing its meaning or structure. Return only the polished document, with no extra comments or explanations:\n\n${content}`,
204
+ enrichDetected: (assistant) => `${assistant} detected — it can analyze this repo's code and replace the generic sections with verified, repo-specific rules.`,
205
+ enrichConfirm: (assistant) => `Do you want ${assistant} to analyze the repository and enrich the generated files? It will use your ${assistant} installation and may take a few minutes.`,
206
+ enrichWorking: (assistant) => `${assistant} is analyzing the repository and enriching the files…`,
207
+ enrichDone: "Files enriched with what was observed in the repository.",
208
+ enrichKept: "Enrichment was not applied; keeping the generated version.",
209
+ enrichFailed: (assistant, error) => `Couldn't enrich the content with ${assistant}, keeping the generated version: ${error}`,
210
+ enrichNoAssistant: "--enrich was requested but no installed assistant (claude or codex) was found; keeping the generated version.",
211
+ enrichEvidenceDropped: (paths) => `Dropped enrichment claims because their cited evidence does not exist in the repo: ${paths.join(", ")}`,
212
+ enrichRetrying: (assistant) => `${assistant}'s response failed validation; retrying once…`,
213
+ enrichPrompt: (filesJson, mustKeep, existingDocsJson) => "You are running at the root of a repository. The following instruction files for AI agents were generated " +
214
+ "from manifests, CI and configuration only, so some sections (conventions, architecture, prompts) are generic.\n" +
215
+ "First investigate the actual repository with your read tools: style configuration (linter, formatter, pre-commit), " +
216
+ "CONTRIBUTING/README, and enough of the source code and tests to understand its real conventions and architecture.\n" +
217
+ "Then rewrite each file, replacing or extending the generic advice with specific, verifiable rules from this repository, " +
218
+ "citing the evidence for every new claim in the form (evidence: `path/to/file`); cited paths will be checked against the repo. " +
219
+ "Do not invent commands, paths or APIs; do not state anything you have not verified. " +
220
+ "Keep each file's language, Markdown format and path.\n" +
221
+ (mustKeep.length > 0
222
+ ? `Keep these commands verbatim, unmodified: ${mustKeep.map((c) => `\`${c}\``).join(", ")}.\n`
223
+ : "") +
224
+ (existingDocsJson
225
+ ? "The repository already contains these hand-maintained instruction documents; they reflect the team's intent. " +
226
+ "Integrate their rules into the corresponding generated files without contradicting or losing them.\n" +
227
+ `Existing documents (JSON):\n${existingDocsJson}\n`
228
+ : "") +
229
+ "Return only a valid JSON array with exactly the same path/content objects in the same order, without a code fence or commentary. " +
230
+ `Input JSON:\n${filesJson}`,
148
231
  fileSkipped: (path) => `${path}: already existed, left unchanged.`,
149
232
  outroWritten: "Review the *.generated.* files and, once you are happy with them, drop the " +
150
233
  '".generated" suffix (e.g. "CLAUDE.generated.md" → "CLAUDE.md") to activate them — ' +
@@ -172,5 +255,7 @@ review their content and drop the suffix to activate them.`,
172
255
  assets: "assets",
173
256
  config: "configuration",
174
257
  },
258
+ testDirNote: "tests",
259
+ entrypointNote: "entry point",
175
260
  },
176
261
  };
@@ -1,10 +1,28 @@
1
1
  import { type Lang } from "./i18n.js";
2
+ import type { GeneratedFile } from "./writer.js";
2
3
  export type AssistantId = "claude" | "codex";
3
4
  export interface ExecResult {
4
5
  stdout: string;
5
6
  exitCode: number;
6
7
  }
7
- export type ExecFn = (command: string, args: string[], stdin?: string) => Promise<ExecResult>;
8
+ export type ExecFn = (command: string, args: string[], stdin?: string, cwd?: string) => Promise<ExecResult>;
8
9
  export declare const defaultExecFn: ExecFn;
9
10
  export declare function detectAvailableAssistants(execFn?: ExecFn): Promise<AssistantId[]>;
10
- export declare function polishWithAssistant(assistant: AssistantId, content: string, execFn?: ExecFn, lang?: Lang): Promise<string>;
11
+ export interface EnrichOptions {
12
+ execFn?: ExecFn;
13
+ lang?: Lang;
14
+ /** Repo root where the assistant is spawned so its read tools explore the right project. */
15
+ cwd?: string;
16
+ /** Commands that must survive verbatim (canonical test/lint/build commands). */
17
+ mustKeep?: readonly string[];
18
+ /** Hand-maintained docs already in the repo (CLAUDE.md, AGENTS.md, …) to integrate, not contradict. */
19
+ existingDocs?: readonly GeneratedFile[];
20
+ /** Model identifier forwarded verbatim to the assistant CLI; its default when omitted. */
21
+ model?: string;
22
+ }
23
+ /**
24
+ * Asks the assistant to investigate the repository it runs in and rewrite the generated
25
+ * files with repo-specific, evidence-backed guidance. Typical runs use one assistant
26
+ * process; only very large outputs are split. Falls back to the originals per batch.
27
+ */
28
+ export declare function enrichFilesWithAssistant(assistant: AssistantId, files: GeneratedFile[], options?: EnrichOptions): Promise<GeneratedFile[]>;