@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,189 @@
1
+ /**
2
+ * Loads, parses, and validates `.pi/qa-guardrail.json`.
3
+ *
4
+ * The config is selection-only: it declares which whitelisted adapter covers
5
+ * which files. It never declares binaries, args, or formatters (cerco).
6
+ *
7
+ * Contract: absent or invalid config → a warning and the gate off. Never a
8
+ * crash, never silence.
9
+ */
10
+
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import type { QaGuardrailConfig, ToolEntry } from "./types.js";
14
+
15
+ /** Config file path relative to the project root. */
16
+ export const CONFIG_FILE = ".pi/qa-guardrail.json";
17
+
18
+ export type ParseResult =
19
+ | { ok: true; config: QaGuardrailConfig }
20
+ | { ok: false; errors: string[] };
21
+
22
+ export type ConfigLoadResult =
23
+ | { ok: true; config: QaGuardrailConfig }
24
+ | { ok: false; reason: string };
25
+
26
+ /** Small I/O surface so tests can inject in-memory fakes. */
27
+ export interface ConfigIO {
28
+ exists(path: string): boolean;
29
+ read(path: string): string;
30
+ }
31
+
32
+ export const defaultConfigIO: ConfigIO = {
33
+ exists: (p) => existsSync(p),
34
+ read: (p) => readFileSync(p, "utf8"),
35
+ };
36
+
37
+ function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+
41
+ function errorMessage(err: unknown): string {
42
+ return err instanceof Error ? err.message : String(err);
43
+ }
44
+
45
+ /**
46
+ * Validate one tool entry. Returns the normalized entry and any errors for it.
47
+ */
48
+ function parseToolEntry(value: unknown, index: number): { entry: ToolEntry | null; errors: string[] } {
49
+ const errors: string[] = [];
50
+ const where = `tools[${index}]`;
51
+
52
+ if (!isRecord(value)) {
53
+ return { entry: null, errors: [`${where}: debe ser un objeto`] };
54
+ }
55
+
56
+ const entry: ToolEntry = {
57
+ id: "",
58
+ include: [],
59
+ exclude: [],
60
+ rootMarkers: [],
61
+ };
62
+
63
+ const id = value.id;
64
+ if (typeof id !== "string" || id.trim() === "") {
65
+ errors.push(`${where}.id: debe ser un string no vacío`);
66
+ } else {
67
+ entry.id = id;
68
+ }
69
+
70
+ for (const field of ["include", "exclude", "rootMarkers"] as const) {
71
+ const raw = value[field];
72
+ if (raw === undefined) continue;
73
+ if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string")) {
74
+ errors.push(`${where}.${field}: debe ser un array de strings`);
75
+ } else {
76
+ entry[field] = raw as string[];
77
+ }
78
+ }
79
+
80
+ if (value.enabled !== undefined && typeof value.enabled !== "boolean") {
81
+ errors.push(`${where}.enabled: debe ser un boolean`);
82
+ } else if (typeof value.enabled === "boolean") {
83
+ entry.enabled = value.enabled;
84
+ }
85
+
86
+ // Fail-fast: un campo desconocido (bin/args/formatter, o un typo como
87
+ // «includes») no se ignora en silencio; se rechaza el config entero.
88
+ const allowedFields = new Set(["id", "include", "exclude", "rootMarkers", "enabled"]);
89
+ for (const field of Object.keys(value)) {
90
+ if (!allowedFields.has(field)) {
91
+ errors.push(
92
+ `${where}.${field}: campo no permitido (la configuración es de solo selección: id, include, exclude, rootMarkers, enabled)`,
93
+ );
94
+ }
95
+ }
96
+
97
+ return errors.length === 0 ? { entry, errors } : { entry: null, errors };
98
+ }
99
+
100
+ /**
101
+ * Parse and validate the raw config text. Pure: no filesystem access.
102
+ */
103
+ export function parseConfig(raw: string): ParseResult {
104
+ let value: unknown;
105
+ try {
106
+ value = JSON.parse(raw);
107
+ } catch (err) {
108
+ return { ok: false, errors: [`JSON inválido: ${errorMessage(err)}`] };
109
+ }
110
+
111
+ if (!isRecord(value)) {
112
+ return { ok: false, errors: ["La raíz del config debe ser un objeto"] };
113
+ }
114
+
115
+ if (!Array.isArray(value.tools)) {
116
+ return { ok: false, errors: ["tools: debe ser un array"] };
117
+ }
118
+
119
+ const config: QaGuardrailConfig = { tools: [] };
120
+ const errors: string[] = [];
121
+
122
+ for (const field of Object.keys(value)) {
123
+ if (field !== "tools") {
124
+ errors.push(`campo no permitido en la raíz: ${field} (solo se admite «tools»)`);
125
+ }
126
+ }
127
+
128
+ for (let i = 0; i < value.tools.length; i++) {
129
+ const { entry, errors: entryErrors } = parseToolEntry(value.tools[i], i);
130
+ if (entry) {
131
+ config.tools.push(entry);
132
+ } else {
133
+ errors.push(...entryErrors);
134
+ }
135
+ }
136
+
137
+ if (errors.length > 0) {
138
+ return { ok: false, errors };
139
+ }
140
+ return { ok: true, config };
141
+ }
142
+
143
+ /**
144
+ * Load the config from `cwd`. Absent or invalid → `{ ok: false, reason }`.
145
+ */
146
+ export function loadConfig(cwd: string, io: ConfigIO = defaultConfigIO): ConfigLoadResult {
147
+ const path = join(cwd, CONFIG_FILE);
148
+
149
+ if (!io.exists(path)) {
150
+ return { ok: false, reason: `Config ausente: ${path}` };
151
+ }
152
+
153
+ let raw: string;
154
+ try {
155
+ raw = io.read(path);
156
+ } catch (err) {
157
+ return { ok: false, reason: `No se pudo leer ${path}: ${errorMessage(err)}` };
158
+ }
159
+
160
+ const parsed = parseConfig(raw);
161
+ if (!parsed.ok) {
162
+ return { ok: false, reason: `Config inválido (${path}): ${parsed.errors.join("; ")}` };
163
+ }
164
+ return { ok: true, config: parsed.config };
165
+ }
166
+
167
+ /**
168
+ * Walk up from `startDir` looking for the first directory that contains one of
169
+ * `markers`. Returns `null` when no ancestor matches (caller falls back to cwd).
170
+ */
171
+ export function findProjectRoot(
172
+ startDir: string,
173
+ markers: string[],
174
+ exists: (p: string) => boolean = existsSync,
175
+ ): string | null {
176
+ let dir = resolve(startDir);
177
+ for (;;) {
178
+ for (const marker of markers) {
179
+ if (exists(join(dir, marker))) {
180
+ return dir;
181
+ }
182
+ }
183
+ const parent = dirname(dir);
184
+ if (parent === dir) {
185
+ return null;
186
+ }
187
+ dir = parent;
188
+ }
189
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Delta-scoped feedback: `current − baseline`.
3
+ *
4
+ * The baseline is the set of violations the file had «as the agent found it»
5
+ * (see baseline.ts). The delta is the set of violations *introduced* by the
6
+ * agent's edit: everything in `current` that is not already accounted for in
7
+ * `baseline`. Removals are never reported — the agent only answers for what it
8
+ * adds, never for inherited violations it happened to fix.
9
+ */
10
+
11
+ import type { Violation } from "./types.js";
12
+
13
+ /**
14
+ * Canonical identity of a violation, used for set difference.
15
+ *
16
+ * A violation is identified by (rule, location). Message and severity are
17
+ * deliberately excluded: making an already-non-conforming function worse keeps
18
+ * the same location and is the documented structural blind spot of delta
19
+ * scoping (A-049). `parse-error` stands in for a missing rule id.
20
+ */
21
+ function violationKey(v: Violation): string {
22
+ const rule = v.ruleId ?? "parse-error";
23
+ return `${rule}\u0000${v.line}\u0000${v.column}`;
24
+ }
25
+
26
+ /**
27
+ * Compute the introduced violations as a multiset difference: each baseline
28
+ * violation cancels one matching current violation. Order follows `current`.
29
+ */
30
+ export function computeDelta(current: Violation[], baseline: Violation[]): Violation[] {
31
+ if (baseline.length === 0) {
32
+ return [...current];
33
+ }
34
+
35
+ const remaining = new Map<string, number>();
36
+ for (const item of baseline) {
37
+ const key = violationKey(item);
38
+ remaining.set(key, (remaining.get(key) ?? 0) + 1);
39
+ }
40
+
41
+ const delta: Violation[] = [];
42
+ for (const item of current) {
43
+ const key = violationKey(item);
44
+ const count = remaining.get(key) ?? 0;
45
+ if (count > 0) {
46
+ remaining.set(key, count - 1);
47
+ } else {
48
+ delta.push(item);
49
+ }
50
+ }
51
+ return delta;
52
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Compact, agent-facing feedback summary appended to a tool_result.
3
+ *
4
+ * The summary is the only thing the agent sees after an edit, so it must be
5
+ * short and scannable: one header line + one line per violation.
6
+ */
7
+
8
+ import type { Violation } from "./types.js";
9
+
10
+ /**
11
+ * Build the compact summary, or `null` when there is nothing to report.
12
+ */
13
+ export function buildSummary(file: string, violations: Violation[]): string | null {
14
+ if (violations.length === 0) {
15
+ return null;
16
+ }
17
+
18
+ const label = violations.length === 1 ? "violación" : "violaciones";
19
+ const header = `qa-guardrail: ${violations.length} ${label} en ${file}`;
20
+ const lines = violations.map((v) => {
21
+ const rule = v.ruleId ?? "parse-error";
22
+ const location = `${v.line}:${v.column}`;
23
+ return `- ${rule} (${location}) ${v.message}`;
24
+ });
25
+ return [header, ...lines].join("\n");
26
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * qa-guardrail extension — delta-scoping (#318) + corte en N (#319) + cerco (#320)
3
+ * + barrido completo `/qa-guardrail check` (#321).
4
+ *
5
+ * Wires config → adapter → tool_call/tool_result:
6
+ *
7
+ * - On `tool_call` of `write`/`edit`, the baseline for the file is frozen
8
+ * (linted before the first edit; empty for new files), and files that hit
9
+ * the N-failure cutoff are blocked.
10
+ * - On `tool_result` of `write`/`edit`, the file is linted again and only the
11
+ * delta (`current − baseline`) is appended as a compact summary, so the agent
12
+ * sees — and only answers for — the violations it introduces. Consecutive
13
+ * delta-fails are counted; the Nth failure blocks the file and appends a
14
+ * STOP report.
15
+ *
16
+ * The cerco (protected paths + eslint-disable detection) runs on `tool_call`
17
+ * before the baseline and blocks protected paths and new directives (#320).
18
+ *
19
+ * `/qa-guardrail check` runs a full, non-blocking sweep: it lists all matching
20
+ * files' violations and inherited directives (#321).
21
+ */
22
+
23
+ import { resolve } from "node:path";
24
+ import type {
25
+ ExtensionAPI,
26
+ ExtensionContext,
27
+ ToolCallEvent,
28
+ ToolCallEventResult,
29
+ ToolResultEvent,
30
+ ToolResultEventResult,
31
+ } from "@earendil-works/pi-coding-agent";
32
+ import { findProjectRoot, loadConfig, type ConfigLoadResult } from "./config.js";
33
+ import { buildSummary } from "./feedback.js";
34
+ import { selectAdapterId } from "./selection.js";
35
+ import { getAdapter, registerAdapter, setUnknownIdWarn } from "./adapters/registry.js";
36
+ import { createEslintAdapter } from "./adapters/eslint.js";
37
+ import { createBaselineStore, type BaselineStore } from "./baseline.js";
38
+ import { computeDelta } from "./delta.js";
39
+ import { createNStopStore, type NStopStore } from "./nstop.js";
40
+ import { checkCerco } from "./cerco.js";
41
+ import { defaultCheckDeps, formatCheckReport, runCheck } from "./check.js";
42
+ import type { LintAdapter, QaGuardrailConfig, Violation } from "./types.js";
43
+
44
+ export interface GuardrailDeps {
45
+ loadConfig: (cwd: string) => ConfigLoadResult;
46
+ selectAdapterId: (file: string, config: QaGuardrailConfig, cwd: string) => string | undefined;
47
+ getAdapter: (id: string) => LintAdapter | undefined;
48
+ findRoot: (cwd: string, markers: string[]) => string | null;
49
+ buildSummary: (file: string, violations: Violation[]) => string | null;
50
+ warn: (message: string) => void;
51
+ baseline: BaselineStore;
52
+ nstop: NStopStore;
53
+ computeDelta: (current: Violation[], baseline: Violation[]) => Violation[];
54
+ resolvePath: (cwd: string, file: string) => string;
55
+ }
56
+
57
+ const defaultDeps: Omit<GuardrailDeps, "baseline" | "nstop"> = {
58
+ loadConfig,
59
+ selectAdapterId,
60
+ getAdapter,
61
+ findRoot: (cwd, markers) => findProjectRoot(cwd, markers),
62
+ buildSummary,
63
+ warn: (message) => console.warn(message),
64
+ computeDelta,
65
+ resolvePath: (cwd, file) => resolve(cwd, file),
66
+ };
67
+
68
+ export interface Guardrail {
69
+ onToolCall: (
70
+ event: ToolCallEvent,
71
+ ctx: ExtensionContext,
72
+ ) => Promise<ToolCallEventResult | undefined>;
73
+ onToolResult: (
74
+ event: ToolResultEvent,
75
+ ctx: ExtensionContext,
76
+ ) => Promise<ToolResultEventResult | undefined>;
77
+ }
78
+
79
+ interface LintTarget {
80
+ adapter: LintAdapter;
81
+ root: string;
82
+ }
83
+
84
+ export function createGuardrail(deps: Partial<GuardrailDeps> = {}): Guardrail {
85
+ // One frozen baseline store and one n-stop store per guardrail instance
86
+ // (= per session).
87
+ const d: GuardrailDeps = {
88
+ baseline: createBaselineStore(),
89
+ nstop: createNStopStore(),
90
+ ...defaultDeps,
91
+ ...deps,
92
+ };
93
+
94
+ const configCache = new Map<string, ConfigLoadResult>();
95
+ const warnedKeys = new Set<string>();
96
+
97
+ function getConfigResult(cwd: string): ConfigLoadResult {
98
+ if (!configCache.has(cwd)) {
99
+ configCache.set(cwd, d.loadConfig(cwd));
100
+ }
101
+ return configCache.get(cwd)!;
102
+ }
103
+
104
+ function warnOnce(key: string, message: string, ctx: ExtensionContext): void {
105
+ if (warnedKeys.has(key)) {
106
+ return;
107
+ }
108
+ warnedKeys.add(key);
109
+ d.warn(message);
110
+ ctx.ui?.notify?.(message, "warning");
111
+ }
112
+
113
+ function filePath(eventInput: unknown): string | undefined {
114
+ const file = (eventInput as { path?: unknown }).path;
115
+ if (typeof file !== "string" || file.trim() === "") {
116
+ return undefined;
117
+ }
118
+ return file;
119
+ }
120
+
121
+ function resolveTarget(file: string, ctx: ExtensionContext): LintTarget | undefined {
122
+ const result = getConfigResult(ctx.cwd);
123
+ if (!result.ok) {
124
+ warnOnce(`config:${ctx.cwd}`, `qa-guardrail: gate off — ${result.reason}`, ctx);
125
+ return undefined;
126
+ }
127
+ const config = result.config;
128
+
129
+ const adapterId = d.selectAdapterId(file, config, ctx.cwd);
130
+ if (!adapterId) {
131
+ return undefined;
132
+ }
133
+
134
+ // Unknown ids are rejected by the registry (which warns); nothing to lint.
135
+ const adapter = d.getAdapter(adapterId);
136
+ if (!adapter) {
137
+ return undefined;
138
+ }
139
+
140
+ const entry = config.tools.find((tool) => tool.id === adapterId);
141
+ const root = d.findRoot(ctx.cwd, entry?.rootMarkers ?? []) ?? ctx.cwd;
142
+ return { adapter, root };
143
+ }
144
+
145
+ type LintOutcome = { ok: true; violations: Violation[] } | { ok: false };
146
+
147
+ async function lintFile(
148
+ target: LintTarget,
149
+ file: string,
150
+ ctx: ExtensionContext,
151
+ ): Promise<LintOutcome> {
152
+ try {
153
+ const violations = await target.adapter.lint(file, { root: target.root, cwd: ctx.cwd });
154
+ return { ok: true, violations };
155
+ } catch (err) {
156
+ const message = err instanceof Error ? err.message : String(err);
157
+ warnOnce(`lint:${file}`, `qa-guardrail: no se pudo lintear ${file} — ${message}`, ctx);
158
+ return { ok: false };
159
+ }
160
+ }
161
+
162
+ async function onToolCall(
163
+ event: ToolCallEvent,
164
+ ctx: ExtensionContext,
165
+ ): Promise<ToolCallEventResult | undefined> {
166
+ if (event.toolName !== "write" && event.toolName !== "edit") {
167
+ return undefined;
168
+ }
169
+ const file = filePath(event.input);
170
+ if (!file) {
171
+ return undefined;
172
+ }
173
+
174
+ const cerco = checkCerco(event.toolName, event.input, file, ctx.cwd);
175
+ if (cerco.blocked) {
176
+ return { block: true, reason: cerco.reason };
177
+ }
178
+
179
+ const absolutePath = d.resolvePath(ctx.cwd, file);
180
+ if (d.nstop.isBlocked(absolutePath)) {
181
+ return {
182
+ block: true,
183
+ reason: `qa-guardrail: ${file} está bloqueado tras el corte en N. Desbloquéalo con /qa-guardrail reset ${file}`,
184
+ };
185
+ }
186
+
187
+ const target = resolveTarget(file, ctx);
188
+ if (!target) {
189
+ return undefined;
190
+ }
191
+
192
+ await d.baseline.capture(absolutePath, async () => {
193
+ const outcome = await lintFile(target, file, ctx);
194
+ return outcome.ok ? outcome.violations : [];
195
+ });
196
+ return undefined;
197
+ }
198
+
199
+ async function onToolResult(
200
+ event: ToolResultEvent,
201
+ ctx: ExtensionContext,
202
+ ): Promise<ToolResultEventResult | undefined> {
203
+ if (event.toolName !== "write" && event.toolName !== "edit") {
204
+ return undefined;
205
+ }
206
+ if (event.isError) {
207
+ return undefined;
208
+ }
209
+
210
+ const file = filePath(event.input);
211
+ if (!file) {
212
+ return undefined;
213
+ }
214
+
215
+ const target = resolveTarget(file, ctx);
216
+ if (!target) {
217
+ return undefined;
218
+ }
219
+
220
+ const linted = await lintFile(target, file, ctx);
221
+ if (!linted.ok) {
222
+ return undefined;
223
+ }
224
+ const violations = linted.violations;
225
+
226
+ const absolutePath = d.resolvePath(ctx.cwd, file);
227
+ const baseline = d.baseline.get(absolutePath) ?? [];
228
+ const delta = d.computeDelta(violations, baseline);
229
+
230
+ // Delta limpio: el agente no introdujo violaciones → reinicia el contador.
231
+ if (delta.length === 0) {
232
+ d.nstop.recordClean(absolutePath);
233
+ return undefined;
234
+ }
235
+
236
+ const outcome = d.nstop.recordFailure(absolutePath);
237
+
238
+ const summary = d.buildSummary(file, delta);
239
+ const appended: Array<{ type: "text"; text: string }> = [];
240
+ if (summary) {
241
+ appended.push({ type: "text", text: summary });
242
+ }
243
+
244
+ if (outcome.blocked) {
245
+ const stop = `qa-guardrail: STOP — ${outcome.failures} deltas-fallo consecutivos en ${file}; reportar a la persona.`;
246
+ appended.push({ type: "text", text: stop });
247
+ ctx.ui?.notify?.(
248
+ `qa-guardrail: ${file} bloqueado tras ${outcome.failures} deltas-fallo consecutivos — reportar a la persona.`,
249
+ "error",
250
+ );
251
+ }
252
+
253
+ return {
254
+ content: [...event.content, ...appended],
255
+ };
256
+ }
257
+
258
+ return { onToolCall, onToolResult };
259
+ }
260
+
261
+ export default function (pi: ExtensionAPI) {
262
+ registerAdapter(createEslintAdapter());
263
+ setUnknownIdWarn((message) => console.warn(message));
264
+
265
+ const nstop = createNStopStore();
266
+ const { onToolCall, onToolResult } = createGuardrail({ nstop });
267
+ pi.on("tool_call", onToolCall);
268
+ pi.on("tool_result", onToolResult);
269
+
270
+ pi.registerCommand("qa-guardrail", {
271
+ description:
272
+ "qa-guardrail: reset <archivo> desbloquea un archivo; check barre violaciones y directivas heredadas",
273
+ handler: async (args, ctx) => {
274
+ const normalized = args.trim();
275
+
276
+ const resetMatch = normalized.match(/^reset\s+(\S+)\s*$/);
277
+ if (resetMatch) {
278
+ const file = resetMatch[1];
279
+ const absolutePath = resolve(ctx.cwd, file);
280
+ nstop.reset(absolutePath);
281
+ ctx.ui?.notify?.(`qa-guardrail: ${file} desbloqueado`, "info");
282
+ return;
283
+ }
284
+
285
+ if (normalized === "check") {
286
+ const report = await runCheck(ctx.cwd, {
287
+ ...defaultCheckDeps,
288
+ warn: (message) => ctx.ui?.notify?.(message, "warning"),
289
+ });
290
+ const text = formatCheckReport(ctx.cwd, report);
291
+ ctx.ui?.notify?.(
292
+ report.ok
293
+ ? `qa-guardrail: check — ${report.findings.length} hallazgos`
294
+ : "qa-guardrail: check no disponible",
295
+ report.ok ? "info" : "warning",
296
+ );
297
+ pi.sendMessage({
298
+ customType: "qa-guardrail-check",
299
+ content: text,
300
+ display: true,
301
+ });
302
+ return;
303
+ }
304
+
305
+ ctx.ui?.notify?.(
306
+ "qa-guardrail: uso — /qa-guardrail reset <archivo> | /qa-guardrail check",
307
+ "warning",
308
+ );
309
+ },
310
+ });
311
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * N-failure cutoff for qa-guardrail (corte en N).
3
+ *
4
+ * Prevents infinite correction loops: every *consecutive* delta-fail on the
5
+ * same file increments a counter. A clean delta (no introduced violations)
6
+ * resets it. When the counter reaches `N`, the file is added to a blocklist
7
+ * that blocks future `write`/`edit` calls until a human runs
8
+ * `/qa-guardrail reset <archivo>`.
9
+ *
10
+ * The counter and blocklist are keyed by the *absolute* file path, matching
11
+ * the path used by `index.ts` (and the rest of the extension) for baseline and
12
+ * linting. Pure module: no filesystem access.
13
+ */
14
+
15
+ /** Hard cutoff. Fixed by A-045/A-049: three consecutive delta-fails. */
16
+ export const DEFAULT_MAX_FAILURES = 3;
17
+
18
+ /** Result of recording a failing delta. */
19
+ export interface NStopResult {
20
+ /** Consecutive delta-fail count for the file *after* this failure. */
21
+ failures: number;
22
+ /** True when this failure reached the cutoff and the file is now blocked. */
23
+ blocked: boolean;
24
+ }
25
+
26
+ export interface NStopStore {
27
+ /**
28
+ * Record one delta-fail for `file` and return the new count plus whether
29
+ * the cutoff was just reached (which adds the file to the blocklist).
30
+ */
31
+ recordFailure(file: string): NStopResult;
32
+ /** Record a clean delta: resets the consecutive-fail counter to zero. */
33
+ recordClean(file: string): void;
34
+ /** Whether `file` is in the blocklist (write/edit must be blocked). */
35
+ isBlocked(file: string): boolean;
36
+ /** Current consecutive delta-fail count for `file` (0 if none). */
37
+ failures(file: string): number;
38
+ /** Human unblock: remove from blocklist and reset the counter. */
39
+ reset(file: string): void;
40
+ /** Forget every counter and blocklist entry (tests / teardown). */
41
+ clear(): void;
42
+ }
43
+
44
+ export function createNStopStore(maxFailures: number = DEFAULT_MAX_FAILURES): NStopStore {
45
+ const cutoff = Math.max(1, Math.floor(maxFailures));
46
+ const counters = new Map<string, number>();
47
+ const blocklist = new Set<string>();
48
+
49
+ return {
50
+ recordFailure(file) {
51
+ const next = (counters.get(file) ?? 0) + 1;
52
+ counters.set(file, next);
53
+ const blocked = next >= cutoff;
54
+ if (blocked) {
55
+ blocklist.add(file);
56
+ }
57
+ return { failures: next, blocked };
58
+ },
59
+
60
+ recordClean(file) {
61
+ counters.set(file, 0);
62
+ },
63
+
64
+ isBlocked(file) {
65
+ return blocklist.has(file);
66
+ },
67
+
68
+ failures(file) {
69
+ return counters.get(file) ?? 0;
70
+ },
71
+
72
+ reset(file) {
73
+ counters.delete(file);
74
+ blocklist.delete(file);
75
+ },
76
+
77
+ clear() {
78
+ counters.clear();
79
+ blocklist.clear();
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "qa-guardrail-extension",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "vitest run",
7
+ "test:watch": "vitest"
8
+ },
9
+ "devDependencies": {
10
+ "vitest": "^3.1.1"
11
+ }
12
+ }