harness-engineering-agent 0.3.32 → 0.3.34

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 (61) hide show
  1. package/README.md +7 -6
  2. package/dist/agent/harness-role-capability-directory-data.d.ts.map +1 -1
  3. package/dist/agent/harness-role-capability-directory-data.js +2 -3
  4. package/dist/agent/harness-role-capability-directory-data.js.map +1 -1
  5. package/dist/controller/failure-factory.d.ts.map +1 -1
  6. package/dist/controller/failure-factory.js +27 -0
  7. package/dist/controller/failure-factory.js.map +1 -1
  8. package/dist/controller/index.d.ts +2 -0
  9. package/dist/controller/index.d.ts.map +1 -1
  10. package/dist/controller/index.js +1 -0
  11. package/dist/controller/index.js.map +1 -1
  12. package/dist/controller/parallel-task-delta.d.ts +24 -0
  13. package/dist/controller/parallel-task-delta.d.ts.map +1 -0
  14. package/dist/controller/parallel-task-delta.js +34 -0
  15. package/dist/controller/parallel-task-delta.js.map +1 -0
  16. package/dist/controller/repair-loop.d.ts.map +1 -1
  17. package/dist/controller/repair-loop.js +3 -4
  18. package/dist/controller/repair-loop.js.map +1 -1
  19. package/dist/controller/route-resolver.d.ts +1 -0
  20. package/dist/controller/route-resolver.d.ts.map +1 -1
  21. package/dist/controller/route-resolver.js +6 -2
  22. package/dist/controller/route-resolver.js.map +1 -1
  23. package/dist/controller/run-controller.d.ts.map +1 -1
  24. package/dist/controller/run-controller.js +1 -0
  25. package/dist/controller/run-controller.js.map +1 -1
  26. package/dist/controller/run-state.d.ts.map +1 -1
  27. package/dist/controller/run-state.js +3 -1
  28. package/dist/controller/run-state.js.map +1 -1
  29. package/dist/controller/task-executor.d.ts.map +1 -1
  30. package/dist/controller/task-executor.js +278 -100
  31. package/dist/controller/task-executor.js.map +1 -1
  32. package/dist/domain/failure.d.ts +221 -0
  33. package/dist/domain/failure.d.ts.map +1 -1
  34. package/dist/domain/failure.js +5 -0
  35. package/dist/domain/failure.js.map +1 -1
  36. package/dist/domain/report.d.ts +470 -0
  37. package/dist/domain/report.d.ts.map +1 -1
  38. package/dist/domain/run.d.ts +481 -0
  39. package/dist/domain/run.d.ts.map +1 -1
  40. package/dist/domain/run.js +9 -1
  41. package/dist/domain/run.js.map +1 -1
  42. package/dist/plugin/harness-chat-hook.d.ts +1 -0
  43. package/dist/plugin/harness-chat-hook.d.ts.map +1 -1
  44. package/dist/plugin/harness-chat-hook.js +18 -2
  45. package/dist/plugin/harness-chat-hook.js.map +1 -1
  46. package/dist/plugin/harness-chat-protocol.d.ts.map +1 -1
  47. package/dist/plugin/harness-chat-protocol.js +1 -1
  48. package/dist/plugin/harness-chat-protocol.js.map +1 -1
  49. package/dist/plugin/harness-coordinator.d.ts.map +1 -1
  50. package/dist/plugin/harness-coordinator.js +14 -3
  51. package/dist/plugin/harness-coordinator.js.map +1 -1
  52. package/dist/plugin/harness-escalate-tool.d.ts.map +1 -1
  53. package/dist/plugin/harness-escalate-tool.js +16 -2
  54. package/dist/plugin/harness-escalate-tool.js.map +1 -1
  55. package/dist/plugin/harness-request-router.d.ts +13 -0
  56. package/dist/plugin/harness-request-router.d.ts.map +1 -1
  57. package/dist/plugin/harness-request-router.js +26 -0
  58. package/dist/plugin/harness-request-router.js.map +1 -1
  59. package/dist/store/run-event.d.ts +470 -0
  60. package/dist/store/run-event.d.ts.map +1 -1
  61. package/package.json +1 -1
