atlas-workflow 0.9.1 → 0.9.3

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 (169) hide show
  1. package/README.md +5 -2
  2. package/VERSION +1 -1
  3. package/build/bump-version.mjs +6 -21
  4. package/build/cli/atlas-init.mjs +92 -5
  5. package/build/tests/classify-findings.test.mjs +20 -0
  6. package/build/tests/etapa3.test.mjs +161 -0
  7. package/build/tests/test_classify_findings.py +79 -0
  8. package/hosts/opencode/.opencode/agents/atlas-findings-repair.md +4 -0
  9. package/hosts/opencode/.opencode/agents/atlas-task-validator.md +18 -1
  10. package/hosts/opencode/.opencode/atlas/VERSION +1 -1
  11. package/hosts/opencode/.opencode/atlas/orchestrator/README.md +7 -5
  12. package/hosts/opencode/.opencode/atlas/orchestrator/commands/workflow.md +1 -1
  13. package/hosts/opencode/.opencode/atlas/orchestrator/references/host-adapters.md +13 -12
  14. package/hosts/opencode/.opencode/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
  15. package/hosts/opencode/.opencode/atlas/packages/mcp-server/README.md +1 -1
  16. package/hosts/opencode/.opencode/atlas/packages/mcp-server/package.json +1 -1
  17. package/hosts/opencode/.opencode/atlas/packages/mcp-server/server.js +514 -20
  18. package/hosts/opencode/.opencode/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  19. package/hosts/opencode/.opencode/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
  20. package/hosts/opencode/.opencode/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
  21. package/hosts/opencode/.opencode/skills/_shared/references/stack-profiles.md +36 -0
  22. package/hosts/opencode/.opencode/skills/_shared/scripts/document_quality.mjs +252 -0
  23. package/hosts/opencode/.opencode/skills/atlas-backlog-generator/SKILL.md +7 -2
  24. package/hosts/opencode/.opencode/skills/atlas-direct-execute/SKILL.md +6 -2
  25. package/hosts/opencode/.opencode/skills/atlas-findings-repair/SKILL.md +11 -1
  26. package/hosts/opencode/.opencode/skills/atlas-plan-execute/SKILL.md +16 -2
  27. package/hosts/opencode/.opencode/skills/atlas-plan-handoff/SKILL.md +6 -4
  28. package/hosts/opencode/.opencode/skills/atlas-prd-interview/SKILL.md +7 -2
  29. package/hosts/opencode/.opencode/skills/atlas-slice-review/SKILL.md +37 -2
  30. package/hosts/opencode/.opencode/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  31. package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  32. package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  33. package/hosts/opencode/.opencode/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  34. package/hosts/opencode/.opencode/skills/atlas-task-validator/SKILL.md +29 -14
  35. package/hosts/opencode/.opencode/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
  36. package/hosts/pi/.pi/agents/atlas-direct-execute.md +6 -2
  37. package/hosts/pi/.pi/agents/atlas-findings-repair.md +15 -1
  38. package/hosts/pi/.pi/agents/atlas-plan-execute.md +16 -2
  39. package/hosts/pi/.pi/agents/atlas-slice-review.md +37 -2
  40. package/hosts/pi/.pi/agents/atlas-task-validator.md +18 -1
  41. package/hosts/pi/atlas/VERSION +1 -1
  42. package/hosts/pi/atlas/orchestrator/README.md +7 -5
  43. package/hosts/pi/atlas/orchestrator/commands/workflow.md +1 -1
  44. package/hosts/pi/atlas/orchestrator/references/host-adapters.md +13 -12
  45. package/hosts/pi/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
  46. package/hosts/pi/atlas/packages/mcp-server/README.md +1 -1
  47. package/hosts/pi/atlas/packages/mcp-server/package.json +1 -1
  48. package/hosts/pi/atlas/packages/mcp-server/server.js +514 -20
  49. package/hosts/pi/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  50. package/hosts/pi/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
  51. package/hosts/pi/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
  52. package/hosts/pi/skills/_shared/references/stack-profiles.md +36 -0
  53. package/hosts/pi/skills/_shared/scripts/document_quality.mjs +252 -0
  54. package/hosts/pi/skills/atlas-backlog-generator/SKILL.md +7 -2
  55. package/hosts/pi/skills/atlas-direct-execute/SKILL.md +6 -2
  56. package/hosts/pi/skills/atlas-findings-repair/SKILL.md +11 -1
  57. package/hosts/pi/skills/atlas-plan-execute/SKILL.md +16 -2
  58. package/hosts/pi/skills/atlas-plan-handoff/SKILL.md +6 -4
  59. package/hosts/pi/skills/atlas-prd-interview/SKILL.md +7 -2
  60. package/hosts/pi/skills/atlas-slice-review/SKILL.md +37 -2
  61. package/hosts/pi/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  62. package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  63. package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  64. package/hosts/pi/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  65. package/hosts/pi/skills/atlas-task-validator/SKILL.md +29 -14
  66. package/hosts/pi/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
  67. package/hosts/zcode/.zcode-plugin/plugin.json +27 -0
  68. package/hosts/zcode/agents/atlas-direct-execute.md +31 -0
  69. package/hosts/zcode/agents/atlas-findings-repair.md +39 -0
  70. package/hosts/zcode/agents/atlas-plan-execute.md +33 -0
  71. package/hosts/zcode/agents/atlas-slice-review.md +27 -0
  72. package/hosts/zcode/agents/atlas-task-validator.md +138 -0
  73. package/hosts/zcode/packages/mcp-server/README.md +29 -0
  74. package/hosts/zcode/packages/mcp-server/VERSION +1 -0
  75. package/hosts/zcode/packages/mcp-server/package.json +15 -0
  76. package/hosts/zcode/packages/mcp-server/server.js +3897 -0
  77. package/hosts/zcode/packages/orchestrator/README.md +270 -0
  78. package/hosts/zcode/packages/orchestrator/commands/workflow.md +37 -0
  79. package/hosts/zcode/packages/orchestrator/defaults/paths.md +21 -0
  80. package/hosts/zcode/packages/orchestrator/references/host-adapters.md +106 -0
  81. package/hosts/zcode/packages/orchestrator/references/qa_s13_matrix.md +141 -0
  82. package/hosts/zcode/packages/orchestrator/references/subagent_dispatch.md +42 -0
  83. package/hosts/zcode/packages/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +391 -0
  84. package/hosts/zcode/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +855 -0
  85. package/hosts/zcode/packages/templates/BOUNDARY_PRD_PLAN.md +93 -0
  86. package/hosts/zcode/packages/templates/PERGUNTAS_EM_ABERTO_TEMPLATE.md +139 -0
  87. package/hosts/zcode/packages/templates/PLAN_TEMPLATE.md +146 -0
  88. package/hosts/zcode/packages/templates/PRD_TEMPLATE.md +150 -0
  89. package/hosts/zcode/packages/templates/STATE_FILE_SCHEMA.md +56 -0
  90. package/hosts/zcode/skills/_shared/references/stack-profiles.md +36 -0
  91. package/hosts/zcode/skills/_shared/scripts/document_quality.mjs +252 -0
  92. package/hosts/zcode/skills/atlas-backlog-generator/SKILL.md +93 -0
  93. package/hosts/zcode/skills/atlas-backlog-generator/agents/openai.yaml +4 -0
  94. package/hosts/zcode/skills/atlas-direct-execute/SKILL.md +221 -0
  95. package/hosts/zcode/skills/atlas-direct-execute/agents/openai.yaml +7 -0
  96. package/hosts/zcode/skills/atlas-findings-repair/SKILL.md +158 -0
  97. package/hosts/zcode/skills/atlas-findings-repair/agents/openai.yaml +7 -0
  98. package/hosts/zcode/skills/atlas-plan-execute/SKILL.md +175 -0
  99. package/hosts/zcode/skills/atlas-plan-execute/agents/openai.yaml +7 -0
  100. package/hosts/zcode/skills/atlas-plan-execute/references/plan-contract.md +88 -0
  101. package/hosts/zcode/skills/atlas-plan-execute/references/quality-gates.md +60 -0
  102. package/hosts/zcode/skills/atlas-plan-execute/scripts/check_budget_state.py +96 -0
  103. package/hosts/zcode/skills/atlas-plan-execute/scripts/extract_plan_contract.py +191 -0
  104. package/hosts/zcode/skills/atlas-plan-execute/scripts/validate_gate_result.py +56 -0
  105. package/hosts/zcode/skills/atlas-plan-handoff/SKILL.md +183 -0
  106. package/hosts/zcode/skills/atlas-plan-handoff/agents/openai.yaml +7 -0
  107. package/hosts/zcode/skills/atlas-prd-interview/SKILL.md +82 -0
  108. package/hosts/zcode/skills/atlas-prd-interview/agents/openai.yaml +7 -0
  109. package/hosts/zcode/skills/atlas-slice-review/SKILL.md +156 -0
  110. package/hosts/zcode/skills/atlas-slice-review/agents/openai.yaml +4 -0
  111. package/hosts/zcode/skills/atlas-slice-review/references/review-contract.md +58 -0
  112. package/hosts/zcode/skills/atlas-slice-review/references/scenario-lenses.md +57 -0
  113. package/hosts/zcode/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  114. package/hosts/zcode/skills/atlas-slice-review/scripts/classify_findings.py +24 -0
  115. package/hosts/zcode/skills/atlas-slice-review/scripts/extract_review_slice.py +158 -0
  116. package/hosts/zcode/skills/atlas-sprint-prd-generator/SKILL.md +77 -0
  117. package/hosts/zcode/skills/atlas-sprint-prd-generator/agents/openai.yaml +7 -0
  118. package/hosts/zcode/skills/atlas-task-validator/SKILL.md +173 -0
  119. package/hosts/zcode/skills/atlas-task-validator/agents/openai.yaml +7 -0
  120. package/hosts/zcode/skills/atlas-workflow-orchestrator/SKILL.md +391 -0
  121. package/package.json +1 -1
  122. package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-findings-repair.toml +1 -1
  123. package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-task-validator.toml +1 -1
  124. package/plugins/atlas-workflow-orchestrator/.codex-plugin/plugin.json +1 -1
  125. package/plugins/atlas-workflow-orchestrator/VERSION +1 -1
  126. package/plugins/atlas-workflow-orchestrator/agents/atlas-findings-repair.md +4 -0
  127. package/plugins/atlas-workflow-orchestrator/agents/atlas-task-validator.md +18 -1
  128. package/plugins/atlas-workflow-orchestrator/orchestrator/README.md +7 -5
  129. package/plugins/atlas-workflow-orchestrator/orchestrator/commands/workflow.md +1 -1
  130. package/plugins/atlas-workflow-orchestrator/orchestrator/references/host-adapters.md +13 -12
  131. package/plugins/atlas-workflow-orchestrator/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
  132. package/plugins/atlas-workflow-orchestrator/packages/mcp-server/README.md +1 -1
  133. package/plugins/atlas-workflow-orchestrator/packages/mcp-server/package.json +1 -1
  134. package/plugins/atlas-workflow-orchestrator/packages/mcp-server/server.js +514 -20
  135. package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/references/stack-profiles.md +36 -0
  136. package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/scripts/document_quality.mjs +252 -0
  137. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-backlog-generator/SKILL.md +7 -2
  138. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-direct-execute/SKILL.md +6 -2
  139. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-findings-repair/SKILL.md +11 -1
  140. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-execute/SKILL.md +16 -2
  141. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-handoff/SKILL.md +6 -4
  142. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-prd-interview/SKILL.md +7 -2
  143. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/SKILL.md +37 -2
  144. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  145. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  146. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  147. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  148. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-task-validator/SKILL.md +29 -14
  149. package/plugins/atlas-workflow-orchestrator/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  150. package/plugins/atlas-workflow-orchestrator/packages/templates/PRD_TEMPLATE.md +2 -1
  151. package/plugins/atlas-workflow-orchestrator/packages/templates/STATE_FILE_SCHEMA.md +25 -1
  152. package/plugins/atlas-workflow-orchestrator/skills/_shared/references/stack-profiles.md +36 -0
  153. package/plugins/atlas-workflow-orchestrator/skills/_shared/scripts/document_quality.mjs +252 -0
  154. package/plugins/atlas-workflow-orchestrator/skills/atlas-backlog-generator/SKILL.md +7 -2
  155. package/plugins/atlas-workflow-orchestrator/skills/atlas-direct-execute/SKILL.md +6 -2
  156. package/plugins/atlas-workflow-orchestrator/skills/atlas-findings-repair/SKILL.md +11 -1
  157. package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-execute/SKILL.md +16 -2
  158. package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-handoff/SKILL.md +6 -4
  159. package/plugins/atlas-workflow-orchestrator/skills/atlas-prd-interview/SKILL.md +7 -2
  160. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/SKILL.md +37 -2
  161. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  162. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  163. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  164. package/plugins/atlas-workflow-orchestrator/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  165. package/plugins/atlas-workflow-orchestrator/skills/atlas-task-validator/SKILL.md +29 -14
  166. package/plugins/atlas-workflow-orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
  167. package/plugins/atlas-workflow-orchestrator/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  168. package/plugins/atlas-workflow-orchestrator/templates/PRD_TEMPLATE.md +2 -1
  169. package/plugins/atlas-workflow-orchestrator/templates/STATE_FILE_SCHEMA.md +25 -1
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import crypto from 'node:crypto';
5
5
  import process from 'node:process';
