oxe-cc 1.8.0 → 1.9.1

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 (52) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +94 -0
  3. package/README.md +15 -12
  4. package/bin/lib/oxe-manifest.cjs +20 -13
  5. package/bin/lib/oxe-operational.cjs +305 -79
  6. package/bin/lib/oxe-project-health.cjs +79 -18
  7. package/bin/lib/oxe-rationality.cjs +9 -7
  8. package/bin/lib/oxe-release.cjs +26 -0
  9. package/bin/oxe-cc.js +224 -52
  10. package/docs/RELEASE-READINESS.md +6 -1
  11. package/lib/runtime/compiler/graph-compiler.js +1 -1
  12. package/lib/runtime/executor/action-tool-map.js +4 -0
  13. package/lib/runtime/executor/built-in-tools.js +27 -0
  14. package/lib/runtime/executor/llm-task-executor.d.ts +4 -1
  15. package/lib/runtime/executor/llm-task-executor.js +41 -5
  16. package/lib/runtime/executor/node-prompt-builder.d.ts +4 -1
  17. package/lib/runtime/executor/node-prompt-builder.js +13 -2
  18. package/lib/runtime/models/failure.d.ts +1 -1
  19. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +39 -0
  20. package/lib/runtime/scheduler/multi-agent-coordinator.js +222 -13
  21. package/lib/runtime/scheduler/scheduler.d.ts +5 -1
  22. package/lib/runtime/scheduler/scheduler.js +82 -14
  23. package/lib/runtime/verification/verification-compiler.js +7 -5
  24. package/lib/runtime/workspace/strategies/git-worktree.js +18 -9
  25. package/lib/sdk/index.cjs +48 -44
  26. package/oxe/templates/PLAN.template.md +23 -9
  27. package/oxe/templates/SPEC.template.md +55 -22
  28. package/oxe/workflows/plan.md +18 -6
  29. package/oxe/workflows/spec.md +31 -9
  30. package/package.json +106 -100
  31. package/packages/runtime/package.json +18 -18
  32. package/packages/runtime/src/compiler/graph-compiler.ts +1 -1
  33. package/packages/runtime/src/evidence/evidence-store.ts +2 -2
  34. package/packages/runtime/src/executor/action-tool-map.ts +4 -0
  35. package/packages/runtime/src/executor/built-in-tools.ts +29 -0
  36. package/packages/runtime/src/executor/llm-task-executor.ts +46 -4
  37. package/packages/runtime/src/executor/node-prompt-builder.ts +18 -1
  38. package/packages/runtime/src/models/failure.ts +2 -0
  39. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +320 -46
  40. package/packages/runtime/src/scheduler/scheduler.ts +93 -15
  41. package/packages/runtime/src/verification/verification-compiler.ts +7 -5
  42. package/packages/runtime/src/workspace/strategies/git-worktree.ts +24 -16
  43. package/vscode-extension/package.json +185 -185
  44. package/vscode-extension/oxe-agents-0.9.1.vsix +0 -0
  45. package/vscode-extension/oxe-agents-0.9.2.vsix +0 -0
  46. package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
  47. package/vscode-extension/oxe-agents-1.4.0.vsix +0 -0
  48. package/vscode-extension/oxe-agents-1.5.0.vsix +0 -0
  49. package/vscode-extension/oxe-agents-1.5.1.vsix +0 -0
  50. package/vscode-extension/oxe-agents-1.6.0.vsix +0 -0
  51. package/vscode-extension/oxe-agents-1.7.0.vsix +0 -0
  52. package/vscode-extension/oxe-agents-1.8.0.vsix +0 -0
@@ -456,6 +456,61 @@ function reduceCanonicalRunState(projectRoot, activeSession, options = {}) {
456
456
  return serializeCanonicalState(reduceCanonicalRunStateLive(projectRoot, activeSession, options));
457
457
  }
458
458
 