@@ -1,14 +1,16 @@
1
1
  import { AgentResultSchema } from "../domain/index.js";
2
2
  import { normalizeVerificationCommand } from "../verification/command-policy.js";
3
3
  import { VerificationExecutionStatus } from "../verification/index.js";
4
- import { createGitWorkspaceAdapter, createGitWorktreeAdapter } from "../workspace/index.js";
4
+ import { createGitWorkspaceAdapter, createGitWorktreeAdapter, } from "../workspace/index.js";
5
5
  import { createEvidence } from "./evidence-factory.js";
6
6
  import { resolveFailureAnalysis } from "./failure-analysis.js";
7
7
  import { classifyFailureScope } from "./failure-classification.js";
8
8
  import { createContextGapFailure, createImplementationFailure, createMergeConflictFailure, createModelFailure, createScopeDriftFailure, createVerificationEnvironmentFailure, } from "./failure-factory.js";
9
9
  import { orderTasks } from "./graph-validator.js";
10
+ import { createParallelTaskDelta } from "./parallel-task-delta.js";
10
11
  import { reassessRemainingTasks } from "./reassessor.js";
11
12
  import { runRepairLoop } from "./repair-loop.js";
13
+ import { resolveRoute } from "./route-resolver.js";
12
14
  import { finalizeRunAcceptance } from "./run-acceptance.js";
13
15
  import { consumeBudget, parsePlannedRun, RunBudgetExceededError, withParts, withPlannedStatus, withTask, } from "./run-state.js";
14
16
  import { readFailureAnalyzerOutput, readTaskReviewOutput, readWorkerOutput, } from "./task-agent-output.js";
