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.
Files changed (111) hide show
  1. package/README.md +2 -2
  2. package/VERSION +1 -1
  3. package/build/cli/atlas-init.mjs +12 -14
  4. package/build/tests/classify-findings.test.mjs +20 -0
  5. package/build/tests/etapa3.test.mjs +161 -0
  6. package/build/tests/test_classify_findings.py +79 -0
  7. package/hosts/opencode/.opencode/agents/atlas-findings-repair.md +4 -0
  8. package/hosts/opencode/.opencode/agents/atlas-task-validator.md +18 -1
  9. package/hosts/opencode/.opencode/atlas/VERSION +1 -1
  10. package/hosts/opencode/.opencode/atlas/orchestrator/README.md +7 -5
  11. package/hosts/opencode/.opencode/atlas/orchestrator/commands/workflow.md +1 -1
  12. package/hosts/opencode/.opencode/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
  13. package/hosts/opencode/.opencode/atlas/packages/mcp-server/README.md +1 -1
  14. package/hosts/opencode/.opencode/atlas/packages/mcp-server/package.json +1 -1
  15. package/hosts/opencode/.opencode/atlas/packages/mcp-server/server.js +446 -14
  16. package/hosts/opencode/.opencode/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  17. package/hosts/opencode/.opencode/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
  18. package/hosts/opencode/.opencode/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
  19. package/hosts/opencode/.opencode/skills/_shared/references/stack-profiles.md +36 -0
  20. package/hosts/opencode/.opencode/skills/_shared/scripts/document_quality.mjs +252 -0
  21. package/hosts/opencode/.opencode/skills/atlas-backlog-generator/SKILL.md +7 -2
  22. package/hosts/opencode/.opencode/skills/atlas-direct-execute/SKILL.md +6 -2
  23. package/hosts/opencode/.opencode/skills/atlas-findings-repair/SKILL.md +11 -1
  24. package/hosts/opencode/.opencode/skills/atlas-plan-execute/SKILL.md +16 -2
  25. package/hosts/opencode/.opencode/skills/atlas-plan-handoff/SKILL.md +6 -4
  26. package/hosts/opencode/.opencode/skills/atlas-prd-interview/SKILL.md +7 -2
  27. package/hosts/opencode/.opencode/skills/atlas-slice-review/SKILL.md +37 -2
  28. package/hosts/opencode/.opencode/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  29. package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  30. package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  31. package/hosts/opencode/.opencode/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  32. package/hosts/opencode/.opencode/skills/atlas-task-validator/SKILL.md +29 -14
  33. package/hosts/opencode/.opencode/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
  34. package/hosts/pi/.pi/agents/atlas-direct-execute.md +6 -2
  35. package/hosts/pi/.pi/agents/atlas-findings-repair.md +15 -1
  36. package/hosts/pi/.pi/agents/atlas-plan-execute.md +16 -2
  37. package/hosts/pi/.pi/agents/atlas-slice-review.md +37 -2
  38. package/hosts/pi/.pi/agents/atlas-task-validator.md +18 -1
  39. package/hosts/pi/atlas/VERSION +1 -1
  40. package/hosts/pi/atlas/orchestrator/README.md +7 -5
  41. package/hosts/pi/atlas/orchestrator/commands/workflow.md +1 -1
  42. package/hosts/pi/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
  43. package/hosts/pi/atlas/packages/mcp-server/README.md +1 -1
  44. package/hosts/pi/atlas/packages/mcp-server/package.json +1 -1
  45. package/hosts/pi/atlas/packages/mcp-server/server.js +446 -14
  46. package/hosts/pi/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  47. package/hosts/pi/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
  48. package/hosts/pi/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
  49. package/hosts/pi/skills/_shared/references/stack-profiles.md +36 -0
  50. package/hosts/pi/skills/_shared/scripts/document_quality.mjs +252 -0
  51. package/hosts/pi/skills/atlas-backlog-generator/SKILL.md +7 -2
  52. package/hosts/pi/skills/atlas-direct-execute/SKILL.md +6 -2
  53. package/hosts/pi/skills/atlas-findings-repair/SKILL.md +11 -1
  54. package/hosts/pi/skills/atlas-plan-execute/SKILL.md +16 -2
  55. package/hosts/pi/skills/atlas-plan-handoff/SKILL.md +6 -4
  56. package/hosts/pi/skills/atlas-prd-interview/SKILL.md +7 -2
  57. package/hosts/pi/skills/atlas-slice-review/SKILL.md +37 -2
  58. package/hosts/pi/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  59. package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  60. package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  61. package/hosts/pi/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  62. package/hosts/pi/skills/atlas-task-validator/SKILL.md +29 -14
  63. package/hosts/pi/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
  64. package/package.json +1 -1
  65. package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-findings-repair.toml +1 -1
  66. package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-task-validator.toml +1 -1
  67. package/plugins/atlas-workflow-orchestrator/.codex-plugin/plugin.json +1 -1
  68. package/plugins/atlas-workflow-orchestrator/VERSION +1 -1
  69. package/plugins/atlas-workflow-orchestrator/agents/atlas-findings-repair.md +4 -0
  70. package/plugins/atlas-workflow-orchestrator/agents/atlas-task-validator.md +18 -1
  71. package/plugins/atlas-workflow-orchestrator/orchestrator/README.md +7 -5
  72. package/plugins/atlas-workflow-orchestrator/orchestrator/commands/workflow.md +1 -1
  73. package/plugins/atlas-workflow-orchestrator/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
  74. package/plugins/atlas-workflow-orchestrator/packages/mcp-server/README.md +1 -1
  75. package/plugins/atlas-workflow-orchestrator/packages/mcp-server/package.json +1 -1
  76. package/plugins/atlas-workflow-orchestrator/packages/mcp-server/server.js +446 -14
  77. package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/references/stack-profiles.md +36 -0
  78. package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/scripts/document_quality.mjs +252 -0
  79. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-backlog-generator/SKILL.md +7 -2
  80. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-direct-execute/SKILL.md +6 -2
  81. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-findings-repair/SKILL.md +11 -1
  82. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-execute/SKILL.md +16 -2
  83. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-handoff/SKILL.md +6 -4
  84. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-prd-interview/SKILL.md +7 -2
  85. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/SKILL.md +37 -2
  86. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  87. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  88. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  89. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  90. package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-task-validator/SKILL.md +29 -14
  91. package/plugins/atlas-workflow-orchestrator/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  92. package/plugins/atlas-workflow-orchestrator/packages/templates/PRD_TEMPLATE.md +2 -1
  93. package/plugins/atlas-workflow-orchestrator/packages/templates/STATE_FILE_SCHEMA.md +25 -1
  94. package/plugins/atlas-workflow-orchestrator/skills/_shared/references/stack-profiles.md +36 -0
  95. package/plugins/atlas-workflow-orchestrator/skills/_shared/scripts/document_quality.mjs +252 -0
  96. package/plugins/atlas-workflow-orchestrator/skills/atlas-backlog-generator/SKILL.md +7 -2
  97. package/plugins/atlas-workflow-orchestrator/skills/atlas-direct-execute/SKILL.md +6 -2
  98. package/plugins/atlas-workflow-orchestrator/skills/atlas-findings-repair/SKILL.md +11 -1
  99. package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-execute/SKILL.md +16 -2
  100. package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-handoff/SKILL.md +6 -4
  101. package/plugins/atlas-workflow-orchestrator/skills/atlas-prd-interview/SKILL.md +7 -2
  102. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/SKILL.md +37 -2
  103. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
  104. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
  105. package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
  106. package/plugins/atlas-workflow-orchestrator/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
  107. package/plugins/atlas-workflow-orchestrator/skills/atlas-task-validator/SKILL.md +29 -14
  108. package/plugins/atlas-workflow-orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
  109. package/plugins/atlas-workflow-orchestrator/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
  110. package/plugins/atlas-workflow-orchestrator/templates/PRD_TEMPLATE.md +2 -1
  111. 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: true, unverifiable: true } — arquivo sumiu no complete (não bloqueia)
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: true, unverifiable: true };
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: null,
2416
- requested_at: null,
2417
- completed_at: null,
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
- findings_packet: null,
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 packet = optionalData(args);
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. `unverifiable` (arquivo sumiu no complete) não bloqueia.
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
- ? 'no_challenge'
2607
- : challengeCheck.unverifiable ? 'unverifiable' : 'verified';
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,