builder-assistant-engineer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/analyst/prompt.js +19 -0
  4. package/dist/analyst/retry.js +38 -0
  5. package/dist/artifacts/diff.js +47 -0
  6. package/dist/artifacts/lessons.js +32 -0
  7. package/dist/artifacts/merge.js +70 -0
  8. package/dist/artifacts/write.js +44 -0
  9. package/dist/backends/agent-cli.js +174 -0
  10. package/dist/backends/api.js +171 -0
  11. package/dist/backends/index.js +10 -0
  12. package/dist/backends/manual.js +52 -0
  13. package/dist/backends/sse.js +24 -0
  14. package/dist/backends/types.js +1 -0
  15. package/dist/bin.js +3 -0
  16. package/dist/cli.js +104 -0
  17. package/dist/commands/context.js +12 -0
  18. package/dist/commands/init.js +199 -0
  19. package/dist/commands/next.js +103 -0
  20. package/dist/commands/plan.js +71 -0
  21. package/dist/commands/replan.js +37 -0
  22. package/dist/commands/review.js +37 -0
  23. package/dist/commands/shared.js +40 -0
  24. package/dist/commands/status.js +95 -0
  25. package/dist/config/commands.js +85 -0
  26. package/dist/config/schema.js +43 -0
  27. package/dist/config/settings.js +8 -0
  28. package/dist/config/store.js +38 -0
  29. package/dist/core/bash.js +105 -0
  30. package/dist/core/errors.js +14 -0
  31. package/dist/core/fs.js +35 -0
  32. package/dist/core/git.js +104 -0
  33. package/dist/core/gitignore.js +14 -0
  34. package/dist/core/json.js +13 -0
  35. package/dist/core/paths.js +14 -0
  36. package/dist/core/process.js +56 -0
  37. package/dist/core/prompt-loader.js +27 -0
  38. package/dist/core/state.js +27 -0
  39. package/dist/core/which.js +24 -0
  40. package/dist/detect/agents.js +17 -0
  41. package/dist/detect/mode.js +11 -0
  42. package/dist/digest/baseline.js +192 -0
  43. package/dist/digest/budget.js +31 -0
  44. package/dist/digest/files.js +167 -0
  45. package/dist/digest/format.js +18 -0
  46. package/dist/digest/git.js +19 -0
  47. package/dist/digest/index.js +100 -0
  48. package/dist/digest/manifests.js +133 -0
  49. package/dist/digest/read.js +28 -0
  50. package/dist/digest/redact.js +37 -0
  51. package/dist/digest/render.js +138 -0
  52. package/dist/digest/select.js +116 -0
  53. package/dist/digest/todos.js +16 -0
  54. package/dist/digest/tree.js +55 -0
  55. package/dist/digest/walk.js +72 -0
  56. package/dist/gates/capture.js +145 -0
  57. package/dist/gates/contract.js +339 -0
  58. package/dist/gates/enforce.js +143 -0
  59. package/dist/gates/findings.js +17 -0
  60. package/dist/gates/gate.js +315 -0
  61. package/dist/gates/ignore-rules.js +104 -0
  62. package/dist/gates/protected-files.js +311 -0
  63. package/dist/gates/regression.js +219 -0
  64. package/dist/gates/reports.js +349 -0
  65. package/dist/gates/results.js +295 -0
  66. package/dist/gates/runners.js +189 -0
  67. package/dist/gates/state-guard.js +56 -0
  68. package/dist/i18n/en.js +288 -0
  69. package/dist/i18n/es.js +288 -0
  70. package/dist/i18n/index.js +18 -0
  71. package/dist/interview/adaptive.js +69 -0
  72. package/dist/interview/brief.js +27 -0
  73. package/dist/interview/questions.js +18 -0
  74. package/dist/interview/render.js +19 -0
  75. package/dist/interview/reply.js +34 -0
  76. package/dist/next/attempts.js +151 -0
  77. package/dist/next/start.js +179 -0
  78. package/dist/plan/continuation.js +40 -0
  79. package/dist/plan/evidence.js +239 -0
  80. package/dist/plan/filter.js +20 -0
  81. package/dist/plan/parser.js +283 -0
  82. package/dist/plan/prior.js +32 -0
  83. package/dist/plan/questions.js +48 -0
  84. package/dist/plan/repair.js +33 -0
  85. package/dist/plan/report.js +37 -0
  86. package/dist/plan/run.js +191 -0
  87. package/dist/prompts/analyst.md +101 -0
  88. package/dist/prompts/continue.md +11 -0
  89. package/dist/prompts/fix-format.md +13 -0
  90. package/dist/prompts/fix-paths.md +23 -0
  91. package/dist/prompts/lesson.md +18 -0
  92. package/dist/prompts/retry.md +9 -0
  93. package/dist/prompts/review.md +36 -0
  94. package/dist/prompts/task.md +9 -0
  95. package/dist/review/changes.js +177 -0
  96. package/dist/review/diff.js +115 -0
  97. package/dist/review/integrity.js +150 -0
  98. package/dist/review/mechanical.js +62 -0
  99. package/dist/review/parse.js +40 -0
  100. package/dist/review/reviewer.js +23 -0
  101. package/dist/review/run.js +134 -0
  102. package/dist/review/scope.js +55 -0
  103. package/dist/review/secrets.js +163 -0
  104. package/dist/review/snapshot.js +38 -0
  105. package/dist/review/tests.js +140 -0
  106. package/dist/tasks/attempts.js +71 -0
  107. package/dist/tasks/checks.js +181 -0
  108. package/dist/tasks/frontmatter.js +58 -0
  109. package/dist/tasks/graph.js +27 -0
  110. package/dist/tasks/handoff.js +62 -0
  111. package/dist/tasks/learn.js +128 -0
  112. package/dist/tasks/load.js +37 -0
  113. package/dist/tasks/metrics.js +40 -0
  114. package/dist/tasks/runs.js +12 -0
  115. package/dist/tasks/schema.js +96 -0
  116. package/dist/tasks/select.js +15 -0
  117. package/dist/tasks/shell-words.js +176 -0
  118. package/dist/tasks/status.js +13 -0
  119. package/dist/tasks/verify.js +181 -0
  120. package/dist/ui/clack.js +46 -0
  121. package/dist/ui/prompter.js +1 -0
  122. package/package.json +64 -0