@@ -712,11 +714,56 @@ export const createTaskExecutor = (input) => {
712
714
  const runTasks = async (run) => {
713
715
  let currentRun = run;
714
716
  const orderedTasks = orderTasks(currentRun.taskGraph);
715
- const isParallelRoute = currentRun.routingDecision?.scheduling === "parallel" ||
716
- currentRun.resolvedRoute === "parallel";
717
- if (isParallelRoute) {
718
- const completedTaskIds = new Set();
719
- const remainingTaskIds = new Set(orderedTasks.map((task) => task.taskId));
717
+ let schedulingMode = currentRun.routingDecision?.scheduling ?? "serial";
718
+ const refreshBatchRouting = async (candidate, completedTaskIds) => {
719
+ const assessment = candidate.complexityAssessment ?? {
720
+ tier: "complex",
721
+ rationale: "No Intake assessment was persisted.",
722
+ estimatedFileCount: Math.max(1, candidate.taskGraph.tasks.length),
723
+ estimatedCrossModuleDeps: 0,
724
+ suggestedRoute: "full",
725
+ confidence: "low",
726
+ };
727
+ const resolution = resolveRoute(assessment, {
728
+ taskGraph: candidate.taskGraph,
729
+ workspaceMode: candidate.workspace.git.mode,
730
+ completedTaskIds,
731
+ });
732
+ const currentScheduling = candidate.routingDecision?.scheduling ?? "serial";
733
+ const currentGovernance = candidate.routingDecision?.governance ??
734
+ (candidate.resolvedRoute === "parallel" ? "full" : candidate.resolvedRoute);
735
+ const currentRoute = candidate.resolvedRoute;
736
+ const nextRoute = resolution.resolvedRoute;
737
+ if (currentScheduling === resolution.routingDecision.scheduling &&
738
+ currentGovernance === resolution.routingDecision.governance &&
739
+ candidate.resolvedTier === resolution.resolvedTier &&
740
+ currentRoute === nextRoute) {
741
+ schedulingMode = resolution.routingDecision.scheduling;
742
+ return candidate;
743
+ }
744
+ const routed = parsePlannedRun({
745
+ ...candidate,
746
+ resolvedTier: resolution.resolvedTier,
747
+ resolvedRoute: nextRoute,
748
+ routingDecision: { ...resolution.routingDecision, source: "task-graph" },
749
+ });
750
+ schedulingMode = routed.routingDecision?.scheduling ?? "serial";
751
+ await input.writer.persist(routed, input.writer.createEvent(candidate.runId, "run.scheduling.recalculated", {
752
+ previousScheduling: currentScheduling,
753
+ scheduling: schedulingMode,
754
+ previousRoute: currentRoute,
755
+ route: nextRoute,
756
+ completedTaskIds: [...completedTaskIds].sort(),
757
+ reasons: resolution.routingDecision.reasons,
758
+ }));
759
+ return routed;
760
+ };
761
+ while (true) {
762
+ const completedTaskIds = new Set(currentRun.taskGraph.tasks
763
+ .filter((task) => task.status === "completed")
764
+ .map((task) => task.taskId));
765
+ const remainingTaskIds = new Set(orderedTasks.map((task) => task.taskId).filter((taskId) => !completedTaskIds.has(taskId)));
766
+ currentRun = await refreshBatchRouting(currentRun, completedTaskIds);
720
767
  while (remainingTaskIds.size > 0) {
721
768
  const readyTasks = orderedTasks.filter((task) => {
722
769
  if (completedTaskIds.has(task.taskId))
@@ -726,10 +773,10 @@ export const createTaskExecutor = (input) => {
726
773
  if (readyTasks.length === 0) {
727
774
  break;
728
775
  }
729
- if (readyTasks.length >= 2) {
730
- const batchResult = await runParallelReadyBatch(currentRun, readyTasks);
776
+ if (readyTasks.length >= 2 && schedulingMode === "parallel") {
777
+ const batchResult = await runParallelReadyBatch(currentRun, orderedTasks, completedTaskIds);
731
778
  if (batchResult.kind === "paused") {
732
- return batchResult.run;
779
+ return reassessAfterFailure(batchResult.run);
733
780
  }
734
781
  if (batchResult.kind === "completed") {
735
782
  currentRun = batchResult.run;
@@ -786,7 +833,7 @@ export const createTaskExecutor = (input) => {
786
833
  break;
787
834
  currentRun = await runTask(currentRun, task);
788
835
  if (currentRun.status !== "executing") {
789
- return currentRun;
836
+ return reassessAfterFailure(currentRun);
790
837
  }
791
838
  const completedTask = currentRun.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
792
839
  if (completedTask?.status === "completed") {
@@ -830,62 +877,19 @@ export const createTaskExecutor = (input) => {
830
877
  reason: event.reason,
831
878
  }));
832
879
  }
880
+ const nextCompletedTaskIds = new Set(currentRun.taskGraph.tasks
881
+ .filter((candidate) => candidate.status === "completed")
882
+ .map((candidate) => candidate.taskId));
883
+ currentRun = await refreshBatchRouting(currentRun, nextCompletedTaskIds);
884
+ if (schedulingMode === "parallel")
885
+ break;
833
886
  }
834
887
  }
835
888
  }
836
- }
837
- else {
838
- for (const task of orderedTasks) {
839
- const currentTask = currentRun.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
840
- if (currentTask?.status === "completed") {
841
- continue;
842
- }
843
- currentRun = await runTask(currentRun, task);
844
- if (currentRun.status !== "executing") {
845
- return currentRun;
846
- }
847
- const completedTask = currentRun.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
848
- if (completedTask?.status === "completed") {
849
- const reassessment = reassessRemainingTasks({
850
- run: currentRun,
851
- completedTask,
852
- allTasks: currentRun.taskGraph.tasks,
853
- });
854
- if (reassessment.shouldReassess && reassessment.event && reassessment.newTier) {
855
- const event = reassessment.event;
856
- const newTier = reassessment.newTier;
857
- const updatedTasks = currentRun.taskGraph.tasks.map((t) => {
858
- if (t.taskId === completedTask.taskId)
859
- return t;
860
- if (!event.affectedRemainingTasks.includes(t.taskId))
861
- return t;
862
- return { ...t, complexityTier: newTier };
863
- });
864
- currentRun = parsePlannedRun({
865
- ...currentRun,
866
- resolvedTier: newTier,
867
- routingDecision: {
868
- ...currentRun.routingDecision,
869
- tier: newTier,
870
- governance: newTier === "complex"
871
- ? "full"
872
- : (currentRun.routingDecision?.governance ?? "streamlined"),
873
- scheduling: currentRun.routingDecision?.scheduling ?? "serial",
874
- source: "reassessment",
875
- reasons: [event.reason],
876
- },
877
- reassessmentHistory: [...currentRun.reassessmentHistory, event],
878
- taskGraph: { ...currentRun.taskGraph, tasks: updatedTasks },
879
- });
880
- await input.writer.persist(currentRun, input.writer.createEvent(currentRun.runId, "task.reassessment", {
881
- triggerTaskId: event.triggerTaskId,
882
- previousTier: event.previousTier,
883
- newTier: event.newTier,
884
- reason: event.reason,
885
- }));
886
- }
887
- }
889
+ if (remainingTaskIds.size > 0 && schedulingMode === "parallel") {
890
+ continue;
888
891
  }
892
+ break;
889
893
  }
890
894
  currentRun = await runIntegrationVerification(currentRun);
891
895
  const runAcceptanceResult = await finalizeRunAcceptance({
@@ -893,9 +897,27 @@ export const createTaskExecutor = (input) => {
893
897
  runtime: input.runtime,
894
898
  run: currentRun,
895
899
  });
900
+ const acceptanceBlockingDetails = runAcceptanceResult.status === "blocked"
901
+ ? {
902
+ kind: "blocked",
903
+ missingContext: runAcceptanceResult.runAcceptance.criterionDecisions
904
+ .filter((decision) => decision.status === "blocked" || decision.status === "unknown")
905
+ .map((decision) => decision.criterionId),
906
+ discoveredEvidence: runAcceptanceResult.runAcceptance.criterionDecisions.flatMap((decision) => decision.evidenceIds),
907
+ nextActions: ["补充缺失证据后执行 resume", "检查阻塞 criterion 后重新验收"],
908
+ failureIds: runAcceptanceResult.runAcceptance.criterionDecisions.flatMap((decision) => decision.failureIds),
909
+ criterionIds: runAcceptanceResult.runAcceptance.criterionDecisions
910
+ .filter((decision) => decision.status === "blocked" || decision.status === "unknown")
911
+ .map((decision) => decision.criterionId),
912
+ allowedOperations: ["resume", "report", "cancel"],
913
+ }
914
+ : undefined;
896
915
  const completed = withPlannedStatus(withParts(parsePlannedRun({
897
916
  ...currentRun,
898
917
  runAcceptance: runAcceptanceResult.runAcceptance,
918
+ ...(acceptanceBlockingDetails === undefined
919
+ ? {}
920
+ : { blockingDetails: acceptanceBlockingDetails }),
899
921
  }), {
900
922
  evidence: currentRun.evidence,
901
923
  failures: runAcceptanceResult.failure === undefined
@@ -921,10 +943,6 @@ export const createTaskExecutor = (input) => {
921
943
  verificationCommands: commands,
922
944
  defaultWorkingDirectory: runWorkingDirectory(run),
923
945
  });
924
- if (result.records.length === 0 ||
925
- result.records.some((record) => record.status !== VerificationExecutionStatus.passed)) {
926
- return persistIntegrationVerificationFailure(run, task, "integration verification failed");
927
- }
928
946
  const evidence = result.records.map((record, index) => createEvidence({
929
947
  ids: input.options.ids,
930
948
  runId: run.runId,
@@ -940,6 +958,63 @@ export const createTaskExecutor = (input) => {
940
958
  agentResults: run.agentResults,
941
959
  workspace: run.workspace,
942
960
  });
961
+ if (result.records.length === 0) {
962
+ const failure = createContextGapFailure({
963
+ failureId: input.options.ids.createFailureId(`${run.runId}-integration-empty`),
964
+ runId: run.runId,
965
+ taskId: task.taskId,
966
+ relatedEvidenceIds: [],
967
+ summary: "Integration verification produced no evidence records.",
968
+ detail: "At least one integration verification record is required before acceptance.",
969
+ recoveryPlan: "Rerun integration verification and capture a real result.",
970
+ missingContext: ["integration-verification-record"],
971
+ });
972
+ return (await analyzeFailure({
973
+ run: withParts(verified, {
974
+ evidence: verified.evidence,
975
+ failures: [...verified.failures, failure],
976
+ agentResults: verified.agentResults,
977
+ workspace: verified.workspace,
978
+ }),
979
+ task,
980
+ attempt: 1,
981
+ failure,
982
+ evidence,
983
+ })).run;
984
+ }
985
+ if (result.records.some((record) => record.status !== VerificationExecutionStatus.passed)) {
986
+ const failure = createImplementationFailure({
987
+ failureId: input.options.ids.createFailureId(`${run.runId}-integration-failed`),
988
+ runId: run.runId,
989
+ taskId: task.taskId,
990
+ ...(evidence[0] === undefined ? {} : { failingEvidenceId: evidence[0].evidenceId }),
991
+ summary: "Integration verification failed after task execution.",
992
+ detail: "A run-level integration verification command returned a failed result.",
993
+ recoveryPlan: "Analyze the integration failure before authorizing a bounded repair.",
994
+ });
995
+ const analyzed = await analyzeFailure({
996
+ run: withParts(verified, {
997
+ evidence: verified.evidence,
998
+ failures: [...verified.failures, failure],
999
+ agentResults: verified.agentResults,
1000
+ workspace: verified.workspace,
1001
+ }),
1002
+ task,
1003
+ attempt: 1,
1004
+ failure,
1005
+ evidence,
1006
+ });
1007
+ if (analyzed.repairRequested && analyzed.run.status === "executing" && task.repairEnabled) {
1008
+ return handleRepairRequest({
1009
+ run: analyzed.run,
1010
+ task,
1011
+ attempt: 1,
1012
+ failure: analyzed.failure,
1013
+ evidence,
1014
+ });
1015
+ }
1016
+ return analyzed.run;
1017
+ }
943
1018
  await input.writer.persist(verified, input.writer.createEvent(run.runId, "run.integration.verified", {
944
1019
  evidenceIds: evidence.map((item) => item.evidenceId),
945
1020
  }));
@@ -948,34 +1023,42 @@ export const createTaskExecutor = (input) => {
948
1023
  catch (error) {
949
1024
  if (!(error instanceof Error))
950
1025
  throw error;
951
- return persistIntegrationVerificationFailure(run, task, error.message);
1026
+ const failure = createVerificationEnvironmentFailure({
1027
+ failureId: input.options.ids.createFailureId(`${run.runId}-integration-environment`),
1028
+ runId: run.runId,
1029
+ taskId: task.taskId,
1030
+ commandName: commands[0]?.name ?? "integration verification",
1031
+ detail: error.message,
1032
+ });
1033
+ const blocked = withPlannedStatus(withParts(run, {
1034
+ evidence: run.evidence,
1035
+ failures: [...run.failures, failure],
1036
+ agentResults: run.agentResults,
1037
+ workspace: run.workspace,
1038
+ }), "blocked");
1039
+ const withDetails = parsePlannedRun({
1040
+ ...blocked,
1041
+ blockingDetails: {
1042
+ kind: "blocked",
1043
+ missingContext: ["integration-environment"],
1044
+ discoveredEvidence: [],
1045
+ nextActions: ["修复集成验证环境后执行 resume"],
1046
+ failureIds: [failure.failureId],
1047
+ criterionIds: [],
1048
+ allowedOperations: ["resume", "report", "cancel"],
1049
+ },
1050
+ });
1051
+ await input.writer.persist(withDetails, input.writer.createEvent(run.runId, "run.integration.blocked", {
1052
+ failureId: failure.failureId,
1053
+ }));
1054
+ return withDetails;
952
1055
  }
953
1056
  };
954
- const persistIntegrationVerificationFailure = async (run, task, detail) => {
955
- const failure = createVerificationEnvironmentFailure({
956
- failureId: input.options.ids.createFailureId(`${run.runId}-integration`),
957
- runId: run.runId,
958
- taskId: task.taskId,
959
- commandName: "integration verification",
960
- detail,
961
- });
962
- const nextRun = withParts(run, {
963
- evidence: run.evidence,
964
- failures: [...run.failures, failure],
965
- agentResults: run.agentResults,
966
- workspace: run.workspace,
967
- });
968
- await input.writer.persist(nextRun, input.writer.createEvent(run.runId, "run.integration.failed", {
969
- failureId: failure.failureId,
970
- }));
971
- return nextRun;
972
- };
973
1057
  return { runTasks };
974
- async function runParallelReadyBatch(run, orderedTasks) {
1058
+ async function runParallelReadyBatch(run, orderedTasks, completedTaskIds) {
975
1059
  if (run.workspace.git.mode !== "git") {
976
1060
  return { kind: "skipped" };
977
1061
  }
978
- const completedTaskIds = new Set(orderedTasks.filter((task) => task.status === "completed").map((task) => task.taskId));
979
1062
  const readyTasks = orderedTasks.filter((task) => task.status !== "completed" &&
980
1063
  task.dependencies.every((dependency) => completedTaskIds.has(dependency)));
981
1064
  if (readyTasks.length < 2) {
@@ -1046,7 +1129,10 @@ export const createTaskExecutor = (input) => {
1046
1129
  return { kind: "completed", run: serialRun };
1047
1130
  }
1048
1131
  case "parallel-completed":
1049
- return { kind: "completed", run: mergeChildRuns(run, childRuns) };
1132
+ return {
1133
+ kind: "completed",
1134
+ run: mergeChildRuns(run, childRuns, new Map(result.results.map((item) => [item.task.taskId, item.lease]))),
1135
+ };
1050
1136
  case "merge-conflict":
1051
1137
  return { kind: "paused", run: await persistMergeConflict(run, result.conflict.detail) };
1052
1138
  case "execution-failed":
@@ -1061,24 +1147,68 @@ export const createTaskExecutor = (input) => {
1061
1147
  const result = run.agentResults.findLast((candidate) => candidate.role === "worker" && candidate.taskId === task.taskId);
1062
1148
  return result?.status === "completed" ? result.changedPaths : [];
1063
1149
  }
1064
- function mergeChildRuns(parentRun, childRuns) {
1150
+ function mergeChildRuns(parentRun, childRuns, leasesByTaskId) {
1065
1151
  let merged = parentRun;
1066
- for (const childRun of childRuns) {
1067
- for (const task of childRun.taskGraph.tasks) {
1068
- const parentTask = merged.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
1069
- if (parentTask !== undefined && task.status === "completed") {
1070
- merged = withTask(merged, task);
1071
- }
1152
+ const appliedDeltaIds = new Set();
1153
+ const deltas = childRuns
1154
+ .map((childRun) => {
1155
+ const task = childRun.taskGraph.tasks.find((candidate) => candidate.status === "completed" && candidate.taskId);
1156
+ if (task === undefined)
1157
+ return undefined;
1158
+ const lease = leasesByTaskId.get(task.taskId);
1159
+ if (lease === undefined)
1160
+ return undefined;
1161
+ return createParallelTaskDelta({
1162
+ parentRun,
1163
+ childRun,
1164
+ task,
1165
+ lease,
1166
+ });
1167
+ })
1168
+ .filter((delta) => delta !== undefined)
1169
+ .sort((left, right) => left.taskId.localeCompare(right.taskId));
1170
+ for (const delta of deltas) {
1171
+ if (delta.runId !== merged.runId)
1172
+ continue;
1173
+ if (appliedDeltaIds.has(delta.deltaId))
1174
+ continue;
1175
+ const parentTask = merged.taskGraph.tasks.find((candidate) => candidate.taskId === delta.taskId);
1176
+ if (parentTask === undefined)
1177
+ continue;
1178
+ if (delta.baselineDigest !== parentRun.workspace.git.baselineDiffDigest) {
1179
+ throw new Error(`stale parallel task delta baseline for ${delta.taskId}`);
1072
1180
  }
1181
+ appliedDeltaIds.add(delta.deltaId);
1182
+ merged = withTask(merged, delta.task);
1073
1183
  merged = withParts(merged, {
1074
- evidence: uniqueById([...merged.evidence, ...childRun.evidence], (item) => item.evidenceId),
1075
- failures: uniqueById([...merged.failures, ...childRun.failures], (item) => item.failureId),
1076
- agentResults: uniqueById([...merged.agentResults, ...childRun.agentResults], (item) => item.resultId),
1077
- workspace: childRun.workspace,
1184
+ evidence: uniqueById([...merged.evidence, ...delta.evidence], (item) => item.evidenceId),
1185
+ failures: uniqueById([...merged.failures, ...delta.failures], (item) => item.failureId),
1186
+ agentResults: uniqueById([...merged.agentResults, ...delta.agentResults], (item) => item.resultId),
1187
+ workspace: merged.workspace,
1078
1188
  });
1189
+ merged = applyBudgetDelta(merged, delta.budgetDelta);
1079
1190
  }
1080
1191
  return merged;
1081
1192
  }
1193
+ function applyBudgetDelta(run, delta) {
1194
+ return parsePlannedRun({
1195
+ ...run,
1196
+ budgets: Object.fromEntries(Object.entries(run.budgets).map(([key, counter]) => [
1197
+ key,
1198
+ {
1199
+ ...counter,
1200
+ used: checkedBudgetUsage(counter.used, delta[key] ?? 0, counter.limit, key, run.runId),
1201
+ },
1202
+ ])),
1203
+ });
1204
+ }
1205
+ function checkedBudgetUsage(used, delta, limit, budget, runId) {
1206
+ const next = used + delta;
1207
+ if (next > limit) {
1208
+ throw new RunBudgetExceededError(runId, budget, limit);
1209
+ }
1210
+ return next;
1211
+ }
1082
1212
  async function persistMergeConflict(run, detail) {
1083
1213
  const task = firstIncompleteTask(run);
1084
1214
  const failure = createMergeConflictFailure({
@@ -1149,6 +1279,54 @@ export const createTaskExecutor = (input) => {
1149
1279
  }));
1150
1280
  return paused;
1151
1281
  }
1282
+ async function reassessAfterFailure(run) {
1283
+ const failure = run.failures.at(-1);
1284
+ if (failure === undefined ||
1285
+ !["implementation-defect", "scope-drift", "merge-conflict"].includes(failure.category)) {
1286
+ return run;
1287
+ }
1288
+ const affectedRemainingTasks = run.taskGraph.tasks
1289
+ .filter((task) => task.status !== "completed")
1290
+ .map((task) => task.taskId);
1291
+ if (affectedRemainingTasks.length === 0 || run.resolvedTier === "complex") {
1292
+ return run;
1293
+ }
1294
+ const event = {
1295
+ triggerTaskId: failure.taskId,
1296
+ previousTier: run.resolvedTier,
1297
+ newTier: "complex",
1298
+ reason: `Failure ${failure.failureId} requires conservative complexity escalation`,
1299
+ affectedRemainingTasks,
1300
+ };
1301
+ const updated = parsePlannedRun({
1302
+ ...run,
1303
+ resolvedTier: "complex",
1304
+ resolvedRoute: "full",
1305
+ routingDecision: {
1306
+ ...run.routingDecision,
1307
+ tier: "complex",
1308
+ governance: "full",
1309
+ scheduling: run.routingDecision?.scheduling ?? "serial",
1310
+ source: "reassessment",
1311
+ reasons: [event.reason],
1312
+ },
1313
+ reassessmentHistory: [...run.reassessmentHistory, event],
1314
+ taskGraph: {
1315
+ ...run.taskGraph,
1316
+ tasks: run.taskGraph.tasks.map((task) => affectedRemainingTasks.includes(task.taskId)
1317
+ ? { ...task, complexityTier: "complex" }
1318
+ : task),
1319
+ },
1320
+ });
1321
+ await input.writer.persist(updated, input.writer.createEvent(run.runId, "task.reassessment.after_failure", {
1322
+ triggerTaskId: event.triggerTaskId,
1323
+ failureId: failure.failureId,
1324
+ previousTier: event.previousTier,
1325
+ newTier: event.newTier,
1326
+ affectedRemainingTasks,
1327
+ }));
1328
+ return updated;
1329
+ }
1152
1330
  };
1153
1331
  const findWorkerScopeViolations = (task, output) => {
1154
1332
  const violations = [];