459
+ /**
460
+ * Static-analysis lints for common pitfalls detectable before execution.
461
+ * Returns hint strings to be appended to validationErrors (shown at compile time).
462
+ */
463
+ function lintPlanForCommonPitfalls(parsedPlan, parsedSpec, projectRoot, rawSpecText = '') {
464
+ const hints = [];
465
+ const specText = (parsedSpec && parsedSpec.objective ? parsedSpec.objective : '') +
466
+ JSON.stringify(parsedSpec && parsedSpec.criteria ? parsedSpec.criteria : []) +
467
+ (rawSpecText || '');
468
+ const planTasks = parsedPlan && Array.isArray(parsedPlan.tasks) ? parsedPlan.tasks : [];
469
+
470
+ // ── Lint 1: HTML/JS SPA sem restrição de file:// ──────────────────────────
471
+ // Detects: spec mentions HTML/SPA + no files restriction, but no verify command
472
+ // checks for fetch() absence. Warns to add a fetch-detection verify.
473
+ const isHtmlApp = /html|spa|browser|page|frontend|estático|static|aplicação web|web app|web page|interface web|\.html/i.test(specText);
474
+ if (isHtmlApp) {
475
+ const hasFetchGuard = planTasks.some(t =>
476
+ t.verifyCommand && /fetch|XMLHttpRequest|file:\/\//i.test(t.verifyCommand)
477
+ );
478
+ const specMentionsServer = /servidor|server|http-server|localhost|npx serve|vite|webpack/i.test(specText);
479
+ const specMentionsFileProtocol = /file:\/\/|sem servidor|without server|no.server/i.test(specText);
480
+ if (!hasFetchGuard && !specMentionsServer && !specMentionsFileProtocol) {
481
+ hints.push(
482
+ 'HINT(html-fetch): SPEC não declara se o app precisa de servidor HTTP. ' +
483
+ 'Se abrir em file://, adicione à SPEC: "sem servidor HTTP" e um verify que detecte fetch(): ' +
484
+ '`node -e "if(require(\'fs\').readFileSync(\'app.js\',\'utf8\').includes(\'fetch(\'))throw new Error(\'fetch not allowed in file://\')"`'
485
+ );
486
+ }
487
+ }
488
+
489
+ // ── Lint 2: Verify commands que só verificam existência de função, não comportamento ──
490
+ const existenceOnlyVerify = planTasks.filter(t =>
491
+ t.verifyCommand &&
492
+ /s\.includes\(/.test(t.verifyCommand) &&
493
+ !/existsSync|readFileSync.*utf8|require\(/.test(t.verifyCommand)
494
+ );
495
+ if (existenceOnlyVerify.length > 2) {
496
+ hints.push(
497
+ `HINT(verify-depth): ${existenceOnlyVerify.length} tarefa(s) verificam apenas presença de string no código ` +
498
+ `(s.includes). Considere adicionar verificações de comportamento: executar o código, não só inspecioná-lo.`
499
+ );
500
+ }
501
+
502
+ // ── Lint 3: Tarefas sem verify command (T-level) ──────────────────────────
503
+ const noVerify = planTasks.filter(t => !t.verifyCommand && !t.done);
504
+ if (noVerify.length > 0) {
505
+ hints.push(
506
+ `HINT(no-verify): ${noVerify.length} tarefa(s) sem Comando de verificação: ${noVerify.map(t => t.id).join(', ')}. ` +
507
+ `Tarefas sem verify não serão testadas inline pelo scheduler.`
508
+ );
509
+ }
510
+
511
+ return hints;
512
+ }
513
+
459
514
  function compileExecutionGraphFromArtifacts(projectRoot, activeSession, options = {}) {
460
515
  const runtime = loadRuntimeModule();
461
516
  const parsers = loadSdkParsers();
@@ -468,12 +523,17 @@ function compileExecutionGraphFromArtifacts(projectRoot, activeSession, options
468
523
  const artifactPaths = resolveRuntimeArtifactPaths(projectRoot, activeSession);
469
524
  const specText = readTextIfExists(artifactPaths.spec);
470
525
  const planText = readTextIfExists(artifactPaths.plan);
471
- if (!specText) throw new Error(`SPEC.md ausente em ${artifactPaths.spec}`);
472
- if (!planText) throw new Error(`PLAN.md ausente em ${artifactPaths.plan}`);
526
+ if (!specText) throw new Error(`SPEC.md ausente em ${artifactPaths.spec}\n Crie .oxe/SPEC.md com /oxe-spec no seu agente, ou use o template em oxe/templates/SPEC.template.md`);
527
+ if (!planText) throw new Error(`PLAN.md ausente em ${artifactPaths.plan}\n Crie .oxe/PLAN.md com /oxe-plan no seu agente (requer SPEC.md), ou use o template em oxe/templates/PLAN.template.md`);
473
528
  const parsedSpec = parsers.parseSpec(specText);
474
529
  const parsedPlan = parsers.parsePlan(planText);
475
530
  const graph = runtime.compile(parsedPlan, parsedSpec, options.compilerOptions || {});
476
531
  const validationErrors = typeof runtime.validateGraph === 'function' ? runtime.validateGraph(graph) : [];
532
+
533
+ // Static-analysis lints: detect common patterns that cause runtime failures
534
+ const lintHints = lintPlanForCommonPitfalls(parsedPlan, parsedSpec, projectRoot, specText);
535
+ if (lintHints.length) validationErrors.push(...lintHints);
536
+
477
537
  const compiledGraph = runtime.toSerializable(graph);
478
538
  const current = options.runState || readRunState(projectRoot, activeSession) || {};
479
539
  const runId = current.run_id || makeRunId();
@@ -662,24 +722,106 @@ async function resolveRuntimeGate(projectRoot, activeSession, options = {}) {
662
722
  };
663
723
  }
664
724
 
665
- // Gap 5: route execution to MultiAgentCoordinator when plan-agents.json exists
666
- async function runRuntimeExecute(projectRoot, activeSession, options = {}) {
667
- const runtime = loadRuntimeModule();
668
- if (!runtime) throw new Error('Runtime package não está disponível. Rode npm run build:runtime.');
725
+ // Gap 5: route execution to MultiAgentCoordinator when plan-agents.json exists
726
+ function buildRuntimeExecutePreflight(projectRoot, activeSession, runState) {
727
+ const health = loadProjectHealth();
728
+ const blockers = [];
729
+ const warnings = [];
730
+ let report = null;
731
+ if (health && typeof health.buildHealthReport === 'function') {
732
+ report = health.buildHealthReport(projectRoot);
733
+ if (report.planSelfEvaluation && report.planSelfEvaluation.executable === false) {
734
+ const confidence = report.planSelfEvaluation.confidence;
735
+ const threshold = report.planConfidenceThreshold || 90;
736
+ blockers.push(`plan_confidence:${confidence == null ? 'missing' : `${confidence}%`}<=${threshold}%`);
737
+ }
738
+ if (report.executionRationality && report.executionRationality.applicable && !report.executionRationalityReady) {
739
+ const gaps = Array.isArray(report.criticalExecutionGaps) ? report.criticalExecutionGaps : [];
740
+ blockers.push(`execution_rationality:${gaps[0] || 'not_ready'}`);
741
+ }
742
+ if (report.fallbackMode && report.fallbackMode !== 'none') {
743
+ warnings.push(`fallback_mode:${report.fallbackMode}`);
744
+ }
745
+ } else {
746
+ warnings.push('health_report_unavailable');
747
+ }
748
+ const runId = runState && runState.run_id ? runState.run_id : null;
749
+ const queue = readRuntimeGates(projectRoot, activeSession, { runId });
750
+ if (queue.pending.length > 0) {
751
+ blockers.push(`pending_gates:${queue.pending.length}`);
752
+ }
753
+ return {
754
+ ok: blockers.length === 0,
755
+ blockers,
756
+ warnings,
757
+ runId,
758
+ gateQueue: {
759
+ pending: queue.pending.length,
760
+ stale: queue.staleCount || 0,
761
+ },
762
+ confidence: report && report.planSelfEvaluation ? report.planSelfEvaluation.confidence : null,
763
+ confidenceThreshold: report ? report.planConfidenceThreshold || 90 : 90,
764
+ executionRationalityReady: report ? Boolean(report.executionRationalityReady) : false,
765
+ };
766
+ }
767
+
768
+ async function runRuntimeExecute(projectRoot, activeSession, options = {}) {
769
+ const runtime = loadRuntimeModule();
770
+ if (!runtime) throw new Error('Runtime package não está disponível. Rode npm run build:runtime.');
669
771
  const parsers = loadSdkParsers();
670
772
  if (!parsers) throw new Error('SDK parsers não disponíveis.');
671
773
 
774
+ // Auto-wire LlmTaskExecutor if providerConfig is supplied
775
+ let executor = options.executor || null;
776
+ if (!executor && options.providerConfig) {
777
+ if (typeof runtime.LlmTaskExecutor !== 'function') throw new Error('Runtime não exporta LlmTaskExecutor.');
778
+ executor = new runtime.LlmTaskExecutor(options.providerConfig, null, options.onProgress || null);
779
+ }
780
+ // Auto-wire InplaceWorkspaceManager as default
781
+ let workspaceManager = options.workspaceManager || null;
782
+ if (!workspaceManager) {
783
+ if (typeof runtime.InplaceWorkspaceManager === 'function') {
784
+ workspaceManager = new runtime.InplaceWorkspaceManager(projectRoot);
785
+ }
786
+ }
787
+
672
788
  // Resolve compiled graph from run state or compile on demand
673
789
  let current = options.runState || readRunState(projectRoot, activeSession);
674
- if (!current || !current.compiled_graph) {
675
- current = compileExecutionGraphFromArtifacts(projectRoot, activeSession, { runState: current }).run;
676
- }
677
- if (!current || !current.compiled_graph) {
678
- throw new Error('Nenhum grafo compilado encontrado. Execute oxe-cc runtime compile primeiro.');
679
- }
680
- const graph = runtime.fromSerializable
681
- ? runtime.fromSerializable(current.compiled_graph)
682
- : current.compiled_graph;
790
+ if (!current || !current.compiled_graph) {
791
+ current = compileExecutionGraphFromArtifacts(projectRoot, activeSession, { runState: current }).run;
792
+ }
793
+ if (!current || !current.compiled_graph) {
794
+ throw new Error('Nenhum grafo compilado encontrado. Execute oxe-cc runtime compile primeiro.');
795
+ }
796
+ const preflight = buildRuntimeExecutePreflight(projectRoot, activeSession, current);
797
+ if (!preflight.ok && !options.skipPreflight) {
798
+ const reason = preflight.blockers[0] || 'runtime_execute_preflight_failed';
799
+ appendEvent(projectRoot, activeSession, {
800
+ type: 'WorkItemBlocked',
801
+ run_id: current.run_id,
802
+ payload: {
803
+ reason: 'runtime_execute_preflight_failed',
804
+ blockers: preflight.blockers,
805
+ },
806
+ });
807
+ return {
808
+ mode: 'preflight',
809
+ agentPlan: null,
810
+ result: {
811
+ run_id: current.run_id,
812
+ status: 'blocked',
813
+ completed: [],
814
+ failed: [],
815
+ blocked: ['runtime_execute_preflight'],
816
+ reason,
817
+ },
818
+ run: current,
819
+ preflight,
820
+ };
821
+ }
822
+ const graph = runtime.fromSerializable
823
+ ? runtime.fromSerializable(current.compiled_graph)
824
+ : current.compiled_graph;
683
825
 
684
826
  // Detect plan-agents.json (session path takes priority over root)
685
827
  const rootAgentPlan = path.join(projectRoot, '.oxe', 'plan-agents.json');
@@ -693,45 +835,53 @@ async function runRuntimeExecute(projectRoot, activeSession, options = {}) {
693
835
  // Build ctx with GateManager (Gap 1)
694
836
  const ctx = createExecutionContext(projectRoot, activeSession, {
695
837
  runId: current.run_id,
696
- executor: options.executor || null,
697
- workspaceManager: options.workspaceManager || null,
838
+ executor,
839
+ workspaceManager,
698
840
  pluginRegistry: options.pluginRegistry || buildRuntimePluginRegistry(projectRoot),
699
841
  schedulerOptions: options.schedulerOptions || {},
700
- onEvent: (event) => appendEvent(projectRoot, activeSession, event),
842
+ onEvent: (event) => {
843
+ appendEvent(projectRoot, activeSession, event);
844
+ options.onProgress?.(event);
845
+ },
701
846
  });
702
847
 
703
848
  // Gap 5: multi-agent path if plan-agents.json exists
704
849
  if (agentPlanPath) {
705
850
  let agentPlan;
706
- try {
707
- agentPlan = JSON.parse(fs.readFileSync(agentPlanPath, 'utf8'));
708
- } catch (err) {
709
- throw new Error(`plan-agents.json inválido: ${err.message}`);
710
- }
711
- if (!Array.isArray(agentPlan.agents) || agentPlan.agents.length === 0) {
712
- throw new Error('plan-agents.json não contém agentes válidos (campo "agents" vazio ou ausente).');
713
- }
714
- if (typeof runtime.MultiAgentCoordinator !== 'function') {
715
- throw new Error('Runtime não exporta MultiAgentCoordinator. Verifique a versão do runtime.');
716
- }
717
- const agents = agentPlan.agents.map((spec) => ({
718
- id: spec.id,
719
- executor: options.executorFactory ? options.executorFactory(spec) : (options.executor || null),
720
- workspaceManager: options.workspaceManager || null,
721
- assignedTaskIds: Array.isArray(spec.tasks) ? spec.tasks : [],
722
- }));
851
+ try {
852
+ agentPlan = JSON.parse(fs.readFileSync(agentPlanPath, 'utf8'));
853
+ } catch (err) {
854
+ throw new Error(`plan-agents.json inválido: ${err.message}`);
855
+ }
856
+ const planErrors = validateMultiAgentPlan(agentPlan);
857
+ if (planErrors.length > 0) {
858
+ throw new Error(`plan-agents.json inválido: ${planErrors.join('; ')}`);
859
+ }
860
+ if (typeof runtime.MultiAgentCoordinator !== 'function') {
861
+ throw new Error('Runtime não exporta MultiAgentCoordinator. Verifique a versão do runtime.');
862
+ }
863
+ if (typeof runtime.GitWorktreeManager !== 'function' && !options.workspaceManager) {
864
+ throw new Error('Runtime não exporta GitWorktreeManager. Multi-agent real exige backend git_worktree.');
865
+ }
866
+ const agents = agentPlan.agents.map((spec) => ({
867
+ id: spec.id,
868
+ executor: options.executorFactory ? options.executorFactory(spec) : (options.executor || executor),
869
+ workspaceManager: options.workspaceManager || new runtime.GitWorktreeManager(projectRoot),
870
+ assignedTaskIds: Array.isArray(spec.tasks) ? spec.tasks : [],
871
+ }));
723
872
  const coordinator = new runtime.MultiAgentCoordinator();
724
873
  const result = await coordinator.run(graph, {
725
874
  mode: agentPlan.mode || 'parallel',
726
875
  agents,
727
876
  projectRoot,
728
877
  sessionId: activeSession || null,
729
- runId: current.run_id,
730
- heartbeatTimeoutMs: options.heartbeatTimeoutMs ?? 120000,
731
- onEvent: ctx.onEvent,
732
- });
733
- return { mode: agentPlan.mode || 'parallel', agentPlan, result, run: current };
734
- }
878
+ runId: current.run_id,
879
+ heartbeatTimeoutMs: options.heartbeatTimeoutMs ?? 120000,
880
+ applyWorkspaceMerges: true,
881
+ onEvent: ctx.onEvent,
882
+ });
883
+ return { mode: agentPlan.mode || 'parallel', agentPlan, result, run: current, preflight };
884
+ }
735
885
 
736
886
  // Single-agent fallback
737
887
  if (typeof runtime.Scheduler !== 'function') {
@@ -739,8 +889,8 @@ async function runRuntimeExecute(projectRoot, activeSession, options = {}) {
739
889
  }
740
890
  const scheduler = new runtime.Scheduler();
741
891
  const result = await scheduler.run(graph, ctx);
742
- return { mode: 'single', agentPlan: null, result, run: current };
743
- }
892
+ return { mode: 'single', agentPlan: null, result, run: current, preflight };
893
+ }
744
894
 
745
895
  function readRuntimeMultiAgentStatus(projectRoot, activeSession, options = {}) {
746
896
  const runtime = loadRuntimeModule();
@@ -755,23 +905,32 @@ function readRuntimeMultiAgentStatus(projectRoot, activeSession, options = {}) {
755
905
  workspaceIsolationEnforced: false,
756
906
  agents: [],
757
907
  ownership: [],
758
- orphanReassignments: [],
759
- handoffs: [],
760
- arbitrationResults: [],
761
- summary: null,
762
- };
908
+ orphanReassignments: [],
909
+ handoffs: [],
910
+ arbitrationResults: [],
911
+ workspaceMergeReport: null,
912
+ worktrees: [],
913
+ mergeBlockers: [],
914
+ mergeReadiness: null,
915
+ arbitrationRequired: false,
916
+ summary: null,
917
+ };
763
918
  }
764
919
  const runDir = path.join(projectRoot, '.oxe', 'runs', runId);
765
920
  const statePath = path.join(runDir, 'multi-agent-state.json');
766
- const summaryPath = path.join(runDir, 'multi-agent-summary.json');
767
- const handoffsPath = path.join(runDir, 'handoffs.json');
768
- const arbitrationPath = path.join(runDir, 'arbitration-results.json');
921
+ const summaryPath = path.join(runDir, 'multi-agent-summary.json');
922
+ const handoffsPath = path.join(runDir, 'handoffs.json');
923
+ const arbitrationPath = path.join(runDir, 'arbitration-results.json');
924
+ const workspaceMergePath = path.join(runDir, 'workspace-merge-report.json');
769
925
  const state = runtime && typeof runtime.loadMultiAgentState === 'function'
770
926
  ? runtime.loadMultiAgentState(projectRoot, runId)
771
927
  : readJsonIfExists(statePath);
772
- const summary = runtime && typeof runtime.loadMultiAgentSummary === 'function'
773
- ? runtime.loadMultiAgentSummary(projectRoot, runId)
774
- : readJsonIfExists(summaryPath);
928
+ const summary = runtime && typeof runtime.loadMultiAgentSummary === 'function'
929
+ ? runtime.loadMultiAgentSummary(projectRoot, runId)
930
+ : readJsonIfExists(summaryPath);
931
+ const workspaceMergeReport = runtime && typeof runtime.loadWorkspaceMergeReport === 'function'
932
+ ? runtime.loadWorkspaceMergeReport(projectRoot, runId)
933
+ : readJsonIfExists(workspaceMergePath);
775
934
  const handoffs = readJsonIfExists(handoffsPath);
776
935
  const arbitrationResults = readJsonIfExists(arbitrationPath);
777
936
  return {
@@ -784,10 +943,41 @@ function readRuntimeMultiAgentStatus(projectRoot, activeSession, options = {}) {
784
943
  ownership: state && Array.isArray(state.ownership) ? state.ownership : [],
785
944
  orphanReassignments: state && Array.isArray(state.orphan_reassignments) ? state.orphan_reassignments : [],
786
945
  handoffs: Array.isArray(handoffs) ? handoffs : [],
787
- arbitrationResults: Array.isArray(arbitrationResults) ? arbitrationResults : [],
788
- summary: summary || null,
789
- };
790
- }
946
+ arbitrationResults: Array.isArray(arbitrationResults) ? arbitrationResults : [],
947
+ workspaceMergeReport: workspaceMergeReport || null,
948
+ worktrees: workspaceMergeReport && Array.isArray(workspaceMergeReport.records) ? workspaceMergeReport.records : [],
949
+ mergeBlockers: workspaceMergeReport && Array.isArray(workspaceMergeReport.blockers) ? workspaceMergeReport.blockers : [],
950
+ mergeReadiness: workspaceMergeReport && workspaceMergeReport.merge_readiness ? workspaceMergeReport.merge_readiness : null,
951
+ arbitrationRequired: Boolean(workspaceMergeReport && workspaceMergeReport.arbitration_required),
952
+ summary: summary || null,
953
+ };
954
+ }
955
+
956
+ function validateMultiAgentPlan(agentPlan) {
957
+ const errors = [];
958
+ const allowedModes = new Set(['parallel', 'competitive', 'cooperative']);
959
+ const mode = agentPlan && agentPlan.mode ? String(agentPlan.mode) : 'parallel';
960
+ if (!allowedModes.has(mode)) errors.push(`mode inválido: ${mode}`);
961
+ if (!Array.isArray(agentPlan && agentPlan.agents) || agentPlan.agents.length === 0) {
962
+ errors.push('campo "agents" vazio ou ausente');
963
+ return errors;
964
+ }
965
+ const schemaVersion = Number(agentPlan.schema_version || agentPlan.schema || 0);
966
+ const seen = new Set();
967
+ for (const [idx, spec] of agentPlan.agents.entries()) {
968
+ const id = spec && spec.id ? String(spec.id) : '';
969
+ if (!id) errors.push(`agents[${idx}].id ausente`);
970
+ if (id && seen.has(id)) errors.push(`agent duplicado: ${id}`);
971
+ if (id) seen.add(id);
972
+ if (schemaVersion >= 3 && !spec.persona) errors.push(`${id || `agents[${idx}]`}.persona ausente`);
973
+ if (schemaVersion >= 3 && !spec.model_hint) errors.push(`${id || `agents[${idx}]`}.model_hint ausente`);
974
+ if (spec.tasks != null && !Array.isArray(spec.tasks)) errors.push(`${id || `agents[${idx}]`}.tasks deve ser array`);
975
+ }
976
+ if ((mode === 'competitive' || mode === 'cooperative') && agentPlan.agents.length < 2) {
977
+ errors.push(`${mode} exige pelo menos 2 agentes`);
978
+ }
979
+ return errors;
980
+ }
791
981
 