@@ -0,0 +1,288 @@
1
+ export const es = {
2
+ "program.description": "Convierte una idea o un repo existente en un plan de ejecución para agentes de IA de consola.",
3
+ "option.backend": "backend de IA a usar",
4
+ "option.lang": "idioma del CLI y de los artefactos generados",
5
+ "option.dryRun": "muestra exactamente qué se enviaría a la IA, sin escribir nada",
6
+ "option.yes": "acepta todas las confirmaciones",
7
+ "option.help": "muestra la ayuda",
8
+ "option.version": "muestra la versión",
9
+ "option.only": "genera solo un grupo de artefactos",
10
+ "option.headless": "ejecuta el agente sin sesión interactiva (acepta ediciones, nunca bypass)",
11
+ "command.init": "detecta el proyecto, elige backend, agentes e idioma, y hace la entrevista",
12
+ "command.plan": "analiza el repo y genera el plan, la memoria y los agentes",
13
+ "command.next": "ejecuta la siguiente tarea pendiente, la verifica, la revisa y la marca como hecha",
14
+ "command.status": "muestra fases, tareas y progreso",
15
+ "command.replan": "re-analiza el repo, conserva lo hecho y actualiza lo pendiente",
16
+ "command.review": "revisa el diff de una tarea contra sus criterios de aceptación y AGENTS.md",
17
+ "command.help": "muestra la ayuda de un comando",
18
+ "option.brief": "archivos separados por comas con tu brief o docs (evita pegar texto)",
19
+ "ui.cancelled": "Cancelado.",
20
+ "format.retrying": "La respuesta no siguió el formato; se pide una vez más corregir solo el formato.",
21
+ "format.failed": "La respuesta sigue sin cumplir el formato ({{details}}). Salida original guardada en {{path}}.",
22
+ "init.intro": "builder-assistant-engineer · configuración",
23
+ "init.lang": "Idioma del CLI y del plan generado",
24
+ "init.mode": "Tipo de proyecto",
25
+ "init.detected": "detectado",
26
+ "init.backend": "¿Qué IA hará el análisis?",
27
+ "init.backendMissing": "`{{command}}` no está en el PATH; instálalo antes de ejecutar plan.",
28
+ "init.targets": "¿Qué agentes trabajarán en este repo?",
29
+ "init.redoInterview": "Ya existe una entrevista en .bae/interview.md. ¿Rehacerla?",
30
+ "init.briefMissing": "No se pudieron leer los archivos del brief: {{path}}",
31
+ "init.dryRunNoAi": "Dry run: init no enviaría nada a la IA.",
32
+ "init.dryRunDone": "Dry run terminado. No se escribió nada.",
33
+ "init.done": "Configuración guardada en .bae/. Siguiente paso: {{command}}",
34
+ "mode.greenfield": "Greenfield — proyecto nuevo, poco o ningún código",
35
+ "mode.brownfield": "Brownfield — código existente",
36
+ "backend.label.manual": "Manual (copiar y pegar)",
37
+ "backend.hint.installed": "instalado",
38
+ "backend.hint.missing": "no está en el PATH",
39
+ "backend.hint.api": "clave de Anthropic o compatible con OpenAI por variables de entorno",
40
+ "backend.hint.apiReady": "credenciales encontradas en el entorno",
41
+ "backend.hint.manual": "funciona con cualquier IA, incluso un chat web",
42
+ "interview.intro": "Entrevista — pulsa Enter para saltar cualquier pregunta.",
43
+ "interview.brief": "Brief: pega un resumen de una línea, o rutas a archivos con tu brief o docs (separadas por comas)",
44
+ "interview.briefPlaceholder": "p. ej. docs/brief.md, notas/idea.md",
45
+ "interview.briefLoaded": "Cargados {{count}} archivo(s): {{paths}}",
46
+ "interview.objective": "¿Cuál es el objetivo sobre este repo?",
47
+ "interview.q.what": "¿Qué quieres construir y para quién?",
48
+ "interview.q.problem": "¿Qué problema resuelve y cómo sabrás que funciona?",
49
+ "interview.q.scope": "¿Qué entra en el primer entregable y qué queda fuera?",
50
+ "interview.q.constraints": "Restricciones: stack obligatorio, plazos, plataformas, integraciones, presupuesto?",
51
+ "interview.q.team": "¿Quién ejecuta el plan (solo IA, tú + IA, un equipo) y con qué nivel?",
52
+ "interview.q.rules": "Preferencias y prohibiciones: convenciones, zonas del repo que no se tocan?",
53
+ "interview.followUps": "El analista puede hacer hasta {{max}} preguntas de seguimiento.",
54
+ "interview.thinking": "Pensando la siguiente pregunta…",
55
+ "interview.other": "Otra (escríbela)",
56
+ "interview.skip": "Saltar",
57
+ "interview.answer": "Tu respuesta",
58
+ "interview.adaptiveFailed": "Se omiten las preguntas de seguimiento restantes: {{details}}",
59
+ "interview.adaptiveManual": "Con el backend manual se omiten las preguntas de seguimiento.",
60
+ "objective.feature": "Construir una funcionalidad nueva",
61
+ "objective.refactor": "Refactorizar",
62
+ "objective.migration": "Migrar (framework, lenguaje, infraestructura)",
63
+ "objective.bugs": "Corregir bugs",
64
+ "objective.docs": "Documentar el código",
65
+ "md.interview": "Entrevista",
66
+ "md.mode": "Modo",
67
+ "md.objective": "Objetivo",
68
+ "md.brief": "Brief",
69
+ "md.followUps": "Preguntas de seguimiento",
70
+ "md.summary": "Resumen",
71
+ "md.skipped": "sin respuesta",
72
+ "config.missing": "No hay configuración en .bae/config.json. Ejecuta primero {{command}}.",
73
+ "digest.reading": "Leyendo el repositorio…",
74
+ "plan.intro": "builder-assistant-engineer · plan",
75
+ "plan.noInterview": "No hay entrevista en .bae/interview.md; se planifica solo a partir del repositorio.",
76
+ "plan.analyzing": "El analista está construyendo el plan (puede tardar varios minutos)",
77
+ "plan.progress": "Recibiendo el plan… {{chars}} caracteres",
78
+ "plan.existingPlan": "Ya existe un plan en docs/plan/tasks. ¿Regenerarlo desde cero? (replan conserva el progreso)",
79
+ "plan.useReplan": "Sin cambios. Usa replan para actualizar el plan conservando lo hecho.",
80
+ "plan.dryRunDone": "Dry run: el prompt de arriba es exactamente lo que se enviaría. No se escribió nada.",
81
+ "plan.continuing": "La respuesta se cortó en {{marker}}; se pide al analista que continúe desde ahí.",
82
+ "plan.summary": "Resumen",
83
+ "plan.blocking": "bloqueante",
84
+ "plan.done": "{{count}} archivo(s) escritos. Siguiente paso: {{next}}",
85
+ "plan.nothingWritten": "No se escribió ningún archivo.",
86
+ "artifacts.changes": "Cambios",
87
+ "artifacts.confirm": "¿Escribir {{count}} archivo(s)?",
88
+ "artifacts.all": "Escribir todo",
89
+ "artifacts.each": "Revisar uno a uno",
90
+ "artifacts.none": "No escribir nada",
91
+ "artifacts.confirmOne": "¿Escribir {{path}}?",
92
+ "artifacts.new": "nuevo, {{lines}} líneas",
93
+ "artifacts.updated": "+{{added}} −{{removed}}",
94
+ "artifacts.unchanged": "sin cambios",
95
+ "artifacts.keptDone": "tarea hecha, se conserva tal cual",
96
+ "tasks.invalid": "Se ignora un archivo de tarea inválido: {{error}}",
97
+ "next.intro": "builder-assistant-engineer · next",
98
+ "next.meta": "fase {{phase}} · tamaño {{size}} · riesgo {{risk}} · depende de {{deps}}",
99
+ "next.dryRunDone": "Dry run: la tarea de arriba es exactamente lo que recibiría el agente. No se cambió nada.",
100
+ "next.headlessNeedsAgent": "--headless necesita un backend de agente (claude, opencode, codex, gemini); se usa el flujo manual.",
101
+ "next.launching": "Abriendo {{backend}} con la tarea. Sal de la sesión cuando la tarea esté terminada.",
102
+ "next.attempt": "Intento {{attempt}} de {{max}} con {{backend}} (headless, acepta ediciones)",
103
+ "next.done": "{{id}} está hecha. Siguiente paso: {{command}}",
104
+ "next.notDone": "{{id}} sigue en curso. Corrígela y vuelve a ejecutar {{command}}.",
105
+ "next.blocked": "{{id}} queda bloqueada: {{reason}} Logs: {{path}}",
106
+ "next.noPlan": "Todavía no hay tareas. Ejecuta primero {{command}}.",
107
+ "next.allDone": "Todas las tareas están hechas.",
108
+ "next.nothingReady": "No hay ninguna tarea lista",
109
+ "next.unblock": "Desbloquea o termina las tareas de arriba, o ejecuta replan.",
110
+ "verify.commands": "Verificación",
111
+ "verify.confirm": "¿Ejecutar ahora estos comandos de verificación?",
112
+ "verify.declined": "No se ejecutó la verificación; la tarea sigue en curso.",
113
+ "verify.none": "La tarea no tiene comandos de verificación en un bloque ```sh dentro de Verification.",
114
+ "verify.failed": "La verificación falló: `{{command}}` terminó con {{code}}.",
115
+ "verify.passed": "Verificación superada.",
116
+ "verify.stillFailing": "`{{command}}` ya fallaba antes de la tarea y sigue fallando (salida {{code}}), así que no bloqueó. Si algún criterio de aceptación exige que pase, la tarea no está hecha.",
117
+ "review.intro": "builder-assistant-engineer · review",
118
+ "review.running": "El revisor está comprobando {{id}}…",
119
+ "review.findings": "Hallazgos de la revisión",
120
+ "review.passed": "Revisión superada.",
121
+ "review.failed": "La revisión de {{id}} no pasó.",
122
+ "review.noGit": "Revisión omitida: no es un repositorio git, así que no hay diff que revisar.",
123
+ "review.noChanges": "La tarea no cambió archivos y sus chequeos pasan, así que no se llamó al revisor.",
124
+ "review.dryRun": "Dry run: el prompt de revisión de arriba no se envió.",
125
+ "review.noTask": "No hay ninguna tarea en curso. Indica un id, p. ej. review T-003.",
126
+ "review.unknownTask": "No se encontró la tarea {{id}} en docs/plan/tasks.",
127
+ "status.empty": "Todavía no hay plan. Ejecuta primero {{command}}.",
128
+ "status.phase": "Fase {{phase}}",
129
+ "status.waiting": "espera a {{ids}}",
130
+ "status.total": "{{done}}/{{total}} hechas ({{percent}}%)",
131
+ "status.next": "siguiente: {{id}} {{title}}",
132
+ "status.noNext": "ninguna tarea lista",
133
+ "replan.intro": "builder-assistant-engineer · replan",
134
+ "replan.noPlan": "No hay plan que actualizar. Ejecuta primero {{command}}.",
135
+ "replan.noChangelog": "El analista no escribió docs/plan/CHANGELOG.md.",
136
+ "artifacts.removed": "se elimina, ya no está en el plan",
137
+ "argument.task": "id de tarea, p. ej. T-003 (por defecto, la tarea en curso)",
138
+ "error.configJson": "{{path}} no es JSON válido: {{details}}",
139
+ "error.configInvalid": "{{path}} no es válido:\n{{details}}",
140
+ "error.promptMissingVars": "{{path}} usa variables que no se proporcionaron: {{vars}}",
141
+ "error.unexpected": "Error inesperado. Repórtalo adjuntando la salida siguiente.",
142
+ "backend.notInstalled": "`{{command}}` no está instalado o no está en el PATH. Instálalo o elige otro backend con --backend.",
143
+ "backend.timedOut": "`{{command}}` no terminó en {{minutes}} minutos y se detuvo (agent.timeoutMinutes en .bae/config.json).",
144
+ "backend.failed": "`{{command}}` terminó con código {{code}}:\n{{details}}",
145
+ "backend.apiNotConfigured": "El backend api necesita ANTHROPIC_API_KEY, u OPENAI_BASE_URL/OPENAI_API_KEY para un endpoint compatible con OpenAI.",
146
+ "backend.apiModelRequired": "Define BAE_MODEL con el modelo a usar en {{baseUrl}}.",
147
+ "backend.apiProviderInvalid": "BAE_API_PROVIDER debe ser anthropic u openai, no {{provider}}.",
148
+ "backend.apiHttp": "Error de la API de {{provider}} ({{status}}): {{details}}",
149
+ "backend.apiNetwork": "No se pudo conectar con {{url}}: {{details}}",
150
+ "backend.apiInteractive": "El backend api no puede abrir una sesión interactiva.",
151
+ "backend.refusal": "El modelo rechazó la solicitud.",
152
+ "backend.truncated": "La respuesta alcanzó el límite de tokens de salida y está incompleta.",
153
+ "manual.copied": "Prompt copiado al portapapeles y guardado en {{path}}.",
154
+ "manual.notCopied": "No se pudo usar el portapapeles; el prompt está guardado en {{path}}.",
155
+ "manual.awaitingResponse": "Pégalo en tu IA. Luego pega aquí la respuesta completa y termina con Ctrl-D (Ctrl-Z, Enter en Windows), o guárdala en {{path}} y pulsa Enter.",
156
+ "manual.awaitingDone": "Ejecútalo con tu agente y pulsa Enter cuando la tarea termine.",
157
+ "manual.emptyResponse": "No se recibió respuesta. Pégala aquí o guárdala en {{path}}.",
158
+ "plan.commands": "Comandos del proyecto guardados en .bae/config.json",
159
+ "regression.title": "Chequeo de regresión",
160
+ "regression.baselineTitle": "Línea base de regresión, antes de la tarea",
161
+ "regression.confirm": "¿Ejecutar ahora los comandos de lint y test del proyecto para registrar la línea base?",
162
+ "regression.preexisting": "`{{command}}` ya falla antes de la tarea (salida {{code}}); queda registrado como preexistente y no bloquea.",
163
+ "regression.found": "Regresión: `{{command}}` termina con {{code}} después de la tarea.",
164
+ "regression.stillFailing": "`{{command}}` sigue fallando (salida {{code}}), igual que antes de la tarea.",
165
+ "regression.passed": "Chequeo de regresión superado.",
166
+ "evidence.retrying": "{{count}} ruta(s) citadas no están en el repositorio ni marcadas (new); se pide al analista corregir solo esas.",
167
+ "evidence.fixing": "El analista está corrigiendo las rutas citadas…",
168
+ "evidence.fixed": "Todas las rutas citadas existen o están marcadas (new).",
169
+ "evidence.unverified": "{{count}} ruta(s) citadas siguen sin verificar; aparecen en el resumen.",
170
+ "evidence.summary": "Rutas sin verificar (no están en el repositorio ni marcadas como nuevas):",
171
+ "mechanical.secretFile": "Parece un archivo de secretos (.env, clave privada o credenciales); déjalo fuera del cambio.",
172
+ "mechanical.secretValue": "Añade lo que parece una credencial ({{kind}}); léela del entorno.",
173
+ "mechanical.noTests": "La tarea exige tests (tests: required), pero no ejecuta más tests que antes ni añade aserciones a un archivo de test.",
174
+ "mechanical.outOfScope": "Cambios fuera del Scope de la tarea: {{files}}",
175
+ "mechanical.failed": "Los chequeos automáticos fallaron, así que no se lanzó el revisor.",
176
+ "plan.openQuestions": "Preguntas abiertas, guardadas en .bae/interview.md; respóndelas allí y ejecuta replan cuando puedas",
177
+ "plan.questionsSaved": "{{count}} pregunta(s) guardadas en .bae/interview.md.",
178
+ "plan.rerun": "Respondiste preguntas bloqueantes. ¿Volver a generar el plan ahora con tus respuestas?",
179
+ "plan.rerunning": "Generando el plan otra vez con tus respuestas.",
180
+ "md.planQuestions": "Preguntas del plan ({{date}})",
181
+ "md.why": "Por qué",
182
+ "md.options": "Opciones",
183
+ "md.blocking": "Bloqueante: el plan asumió una respuesta",
184
+ "md.answer": "Respuesta",
185
+ "md.open": "abierta, sin responder",
186
+ "handoff.missing": "{{path}} no tiene nota de traspaso: escribe como máximo {{max}} líneas bajo ## Log (qué cambió, decisiones, trampas).",
187
+ "handoff.tooLong": "La nota de traspaso de {{path}} tiene {{count}} líneas; déjala en {{max}} como máximo.",
188
+ "handoff.passed": "Nota de traspaso presente.",
189
+ "md.lessons": "Lecciones aprendidas",
190
+ "lesson.asking": "{{id}} falló varias veces; pidiendo al agente la causa raíz y una regla…",
191
+ "lesson.title": "Lección de {{id}}",
192
+ "lesson.body": "Causa raíz: {{cause}}\nRegla: {{rule}}",
193
+ "lesson.confirm": "¿Añadir esta regla a AGENTS.md?",
194
+ "lesson.added": "Regla añadida a AGENTS.md.",
195
+ "lesson.skipped": "Regla no añadida; queda en {{path}}.",
196
+ "lesson.failed": "No se pudo obtener una lección del agente: {{details}}",
197
+ "status.blockedReason": "{{id}} está bloqueada: {{reason}}",
198
+ "status.metrics": "Métricas locales",
199
+ "status.attempts": "intentos: {{attempts}} en {{tasks}} tarea(s), {{average}} por tarea",
200
+ "status.firstAttempt": "hechas al primer intento: {{count}}/{{tasks}} ({{percent}}%)",
201
+ "status.regressions": "regresiones atrapadas: {{count}}",
202
+ "status.time": "tiempo por tarea hecha: {{average}} de media, {{total}} en total",
203
+ "status.taskRuns": "{{attempts}} intento(s), {{time}}",
204
+ "format.repaired": "Respuesta reparada localmente: {{repairs}}.",
205
+ "evidence.retryFailed": "Falló la petición para corregir las rutas citadas, así que el plan las conserva tal cual: {{details}}",
206
+ "regression.noCommands": "No hay comando de lint ni de test en .bae/config.json ni en los manifiestos; el chequeo de regresión queda desactivado hasta que los añadas en commands.",
207
+ "option.acceptFinding": "acepta un hallazgo por su id (repetible); solo secretos, y hallazgos de contrato o de tests en archivos que el Alcance de la tarea lista",
208
+ "review.noBase": "Revisión fallida: el commit registrado al empezar la tarea ya no existe, así que no se pueden aislar sus cambios.",
209
+ "review.noCapture": "{{id}} no tiene captura de next; se revisa contra el HEAD actual.",
210
+ "contract.title": "Contrato",
211
+ "contract.failed": "La tarea cambió archivos que definen sus propios chequeos; se restauraron desde el estado capturado antes de la tarea.",
212
+ "contract.recovered": "Una ejecución anterior de {{id}} terminó antes de sus chequeos; se restauraron los archivos que cambió y que definen los chequeos.",
213
+ "contract.task": "Editó el archivo de la tarea fuera de ## Log.",
214
+ "contract.tasks": "Editó otro archivo de tarea.",
215
+ "contract.bae": "Cambió la configuración o los prompts de bae.",
216
+ "contract.agents": "Cambió la definición de un agente o la configuración con la que corre el revisor.",
217
+ "contract.gitignore": "Cambió un archivo de ignore, que decide qué ve la revisión.",
218
+ "contract.scripts": "Cambió {{detail}}, que los chequeos ejecutan.",
219
+ "contract.runner": "Cambió la configuración del runner de tests.",
220
+ "contract.restored": "Restaurado.",
221
+ "contract.removed": "Eliminado.",
222
+ "findings.accepted": "Aceptado con --accept-finding.",
223
+ "findings.acceptedTitle": "Hallazgos aceptados",
224
+ "regression.failed": "El chequeo de regresión falló.",
225
+ "regression.mustPass": "`{{command}}` sigue fallando (exit {{code}}) y la tarea es tests: fix, así que la suite de tests debe terminar en verde.",
226
+ "regression.uncomparable": "`{{command}}` ya fallaba antes de la tarea y sigue fallando (exit {{code}}); su salida no tiene conteos para comparar, así que solo pasa en verde.",
227
+ "regression.worse": "Regresión: `{{command}}` falla más chequeos que antes de la tarea ({{now}}; antes: {{before}}).",
228
+ "regression.lateStop": "{{id}} no tiene línea base de regresión de antes de que corriera su agente.",
229
+ "regression.declinedStop": "Sin correr antes lint y tests no hay línea base, así que la tarea no podría completarse.",
230
+ "regression.unusable": "`{{command}}` no da una línea base usable (exit {{code}}): no terminó, o falla sin conteos que comparar. La tarea no podría completarse mientras siga en rojo.",
231
+ "review.noVerdict": "El revisor no dio veredicto.",
232
+ "review.error": "El revisor no pudo dar un veredicto: {{details}}",
233
+ "skip.used": "Se continúa sin este chequeo por --allow-skip: {{what}}",
234
+ "skip.title": "Omitido con --allow-skip",
235
+ "skip.stopped": "No se lanzó nada. Corrige la causa, o vuelve a correr next con --allow-skip para seguir sin ese chequeo; la omisión queda en el log de la ejecución.",
236
+ "next.budgetUsed": "La tarea ya usó sus {{max}} intentos automáticos.",
237
+ "next.refusedBlocked": "{{id}} queda bloqueada antes de lanzar el agente, porque sus chequeos no pueden correr. Corrige la Verificación de la tarea o los comandos de .bae/config.json y vuelve a ponerla en pending.",
238
+ "capture.lateStop": "{{id}} está en curso sin una captura de antes de que corriera su agente, así que sus chequeos no tienen un punto de partida fiable. Vuelve a ponerla en pending, o corre con --allow-skip.",
239
+ "capture.noGit": "No es un repositorio git, así que la revisión no puede ver los cambios de la tarea.",
240
+ "option.allowSkip": "continúa cuando un chequeo no puede correr (sin git, sin línea base, sin veredicto del revisor); cada omisión queda registrada",
241
+ "integrity.title": "Integridad de los tests",
242
+ "integrity.failed": "La tarea quitó o desactivó tests.",
243
+ "integrity.deleted": "Borra un archivo de test.",
244
+ "integrity.skipMarker": "Añade un marcador que omite o aísla tests ({{marker}}).",
245
+ "integrity.exclusion": "Añade una configuración del runner de tests que deja tests fuera ({{keys}}).",
246
+ "integrity.fewerTests": "`{{command}}` ejecuta menos tests que antes de la tarea ({{now}}; antes: {{before}}).",
247
+ "integrity.moreSkipped": "`{{command}}` omite más tests que antes de la tarea ({{now}}; antes: {{before}}).",
248
+ "verify.noBash": "La Verificación corre como un script de bash y no se encontró bash. En Windows instala Git for Windows, que trae Git Bash; en otros sistemas pon bash en el PATH.",
249
+ "verify.masks": "La Verificación oculta fallos en `{{command}}` (|| true, set +e). El bloque corre con set -euo pipefail y debe fallar cuando falla un chequeo.",
250
+ "verify.trivial": "La Verificación no ejecuta nada que compruebe la tarea:\n{{command}}\nUsa el runner de tests del proyecto, un linter o un chequeo con resultado esperado (test -f, grep -q, curl -f).",
251
+ "verify.notAllowed": "`{{command}}` no está en la lista de comandos que bae corre cuando nadie los confirma ({{why}}). Añade un prefijo a verify.allow en .bae/config.json, o corre next sin --yes ni --headless y confírmalo tú.",
252
+ "regression.notAllowed": "El comando del proyecto `{{command}}` no está en la lista de comandos que bae corre cuando nadie los confirma ({{why}}). Añade un prefijo a verify.allow en .bae/config.json.",
253
+ "verify.dynamic": "ejecuta código que se arma en tiempo de ejecución o en otro shell",
254
+ "verify.unknown": "programa desconocido",
255
+ "verify.unsafeWarning": "`{{command}}` parece peligroso ({{reason}}); léelo antes de confirmar.",
256
+ "verify.onlyExcused": "La Verificación solo ejecuta comandos que ya fallaban antes de la tarea, así que no comprueba nada de esta tarea.",
257
+ "evidence.linesFailed": "El plan cita líneas que no existen, incluso después de pedir al analista que las corrija:\n{{list}}\nEl plan no se escribió; la respuesta rechazada está en {{path}}. Vuelve a correr plan.",
258
+ "state.tampered": "Código ejecutado para esta tarea cambió el estado propio de bae fuera del repositorio ({{files}}); se restauró y el intento falla.",
259
+ "review.gitError": "Revisión fallida: git no pudo listar los cambios de la tarea (índice, configuración o filtro rotos), así que los chequeos no pueden verlos.",
260
+ "contract.memory": "Cambió un archivo de memoria de agentes, que leen los agentes y el revisor.",
261
+ "contract.git": "Cambió atributos de git, que deciden cómo ve la revisión los archivos.",
262
+ "contract.gitdir": "Cambió la configuración de git del repositorio en .git, de la que dependen los chequeos.",
263
+ "contract.toolchain": "Cambió una configuración del gestor de paquetes o de una herramienta que decide cómo corren los chequeos.",
264
+ "contract.shadow": "Añadió un archivo que se ejecutaría en lugar de la herramienta del proyecto.",
265
+ "contract.indexFlags": "Marcó archivos como sin cambios en el índice de git (assume-unchanged o skip-worktree), lo que los oculta de la revisión: {{files}}. Se quitaron las marcas.",
266
+ "integrity.removedTests": "Quita tests que no vuelven a añadirse: {{tests}}.",
267
+ "integrity.lostAssertions": "Quita {{count}} línea(s) de aserción de un archivo de test sin volver a añadirlas.",
268
+ "capture.headMoved": "{{id}} conserva el commit registrado cuando empezó; los commits hechos desde entonces cuentan como parte de los cambios de la tarea.",
269
+ "contract.restoreFailed": "No se pudo restaurar {{files}} desde la captura. Corrígelos a mano; la siguiente ejecución los vuelve a comprobar antes de empezar.",
270
+ "integrity.suppression": "Añade un comentario que silencia un chequeo ({{markers}}).",
271
+ "integrity.expectedOutput": "Cambia un snapshot o un archivo de salida esperada, que decide qué aceptan los tests.",
272
+ "regression.baselineTampered": "Correr los comandos del proyecto para la línea base cambió archivos de los que dependen los chequeos ({{files}}); se restauraron y no se lanzó nada.",
273
+ "regression.unknown": "`{{command}}` termina con {{code}}, pero bae no pudo leer cuántos tests corrieron, así que no puede saber si pasaron.",
274
+ "regression.unknownRunner": "bae no reconoce el runner de tests detrás de `{{command}}`, así que no puede saber si los tests pasan. Pon en commands.test de .bae/config.json el runner mismo, por ejemplo `npx vitest run`, `npx jest`, `pytest`, `go test -v ./...`, `cargo test` o `dotnet test`.",
275
+ "regression.noCounts": "`{{command}}` no da una línea base usable: pasa, pero no dice cuántos tests corrieron, así que la tarea no podría compararse con ella.",
276
+ "regression.unreadSkipped": "`{{command}}` no dijo cuántos tests corrieron",
277
+ "contract.scoped": "El Scope de la tarea lista este archivo, así que el cambio se queda: los chequeos corren con él y el revisor lo ve.",
278
+ "contract.weakerScripts": "Deja de correr lo que corrían estos scripts: {{scripts}}.",
279
+ "contract.weakerRunner": "Añade ajustes que dejan tests fuera ({{keys}}).",
280
+ "contract.weakerToolchain": "Añade un ajuste que cambia cómo arranca el gestor de paquetes o el runner de tests.",
281
+ "review.noEvidence": "(ninguna: esta revisión no se corrió justo después de los chequeos de next)",
282
+ "integrity.scoped": "El Scope de la tarea lista este archivo, así que el revisor debe decir por qué este cambio es correcto.",
283
+ "review.unjustified": "El revisor aprobó la tarea sin decir por qué son correctos estos cambios en archivos de test: {{files}}.",
284
+ "mechanical.secretHistory": "Un commit hecho durante la tarea añade algo que parece una credencial ({{kind}}). Se queda en el historial de git aunque el archivo ya no lo tenga, así que reescribe esos commits.",
285
+ "mechanical.secretFileHistory": "Un commit hecho durante la tarea añade un archivo de secretos. Se queda en el historial de git aunque el archivo ya no exista, así que reescribe esos commits.",
286
+ "regression.noTestsYet": "`{{command}}` todavía no tiene tests que correr, así que no hay línea base; después de la tarea debe pasar y decir cuántos tests corrieron.",
287
+ "integrity.planned": "El Scope de la tarea marca archivos de test que elimina, así que lo juzga el revisor.",
288
+ };
@@ -0,0 +1,18 @@
1
+ import { en } from "./en.js";
2
+ import { es } from "./es.js";
3
+ export const LANGS = ["en", "es"];
4
+ const catalogs = { en, es };
5
+ const PLACEHOLDER = /\{\{(\w+)\}\}/g;
6
+ let current = "en";
7
+ export function setLang(lang) {
8
+ current = lang;
9
+ }
10
+ export function getLang() {
11
+ return current;
12
+ }
13
+ export function isLang(value) {
14
+ return LANGS.some((lang) => lang === value);
15
+ }
16
+ export function t(key, vars = {}) {
17
+ return catalogs[current][key].replace(PLACEHOLDER, (match, name) => String(vars[name] ?? match));
18
+ }
@@ -0,0 +1,69 @@
1
+ import { buildAnalystPrompt } from "../analyst/prompt.js";
2
+ import { runWithFormatRetry } from "../analyst/retry.js";
3
+ import { UserError } from "../core/errors.js";
4
+ import { t } from "../i18n/index.js";
5
+ import { MAX_FOLLOW_UPS } from "./questions.js";
6
+ import { renderInterview } from "./render.js";
7
+ import { INTERVIEW_FORMAT, parseInterviewReply } from "./reply.js";
8
+ export async function runAdaptiveRound(context, data) {
9
+ const followUps = [];
10
+ context.prompter.info(t("interview.followUps", { max: MAX_FOLLOW_UPS }));
11
+ try {
12
+ for (let round = 0; round < MAX_FOLLOW_UPS; round++) {
13
+ const reply = await nextReply(context, { ...data, followUps });
14
+ if (reply.done)
15
+ return { ...data, followUps, summary: reply.summary };
16
+ followUps.push({
17
+ question: reply.question,
18
+ answer: await askQuestion(context.prompter, reply),
19
+ });
20
+ }
21
+ }
22
+ catch (error) {
23
+ if (!(error instanceof UserError) || error.message === t("ui.cancelled"))
24
+ throw error;
25
+ context.prompter.warn(t("interview.adaptiveFailed", { details: error.message }));
26
+ }
27
+ return { ...data, followUps };
28
+ }
29
+ export function interviewPrompt(context, data) {
30
+ return buildAnalystPrompt(context.cwd, {
31
+ mode: "INTERVIEW",
32
+ projectType: context.projectType,
33
+ lang: context.lang,
34
+ targets: context.targets,
35
+ interview: renderInterview(data),
36
+ digest: context.digest,
37
+ canExplore: context.canExplore,
38
+ priorPlan: "",
39
+ });
40
+ }
41
+ async function nextReply(context, data) {
42
+ const prompt = await interviewPrompt(context, data);
43
+ return context.prompter.spinner(t("interview.thinking"), () => runWithFormatRetry({
44
+ backend: context.backend,
45
+ prompt,
46
+ options: { cwd: context.cwd, access: "read" },
47
+ parse: parseInterviewReply,
48
+ format: INTERVIEW_FORMAT,
49
+ onRetry: () => context.prompter.warn(t("format.retrying")),
50
+ }));
51
+ }
52
+ export async function askQuestion(prompter, reply) {
53
+ if (reply.why)
54
+ prompter.note(reply.why, reply.question);
55
+ const options = reply.options ?? [];
56
+ if (options.length === 0)
57
+ return prompter.text(reply.question, t("interview.answer"));
58
+ const choices = [
59
+ ...options.map((option, index) => ({ value: `option-${index}`, label: option })),
60
+ { value: "other", label: t("interview.other") },
61
+ { value: "skip", label: t("interview.skip") },
62
+ ];
63
+ const picked = await prompter.select(reply.question, choices);
64
+ if (picked === "skip")
65
+ return "";
66
+ if (picked === "other")
67
+ return prompter.text(reply.question, t("interview.answer"));
68
+ return options[Number(picked.slice("option-".length))] ?? "";
69
+ }
@@ -0,0 +1,27 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { redact } from "../digest/redact.js";
4
+ const MAX_FILE_CHARS = 50_000;
5
+ export async function loadBrief(cwd, raw) {
6
+ const tokens = raw
7
+ .split(",")
8
+ .map((token) => token.trim())
9
+ .filter(Boolean);
10
+ if (tokens.length === 0)
11
+ return { text: "", files: [] };
12
+ const contents = await Promise.all(tokens.map((token) => readIfFile(resolve(cwd, token))));
13
+ if (contents.some((content) => content === undefined))
14
+ return { text: redact(raw.trim()), files: [] };
15
+ const sections = tokens.map((token, index) => `### ${token}\n\n${contents[index] ?? ""}`);
16
+ return { text: redact(sections.join("\n\n")), files: tokens };
17
+ }
18
+ async function readIfFile(path) {
19
+ try {
20
+ if (!(await stat(path)).isFile())
21
+ return undefined;
22
+ return (await readFile(path, "utf8")).slice(0, MAX_FILE_CHARS).trim();
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ export const BASE_QUESTIONS = [
2
+ "interview.q.what",
3
+ "interview.q.problem",
4
+ "interview.q.scope",
5
+ "interview.q.constraints",
6
+ "interview.q.team",
7
+ "interview.q.rules",
8
+ ];
9
+ export const OBJECTIVES = ["feature", "refactor", "migration", "bugs", "docs", "skip"];
10
+ export const OBJECTIVE_LABELS = {
11
+ feature: "objective.feature",
12
+ refactor: "objective.refactor",
13
+ migration: "objective.migration",
14
+ bugs: "objective.bugs",
15
+ docs: "objective.docs",
16
+ skip: "interview.skip",
17
+ };
18
+ export const MAX_FOLLOW_UPS = 5;
@@ -0,0 +1,19 @@
1
+ import { t } from "../i18n/index.js";
2
+ export function renderInterview(data) {
3
+ const header = [`# ${t("md.interview")}`, "", `- ${t("md.mode")}: ${data.mode}`];
4
+ if (data.objective)
5
+ header.push(`- ${t("md.objective")}: ${data.objective}`);
6
+ const sections = [header.join("\n")];
7
+ if (data.brief)
8
+ sections.push(`## ${t("md.brief")}\n\n${data.brief}`);
9
+ sections.push(...data.answers.map(renderAnswer("##")));
10
+ if (data.followUps.length > 0) {
11
+ sections.push(`## ${t("md.followUps")}`, ...data.followUps.map(renderAnswer("###")));
12
+ }
13
+ if (data.summary)
14
+ sections.push(`## ${t("md.summary")}\n\n${data.summary}`);
15
+ return `${sections.join("\n\n")}\n`;
16
+ }
17
+ function renderAnswer(level) {
18
+ return ({ question, answer }) => `${level} ${question}\n\n${answer.trim() || `_${t("md.skipped")}_`}`;
19
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { FormatError } from "../core/errors.js";
3
+ const replySchema = z.discriminatedUnion("done", [
4
+ z.object({
5
+ done: z.literal(false),
6
+ question: z.string().min(1),
7
+ why: z.string().optional(),
8
+ options: z.array(z.string().min(1)).optional(),
9
+ }),
10
+ z.object({ done: z.literal(true), summary: z.string().min(1) }),
11
+ ]);
12
+ export const INTERVIEW_FORMAT = [
13
+ "JSON only, one of:",
14
+ '{"done": false, "question": "...", "why": "one line on what this unblocks", "options": ["...", "..."]}',
15
+ '{"done": true, "summary": "3-6 lines restating goal, users, scope, constraints and success criteria"}',
16
+ ].join("\n");
17
+ export function parseInterviewReply(text) {
18
+ const result = replySchema.safeParse(extractJson(text));
19
+ if (!result.success)
20
+ throw new FormatError(z.prettifyError(result.error));
21
+ return result.data;
22
+ }
23
+ function extractJson(text) {
24
+ const start = text.indexOf("{");
25
+ const end = text.lastIndexOf("}");
26
+ if (start === -1 || end < start)
27
+ throw new FormatError("no JSON object found");
28
+ try {
29
+ return JSON.parse(text.slice(start, end + 1));
30
+ }
31
+ catch (error) {
32
+ throw new FormatError(`invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
33
+ }
34
+ }
@@ -0,0 +1,151 @@
1
+ import { CLI, isAgentBackend } from "../commands/shared.js";
2
+ import { UserError } from "../core/errors.js";
3
+ import { renderPrompt } from "../core/prompt-loader.js";
4
+ import { displayPath } from "../core/state.js";
5
+ import { capturedPrompt, clearActive, markActive, saveCapture, } from "../gates/capture.js";
6
+ import { enforceContract } from "../gates/enforce.js";
7
+ import { runGate } from "../gates/gate.js";
8
+ import { guardState } from "../gates/state-guard.js";
9
+ import { t } from "../i18n/index.js";
10
+ import { capturedTask } from "../review/run.js";
11
+ import { attemptsLeft, MAX_ATTEMPTS, readAttempts, recordAttempt, } from "../tasks/attempts.js";
12
+ import { MAX_LOG_LINES } from "../tasks/handoff.js";
13
+ import { learnFromFailure } from "../tasks/learn.js";
14
+ import { runDir, writeRunLog } from "../tasks/runs.js";
15
+ import { setTaskStatus } from "../tasks/status.js";
16
+ const REVIEW_FAILURES_FOR_LESSON = 2;
17
+ export async function attemptOnce(ctx, run, prompt) {
18
+ const { capture } = run;
19
+ const backend = ctx.createBackend(isAgentBackend(capture.config.backend) ? capture.config.backend : "manual");
20
+ ctx.prompter.info(t("next.launching", { backend: backend.name }));
21
+ const startedAt = new Date();
22
+ const launched = await launch(ctx, run, startedAt, false, () => backend.run(prompt, { cwd: ctx.cwd, interactive: true }));
23
+ if (!launched.ok)
24
+ return notDone(ctx, capture);
25
+ const result = await gate(ctx, run);
26
+ await writeRunLog(ctx.cwd, capture.id, result.report);
27
+ const outcome = result.passed ? "done" : result.stage === "refused" ? "blocked" : "failed";
28
+ await record(ctx, run, startedAt, false, { outcome, gate: result });
29
+ if (result.passed)
30
+ return complete(ctx, capture);
31
+ if (outcome === "blocked")
32
+ return block(ctx, capture, result.reason ?? "");
33
+ await learnFromReviews(ctx, capture, result);
34
+ return notDone(ctx, capture);
35
+ }
36
+ export async function headlessLoop(ctx, run, first) {
37
+ const { capture } = run;
38
+ const backend = ctx.createBackend(capture.config.backend);
39
+ let left = await attemptsLeft(ctx.cwd, capture.id);
40
+ if (left === 0) {
41
+ const reason = t("next.budgetUsed", { max: MAX_ATTEMPTS });
42
+ await record(ctx, run, new Date(), true, { outcome: "blocked", reason });
43
+ return block(ctx, capture, reason);
44
+ }
45
+ let prompt = first;
46
+ let attempt = MAX_ATTEMPTS - left;
47
+ const timeoutMs = capture.config.agent.timeoutMinutes * 60_000;
48
+ while (left > 0) {
49
+ attempt++;
50
+ left--;
51
+ ctx.prompter.info(t("next.attempt", { attempt, max: MAX_ATTEMPTS, backend: backend.name }));
52
+ const startedAt = new Date();
53
+ const launched = await launch(ctx, run, startedAt, true, () => backend.run(prompt, { cwd: ctx.cwd, access: "edit", stream: ctx.print, timeoutMs }), left === 0);
54
+ if (!launched.ok) {
55
+ return left === 0 ? block(ctx, capture, launched.reason) : notDone(ctx, capture);
56
+ }
57
+ const result = await gate(ctx, run);
58
+ await writeRunLog(ctx.cwd, capture.id, `# Attempt ${attempt}\n\n${result.report}`);
59
+ const blocked = !result.passed && (result.stage === "refused" || (result.retryable && left === 0));
60
+ const outcome = result.passed ? "done" : blocked ? "blocked" : "failed";
61
+ await record(ctx, run, startedAt, true, { outcome, gate: result });
62
+ if (result.passed)
63
+ return complete(ctx, capture);
64
+ if (blocked)
65
+ return block(ctx, capture, result.reason ?? "");
66
+ if (!result.retryable)
67
+ return notDone(ctx, capture);
68
+ await learnFromReviews(ctx, capture, result);
69
+ prompt = renderPrompt(capturedPrompt(capture, "retry"), {
70
+ task: capture.task,
71
+ failure: result.report,
72
+ attempt: String(attempt),
73
+ task_path: capture.path,
74
+ max_log_lines: String(MAX_LOG_LINES),
75
+ });
76
+ }
77
+ return false;
78
+ }
79
+ async function launch(ctx, run, startedAt, headless, go, final = false) {
80
+ await markActive(ctx.cwd, run.capture.id);
81
+ const guard = await guardState(ctx.cwd);
82
+ let failure;
83
+ let failed = false;
84
+ try {
85
+ await go();
86
+ }
87
+ catch (error) {
88
+ failure = error;
89
+ failed = true;
90
+ }
91
+ run.tampered.push(...(await guard.verify()));
92
+ await setTaskStatus(ctx.cwd, capturedTask(run.capture), "in_progress");
93
+ if (!failed)
94
+ return { ok: true };
95
+ await enforceContract(ctx, run.capture, run.acceptance);
96
+ await clearActive(ctx.cwd);
97
+ const reason = failure instanceof Error ? failure.message : String(failure);
98
+ const outcome = final ? "blocked" : "failed";
99
+ await record(ctx, run, startedAt, headless, { outcome, stage: "agent", reason });
100
+ if (!(failure instanceof UserError))
101
+ throw failure;
102
+ ctx.prompter.warn(reason);
103
+ return { ok: false, reason };
104
+ }
105
+ async function gate(ctx, run) {
106
+ const result = await runGate(ctx, run);
107
+ await clearActive(ctx.cwd);
108
+ return result;
109
+ }
110
+ async function record(ctx, run, startedAt, headless, result) {
111
+ const stage = result.gate?.stage ?? result.stage;
112
+ const reason = result.gate?.reason ?? result.reason;
113
+ const skips = result.gate?.skips ?? [];
114
+ const accepted = run.acceptance.accepted.flatMap((finding) => (finding.id ? [finding.id] : []));
115
+ await recordAttempt(ctx.cwd, run.capture.id, {
116
+ startedAt: startedAt.toISOString(),
117
+ durationMs: Date.now() - startedAt.getTime(),
118
+ headless,
119
+ outcome: result.outcome,
120
+ ...(stage ? { stage } : {}),
121
+ regressions: result.gate?.regressions ?? [],
122
+ ...(reason ? { reason } : {}),
123
+ ...(skips.length > 0 ? { skips } : {}),
124
+ ...(accepted.length > 0 ? { accepted } : {}),
125
+ });
126
+ }
127
+ async function block(ctx, capture, reason) {
128
+ await setTaskStatus(ctx.cwd, capturedTask(capture), "blocked");
129
+ await learnFromFailure(ctx, capture, "blocked");
130
+ const path = displayPath(runDir(ctx.cwd, capture.id));
131
+ ctx.prompter.outro(t("next.blocked", { id: capture.id, path, reason }));
132
+ return false;
133
+ }
134
+ function notDone(ctx, capture) {
135
+ ctx.prompter.outro(t("next.notDone", { id: capture.id, command: `${CLI} next` }));
136
+ return false;
137
+ }
138
+ async function learnFromReviews(ctx, capture, result) {
139
+ if (result.stage !== "review")
140
+ return;
141
+ const failures = (await readAttempts(ctx.cwd, capture.id)).filter((attempt) => attempt.stage === "review");
142
+ if (failures.length >= REVIEW_FAILURES_FOR_LESSON) {
143
+ await learnFromFailure(ctx, capture, "review");
144
+ }
145
+ }
146
+ async function complete(ctx, capture) {
147
+ await setTaskStatus(ctx.cwd, capturedTask(capture), "done");
148
+ await saveCapture(ctx.cwd, { ...capture, finished: true });
149
+ ctx.prompter.outro(t("next.done", { id: capture.id, command: `${CLI} next` }));
150
+ return true;
151
+ }