atlas-workflow 0.9.0 → 0.9.2
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 +2 -2
- package/VERSION +1 -1
- package/build/cli/atlas-init.mjs +12 -14
- 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/skills/atlas-workflow-orchestrator/SKILL.md +22 -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 +446 -14
- 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 +22 -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/skills/atlas-workflow-orchestrator/SKILL.md +22 -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 +446 -14
- 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 +22 -17
- 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/skills/atlas-workflow-orchestrator/SKILL.md +22 -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 +446 -14
- 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 +22 -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
|
|
@@ -281,6 +296,7 @@ const HOST_ADAPTERS = {
|
|
|
281
296
|
mechanism: 'invoke_subagent bloqueante por design do host',
|
|
282
297
|
},
|
|
283
298
|
},
|
|
299
|
+
question_prompt: { mechanism: 'notify_user', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
|
|
284
300
|
todo_tool: null,
|
|
285
301
|
hooks: { supported: false, mechanism: null },
|
|
286
302
|
capabilities_flags: { subagent_available: true, mcp_available: true, todo_available: false },
|
|
@@ -300,6 +316,7 @@ const HOST_ADAPTERS = {
|
|
|
300
316
|
mechanism: 'indeterminado; host deve reportar',
|
|
301
317
|
},
|
|
302
318
|
},
|
|
319
|
+
question_prompt: { mechanism: 'native_structured_question', mode: 'structured', max_questions: 4, options_per_question: 3, persistence: 'prd_after_each_round' },
|
|
303
320
|
todo_tool: null,
|
|
304
321
|
hooks: { supported: false, mechanism: null },
|
|
305
322
|
// generic EXIGE subagente+MCP do host (DEC-004); host MCP-only sem subagente
|
|
@@ -378,6 +395,7 @@ function capabilities(args = {}) {
|
|
|
378
395
|
schema_version: CAPABILITIES_SCHEMA_VERSION,
|
|
379
396
|
subagent_dispatch: adapter.subagent_dispatch,
|
|
380
397
|
validator_dispatch: adapter.validator_dispatch,
|
|
398
|
+
question_prompt: adapter.question_prompt,
|
|
381
399
|
todo_tool: adapter.todo_tool,
|
|
382
400
|
hooks: adapter.hooks,
|
|
383
401
|
capabilities_flags: adapter.capabilities_flags,
|
|
@@ -1695,6 +1713,7 @@ function preflight(args = {}) {
|
|
|
1695
1713
|
...(guaranteeLevel ? { guarantee_level: guaranteeLevel } : {}),
|
|
1696
1714
|
routing: {
|
|
1697
1715
|
mode,
|
|
1716
|
+
...(expectedExecutorSkill(mode) ? { executor_skill: expectedExecutorSkill(mode) } : {}),
|
|
1698
1717
|
...(guaranteeLevel ? { guarantee_level: guaranteeLevel } : {}),
|
|
1699
1718
|
skills: config.skills,
|
|
1700
1719
|
version: version.version,
|
|
@@ -2238,8 +2257,7 @@ function pickValidatorChallenge(statePathValue, args, dispatchToken) {
|
|
|
2238
2257
|
|
|
2239
2258
|
// Verifica o challenge_response no complete recomputando o hash do disco.
|
|
2240
2259
|
// { ok: true } — sem challenge emitido OU hash confere
|
|
2241
|
-
// { ok:
|
|
2242
|
-
// { ok: false, reason } — resposta ausente ou hash divergente
|
|
2260
|
+
// { ok: false, reason } — resposta ausente, arquivo ilegível ou hash divergente
|
|
2243
2261
|
function verifyValidatorChallenge(challenge, response, args) {
|
|
2244
2262
|
if (!challenge || typeof challenge.file !== 'string') return { ok: true };
|
|
2245
2263
|
if (typeof response !== 'string' || response.trim() === '') {
|
|
@@ -2251,13 +2269,249 @@ function verifyValidatorChallenge(challenge, response, args) {
|
|
|
2251
2269
|
.update(fs.readFileSync(resolveConsumerPath(challenge.file, args)))
|
|
2252
2270
|
.digest('hex');
|
|
2253
2271
|
} catch {
|
|
2254
|
-
return { ok:
|
|
2272
|
+
return { ok: false, reason: 'challenge_file_unreadable' };
|
|
2255
2273
|
}
|
|
2256
2274
|
// Aceita hex puro ou saída de `shasum` (`<hash> <arquivo>`): primeiro token.
|
|
2257
2275
|
const submitted = response.trim().toLowerCase().split(/\s+/)[0];
|
|
2258
2276
|
return submitted === actual ? { ok: true } : { ok: false, reason: 'challenge_hash_divergente' };
|
|
2259
2277
|
}
|
|
2260
2278
|
|
|
2279
|
+
const STATE_REQUIRED_FIELDS = [
|
|
2280
|
+
'run_id', 'slice', 'tasks', 'files_changed', 'diff_stat', 'plan_path',
|
|
2281
|
+
'boundary_refs', 'executed_at', 'executor_skill',
|
|
2282
|
+
];
|
|
2283
|
+
const STATE_EXTENSION_ARRAYS = [
|
|
2284
|
+
'obligations', 'invariants', 'scenario_probes', 'risk_probes',
|
|
2285
|
+
'validation_map', 'task_evidence', 'worktree_baseline', 'worktree_final',
|
|
2286
|
+
];
|
|
2287
|
+
|
|
2288
|
+
function gitOutput(root, gitArgs) {
|
|
2289
|
+
return execFileSync('git', ['-C', root, ...gitArgs], {
|
|
2290
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
2291
|
+
}).trim();
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
function gitLines(root, gitArgs) {
|
|
2295
|
+
const output = gitOutput(root, gitArgs);
|
|
2296
|
+
return output ? output.split(/\r?\n/).filter(Boolean) : [];
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function stateEvidenceFiles(state) {
|
|
2300
|
+
const result = [];
|
|
2301
|
+
for (const item of [...(state.task_evidence ?? []), ...(state.repair_evidence ?? [])]) {
|
|
2302
|
+
if (!item || typeof item !== 'object') continue;
|
|
2303
|
+
for (const key of ['files', 'files_touched']) {
|
|
2304
|
+
if (Array.isArray(item[key])) result.push(...item[key]);
|
|
2305
|
+
}
|
|
2306
|
+
if (typeof item.file === 'string') result.push(item.file);
|
|
2307
|
+
}
|
|
2308
|
+
return [...new Set(result.filter((item) => typeof item === 'string' && item.trim()))].sort();
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function snapshotStatus(xy) {
|
|
2312
|
+
if (xy === '??') return 'A';
|
|
2313
|
+
for (const status of ['U', 'D', 'R', 'C', 'A', 'T', 'M']) {
|
|
2314
|
+
if (xy.includes(status)) return status;
|
|
2315
|
+
}
|
|
2316
|
+
return 'M';
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function normalizeSnapshotPath(input) {
|
|
2320
|
+
if (typeof input !== 'string' || !input.trim()) throw new Error('path vazio');
|
|
2321
|
+
const normalized = path.posix.normalize(input.replaceAll('\\', '/'));
|
|
2322
|
+
if (path.posix.isAbsolute(normalized) || normalized === '..' || normalized.startsWith('../')) {
|
|
2323
|
+
throw new Error('path fora do projeto');
|
|
2324
|
+
}
|
|
2325
|
+
return normalized;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function snapshotHash(root, rel) {
|
|
2329
|
+
try {
|
|
2330
|
+
const normalized = normalizeSnapshotPath(rel);
|
|
2331
|
+
const abs = path.resolve(root, normalized);
|
|
2332
|
+
if (abs !== root && !abs.startsWith(`${root}${path.sep}`)) throw new Error('path fora do projeto');
|
|
2333
|
+
const stat = fs.lstatSync(abs);
|
|
2334
|
+
const content = stat.isSymbolicLink()
|
|
2335
|
+
? Buffer.from(fs.readlinkSync(abs))
|
|
2336
|
+
: fs.readFileSync(abs);
|
|
2337
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
2338
|
+
} catch {
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function captureWorktreeSnapshot(root) {
|
|
2344
|
+
const raw = execFileSync('git', [
|
|
2345
|
+
'-C', root, 'status', '--porcelain=v1', '-z', '--untracked-files=all',
|
|
2346
|
+
]);
|
|
2347
|
+
const records = raw.toString('utf8').split('\0').filter(Boolean);
|
|
2348
|
+
const snapshot = [];
|
|
2349
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2350
|
+
const record = records[index];
|
|
2351
|
+
const xy = record.slice(0, 2);
|
|
2352
|
+
const rel = normalizeSnapshotPath(record.slice(3));
|
|
2353
|
+
const status = snapshotStatus(xy);
|
|
2354
|
+
if (status === 'R' || status === 'C') {
|
|
2355
|
+
const previous = normalizeSnapshotPath(records[index + 1]);
|
|
2356
|
+
index += 1;
|
|
2357
|
+
if (!previous.startsWith('.atlas/state/')) {
|
|
2358
|
+
snapshot.push({ path: previous, status: 'D', sha256: null });
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (!rel.startsWith('.atlas/state/')) {
|
|
2362
|
+
snapshot.push({ path: rel, status, sha256: status === 'D' ? null : snapshotHash(root, rel) });
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return snapshot.sort((a, b) => a.path.localeCompare(b.path) || a.status.localeCompare(b.status));
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function validateSnapshot(snapshot, field, violations) {
|
|
2369
|
+
if (!Array.isArray(snapshot)) return;
|
|
2370
|
+
const paths = new Set();
|
|
2371
|
+
const normalized = [];
|
|
2372
|
+
for (const [index, entry] of snapshot.entries()) {
|
|
2373
|
+
const label = `${field}[${index}]`;
|
|
2374
|
+
if (!entry || typeof entry !== 'object') {
|
|
2375
|
+
violations.push(`${label} deve ser objeto`);
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
let rel;
|
|
2379
|
+
try {
|
|
2380
|
+
rel = normalizeSnapshotPath(entry.path);
|
|
2381
|
+
} catch {
|
|
2382
|
+
violations.push(`${label}.path inválido`);
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (paths.has(rel)) violations.push(`${field} contém path duplicado: ${rel}`);
|
|
2386
|
+
paths.add(rel);
|
|
2387
|
+
if (!['A', 'M', 'D', 'R', 'C', 'T', 'U'].includes(entry.status)) {
|
|
2388
|
+
violations.push(`${label}.status inválido`);
|
|
2389
|
+
}
|
|
2390
|
+
if (entry.status === 'D') {
|
|
2391
|
+
if (entry.sha256 !== null) violations.push(`${label}.sha256 deve ser null para delete`);
|
|
2392
|
+
} else if (typeof entry.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
|
|
2393
|
+
violations.push(`${label}.sha256 inválido`);
|
|
2394
|
+
}
|
|
2395
|
+
normalized.push({ path: rel, status: entry.status, sha256: entry.sha256 });
|
|
2396
|
+
}
|
|
2397
|
+
const sorted = [...normalized].sort((a, b) => a.path.localeCompare(b.path) || a.status.localeCompare(b.status));
|
|
2398
|
+
if (JSON.stringify(normalized) !== JSON.stringify(sorted)) violations.push(`${field} deve estar ordenado por path/status`);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function snapshotDeltaFiles(baseline, finalSnapshot) {
|
|
2402
|
+
const before = new Map(baseline.map((entry) => [entry.path, JSON.stringify(entry)]));
|
|
2403
|
+
const after = new Map(finalSnapshot.map((entry) => [entry.path, JSON.stringify(entry)]));
|
|
2404
|
+
return [...new Set([...before.keys(), ...after.keys()])]
|
|
2405
|
+
.filter((rel) => before.get(rel) !== after.get(rel))
|
|
2406
|
+
.sort();
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function validateStateBoundary(statePathValue, args = {}) {
|
|
2410
|
+
let state;
|
|
2411
|
+
try {
|
|
2412
|
+
state = JSON.parse(fs.readFileSync(resolveConsumerPath(statePathValue, args), 'utf8'));
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
return { ok: false, violations: [`state_path inválido: ${error.message}`] };
|
|
2415
|
+
}
|
|
2416
|
+
const violations = [];
|
|
2417
|
+
for (const field of STATE_REQUIRED_FIELDS) {
|
|
2418
|
+
if (state[field] === undefined || state[field] === null) violations.push(`campo obrigatório ausente: ${field}`);
|
|
2419
|
+
}
|
|
2420
|
+
for (const field of ['tasks', 'files_changed', 'boundary_refs']) {
|
|
2421
|
+
if (!Array.isArray(state[field])) violations.push(`${field} deve ser array`);
|
|
2422
|
+
}
|
|
2423
|
+
const isDirect = state.executor_skill === 'atlas-direct-execute';
|
|
2424
|
+
const hasExtension = state.contract_kind !== undefined;
|
|
2425
|
+
if (!hasExtension && state.executor_skill !== 'atlas-plan-execute') violations.push('schema legado permitido somente para atlas-plan-execute');
|
|
2426
|
+
if (isDirect && state.contract_kind !== 'direct') violations.push('atlas-direct-execute exige contract_kind=direct');
|
|
2427
|
+
if (hasExtension && state.executor_skill === 'atlas-plan-execute' && state.contract_kind !== 'plan') violations.push('atlas-plan-execute exige contract_kind=plan');
|
|
2428
|
+
if (hasExtension && !['plan', 'direct'].includes(state.contract_kind)) violations.push('contract_kind deve ser plan ou direct');
|
|
2429
|
+
if (hasExtension || isDirect) {
|
|
2430
|
+
for (const field of ['base_sha', 'head_sha']) {
|
|
2431
|
+
if (typeof state[field] !== 'string' || !state[field].trim()) violations.push(`${field} obrigatório na extensão`);
|
|
2432
|
+
}
|
|
2433
|
+
for (const field of STATE_EXTENSION_ARRAYS) {
|
|
2434
|
+
if (!Array.isArray(state[field])) violations.push(`${field} deve ser array na extensão`);
|
|
2435
|
+
}
|
|
2436
|
+
if (isDirect && Array.isArray(state.obligations) && state.obligations.length === 0) violations.push('direct exige obligations não vazio');
|
|
2437
|
+
validateSnapshot(state.worktree_baseline, 'worktree_baseline', violations);
|
|
2438
|
+
validateSnapshot(state.worktree_final, 'worktree_final', violations);
|
|
2439
|
+
}
|
|
2440
|
+
if (violations.length > 0 || !hasExtension) {
|
|
2441
|
+
return { ok: violations.length === 0, legacy: !hasExtension, state, violations };
|
|
2442
|
+
}
|
|
2443
|
+
const root = consumerRoot(args);
|
|
2444
|
+
try {
|
|
2445
|
+
gitOutput(root, ['rev-parse', '--verify', `${state.base_sha}^{commit}`]);
|
|
2446
|
+
gitOutput(root, ['rev-parse', '--verify', `${state.head_sha}^{commit}`]);
|
|
2447
|
+
const currentHead = gitOutput(root, ['rev-parse', 'HEAD']);
|
|
2448
|
+
if (currentHead !== state.head_sha) violations.push(`head_sha stale: state=${state.head_sha}, real=${currentHead}`);
|
|
2449
|
+
const committed = gitLines(root, ['diff', '--name-only', `${state.base_sha}...${state.head_sha}`]);
|
|
2450
|
+
const actualFinal = captureWorktreeSnapshot(root);
|
|
2451
|
+
if (JSON.stringify(actualFinal) !== JSON.stringify(state.worktree_final)) {
|
|
2452
|
+
violations.push('worktree_final stale: snapshot diverge do working tree atual');
|
|
2453
|
+
}
|
|
2454
|
+
const worktreeDelta = snapshotDeltaFiles(state.worktree_baseline, state.worktree_final);
|
|
2455
|
+
const claimedEvidence = stateEvidenceFiles(state);
|
|
2456
|
+
const expectedFiles = [...new Set([...committed, ...worktreeDelta])].sort();
|
|
2457
|
+
const declaredFiles = [...new Set((state.files_changed ?? []).filter((f) => typeof f === 'string'))].sort();
|
|
2458
|
+
if (JSON.stringify(expectedFiles) !== JSON.stringify(declaredFiles)) {
|
|
2459
|
+
violations.push(`files_changed diverge do boundary real: esperado=${JSON.stringify(expectedFiles)} recebido=${JSON.stringify(declaredFiles)}`);
|
|
2460
|
+
}
|
|
2461
|
+
if (JSON.stringify(expectedFiles) !== JSON.stringify(claimedEvidence)) {
|
|
2462
|
+
violations.push(`evidência diverge do boundary real: esperado=${JSON.stringify(expectedFiles)} recebido=${JSON.stringify(claimedEvidence)}`);
|
|
2463
|
+
}
|
|
2464
|
+
const statMatch = /^(\d+)\s+files?\b/.exec(String(state.diff_stat).trim());
|
|
2465
|
+
if (!statMatch || Number(statMatch[1]) !== expectedFiles.length) {
|
|
2466
|
+
violations.push(`diff_stat stale: esperado ${expectedFiles.length} files, recebido=${JSON.stringify(state.diff_stat)}`);
|
|
2467
|
+
}
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
violations.push(`boundary Git inválido: ${error.message}`);
|
|
2470
|
+
}
|
|
2471
|
+
return { ok: violations.length === 0, legacy: false, state, violations };
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
function structuredBlockingFindings(packet) {
|
|
2475
|
+
const findings = Array.isArray(packet?.findings) ? packet.findings : [];
|
|
2476
|
+
return findings.filter((finding) => finding && ['P0', 'P1'].includes(finding.severity));
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function normalizeFindingsPacket(packet) {
|
|
2480
|
+
if (!packet || typeof packet !== 'object') {
|
|
2481
|
+
return { packet, violations: ['finding packet obrigatório'] };
|
|
2482
|
+
}
|
|
2483
|
+
if (!Array.isArray(packet.findings)) {
|
|
2484
|
+
return { packet, violations: ['findings deve ser array'] };
|
|
2485
|
+
}
|
|
2486
|
+
const violations = [];
|
|
2487
|
+
const ids = new Set();
|
|
2488
|
+
const findings = packet.findings.map((finding, index) => {
|
|
2489
|
+
if (!finding || typeof finding !== 'object') {
|
|
2490
|
+
violations.push(`finding ${index} deve ser objeto`);
|
|
2491
|
+
return finding;
|
|
2492
|
+
}
|
|
2493
|
+
const label = typeof finding.id === 'string' && finding.id.trim()
|
|
2494
|
+
? finding.id.trim()
|
|
2495
|
+
: `finding ${index}`;
|
|
2496
|
+
if (typeof finding.id !== 'string' || !/^F-\d{3}$/.test(finding.id.trim())) {
|
|
2497
|
+
violations.push(`${label}: id obrigatório no formato F-NNN`);
|
|
2498
|
+
} else if (ids.has(finding.id.trim())) {
|
|
2499
|
+
violations.push(`${label}: id duplicado`);
|
|
2500
|
+
} else {
|
|
2501
|
+
ids.add(finding.id.trim());
|
|
2502
|
+
}
|
|
2503
|
+
if (!['P0', 'P1', 'P2', 'P3'].includes(finding.severity)) {
|
|
2504
|
+
violations.push(`${label}: severity deve ser P0|P1|P2|P3`);
|
|
2505
|
+
}
|
|
2506
|
+
for (const field of ['file', 'failure_mode', 'evidence', 'recommendation', 'fix_validation']) {
|
|
2507
|
+
if (typeof finding[field] !== 'string' || !finding[field].trim()) violations.push(`${label}: ${field} obrigatório`);
|
|
2508
|
+
}
|
|
2509
|
+
if (!Number.isInteger(finding.line) || finding.line < 1) violations.push(`${label}: line inválida`);
|
|
2510
|
+
return { ...finding, msg: `${finding.failure_mode ?? ''}: ${finding.evidence ?? ''}`.trim() };
|
|
2511
|
+
});
|
|
2512
|
+
return { packet: { ...packet, findings }, violations };
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2261
2515
|
function validatorStart(args, context) {
|
|
2262
2516
|
const runId = validateRunId(args.run_id);
|
|
2263
2517
|
const statePathValue = requiredString(args, 'state_path');
|
|
@@ -2303,6 +2557,17 @@ function validatorStart(args, context) {
|
|
|
2303
2557
|
};
|
|
2304
2558
|
}
|
|
2305
2559
|
|
|
2560
|
+
const boundaryValidation = validateStateBoundary(statePathValue, args);
|
|
2561
|
+
if (!boundaryValidation.ok) {
|
|
2562
|
+
return {
|
|
2563
|
+
gate: 'G4', action: 'start', status: 'blocked', timestamp,
|
|
2564
|
+
state_path: statePathValue,
|
|
2565
|
+
boundary_violations: boundaryValidation.violations,
|
|
2566
|
+
error: `State/boundary inválido: ${boundaryValidation.violations.join('; ')}`,
|
|
2567
|
+
next_action: 'regerar_state_path_com_boundary_real',
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2306
2571
|
if (cycle.active) {
|
|
2307
2572
|
return {
|
|
2308
2573
|
gate: 'G4',
|
|
@@ -2411,13 +2676,15 @@ function validatorStart(args, context) {
|
|
|
2411
2676
|
last_state_path: statePathValue,
|
|
2412
2677
|
repair: {
|
|
2413
2678
|
skill: WORKFLOW_CONFIG.skills.findings_repair,
|
|
2414
|
-
status: 'not_needed',
|
|
2415
|
-
required_from_attempt:
|
|
2416
|
-
requested_at:
|
|
2417
|
-
completed_at:
|
|
2679
|
+
status: cycle.repair.status === 'completed' ? 'completed' : 'not_needed',
|
|
2680
|
+
required_from_attempt: cycle.repair.required_from_attempt,
|
|
2681
|
+
requested_at: cycle.repair.requested_at,
|
|
2682
|
+
completed_at: cycle.repair.completed_at,
|
|
2418
2683
|
active: null,
|
|
2419
2684
|
},
|
|
2420
|
-
|
|
2685
|
+
// O retry precisa manter os findings originais: o complete do attempt 2
|
|
2686
|
+
// correlaciona `repaired_finding_ids` contra exatamente esse packet.
|
|
2687
|
+
findings_packet: attempt === 2 ? cycle.findings_packet : null,
|
|
2421
2688
|
},
|
|
2422
2689
|
};
|
|
2423
2690
|
}
|
|
@@ -2428,7 +2695,8 @@ function validatorComplete(args, context) {
|
|
|
2428
2695
|
const statePathValue = requiredString(args, 'state_path');
|
|
2429
2696
|
const activeValidatorRunId = requiredString(args, 'validator_run_id');
|
|
2430
2697
|
const verdict = requiredString(args, 'verdict');
|
|
2431
|
-
const
|
|
2698
|
+
const packetResult = normalizeFindingsPacket(optionalData(args));
|
|
2699
|
+
const packet = packetResult.packet;
|
|
2432
2700
|
// S04/S16: token de dispatch é obrigatório para fechar o slot ativo. Ele vem
|
|
2433
2701
|
// do validator_recovery lido pela folha fria e volta no output estruturado do
|
|
2434
2702
|
// validator. Sem token não existe garantia anti-stale completa.
|
|
@@ -2545,7 +2813,7 @@ function validatorComplete(args, context) {
|
|
|
2545
2813
|
// Falha (resposta ausente/hash divergente) NÃO fecha o slot — igual stale: active é
|
|
2546
2814
|
// preservado (não retornamos validator_cycle), o orquestrador re-despacha o MESMO
|
|
2547
2815
|
// validador (mesmo attempt) que lê o boundary e reenvia o hash correto. Não consome
|
|
2548
|
-
// attempt nem reabre terminal.
|
|
2816
|
+
// attempt nem reabre terminal. Arquivo sumido/ilegível consome o mesmo orçamento bounded.
|
|
2549
2817
|
const challengeCheck = verifyValidatorChallenge(cycle.active.challenge, challengeResponse, args);
|
|
2550
2818
|
if (!challengeCheck.ok) {
|
|
2551
2819
|
// P2-1: falhas de challenge são bounded por attempt. As anteriores ficam no
|
|
@@ -2602,9 +2870,17 @@ function validatorComplete(args, context) {
|
|
|
2602
2870
|
next_action: 'redespachar_o_mesmo_validador_irmao_que_le_o_boundary_e_reenvia_challenge_response',
|
|
2603
2871
|
};
|
|
2604
2872
|
}
|
|
2605
|
-
const challengeVerified = !cycle.active.challenge
|
|
2606
|
-
|
|
2607
|
-
|
|
2873
|
+
const challengeVerified = !cycle.active.challenge ? 'no_challenge' : 'verified';
|
|
2874
|
+
|
|
2875
|
+
if (packetResult.violations.length > 0) {
|
|
2876
|
+
return {
|
|
2877
|
+
gate: 'G4', action: 'complete', status: 'blocked', timestamp,
|
|
2878
|
+
validator_attempt: cycle.active.attempt, validator_run_id: activeValidatorRunId,
|
|
2879
|
+
state_path: statePathValue, validator_status: 'invalid_finding_shape',
|
|
2880
|
+
error: `Finding estruturado inválido: ${packetResult.violations.join('; ')}`,
|
|
2881
|
+
next_action: 'corrigir_shape_estruturado_do_finding',
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2608
2884
|
|
|
2609
2885
|
const normalizedVerdict = verdict === 'pass'
|
|
2610
2886
|
? 'passed'
|
|
@@ -2612,6 +2888,40 @@ function validatorComplete(args, context) {
|
|
|
2612
2888
|
? 'passed_with_observations'
|
|
2613
2889
|
: verdict;
|
|
2614
2890
|
|
|
2891
|
+
const blockingFindings = structuredBlockingFindings(packet);
|
|
2892
|
+
if (VALIDATOR_PASSED_STATUSES.has(normalizedVerdict) && blockingFindings.length > 0) {
|
|
2893
|
+
return {
|
|
2894
|
+
gate: 'G4', action: 'complete', status: 'blocked', timestamp,
|
|
2895
|
+
validator_attempt: cycle.active.attempt,
|
|
2896
|
+
validator_run_id: activeValidatorRunId,
|
|
2897
|
+
state_path: statePathValue,
|
|
2898
|
+
validator_status: 'invalid_verdict_severity',
|
|
2899
|
+
finding_ids: blockingFindings.map((finding) => finding.id ?? null),
|
|
2900
|
+
error: `${blockingFindings[0].severity} exige verdict fail`,
|
|
2901
|
+
next_action: 'corrigir_veredito_estruturado_do_validator',
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
if (cycle.active.attempt === 2
|
|
2906
|
+
&& cycle.repair.status === 'completed'
|
|
2907
|
+
&& VALIDATOR_PASSED_STATUSES.has(normalizedVerdict)) {
|
|
2908
|
+
const targetIds = structuredBlockingFindings(cycle.findings_packet)
|
|
2909
|
+
.map((finding) => finding.id)
|
|
2910
|
+
.filter((id) => typeof id === 'string' && id.trim());
|
|
2911
|
+
const correlated = new Set(Array.isArray(packet?.repaired_finding_ids) ? packet.repaired_finding_ids : []);
|
|
2912
|
+
const missingIds = targetIds.filter((id) => !correlated.has(id));
|
|
2913
|
+
if (missingIds.length > 0) {
|
|
2914
|
+
return {
|
|
2915
|
+
gate: 'G4', action: 'complete', status: 'blocked', timestamp,
|
|
2916
|
+
validator_attempt: cycle.active.attempt, validator_run_id: activeValidatorRunId,
|
|
2917
|
+
state_path: statePathValue, validator_status: 'repair_correlation_missing',
|
|
2918
|
+
missing_finding_ids: missingIds,
|
|
2919
|
+
error: 'Segundo validator não correlacionou todos os findings reparados',
|
|
2920
|
+
next_action: 'revalidar_repair_e_retornar_repaired_finding_ids',
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2615
2925
|
if (VALIDATOR_PASSED_STATUSES.has(normalizedVerdict)) {
|
|
2616
2926
|
return {
|
|
2617
2927
|
gate: 'G4',
|
|
@@ -2775,6 +3085,18 @@ function validatorRepairStart(args, context) {
|
|
|
2775
3085
|
};
|
|
2776
3086
|
}
|
|
2777
3087
|
|
|
3088
|
+
|
|
3089
|
+
const boundaryBefore = validateStateBoundary(statePathValue, args);
|
|
3090
|
+
if (!boundaryBefore.ok) {
|
|
3091
|
+
return {
|
|
3092
|
+
gate: 'G4', action: 'repair_start', status: 'blocked', timestamp,
|
|
3093
|
+
state_path: statePathValue,
|
|
3094
|
+
boundary_violations: boundaryBefore.violations,
|
|
3095
|
+
error: `Repair bloqueado por state/boundary inválido: ${boundaryBefore.violations.join('; ')}`,
|
|
3096
|
+
next_action: 'corrigir_state_path_antes_do_repair',
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
|
|
2778
3100
|
const activeRepairRunId = repairRunId(runId, cycle.attempts_used, timestamp);
|
|
2779
3101
|
return {
|
|
2780
3102
|
gate: 'G4',
|
|
@@ -2784,6 +3106,7 @@ function validatorRepairStart(args, context) {
|
|
|
2784
3106
|
validator_attempt: cycle.attempts_used,
|
|
2785
3107
|
repair_run_id: activeRepairRunId,
|
|
2786
3108
|
repair_budget: 1,
|
|
3109
|
+
findings: cycle.findings_packet?.findings ?? [],
|
|
2787
3110
|
state_path: statePathValue,
|
|
2788
3111
|
validator_status: 'repair_running',
|
|
2789
3112
|
next_action: `dispatch_${WORKFLOW_CONFIG.skills.findings_repair}`,
|
|
@@ -2800,6 +3123,12 @@ function validatorRepairStart(args, context) {
|
|
|
2800
3123
|
run_id: activeRepairRunId,
|
|
2801
3124
|
state_path: statePathValue,
|
|
2802
3125
|
started_at: timestamp,
|
|
3126
|
+
boundary_before: {
|
|
3127
|
+
head_sha: boundaryBefore.state.head_sha ?? null,
|
|
3128
|
+
diff_stat: boundaryBefore.state.diff_stat,
|
|
3129
|
+
files_changed: boundaryBefore.state.files_changed,
|
|
3130
|
+
worktree_final: boundaryBefore.state.worktree_final ?? null,
|
|
3131
|
+
},
|
|
2803
3132
|
},
|
|
2804
3133
|
},
|
|
2805
3134
|
},
|
|
@@ -2811,6 +3140,7 @@ function validatorRepairComplete(args, context) {
|
|
|
2811
3140
|
const cycle = normalizeValidatorCycle(context.state.data?.validator_cycle ?? {});
|
|
2812
3141
|
const statePathValue = requiredString(args, 'state_path');
|
|
2813
3142
|
const activeRepairRunId = requiredString(args, 'repair_run_id');
|
|
3143
|
+
const repairData = optionalData(args);
|
|
2814
3144
|
|
|
2815
3145
|
if (cycle.active) {
|
|
2816
3146
|
return {
|
|
@@ -2900,6 +3230,105 @@ function validatorRepairComplete(args, context) {
|
|
|
2900
3230
|
};
|
|
2901
3231
|
}
|
|
2902
3232
|
|
|
3233
|
+
|
|
3234
|
+
const boundaryAfter = validateStateBoundary(statePathValue, args);
|
|
3235
|
+
if (!boundaryAfter.ok) {
|
|
3236
|
+
return {
|
|
3237
|
+
gate: 'G4', action: 'repair_complete', status: 'blocked', timestamp,
|
|
3238
|
+
repair_run_id: activeRepairRunId, state_path: statePathValue,
|
|
3239
|
+
boundary_violations: boundaryAfter.violations,
|
|
3240
|
+
error: `Repair não atualizou state/boundary completo: ${boundaryAfter.violations.join('; ')}`,
|
|
3241
|
+
next_action: 'atualizar_head_stat_snapshots_files_e_evidencias_no_state_original',
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
const targets = structuredBlockingFindings(cycle.findings_packet).filter(
|
|
3246
|
+
(finding) => typeof finding.id === 'string' && finding.id.trim(),
|
|
3247
|
+
);
|
|
3248
|
+
const findings = Array.isArray(cycle.findings_packet?.findings) ? cycle.findings_packet.findings : [];
|
|
3249
|
+
const receivedIds = new Set(findings.map((finding) => finding?.id).filter(Boolean));
|
|
3250
|
+
const repairs = Array.isArray(repairData?.repairs) ? repairData.repairs : [];
|
|
3251
|
+
const stateRepairs = Array.isArray(boundaryAfter.state.repair_evidence)
|
|
3252
|
+
? boundaryAfter.state.repair_evidence
|
|
3253
|
+
: [];
|
|
3254
|
+
const repairViolations = [];
|
|
3255
|
+
for (const [label, entries] of [['output', repairs], ['state', stateRepairs]]) {
|
|
3256
|
+
const seen = new Set();
|
|
3257
|
+
for (const repair of entries) {
|
|
3258
|
+
const id = repair?.finding_id;
|
|
3259
|
+
if (!receivedIds.has(id)) repairViolations.push(`${label}: repair ID desconhecido ${id ?? '<ausente>'}`);
|
|
3260
|
+
if (seen.has(id)) repairViolations.push(`${label}: repair ID duplicado ${id}`);
|
|
3261
|
+
seen.add(id);
|
|
3262
|
+
if (!Array.isArray(repair?.files_touched) || repair.files_touched.length === 0) {
|
|
3263
|
+
repairViolations.push(`${label}: ${id ?? '<ausente>'} sem files_touched`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
const normalizeRepair = (repair) => ({
|
|
3268
|
+
finding_id: repair.finding_id,
|
|
3269
|
+
files_touched: [...repair.files_touched].sort(),
|
|
3270
|
+
checks_run: [...(repair.checks_run ?? [])].sort(),
|
|
3271
|
+
status: repair.status,
|
|
3272
|
+
});
|
|
3273
|
+
if (repairViolations.length === 0) {
|
|
3274
|
+
const outputNormalized = repairs.map(normalizeRepair).sort((a, b) => a.finding_id.localeCompare(b.finding_id));
|
|
3275
|
+
const stateNormalized = stateRepairs.map(normalizeRepair).sort((a, b) => a.finding_id.localeCompare(b.finding_id));
|
|
3276
|
+
if (JSON.stringify(outputNormalized) !== JSON.stringify(stateNormalized)) {
|
|
3277
|
+
repairViolations.push('output do repair diverge de repair_evidence persistido');
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
const before = cycle.repair.active.boundary_before;
|
|
3281
|
+
if (Array.isArray(before?.worktree_final) && Array.isArray(boundaryAfter.state.worktree_final)) {
|
|
3282
|
+
let committedDuringRepair = [];
|
|
3283
|
+
if (before.head_sha && before.head_sha !== boundaryAfter.state.head_sha) {
|
|
3284
|
+
try {
|
|
3285
|
+
committedDuringRepair = gitLines(consumerRoot(args), [
|
|
3286
|
+
'diff', '--name-only', `${before.head_sha}...${boundaryAfter.state.head_sha}`,
|
|
3287
|
+
]);
|
|
3288
|
+
} catch (error) {
|
|
3289
|
+
repairViolations.push(`não foi possível derivar commits do repair: ${error.message}`);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
const touchedReal = [...new Set([
|
|
3293
|
+
...snapshotDeltaFiles(before.worktree_final, boundaryAfter.state.worktree_final),
|
|
3294
|
+
...committedDuringRepair,
|
|
3295
|
+
])].sort();
|
|
3296
|
+
const touchedClaimed = [...new Set(repairs.flatMap((repair) => repair?.files_touched ?? []))].sort();
|
|
3297
|
+
if (JSON.stringify(touchedReal) !== JSON.stringify(touchedClaimed)) {
|
|
3298
|
+
repairViolations.push(`arquivos do repair divergem do delta real: esperado=${JSON.stringify(touchedReal)} recebido=${JSON.stringify(touchedClaimed)}`);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
if (repairViolations.length > 0) {
|
|
3302
|
+
return {
|
|
3303
|
+
gate: 'G4', action: 'repair_complete', status: 'blocked', timestamp,
|
|
3304
|
+
repair_run_id: activeRepairRunId, state_path: statePathValue,
|
|
3305
|
+
repair_violations: repairViolations,
|
|
3306
|
+
error: `Repair fora do boundary recebido: ${repairViolations.join('; ')}`,
|
|
3307
|
+
next_action: 'corrigir_correlacao_ids_arquivos_e_state_do_repair',
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
if (targets.length > 0) {
|
|
3311
|
+
const missing = targets.filter((target) => {
|
|
3312
|
+
const output = repairs.find((repair) => repair?.finding_id === target.id);
|
|
3313
|
+
const persisted = stateRepairs.find((repair) => repair?.finding_id === target.id);
|
|
3314
|
+
return output?.status !== 'resolved'
|
|
3315
|
+
|| !Array.isArray(output.files_touched) || output.files_touched.length === 0
|
|
3316
|
+
|| !Array.isArray(output.checks_run) || output.checks_run.length === 0
|
|
3317
|
+
|| persisted?.status !== 'resolved'
|
|
3318
|
+
|| !Array.isArray(persisted.files_touched) || persisted.files_touched.length === 0
|
|
3319
|
+
|| !Array.isArray(persisted.checks_run) || persisted.checks_run.length === 0;
|
|
3320
|
+
});
|
|
3321
|
+
if (missing.length > 0) {
|
|
3322
|
+
return {
|
|
3323
|
+
gate: 'G4', action: 'repair_complete', status: 'blocked', timestamp,
|
|
3324
|
+
repair_run_id: activeRepairRunId, state_path: statePathValue,
|
|
3325
|
+
unresolved_finding_ids: missing.map((finding) => finding.id),
|
|
3326
|
+
error: 'Repair sem evidência de resolução para finding P0/P1 alvo',
|
|
3327
|
+
next_action: 'persistir_correlacao_finding_arquivo_check_status',
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
|
|
2903
3332
|
return {
|
|
2904
3333
|
gate: 'G4',
|
|
2905
3334
|
action: 'repair_complete',
|
|
@@ -3384,6 +3813,7 @@ export {
|
|
|
3384
3813
|
checkPrerequisites,
|
|
3385
3814
|
checkJoinCapability,
|
|
3386
3815
|
expectedNextPhase,
|
|
3816
|
+
expectedExecutorSkill,
|
|
3387
3817
|
guaranteeLevelForMode,
|
|
3388
3818
|
classifyArtifactContent,
|
|
3389
3819
|
BANNER_TEMPLATES,
|
|
@@ -3396,6 +3826,8 @@ export {
|
|
|
3396
3826
|
preflight,
|
|
3397
3827
|
lockDispatch,
|
|
3398
3828
|
lockValidator,
|
|
3829
|
+
captureWorktreeSnapshot,
|
|
3830
|
+
validateStateBoundary,
|
|
3399
3831
|
assertAfterPlan,
|
|
3400
3832
|
runState,
|
|
3401
3833
|
ping,
|