oxe-cc 1.10.0 → 1.11.0

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.
@@ -86,6 +86,19 @@ function createExecutionContext(projectRoot, activeSession, options = {}) {
86
86
  const gateManager = (runtime && typeof runtime.GateManager === 'function')
87
87
  ? new runtime.GateManager(projectRoot, activeSession || null, runId)
88
88
  : null;
89
+ // Auto-wire PolicyEngine from .oxe/config.json runtime.policy when not explicitly injected
90
+ let policyEngine = options.policyEngine || null;
91
+ if (!policyEngine && runtime && typeof runtime.PolicyEngine === 'function') {
92
+ try {
93
+ const cfgPath = path.join(projectRoot, '.oxe', 'config.json');
94
+ if (fs.existsSync(cfgPath)) {
95
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
96
+ if (cfg.runtime && cfg.runtime.policy && typeof cfg.runtime.policy === 'object') {
97
+ policyEngine = runtime.PolicyEngine.fromConfig(cfg.runtime.policy);
98
+ }
99
+ }
100
+ } catch {}
101
+ }
89
102
  return {
90
103
  projectRoot,
91
104
  sessionId: activeSession || null,
@@ -93,7 +106,7 @@ function createExecutionContext(projectRoot, activeSession, options = {}) {
93
106
  executor: options.executor || null,
94
107
  workspaceManager: options.workspaceManager || null,
95
108
  gateManager,
96
- policyEngine: options.policyEngine || null,
109
+ policyEngine,
97
110
  policyActor: options.policyActor || 'runtime',
98
111
  quota: options.quota || null,
99
112
  pluginRegistry: options.pluginRegistry || null,
@@ -104,6 +117,44 @@ function createExecutionContext(projectRoot, activeSession, options = {}) {
104
117
  };
105
118
  }
106
119
 
120
+ /**
121
+ * Persists LLM provider config to .oxe/config.json under runtime.provider.
122
+ * @param {string} projectRoot
123
+ * @param {{ baseUrl?: string, model?: string, apiKeyEnv?: string, maxTurns?: number }} providerConfig
124
+ */
125
+ function saveRuntimeProviderConfig(projectRoot, providerConfig) {
126
+ const configPath = path.join(projectRoot, '.oxe', 'config.json');
127
+ let cfg = {};
128
+ try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
129
+ if (!cfg.runtime || typeof cfg.runtime !== 'object') cfg.runtime = {};
130
+ cfg.runtime.provider = {
131
+ baseUrl: providerConfig.baseUrl || 'https://api.anthropic.com/v1',
132
+ model: providerConfig.model || 'claude-sonnet-4-6',
133
+ };
134
+ if (providerConfig.apiKeyEnv) cfg.runtime.provider.apiKeyEnv = providerConfig.apiKeyEnv;
135
+ if (providerConfig.maxTurns != null) cfg.runtime.provider.maxTurns = providerConfig.maxTurns;
136
+ const oxeDir = path.join(projectRoot, '.oxe');
137
+ if (!fs.existsSync(oxeDir)) fs.mkdirSync(oxeDir, { recursive: true });
138
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
139
+ return cfg.runtime.provider;
140
+ }
141
+
142
+ /**
143
+ * Reads persisted LLM provider config from .oxe/config.json runtime.provider.
144
+ * @param {string} projectRoot
145
+ * @returns {{ baseUrl?: string, model?: string, apiKeyEnv?: string, maxTurns?: number } | null}
146
+ */
147
+ function loadRuntimeProviderConfig(projectRoot) {
148
+ try {
149
+ const cfg = JSON.parse(fs.readFileSync(path.join(projectRoot, '.oxe', 'config.json'), 'utf8'));
150
+ return (cfg.runtime && cfg.runtime.provider && typeof cfg.runtime.provider === 'object')
151
+ ? cfg.runtime.provider
152
+ : null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
107
158
  function buildRuntimePluginRegistry(projectRoot) {
108
159
  const runtime = loadRuntimeModule();
109
160
  if (!runtime || typeof runtime.PluginRegistry !== 'function') return null;
@@ -535,6 +586,37 @@ function compileExecutionGraphFromArtifacts(projectRoot, activeSession, options
535
586
  if (lintHints.length) validationErrors.push(...lintHints);
536
587
 
537
588
  const compiledGraph = runtime.toSerializable(graph);
589
+
590
+ // Gap B: enrich node verify.command from SPEC criteria howToVerify when linked via acceptance_refs.
591
+ // Extracts commands from backtick patterns (e.g. "Run `npm test`") and appends to existing command
592
+ // or sets as command when none exists. Only runs when spec has criteria.
593
+ if (parsedSpec.criteria && parsedSpec.criteria.length > 0 && compiledGraph.nodes) {
594
+ const criteriaMap = new Map(parsedSpec.criteria.map(c => [c.id, c]));
595
+ const nodeList = Array.isArray(compiledGraph.nodes)
596
+ ? compiledGraph.nodes
597
+ : Object.values(compiledGraph.nodes);
598
+ for (const node of nodeList) {
599
+ if (!node || !node.verify) continue;
600
+ const refs = Array.isArray(node.verify.acceptance_refs) ? node.verify.acceptance_refs : [];
601
+ const criteriaCommands = [];
602
+ for (const ref of refs) {
603
+ const criterion = criteriaMap.get(ref);
604
+ if (criterion && criterion.howToVerify) {
605
+ const cmdMatch = criterion.howToVerify.match(/`([^`]+)`/);
606
+ if (cmdMatch) criteriaCommands.push(cmdMatch[1].trim());
607
+ }
608
+ }
609
+ if (criteriaCommands.length === 0) continue;
610
+ const unique = [...new Set(criteriaCommands)];
611
+ if (node.verify.command) {
612
+ const extra = unique.filter(c => !node.verify.command.includes(c));
613
+ if (extra.length > 0) node.verify.command += ' && ' + extra.join(' && ');
614
+ } else {
615
+ node.verify.command = unique.join(' && ');
616
+ }
617
+ }
618
+ }
619
+
538
620
  const current = options.runState || readRunState(projectRoot, activeSession) || {};
539
621
  const runId = current.run_id || makeRunId();
540
622
  const next = writeRunState(projectRoot, activeSession, {
@@ -722,52 +804,52 @@ async function resolveRuntimeGate(projectRoot, activeSession, options = {}) {
722
804
  };
723
805
  }
724
806
 
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.');
807
+ // Gap 5: route execution to MultiAgentCoordinator when plan-agents.json exists
808
+ function buildRuntimeExecutePreflight(projectRoot, activeSession, runState) {
809
+ const health = loadProjectHealth();
810
+ const blockers = [];
811
+ const warnings = [];
812
+ let report = null;
813
+ if (health && typeof health.buildHealthReport === 'function') {
814
+ report = health.buildHealthReport(projectRoot);
815
+ if (report.planSelfEvaluation && report.planSelfEvaluation.executable === false) {
816
+ const confidence = report.planSelfEvaluation.confidence;
817
+ const threshold = report.planConfidenceThreshold || 90;
818
+ blockers.push(`plan_confidence:${confidence == null ? 'missing' : `${confidence}%`}<=${threshold}%`);
819
+ }
820
+ if (report.executionRationality && report.executionRationality.applicable && !report.executionRationalityReady) {
821
+ const gaps = Array.isArray(report.criticalExecutionGaps) ? report.criticalExecutionGaps : [];
822
+ blockers.push(`execution_rationality:${gaps[0] || 'not_ready'}`);
823
+ }
824
+ if (report.fallbackMode && report.fallbackMode !== 'none') {
825
+ warnings.push(`fallback_mode:${report.fallbackMode}`);
826
+ }
827
+ } else {
828
+ warnings.push('health_report_unavailable');
829
+ }
830
+ const runId = runState && runState.run_id ? runState.run_id : null;
831
+ const queue = readRuntimeGates(projectRoot, activeSession, { runId });
832
+ if (queue.pending.length > 0) {
833
+ blockers.push(`pending_gates:${queue.pending.length}`);
834
+ }
835
+ return {
836
+ ok: blockers.length === 0,
837
+ blockers,
838
+ warnings,
839
+ runId,
840
+ gateQueue: {
841
+ pending: queue.pending.length,
842
+ stale: queue.staleCount || 0,
843
+ },
844
+ confidence: report && report.planSelfEvaluation ? report.planSelfEvaluation.confidence : null,
845
+ confidenceThreshold: report ? report.planConfidenceThreshold || 90 : 90,
846
+ executionRationalityReady: report ? Boolean(report.executionRationalityReady) : false,
847
+ };
848
+ }
849
+
850
+ async function runRuntimeExecute(projectRoot, activeSession, options = {}) {
851
+ const runtime = loadRuntimeModule();
852
+ if (!runtime) throw new Error('Runtime package não está disponível. Rode npm run build:runtime.');
771
853
  const parsers = loadSdkParsers();
772
854
  if (!parsers) throw new Error('SDK parsers não disponíveis.');
773
855
 
@@ -787,50 +869,52 @@ function buildRuntimeExecutePreflight(projectRoot, activeSession, runState) {
787
869
 
788
870
  // Resolve compiled graph from run state or compile on demand
789
871
  let current = options.runState || readRunState(projectRoot, activeSession);
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;
825
-
826
- // Detect plan-agents.json (session path takes priority over root)
872
+ if (!current || !current.compiled_graph) {
873
+ current = compileExecutionGraphFromArtifacts(projectRoot, activeSession, { runState: current }).run;
874
+ }
875
+ if (!current || !current.compiled_graph) {
876
+ throw new Error('Nenhum grafo compilado encontrado. Execute oxe-cc runtime compile primeiro.');
877
+ }
878
+ const preflight = buildRuntimeExecutePreflight(projectRoot, activeSession, current);
879
+ if (!preflight.ok && !options.skipPreflight) {
880
+ const reason = preflight.blockers[0] || 'runtime_execute_preflight_failed';
881
+ appendEvent(projectRoot, activeSession, {
882
+ type: 'WorkItemBlocked',
883
+ run_id: current.run_id,
884
+ payload: {
885
+ reason: 'runtime_execute_preflight_failed',
886
+ blockers: preflight.blockers,
887
+ },
888
+ });
889
+ return {
890
+ mode: 'preflight',
891
+ agentPlan: null,
892
+ result: {
893
+ run_id: current.run_id,
894
+ status: 'blocked',
895
+ completed: [],
896
+ failed: [],
897
+ blocked: ['runtime_execute_preflight'],
898
+ reason,
899
+ },
900
+ run: current,
901
+ preflight,
902
+ };
903
+ }
904
+ const graph = runtime.fromSerializable
905
+ ? runtime.fromSerializable(current.compiled_graph)
906
+ : current.compiled_graph;
907
+
908
+ // Detect plan-agents.json: explicit override > session path > root path
827
909
  const rootAgentPlan = path.join(projectRoot, '.oxe', 'plan-agents.json');
828
910
  const sessAgentPlan = activeSession
829
911
  ? path.join(projectRoot, '.oxe', activeSession, 'plan', 'plan-agents.json')
830
912
  : null;
831
- const agentPlanPath = (sessAgentPlan && fs.existsSync(sessAgentPlan))
832
- ? sessAgentPlan
833
- : (fs.existsSync(rootAgentPlan) ? rootAgentPlan : null);
913
+ const agentPlanPath = (options.agentsPlanPath && fs.existsSync(options.agentsPlanPath))
914
+ ? options.agentsPlanPath
915
+ : (sessAgentPlan && fs.existsSync(sessAgentPlan))
916
+ ? sessAgentPlan
917
+ : (fs.existsSync(rootAgentPlan) ? rootAgentPlan : null);
834
918
 
835
919
  // Build ctx with GateManager (Gap 1)
836
920
  const ctx = createExecutionContext(projectRoot, activeSession, {
@@ -848,40 +932,40 @@ function buildRuntimeExecutePreflight(projectRoot, activeSession, runState) {
848
932
  // Gap 5: multi-agent path if plan-agents.json exists
849
933
  if (agentPlanPath) {
850
934
  let agentPlan;
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
- }));
935
+ try {
936
+ agentPlan = JSON.parse(fs.readFileSync(agentPlanPath, 'utf8'));
937
+ } catch (err) {
938
+ throw new Error(`plan-agents.json inválido: ${err.message}`);
939
+ }
940
+ const planErrors = validateMultiAgentPlan(agentPlan);
941
+ if (planErrors.length > 0) {
942
+ throw new Error(`plan-agents.json inválido: ${planErrors.join('; ')}`);
943
+ }
944
+ if (typeof runtime.MultiAgentCoordinator !== 'function') {
945
+ throw new Error('Runtime não exporta MultiAgentCoordinator. Verifique a versão do runtime.');
946
+ }
947
+ if (typeof runtime.GitWorktreeManager !== 'function' && !options.workspaceManager) {
948
+ throw new Error('Runtime não exporta GitWorktreeManager. Multi-agent real exige backend git_worktree.');
949
+ }
950
+ const agents = agentPlan.agents.map((spec) => ({
951
+ id: spec.id,
952
+ executor: options.executorFactory ? options.executorFactory(spec) : (options.executor || executor),
953
+ workspaceManager: options.workspaceManager || new runtime.GitWorktreeManager(projectRoot),
954
+ assignedTaskIds: Array.isArray(spec.tasks) ? spec.tasks : [],
955
+ }));
872
956
  const coordinator = new runtime.MultiAgentCoordinator();
873
957
  const result = await coordinator.run(graph, {
874
958
  mode: agentPlan.mode || 'parallel',
875
959
  agents,
876
960
  projectRoot,
877
961
  sessionId: activeSession || null,
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
- }
962
+ runId: current.run_id,
963
+ heartbeatTimeoutMs: options.heartbeatTimeoutMs ?? 120000,
964
+ applyWorkspaceMerges: true,
965
+ onEvent: ctx.onEvent,
966
+ });
967
+ return { mode: agentPlan.mode || 'parallel', agentPlan, result, run: current, preflight };
968
+ }
885
969
 
886
970
  // Single-agent fallback
887
971
  if (typeof runtime.Scheduler !== 'function') {
@@ -889,8 +973,8 @@ function buildRuntimeExecutePreflight(projectRoot, activeSession, runState) {
889
973
  }
890
974
  const scheduler = new runtime.Scheduler();
891
975
  const result = await scheduler.run(graph, ctx);
892
- return { mode: 'single', agentPlan: null, result, run: current, preflight };
893
- }
976
+ return { mode: 'single', agentPlan: null, result, run: current, preflight };
977
+ }
894
978
 
895
979
  function readRuntimeMultiAgentStatus(projectRoot, activeSession, options = {}) {
896
980
  const runtime = loadRuntimeModule();
@@ -905,49 +989,49 @@ function readRuntimeMultiAgentStatus(projectRoot, activeSession, options = {}) {
905
989
  workspaceIsolationEnforced: false,
906
990
  agents: [],
907
991
  ownership: [],
908
- orphanReassignments: [],
909
- handoffs: [],
910
- arbitrationResults: [],
911
- workspaceMergeReport: null,
912
- worktrees: [],
913
- mergeBlockers: [],
914
- mergeReadiness: null,
915
- arbitrationRequired: false,
916
- health: 'unknown',
917
- nextAction: 'Execute ou compile uma run antes de inspecionar status multi-agent.',
918
- summary: null,
919
- };
920
- }
992
+ orphanReassignments: [],
993
+ handoffs: [],
994
+ arbitrationResults: [],
995
+ workspaceMergeReport: null,
996
+ worktrees: [],
997
+ mergeBlockers: [],
998
+ mergeReadiness: null,
999
+ arbitrationRequired: false,
1000
+ health: 'unknown',
1001
+ nextAction: 'Execute ou compile uma run antes de inspecionar status multi-agent.',
1002
+ summary: null,
1003
+ };
1004
+ }
921
1005
  const runDir = path.join(projectRoot, '.oxe', 'runs', runId);
922
1006
  const statePath = path.join(runDir, 'multi-agent-state.json');
923
- const summaryPath = path.join(runDir, 'multi-agent-summary.json');
924
- const handoffsPath = path.join(runDir, 'handoffs.json');
925
- const arbitrationPath = path.join(runDir, 'arbitration-results.json');
926
- const workspaceMergePath = path.join(runDir, 'workspace-merge-report.json');
1007
+ const summaryPath = path.join(runDir, 'multi-agent-summary.json');
1008
+ const handoffsPath = path.join(runDir, 'handoffs.json');
1009
+ const arbitrationPath = path.join(runDir, 'arbitration-results.json');
1010
+ const workspaceMergePath = path.join(runDir, 'workspace-merge-report.json');
927
1011
  const state = runtime && typeof runtime.loadMultiAgentState === 'function'
928
1012
  ? runtime.loadMultiAgentState(projectRoot, runId)
929
1013
  : readJsonIfExists(statePath);
930
- const summary = runtime && typeof runtime.loadMultiAgentSummary === 'function'
931
- ? runtime.loadMultiAgentSummary(projectRoot, runId)
932
- : readJsonIfExists(summaryPath);
933
- const workspaceMergeReport = runtime && typeof runtime.loadWorkspaceMergeReport === 'function'
934
- ? runtime.loadWorkspaceMergeReport(projectRoot, runId)
935
- : readJsonIfExists(workspaceMergePath);
936
- const handoffs = readJsonIfExists(handoffsPath);
937
- const arbitrationResults = readJsonIfExists(arbitrationPath);
938
- const mergeBlockers = workspaceMergeReport && Array.isArray(workspaceMergeReport.blockers) ? workspaceMergeReport.blockers : [];
939
- const mergeReadiness = workspaceMergeReport && workspaceMergeReport.merge_readiness ? workspaceMergeReport.merge_readiness : null;
940
- let nextAction = null;
941
- if (!state) {
942
- nextAction = 'Execute runtime com plan-agents.json válido para materializar o estado multi-agent.';
943
- } else if (mergeBlockers.length > 0) {
944
- nextAction = 'Resolva os merge blockers do workspace antes de promover ou aplicar novos resultados.';
945
- } else if (mergeReadiness === 'partial') {
946
- nextAction = 'Conclua verify/evidence pós-merge ou aplique os worktrees pendentes antes de fechar a run.';
947
- } else if (mergeReadiness === 'ready') {
948
- nextAction = 'Multi-agent merge pronto; avance para runtime verify ou promotion conforme o ciclo.';
949
- }
950
- return {
1014
+ const summary = runtime && typeof runtime.loadMultiAgentSummary === 'function'
1015
+ ? runtime.loadMultiAgentSummary(projectRoot, runId)
1016
+ : readJsonIfExists(summaryPath);
1017
+ const workspaceMergeReport = runtime && typeof runtime.loadWorkspaceMergeReport === 'function'
1018
+ ? runtime.loadWorkspaceMergeReport(projectRoot, runId)
1019
+ : readJsonIfExists(workspaceMergePath);
1020
+ const handoffs = readJsonIfExists(handoffsPath);
1021
+ const arbitrationResults = readJsonIfExists(arbitrationPath);
1022
+ const mergeBlockers = workspaceMergeReport && Array.isArray(workspaceMergeReport.blockers) ? workspaceMergeReport.blockers : [];
1023
+ const mergeReadiness = workspaceMergeReport && workspaceMergeReport.merge_readiness ? workspaceMergeReport.merge_readiness : null;
1024
+ let nextAction = null;
1025
+ if (!state) {
1026
+ nextAction = 'Execute runtime com plan-agents.json válido para materializar o estado multi-agent.';
1027
+ } else if (mergeBlockers.length > 0) {
1028
+ nextAction = 'Resolva os merge blockers do workspace antes de promover ou aplicar novos resultados.';
1029
+ } else if (mergeReadiness === 'partial') {
1030
+ nextAction = 'Conclua verify/evidence pós-merge ou aplique os worktrees pendentes antes de fechar a run.';
1031
+ } else if (mergeReadiness === 'ready') {
1032
+ nextAction = 'Multi-agent merge pronto; avance para runtime verify ou promotion conforme o ciclo.';
1033
+ }
1034
+ return {
951
1035
  path: statePath,
952
1036
  enabled: Boolean(state),
953
1037
  runId,
@@ -956,44 +1040,44 @@ function readRuntimeMultiAgentStatus(projectRoot, activeSession, options = {}) {
956
1040
  agents: state && Array.isArray(state.agent_results) ? state.agent_results : [],
957
1041
  ownership: state && Array.isArray(state.ownership) ? state.ownership : [],
958
1042
  orphanReassignments: state && Array.isArray(state.orphan_reassignments) ? state.orphan_reassignments : [],
959
- handoffs: Array.isArray(handoffs) ? handoffs : [],
960
- arbitrationResults: Array.isArray(arbitrationResults) ? arbitrationResults : [],
961
- workspaceMergeReport: workspaceMergeReport || null,
962
- worktrees: workspaceMergeReport && Array.isArray(workspaceMergeReport.records) ? workspaceMergeReport.records : [],
963
- mergeBlockers,
964
- mergeReadiness,
965
- arbitrationRequired: Boolean(workspaceMergeReport && workspaceMergeReport.arbitration_required),
966
- health: summary && summary.health ? summary.health : (mergeBlockers.length > 0 ? 'degraded' : 'unknown'),
967
- nextAction,
968
- summary: summary || null,
969
- };
970
- }
971
-
972
- function validateMultiAgentPlan(agentPlan) {
973
- const errors = [];
974
- const allowedModes = new Set(['parallel', 'competitive', 'cooperative']);
975
- const mode = agentPlan && agentPlan.mode ? String(agentPlan.mode) : 'parallel';
976
- if (!allowedModes.has(mode)) errors.push(`mode inválido: ${mode}`);
977
- if (!Array.isArray(agentPlan && agentPlan.agents) || agentPlan.agents.length === 0) {
978
- errors.push('campo "agents" vazio ou ausente');
979
- return errors;
980
- }
981
- const schemaVersion = Number(agentPlan.schema_version || agentPlan.schema || 0);
982
- const seen = new Set();
983
- for (const [idx, spec] of agentPlan.agents.entries()) {
984
- const id = spec && spec.id ? String(spec.id) : '';
985
- if (!id) errors.push(`agents[${idx}].id ausente`);
986
- if (id && seen.has(id)) errors.push(`agent duplicado: ${id}`);
987
- if (id) seen.add(id);
988
- if (schemaVersion >= 3 && !spec.persona) errors.push(`${id || `agents[${idx}]`}.persona ausente`);
989
- if (schemaVersion >= 3 && !spec.model_hint) errors.push(`${id || `agents[${idx}]`}.model_hint ausente`);
990
- if (spec.tasks != null && !Array.isArray(spec.tasks)) errors.push(`${id || `agents[${idx}]`}.tasks deve ser array`);
991
- }
992
- if ((mode === 'competitive' || mode === 'cooperative') && agentPlan.agents.length < 2) {
993
- errors.push(`${mode} exige pelo menos 2 agentes`);
994
- }
995
- return errors;
996
- }
1043
+ handoffs: Array.isArray(handoffs) ? handoffs : [],
1044
+ arbitrationResults: Array.isArray(arbitrationResults) ? arbitrationResults : [],
1045
+ workspaceMergeReport: workspaceMergeReport || null,
1046
+ worktrees: workspaceMergeReport && Array.isArray(workspaceMergeReport.records) ? workspaceMergeReport.records : [],
1047
+ mergeBlockers,
1048
+ mergeReadiness,
1049
+ arbitrationRequired: Boolean(workspaceMergeReport && workspaceMergeReport.arbitration_required),
1050
+ health: summary && summary.health ? summary.health : (mergeBlockers.length > 0 ? 'degraded' : 'unknown'),
1051
+ nextAction,
1052
+ summary: summary || null,
1053
+ };
1054
+ }
1055
+
1056
+ function validateMultiAgentPlan(agentPlan) {
1057
+ const errors = [];
1058
+ const allowedModes = new Set(['parallel', 'competitive', 'cooperative']);
1059
+ const mode = agentPlan && agentPlan.mode ? String(agentPlan.mode) : 'parallel';
1060
+ if (!allowedModes.has(mode)) errors.push(`mode inválido: ${mode}`);
1061
+ if (!Array.isArray(agentPlan && agentPlan.agents) || agentPlan.agents.length === 0) {
1062
+ errors.push('campo "agents" vazio ou ausente');
1063
+ return errors;
1064
+ }
1065
+ const schemaVersion = Number(agentPlan.schema_version || agentPlan.schema || 0);
1066
+ const seen = new Set();
1067
+ for (const [idx, spec] of agentPlan.agents.entries()) {
1068
+ const id = spec && spec.id ? String(spec.id) : '';
1069
+ if (!id) errors.push(`agents[${idx}].id ausente`);
1070
+ if (id && seen.has(id)) errors.push(`agent duplicado: ${id}`);
1071
+ if (id) seen.add(id);
1072
+ if (schemaVersion >= 3 && !spec.persona) errors.push(`${id || `agents[${idx}]`}.persona ausente`);
1073
+ if (schemaVersion >= 3 && !spec.model_hint) errors.push(`${id || `agents[${idx}]`}.model_hint ausente`);
1074
+ if (spec.tasks != null && !Array.isArray(spec.tasks)) errors.push(`${id || `agents[${idx}]`}.tasks deve ser array`);
1075
+ }
1076
+ if ((mode === 'competitive' || mode === 'cooperative') && agentPlan.agents.length < 2) {
1077
+ errors.push(`${mode} exige pelo menos 2 agentes`);
1078
+ }
1079
+ return errors;
1080
+ }
997
1081
 
998
1082
  function loadRuntimeVerificationArtifacts(projectRoot, runState) {
999
1083
  const runtime = loadRuntimeModule();
@@ -1019,7 +1103,7 @@ function loadRuntimeVerificationArtifacts(projectRoot, runState) {
1019
1103
  return { manifest, residualRisks, evidenceCoverage };
1020
1104
  }
1021
1105
 
1022
- function countVerificationEvidenceRefs(runState, verificationArtifacts) {
1106
+ function countVerificationEvidenceRefs(runState, verificationArtifacts) {
1023
1107
  if (verificationArtifacts && verificationArtifacts.manifest && Array.isArray(verificationArtifacts.manifest.checks)) {
1024
1108
  return verificationArtifacts.manifest.checks.reduce((sum, check) => {
1025
1109
  return sum + (Array.isArray(check.evidence_refs) ? check.evidence_refs.length : 0);
@@ -1031,26 +1115,26 @@ function countVerificationEvidenceRefs(runState, verificationArtifacts) {
1031
1115
  }, 0);
1032
1116
  }
1033
1117
  return 0;
1034
- }
1035
-
1036
- function detectOrphanWorktrees(projectRoot, runId) {
1037
- const workspacesDir = path.join(projectRoot, '.oxe', 'workspaces');
1038
- if (!fs.existsSync(workspacesDir)) return [];
1039
- const active = new Set();
1040
- const mergeReport = readJsonIfExists(path.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json'));
1041
- for (const record of (mergeReport && Array.isArray(mergeReport.records) ? mergeReport.records : [])) {
1042
- if (record && record.workspace_id) active.add(String(record.workspace_id));
1043
- }
1044
- return fs.readdirSync(workspacesDir, { withFileTypes: true })
1045
- .filter((entry) => entry.isDirectory())
1046
- .map((entry) => entry.name)
1047
- .filter((name) => !active.has(name))
1048
- .map((name) => ({
1049
- workspace_id: name,
1050
- path: path.join(workspacesDir, name),
1051
- next_action: 'inspecione o worktree órfão; depois remova com git worktree remove --force se não houver diffs úteis',
1052
- }));
1053
- }
1118
+ }
1119
+
1120
+ function detectOrphanWorktrees(projectRoot, runId) {
1121
+ const workspacesDir = path.join(projectRoot, '.oxe', 'workspaces');
1122
+ if (!fs.existsSync(workspacesDir)) return [];
1123
+ const active = new Set();
1124
+ const mergeReport = readJsonIfExists(path.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json'));
1125
+ for (const record of (mergeReport && Array.isArray(mergeReport.records) ? mergeReport.records : [])) {
1126
+ if (record && record.workspace_id) active.add(String(record.workspace_id));
1127
+ }
1128
+ return fs.readdirSync(workspacesDir, { withFileTypes: true })
1129
+ .filter((entry) => entry.isDirectory())
1130
+ .map((entry) => entry.name)
1131
+ .filter((name) => !active.has(name))
1132
+ .map((name) => ({
1133
+ workspace_id: name,
1134
+ path: path.join(workspacesDir, name),
1135
+ next_action: 'inspecione o worktree órfão; depois remova com git worktree remove --force se não houver diffs úteis',
1136
+ }));
1137
+ }
1054
1138
 
1055
1139
  function buildRuntimeModeStatus(runState) {
1056
1140
  if (!runState) {
@@ -1222,17 +1306,17 @@ function writeRecoverySummaryMarkdown(projectRoot, activeSession, runState, reco
1222
1306
  '',
1223
1307
  '## Work items órfãos',
1224
1308
  '',
1225
- ...(Array.isArray(recoverySummary.orphan_work_items) && recoverySummary.orphan_work_items.length
1226
- ? recoverySummary.orphan_work_items.map((item) => `- ${item}`)
1227
- : ['- Nenhum']),
1228
- '',
1229
- '## Worktrees órfãos',
1230
- '',
1231
- ...(Array.isArray(recoverySummary.orphan_worktrees) && recoverySummary.orphan_worktrees.length
1232
- ? recoverySummary.orphan_worktrees.map((item) => `- ${item.workspace_id} · ${item.next_action}`)
1233
- : ['- Nenhum']),
1234
- '',
1235
- '## Tentativas incompletas',
1309
+ ...(Array.isArray(recoverySummary.orphan_work_items) && recoverySummary.orphan_work_items.length
1310
+ ? recoverySummary.orphan_work_items.map((item) => `- ${item}`)
1311
+ : ['- Nenhum']),
1312
+ '',
1313
+ '## Worktrees órfãos',
1314
+ '',
1315
+ ...(Array.isArray(recoverySummary.orphan_worktrees) && recoverySummary.orphan_worktrees.length
1316
+ ? recoverySummary.orphan_worktrees.map((item) => `- ${item.workspace_id} · ${item.next_action}`)
1317
+ : ['- Nenhum']),
1318
+ '',
1319
+ '## Tentativas incompletas',
1236
1320
  '',
1237
1321
  ...(recoverySummary.consistency && Array.isArray(recoverySummary.consistency.incomplete_attempts) && recoverySummary.consistency.incomplete_attempts.length
1238
1322
  ? recoverySummary.consistency.incomplete_attempts.map((item) => `- ${item.work_item_id} · ${item.attempt_id || 'attempt'} · ${item.outcome || 'unknown'}`)
@@ -1337,6 +1421,94 @@ async function runRuntimeVerify(projectRoot, activeSession, options = {}) {
1337
1421
  };
1338
1422
  }
1339
1423
 
1424
+ // Marks SPEC.md checklist items as [x] when the run is completed.
1425
+ // Finds **DoD Wave N:** blocks and numbered checklist sections (e.g. "21.1 MVP"),
1426
+ // marks unchecked items only within sections whose waves are fully completed.
1427
+ function applySpecChecklistSync(specPath, canonicalLive, compiledGraph) {
1428
+ if (!canonicalLive || (canonicalLive.run && canonicalLive.run.status !== 'completed')) return;
1429
+ if (!specPath || !fs.existsSync(specPath)) return;
1430
+
1431
+ const completedIds = new Set(
1432
+ canonicalLive.completedWorkItems instanceof Set
1433
+ ? [...canonicalLive.completedWorkItems]
1434
+ : Array.isArray(canonicalLive.completedWorkItems)
1435
+ ? canonicalLive.completedWorkItems
1436
+ : []
1437
+ );
1438
+ if (completedIds.size === 0) return;
1439
+
1440
+ const waves = Array.isArray(compiledGraph && compiledGraph.waves) ? compiledGraph.waves : [];
1441
+
1442
+ // Which wave numbers are fully completed (all node_ids in completedIds)?
1443
+ const completedWaveNums = new Set();
1444
+ for (const wave of waves) {
1445
+ if (Array.isArray(wave.node_ids) && wave.node_ids.length > 0 &&
1446
+ wave.node_ids.every(id => completedIds.has(id))) {
1447
+ completedWaveNums.add(wave.wave_number);
1448
+ }
1449
+ }
1450
+
1451
+ // Are ALL waves completed? (run is complete = yes, since status === 'completed')
1452
+ const allWavesDone = waves.length === 0 || waves.every(w =>
1453
+ Array.isArray(w.node_ids) && w.node_ids.every(id => completedIds.has(id))
1454
+ );
1455
+
1456
+ const lines = fs.readFileSync(specPath, 'utf8').split('\n');
1457
+ let changed = false;
1458
+
1459
+ // Tracking state: which section type are we marking?
1460
+ // 'none' | 'dod_wave' | 'checklist_mvp' | 'checklist_full'
1461
+ let markMode = 'none';
1462
+
1463
+ const result = lines.map((line, i) => {
1464
+ // Detect DoD Wave block header: **DoD Wave N:** or **DoD Wave N:** (with optional emoji/text)
1465
+ const dodMatch = line.match(/\*\*DoD Wave\s+(\d+):/i);
1466
+ if (dodMatch) {
1467
+ const waveNum = parseInt(dodMatch[1], 10);
1468
+ markMode = completedWaveNums.has(waveNum) ? 'dod_wave' : 'none';
1469
+ return line;
1470
+ }
1471
+
1472
+ // Detect numbered checklist section headers (e.g. "### 21.1 MVP", "### 21.2 v1.0.0")
1473
+ const checklistMatch = line.match(/^#{2,4}\s+\d+\.\d+\s+(.+)/i);
1474
+ if (checklistMatch) {
1475
+ const sectionTitle = checklistMatch[1].toLowerCase();
1476
+ // MVP / v0.x.x → mark when run is fully complete (pre-release milestones)
1477
+ // v1.0.0 or any X.Y.Z ≥ 1.0.0 → never auto-mark (requires explicit human sign-off)
1478
+ if (/mvp|v?0\.\d+\.\d+/i.test(sectionTitle) && allWavesDone) {
1479
+ markMode = 'checklist_mvp';
1480
+ } else {
1481
+ markMode = 'none';
1482
+ }
1483
+ return line;
1484
+ }
1485
+
1486
+ // Exit section on any top-level heading (##, ###) that isn't a checklist header
1487
+ if (/^#{1,3}\s/.test(line) && !checklistMatch) {
1488
+ markMode = 'none';
1489
+ return line;
1490
+ }
1491
+
1492
+ // Exit DoD section on **Limite técnico or **Condição para replanejar etc.
1493
+ if (markMode === 'dod_wave' && /^\*\*(?:Limite|Condição|Nota|Atenção|Warning)/.test(line)) {
1494
+ markMode = 'none';
1495
+ return line;
1496
+ }
1497
+
1498
+ // Mark unchecked items in active section
1499
+ if (markMode !== 'none' && /^- \[ \]/.test(line)) {
1500
+ changed = true;
1501
+ return line.replace(/^- \[ \]/, '- [x]');
1502
+ }
1503
+
1504
+ return line;
1505
+ });
1506
+
1507
+ if (changed) {
1508
+ fs.writeFileSync(specPath, result.join('\n'), 'utf8');
1509
+ }
1510
+ }
1511
+
1340
1512
  function projectRuntimeArtifacts(projectRoot, activeSession, options = {}) {
1341
1513
  const runtime = loadRuntimeModule();
1342
1514
  if (!runtime || typeof runtime.ProjectionEngine !== 'function' || typeof runtime.fromSerializable !== 'function') {
@@ -1406,6 +1578,8 @@ function projectRuntimeArtifacts(projectRoot, activeSession, options = {}) {
1406
1578
  fs.writeFileSync(path.join(op.executionRoot, 'COMMIT-SUMMARY.md'), projections.commitSummary + '\n', 'utf8');
1407
1579
  fs.writeFileSync(path.join(op.executionRoot, 'PROMOTION-SUMMARY.md'), projections.promotionSummary + '\n', 'utf8');
1408
1580
  fs.writeFileSync(path.join(op.executionRoot, 'PR-SUMMARY.md'), projections.prSummary + '\n', 'utf8');
1581
+ // Sync SPEC.md checklist: mark completed wave DoD sections and MVP checklist when run closes
1582
+ applySpecChecklistSync(paths.spec, canonicalLive, current.compiled_graph);
1409
1583
  }
1410
1584
  const next = writeRunState(projectRoot, activeSession, {
1411
1585
  ...current,
@@ -1595,14 +1769,14 @@ function recoverRuntimeState(projectRoot, activeSession, options = {}) {
1595
1769
  journal,
1596
1770
  verificationArtifacts
1597
1771
  );
1598
- const recoverySummary = {
1599
- recovered_at: new Date().toISOString(),
1600
- journal_state: journal.scheduler_state,
1601
- orphan_work_items: orphanWorkItems,
1602
- orphan_worktrees: detectOrphanWorktrees(projectRoot, current.run_id),
1603
- pending_gates: readRuntimeGates(projectRoot, activeSession, { runId: current.run_id }).pending.map((gate) => gate.gate_id),
1604
- consistency,
1605
- };
1772
+ const recoverySummary = {
1773
+ recovered_at: new Date().toISOString(),
1774
+ journal_state: journal.scheduler_state,
1775
+ orphan_work_items: orphanWorkItems,
1776
+ orphan_worktrees: detectOrphanWorktrees(projectRoot, current.run_id),
1777
+ pending_gates: readRuntimeGates(projectRoot, activeSession, { runId: current.run_id }).pending.map((gate) => gate.gate_id),
1778
+ consistency,
1779
+ };
1606
1780
  const runDir = path.join(projectRoot, '.oxe', 'runs', current.run_id);
1607
1781
  ensureDir(runDir);
1608
1782
  const recoverySummaryPath = path.join(runDir, 'recovery-summary.json');
@@ -2484,9 +2658,11 @@ module.exports = {
2484
2658
  buildRecoveryConsistency,
2485
2659
  readRuntimeGates,
2486
2660
  resolveRuntimeGate,
2487
- createExecutionContext,
2488
- buildRuntimeExecutePreflight,
2489
- runRuntimeExecute,
2661
+ createExecutionContext,
2662
+ saveRuntimeProviderConfig,
2663
+ loadRuntimeProviderConfig,
2664
+ buildRuntimeExecutePreflight,
2665
+ runRuntimeExecute,
2490
2666
  runRuntimeVerify,
2491
2667
  projectRuntimeArtifacts,
2492
2668
  runRuntimeCiChecks,
@@ -2496,4 +2672,5 @@ module.exports = {
2496
2672
  replayEvents,
2497
2673
  replayRuntimeState,
2498
2674
  readRuntimeMultiAgentStatus,
2675
+ applySpecChecklistSync,
2499
2676
  };