6
+ import { execFileSync } from 'node:child_process';
6
7
  import { fileURLToPath } from 'node:url';
7
8
 
8
9
  const SERVER_NAME = 'atlas-workflow-orchestrator';
@@ -62,6 +63,7 @@ const WORKFLOW_CONFIG = {
62
63
  prd_interview: 'atlas-prd-interview',
63
64
  plan_handoff: 'atlas-plan-handoff',
64
65
  plan_execute: 'atlas-plan-execute',
66
+ direct_execute: 'atlas-direct-execute',
65
67
  findings_repair: 'atlas-findings-repair',
66
68
  slice_review: 'atlas-slice-review',
67
69
  task_validator: 'atlas-task-validator',
@@ -110,10 +112,19 @@ const MODE_GUARANTEE_LEVEL = {
110
112
  direct: 'full_pipeline',
111
113
  execute: 'full_pipeline',
112
114
  };
115
+ const MODE_EXECUTOR_SKILL = {
116
+ full: WORKFLOW_CONFIG.skills.plan_execute,
117
+ direct: WORKFLOW_CONFIG.skills.direct_execute,
118
+ execute: WORKFLOW_CONFIG.skills.plan_execute,
119
+ };
113
120
  function guaranteeLevelForMode(mode) {
114
121
  return MODE_GUARANTEE_LEVEL[mode] ?? null;
115
122
  }
116
123
 
124
+ function expectedExecutorSkill(mode) {
125
+ return MODE_EXECUTOR_SKILL[mode] ?? null;
126
+ }
127
+
117
128
  // Banco canônico de templates de banner de fase (PRD §4 Fluxos / D*, PLAN §6.2).
118
129
  // Fonte única na camada determinística: o orquestrador apenas ECOA a string
119
130
  // pronta — nunca monta texto livre. Data-driven como HOST_ADAPTERS: tabela única
@@ -164,7 +175,7 @@ function renderBanner(event, slots = {}) {
164
175
  // Skills consultam atlas_capabilities e usam o descritor retornado em vez de
165
176
  // hardcodar nome de host. Adicionar host novo = adicionar entrada aqui.
166
177
  // Contrato HostAdapter (DEC-007): entrada runtime data-driven. Campos:
167
- // subagent_dispatch, todo_tool, hooks, capabilities_flags. plan_paths/state são
178
+ // subagent_dispatch, question_prompt, todo_tool, hooks, capabilities_flags. plan_paths/state são
168
179
  // portáveis (iguais a todos os hosts) e vivem em capabilities(). Adicionar host =
169
180
  // adicionar entrada aqui; nenhum ramo `if host==` em outro lugar.
170
181
  // capabilities_flags: pré-requisitos essenciais (subagent_available, mcp_available)
@@ -185,6 +196,7 @@ const HOST_ADAPTERS = {
185
196
  mechanism: 'Agent(subagent_type) bloqueante por design do host',
186
197
  },
187
198
  },
199
+ question_prompt: { mechanism: 'AskUserQuestion', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
188
200
  todo_tool: 'TodoWrite',
189
201
  hooks: { supported: true, mechanism: 'hooks/claude/settings.snippet.json' },
190
202
  capabilities_flags: { subagent_available: true, mcp_available: true, todo_available: true },
@@ -207,6 +219,7 @@ const HOST_ADAPTERS = {
207
219
  mechanism: 'spawn_agent bloqueante; retorno via state_path + veredito; no Codex deve usar explicitamente agent_type="atlas-task-validator"',
208
220
  },
209
221
  },
222
+ question_prompt: { mechanism: 'request_user_input', mode: 'structured', max_questions: 3, options_per_question: 3, persistence: 'prd_after_each_round' },
210
223
  todo_tool: 'tasks',
211
224
  hooks: { supported: false, mechanism: null },
212
225
  // Codex subagents are native, but spawned agents do not receive spawn_agent in
@@ -229,6 +242,7 @@ const HOST_ADAPTERS = {
229
242
  mechanism: '@<name> bloqueante presumido',
230
243
  },
231
244
  },