792
982
  function loadRuntimeVerificationArtifacts(projectRoot, runState) {
793
983
  const runtime = loadRuntimeModule();
@@ -813,7 +1003,7 @@ function loadRuntimeVerificationArtifacts(projectRoot, runState) {
813
1003
  return { manifest, residualRisks, evidenceCoverage };
814
1004
  }
815
1005
 
816
- function countVerificationEvidenceRefs(runState, verificationArtifacts) {
1006
+ function countVerificationEvidenceRefs(runState, verificationArtifacts) {
817
1007
  if (verificationArtifacts && verificationArtifacts.manifest && Array.isArray(verificationArtifacts.manifest.checks)) {
818
1008
  return verificationArtifacts.manifest.checks.reduce((sum, check) => {
819
1009
  return sum + (Array.isArray(check.evidence_refs) ? check.evidence_refs.length : 0);
@@ -825,7 +1015,26 @@ function countVerificationEvidenceRefs(runState, verificationArtifacts) {
825
1015
  }, 0);
826
1016
  }
827
1017
  return 0;
828
- }
1018
+ }
1019
+
1020
+ function detectOrphanWorktrees(projectRoot, runId) {
1021
+ const workspacesDir = path.join(projectRoot, '.oxe', 'workspaces');
1022
+ if (!fs.existsSync(workspacesDir)) return [];
1023
+ const active = new Set();
1024
+ const mergeReport = readJsonIfExists(path.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json'));
1025
+ for (const record of (mergeReport && Array.isArray(mergeReport.records) ? mergeReport.records : [])) {
1026
+ if (record && record.workspace_id) active.add(String(record.workspace_id));
1027
+ }
1028
+ return fs.readdirSync(workspacesDir, { withFileTypes: true })
1029
+ .filter((entry) => entry.isDirectory())
1030
+ .map((entry) => entry.name)
1031
+ .filter((name) => !active.has(name))
1032
+ .map((name) => ({
1033
+ workspace_id: name,
1034
+ path: path.join(workspacesDir, name),
1035
+ next_action: 'inspecione o worktree órfão; depois remova com git worktree remove --force se não houver diffs úteis',
1036
+ }));
1037
+ }
829
1038
 
830
1039
  function buildRuntimeModeStatus(runState) {
831
1040
  if (!runState) {
@@ -906,6 +1115,11 @@ function buildRecoveryConsistency(projectRoot, activeSession, runState, journal,
906
1115
  const runDir = runState && runState.run_id ? path.join(projectRoot, '.oxe', 'runs', runState.run_id) : null;
907
1116
  const allEvents = readEvents(projectRoot, activeSession);
908
1117
  const runEvents = runState && runState.run_id ? allEvents.filter((event) => event.run_id === runState.run_id) : [];
1118
+ // Detect if execution has ever started (at least one attempt recorded)
1119
+ const attemptCount = runState && runState.canonical_state && runState.canonical_state.summary
1120
+ ? (runState.canonical_state.summary.attempt_count || 0)
1121
+ : 0;
1122
+ const executionStarted = attemptCount > 0;
909
1123
  const issues = [];
910
1124
  if (!activeRunRef || activeRunRef.run_id !== (runState && runState.run_id)) {
911
1125
  issues.push('ACTIVE-RUN.json não referencia o mesmo run persistido em .oxe/runs/.');
@@ -913,10 +1127,12 @@ function buildRecoveryConsistency(projectRoot, activeSession, runState, journal,
913
1127
  if (!runFile || !fs.existsSync(runFile)) {
914
1128
  issues.push('Arquivo canónico da run ausente em .oxe/runs/<run>.json.');
915
1129
  }
916
- if (!journal) {
1130
+ // Journal is only created after execution starts — skip this check pre-execution
1131
+ if (!journal && executionStarted) {
917
1132
  issues.push('Journal ausente para recover/replay.');
918
1133
  }
919
- if (runEvents.length === 0) {
1134
+ // Events for this run only exist after execution — skip pre-execution
1135
+ if (runEvents.length === 0 && executionStarted) {
920
1136
  issues.push('Nenhum evento NDJSON encontrado para a run ativa.');
921
1137
  }
922
1138
  if (!runState || !runState.canonical_state) {
@@ -990,11 +1206,17 @@ function writeRecoverySummaryMarkdown(projectRoot, activeSession, runState, reco
990
1206
  '',
991
1207
  '## Work items órfãos',
992
1208
  '',
993
- ...(Array.isArray(recoverySummary.orphan_work_items) && recoverySummary.orphan_work_items.length
994
- ? recoverySummary.orphan_work_items.map((item) => `- ${item}`)
995
- : ['- Nenhum']),
996
- '',
997
- '## Tentativas incompletas',
1209
+ ...(Array.isArray(recoverySummary.orphan_work_items) && recoverySummary.orphan_work_items.length
1210
+ ? recoverySummary.orphan_work_items.map((item) => `- ${item}`)
1211
+ : ['- Nenhum']),
1212
+ '',
1213
+ '## Worktrees órfãos',
1214
+ '',
1215
+ ...(Array.isArray(recoverySummary.orphan_worktrees) && recoverySummary.orphan_worktrees.length
1216
+ ? recoverySummary.orphan_worktrees.map((item) => `- ${item.workspace_id} · ${item.next_action}`)
1217
+ : ['- Nenhum']),
1218
+ '',
1219
+ '## Tentativas incompletas',
998
1220
  '',
999
1221
  ...(recoverySummary.consistency && Array.isArray(recoverySummary.consistency.incomplete_attempts) && recoverySummary.consistency.incomplete_attempts.length
1000
1222
  ? recoverySummary.consistency.incomplete_attempts.map((item) => `- ${item.work_item_id} · ${item.attempt_id || 'attempt'} · ${item.outcome || 'unknown'}`)
@@ -1146,7 +1368,7 @@ function projectRuntimeArtifacts(projectRoot, activeSession, options = {}) {
1146
1368
  const paths = resolveRuntimeArtifactPaths(projectRoot, activeSession);
1147
1369
  const op = operationalPaths(projectRoot, activeSession);
1148
1370
  const projectionRefs = {
1149
- plan_ref: path.relative(projectRoot, paths.plan).replace(/\\/g, '/'),
1371
+ plan_ref: path.relative(projectRoot, paths.plan.replace(/PLAN\.md$/, 'PLAN-STATUS.md')).replace(/\\/g, '/'),
1150
1372
  verify_ref: path.relative(projectRoot, paths.verify).replace(/\\/g, '/'),
1151
1373
  state_ref: path.relative(projectRoot, paths.state).replace(/\\/g, '/'),
1152
1374
  run_summary_ref: path.relative(projectRoot, path.join(op.executionRoot, 'RUN-SUMMARY.md')).replace(/\\/g, '/'),
@@ -1156,10 +1378,12 @@ function projectRuntimeArtifacts(projectRoot, activeSession, options = {}) {
1156
1378
  generated_at: new Date().toISOString(),
1157
1379
  };
1158
1380
  if (options.write !== false) {
1159
- ensureDirForFile(paths.plan);
1381
+ // Write plan projection to PLAN-STATUS.md — never overwrite the source PLAN.md
1382
+ const planStatusPath = paths.plan.replace(/PLAN\.md$/, 'PLAN-STATUS.md');
1383
+ ensureDirForFile(planStatusPath);
1160
1384
  ensureDirForFile(paths.verify);
1161
1385
  ensureDirForFile(paths.state);
1162
- fs.writeFileSync(paths.plan, projections.plan + '\n', 'utf8');
1386
+ fs.writeFileSync(planStatusPath, projections.plan + '\n', 'utf8');
1163
1387
  fs.writeFileSync(paths.verify, projections.verify + '\n', 'utf8');
1164
1388
  fs.writeFileSync(paths.state, projections.state + '\n', 'utf8');
1165
1389
  fs.writeFileSync(path.join(op.executionRoot, 'RUN-SUMMARY.md'), projections.runSummary + '\n', 'utf8');
@@ -1355,13 +1579,14 @@ function recoverRuntimeState(projectRoot, activeSession, options = {}) {
1355
1579
  journal,
1356
1580
  verificationArtifacts
1357
1581
  );
1358
- const recoverySummary = {
1359
- recovered_at: new Date().toISOString(),
1360
- journal_state: journal.scheduler_state,
1361
- orphan_work_items: orphanWorkItems,
1362
- pending_gates: readRuntimeGates(projectRoot, activeSession, { runId: current.run_id }).pending.map((gate) => gate.gate_id),
1363
- consistency,
1364
- };
1582
+ const recoverySummary = {
1583
+ recovered_at: new Date().toISOString(),
1584
+ journal_state: journal.scheduler_state,
1585
+ orphan_work_items: orphanWorkItems,
1586
+ orphan_worktrees: detectOrphanWorktrees(projectRoot, current.run_id),
1587
+ pending_gates: readRuntimeGates(projectRoot, activeSession, { runId: current.run_id }).pending.map((gate) => gate.gate_id),
1588
+ consistency,
1589
+ };
1365
1590
  const runDir = path.join(projectRoot, '.oxe', 'runs', current.run_id);
1366
1591
  ensureDir(runDir);
1367
1592
  const recoverySummaryPath = path.join(runDir, 'recovery-summary.json');
@@ -2243,8 +2468,9 @@ module.exports = {
2243
2468
  buildRecoveryConsistency,
2244
2469
  readRuntimeGates,
2245
2470
  resolveRuntimeGate,
2246
- createExecutionContext,
2247
- runRuntimeExecute,
2471
+ createExecutionContext,
2472
+ buildRuntimeExecutePreflight,
2473
+ runRuntimeExecute,
2248
2474
  runRuntimeVerify,
2249
2475
  projectRuntimeArtifacts,
2250
2476
  runRuntimeCiChecks,