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.
- package/README.md +5 -2
- package/VERSION +1 -1
- package/build/bump-version.mjs +6 -21
- package/build/cli/atlas-init.mjs +92 -5
- package/build/tests/classify-findings.test.mjs +20 -0
- package/build/tests/etapa3.test.mjs +161 -0
- package/build/tests/test_classify_findings.py +79 -0
- package/hosts/opencode/.opencode/agents/atlas-findings-repair.md +4 -0
- package/hosts/opencode/.opencode/agents/atlas-task-validator.md +18 -1
- package/hosts/opencode/.opencode/atlas/VERSION +1 -1
- package/hosts/opencode/.opencode/atlas/orchestrator/README.md +7 -5
- package/hosts/opencode/.opencode/atlas/orchestrator/commands/workflow.md +1 -1
- package/hosts/opencode/.opencode/atlas/orchestrator/references/host-adapters.md +13 -12
- package/hosts/opencode/.opencode/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
- package/hosts/opencode/.opencode/atlas/packages/mcp-server/README.md +1 -1
- package/hosts/opencode/.opencode/atlas/packages/mcp-server/package.json +1 -1
- package/hosts/opencode/.opencode/atlas/packages/mcp-server/server.js +514 -20
- package/hosts/opencode/.opencode/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/hosts/opencode/.opencode/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
- package/hosts/opencode/.opencode/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
- package/hosts/opencode/.opencode/skills/_shared/references/stack-profiles.md +36 -0
- package/hosts/opencode/.opencode/skills/_shared/scripts/document_quality.mjs +252 -0
- package/hosts/opencode/.opencode/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/hosts/opencode/.opencode/skills/atlas-direct-execute/SKILL.md +6 -2
- package/hosts/opencode/.opencode/skills/atlas-findings-repair/SKILL.md +11 -1
- package/hosts/opencode/.opencode/skills/atlas-plan-execute/SKILL.md +16 -2
- package/hosts/opencode/.opencode/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/hosts/opencode/.opencode/skills/atlas-prd-interview/SKILL.md +7 -2
- package/hosts/opencode/.opencode/skills/atlas-slice-review/SKILL.md +37 -2
- package/hosts/opencode/.opencode/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/hosts/opencode/.opencode/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/hosts/opencode/.opencode/skills/atlas-task-validator/SKILL.md +29 -14
- package/hosts/opencode/.opencode/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
- package/hosts/pi/.pi/agents/atlas-direct-execute.md +6 -2
- package/hosts/pi/.pi/agents/atlas-findings-repair.md +15 -1
- package/hosts/pi/.pi/agents/atlas-plan-execute.md +16 -2
- package/hosts/pi/.pi/agents/atlas-slice-review.md +37 -2
- package/hosts/pi/.pi/agents/atlas-task-validator.md +18 -1
- package/hosts/pi/atlas/VERSION +1 -1
- package/hosts/pi/atlas/orchestrator/README.md +7 -5
- package/hosts/pi/atlas/orchestrator/commands/workflow.md +1 -1
- package/hosts/pi/atlas/orchestrator/references/host-adapters.md +13 -12
- package/hosts/pi/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
- package/hosts/pi/atlas/packages/mcp-server/README.md +1 -1
- package/hosts/pi/atlas/packages/mcp-server/package.json +1 -1
- package/hosts/pi/atlas/packages/mcp-server/server.js +514 -20
- package/hosts/pi/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/hosts/pi/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
- package/hosts/pi/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
- package/hosts/pi/skills/_shared/references/stack-profiles.md +36 -0
- package/hosts/pi/skills/_shared/scripts/document_quality.mjs +252 -0
- package/hosts/pi/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/hosts/pi/skills/atlas-direct-execute/SKILL.md +6 -2
- package/hosts/pi/skills/atlas-findings-repair/SKILL.md +11 -1
- package/hosts/pi/skills/atlas-plan-execute/SKILL.md +16 -2
- package/hosts/pi/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/hosts/pi/skills/atlas-prd-interview/SKILL.md +7 -2
- package/hosts/pi/skills/atlas-slice-review/SKILL.md +37 -2
- package/hosts/pi/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/hosts/pi/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/hosts/pi/skills/atlas-task-validator/SKILL.md +29 -14
- package/hosts/pi/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
- package/hosts/zcode/.zcode-plugin/plugin.json +27 -0
- package/hosts/zcode/agents/atlas-direct-execute.md +31 -0
- package/hosts/zcode/agents/atlas-findings-repair.md +39 -0
- package/hosts/zcode/agents/atlas-plan-execute.md +33 -0
- package/hosts/zcode/agents/atlas-slice-review.md +27 -0
- package/hosts/zcode/agents/atlas-task-validator.md +138 -0
- package/hosts/zcode/packages/mcp-server/README.md +29 -0
- package/hosts/zcode/packages/mcp-server/VERSION +1 -0
- package/hosts/zcode/packages/mcp-server/package.json +15 -0
- package/hosts/zcode/packages/mcp-server/server.js +3897 -0
- package/hosts/zcode/packages/orchestrator/README.md +270 -0
- package/hosts/zcode/packages/orchestrator/commands/workflow.md +37 -0
- package/hosts/zcode/packages/orchestrator/defaults/paths.md +21 -0
- package/hosts/zcode/packages/orchestrator/references/host-adapters.md +106 -0
- package/hosts/zcode/packages/orchestrator/references/qa_s13_matrix.md +141 -0
- package/hosts/zcode/packages/orchestrator/references/subagent_dispatch.md +42 -0
- package/hosts/zcode/packages/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +391 -0
- package/hosts/zcode/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +855 -0
- package/hosts/zcode/packages/templates/BOUNDARY_PRD_PLAN.md +93 -0
- package/hosts/zcode/packages/templates/PERGUNTAS_EM_ABERTO_TEMPLATE.md +139 -0
- package/hosts/zcode/packages/templates/PLAN_TEMPLATE.md +146 -0
- package/hosts/zcode/packages/templates/PRD_TEMPLATE.md +150 -0
- package/hosts/zcode/packages/templates/STATE_FILE_SCHEMA.md +56 -0
- package/hosts/zcode/skills/_shared/references/stack-profiles.md +36 -0
- package/hosts/zcode/skills/_shared/scripts/document_quality.mjs +252 -0
- package/hosts/zcode/skills/atlas-backlog-generator/SKILL.md +93 -0
- package/hosts/zcode/skills/atlas-backlog-generator/agents/openai.yaml +4 -0
- package/hosts/zcode/skills/atlas-direct-execute/SKILL.md +221 -0
- package/hosts/zcode/skills/atlas-direct-execute/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-findings-repair/SKILL.md +158 -0
- package/hosts/zcode/skills/atlas-findings-repair/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-plan-execute/SKILL.md +175 -0
- package/hosts/zcode/skills/atlas-plan-execute/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-plan-execute/references/plan-contract.md +88 -0
- package/hosts/zcode/skills/atlas-plan-execute/references/quality-gates.md +60 -0
- package/hosts/zcode/skills/atlas-plan-execute/scripts/check_budget_state.py +96 -0
- package/hosts/zcode/skills/atlas-plan-execute/scripts/extract_plan_contract.py +191 -0
- package/hosts/zcode/skills/atlas-plan-execute/scripts/validate_gate_result.py +56 -0
- package/hosts/zcode/skills/atlas-plan-handoff/SKILL.md +183 -0
- package/hosts/zcode/skills/atlas-plan-handoff/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-prd-interview/SKILL.md +82 -0
- package/hosts/zcode/skills/atlas-prd-interview/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-slice-review/SKILL.md +156 -0
- package/hosts/zcode/skills/atlas-slice-review/agents/openai.yaml +4 -0
- package/hosts/zcode/skills/atlas-slice-review/references/review-contract.md +58 -0
- package/hosts/zcode/skills/atlas-slice-review/references/scenario-lenses.md +57 -0
- package/hosts/zcode/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/hosts/zcode/skills/atlas-slice-review/scripts/classify_findings.py +24 -0
- package/hosts/zcode/skills/atlas-slice-review/scripts/extract_review_slice.py +158 -0
- package/hosts/zcode/skills/atlas-sprint-prd-generator/SKILL.md +77 -0
- package/hosts/zcode/skills/atlas-sprint-prd-generator/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-task-validator/SKILL.md +173 -0
- package/hosts/zcode/skills/atlas-task-validator/agents/openai.yaml +7 -0
- package/hosts/zcode/skills/atlas-workflow-orchestrator/SKILL.md +391 -0
- package/package.json +1 -1
- package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-findings-repair.toml +1 -1
- package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-task-validator.toml +1 -1
- package/plugins/atlas-workflow-orchestrator/.codex-plugin/plugin.json +1 -1
- package/plugins/atlas-workflow-orchestrator/VERSION +1 -1
- package/plugins/atlas-workflow-orchestrator/agents/atlas-findings-repair.md +4 -0
- package/plugins/atlas-workflow-orchestrator/agents/atlas-task-validator.md +18 -1
- package/plugins/atlas-workflow-orchestrator/orchestrator/README.md +7 -5
- package/plugins/atlas-workflow-orchestrator/orchestrator/commands/workflow.md +1 -1
- package/plugins/atlas-workflow-orchestrator/orchestrator/references/host-adapters.md +13 -12
- package/plugins/atlas-workflow-orchestrator/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
- package/plugins/atlas-workflow-orchestrator/packages/mcp-server/README.md +1 -1
- package/plugins/atlas-workflow-orchestrator/packages/mcp-server/package.json +1 -1
- package/plugins/atlas-workflow-orchestrator/packages/mcp-server/server.js +514 -20
- package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/references/stack-profiles.md +36 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/scripts/document_quality.mjs +252 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-direct-execute/SKILL.md +6 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-findings-repair/SKILL.md +11 -1
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-execute/SKILL.md +16 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-prd-interview/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/SKILL.md +37 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-task-validator/SKILL.md +29 -14
- package/plugins/atlas-workflow-orchestrator/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/plugins/atlas-workflow-orchestrator/packages/templates/PRD_TEMPLATE.md +2 -1
- package/plugins/atlas-workflow-orchestrator/packages/templates/STATE_FILE_SCHEMA.md +25 -1
- package/plugins/atlas-workflow-orchestrator/skills/_shared/references/stack-profiles.md +36 -0
- package/plugins/atlas-workflow-orchestrator/skills/_shared/scripts/document_quality.mjs +252 -0
- package/plugins/atlas-workflow-orchestrator/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-direct-execute/SKILL.md +6 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-findings-repair/SKILL.md +11 -1
- package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-execute/SKILL.md +16 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/plugins/atlas-workflow-orchestrator/skills/atlas-prd-interview/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/SKILL.md +37 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/plugins/atlas-workflow-orchestrator/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/plugins/atlas-workflow-orchestrator/skills/atlas-task-validator/SKILL.md +29 -14
- package/plugins/atlas-workflow-orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +24 -17
- package/plugins/atlas-workflow-orchestrator/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/plugins/atlas-workflow-orchestrator/templates/PRD_TEMPLATE.md +2 -1
- 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: "<
|
|
274
|
-
registration: '
|
|
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:
|
|
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:
|
|
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:
|
|
2416
|
-
requested_at:
|
|
2417
|
-
completed_at:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
2607
|
-
|
|
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,
|