@imolko/ultra-agent 0.1.0-beta

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.
Files changed (239) hide show
  1. package/README.md +93 -0
  2. package/bin/cli.js +2 -0
  3. package/dist-node/bin.d.ts +2 -0
  4. package/dist-node/bin.d.ts.map +1 -0
  5. package/dist-node/bin.js +5 -0
  6. package/dist-node/bin.js.map +1 -0
  7. package/dist-node/cli.d.ts +41 -0
  8. package/dist-node/cli.d.ts.map +1 -0
  9. package/dist-node/cli.js +170 -0
  10. package/dist-node/cli.js.map +1 -0
  11. package/dist-node/index.d.ts +14 -0
  12. package/dist-node/index.d.ts.map +1 -0
  13. package/dist-node/index.js +40 -0
  14. package/dist-node/index.js.map +1 -0
  15. package/dist-node/init-lint.d.ts +43 -0
  16. package/dist-node/init-lint.d.ts.map +1 -0
  17. package/dist-node/init-lint.js +84 -0
  18. package/dist-node/init-lint.js.map +1 -0
  19. package/dist-node/manifest.d.ts +33 -0
  20. package/dist-node/manifest.d.ts.map +1 -0
  21. package/dist-node/manifest.js +59 -0
  22. package/dist-node/manifest.js.map +1 -0
  23. package/dist-node/materializer.d.ts +52 -0
  24. package/dist-node/materializer.d.ts.map +1 -0
  25. package/dist-node/materializer.js +194 -0
  26. package/dist-node/materializer.js.map +1 -0
  27. package/dist-node/package-root.d.ts +5 -0
  28. package/dist-node/package-root.d.ts.map +1 -0
  29. package/dist-node/package-root.js +17 -0
  30. package/dist-node/package-root.js.map +1 -0
  31. package/dist-node/payload.d.ts +22 -0
  32. package/dist-node/payload.d.ts.map +1 -0
  33. package/dist-node/payload.js +49 -0
  34. package/dist-node/payload.js.map +1 -0
  35. package/dist-node/pointer-hub.d.ts +22 -0
  36. package/dist-node/pointer-hub.d.ts.map +1 -0
  37. package/dist-node/pointer-hub.js +50 -0
  38. package/dist-node/pointer-hub.js.map +1 -0
  39. package/dist-node/publication.cli.d.ts +9 -0
  40. package/dist-node/publication.cli.d.ts.map +1 -0
  41. package/dist-node/publication.cli.js +17 -0
  42. package/dist-node/publication.cli.js.map +1 -0
  43. package/dist-node/publication.d.ts +23 -0
  44. package/dist-node/publication.d.ts.map +1 -0
  45. package/dist-node/publication.js +47 -0
  46. package/dist-node/publication.js.map +1 -0
  47. package/dist-node/reconcile.cli.d.ts +11 -0
  48. package/dist-node/reconcile.cli.d.ts.map +1 -0
  49. package/dist-node/reconcile.cli.js +39 -0
  50. package/dist-node/reconcile.cli.js.map +1 -0
  51. package/dist-node/reconcile.d.ts +30 -0
  52. package/dist-node/reconcile.d.ts.map +1 -0
  53. package/dist-node/reconcile.js +51 -0
  54. package/dist-node/reconcile.js.map +1 -0
  55. package/dist-node/ship/prompts/fix.md +22 -0
  56. package/dist-node/ship/prompts/implement.md +34 -0
  57. package/dist-node/ship/prompts/review.md +28 -0
  58. package/dist-node/ship/ship.agent.d.ts +22 -0
  59. package/dist-node/ship/ship.agent.d.ts.map +1 -0
  60. package/dist-node/ship/ship.agent.js +142 -0
  61. package/dist-node/ship/ship.agent.js.map +1 -0
  62. package/dist-node/ship/ship.args.d.ts +15 -0
  63. package/dist-node/ship/ship.args.d.ts.map +1 -0
  64. package/dist-node/ship/ship.args.js +93 -0
  65. package/dist-node/ship/ship.args.js.map +1 -0
  66. package/dist-node/ship/ship.context.d.ts +12 -0
  67. package/dist-node/ship/ship.context.d.ts.map +1 -0
  68. package/dist-node/ship/ship.context.js +3 -0
  69. package/dist-node/ship/ship.context.js.map +1 -0
  70. package/dist-node/ship/ship.d.ts +4 -0
  71. package/dist-node/ship/ship.d.ts.map +1 -0
  72. package/dist-node/ship/ship.js +221 -0
  73. package/dist-node/ship/ship.js.map +1 -0
  74. package/dist-node/ship/ship.mr.d.ts +9 -0
  75. package/dist-node/ship/ship.mr.d.ts.map +1 -0
  76. package/dist-node/ship/ship.mr.js +171 -0
  77. package/dist-node/ship/ship.mr.js.map +1 -0
  78. package/dist-node/ship/ship.output.d.ts +9 -0
  79. package/dist-node/ship/ship.output.d.ts.map +1 -0
  80. package/dist-node/ship/ship.output.js +21 -0
  81. package/dist-node/ship/ship.output.js.map +1 -0
  82. package/dist-node/ship/ship.tracker.d.ts +37 -0
  83. package/dist-node/ship/ship.tracker.d.ts.map +1 -0
  84. package/dist-node/ship/ship.tracker.js +220 -0
  85. package/dist-node/ship/ship.tracker.js.map +1 -0
  86. package/dist-node/version.cli.d.ts +9 -0
  87. package/dist-node/version.cli.d.ts.map +1 -0
  88. package/dist-node/version.cli.js +30 -0
  89. package/dist-node/version.cli.js.map +1 -0
  90. package/dist-node/version.d.ts +24 -0
  91. package/dist-node/version.d.ts.map +1 -0
  92. package/dist-node/version.js +76 -0
  93. package/dist-node/version.js.map +1 -0
  94. package/docs/adr/ADR-0000-proceso-y-formato-de-adrs.md +115 -0
  95. package/docs/adr/foundational/A-001-clean-architecture-4-capas.md +88 -0
  96. package/docs/adr/foundational/A-002-cqrs-semantico.md +58 -0
  97. package/docs/adr/foundational/A-003-tdd.md +97 -0
  98. package/docs/adr/foundational/A-005-either-return-custom-methods.md +80 -0
  99. package/docs/adr/foundational/A-006-feature-completion-criteria.md +47 -0
  100. package/docs/adr/foundational/A-007-data-contract-fail-fast.md +109 -0
  101. package/docs/adr/foundational/A-008-bounded-context-decomposition.md +130 -0
  102. package/docs/adr/foundational/A-009-use-case-taxonomy.md +99 -0
  103. package/docs/adr/foundational/A-010-propiedad-emision-eventos-dominio.md +95 -0
  104. package/docs/adr/foundational/A-011-characterization-first-refactoring.md +90 -0
  105. package/docs/adr/foundational/A-012-helm-mecanismo-primario-despliegue.md +67 -0
  106. package/docs/adr/foundational/A-013-contrato-settings-unica-fuente-configuracion.md +74 -0
  107. package/docs/adr/foundational/A-014-generacion-artefactos-despliegue-desde-contrato-settings.md +75 -0
  108. package/docs/adr/foundational/A-015-cloudevents-estandar-eventos.md +85 -0
  109. package/docs/adr/foundational/A-016-cicd-gitlab-pipeline-raiz-pipelines-modulares.md +92 -0
  110. package/docs/adr/foundational/A-017-taxonomia-repositorios-librerias-sistemas.md +93 -0
  111. package/docs/adr/foundational/A-018-practicas-build-imagenes-docker.md +85 -0
  112. package/docs/adr/foundational/A-019-convenciones-autoria-helm-charts.md +97 -0
  113. package/docs/adr/foundational/A-020-entorno-desarrollo-estandarizado-devcontainer.md +58 -0
  114. package/docs/adr/foundational/A-022-formato-reporte-tests-ctrf.md +55 -0
  115. package/docs/adr/foundational/A-023-composicion-bloques-prompt-agentes.md +65 -0
  116. package/docs/adr/foundational/A-024-features-slices-verticales-portables.md +41 -0
  117. package/docs/adr/foundational/A-025-anatomia-mecanica-feature-puertos-adaptadores-drivers.md +104 -0
  118. package/docs/adr/foundational/A-026-infraestructura-compartida.md +44 -0
  119. package/docs/adr/foundational/A-027-patron-wiring-entry-point.md +67 -0
  120. package/docs/adr/foundational/A-029-verbos-intencion-metodos-lado-negocio.md +54 -0
  121. package/docs/adr/foundational/A-030-global-development-tooling.md +52 -0
  122. package/docs/adr/foundational/A-031-ubiquitous-language.md +64 -0
  123. package/docs/adr/foundational/A-032-domain-artifact-documentation-format.md +128 -0
  124. package/docs/adr/foundational/A-033-estandar-mensajes-commit.md +127 -0
  125. package/docs/adr/foundational/A-034-canonical-skill-set.md +64 -0
  126. package/docs/adr/foundational/A-035-captura-ideas-reanclaje-candies.md +51 -0
  127. package/docs/adr/foundational/A-036-adopcion-wayfinder-ortogonalidad-labels.md +37 -0
  128. package/docs/adr/foundational/A-037-formalizacion-taxonomia-labels.md +98 -0
  129. package/docs/adr/foundational/A-038-issues-referencian-skills-nombre.md +54 -0
  130. package/docs/adr/foundational/A-039-pipeline-ship-issues-hijos-spec.md +64 -0
  131. package/docs/adr/foundational/A-040-punteros-configuracion-repo-skills.md +44 -0
  132. package/docs/adr/foundational/A-041-estrategia-de-ramas.md +45 -0
  133. package/docs/adr/foundational/A-042-formato-documentos-issue-tracker.md +78 -0
  134. package/docs/adr/foundational/A-043-vocabulario-etiquetas-adrs.md +42 -0
  135. package/docs/adr/foundational/A-044-composicion-funcional-procesos-operaciones.md +44 -0
  136. package/docs/adr/foundational/A-045-guardrails-aplicables-no-directrices.md +48 -0
  137. package/docs/adr/foundational/A-046-metricas-de-forma-y-legibilidad-del-codigo.md +50 -0
  138. package/docs/adr/foundational/A-047-dependencias-explicitas-sin-globales-ambientales.md +40 -0
  139. package/docs/adr/foundational/A-048-cobertura-minima-codigo.md +39 -0
  140. package/docs/adr/foundational/A-049-materializacion-bucle-guardrails-extension-nativa-pi.md +49 -0
  141. package/docs/adr/foundational/A-050-el-caso-de-uso-pide-al-dominio-no-lee-su-representacion.md +50 -0
  142. package/docs/adr/foundational/A-051-orden-imports-por-distancia.md +47 -0
  143. package/docs/adr/foundational/A-052-distribucion-metodologia-paquete-versionado.md +42 -0
  144. package/docs/adr/foundational/A-053-errores-compartidos-entre-features-dos-hogares-por-capa.md +43 -0
  145. package/docs/adr/foundational/A-054-contrato-costura-driver-port-raw-data-canal-error.md +65 -0
  146. package/docs/adr/foundational/index.md +98 -0
  147. package/docs/adr/index.md +19 -0
  148. package/docs/adr/python/P-001-dependencias-por-feature-fastapi-depends.md +63 -0
  149. package/docs/adr/python/P-002-inyeccion-token-por-request-sin-persistirlo.md +35 -0
  150. package/docs/adr/python/P-003-sqlalchemy-async-connection-pooling.md +59 -0
  151. package/docs/adr/python/P-004-serializer-json-custom-modelos-pydantic.md +47 -0
  152. package/docs/adr/python/P-005-patrones-pitfalls-nicegui.md +148 -0
  153. package/docs/adr/python/P-006-convenciones-pytest.md +96 -0
  154. package/docs/adr/python/P-007-ci-python-alineacion-poetry-version-unica.md +55 -0
  155. package/docs/adr/python/P-008-empaquetado-infraestructura-compartida-por-tecnologia.md +38 -0
  156. package/docs/adr/python/index.md +14 -0
  157. package/docs/adr/typescript/T-001-ddd-artifact-validation-zod-primitives.md +136 -0
  158. package/docs/adr/typescript/T-002-test-documentation-jsdoc.md +133 -0
  159. package/docs/adr/typescript/T-003-readme-formatting-domain-artifacts.md +51 -0
  160. package/docs/adr/typescript/T-005-input-optionality-use-case-not-entity.md +130 -0
  161. package/docs/adr/typescript/T-006-small-enums-inlined-zod-regex.md +77 -0
  162. package/docs/adr/typescript/T-007-controller-input-zod-schema.md +79 -0
  163. package/docs/adr/typescript/T-008-expressive-use-case-pipe.md +207 -0
  164. package/docs/adr/typescript/T-009-mdx-brace-escaping.md +80 -0
  165. package/docs/adr/typescript/T-010-stack-tooling-typescript-dominio.md +53 -0
  166. package/docs/adr/typescript/T-011-ultra-reporter-herramienta-canonica-documentacion.md +52 -0
  167. package/docs/adr/typescript/T-012-npm-workspaces-estructura-monorepo-clean-architecture.md +54 -0
  168. package/docs/adr/typescript/T-013-definicion-eventos-versionados-ultra-ddd.md +45 -0
  169. package/docs/adr/typescript/T-014-publicacion-distribucion-librerias-npm-semver.md +53 -0
  170. package/docs/adr/typescript/T-015-method-placement-definition.md +35 -0
  171. package/docs/adr/typescript/T-016-contrato-settings-zod.md +34 -0
  172. package/docs/adr/typescript/T-017-characterization-tests-golden-files.md +39 -0
  173. package/docs/adr/typescript/T-018-procedencia-campos-frontmatter.md +55 -0
  174. package/docs/adr/typescript/T-019-ultra-generator-herramienta-canonica-generacion.md +35 -0
  175. package/docs/adr/typescript/T-020-ultra-settings-herramienta-canonica-exportacion-despliegue.md +36 -0
  176. package/docs/adr/typescript/T-021-pipe-flujo-datos-operadores-semantica-contexto-inmutable.md +105 -0
  177. package/docs/adr/typescript/T-022-construction-is-execution-single-boundary.md +112 -0
  178. package/docs/adr/typescript/T-023-registry-cli-declara-dependencias-reales.md +84 -0
  179. package/docs/adr/typescript/T-024-paquete-ultra-agent-manifiesto-pi-peers-cli.md +39 -0
  180. package/docs/adr/typescript/T-025-publicacion-npm-trusted-publishing-procedencia.md +59 -0
  181. package/docs/adr/typescript/index.md +30 -0
  182. package/eslint/index.mjs +721 -0
  183. package/package.json +94 -0
  184. package/pi/extensions/handoff/README.md +92 -0
  185. package/pi/extensions/handoff/handoff-prompt.txt +9 -0
  186. package/pi/extensions/handoff/index.ts +296 -0
  187. package/pi/extensions/handoff/package.json +12 -0
  188. package/pi/extensions/qa-guardrail/README.md +108 -0
  189. package/pi/extensions/qa-guardrail/adapters/cli.ts +100 -0
  190. package/pi/extensions/qa-guardrail/adapters/eslint.ts +90 -0
  191. package/pi/extensions/qa-guardrail/adapters/registry.ts +45 -0
  192. package/pi/extensions/qa-guardrail/baseline.ts +74 -0
  193. package/pi/extensions/qa-guardrail/cerco.ts +134 -0
  194. package/pi/extensions/qa-guardrail/check.ts +185 -0
  195. package/pi/extensions/qa-guardrail/config.ts +189 -0
  196. package/pi/extensions/qa-guardrail/delta.ts +52 -0
  197. package/pi/extensions/qa-guardrail/feedback.ts +26 -0
  198. package/pi/extensions/qa-guardrail/index.ts +311 -0
  199. package/pi/extensions/qa-guardrail/nstop.ts +82 -0
  200. package/pi/extensions/qa-guardrail/package.json +12 -0
  201. package/pi/extensions/qa-guardrail/selection.ts +98 -0
  202. package/pi/extensions/qa-guardrail/types.ts +64 -0
  203. package/scripts/copy-assets.sh +10 -0
  204. package/scripts/reconcile-tags.sh +94 -0
  205. package/scripts/release-beta.sh +64 -0
  206. package/scripts/release-build.sh +53 -0
  207. package/scripts/release.sh +51 -0
  208. package/scripts/verify-adr-consistency.py +217 -0
  209. package/skills/code-review/SKILL.md +66 -0
  210. package/skills/codebase-design/SKILL.md +46 -0
  211. package/skills/configure-repo/SKILL.md +33 -0
  212. package/skills/diagnose-bug/SKILL.md +63 -0
  213. package/skills/domain-modeling/SKILL.md +56 -0
  214. package/skills/grill-me/SKILL.md +34 -0
  215. package/skills/i-have-an-idea/SKILL.md +166 -0
  216. package/skills/implement/SKILL.md +22 -0
  217. package/skills/research/SKILL.md +17 -0
  218. package/skills/ship/SKILL.md +26 -0
  219. package/skills/spike/SKILL.md +27 -0
  220. package/skills/tdd/SKILL.md +68 -0
  221. package/skills/tdd/references/deep-modules.md +53 -0
  222. package/skills/tdd/references/interface-design.md +74 -0
  223. package/skills/tdd/references/mocking.md +80 -0
  224. package/skills/tdd/references/refactoring.md +49 -0
  225. package/skills/tdd/references/tests.md +184 -0
  226. package/skills/to-specs/SKILL.md +141 -0
  227. package/skills/to-tasks/SKILL.md +143 -0
  228. package/skills/triage/SKILL.md +73 -0
  229. package/skills/ultra-generator/SKILL.md +263 -0
  230. package/skills/ultra-generator/references/aggregate-root.md +149 -0
  231. package/skills/ultra-generator/references/domain-event.md +174 -0
  232. package/skills/ultra-generator/references/entity.md +133 -0
  233. package/skills/ultra-generator/references/service.md +92 -0
  234. package/skills/ultra-generator/references/value-object.md +135 -0
  235. package/skills/wayfinder/SKILL.md +201 -0
  236. package/skills/workflow/SKILL.md +31 -0
  237. package/skills/writing-for-agents/SKILL-MECHANICS.md +32 -0
  238. package/skills/writing-for-agents/SKILL.md +86 -0
  239. package/skills/writing-for-agents/references/adrs.md +19 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Base for future CLI adapters (ruff, pylint, …).
