@gajae-code/coding-agent 0.12.6 → 0.12.7

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 (70) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +8 -10
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  6. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  7. package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
  9. package/dist/types/sdk/host/host.d.ts +2 -23
  10. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  11. package/dist/types/session/artifacts.d.ts +5 -8
  12. package/dist/types/session/client-bridge.d.ts +0 -14
  13. package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
  14. package/dist/types/session/messages.d.ts +1 -4
  15. package/dist/types/session/session-manager.d.ts +4 -5
  16. package/dist/types/session/streaming-output.d.ts +0 -32
  17. package/dist/types/task/receipt.d.ts +1 -2
  18. package/dist/types/task/types.d.ts +43 -178
  19. package/dist/types/tools/bash.d.ts +1 -1
  20. package/dist/types/tools/fetch.d.ts +0 -3
  21. package/dist/types/tools/output-meta.d.ts +5 -37
  22. package/dist/types/tools/subagent.d.ts +15 -0
  23. package/dist/types/tools/todo-write.d.ts +3 -0
  24. package/dist/types/tools/tool-result.d.ts +0 -1
  25. package/package.json +8 -8
  26. package/src/async/job-manager.ts +430 -46
  27. package/src/cursor.ts +13 -20
  28. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  29. package/src/eval/backend.ts +2 -2
  30. package/src/exec/bash-executor.ts +151 -425
  31. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  32. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  33. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  34. package/src/internal-urls/agent-protocol.ts +38 -98
  35. package/src/internal-urls/artifact-protocol.ts +17 -28
  36. package/src/internal-urls/docs-index.generated.ts +1 -1
  37. package/src/internal-urls/registry-helpers.ts +0 -62
  38. package/src/modes/acp/acp-agent.ts +0 -24
  39. package/src/modes/acp/acp-client-bridge.ts +11 -154
  40. package/src/prompts/tools/bash.md +2 -2
  41. package/src/sdk/acp/adapter.ts +9 -200
  42. package/src/sdk/bus/chat-daemon-control.ts +3 -5
  43. package/src/sdk/bus/index.ts +8 -324
  44. package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
  45. package/src/sdk/host/host.ts +9 -187
  46. package/src/sdk/host/reverse-leases.ts +42 -249
  47. package/src/session/agent-session.ts +15 -178
  48. package/src/session/artifacts.ts +74 -211
  49. package/src/session/client-bridge.ts +0 -15
  50. package/src/session/internal/managed-session-storage.ts +5 -104
  51. package/src/session/messages.ts +7 -32
  52. package/src/session/session-manager.ts +21 -12
  53. package/src/session/streaming-output.ts +40 -198
  54. package/src/task/index.ts +230 -64
  55. package/src/task/receipt.ts +2 -4
  56. package/src/task/render.ts +0 -30
  57. package/src/task/types.ts +10 -34
  58. package/src/tools/bash.ts +239 -818
  59. package/src/tools/eval.ts +4 -99
  60. package/src/tools/fetch.ts +15 -77
  61. package/src/tools/find.ts +3 -7
  62. package/src/tools/output-meta.ts +114 -696
  63. package/src/tools/search.ts +4 -9
  64. package/src/tools/subagent.ts +97 -75
  65. package/src/tools/todo-write.ts +47 -0
  66. package/src/tools/tool-result.ts +0 -1
  67. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  68. package/dist/types/utils/artifact-id.d.ts +0 -2
  69. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  70. package/src/utils/artifact-id.ts +0 -6
package/src/task/index.ts CHANGED
@@ -53,9 +53,6 @@ import {
53
53
  RepositoryBindingError,
54
54
  resolveTaskRepositoryBinding,
55
55
  } from "../gjc-runtime/repository-binding";
56
- import { reconcileReviewSourceDelivery } from "../gjc-runtime/ultragoal-review-source";
57
- import { classifyUltragoalReviewDelivery, validateUltragoalReviewDispatch } from "../gjc-runtime/ultragoal-runtime";
58
-
59
56
  import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
60
57
  import { ArtifactManager } from "../session/artifacts";