245
+ question_prompt: { mechanism: 'question', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
232
246
  // opencode expõe `todowrite` nativo ao agente primário (orquestrador). O `todoread`
233
247
  // foi fundido em `todowrite` (mar/2026): a tool retorna a lista atual no output.
234
248
  // Subagentes têm `todowrite` desabilitado por padrão, mas o todo é usado pelo
@@ -255,6 +269,7 @@ const HOST_ADAPTERS = {
255
269
  mechanism: 'subagent({agent,task}) via pi-subagents; join depende de dep externa',
256
270
  },
257
271
  },
272
+ question_prompt: { mechanism: 'interactive_prompt', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
258
273
  todo_tool: null,
259
274
  hooks: { supported: false, mechanism: null },
260
275
  // pi exige 2 deps externas obrigatórias (DEC-005): pi-mcp-adapter (MCP) e
@@ -268,22 +283,81 @@ const HOST_ADAPTERS = {
268
283
  },
269
284
  antigravity: {
270
285
  label: 'Antigravity',
286
+ // Antigravity não tem skill loader nativo em subagentes — o SKILL.md completo
287
+ // DEVE ser embutido no Prompt de cada invoke_subagent (via define_subagent como
288
+ // system_prompt ou diretamente no Prompt). Nunca despachar subagente sem SKILL.md
289
+ // injetado, pois o subagente não carregará o contrato e o pipeline vai impasse.
290
+ //
291
+ // Fluxo para fases de execução/validação (executor, validator, repair, review):
292
+ // 1. define_subagent(name: "<atlas-exec>", system_prompt: "<SKILL.MD completo>")
293
+ // 2. invoke_subagent(Subagents: [{TypeName: "<atlas-exec>", Role: "<papel>",
294
+ // Prompt: "<state_path ou plan_path>",
295
+ // Workspace: "branch"}])
296
+ // — invoke_subagent é BLOQUEANTE por design: não polling, não background.
297
+ // — Workspace: "branch" garante isolamento de contexto (fronteira G4/G9).
298
+ //
299
+ // Fases documentais (PRD, entrevista, plano) NÃO usam subagente — o orquestrador
300
+ // conduz no fio principal; define_subagent não é chamado para essas fases.
271
301
  subagent_dispatch: {
272
- mechanism: 'define_subagent(name, system_prompt) + invoke_subagent(Subagents)',
273
- example: 'define_subagent(name: "atlas-task-validator", system_prompt: "<SKILL_MD>") e invoke_subagent(Subagents: [{TypeName: "atlas-task-validator", Role: "Validator", Prompt: "<state_path>"}])',
274
- registration: 'Mapeamento de skills e agents via define_subagent dinâmico',
302
+ mechanism: 'define_subagent(name, system_prompt) + invoke_subagent(Subagents: [{TypeName, Role, Prompt, Workspace}])',
303
+ example: 'define_subagent(name: "atlas-task-validator", system_prompt: "<SKILL.MD completo do atlas-task-validator>") seguido de invoke_subagent(Subagents: [{TypeName: "atlas-task-validator", Role: "Validador frio", Prompt: "<state_path>", Workspace: "branch"}])',
304
+ registration: 'define_subagent dinâmico por sessão o SKILL.md canônico é passado como system_prompt; sem pré-registro persistente',
305
+ // Sem loader nativo: o SKILL.md DEVE ser embutido no system_prompt do define_subagent.
306
+ // Não usar TypeName: "self" sem injetar o SKILL.md — o subagente herdaria o contexto
307
+ // do orquestrador e violaria o isolamento frio (G4/G9).
308
+ skill_loading: 'embed_in_system_prompt',
275
309
  },
276
310
  validator_dispatch: {
277
311
  dispatcher: 'orchestrator',
278
312
  join: {
279
313
  sync: 'self_evident',
280
314
  confidence: 'high',
281
- mechanism: 'invoke_subagent bloqueante por design do host',
315
+ mechanism: 'invoke_subagent bloqueante por design do host — sem polling, sem callback',
282
316
  },
283
317
  },
318
+ // question_prompt: usado pela atlas-prd-interview para fazer perguntas ao usuário.
319
+ // No Antigravity, usar ask_question (ferramenta nativa de perguntas interativas).
320
+ // IMPORTANTE — resume_after_interview: após receber respostas via ask_question,
321
+ // persistir no PRD e RETOMAR O PIPELINE IMEDIATAMENTE sem nova confirmação.
322
+ // Nunca aguardar input adicional do usuário entre fases — viola fire-and-continue.
323
+ question_prompt: {
324
+ mechanism: 'ask_question',
325
+ mode: 'structured',
326
+ max_questions: 4,
327
+ options_per_question: 3,
328
+ persistence: 'prd_after_each_round',
329
+ resume_after_interview: 'automatic',
330
+ },
284
331
  todo_tool: null,
285
332
  hooks: { supported: false, mechanism: null },
286
333
  capabilities_flags: { subagent_available: true, mcp_available: true, todo_available: false },
334
+ // self_evident: MCP nativo + invoke_subagent bloqueante provados pelo boot do host.
335
+ // Não exige host_capabilities report (igual claude/codex/opencode).
336
+ prereq_policy: 'self_evident',
337
+ },
338
+ zcode: {
339
+ label: 'ZCode',
340
+ subagent_dispatch: {
341
+ // ZCode roda no Claude Agent SDK: Agent(subagent_type) nativo e bloqueante.
342
+ // Skills/agents do plugin vivem no bundle (.zcode-plugin) carregado pelo host.
343
+ mechanism: 'Agent(subagent_type)',
344
+ example: 'Agent(subagent_type: "atlas-task-validator", prompt: "<state_path>")',
345
+ registration: 'agents/<name>.md na raiz do plugin (descoberto via .zcode-plugin/plugin.json)',
346
+ },
347
+ validator_dispatch: {
348
+ dispatcher: 'orchestrator',
349
+ join: {
350
+ sync: 'self_evident',
351
+ confidence: 'presumed',
352
+ mechanism: 'Agent(subagent_type) bloqueante por design do host (Claude Agent SDK)',
353
+ },
354
+ },
355
+ question_prompt: { mechanism: 'AskUserQuestion', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
356
+ todo_tool: 'TodoWrite',
357
+ hooks: { supported: true, mechanism: '.zcode-plugin/plugin.json (hooks)' },
358
+ // ZCode é clone estrutural do Claude Code (Claude Agent SDK): subagente +
359
+ // MCP-local + TodoWrite nativos. Perfil self_evident — passa PREREQ/JOIN sem report.
360
+ capabilities_flags: { subagent_available: true, mcp_available: true, todo_available: true },
287
361
  },
288
362
  generic: {
289
363
  label: 'Host genérico',
@@ -300,6 +374,7 @@ const HOST_ADAPTERS = {
300
374
  mechanism: 'indeterminado; host deve reportar',
301
375
  },
302
376
  },
377
+ question_prompt: { mechanism: 'native_structured_question', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
303
378
  todo_tool: null,
304
379
  hooks: { supported: false, mechanism: null },
305
380
  // generic EXIGE subagente+MCP do host (DEC-004); host MCP-only sem subagente
@@ -350,6 +425,10 @@ const HOST_NAMES = Object.keys(HOST_ADAPTERS);
350
425
  const HOST_DETECTORS = [
351
426
  { via: 'env:CLAUDE_PLUGIN_ROOT', detect: (env) => (env.CLAUDE_PLUGIN_ROOT ? 'claude' : null) },
352
427
  { via: 'env:CODEX', detect: (env) => (env.CODEX_HOME || env.CODEX_PLUGIN_ROOT ? 'codex' : null) },
428
+ // ZCode (app Electron no Claude Agent SDK) injeta ZCODE_PLUGIN_ROOT ao spawnar o
429
+ // subprocesso MCP do plugin (comprovado no bundle zcode.cjs: interpolação análoga a
430
+ // CLAUDE_PLUGIN_ROOT). Sinal próprio e determinístico — precedência sobre ATLAS_HOST.
431
+ { via: 'env:ZCODE_PLUGIN_ROOT', detect: (env) => (env.ZCODE_PLUGIN_ROOT ? 'zcode' : null) },
353
432
  // opencode/pi não expõem env distintivo garantido no subprocesso MCP (S01).
354
433
  // Detecção determinística: o packaging injeta ATLAS_HOST no env do MCP —
355
434
  // opencode: opencode.json → mcp.<name>.environment.ATLAS_HOST = "opencode"
@@ -378,6 +457,7 @@ function capabilities(args = {}) {
378
457
  schema_version: CAPABILITIES_SCHEMA_VERSION,
379
458
  subagent_dispatch: adapter.subagent_dispatch,
380
459
  validator_dispatch: adapter.validator_dispatch,
460
+ question_prompt: adapter.question_prompt,
381
461
  todo_tool: adapter.todo_tool,
382
462
  hooks: adapter.hooks,
383
463
  capabilities_flags: adapter.capabilities_flags,
@@ -401,7 +481,7 @@ function capabilities(args = {}) {
401
481
  // do host com a disponibilidade real reportada pelo caller (`host_capabilities`).
402
482
  //
403
483
  // Política por host (`prereq_policy`):
404
- // - 'self_evident' (claude/codex/opencode, default): runtime nativo. Flag essencial
484
+ // - 'self_evident' (claude/codex/opencode/zcode, default): runtime nativo. Flag essencial
405
485
  // vem do report quando presente, senão do perfil (otimista justificado: MCP-vivo
406
486
  // prova-se no boot; subagente é nativo do host/plugin instalado).
407
487
  // - 'must_report' (pi/generic): essencial depende de dep externa (pi) ou de host
@@ -451,7 +531,7 @@ function checkPrerequisites(args = {}) {
451
531
 
452
532
  // Gate JOIN (DEC-SIB-003, SPEC_JOIN_CAPABILITY_S03 §3/§5). Espelha checkPrerequisites:
453
533
  // lê validator_dispatch.join do adapter e decide hard-fail por política.
454
- // - join.sync === 'self_evident' (claude/codex/opencode): host nativo conhecido;
534
+ // - join.sync === 'self_evident' (claude/codex/opencode/zcode): host nativo conhecido;
455
535
  // o runtime presume join disponível e NÃO exige report. confidence 'presumed'
456
536
  // (claude/opencode) passa, mas é registrado para observabilidade (smoke S13).
457
537
  // - join.sync === 'must_report' (pi/generic): fail-closed. Só passa se o caller
@@ -1695,6 +1775,7 @@ function preflight(args = {}) {
1695
1775
  ...(guaranteeLevel ? { guarantee_level: guaranteeLevel } : {}),
1696
1776
  routing: {
1697
1777
  mode,
1778
+ ...(expectedExecutorSkill(mode) ? { executor_skill: expectedExecutorSkill(mode) } : {}),
1698
1779
  ...(guaranteeLevel ? { guarantee_level: guaranteeLevel } : {}),
1699
1780
  skills: config.skills,
1700
1781
  version: version.version,
@@ -2238,8 +2319,7 @@ function pickValidatorChallenge(statePathValue, args, dispatchToken) {
2238
2319
 
2239
2320
  // Verifica o challenge_response no complete recomputando o hash do disco.
2240
2321
  // { ok: true } — sem challenge emitido OU hash confere
2241
- // { ok: true, unverifiable: true } — arquivo sumiu no complete (não bloqueia)
2242
- // { ok: false, reason } — resposta ausente ou hash divergente
2322
+ // { ok: false, reason } resposta ausente, arquivo ilegível ou hash divergente
2243
2323
  function verifyValidatorChallenge(challenge, response, args) {
2244
2324
  if (!challenge || typeof challenge.file !== 'string') return { ok: true };
2245
2325
  if (typeof response !== 'string' || response.trim() === '') {
@@ -2251,13 +2331,249 @@ function verifyValidatorChallenge(challenge, response, args) {
2251
2331
  .update(fs.readFileSync(resolveConsumerPath(challenge.file, args)))
2252
2332
  .digest('hex');
2253
2333
  } catch {
2254
- return { ok: true, unverifiable: true };
2334
+ return { ok: false, reason: 'challenge_file_unreadable' };
2255
2335
  }
2256
2336
  // Aceita hex puro ou saída de `shasum` (`<hash> <arquivo>`): primeiro token.
2257
2337
  const submitted = response.trim().toLowerCase().split(/\s+/)[0];
2258
2338
  return submitted === actual ? { ok: true } : { ok: false, reason: 'challenge_hash_divergente' };
2259
2339
  }
2260
2340
 
2341
+ const STATE_REQUIRED_FIELDS = [
2342
+ 'run_id', 'slice', 'tasks', 'files_changed', 'diff_stat', 'plan_path',
2343
+ 'boundary_refs', 'executed_at', 'executor_skill',
2344
+ ];
2345
+ const STATE_EXTENSION_ARRAYS = [
2346
+ 'obligations', 'invariants', 'scenario_probes', 'risk_probes',
2347
+ 'validation_map', 'task_evidence', 'worktree_baseline', 'worktree_final',
2348
+ ];
2349
+
2350
+ function gitOutput(root, gitArgs) {
2351
+ return execFileSync('git', ['-C', root, ...gitArgs], {
2352
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
2353
+ }).trim();
2354
+ }
2355
+
2356
+ function gitLines(root, gitArgs) {
2357
+ const output = gitOutput(root, gitArgs);
2358
+ return output ? output.split(/\r?\n/).filter(Boolean) : [];
2359
+ }
2360
+
2361
+ function stateEvidenceFiles(state) {
2362
+ const result = [];
2363
+ for (const item of [...(state.task_evidence ?? []), ...(state.repair_evidence ?? [])]) {
2364
+ if (!item || typeof item !== 'object') continue;
2365
+ for (const key of ['files', 'files_touched']) {
2366
+ if (Array.isArray(item[key])) result.push(...item[key]);
2367
+ }
2368
+ if (typeof item.file === 'string') result.push(item.file);
2369
+ }
2370
+ return [...new Set(result.filter((item) => typeof item === 'string' && item.trim()))].sort();
2371
+ }
2372
+
2373
+ function snapshotStatus(xy) {
2374
+ if (xy === '??') return 'A';
2375
+ for (const status of ['U', 'D', 'R', 'C', 'A', 'T', 'M']) {
2376
+ if (xy.includes(status)) return status;
2377
+ }
2378
+ return 'M';
2379
+ }
2380
+
2381
+ function normalizeSnapshotPath(input) {
2382
+ if (typeof input !== 'string' || !input.trim()) throw new Error('path vazio');
2383
+ const normalized = path.posix.normalize(input.replaceAll('\\', '/'));
2384
+ if (path.posix.isAbsolute(normalized) || normalized === '..' || normalized.startsWith('../')) {
2385
+ throw new Error('path fora do projeto');
2386
+ }
2387
+ return normalized;
2388
+ }
2389
+
2390
+ function snapshotHash(root, rel) {
2391
+ try {
2392
+ const normalized = normalizeSnapshotPath(rel);
2393
+ const abs = path.resolve(root, normalized);
2394
+ if (abs !== root && !abs.startsWith(`${root}${path.sep}`)) throw new Error('path fora do projeto');
2395
+ const stat = fs.lstatSync(abs);
2396
+ const content = stat.isSymbolicLink()
2397
+ ? Buffer.from(fs.readlinkSync(abs))
2398
+ : fs.readFileSync(abs);
2399
+ return crypto.createHash('sha256').update(content).digest('hex');
2400
+ } catch {
2401
+ return null;
2402
+ }
2403
+ }
2404
+
2405
+ function captureWorktreeSnapshot(root) {
2406
+ const raw = execFileSync('git', [
2407
+ '-C', root, 'status', '--porcelain=v1', '-z', '--untracked-files=all',
2408
+ ]);
2409
+ const records = raw.toString('utf8').split('\0').filter(Boolean);
2410
+ const snapshot = [];
2411
+ for (let index = 0; index < records.length; index += 1) {
2412
+ const record = records[index];
2413
+ const xy = record.slice(0, 2);
2414
+ const rel = normalizeSnapshotPath(record.slice(3));
2415
+ const status = snapshotStatus(xy);
2416
+ if (status === 'R' || status === 'C') {
2417
+ const previous = normalizeSnapshotPath(records[index + 1]);
2418
+ index += 1;
2419
+ if (!previous.startsWith('.atlas/state/')) {
2420
+ snapshot.push({ path: previous, status: 'D', sha256: null });
2421
+ }
2422
+ }
2423
+ if (!rel.startsWith('.atlas/state/')) {
2424
+ snapshot.push({ path: rel, status, sha256: status === 'D' ? null : snapshotHash(root, rel) });
2425
+ }
2426
+ }
2427
+ return snapshot.sort((a, b) => a.path.localeCompare(b.path) || a.status.localeCompare(b.status));
2428
+ }
2429
+
2430
+ function validateSnapshot(snapshot, field, violations) {
2431
+ if (!Array.isArray(snapshot)) return;
2432
+ const paths = new Set();
2433
+ const normalized = [];
2434
+ for (const [index, entry] of snapshot.entries()) {
2435
+ const label = `${field}[${index}]`;
2436
+ if (!entry || typeof entry !== 'object') {
2437
+ violations.push(`${label} deve ser objeto`);
2438
+ continue;
2439
+ }
2440
+ let rel;
2441
+ try {
2442
+ rel = normalizeSnapshotPath(entry.path);
2443
+ } catch {
2444
+ violations.push(`${label}.path inválido`);
2445
+ continue;
2446
+ }
2447
+ if (paths.has(rel)) violations.push(`${field} contém path duplicado: ${rel}`);
2448
+ paths.add(rel);
2449
+ if (!['A', 'M', 'D', 'R', 'C', 'T', 'U'].includes(entry.status)) {
2450
+ violations.push(`${label}.status inválido`);
2451
+ }
2452
+ if (entry.status === 'D') {
2453
+ if (entry.sha256 !== null) violations.push(`${label}.sha256 deve ser null para delete`);
2454
+ } else if (typeof entry.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
2455
+ violations.push(`${label}.sha256 inválido`);
2456
+ }
2457
+ normalized.push({ path: rel, status: entry.status, sha256: entry.sha256 });
2458
+ }
2459
+ const sorted = [...normalized].sort((a, b) => a.path.localeCompare(b.path) || a.status.localeCompare(b.status));
2460
+ if (JSON.stringify(normalized) !== JSON.stringify(sorted)) violations.push(`${field} deve estar ordenado por path/status`);
2461
+ }
2462
+
2463
+ function snapshotDeltaFiles(baseline, finalSnapshot) {
2464
+ const before = new Map(baseline.map((entry) => [entry.path, JSON.stringify(entry)]));
2465
+ const after = new Map(finalSnapshot.map((entry) => [entry.path, JSON.stringify(entry)]));
2466
+ return [...new Set([...before.keys(), ...after.keys()])]
2467
+ .filter((rel) => before.get(rel) !== after.get(rel))
2468
+ .sort();
2469
+ }
2470
+
2471
+ function validateStateBoundary(statePathValue, args = {}) {
2472
+ let state;
2473
+ try {
2474
+ state = JSON.parse(fs.readFileSync(resolveConsumerPath(statePathValue, args), 'utf8'));
2475
+ } catch (error) {
2476
+ return { ok: false, violations: [`state_path inválido: ${error.message}`] };
2477
+ }
2478
+ const violations = [];
2479
+ for (const field of STATE_REQUIRED_FIELDS) {
2480
+ if (state[field] === undefined || state[field] === null) violations.push(`campo obrigatório ausente: ${field}`);
2481
+ }
2482
+ for (const field of ['tasks', 'files_changed', 'boundary_refs']) {
2483
+ if (!Array.isArray(state[field])) violations.push(`${field} deve ser array`);
2484
+ }
2485
+ const isDirect = state.executor_skill === 'atlas-direct-execute';
2486
+ const hasExtension = state.contract_kind !== undefined;
2487
+ if (!hasExtension && state.executor_skill !== 'atlas-plan-execute') violations.push('schema legado permitido somente para atlas-plan-execute');
2488
+ if (isDirect && state.contract_kind !== 'direct') violations.push('atlas-direct-execute exige contract_kind=direct');
2489
+ if (hasExtension && state.executor_skill === 'atlas-plan-execute' && state.contract_kind !== 'plan') violations.push('atlas-plan-execute exige contract_kind=plan');
2490
+ if (hasExtension && !['plan', 'direct'].includes(state.contract_kind)) violations.push('contract_kind deve ser plan ou direct');
2491
+ if (hasExtension || isDirect) {
2492
+ for (const field of ['base_sha', 'head_sha']) {
2493
+ if (typeof state[field] !== 'string' || !state[field].trim()) violations.push(`${field} obrigatório na extensão`);
2494
+ }
2495
+ for (const field of STATE_EXTENSION_ARRAYS) {
2496
+ if (!Array.isArray(state[field])) violations.push(`${field} deve ser array na extensão`);
2497
+ }
2498
+ if (isDirect && Array.isArray(state.obligations) && state.obligations.length === 0) violations.push('direct exige obligations não vazio');
2499
+ validateSnapshot(state.worktree_baseline, 'worktree_baseline', violations);
2500
+ validateSnapshot(state.worktree_final, 'worktree_final', violations);
2501
+ }
2502
+ if (violations.length > 0 || !hasExtension) {
2503
+ return { ok: violations.length === 0, legacy: !hasExtension, state, violations };
2504
+ }
2505
+ const root = consumerRoot(args);
2506
+ try {
2507
+ gitOutput(root, ['rev-parse', '--verify', `${state.base_sha}^{commit}`]);
2508
+ gitOutput(root, ['rev-parse', '--verify', `${state.head_sha}^{commit}`]);
2509
+ const currentHead = gitOutput(root, ['rev-parse', 'HEAD']);
2510
+ if (currentHead !== state.head_sha) violations.push(`head_sha stale: state=${state.head_sha}, real=${currentHead}`);
2511
+ const committed = gitLines(root, ['diff', '--name-only', `${state.base_sha}...${state.head_sha}`]);
2512
+ const actualFinal = captureWorktreeSnapshot(root);
2513
+ if (JSON.stringify(actualFinal) !== JSON.stringify(state.worktree_final)) {
2514
+ violations.push('worktree_final stale: snapshot diverge do working tree atual');
2515
+ }
2516
+ const worktreeDelta = snapshotDeltaFiles(state.worktree_baseline, state.worktree_final);
2517
+ const claimedEvidence = stateEvidenceFiles(state);
2518
+ const expectedFiles = [...new Set([...committed, ...worktreeDelta])].sort();
2519
+ const declaredFiles = [...new Set((state.files_changed ?? []).filter((f) => typeof f === 'string'))].sort();
2520
+ if (JSON.stringify(expectedFiles) !== JSON.stringify(declaredFiles)) {
2521
+ violations.push(`files_changed diverge do boundary real: esperado=${JSON.stringify(expectedFiles)} recebido=${JSON.stringify(declaredFiles)}`);
2522
+ }
2523
+ if (JSON.stringify(expectedFiles) !== JSON.stringify(claimedEvidence)) {
2524
+ violations.push(`evidência diverge do boundary real: esperado=${JSON.stringify(expectedFiles)} recebido=${JSON.stringify(claimedEvidence)}`);
2525
+ }
2526
+ const statMatch = /^(\d+)\s+files?\b/.exec(String(state.diff_stat).trim());
2527
+ if (!statMatch || Number(statMatch[1]) !== expectedFiles.length) {
2528
+ violations.push(`diff_stat stale: esperado ${expectedFiles.length} files, recebido=${JSON.stringify(state.diff_stat)}`);
2529
+ }
2530
+ } catch (error) {
2531
+ violations.push(`boundary Git inválido: ${error.message}`);
2532
+ }
2533
+ return { ok: violations.length === 0, legacy: false, state, violations };
2534
+ }
2535
+
2536
+ function structuredBlockingFindings(packet) {
2537
+ const findings = Array.isArray(packet?.findings) ? packet.findings : [];
2538
+ return findings.filter((finding) => finding && ['P0', 'P1'].includes(finding.severity));
2539
+ }
2540
+
2541
+ function normalizeFindingsPacket(packet) {
2542
+ if (!packet || typeof packet !== 'object') {
2543
+ return { packet, violations: ['finding packet obrigatório'] };
2544
+ }
2545
+ if (!Array.isArray(packet.findings)) {
2546
+ return { packet, violations: ['findings deve ser array'] };
2547
+ }
2548
+ const violations = [];
2549
+ const ids = new Set();
2550
+ const findings = packet.findings.map((finding, index) => {
2551
+ if (!finding || typeof finding !== 'object') {
2552
+ violations.push(`finding ${index} deve ser objeto`);
2553
+ return finding;
2554
+ }
2555
+ const label = typeof finding.id === 'string' && finding.id.trim()
2556
+ ? finding.id.trim()
2557
+ : `finding ${index}`;
2558
+ if (typeof finding.id !== 'string' || !/^F-\d{3}$/.test(finding.id.trim())) {
2559
+ violations.push(`${label}: id obrigatório no formato F-NNN`);
2560
+ } else if (ids.has(finding.id.trim())) {
2561
+ violations.push(`${label}: id duplicado`);
2562
+ } else {
2563
+ ids.add(finding.id.trim());
2564
+ }
2565
+ if (!['P0', 'P1', 'P2', 'P3'].includes(finding.severity)) {
2566
+ violations.push(`${label}: severity deve ser P0|P1|P2|P3`);
2567
+ }
2568
+ for (const field of ['file', 'failure_mode', 'evidence', 'recommendation', 'fix_validation']) {
2569
+ if (typeof finding[field] !== 'string' || !finding[field].trim()) violations.push(`${label}: ${field} obrigatório`);
2570
+ }
2571
+ if (!Number.isInteger(finding.line) || finding.line < 1) violations.push(`${label}: line inválida`);
2572
+ return { ...finding, msg: `${finding.failure_mode ?? ''}: ${finding.evidence ?? ''}`.trim() };
2573
+ });
2574
+ return { packet: { ...packet, findings }, violations };
2575
+ }
2576
+
2261
2577
  function validatorStart(args, context) {
2262
2578
  const runId = validateRunId(args.run_id);
2263
2579
  const statePathValue = requiredString(args, 'state_path');
@@ -2303,6 +2619,17 @@ function validatorStart(args, context) {
2303
2619
  };
2304
2620
  }
2305
2621
 
2622
+ const boundaryValidation = validateStateBoundary(statePathValue, args);
2623
+ if (!boundaryValidation.ok) {
2624
+ return {
2625
+ gate: 'G4', action: 'start', status: 'blocked', timestamp,
2626
+ state_path: statePathValue,
2627
+ boundary_violations: boundaryValidation.violations,
2628
+ error: `State/boundary inválido: ${boundaryValidation.violations.join('; ')}`,
2629
+ next_action: 'regerar_state_path_com_boundary_real',
2630
+ };
2631
+ }
2632
+
2306
2633
  if (cycle.active) {
2307
2634
  return {
2308
2635
  gate: 'G4',
@@ -2411,13 +2738,15 @@ function validatorStart(args, context) {
2411
2738
  last_state_path: statePathValue,
2412
2739
  repair: {
2413
2740
  skill: WORKFLOW_CONFIG.skills.findings_repair,
2414
- status: 'not_needed',
2415
- required_from_attempt: null,
2416
- requested_at: null,
2417
- completed_at: null,
2741
+ status: cycle.repair.status === 'completed' ? 'completed' : 'not_needed',
2742
+ required_from_attempt: cycle.repair.required_from_attempt,
2743
+ requested_at: cycle.repair.requested_at,
2744
+ completed_at: cycle.repair.completed_at,
2418
2745
  active: null,
2419
2746
  },
2420
- findings_packet: null,
2747
+ // O retry precisa manter os findings originais: o complete do attempt 2
2748
+ // correlaciona `repaired_finding_ids` contra exatamente esse packet.
2749
+ findings_packet: attempt === 2 ? cycle.findings_packet : null,
2421
2750
  },
2422
2751
  };
2423
2752
  }
@@ -2428,7 +2757,8 @@ function validatorComplete(args, context) {
2428
2757
  const statePathValue = requiredString(args, 'state_path');
2429
2758
  const activeValidatorRunId = requiredString(args, 'validator_run_id');
2430
2759
  const verdict = requiredString(args, 'verdict');
2431
- const packet = optionalData(args);
2760
+ const packetResult = normalizeFindingsPacket(optionalData(args));
2761
+ const packet = packetResult.packet;
2432
2762
  // S04/S16: token de dispatch é obrigatório para fechar o slot ativo. Ele vem
2433
2763
  // do validator_recovery lido pela folha fria e volta no output estruturado do
2434
2764
  // validator. Sem token não existe garantia anti-stale completa.
@@ -2545,7 +2875,7 @@ function validatorComplete(args, context) {
2545
2875
  // Falha (resposta ausente/hash divergente) NÃO fecha o slot — igual stale: active é
2546
2876
  // preservado (não retornamos validator_cycle), o orquestrador re-despacha o MESMO
2547
2877
  // validador (mesmo attempt) que lê o boundary e reenvia o hash correto. Não consome
2548
- // attempt nem reabre terminal. `unverifiable` (arquivo sumiu no complete) não bloqueia.
2878
+ // attempt nem reabre terminal. Arquivo sumido/ilegível consome o mesmo orçamento bounded.
2549
2879
  const challengeCheck = verifyValidatorChallenge(cycle.active.challenge, challengeResponse, args);
2550
2880
  if (!challengeCheck.ok) {
2551
2881
  // P2-1: falhas de challenge são bounded por attempt. As anteriores ficam no
@@ -2602,9 +2932,17 @@ function validatorComplete(args, context) {
2602
2932
  next_action: 'redespachar_o_mesmo_validador_irmao_que_le_o_boundary_e_reenvia_challenge_response',
2603
2933
  };
2604
2934
  }
2605
- const challengeVerified = !cycle.active.challenge
2606
- ? 'no_challenge'
2607
- : challengeCheck.unverifiable ? 'unverifiable' : 'verified';
2935
+ const challengeVerified = !cycle.active.challenge ? 'no_challenge' : 'verified';
2936
+
2937
+ if (packetResult.violations.length > 0) {
2938
+ return {
2939
+ gate: 'G4', action: 'complete', status: 'blocked', timestamp,
2940
+ validator_attempt: cycle.active.attempt, validator_run_id: activeValidatorRunId,
2941
+ state_path: statePathValue, validator_status: 'invalid_finding_shape',
2942
+ error: `Finding estruturado inválido: ${packetResult.violations.join('; ')}`,
2943
+ next_action: 'corrigir_shape_estruturado_do_finding',
2944
+ };
2945
+ }
2608
2946
 
2609
2947
  const normalizedVerdict = verdict === 'pass'
2610
2948
  ? 'passed'
@@ -2612,6 +2950,40 @@ function validatorComplete(args, context) {
2612
2950
  ? 'passed_with_observations'
2613
2951
  : verdict;
2614
2952
 
2953
+ const blockingFindings = structuredBlockingFindings(packet);
2954
+ if (VALIDATOR_PASSED_STATUSES.has(normalizedVerdict) && blockingFindings.length > 0) {
2955
+ return {
2956
+ gate: 'G4', action: 'complete', status: 'blocked', timestamp,
2957
+ validator_attempt: cycle.active.attempt,
2958
+ validator_run_id: activeValidatorRunId,
2959
+ state_path: statePathValue,
2960
+ validator_status: 'invalid_verdict_severity',
2961
+ finding_ids: blockingFindings.map((finding) => finding.id ?? null),
2962
+ error: `${blockingFindings[0].severity} exige verdict fail`,
2963
+ next_action: 'corrigir_veredito_estruturado_do_validator',
2964
+ };
2965
+ }
2966
+
2967
+ if (cycle.active.attempt === 2
2968
+ && cycle.repair.status === 'completed'
2969
+ && VALIDATOR_PASSED_STATUSES.has(normalizedVerdict)) {
2970
+ const targetIds = structuredBlockingFindings(cycle.findings_packet)
2971
+ .map((finding) => finding.id)
2972
+ .filter((id) => typeof id === 'string' && id.trim());
2973
+ const correlated = new Set(Array.isArray(packet?.repaired_finding_ids) ? packet.repaired_finding_ids : []);
2974
+ const missingIds = targetIds.filter((id) => !correlated.has(id));
2975
+ if (missingIds.length > 0) {
2976
+ return {
2977
+ gate: 'G4', action: 'complete', status: 'blocked', timestamp,
2978
+ validator_attempt: cycle.active.attempt, validator_run_id: activeValidatorRunId,
2979
+ state_path: statePathValue, validator_status: 'repair_correlation_missing',
2980
+ missing_finding_ids: missingIds,
2981
+ error: 'Segundo validator não correlacionou todos os findings reparados',
2982
+ next_action: 'revalidar_repair_e_retornar_repaired_finding_ids',
2983
+ };
2984
+ }
2985
+ }
2986
+
2615
2987
  if (VALIDATOR_PASSED_STATUSES.has(normalizedVerdict)) {
2616
2988
  return {
2617
2989
  gate: 'G4',
@@ -2775,6 +3147,18 @@ function validatorRepairStart(args, context) {
2775
3147
  };
2776
3148
  }
2777
3149
 
3150
+
3151
+ const boundaryBefore = validateStateBoundary(statePathValue, args);
3152
+ if (!boundaryBefore.ok) {
3153
+ return {
3154
+ gate: 'G4', action: 'repair_start', status: 'blocked', timestamp,
3155
+ state_path: statePathValue,
3156
+ boundary_violations: boundaryBefore.violations,
3157
+ error: `Repair bloqueado por state/boundary inválido: ${boundaryBefore.violations.join('; ')}`,
3158
+ next_action: 'corrigir_state_path_antes_do_repair',
3159
+ };
3160
+ }
3161
+
2778
3162
  const activeRepairRunId = repairRunId(runId, cycle.attempts_used, timestamp);
2779
3163
  return {
2780
3164
  gate: 'G4',
@@ -2784,6 +3168,7 @@ function validatorRepairStart(args, context) {
2784
3168
  validator_attempt: cycle.attempts_used,
2785
3169
  repair_run_id: activeRepairRunId,
2786
3170
  repair_budget: 1,
3171
+ findings: cycle.findings_packet?.findings ?? [],
2787
3172
  state_path: statePathValue,
2788
3173
  validator_status: 'repair_running',
2789
3174
  next_action: `dispatch_${WORKFLOW_CONFIG.skills.findings_repair}`,
@@ -2800,6 +3185,12 @@ function validatorRepairStart(args, context) {
2800
3185
  run_id: activeRepairRunId,
2801
3186
  state_path: statePathValue,
2802
3187
  started_at: timestamp,
3188
+ boundary_before: {
3189
+ head_sha: boundaryBefore.state.head_sha ?? null,
3190
+ diff_stat: boundaryBefore.state.diff_stat,
3191
+ files_changed: boundaryBefore.state.files_changed,
3192
+ worktree_final: boundaryBefore.state.worktree_final ?? null,
3193
+ },
2803
3194
  },
2804
3195
  },
2805
3196
  },
@@ -2811,6 +3202,7 @@ function validatorRepairComplete(args, context) {
2811
3202
  const cycle = normalizeValidatorCycle(context.state.data?.validator_cycle ?? {});
2812
3203
  const statePathValue = requiredString(args, 'state_path');
2813
3204
  const activeRepairRunId = requiredString(args, 'repair_run_id');
3205
+ const repairData = optionalData(args);
2814
3206
 
2815
3207
  if (cycle.active) {
2816
3208
  return {
@@ -2900,6 +3292,105 @@ function validatorRepairComplete(args, context) {
2900
3292
  };
2901
3293
  }
2902
3294
 
3295
+
3296
+ const boundaryAfter = validateStateBoundary(statePathValue, args);
3297
+ if (!boundaryAfter.ok) {
3298
+ return {
3299
+ gate: 'G4', action: 'repair_complete', status: 'blocked', timestamp,
3300
+ repair_run_id: activeRepairRunId, state_path: statePathValue,
3301
+ boundary_violations: boundaryAfter.violations,
3302
+ error: `Repair não atualizou state/boundary completo: ${boundaryAfter.violations.join('; ')}`,
3303
+ next_action: 'atualizar_head_stat_snapshots_files_e_evidencias_no_state_original',
3304
+ };
3305
+ }
3306
+
3307
+ const targets = structuredBlockingFindings(cycle.findings_packet).filter(
3308
+ (finding) => typeof finding.id === 'string' && finding.id.trim(),
3309
+ );
3310
+ const findings = Array.isArray(cycle.findings_packet?.findings) ? cycle.findings_packet.findings : [];
3311
+ const receivedIds = new Set(findings.map((finding) => finding?.id).filter(Boolean));
3312
+ const repairs = Array.isArray(repairData?.repairs) ? repairData.repairs : [];
3313
+ const stateRepairs = Array.isArray(boundaryAfter.state.repair_evidence)
3314
+ ? boundaryAfter.state.repair_evidence
3315
+ : [];
3316
+ const repairViolations = [];
3317
+ for (const [label, entries] of [['output', repairs], ['state', stateRepairs]]) {
3318
+ const seen = new Set();
3319
+ for (const repair of entries) {
3320
+ const id = repair?.finding_id;
3321
+ if (!receivedIds.has(id)) repairViolations.push(`${label}: repair ID desconhecido ${id ?? '<ausente>'}`);
3322
+ if (seen.has(id)) repairViolations.push(`${label}: repair ID duplicado ${id}`);
3323
+ seen.add(id);
3324
+ if (!Array.isArray(repair?.files_touched) || repair.files_touched.length === 0) {
3325
+ repairViolations.push(`${label}: ${id ?? '<ausente>'} sem files_touched`);
3326
+ }
3327
+ }
3328
+ }
3329
+ const normalizeRepair = (repair) => ({
3330
+ finding_id: repair.finding_id,
3331
+ files_touched: [...repair.files_touched].sort(),
3332
+ checks_run: [...(repair.checks_run ?? [])].sort(),
3333
+ status: repair.status,
3334
+ });
3335
+ if (repairViolations.length === 0) {
3336
+ const outputNormalized = repairs.map(normalizeRepair).sort((a, b) => a.finding_id.localeCompare(b.finding_id));
3337
+ const stateNormalized = stateRepairs.map(normalizeRepair).sort((a, b) => a.finding_id.localeCompare(b.finding_id));
3338
+ if (JSON.stringify(outputNormalized) !== JSON.stringify(stateNormalized)) {
3339
+ repairViolations.push('output do repair diverge de repair_evidence persistido');
3340
+ }
3341
+ }
3342
+ const before = cycle.repair.active.boundary_before;
3343
+ if (Array.isArray(before?.worktree_final) && Array.isArray(boundaryAfter.state.worktree_final)) {
3344
+ let committedDuringRepair = [];
3345
+ if (before.head_sha && before.head_sha !== boundaryAfter.state.head_sha) {
3346
+ try {
3347
+ committedDuringRepair = gitLines(consumerRoot(args), [
3348
+ 'diff', '--name-only', `${before.head_sha}...${boundaryAfter.state.head_sha}`,
3349
+ ]);
3350
+ } catch (error) {
3351
+ repairViolations.push(`não foi possível derivar commits do repair: ${error.message}`);
3352
+ }
3353
+ }
3354
+ const touchedReal = [...new Set([
3355
+ ...snapshotDeltaFiles(before.worktree_final, boundaryAfter.state.worktree_final),
3356
+ ...committedDuringRepair,
3357
+ ])].sort();
3358
+ const touchedClaimed = [...new Set(repairs.flatMap((repair) => repair?.files_touched ?? []))].sort();
3359
+ if (JSON.stringify(touchedReal) !== JSON.stringify(touchedClaimed)) {
3360
+ repairViolations.push(`arquivos do repair divergem do delta real: esperado=${JSON.stringify(touchedReal)} recebido=${JSON.stringify(touchedClaimed)}`);
3361
+ }
3362
+ }
3363
+ if (repairViolations.length > 0) {
3364
+ return {
3365
+ gate: 'G4', action: 'repair_complete', status: 'blocked', timestamp,
3366
+ repair_run_id: activeRepairRunId, state_path: statePathValue,
3367
+ repair_violations: repairViolations,
3368
+ error: `Repair fora do boundary recebido: ${repairViolations.join('; ')}`,
3369
+ next_action: 'corrigir_correlacao_ids_arquivos_e_state_do_repair',
3370
+ };
3371
+ }
3372
+ if (targets.length > 0) {
3373
+ const missing = targets.filter((target) => {
3374
+ const output = repairs.find((repair) => repair?.finding_id === target.id);
3375
+ const persisted = stateRepairs.find((repair) => repair?.finding_id === target.id);
3376
+ return output?.status !== 'resolved'
3377
+ || !Array.isArray(output.files_touched) || output.files_touched.length === 0
3378
+ || !Array.isArray(output.checks_run) || output.checks_run.length === 0
3379
+ || persisted?.status !== 'resolved'
3380
+ || !Array.isArray(persisted.files_touched) || persisted.files_touched.length === 0
3381
+ || !Array.isArray(persisted.checks_run) || persisted.checks_run.length === 0;
3382
+ });
3383
+ if (missing.length > 0) {
3384
+ return {
3385
+ gate: 'G4', action: 'repair_complete', status: 'blocked', timestamp,
3386
+ repair_run_id: activeRepairRunId, state_path: statePathValue,
3387
+ unresolved_finding_ids: missing.map((finding) => finding.id),
3388
+ error: 'Repair sem evidência de resolução para finding P0/P1 alvo',
3389
+ next_action: 'persistir_correlacao_finding_arquivo_check_status',
3390
+ };
3391
+ }
3392
+ }
3393
+
2903
3394
  return {
2904
3395
  gate: 'G4',
2905
3396
  action: 'repair_complete',
@@ -3384,6 +3875,7 @@ export {
3384
3875
  checkPrerequisites,
3385
3876
  checkJoinCapability,
3386
3877
  expectedNextPhase,
3878
+ expectedExecutorSkill,
3387
3879
  guaranteeLevelForMode,
3388
3880
  classifyArtifactContent,
3389
3881
  BANNER_TEMPLATES,
@@ -3396,6 +3888,8 @@ export {
3396
3888
  preflight,
3397
3889
  lockDispatch,
3398
3890
  lockValidator,
3891
+ captureWorktreeSnapshot,
3892
+ validateStateBoundary,
3399
3893
  assertAfterPlan,
3400
3894
  runState,
3401
3895
  ping,