3
+ *
4
+ * Same contract as the in-process eslint adapter — `lint(file) → Violation[]`,
5
+ * never failing silently. When linting is impossible (binary missing or the
6
+ * run/parse fails), the adapter throws and the runner catches, warns, and
7
+ * skips feedback:
8
+ *
9
+ * - Lazy: the binary is resolved from PATH on the *first* lint call, never at
10
+ * adapter creation, so loading the extension requires no linter at all.
11
+ * - Missing binary → throw (message = missingBinaryWarning).
12
+ * - Run/parse errors → rethrow.
13
+ */
14
+
15
+ import { execFile } from "node:child_process";
16
+ import { existsSync } from "node:fs";
17
+ import { delimiter, join, resolve } from "node:path";
18
+ import { promisify } from "node:util";
19
+ import type { LintAdapter, LintContext, Violation } from "../types.js";
20
+
21
+ const execFileAsync = promisify(execFile);
22
+
23
+ /**
24
+ * Resolve a binary name against PATH. Returns the absolute path, or `null`
25
+ * when not found. `env` and `exists` are injectable for tests.
26
+ */
27
+ export function resolveBinary(
28
+ binary: string,
29
+ env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env,
30
+ exists: (p: string) => boolean = existsSync,
31
+ ): string | null {
32
+ const pathVar = env.PATH ?? env.Path ?? env.path;
33
+ if (typeof pathVar !== "string" || pathVar === "") {
34
+ return null;
35
+ }
36
+ for (const dir of pathVar.split(delimiter)) {
37
+ if (dir === "") continue;
38
+ const candidate = join(dir, binary);
39
+ if (exists(candidate)) {
40
+ return candidate;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /** Message thrown when a future CLI adapter's binary is absent. */
47
+ export function missingBinaryWarning(id: string, binary: string): string {
48
+ return `qa-guardrail: binario ausente — ${binary} (adaptador ${id}); no se linteará`;
49
+ }
50
+
51
+ export interface CliAdapterOptions {
52
+ /** Whitelisted id, e.g. "ruff". */
53
+ id: string;
54
+ /** Binary name resolved from PATH. */
55
+ binary: string;
56
+ /** Extra args placed before the file argument. */
57
+ args?: string[];
58
+ /** Parse stdout/stderr into normalized violations. */
59
+ parse: (stdout: string, stderr: string, file: string) => Violation[];
60
+ /** Binary resolver. Defaults to `resolveBinary`. Injectable for tests. */
61
+ findBinary?: (binary: string) => string | null;
62
+ /** Runner. Defaults to `execFile`. Injectable for tests. */
63
+ run?: (
64
+ binary: string,
65
+ args: string[],
66
+ ctx: LintContext,
67
+ ) => Promise<{ stdout: string; stderr: string }>;
68
+ }
69
+
70
+ async function defaultRun(
71
+ binary: string,
72
+ args: string[],
73
+ ctx: LintContext,
74
+ ): Promise<{ stdout: string; stderr: string }> {
75
+ const { stdout, stderr } = await execFileAsync(binary, args, { cwd: ctx.cwd });
76
+ return { stdout: stdout as string, stderr: stderr as string };
77
+ }
78
+
79
+ export function createCliAdapter(options: CliAdapterOptions): LintAdapter {
80
+ let binaryPath: string | null | undefined;
81
+ const findBinary = options.findBinary ?? resolveBinary;
82
+ const run = options.run ?? defaultRun;
83
+
84
+ return {
85
+ id: options.id,
86
+ async lint(file, ctx) {
87
+ if (binaryPath === undefined) {
88
+ binaryPath = findBinary(options.binary);
89
+ }
90
+ if (binaryPath === null) {
91
+ // Fail-fast: linter missing → throw; the runner warns and skips.
92
+ throw new Error(missingBinaryWarning(options.id, options.binary));
93
+ }
94
+ const absolutePath = resolve(ctx.cwd, file);
95
+ const args = [...(options.args ?? []), absolutePath];
96
+ const { stdout, stderr } = await run(binaryPath, args, ctx);
97
+ return options.parse(stdout, stderr, absolutePath);
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * In-process ESLint adapter (Node API).
3
+ *
4
+ * - Lazy: `import("eslint")` only happens on the first lint call, so loading
5
+ * the extension never requires eslint to be installed.
6
+ * - Single instance: one ESLint instance is reused across calls; it is rebuilt
7
+ * only if the project root changes or `eslint.config.mjs` changes (mtime),
8
+ * so mid-session edits to the rules take effect on the next lint.
9
+ * - Throws when linting is impossible (missing module or broken config); the
10
+ * runner catches, warns, and skips. Returns `[]` for a clean or ignored file.
11
+ */
12
+
13
+ import { statSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import type { LintAdapter, Violation } from "../types.js";
16
+
17
+ /** Flat-config file whose mtime invalidates the cached ESLint instance. */
18
+ export const ESLINT_CONFIG_FILE = "eslint.config.mjs";
19
+
20
+ /** Last-modified time of the flat config, or `null` when it does not exist. */
21
+ function configMtimeMs(root: string): number | null {
22
+ try {
23
+ return statSync(resolve(root, ESLINT_CONFIG_FILE)).mtimeMs;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ interface EslintMessage {
30
+ ruleId?: string | null;
31
+ message: string;
32
+ line?: number;
33
+ column?: number;
34
+ severity?: number;
35
+ endLine?: number;
36
+ endColumn?: number;
37
+ }
38
+
39
+ interface EslintLintResult {
40
+ messages: EslintMessage[];
41
+ }
42
+
43
+ interface EslintInstance {
44
+ lintFiles(patterns: string[]): Promise<EslintLintResult[]>;
45
+ }
46
+
47
+ function toViolation(message: EslintMessage): Violation {
48
+ return {
49
+ ruleId: message.ruleId ?? null,
50
+ message: message.message,
51
+ line: message.line ?? 0,
52
+ column: message.column ?? 0,
53
+ severity: message.severity ?? 2,
54
+ endLine: message.endLine,
55
+ endColumn: message.endColumn,
56
+ };
57
+ }
58
+
59
+ export function createEslintAdapter(): LintAdapter {
60
+ let currentRoot: string | null = null;
61
+ let currentConfigMtime: number | null = null;
62
+ let current: Promise<EslintInstance> | null = null;
63
+
64
+ function getInstance(root: string): Promise<EslintInstance> {
65
+ const mtime = configMtimeMs(root);
66
+ if (currentRoot === root && currentConfigMtime === mtime && current) {
67
+ return current;
68
+ }
69
+ currentRoot = root;
70
+ currentConfigMtime = mtime;
71
+ current = import("eslint")
72
+ .then((mod) => mod.loadESLint())
73
+ .then(
74
+ (ESLintClass) =>
75
+ new ESLintClass({ cwd: root, warnIgnored: false }) as EslintInstance,
76
+ );
77
+ return current;
78
+ }
79
+
80
+ return {
81
+ id: "eslint",
82
+ async lint(file, ctx) {
83
+ const absolutePath = resolve(ctx.cwd, file);
84
+ const instance = await getInstance(ctx.root);
85
+ const results = await instance.lintFiles([absolutePath]);
86
+ const messages = results[0]?.messages ?? [];
87
+ return messages.map(toViolation);
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Whitelisted adapter registry: id → adapter.
3
+ *
4
+ * Only adapters explicitly registered here can be selected by config. Unknown
5
+ * ids resolve to `undefined` *and* emit a warning (never a crash): this is the
6
+ * whitelist that makes the config selection-only.
7
+ */
8
+
9
+ import type { LintAdapter } from "../types.js";
10
+
11
+ const registry = new Map<string, LintAdapter>();
12
+
13
+ /** Emits the warning for an unknown id. No-op by default; tests override it. */
14
+ let unknownIdWarn: (message: string) => void = () => {};
15
+
16
+ export function registerAdapter(adapter: LintAdapter): void {
17
+ registry.set(adapter.id, adapter);
18
+ }
19
+
20
+ /**
21
+ * Replace the unknown-id warning sink. The default is a no-op so the registry
22
+ * stays usable without a UI/logger; the extension wires it to its own warning
23
+ * channel at startup.
24
+ */
25
+ export function setUnknownIdWarn(fn: (message: string) => void): void {
26
+ unknownIdWarn = fn;
27
+ }
28
+
29
+ export function getAdapter(id: string): LintAdapter | undefined {
30
+ const adapter = registry.get(id);
31
+ if (!adapter) {
32
+ unknownIdWarn(`qa-guardrail: adaptador no registrado — ${id}`);
33
+ }
34
+ return adapter;
35
+ }
36
+
37
+ export function isWhitelisted(id: string): boolean {
38
+ return registry.has(id);
39
+ }
40
+
41
+ /** Test helper: empty the registry and restore the warning no-op. */
42
+ export function clearRegistry(): void {
43
+ registry.clear();
44
+ unknownIdWarn = () => {};
45
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Frozen per-file, per-session baseline for delta scoping.
3
+ *
4
+ * The baseline is captured on `tool_call` *before* the first edit: the
5
+ * violations the file already had when the agent found it. Once captured it is
6
+ * frozen for the rest of the session — subsequent edits must not re-capture
7
+ * (that would swallow the agent's own changes into the baseline). New files
8
+ * (which do not exist yet at capture time) get an empty baseline.
9
+ */
10
+
11
+ import { existsSync } from "node:fs";
12
+ import type { Violation } from "./types.js";
13
+
14
+ export interface BaselineDeps {
15
+ /** Filesystem probe used to decide whether a file is new. */
16
+ exists(path: string): boolean;
17
+ }
18
+
19
+ export interface BaselineStore {
20
+ /**
21
+ * Capture the baseline for `file` if it has not been captured yet, and
22
+ * return it. Existing files are linted via `lint`; new files get `[]`
23
+ * without linting. Never throws: a lint failure yields `[]`.
24
+ */
25
+ capture(file: string, lint: (file: string) => Promise<Violation[]>): Promise<Violation[]>;
26
+ /** The frozen baseline for `file`, or `undefined` if not yet captured. */
27
+ get(file: string): Violation[] | undefined;
28
+ /** Whether a baseline has been captured for `file`. */
29
+ has(file: string): boolean;
30
+ /** Forget the baseline for `file` (human reset), so it can be re-captured. */
31
+ reset(file: string): void;
32
+ /** Forget every baseline (session teardown / tests). */
33
+ clear(): void;
34
+ }
35
+
36
+ export function createBaselineStore(deps: Partial<BaselineDeps> = {}): BaselineStore {
37
+ const exists = deps.exists ?? existsSync;
38
+ const store = new Map<string, Violation[]>();
39
+
40
+ return {
41
+ async capture(file, lint) {
42
+ if (store.has(file)) {
43
+ return store.get(file)!;
44
+ }
45
+
46
+ let baseline: Violation[] = [];
47
+ if (exists(file)) {
48
+ try {
49
+ baseline = await lint(file);
50
+ } catch {
51
+ baseline = [];
52
+ }
53
+ }
54
+ store.set(file, baseline);
55
+ return baseline;
56
+ },
57
+
58
+ get(file) {
59
+ return store.get(file);
60
+ },
61
+
62
+ has(file) {
63
+ return store.has(file);
64
+ },
65
+
66
+ reset(file) {
67
+ store.delete(file);
68
+ },
69
+
70
+ clear() {
71
+ store.clear();
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Cerco (fence) for qa-guardrail (#320).
3
+ *
4
+ * Two independent guards the agent cannot bypass:
5
+ *
6
+ * 1. A hardcoded protected-path list — `eslint.config.mjs`,
7
+ * `.pi/qa-guardrail.json`, and this extension's own directory. Writes/edits
8
+ * to those paths are blocked at `tool_call`: the rules definition and the
9
+ * cerco itself are not editable by the agent (A-045/A-049).
10
+ * 2. Detection of `eslint-disable*` directives, independent of ESLint (which
11
+ * currently lets them pass via the `eslint` exemption of `no-comments`).
12
+ * New directives in `write.content` / `edit.newText` are blocked at
13
+ * `tool_call`. Inherited ones are out of scope here and only surface in
14
+ * `/qa-guardrail check` (#321).
15
+ */
16
+
17
+ import { relative, resolve, sep } from "node:path";
18
+
19
+ /** Hardcoded protected list. Relative to the project root (cwd). */
20
+ export const PROTECTED_PATHS: readonly string[] = [
21
+ "eslint.config.mjs",
22
+ ".pi/qa-guardrail.json",
23
+ ".pi/extensions/qa-guardrail/**",
24
+ ];
25
+
26
+ /** Matches `eslint-disable`, `eslint-disable-line`, `eslint-disable-next-line`. */
27
+ export const ESLINT_DISABLE_PATTERN = /eslint-disable(?:-line|-next-line)?\b/;
28
+
29
+ /** True when `text` contains an eslint-disable* directive. */
30
+ export function hasEslintDisableDirective(text: string): boolean {
31
+ return ESLINT_DISABLE_PATTERN.test(text);
32
+ }
33
+
34
+ /** Every eslint-disable* directive found in `text`, in order of appearance. */
35
+ export function findEslintDisableDirectives(text: string): string[] {
36
+ return text.match(/eslint-disable(?:-line|-next-line)?\b/g) ?? [];
37
+ }
38
+
39
+ function toPosix(p: string): string {
40
+ return p.split(sep).join("/");
41
+ }
42
+
43
+ /** True when `file` (relative to `cwd`) is in the protected list. */
44
+ export function isProtectedPath(file: string, cwd: string): boolean {
45
+ return protectedPathReason(file, cwd) !== undefined;
46
+ }
47
+
48
+ /** Human-readable reason why `file` is protected, or `undefined` if it is not. */
49
+ export function protectedPathReason(file: string, cwd: string): string | undefined {
50
+ const abs = resolve(cwd, file);
51
+ const rel = toPosix(relative(resolve(cwd), abs));
52
+
53
+ for (const pattern of PROTECTED_PATHS) {
54
+ if (pattern.endsWith("/**")) {
55
+ const dir = pattern.slice(0, -"/**".length);
56
+ if (rel === dir || rel.startsWith(`${dir}/`)) {
57
+ return `qa-guardrail: ${file} está protegido (${pattern}) — el cerco no es editable por el agente`;
58
+ }
59
+ } else if (rel === pattern) {
60
+ return `qa-guardrail: ${file} está protegido (${pattern}) — el cerco no es editable por el agente`;
61
+ }
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ export type CercoCheck = { blocked: true; reason: string } | { blocked: false };
67
+
68
+ function isRecord(value: unknown): value is Record<string, unknown> {
69
+ return typeof value === "object" && value !== null && !Array.isArray(value);
70
+ }
71
+
72
+ /** The strings a write/edit is about to introduce (write.content / edit.newText). */
73
+ function extractNewTexts(toolName: string, input: unknown): string[] {
74
+ if (!isRecord(input)) {
75
+ return [];
76
+ }
77
+
78
+ if (toolName === "write") {
79
+ const content = input.content;
80
+ return typeof content === "string" ? [content] : [];
81
+ }
82
+
83
+ if (toolName === "edit") {
84
+ const texts: string[] = [];
85
+ const edits = input.edits;
86
+ if (Array.isArray(edits)) {
87
+ for (const edit of edits) {
88
+ if (isRecord(edit) && typeof edit.newText === "string") {
89
+ texts.push(edit.newText);
90
+ }
91
+ }
92
+ }
93
+ // Legacy session format: top-level oldText/newText (see pi extension docs).
94
+ const newText = input.newText;
95
+ if (typeof newText === "string") {
96
+ texts.push(newText);
97
+ }
98
+ return texts;
99
+ }
100
+
101
+ return [];
102
+ }
103
+
104
+ /**
105
+ * Check both cerco guards for a `tool_call`. Returns the first block, if any.
106
+ * Non-write/edit tools always pass (the cerco only fences edits).
107
+ */
108
+ export function checkCerco(
109
+ toolName: string,
110
+ input: unknown,
111
+ file: string,
112
+ cwd: string,
113
+ ): CercoCheck {
114
+ if (toolName !== "write" && toolName !== "edit") {
115
+ return { blocked: false };
116
+ }
117
+
118
+ const protectedReason = protectedPathReason(file, cwd);
119
+ if (protectedReason) {
120
+ return { blocked: true, reason: protectedReason };
121
+ }
122
+
123
+ for (const text of extractNewTexts(toolName, input)) {
124
+ const directive = findEslintDisableDirectives(text)[0];
125
+ if (directive) {
126
+ return {
127
+ blocked: true,
128
+ reason: `qa-guardrail: directiva ${directive} nueva en ${file} bloqueada — el cerco prohíbe desactivar reglas en línea`,
129
+ };
130
+ }
131
+ }
132
+
133
+ return { blocked: false };
134
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Full-sweep `/qa-guardrail check`: list every matching file's violations and
3
+ * inherited `eslint-disable*` directives, without blocking.
4
+ *
5
+ * Unlike the tool_result delta feedback, this is a *complete* sweep: it reports
6
+ * inherited violations too (the migration inventory). Inherited directives are
7
+ * detected with the same independent regex as the cerco (slice 4) and listed,
8
+ * never blocked.
9
+ */
10
+
11
+ import { readFileSync, readdirSync } from "node:fs";
12
+ import { relative, resolve } from "node:path";
13
+ import type { ConfigLoadResult } from "./config.js";
14
+ import { findProjectRoot, loadConfig } from "./config.js";
15
+ import { findEslintDisableDirectives } from "./cerco.js";
16
+ import { getAdapter } from "./adapters/registry.js";
17
+ import { isFileSelected } from "./selection.js";
18
+ import type { LintAdapter, Violation } from "./types.js";
19
+
20
+ /** One file with at least one violation or inherited directive. */
21
+ export interface CheckFinding {
22
+ /** File path relative to `cwd` (posix separators). */
23
+ file: string;
24
+ /** Adapter that covered the file. */
25
+ adapterId: string;
26
+ violations: Violation[];
27
+ disableDirectives: string[];
28
+ }
29
+
30
+ export interface CheckReport {
31
+ /** False when the config is absent/invalid (gate off). */
32
+ ok: boolean;
33
+ /** Gate-off reason when `ok` is false. */
34
+ reason?: string;
35
+ findings: CheckFinding[];
36
+ /** Files assigned to an adapter (selected), whether or not they had findings. */
37
+ filesScanned: number;
38
+ }
39
+
40
+ export interface CheckDeps {
41
+ loadConfig(cwd: string): ConfigLoadResult;
42
+ getAdapter(id: string): LintAdapter | undefined;
43
+ findRoot(cwd: string, markers: string[]): string | null;
44
+ listFiles(root: string): Promise<string[]>;
45
+ readFile(path: string): string | null;
46
+ warn(message: string): void;
47
+ }
48
+
49
+ /** Directories the sweep never descends into (repo hygiene, not config). */
50
+ export const CHECK_SKIP_DIRS: ReadonlySet<string> = new Set(["node_modules", ".git"]);
51
+
52
+ /** Recursive, cycle-safe file enumeration (absolute paths). */
53
+ export function listFilesSync(root: string): string[] {
54
+ const out: string[] = [];
55
+ const stack = [root];
56
+ while (stack.length > 0) {
57
+ const dir = stack.pop()!;
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(dir, { withFileTypes: true });
61
+ } catch {
62
+ continue;
63
+ }
64
+ for (const entry of entries) {
65
+ if (entry.isSymbolicLink()) continue;
66
+ const abs = resolve(dir, entry.name);
67
+ if (entry.isDirectory()) {
68
+ if (CHECK_SKIP_DIRS.has(entry.name)) continue;
69
+ stack.push(abs);
70
+ } else if (entry.isFile()) {
71
+ out.push(abs);
72
+ }
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function toPosix(p: string): string {
79
+ return p.split("\\").join("/");
80
+ }
81
+
82
+ /**
83
+ * Sweep `cwd`: for each enabled tool entry, walk its project root and assign
84
+ * each selected file to the first entry that covers it (mirrors
85
+ * `selectAdapterId` first-wins). Then lint each assigned file once and collect
86
+ * violations plus inherited directives.
87
+ */
88
+ export async function runCheck(cwd: string, deps: CheckDeps): Promise<CheckReport> {
89
+ const result = deps.loadConfig(cwd);
90
+ if (!result.ok) {
91
+ return { ok: false, reason: result.reason, findings: [], filesScanned: 0 };
92
+ }
93
+ const config = result.config;
94
+
95
+ const assigned = new Map<string, { adapterId: string; root: string }>();
96
+
97
+ for (const entry of config.tools) {
98
+ if (entry.enabled === false) continue;
99
+ const root = deps.findRoot(cwd, entry.rootMarkers) ?? cwd;
100
+ let files: string[];
101
+ try {
102
+ files = await deps.listFiles(root);
103
+ } catch (err) {
104
+ deps.warn(`qa-guardrail: no se pudo barrer ${root} — ${String(err)}`);
105
+ continue;
106
+ }
107
+ for (const abs of files) {
108
+ if (assigned.has(abs)) continue;
109
+ if (isFileSelected(abs, entry, cwd)) {
110
+ assigned.set(abs, { adapterId: entry.id, root });
111
+ }
112
+ }
113
+ }
114
+
115
+ const findings: CheckFinding[] = [];
116
+
117
+ for (const [abs, info] of assigned) {
118
+ const adapter = deps.getAdapter(info.adapterId);
119
+ if (!adapter) {
120
+ deps.warn(`qa-guardrail: adaptador no registrado — ${info.adapterId}`);
121
+ continue;
122
+ }
123
+
124
+ let violations: Violation[] = [];
125
+ try {
126
+ violations = await adapter.lint(abs, { root: info.root, cwd });
127
+ } catch (err) {
128
+ deps.warn(`qa-guardrail: lint falló para ${abs} — ${String(err)}`);
129
+ }
130
+
131
+ const content = deps.readFile(abs) ?? "";
132
+ const disableDirectives = findEslintDisableDirectives(content);
133
+
134
+ if (violations.length > 0 || disableDirectives.length > 0) {
135
+ findings.push({
136
+ file: toPosix(relative(resolve(cwd), abs)),
137
+ adapterId: info.adapterId,
138
+ violations,
139
+ disableDirectives,
140
+ });
141
+ }
142
+ }
143
+
144
+ return { ok: true, findings, filesScanned: assigned.size };
145
+ }
146
+
147
+ /** Compact, human/agent-readable report for the check command. */
148
+ export function formatCheckReport(cwd: string, report: CheckReport): string {
149
+ if (!report.ok) {
150
+ return `qa-guardrail: check no disponible — ${report.reason ?? "config ausente"}`;
151
+ }
152
+ if (report.findings.length === 0) {
153
+ return `qa-guardrail: check — ${report.filesScanned} archivos barridos, sin violaciones ni directivas heredadas.`;
154
+ }
155
+
156
+ const header = `qa-guardrail: check — ${report.findings.length} archivo(s) con hallazgos (${report.filesScanned} barridos).`;
157
+ const lines: string[] = [header];
158
+ for (const finding of report.findings) {
159
+ lines.push(`\n${finding.file} (${finding.adapterId}):`);
160
+ for (const v of finding.violations) {
161
+ const rule = v.ruleId ?? "parse-error";
162
+ lines.push(`- ${rule} (${v.line}:${v.column}) ${v.message}`);
163
+ }
164
+ for (const directive of finding.disableDirectives) {
165
+ lines.push(`- directiva heredada: ${directive}`);
166
+ }
167
+ }
168
+ return lines.join("\n");
169
+ }
170
+
171
+ /** Real dependencies for the extension's own `/qa-guardrail check` command. */
172
+ export const defaultCheckDeps: CheckDeps = {
173
+ loadConfig,
174
+ getAdapter,
175
+ findRoot: (cwd, markers) => findProjectRoot(cwd, markers),
176
+ listFiles: async (root) => listFilesSync(root),
177
+ readFile: (path) => {
178
+ try {
179
+ return readFileSync(path, "utf8");
180
+ } catch {
181
+ return null;
182
+ }
183
+ },
184
+ warn: (message) => console.warn(message),
185
+ };