61
58
  import { generateCommitMessage } from "../utils/commit-message-generator";
@@ -91,6 +88,18 @@ import {
91
88
  type WorktreeBaseline,
92
89
  } from "./worktree";
93
90
 
91
+ interface DuplicateIdentity {
92
+ role: string;
93
+ ownerId?: string;
94
+ parentSession: string | null;
95
+ repository: { root: string; relativeSubdir: string | null };
96
+ }
97
+
98
+ interface DuplicateDisposition {
99
+ action: "warned" | "superseded";
100
+ predecessorIds: string[];
101
+ }
102
+
94
103
  interface TaskResumeDescriptor {
95
104
  toolCallId: string;
96
105
  params: TaskParams;
@@ -99,6 +108,11 @@ interface TaskResumeDescriptor {
99
108
  durableOutputAllowed?: boolean;
100
109
  forkContextSeed?: ForkContextSeed;
101
110
  agentSource: AgentDefinition["source"];
111
+ repositoryBinding: RepositoryBinding;
112
+ duplicateIdentity: DuplicateIdentity;
113
+ duplicatePolicy: "warn" | "supersede";
114
+ initialDisposition?: DuplicateDisposition;
115
+ lastAdmissionFailure?: string;
102
116
  }
103
117
 
104
118
  function isTaskResumeDescriptor(value: unknown): value is TaskResumeDescriptor {
@@ -188,18 +202,12 @@ async function resolveTaskItemsWithRepositoryBindings(
188
202
  for (const task of tasks) {
189
203
  try {
190
204
  const binding = await resolveTaskRepositoryBinding(cwd, task.repositoryBinding);
191
- if (binding.relativeSubdir) {
192
- assertPathUnderRepositoryBinding(binding, ".");
193
- }
194
- resolved.push({
195
- ...task,
196
- repositoryBinding: binding,
197
- });
205
+ if (binding.relativeSubdir) assertPathUnderRepositoryBinding(binding, ".");
206
+ resolved.push({ ...task, repositoryBinding: binding });
198
207
  } catch (error) {
199
208
  const id = task.id?.trim() ? task.id : "(missing-id)";
200
- if (error instanceof RepositoryBindingError) {
209
+ if (error instanceof RepositoryBindingError)
201
210
  return { tasks: [], error: `Task "${id}" repository binding rejected: ${error.message}` };
202
- }
203
211
  return {
204
212
  tasks: [],
205
213
  error: `Task "${id}" repository binding rejected: ${error instanceof Error ? error.message : String(error)}`,
@@ -209,6 +217,31 @@ async function resolveTaskItemsWithRepositoryBindings(
209
217
  return { tasks: resolved };
210
218
  }
211
219
 
220
+ function duplicateIdentityForTask(
221
+ task: TaskItem,
222
+ role: string,
223
+ ownerId: string | undefined,
224
+ parentSession: string | null,
225
+ ): DuplicateIdentity {
226
+ const binding = task.repositoryBinding as RepositoryBinding;
227
+ return {
228
+ role: role.trim(),
229
+ ownerId,
230
+ parentSession,
231
+ repository: { root: binding.commonDir ?? binding.worktreeRoot, relativeSubdir: binding.relativeSubdir ?? null },
232
+ };
233
+ }
234
+
235
+ function duplicateIdentityKey(identity: DuplicateIdentity): string {
236
+ return JSON.stringify([
237
+ identity.role,
238
+ identity.ownerId ?? null,
239
+ identity.parentSession,
240
+ identity.repository.root,
241
+ identity.repository.relativeSubdir,
242
+ ]);
243
+ }
244
+
212
245
  function repositoryBindingFromTask(task: TaskItem): RepositoryBinding | undefined {
213
246
  return task.repositoryBinding ? publicRepositoryBinding(task.repositoryBinding as RepositoryBinding) : undefined;
214
247
  }
@@ -859,7 +892,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
859
892
  }
860
893
 
861
894
  const startedJobs: Array<{ jobId: string; taskId: string }> = [];
862
- const failedSchedules: string[] = [];
895
+ const failedSchedules: Array<{
896
+ task: TaskItem & { id: string };
897
+ taskIndex: number;
898
+ message: string;
899
+ signalSkip: boolean;
900
+ duplicateDisposition?: DuplicateDisposition;
901
+ }> = [];
863
902
  let completedJobs = 0;
864
903
  let failedJobs = 0;
865
904
 
@@ -886,21 +925,106 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
886
925
  };
887
926
 
888
927
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
928
+ const ownerId = this.session.getAgentId?.() ?? undefined;
929
+ const parentSession = this.session.getSessionFile();
930
+ const admitDuplicateLaunch = (
931
+ subagentId: string,
932
+ task: TaskItem,
933
+ policy: "warn" | "supersede",
934
+ selfSubagentId?: string,
935
+ ): { ok: boolean; disposition?: DuplicateDisposition; error?: string; identity: DuplicateIdentity } => {
936
+ const identity = duplicateIdentityForTask(task, params.agent, ownerId, parentSession);
937
+ const key = duplicateIdentityKey(identity);
938
+ const predecessors = manager
939
+ .getSubagentRecords({ ownerId })
940
+ .filter(record => {
941
+ if (record.subagentId === selfSubagentId || record.subagentId === subagentId) return false;
942
+ if (record.status !== "running" && record.status !== "paused" && record.status !== "queued")
943
+ return false;
944
+ return record.duplicateIdentity === key;
945
+ })
946
+ .sort(
947
+ (a, b) =>
948
+ (b.currentJobId ? (manager.getJob(b.currentJobId)?.startTime ?? 0) : 0) -
949
+ (a.currentJobId ? (manager.getJob(a.currentJobId)?.startTime ?? 0) : 0) ||
950
+ a.subagentId.localeCompare(b.subagentId),
951
+ );
952
+ if (predecessors.length === 0) return { ok: true, identity };
953
+ if (policy === "warn")
954
+ return {
955
+ ok: true,
956
+ identity,
957
+ disposition: { action: "warned", predecessorIds: predecessors.map(record => record.subagentId) },
958
+ };
959
+ for (const predecessor of predecessors)
960
+ if (!manager.cancelSubagent(predecessor.subagentId, { ownerId }))
961
+ return { ok: false, identity, error: "duplicate_supersede_failed" };
962
+ return {
963
+ ok: true,
964
+ identity,
965
+ disposition: { action: "superseded", predecessorIds: predecessors.map(record => record.subagentId) },
966
+ };
967
+ };
889
968
  let resumeRunner: ResumeRunner | undefined;
890
969
  if (typeof manager.setResumeRunner === "function") {
891
970
  resumeRunner = (_subagentId, message, resumeDescriptor) => {
892
971
  const descriptor = isTaskResumeDescriptor(resumeDescriptor?.data) ? resumeDescriptor.data : undefined;
893
972
  if (!descriptor) return undefined;
973
+ const admission = (() => {
974
+ const identity = descriptor.duplicateIdentity;
975
+ const key = duplicateIdentityKey(identity);
976
+ const predecessors = manager
977
+ .getSubagentRecords({ ownerId })
978
+ .filter(record => {
979
+ if (
980
+ record.subagentId === descriptor.task.id ||
981
+ !["running", "paused", "queued"].includes(record.status)
982
+ )
983
+ return false;
984
+ return record.duplicateIdentity === key;
985
+ })
986
+ .sort(
987
+ (a, b) =>
988
+ (b.currentJobId ? (manager.getJob(b.currentJobId)?.startTime ?? 0) : 0) -
989
+ (a.currentJobId ? (manager.getJob(a.currentJobId)?.startTime ?? 0) : 0) ||
990
+ a.subagentId.localeCompare(b.subagentId),
991
+ );
992
+ if (predecessors.length === 0) return { ok: true, identity };
993
+ if (descriptor.duplicatePolicy === "warn")
994
+ return {
995
+ ok: true,
996
+ identity,
997
+ disposition: { action: "warned", predecessorIds: predecessors.map(record => record.subagentId) },
998
+ };
999
+ for (const predecessor of predecessors)
1000
+ if (!manager.cancelSubagent(predecessor.subagentId, { ownerId }))
1001
+ return { ok: false, identity, error: "duplicate_supersede_failed" };
1002
+ return {
1003
+ ok: true,
1004
+ identity,
1005
+ disposition: { action: "superseded", predecessorIds: predecessors.map(record => record.subagentId) },
1006
+ };
1007
+ })();
1008
+ if (!admission.ok) {
1009
+ descriptor.lastAdmissionFailure = admission.error;
1010
+ return undefined;
1011
+ }
1012
+ if (!descriptor) return undefined;
894
1013
  const forkSeeds = descriptor.forkContextSeed
895
1014
  ? new Map([[descriptor.task.id, descriptor.forkContextSeed]])
896
1015
  : undefined;
1016
+ const resumedTask = {
1017
+ ...descriptor.task,
1018
+ repositoryBinding: descriptor.repositoryBinding,
1019
+ duplicate_policy: descriptor.duplicatePolicy,
1020
+ };
897
1021
  return manager.register(
898
1022
  "task",
899
1023
  descriptor.task.id,
900
1024
  async ({ signal: runSignal }) => {
901
1025
  const result = await this.#executeSync(
902
1026
  descriptor.toolCallId,
903
- { ...descriptor.params, tasks: [descriptor.task] },
1027
+ { ...descriptor.params, tasks: [resumedTask] },
904
1028
  runSignal,
905
1029
  undefined,
906
1030
  [descriptor.task.id],
@@ -921,7 +1045,22 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
921
1045
  },
922
1046
  );
923
1047
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
924
- const singleResult = result.details?.results[0];
1048
+ const rawSingleResult = result.details?.results[0];
1049
+ const singleResult = rawSingleResult
1050
+ ? {
1051
+ ...rawSingleResult,
1052
+ duplicateDisposition: (() => {
1053
+ const initial = descriptor.initialDisposition;
1054
+ const current = admission.disposition;
1055
+ if (!initial) return current;
1056
+ if (!current) return initial;
1057
+ return {
1058
+ action: current.action,
1059
+ predecessorIds: [...new Set([...initial.predecessorIds, ...current.predecessorIds])],
1060
+ };
1061
+ })(),
1062
+ }
1063
+ : rawSingleResult;
925
1064
  return subagentRunOutcomeFromSingleResult(finalText, singleResult);
926
1065
  },
927
1066
  {
@@ -932,6 +1071,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
932
1071
  id: descriptor.task.id,
933
1072
  agent: descriptor.params.agent,
934
1073
  agentSource: descriptor.agentSource,
1074
+ duplicateIdentity: duplicateIdentityKey(descriptor.duplicateIdentity),
1075
+ duplicateDisposition: (admission.disposition?.action ??
1076
+ descriptor.initialDisposition?.action) as DuplicateDisposition["action"] | undefined,
935
1077
  description: descriptor.task.description,
936
1078
  assignment: descriptor.task.assignment.trim(),
937
1079
  },
@@ -968,7 +1110,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
968
1110
  for (let i = 0; i < taskItems.length; i++) {
969
1111
  const taskItem = taskItems[i];
970
1112
  if (signal?.aborted) {
971
- failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
1113
+ failedSchedules.push({
1114
+ task: taskItem as TaskItem & { id: string },
1115
+ taskIndex: i,
1116
+ message: "cancelled before scheduling",
1117
+ signalSkip: true,
1118
+ });
972
1119
  const progress = progressByTaskId.get(taskItem.id);
973
1120
  if (progress) {
974
1121
  progress.status = "aborted";
@@ -981,7 +1128,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
981
1128
  if (signal?.aborted) {
982
1129
  for (let skippedIndex = i; skippedIndex < taskItems.length; skippedIndex++) {
983
1130
  const skippedTask = taskItems[skippedIndex]!;
984
- failedSchedules.push(`${skippedTask.id}: cancelled before scheduling`);
1131
+ failedSchedules.push({
1132
+ task: skippedTask as TaskItem & { id: string },
1133
+ taskIndex: skippedIndex,
1134
+ message: "cancelled before scheduling",
1135
+ signalSkip: true,
1136
+ });
985
1137
  const skippedProgress = progressByTaskId.get(skippedTask.id);
986
1138
  if (skippedProgress) skippedProgress.status = "aborted";
987
1139
  }
@@ -997,6 +1149,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
997
1149
  const subtaskSessionFile = managedPersistence
998
1150
  ? null
999
1151
  : path.join(batchArtifactsDir ?? externalTaskSessionsDir!, `${uniqueId}.jsonl`);
1152
+ const admission = admitDuplicateLaunch(uniqueId, taskItem, taskItem.duplicate_policy ?? "warn");
1153
+ if (!admission.ok) {
1154
+ failedSchedules.push({
1155
+ task: taskItem as TaskItem & { id: string },
1156
+ taskIndex: i,
1157
+ message: admission.error ?? "duplicate_supersede_failed",
1158
+ signalSkip: false,
1159
+ duplicateDisposition: admission.disposition,
1160
+ });
1161
+ continue;
1162
+ }
1000
1163
  const jobId = manager.register(
1001
1164
  "task",
1002
1165
  label,
@@ -1036,7 +1199,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1036
1199
  },
1037
1200
  );
1038
1201
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
1039
- const singleResult = result.details?.results[0];
1202
+ const rawSingleResult = result.details?.results[0];
1203
+ const singleResult = rawSingleResult
1204
+ ? {
1205
+ ...rawSingleResult,
1206
+ duplicateDisposition: rawSingleResult.duplicateDisposition ?? admission.disposition,
1207
+ }
1208
+ : rawSingleResult;
1040
1209
  if (progress) {
1041
1210
  progress.status = singleResult?.paused
1042
1211
  ? "paused"
@@ -1119,6 +1288,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1119
1288
  id: uniqueId,
1120
1289
  agent: params.agent,
1121
1290
  agentSource: fallbackAgentSource,
1291
+ duplicateIdentity: duplicateIdentityKey(admission.identity),
1292
+ duplicateDisposition: admission.disposition?.action,
1122
1293
  description: taskItem.description,
1123
1294
  assignment: taskItem.assignment.trim(),
1124
1295
  },
@@ -1145,6 +1316,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1145
1316
  durableOutputAllowed: Boolean(batchArtifactsDir),
1146
1317
  forkContextSeed: frozenForkSeed,
1147
1318
  agentSource: fallbackAgentSource,
1319
+ repositoryBinding: taskItem.repositoryBinding as RepositoryBinding,
1320
+ duplicateIdentity: admission.identity,
1321
+ duplicatePolicy: taskItem.duplicate_policy ?? "warn",
1322
+ initialDisposition: admission.disposition,
1148
1323
  } satisfies TaskResumeDescriptor,
1149
1324
  },
1150
1325
  resumeRunner,
@@ -1155,10 +1330,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1155
1330
  subagentId: uniqueId,
1156
1331
  ownerId: this.session.getAgentId?.() ?? undefined,
1157
1332
  currentJobId: jobId,
1333
+ currentJobGeneration: manager.getJob(jobId)?.generation,
1158
1334
  historicalJobIds: [],
1159
1335
  status: manager.getJob(jobId)?.status ?? "running",
1160
1336
  sessionFile: null,
1161
1337
  resumable: true,
1338
+ duplicateIdentity: duplicateIdentityKey(admission.identity),
1339
+ duplicateDisposition: admission.disposition?.action,
1162
1340
  });
1163
1341
  }
1164
1342
  } catch (error) {
@@ -1168,7 +1346,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1168
1346
  : error instanceof Error
1169
1347
  ? error.message
1170
1348
  : String(error);
1171
- failedSchedules.push(`${taskItem.id}: ${message}`);
1349
+ failedSchedules.push({
1350
+ task: taskItem as TaskItem & { id: string },
1351
+ taskIndex: i,
1352
+ message,
1353
+ signalSkip: false,
1354
+ });
1172
1355
  const progress = progressByTaskId.get(taskItem.id);
1173
1356
  if (progress) {
1174
1357
  progress.status = "failed";
@@ -1181,11 +1364,36 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1181
1364
  failedJobs += failedSchedules.length;
1182
1365
  }
1183
1366
 
1367
+ const scheduleFailureReceipts = failedSchedules
1368
+ .slice()
1369
+ .sort((a, b) => a.taskIndex - b.taskIndex)
1370
+ .map((entry, index) =>
1371
+ buildTaskReceipt({
1372
+ index,
1373
+ id: entry.task.id,
1374
+ agent: params.agent,
1375
+ agentSource: fallbackAgentSource,
1376
+ task: renderTaskAssignment(entry.task.assignment, simpleMode),
1377
+ assignment: entry.task.assignment,
1378
+ description: entry.task.description,
1379
+ status: "failed",
1380
+ exitCode: 1,
1381
+ aborted: entry.signalSkip ? true : undefined,
1382
+ abortReason: entry.signalSkip ? "Cancelled before start" : undefined,
1383
+ truncated: false,
1384
+ durationMs: 0,
1385
+ tokens: 0,
1386
+ output: "",
1387
+ stderr: entry.message,
1388
+ error: entry.message,
1389
+ duplicateDisposition: entry.duplicateDisposition,
1390
+ } as SingleResult),
1391
+ );
1184
1392
  if (startedJobs.length === 0) {
1185
- const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
1393
+ const failureText = `Failed to start background task jobs: ${failedSchedules.map(entry => `${entry.task.id}: ${entry.message}`).join("; ")}`;
1186
1394
  return {
1187
1395
  content: [{ type: "text", text: failureText }],
1188
- details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
1396
+ details: { projectAgentsDir: null, results: scheduleFailureReceipts, totalDurationMs: 0 },
1189
1397
  };
1190
1398
  }
1191
1399
 
@@ -1233,7 +1441,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1233
1441
  ],
1234
1442
  details: {
1235
1443
  projectAgentsDir: null,
1236
- results: [],
1444
+ results: scheduleFailureReceipts,
1237
1445
  totalDurationMs: 0,
1238
1446
  progress: getProgressSnapshot(),
1239
1447
  async: { state: asyncState, jobId: startedJobs[0].jobId, type: "task" },
@@ -1705,27 +1913,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1705
1913
  if (taskRepositoryBinding.relativeSubdir) {
1706
1914
  assertPathUnderRepositoryBinding(taskRepositoryBinding, ".");
1707
1915
  }
1708
- if (task.reviewSource) {
1709
- await validateUltragoalReviewDispatch({
1710
- cwd: this.session.cwd,
1711
- dispatchId: task.reviewSource.dispatchId,
1712
- cohortId: task.reviewSource.cohortId,
1713
- taskId: task.id,
1714
- lane: task.reviewSource.lane,
1715
- snapshotId: task.reviewSource.snapshotId,
1716
- generation: task.reviewSource.generation,
1717
- repositoryBindingDigest: task.reviewSource.repositoryBindingDigest,
1718
- stateRevision: task.reviewSource.stateRevision,
1719
- rerunCommand: task.reviewSource.rerunCommand,
1720
- taskSourceTaskId: task.reviewSource.taskId,
1721
- createdAt: task.reviewSource.createdAt,
1722
- });
1723
- }
1724
- if (isIsolated && task.reviewSource) {
1725
- throw new Error(
1726
- "source-aware review tasks cannot use generic task isolation; dispatch them against the coordinator-owned source snapshot",
1727
- );
1728
- }
1729
1916
  if (!isIsolated) {
1730
1917
  await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, taskRepositoryBinding);
1731
1918
  const result = await runSubprocess({
@@ -1780,32 +1967,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1780
1967
  parentTelemetry: this.session.getTelemetry?.(),
1781
1968
  forkContextSeed,
1782
1969
  });
1783
- const reviewSourceDisposition = task.reviewSource
1784
- ? await reconcileReviewSourceDelivery({
1785
- cwd: this.session.cwd,
1786
- repositoryBinding: taskRepositoryBinding,
1787
- reviewSource: task.reviewSource,
1788
- })
1789
- : undefined;
1790
- const reviewSource =
1791
- task.reviewSource && reviewSourceDisposition
1792
- ? {
1793
- ...task.reviewSource,
1794
- ...reviewSourceDisposition,
1795
- ...(await classifyUltragoalReviewDelivery({
1796
- cwd: this.session.cwd,
1797
- cohortId: task.reviewSource.cohortId,
1798
- dispatchId: task.reviewSource.dispatchId,
1799
- observedDisposition: reviewSourceDisposition.disposition,
1800
- })),
1801
- }
1802
- : undefined;
1803
1970
  return {
1804
1971
  ...result,
1805
1972
  ...(forkContext ? { forkContext } : {}),
1806
1973
  forkContextAdvisory,
1807
1974
  repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1808
- ...(reviewSource ? { reviewSource } : {}),
1809
1975
  };
1810
1976
  }
1811
1977
 
@@ -43,6 +43,7 @@ export interface TaskResultReceipt {
43
43
  retryFailure?: { attempt: number; errorSummary: string };
44
44
  setupFailure?: { summary: string };
45
45
  errorSummary?: string;
46
+ duplicateDisposition?: SingleResult["duplicateDisposition"];
46
47
  abortSummary?: string;
47
48
  preview: string;
48
49
  previewTruncated: boolean;
@@ -67,9 +68,6 @@ export interface TaskResultReceipt {
67
68
  forkContextAdvisory?: SingleResult["forkContextAdvisory"];
68
69
  /** Resolved repository identity for this delegated lane (#2901). */
69
70
  repositoryBinding?: SingleResult["repositoryBinding"];
70
- /** Runtime-derived review source disposition and rerun guidance (#3469). */
71
- reviewSource?: SingleResult["reviewSource"];
72
-
73
71
  roi?: TaskRoi;
74
72
  }
75
73
 
@@ -296,6 +294,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
296
294
  retryFailure: raw.retryFailure
297
295
  ? { attempt: raw.retryFailure.attempt, errorSummary: "Retry failure recorded." }
298
296
  : undefined,
297
+ duplicateDisposition: raw.duplicateDisposition,
299
298
  errorSummary:
300
299
  raw.setupFailure?.summary ??
301
300
  (raw.error
@@ -314,7 +313,6 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
314
313
  forkContext: raw.forkContext,
315
314
  forkContextAdvisory: raw.forkContextAdvisory,
316
315
  repositoryBinding: raw.repositoryBinding,
317
- reviewSource: raw.reviewSource,
318
316
  roi: buildTaskRoi(raw),
319
317
  };
320
318
  }
@@ -916,36 +916,6 @@ function renderAgentResult(result: TaskResultReceipt, isLast: boolean, expanded:
916
916
  if (result.roi?.lowRoi) {
917
917
  lines.push(`${continuePrefix}${theme.fg("warning", "low ROI: produced no material contribution")}`);
918
918
  }
919
- if (result.reviewSource?.disposition === "stale_review_delivery") {
920
- lines.push(
921
- `${continuePrefix}${theme.fg(
922
- "warning",
923
- truncateToWidth(
924
- replaceTabs(
925
- sanitizeText(
926
- `STALE REVIEW: source advanced after dispatch; excluded from review gates. Rerun: ${result.reviewSource.rerunCommand}`,
927
- ),
928
- ),
929
-
930
- 100,
931
- ),
932
- )}`,
933
- );
934
- } else if (result.reviewSource?.disposition === "invalid_provenance") {
935
- lines.push(
936
- `${continuePrefix}${theme.fg(
937
- "error",
938
- truncateToWidth(
939
- replaceTabs(
940
- sanitizeText(
941
- `INVALID REVIEW PROVENANCE: receipt cannot satisfy review gates. Rerun: ${result.reviewSource.rerunCommand}`,
942
- ),
943
- ),
944
- 100,
945
- ),
946
- )}`,
947
- );
948
- }
949
919
 
950
920
  if (result.persistence?.recoveryRef) {
951
921
  const label = result.persistence.ownerWorktreeApplied ? "Recovery patch" : "Unapplied recovery patch";
package/src/task/types.ts CHANGED
@@ -2,7 +2,6 @@ import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Usage } from "@gajae-code/ai";
3
3
  import { $env } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
- import type { ReviewDeliveryDisposition, ReviewSourceLane } from "../gjc-runtime/ultragoal-review-source";
6
5
  import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
7
6
  import type { TaskResultReceipt } from "./receipt";
8
7
  import type { SpawnRoiReconciliation } from "./roi-reconciliation";
@@ -115,26 +114,9 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
115
114
  .describe(
116
115
  "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
117
116
  ),
118
- reviewSource: reviewSourceSchema.optional(),
117
+ duplicate_policy: z.enum(["warn", "supersede"]).optional().describe("duplicate launch policy; defaults to warn"),
119
118
  });
120
119
 
121
- const reviewSourceSchema = z
122
- .object({
123
- schema: z.literal("gjc.review_source_dispatch.v1"),
124
- cohortId: z.string().min(1),
125
- generation: z.number().int().min(1),
126
- lane: z.enum(["cleaner", "architect", "qa", "critic"]),
127
- snapshotId: z.string().startsWith("sha256:"),
128
- repositoryBindingDigest: z.string().startsWith("sha256:"),
129
- stateRevision: z.number().int().min(0),
130
- dispatchId: z.string().min(1),
131
- rerunCommand: z.string().min(1),
132
- taskId: z.string().min(1),
133
- createdAt: z.string().min(1),
134
- })
135
- .strict()
136
- .describe("leader-issued immutable review source identity; review lanes only");
137
-
138
120
  /** Single task item for parallel execution (default shape with context enabled). */
139
121
  export const taskItemSchema = createTaskItemSchema(true);
140
122
  export type TaskItem = z.infer<typeof taskItemSchema>;
@@ -218,6 +200,7 @@ export interface TaskParams {
218
200
  spawnPlan?: SpawnPlanReceipt;
219
201
  tasks: TaskItem[];
220
202
  isolated?: boolean;
203
+ duplicate_policy?: "warn" | "supersede";
221
204
  }
222
205
 
223
206
  /** A code review finding reported by the reviewer agent */
@@ -452,6 +435,12 @@ export interface TaskRecoveryArtifactRef {
452
435
  durability: "session";
453
436
  }
454
437
 
438
+ /** Bounded duplicate-launch disposition carried into task receipts. */
439
+ export interface DuplicateDisposition {
440
+ action: "warned" | "superseded";
441
+ predecessorIds: string[];
442
+ }
443
+
455
444
  export interface TaskPersistenceResult {
456
445
  outcome: "applied" | "no_changes" | "recovery_available";
457
446
  ownerWorktreeApplied: boolean;
@@ -488,6 +477,8 @@ export interface SingleResult {
488
477
  setupFailure?: SetupFailureSummary;
489
478
  aborted?: boolean;
490
479
  abortReason?: string;
480
+
481
+ duplicateDisposition?: DuplicateDisposition;
491
482
  paused?: boolean;
492
483
  /** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
493
484
  usage?: Usage;
@@ -543,21 +534,6 @@ export interface SingleResult {
543
534
  head?: string;
544
535
  branch?: string;
545
536
  };
546
- /** Leader-issued immutable source identity for source-aware review lanes (#3469). */
547
- reviewSource?: {
548
- schema: "gjc.review_source_dispatch.v1";
549
- cohortId: string;
550
- generation: number;
551
- lane: ReviewSourceLane;
552
- snapshotId: string;
553
- repositoryBindingDigest: string;
554
- stateRevision: number;
555
- dispatchId: string;
556
- rerunCommand: string;
557
- disposition: ReviewDeliveryDisposition;
558
- currentSnapshotId?: string;
559
- deliveryId?: string;
560
- };
561
537
  }
562
538
 
563
539
  /** True only for complete, factual five-bucket cost accounting. */