@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
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@imolko/ultra-agent",
3
+ "version": "0.1.0-beta",
4
+ "description": "Metodología ultra como paquete versionado: skills canónicos, registro ADR A/T/P, extensiones de pi y CLI (install/update/status/ship).",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Roberto Matute",
8
+ "email": "robertomatute@imolko.com"
9
+ },
10
+ "keywords": [
11
+ "pi-package",
12
+ "agent-skills",
13
+ "adr",
14
+ "cli",
15
+ "methodology"
16
+ ],
17
+ "homepage": "https://gitlab.com/imolko/ultra-stack/ultra-agent",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://gitlab.com/imolko/ultra-stack/ultra-agent"
21
+ },
22
+ "bin": {
23
+ "ultra-agent": "./bin/cli.js"
24
+ },
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist-node/index.d.ts",
31
+ "default": "./dist-node/index.js"
32
+ },
33
+ "./eslint": {
34
+ "default": "./eslint/index.mjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "main": "./dist-node/index.js",
39
+ "types": "./dist-node/index.d.ts",
40
+ "pi": {
41
+ "skills": [
42
+ "./skills"
43
+ ],
44
+ "extensions": [
45
+ "./pi/extensions"
46
+ ]
47
+ },
48
+ "scripts": {
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "clean": "rm -rf dist-node",
53
+ "build": "npm run clean && tsc -p tsconfig.json && bash scripts/copy-assets.sh",
54
+ "gate": "python3 scripts/verify-adr-consistency.py"
55
+ },
56
+ "files": [
57
+ "bin",
58
+ "dist-node",
59
+ "eslint",
60
+ "!eslint/__fixtures__/**",
61
+ "skills",
62
+ "docs/adr",
63
+ "pi/extensions",
64
+ "scripts",
65
+ "README.md",
66
+ "!**/*.test.ts",
67
+ "!**/*.integration.test.ts",
68
+ "!**/*.spec.ts",
69
+ "!**/package-lock.json",
70
+ "!**/vitest.config.ts",
71
+ "!**/node_modules/**",
72
+ "!**/.DS_Store"
73
+ ],
74
+ "publishConfig": {
75
+ "access": "public"
76
+ },
77
+ "peerDependencies": {
78
+ "@earendil-works/pi-coding-agent": "*"
79
+ },
80
+ "dependencies": {
81
+ "@typescript-eslint/eslint-plugin": "^8.71.0",
82
+ "@typescript-eslint/parser": "^8.71.0",
83
+ "eslint": "^9.0.0",
84
+ "eslint-import-resolver-typescript": "^4.4.5",
85
+ "eslint-plugin-boundaries": "^7.2.0",
86
+ "eslint-plugin-import-x": "^4.17.1"
87
+ },
88
+ "devDependencies": {
89
+ "@imolko/ultra-tsconfig": "1.1.0",
90
+ "@types/node": "^22.10.0",
91
+ "typescript": "^6.0.3",
92
+ "vitest": "^3.1.1"
93
+ }
94
+ }
@@ -0,0 +1,92 @@
1
+ # Handoff Extension
2
+
3
+ Generates a context handoff document, renames the current session, and spins up a new versioned session seeded with the handoff and task for a fresh agent to continue the work.
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ /handoff now implement this for teams as well
9
+ /handoff execute phase one of the plan
10
+ /handoff check other places that need this fix
11
+ ```
12
+
13
+ The argument is the **task** the next session should focus on. It becomes the first user message in the new session, preceded by the LLM-generated handoff note.
14
+
15
+ ## What it does
16
+
17
+ 1. **Names the current session** — if unnamed, prompts for a slug (defaults from the task text).
18
+ 2. **Injects a handoff prompt** — sends the bundled `handoff-prompt.txt` + task to the LLM.
19
+ 3. **Captures the LLM response** — polls for an assistant message with `stopReason: "stop"`. Times out after 5 minutes.
20
+ 4. **Creates a new session** — version-bumped name (e.g. `fix-auth-v2` → `fix-auth-v3`), parented to the current session.
21
+ 5. **Seeds the new session** — injects the handoff note followed by the task. The fresh session is ready for a new agent.
22
+
23
+ ## Architecture
24
+
25
+ ### Extension replaces the skill
26
+
27
+ The handoff **extension** (this directory) replaces the former handoff skill (`.pi/skills/handoff/SKILL.md`, now removed). The extension owns the whole flow:
28
+
29
+ | Artifact | Purpose |
30
+ |----------|---------|
31
+ | `handoff-prompt.txt` (extension) | Tells the LLM to output the handoff **directly in the response** so the extension can capture it programmatically. |
32
+
33
+ The extension reads its own bundled prompt; there is no skill file anymore.
34
+
35
+ ### Polling mechanism
36
+
37
+ After injecting the handoff prompt at index `startIndex`, the extension polls `ctx.sessionManager.getBranch()` every 25ms. It uses **positional matching** — it waits for the first assistant message after `startIndex`, regardless of text content. This eliminates coupling to the prompt text and avoids silent breakage if whitespace or wording changes.
38
+
39
+ ### Session replacement safety
40
+
41
+ After `ctx.newSession()`, the old session runtime is torn down. The `withSession` callback uses only the **replacement `ctx`** parameter — it never references the original command `ctx`, whose `.ui` object is stale after shutdown and will throw.
42
+
43
+ ## Files
44
+
45
+ | File | Purpose |
46
+ |------|---------|
47
+ | `index.ts` | Extension entry point. Registers `/handoff` command. |
48
+ | `handoff-prompt.txt` | Prompt template injected into the LLM to generate the handoff note. |
49
+ | `index.test.ts` | Unit tests (83) covering all exported helpers. |
50
+ | `index.integration.test.ts` | Integration tests (15) with mocked `ExtensionAPI`. |
51
+ | `package.json` | Vitest dev dependency + test scripts. |
52
+ | `vitest.config.ts` | Vitest configuration. |
53
+
54
+ ## Exported helpers
55
+
56
+ The extension exports utility functions so they can be imported and tested:
57
+
58
+ | Export | Description |
59
+ |--------|-------------|
60
+ | `isRecord(value)` | Type guard for plain objects. |
61
+ | `getRoleMessage(entry, role)` | Extracts a `user` or `assistant` message from a session entry. |
62
+ | `getAssistantMessage(entry)` | Extracts an assistant message (with optional `stopReason`). |
63
+ | `getMessageText(entry)` | Gets text content from string or content-array messages. |
64
+ | `findUserMessageIndex(entries, from, text)` | Finds a user message by exact text match (legacy; positional matching is used at runtime). |
65
+ | `hasAssistantAfterIndex(entries, from)` | Returns `true` if any assistant message exists after a given index. |
66
+ | `getAssistantText(entries, from)` | Gets the text of the first assistant message with `stopReason: "stop"` after `from`. |
67
+ | `stripFrontmatter(text)` | Strips YAML frontmatter (`---` … `---`). |
68
+ | `stripSaveInstruction(text)` | Strips "Save to the temp directory…" instructions from text. |
69
+ | `slugify(text)` | Converts text to a URL-safe slug. |
70
+ | `bumpVersion(name)` | Increments a `-vN` suffix or appends `-v2`. |
71
+ | `loadPrompt(extensionDir)` | Reads `handoff-prompt.txt` from the extension directory. |
72
+
73
+ ## Testing
74
+
75
+ ```bash
76
+ cd .pi/extensions/handoff
77
+ npm test # Run all tests once
78
+ npm run test:watch # Watch mode
79
+ ```
80
+
81
+ - **Unit tests** (`index.test.ts`): 83 tests covering every exported helper and internal logic — input validation, edge cases, empty/malformed entries, frontmatter stripping, version bumping, slug generation, positional matching, text extraction with `stopReason` filtering.
82
+ - **Integration tests** (`index.integration.test.ts`): 15 tests with a mocked `ExtensionAPI` — missing model/task errors, session naming (prompt/cancel/fallback), full happy path, cancellation, timeout, truncated/empty responses, stale ctx prevention.
83
+
84
+ ## Development
85
+
86
+ When adding features, follow the red-green-refactor loop:
87
+
88
+ ```bash
89
+ cd .pi/extensions/handoff && npm run test:watch
90
+ ```
91
+
92
+ The extension uses pi's `ExtensionCommandContext` API — see [extension docs](https://github.com/earendil-works/pi-coding-agent/blob/main/docs/extensions.md) for the full API surface.
@@ -0,0 +1,9 @@
1
+ Write a handoff document summarising the current conversation so a fresh agent can continue the work. Output the handoff directly in your response — do not save to a file.
2
+
3
+ Include a "Suggested skills" section in the document, which suggests skills that the agent should invoke.
4
+
5
+ Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. For GitLab issues, use the full web URL (e.g., `https://gitlab.com/imolko/cortex/-/work_items/1`).
6
+
7
+ Redact any sensitive information, such as API keys, passwords, tokens, or personally identifiable information.
8
+
9
+ If the user passed arguments, treat them as a description of what the next session will focus on and tailor the document accordingly.
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Handoff extension — generate context handoff and move it to a new session.
3
+ *
4
+ * Usage:
5
+ * /handoff now implement this for teams as well
6
+ * /handoff execute phase one of the plan
7
+ * /handoff check other places that need this fix
8
+ */
9
+
10
+ import type { ExtensionAPI, ExtensionCommandContext, SessionEntry } from "@earendil-works/pi-coding-agent";
11
+ import { readFileSync, existsSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+
14
+ // ── helpers ──────────────────────────────────────────────────────────────────
15
+
16
+ export function isRecord(value: unknown): value is Record<string, unknown> {
17
+ return typeof value === "object" && value !== null;
18
+ }
19
+
20
+ type RoleMessage = {
21
+ role: "user" | "assistant";
22
+ content: unknown;
23
+ };
24
+
25
+ type AssistantRoleMessage = RoleMessage & {
26
+ role: "assistant";
27
+ stopReason?: unknown;
28
+ };
29
+
30
+ const HANDOFF_TIMEOUT_MS = 5 * 60 * 1000;
31
+ const HANDOFF_POLL_INTERVAL_MS = 25;
32
+
33
+ export function getRoleMessage(entry: SessionEntry, role: "user" | "assistant"): RoleMessage | null {
34
+ if (entry.type !== "message" || !isRecord(entry.message)) return null;
35
+ const message = entry.message;
36
+ if (message.role !== role || !("content" in message)) return null;
37
+ return message as RoleMessage;
38
+ }
39
+
40
+ export function getAssistantMessage(entry: SessionEntry): AssistantRoleMessage | null {
41
+ const message = getRoleMessage(entry, "assistant");
42
+ if (!message || !isRecord(message)) return null;
43
+ return message as AssistantRoleMessage;
44
+ }
45
+
46
+ export function getMessageText(entry: SessionEntry): string {
47
+ if (entry.type !== "message" || !isRecord(entry.message) || !("content" in entry.message)) return "";
48
+ const content = entry.message.content;
49
+ if (typeof content === "string") return content.trim();
50
+ if (!Array.isArray(content)) return "";
51
+ return content
52
+ .filter((block): block is { type: "text"; text: string } => isRecord(block) && block.type === "text" && typeof block.text === "string")
53
+ .map((block) => block.text)
54
+ .join("\n")
55
+ .trim();
56
+ }
57
+
58
+ export function findUserMessageIndex(entries: SessionEntry[], fromIndex: number, text: string): number {
59
+ for (let i = entries.length - 1; i >= fromIndex; i--) {
60
+ const entry = entries[i];
61
+ if (!getRoleMessage(entry, "user")) continue;
62
+ if (getMessageText(entry) === text) return i;
63
+ }
64
+ return -1;
65
+ }
66
+
67
+ export function hasAssistantAfterIndex(entries: SessionEntry[], fromIndex: number): boolean {
68
+ for (let i = entries.length - 1; i > fromIndex; i--) {
69
+ if (getRoleMessage(entries[i], "assistant")) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ type HandoffTurn = {
75
+ branch: SessionEntry[];
76
+ handoffUserIndex: number;
77
+ };
78
+
79
+ async function waitForHandoffTurn(
80
+ ctx: ExtensionCommandContext,
81
+ startIndex: number,
82
+ timeoutMs = HANDOFF_TIMEOUT_MS,
83
+ ): Promise<HandoffTurn | null> {
84
+ const deadline = Date.now() + timeoutMs;
85
+ while (Date.now() < deadline) {
86
+ const branch = ctx.sessionManager.getBranch();
87
+ // Positional match: we injected at startIndex; wait for the first
88
+ // assistant response past that point. No text matching needed.
89
+ if (hasAssistantAfterIndex(branch, startIndex) && ctx.isIdle()) {
90
+ return { branch, handoffUserIndex: startIndex };
91
+ }
92
+ await new Promise<void>((resolve) => setTimeout(resolve, HANDOFF_POLL_INTERVAL_MS));
93
+ }
94
+ return null;
95
+ }
96
+
97
+ export function getAssistantText(entries: SessionEntry[], fromIndex: number): string | null {
98
+ for (let i = entries.length - 1; i >= fromIndex; i--) {
99
+ const message = getAssistantMessage(entries[i]);
100
+ if (!message) continue;
101
+ if (message.stopReason !== "stop") return null;
102
+ const text = getMessageText(entries[i]);
103
+ if (text.length > 0) return text;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ // ── prompt loading ───────────────────────────────────────────────────────────
109
+
110
+ export function stripFrontmatter(text: string): string {
111
+ const lines = text.split("\n");
112
+ if (lines[0]?.trim() === "---") {
113
+ const end = lines.indexOf("---", 1);
114
+ if (end !== -1) {
115
+ return lines.slice(end + 1).join("\n").trim();
116
+ }
117
+ }
118
+ return text.trim();
119
+ }
120
+
121
+ export function stripSaveInstruction(text: string): string {
122
+ // Remove file-saving instructions like:
123
+ // "Save to the temporary directory of the user's OS — not the current workspace."
124
+ // "Save it to the temp directory of the user's OS."
125
+ return text
126
+ .replace(
127
+ /\bSave\s+(it\s+|the\s+document\s+)?to\s+the\s+(temporary|temp)\s+directory\s+of\s+the\s+user'?s\s+OS\s*(—[^.]+)?\.?\s*/gi,
128
+ "",
129
+ )
130
+ .trim();
131
+ }
132
+
133
+ export function loadPrompt(extensionDir: string): string {
134
+ // Use the extension's own prompt, not the skill's SKILL.md.
135
+ // The skill tells the agent to save to a file — that's correct for
136
+ // general skill invocation, but wrong for the extension which needs
137
+ // the LLM to output text directly so it can capture the response.
138
+ const promptPath = join(extensionDir, "handoff-prompt.txt");
139
+ if (existsSync(promptPath)) {
140
+ return readFileSync(promptPath, "utf8").trim();
141
+ }
142
+ return "";
143
+ }
144
+
145
+ // ── naming ───────────────────────────────────────────────────────────────────
146
+
147
+ export function slugify(text: string): string {
148
+ return text
149
+ .toLowerCase()
150
+ .replace(/[^a-z0-9]+/g, "-")
151
+ .replace(/^-|-$/g, "")
152
+ .slice(0, 40);
153
+ }
154
+
155
+ export function bumpVersion(name: string): string {
156
+ const match = name.match(/^(.*?)-v(\d+)$/);
157
+ if (match) {
158
+ const base = match[1];
159
+ const num = parseInt(match[2], 10);
160
+ return `${base}-v${num + 1}`;
161
+ }
162
+ return `${name}-v2`;
163
+ }
164
+
165
+ // ── extension ────────────────────────────────────────────────────────────────
166
+
167
+ export default function (pi: ExtensionAPI) {
168
+ // Resolve the extension's own directory for reading bundled files
169
+ const extensionDir = dirname(new URL(import.meta.url).pathname);
170
+
171
+ pi.registerCommand("handoff", {
172
+ description: "Transfer context to a new focused session",
173
+ handler: async (args, ctx) => {
174
+ const notify = (message: string, level: "info" | "warning" | "error") => {
175
+ ctx.ui?.notify?.(message, level);
176
+ };
177
+
178
+ const setStatus = (text: string) => {
179
+ ctx.ui?.setStatus?.("handoff", text);
180
+ };
181
+
182
+ const clearStatus = () => {
183
+ ctx.ui?.setStatus?.("handoff", "");
184
+ };
185
+
186
+ if (!ctx.model) {
187
+ notify("No model selected", "error");
188
+ return;
189
+ }
190
+
191
+ const task = args.trim();
192
+ if (!task) {
193
+ notify("Usage: /handoff <goal or task for new thread>", "error");
194
+ return;
195
+ }
196
+
197
+ // ── 1. Name the current session if unnamed ──────────────────────────
198
+ const currentName = ctx.sessionManager.getSessionName();
199
+ let baseName: string;
200
+
201
+ if (!currentName) {
202
+ if (ctx.ui?.input) {
203
+ const suggestion = slugify(task) || "session";
204
+ const input = await ctx.ui.input(
205
+ "Name this session (for handoff)",
206
+ suggestion,
207
+ );
208
+ if (!input) {
209
+ notify("Handoff cancelled", "warning");
210
+ return;
211
+ }
212
+ baseName = input.trim() || suggestion;
213
+ } else {
214
+ baseName = slugify(task) || "session";
215
+ }
216
+ // Set the name on the current session
217
+ ctx.sessionManager.appendSessionInfo(baseName);
218
+ } else {
219
+ baseName = currentName;
220
+ }
221
+
222
+ const newSessionName = bumpVersion(baseName);
223
+ setStatus("Handoff: generating note…");
224
+
225
+ // ── 2. Load prompt ─────────────────────────────────────────────────
226
+ const promptBody = loadPrompt(extensionDir);
227
+ if (!promptBody) {
228
+ clearStatus();
229
+ notify("Handoff prompt not found", "error");
230
+ return;
231
+ }
232
+
233
+ const handoffRequest = `${promptBody}\n\nTask for the next agent:\n${task}`;
234
+
235
+ // ── 3. Inject handoff-generation prompt ────────────────────────────
236
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
237
+ const startIndex = ctx.sessionManager.getBranch().length;
238
+
239
+ if (ctx.isIdle()) {
240
+ pi.sendUserMessage(handoffRequest);
241
+ } else {
242
+ pi.sendUserMessage(handoffRequest, { deliverAs: "followUp" });
243
+ }
244
+
245
+ notify("Handoff started — generating note…", "info");
246
+
247
+ // ── 4. Poll for LLM response ───────────────────────────────────────
248
+ const handoffTurn = await waitForHandoffTurn(ctx, startIndex);
249
+
250
+ if (!handoffTurn) {
251
+ clearStatus();
252
+ notify("Handoff timed out after 5 minutes", "error");
253
+ return;
254
+ }
255
+
256
+ const handoffNote = getAssistantText(handoffTurn.branch, handoffTurn.handoffUserIndex + 1);
257
+
258
+ if (!handoffNote) {
259
+ clearStatus();
260
+ notify("Handoff failed — empty or truncated response", "error");
261
+ return;
262
+ }
263
+
264
+ setStatus("Handoff: creating session…");
265
+ notify("Handoff captured — creating new session…", "info");
266
+
267
+ // ── 5. Create new session ──────────────────────────────────────────
268
+ const promptForNewSession = `${handoffNote}\n\n---\n\n## Task\n${task}`;
269
+
270
+ const newSessionResult = await ctx.newSession({
271
+ parentSession: currentSessionFile ?? undefined,
272
+ setup: (sm) => {
273
+ sm.appendSessionInfo(newSessionName);
274
+ },
275
+ withSession: async (newSessionCtx) => {
276
+ // Use newSessionCtx, not the captured old ctx — the old session
277
+ // has been shut down and its ctx.ui is stale (will throw).
278
+ newSessionCtx.ui?.setStatus?.("handoff", "");
279
+
280
+ if (newSessionCtx.isIdle()) {
281
+ await newSessionCtx.sendUserMessage(promptForNewSession);
282
+ } else {
283
+ await newSessionCtx.sendUserMessage(promptForNewSession, { deliverAs: "followUp" });
284
+ }
285
+
286
+ newSessionCtx.ui?.notify?.(`Handoff complete — session: ${newSessionName}`, "info");
287
+ },
288
+ });
289
+
290
+ if (newSessionResult.cancelled) {
291
+ clearStatus();
292
+ notify("Handoff cancelled", "warning");
293
+ }
294
+ },
295
+ });
296
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "handoff-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
+ }
@@ -0,0 +1,108 @@
1
+ # qa-guardrail Extension
2
+
3
+ Materializa el bucle de linting de [A-045](../../../docs/adr/foundational/A-045-guardrails-aplicables-no-directrices.md) para el agente pi, según el diseño de [A-049](../../../docs/adr/foundational/A-049-materializacion-bucle-guardrails-extension-nativa-pi.md). Este directorio contiene el **slice tracer bullet** (#317), el **delta-scoping** (#318), el **corte en N** (#319), el **cerco** (#320) y el **barrido completo** (#321): el camino completo `config → adaptador → hook → retroalimentación`, limitado a «solo mis cambios», el bloqueo tras N deltas-fallo consecutivos sobre el mismo archivo, y el cerco que impide editar la configuración o desactivar reglas en línea.
4
+
5
+ ## Qué hace hoy
6
+
7
+ Tras cada `write`/`edit` exitoso, el hook `tool_result`:
8
+
9
+ 1. Carga y valida `.pi/qa-guardrail.json` (ausente/inválido → warning y gate off, nunca crash).
10
+ 2. Selecciona el adaptador whitelisted que cubre el archivo (`include`/`exclude`).
11
+ 3. Lintea el archivo con la API Node de ESLint (in-process, instancia única, import perezoso).
12
+ 4. Reporta **solo el delta** contra la línea base congelada (ver más abajo).
13
+
14
+ Además, el hook `tool_call` congela la línea base del archivo *antes* de la primera edición; los archivos nuevos parten de línea base vacía. Y si el agente introduce violaciones en N=3 ediciones consecutivas sobre el mismo archivo (sin una edición limpia entre medias), ese archivo se bloquea para `write`/`edit` hasta que la persona lo desbloquee con `/qa-guardrail reset <archivo>`.
15
+
16
+ Antes de todo eso, el **cerco** bloquea en `tool_call`: los caminos protegidos hardcodeados (`eslint.config.mjs`, `.pi/qa-guardrail.json`, `.pi/extensions/qa-guardrail/**`) y las directivas nuevas `eslint-disable*` en `write.content` / `edit.newText`. El cerco es independiente de ESLint y de la selección: se aplica aunque el config esté ausente o el archivo no esté cubierto por ningún adaptador.
17
+ Por conservadurismo, `checkCerco` bloquea cualquier directiva presente en `edit.newText` aunque ya estuviera en `oldText` (mover una directiva heredada se bloquea como «nueva»): posible falso positivo, coherente con «el cerco prohíbe desactivar reglas en línea».
18
+
19
+ **Límite del cerco**: solo cerca las herramientas `write`/`edit`. El agente aún podría tocar `eslint.config.mjs` o inyectar `eslint-disable*` vía `bash` (p. ej. `sed`/`cat`); ese hueco es una limitación estructural de A-045 heredada del alcance del PRD (el cerco opera sobre `tool_call` de write/edit), no una desviación del spec.
20
+
21
+ Sin violaciones nuevas, no se anexa nada.
22
+ Además, `/qa-guardrail check` hace un **barrido completo** (no delta-scoped) de los archivos que coinciden con la configuración: lista las violaciones y las directivas `eslint-disable*` heredadas, sin bloquear nada. Sirve de inventario de migración.
23
+
24
+ ### Delta-scoping
25
+
26
+ - `baseline.ts`: línea base congelada por archivo y sesión, capturada en `tool_call` (lint del archivo tal como el agente lo encontró; vacía para archivos nuevos).
27
+ - `delta.ts`: `current − baseline` como diferencia de multiconjuntos; solo se reportan las violaciones introducidas. Las heredadas nunca se atribuyen al agente.
28
+ - La identidad delta es `(rule, line, column)`. Si un edit desplaza líneas por encima de una violación heredada, su `line` cambia y puede reportarse como introducida (falso delta-fallo que suma al corte N). Es el punto ciego inverso al documentado en A-049 (agravar sin reportar).
29
+ - `nstop.ts`: contador consecutivo de deltas-fallo por archivo (N=3). Una edición con delta limpio reinicia el contador; al tercer fallo consecutivo el archivo entra en la blocklist y `tool_call` bloquea `write`/`edit`. Solo `/qa-guardrail reset <archivo>` (desbloqueo humano) lo saca.
30
+
31
+ ## Qué NO hace todavía
32
+
33
+ - Adaptadores `ruff`/`pylint` concretos (quedan el registro whitelisted y la base CLI preparados), formatter/autofix, y el job de lint en CI/`ship.ts`.
34
+
35
+ ## Configuración
36
+
37
+ `.pi/qa-guardrail.json` (versionado en la raíz del repo) es de **solo selección**: declara qué adaptador cubre qué archivos, nunca binarios, args ni formatter (cerco autosellante):
38
+
39
+ ```json
40
+ {
41
+ "tools": [
42
+ {
43
+ "id": "eslint",
44
+ "include": ["**/*.{ts,tsx,js,mjs,cjs}"],
45
+ "exclude": [],
46
+ "rootMarkers": ["package.json"],
47
+ "enabled": true
48
+ }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ Esquema: `tools: [{ id, include, exclude, rootMarkers, enabled? }]`. `include` vacío no cubre nada (fail-closed, igual que ESLint flat config). `exclude` gana sobre `include`. `rootMarkers` ubica la raíz del proyecto (donde vive `eslint.config.mjs`).
54
+
55
+ ## Comandos
56
+
57
+ - `/qa-guardrail reset <archivo>` — desbloqueo humano tras el corte en N.
58
+ - `/qa-guardrail check` — barrido completo: lista violaciones y directivas heredadas (sin bloquear).
59
+
60
+ ## Arquitectura
61
+
62
+ | Módulo | Responsabilidad |
63
+ |--------|-----------------|
64
+ | `config.ts` | Carga, parsea y valida `.pi/qa-guardrail.json`; ausente/inválido → warning y gate off. También `findProjectRoot`. |
65
+ | `selection.ts` | Mapea un archivo al id del adaptador que lo cubre (globs `include`/`exclude`). |
66
+ | `adapters/registry.ts` | Registro whitelisted `id → adaptador`. Ids desconocidos se rechazan con warning. |
67
+ | `adapters/eslint.ts` | `lint(file, {root,cwd}) → Violation[]` vía API Node de ESLint. Import dinámico perezoso, instancia única; lanza cuando eslint falta o su config está rota (el runner avisa y omite el feedback). |
68
+ | `adapters/cli.ts` | Base para adaptadores CLI futuros: resolución perezosa del binario; lanza cuando el binario falta o el run/parse falla (el runner avisa y omite el feedback). |
69
+ | `feedback.ts` | Construye el resumen compacto (o `null` si no hay violaciones). |
70
+ | `baseline.ts` | Línea base congelada por archivo y sesión (capturada en `tool_call`; vacía para archivos nuevos). |
71
+ | `delta.ts` | `current − baseline` como diferencia de multiconjuntos. |
72
+ | `nstop.ts` | Contador consecutivo de deltas-fallo (N=3), blocklist y desbloqueo humano. |
73
+ | `cerco.ts` | Lista protegida hardcodeada y detección de directivas `eslint-disable*` (bloqueo en `tool_call`). |
74
+ | `check.ts` | Barrido completo `/qa-guardrail check`: enumera archivos coincidentes, lista violaciones y directivas heredadas. |
75
+ | `index.ts` | Cablea `tool_call` (cerco → línea base + bloqueo) y `tool_result` (lint → delta → resumen → corte en N); registra `/qa-guardrail reset` y `/qa-guardrail check`. |
76
+
77
+ ## Archivos
78
+
79
+ | Archivo | Propósito |
80
+ |---------|-----------|
81
+ | `index.ts` | Entry point. Registra el adaptador eslint y los hooks `tool_call` y `tool_result`. |
82
+ | `config.ts`, `selection.ts`, `feedback.ts`, `baseline.ts`, `delta.ts`, `nstop.ts`, `cerco.ts` | Lógica pura y testeable. |
83
+ | `adapters/` | Registro y adaptador eslint. |
84
+ | `*.test.ts` | Tests unitarios de cada módulo puro. |
85
+ | `adapters/eslint.test.ts`, `adapters/eslint-lazy.test.ts` | Tests de integración del adaptador contra un fixture y simulación de eslint ausente. |
86
+ | `adapters/cli.test.ts` | Tests unitarios de la base CLI (carga perezosa, binario ausente, run/parse). |
87
+ | `check.test.ts` | Tests unitarios del barrido y del formato de reporte. |
88
+ | `check.integration.test.ts` | Tests de integración de `/qa-guardrail check` con `ExtensionAPI` mockeado. |
89
+ | `load.integration.test.ts` | Test de integración: la extensión carga sin ningún linter instalado. |
90
+ | `index.integration.test.ts` | Tests de integración con `ExtensionAPI` mockeado (patrón handoff). |
91
+ | `nstop.integration.test.ts` | Tests de integración del corte en N (bloqueo, STOP, reset) con `ExtensionAPI` mockeado. |
92
+ | `cerco.integration.test.ts` | Tests de integración del cerco (caminos protegidos y directivas) con `ExtensionAPI` mockeado. |
93
+ | `package.json` | Vitest como única devDependency. |
94
+ | `vitest.config.ts` | Configuración de Vitest. |
95
+
96
+ ## Testing
97
+
98
+ ```bash
99
+ cd .pi/extensions/qa-guardrail
100
+ npm test # todos los tests una vez
101
+ npm run test:watch # watch mode
102
+ ```
103
+
104
+ `eslint` y `minimatch` son peers implícitos resueltos desde el `node_modules` de la raíz del repo. Ambos se cargan perezosamente: `eslint` se importa dinámicamente en el primer lint y `minimatch` se resuelve en la primera selección. Si `minimatch` no está disponible, la selección no cubre nada (fail-closed); si `eslint` no está (o su config está rota), el adaptador lanza y el runner avisa y omite el feedback — nunca un pase silencioso. Cargar la extensión no exige ninguno de los dos.
105
+
106
+ ## Desarrollo
107
+
108
+ Sigue el ciclo red-green-refactor ([A-003](../../../docs/adr/foundational/A-003-tdd.md)). La API de extensiones de pi está documentada en `docs/extensions.md` del paquete pi.