@gajae-code/coding-agent 0.12.6 → 0.12.8

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 (150) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/config/file-lock.d.ts +3 -0
  5. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  6. package/dist/types/config/model-registry.d.ts +2 -0
  7. package/dist/types/config/resolve-config-value.d.ts +2 -1
  8. package/dist/types/config/settings-schema.d.ts +17 -1
  9. package/dist/types/eval/backend.d.ts +2 -2
  10. package/dist/types/exec/bash-executor.d.ts +8 -10
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  12. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  15. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  18. package/dist/types/sdk/broker/broker.d.ts +5 -1
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
  21. package/dist/types/sdk/bus/config.d.ts +75 -113
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/host.d.ts +2 -23
  33. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  34. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  35. package/dist/types/sdk/index.d.ts +1 -0
  36. package/dist/types/sdk/providers.d.ts +22 -0
  37. package/dist/types/sdk/session-directory.d.ts +1 -1
  38. package/dist/types/session/artifacts.d.ts +5 -8
  39. package/dist/types/session/client-bridge.d.ts +0 -14
  40. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  41. package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
  42. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  43. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  44. package/dist/types/session/messages.d.ts +1 -4
  45. package/dist/types/session/session-manager.d.ts +7 -6
  46. package/dist/types/session/session-storage.d.ts +2 -0
  47. package/dist/types/session/streaming-output.d.ts +0 -32
  48. package/dist/types/task/receipt.d.ts +1 -2
  49. package/dist/types/task/types.d.ts +43 -178
  50. package/dist/types/tools/bash.d.ts +1 -1
  51. package/dist/types/tools/fetch.d.ts +0 -3
  52. package/dist/types/tools/output-meta.d.ts +5 -37
  53. package/dist/types/tools/subagent.d.ts +15 -0
  54. package/dist/types/tools/todo-write.d.ts +3 -0
  55. package/dist/types/tools/tool-result.d.ts +0 -1
  56. package/package.json +10 -8
  57. package/scripts/build-sdk-package-smoke.ts +51 -0
  58. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  59. package/src/async/job-manager.ts +430 -46
  60. package/src/cli/notify-cli.ts +204 -94
  61. package/src/commands/notify.ts +19 -8
  62. package/src/config/file-lock.ts +6 -0
  63. package/src/config/model-discovery-manager.ts +50 -8
  64. package/src/config/model-registry.ts +862 -75
  65. package/src/config/resolve-config-value.ts +10 -8
  66. package/src/config/settings-schema.ts +25 -15
  67. package/src/config/settings.ts +15 -15
  68. package/src/cursor.ts +13 -20
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  70. package/src/eval/backend.ts +2 -2
  71. package/src/exec/bash-executor.ts +151 -425
  72. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  73. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  74. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  75. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  76. package/src/internal-urls/agent-protocol.ts +38 -98
  77. package/src/internal-urls/artifact-protocol.ts +17 -28
  78. package/src/internal-urls/docs-index.generated.ts +7 -7
  79. package/src/internal-urls/registry-helpers.ts +0 -62
  80. package/src/main.ts +43 -7
  81. package/src/modes/DESIGN.md +11 -11
  82. package/src/modes/acp/acp-agent.ts +0 -24
  83. package/src/modes/acp/acp-client-bridge.ts +11 -154
  84. package/src/modes/components/hook-selector.ts +155 -44
  85. package/src/modes/components/model-selector.ts +242 -49
  86. package/src/modes/components/notifications-settings-editor.ts +631 -85
  87. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  88. package/src/modes/controllers/selector-controller.ts +464 -50
  89. package/src/prompts/tools/bash.md +2 -2
  90. package/src/sdk/acp/adapter.ts +9 -200
  91. package/src/sdk/broker/broker.ts +54 -10
  92. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  93. package/src/sdk/broker/lifecycle.ts +47 -50
  94. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  95. package/src/sdk/bus/chat-daemon-control.ts +35 -8
  96. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  97. package/src/sdk/bus/config.ts +534 -286
  98. package/src/sdk/bus/discord-live-provider.ts +57 -2
  99. package/src/sdk/bus/discord-provider.ts +21 -0
  100. package/src/sdk/bus/index.ts +105 -359
  101. package/src/sdk/bus/notification-orchestration.ts +371 -68
  102. package/src/sdk/bus/notification-service.ts +431 -130
  103. package/src/sdk/bus/session-control.ts +119 -44
  104. package/src/sdk/bus/slack-live-provider.ts +78 -2
  105. package/src/sdk/bus/slack-provider.ts +24 -0
  106. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  107. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  108. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  109. package/src/sdk/bus/telegram-daemon.ts +23 -15
  110. package/src/sdk/client/discovery.ts +10 -4
  111. package/src/sdk/host/host.ts +9 -187
  112. package/src/sdk/host/query/handlers.ts +20 -1
  113. package/src/sdk/host/reverse-leases.ts +42 -249
  114. package/src/sdk/index.ts +1 -0
  115. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  116. package/src/sdk/protocol/operation-registry.ts +5 -2
  117. package/src/sdk/providers.ts +51 -0
  118. package/src/sdk/session-directory.ts +1 -0
  119. package/src/sdk/session.ts +10 -7
  120. package/src/session/agent-session.ts +59 -202
  121. package/src/session/artifacts.ts +74 -211
  122. package/src/session/client-bridge.ts +0 -15
  123. package/src/session/internal/managed-session-scope.ts +981 -450
  124. package/src/session/internal/managed-session-storage.ts +362 -124
  125. package/src/session/internal/native-publish-outcome.ts +3 -1
  126. package/src/session/internal/session-open-errors.ts +11 -0
  127. package/src/session/messages.ts +7 -32
  128. package/src/session/session-manager.ts +48 -15
  129. package/src/session/session-storage.ts +72 -5
  130. package/src/session/streaming-output.ts +40 -198
  131. package/src/slash-commands/builtin-registry.ts +82 -4
  132. package/src/task/index.ts +237 -65
  133. package/src/task/receipt.ts +2 -4
  134. package/src/task/render.ts +0 -30
  135. package/src/task/types.ts +10 -34
  136. package/src/tools/ask.ts +1 -1
  137. package/src/tools/bash.ts +239 -818
  138. package/src/tools/eval.ts +4 -99
  139. package/src/tools/fetch.ts +15 -77
  140. package/src/tools/find.ts +3 -7
  141. package/src/tools/output-meta.ts +114 -696
  142. package/src/tools/search.ts +4 -9
  143. package/src/tools/subagent.ts +97 -75
  144. package/src/tools/telegram-send.ts +4 -2
  145. package/src/tools/todo-write.ts +47 -0
  146. package/src/tools/tool-result.ts +0 -1
  147. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  148. package/dist/types/utils/artifact-id.d.ts +0 -2
  149. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  150. 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,105 @@ 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.getSubagentRecords?.({ ownerId }) ?? [])
939
+ .filter(record => {
940
+ if (record.subagentId === selfSubagentId || record.subagentId === subagentId) return false;
941
+ if (record.status !== "running" && record.status !== "paused" && record.status !== "queued")
942
+ return false;
943
+ return record.duplicateIdentity === key;
944
+ })
945
+ .sort(
946
+ (a, b) =>
947
+ (b.currentJobId ? (manager.getJob(b.currentJobId)?.startTime ?? 0) : 0) -
948
+ (a.currentJobId ? (manager.getJob(a.currentJobId)?.startTime ?? 0) : 0) ||
949
+ a.subagentId.localeCompare(b.subagentId),
950
+ );
951
+ if (predecessors.length === 0) return { ok: true, identity };
952
+ if (policy === "warn")
953
+ return {
954
+ ok: true,
955
+ identity,
956
+ disposition: { action: "warned", predecessorIds: predecessors.map(record => record.subagentId) },
957
+ };
958
+ for (const predecessor of predecessors)
959
+ if (!manager.cancelSubagent(predecessor.subagentId, { ownerId }))
960
+ return { ok: false, identity, error: "duplicate_supersede_failed" };
961
+ return {
962
+ ok: true,
963
+ identity,
964
+ disposition: { action: "superseded", predecessorIds: predecessors.map(record => record.subagentId) },
965
+ };
966
+ };
889
967
  let resumeRunner: ResumeRunner | undefined;
890
968
  if (typeof manager.setResumeRunner === "function") {
891
969
  resumeRunner = (_subagentId, message, resumeDescriptor) => {
892
970
  const descriptor = isTaskResumeDescriptor(resumeDescriptor?.data) ? resumeDescriptor.data : undefined;
893
971
  if (!descriptor) return undefined;
972
+ const admission = (() => {
973
+ const identity = descriptor.duplicateIdentity;
974
+ const key = duplicateIdentityKey(identity);
975
+ const predecessors = manager
976
+ .getSubagentRecords({ ownerId })
977
+ .filter(record => {
978
+ if (
979
+ record.subagentId === descriptor.task.id ||
980
+ !["running", "paused", "queued"].includes(record.status)
981
+ )
982
+ return false;
983
+ return record.duplicateIdentity === key;
984
+ })
985
+ .sort(
986
+ (a, b) =>
987
+ (b.currentJobId ? (manager.getJob(b.currentJobId)?.startTime ?? 0) : 0) -
988
+ (a.currentJobId ? (manager.getJob(a.currentJobId)?.startTime ?? 0) : 0) ||
989
+ a.subagentId.localeCompare(b.subagentId),
990
+ );
991
+ if (predecessors.length === 0) return { ok: true, identity };
992
+ if (descriptor.duplicatePolicy === "warn")
993
+ return {
994
+ ok: true,
995
+ identity,
996
+ disposition: { action: "warned", predecessorIds: predecessors.map(record => record.subagentId) },
997
+ };
998
+ for (const predecessor of predecessors)
999
+ if (!manager.cancelSubagent(predecessor.subagentId, { ownerId }))
1000
+ return { ok: false, identity, error: "duplicate_supersede_failed" };
1001
+ return {
1002
+ ok: true,
1003
+ identity,
1004
+ disposition: { action: "superseded", predecessorIds: predecessors.map(record => record.subagentId) },
1005
+ };
1006
+ })();
1007
+ if (!admission.ok) {
1008
+ descriptor.lastAdmissionFailure = admission.error;
1009
+ return undefined;
1010
+ }
1011
+ if (!descriptor) return undefined;
894
1012
  const forkSeeds = descriptor.forkContextSeed
895
1013
  ? new Map([[descriptor.task.id, descriptor.forkContextSeed]])
896
1014
  : undefined;
1015
+ const resumedTask = {
1016
+ ...descriptor.task,
1017
+ repositoryBinding: descriptor.repositoryBinding,
1018
+ duplicate_policy: descriptor.duplicatePolicy,
1019
+ };
897
1020
  return manager.register(
898
1021
  "task",
899
1022
  descriptor.task.id,
900
1023
  async ({ signal: runSignal }) => {
901
1024
  const result = await this.#executeSync(
902
1025
  descriptor.toolCallId,
903
- { ...descriptor.params, tasks: [descriptor.task] },
1026
+ { ...descriptor.params, tasks: [resumedTask] },
904
1027
  runSignal,
905
1028
  undefined,
906
1029
  [descriptor.task.id],
@@ -921,7 +1044,22 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
921
1044
  },
922
1045
  );
923
1046
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
924
- const singleResult = result.details?.results[0];
1047
+ const rawSingleResult = result.details?.results[0];
1048
+ const singleResult = rawSingleResult
1049
+ ? {
1050
+ ...rawSingleResult,
1051
+ duplicateDisposition: (() => {
1052
+ const initial = descriptor.initialDisposition;
1053
+ const current = admission.disposition;
1054
+ if (!initial) return current;
1055
+ if (!current) return initial;
1056
+ return {
1057
+ action: current.action,
1058
+ predecessorIds: [...new Set([...initial.predecessorIds, ...current.predecessorIds])],
1059
+ };
1060
+ })(),
1061
+ }
1062
+ : rawSingleResult;
925
1063
  return subagentRunOutcomeFromSingleResult(finalText, singleResult);
926
1064
  },
927
1065
  {
@@ -932,6 +1070,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
932
1070
  id: descriptor.task.id,
933
1071
  agent: descriptor.params.agent,
934
1072
  agentSource: descriptor.agentSource,
1073
+ duplicateIdentity: duplicateIdentityKey(descriptor.duplicateIdentity),
1074
+ duplicateDisposition: (admission.disposition?.action ??
1075
+ descriptor.initialDisposition?.action) as DuplicateDisposition["action"] | undefined,
935
1076
  description: descriptor.task.description,
936
1077
  assignment: descriptor.task.assignment.trim(),
937
1078
  },
@@ -968,7 +1109,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
968
1109
  for (let i = 0; i < taskItems.length; i++) {
969
1110
  const taskItem = taskItems[i];
970
1111
  if (signal?.aborted) {
971
- failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
1112
+ failedSchedules.push({
1113
+ task: taskItem as TaskItem & { id: string },
1114
+ taskIndex: i,
1115
+ message: "cancelled before scheduling",
1116
+ signalSkip: true,
1117
+ });
972
1118
  const progress = progressByTaskId.get(taskItem.id);
973
1119
  if (progress) {
974
1120
  progress.status = "aborted";
@@ -981,13 +1127,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
981
1127
  if (signal?.aborted) {
982
1128
  for (let skippedIndex = i; skippedIndex < taskItems.length; skippedIndex++) {
983
1129
  const skippedTask = taskItems[skippedIndex]!;
984
- failedSchedules.push(`${skippedTask.id}: cancelled before scheduling`);
1130
+ failedSchedules.push({
1131
+ task: skippedTask as TaskItem & { id: string },
1132
+ taskIndex: skippedIndex,
1133
+ message: "cancelled before scheduling",
1134
+ signalSkip: true,
1135
+ });
985
1136
  const skippedProgress = progressByTaskId.get(skippedTask.id);
986
1137
  if (skippedProgress) skippedProgress.status = "aborted";
987
1138
  }
988
1139
  break;
989
1140
  }
990
- if (frozenForkSeed) frozenForkSeeds.set(uniqueId, frozenForkSeed);
1141
+ if (frozenForkSeed) {
1142
+ frozenForkSeeds.set(uniqueId, frozenForkSeed);
1143
+ // The async callback may execute through a compatibility path that retains
1144
+ // the caller's task id instead of the allocated artifact id. Preserve the
1145
+ // same immutable seed under both identities so detached execution never
1146
+ // rebuilds context after dispatch.
1147
+ frozenForkSeeds.set(taskItem.id, frozenForkSeed);
1148
+ }
991
1149
  const singleParams: TaskParams = { ...params, tasks: [taskItem] };
992
1150
  const label = uniqueId;
993
1151
  try {
@@ -997,6 +1155,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
997
1155
  const subtaskSessionFile = managedPersistence
998
1156
  ? null
999
1157
  : path.join(batchArtifactsDir ?? externalTaskSessionsDir!, `${uniqueId}.jsonl`);
1158
+ const admission = admitDuplicateLaunch(uniqueId, taskItem, taskItem.duplicate_policy ?? "warn");
1159
+ if (!admission.ok) {
1160
+ failedSchedules.push({
1161
+ task: taskItem as TaskItem & { id: string },
1162
+ taskIndex: i,
1163
+ message: admission.error ?? "duplicate_supersede_failed",
1164
+ signalSkip: false,
1165
+ duplicateDisposition: admission.disposition,
1166
+ });
1167
+ continue;
1168
+ }
1000
1169
  const jobId = manager.register(
1001
1170
  "task",
1002
1171
  label,
@@ -1036,7 +1205,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1036
1205
  },
1037
1206
  );
1038
1207
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
1039
- const singleResult = result.details?.results[0];
1208
+ const rawSingleResult = result.details?.results[0];
1209
+ const singleResult = rawSingleResult
1210
+ ? {
1211
+ ...rawSingleResult,
1212
+ duplicateDisposition: rawSingleResult.duplicateDisposition ?? admission.disposition,
1213
+ }
1214
+ : rawSingleResult;
1040
1215
  if (progress) {
1041
1216
  progress.status = singleResult?.paused
1042
1217
  ? "paused"
@@ -1119,6 +1294,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1119
1294
  id: uniqueId,
1120
1295
  agent: params.agent,
1121
1296
  agentSource: fallbackAgentSource,
1297
+ duplicateIdentity: duplicateIdentityKey(admission.identity),
1298
+ duplicateDisposition: admission.disposition?.action,
1122
1299
  description: taskItem.description,
1123
1300
  assignment: taskItem.assignment.trim(),
1124
1301
  },
@@ -1145,6 +1322,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1145
1322
  durableOutputAllowed: Boolean(batchArtifactsDir),
1146
1323
  forkContextSeed: frozenForkSeed,
1147
1324
  agentSource: fallbackAgentSource,
1325
+ repositoryBinding: taskItem.repositoryBinding as RepositoryBinding,
1326
+ duplicateIdentity: admission.identity,
1327
+ duplicatePolicy: taskItem.duplicate_policy ?? "warn",
1328
+ initialDisposition: admission.disposition,
1148
1329
  } satisfies TaskResumeDescriptor,
1149
1330
  },
1150
1331
  resumeRunner,
@@ -1155,10 +1336,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1155
1336
  subagentId: uniqueId,
1156
1337
  ownerId: this.session.getAgentId?.() ?? undefined,
1157
1338
  currentJobId: jobId,
1339
+ currentJobGeneration: manager.getJob(jobId)?.generation,
1158
1340
  historicalJobIds: [],
1159
1341
  status: manager.getJob(jobId)?.status ?? "running",
1160
1342
  sessionFile: null,
1161
1343
  resumable: true,
1344
+ duplicateIdentity: duplicateIdentityKey(admission.identity),
1345
+ duplicateDisposition: admission.disposition?.action,
1162
1346
  });
1163
1347
  }
1164
1348
  } catch (error) {
@@ -1168,7 +1352,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1168
1352
  : error instanceof Error
1169
1353
  ? error.message
1170
1354
  : String(error);
1171
- failedSchedules.push(`${taskItem.id}: ${message}`);
1355
+ failedSchedules.push({
1356
+ task: taskItem as TaskItem & { id: string },
1357
+ taskIndex: i,
1358
+ message,
1359
+ signalSkip: false,
1360
+ });
1172
1361
  const progress = progressByTaskId.get(taskItem.id);
1173
1362
  if (progress) {
1174
1363
  progress.status = "failed";
@@ -1181,11 +1370,36 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1181
1370
  failedJobs += failedSchedules.length;
1182
1371
  }
1183
1372
 
1373
+ const scheduleFailureReceipts = failedSchedules
1374
+ .slice()
1375
+ .sort((a, b) => a.taskIndex - b.taskIndex)
1376
+ .map((entry, index) =>
1377
+ buildTaskReceipt({
1378
+ index,
1379
+ id: entry.task.id,
1380
+ agent: params.agent,
1381
+ agentSource: fallbackAgentSource,
1382
+ task: renderTaskAssignment(entry.task.assignment, simpleMode),
1383
+ assignment: entry.task.assignment,
1384
+ description: entry.task.description,
1385
+ status: "failed",
1386
+ exitCode: 1,
1387
+ aborted: entry.signalSkip ? true : undefined,
1388
+ abortReason: entry.signalSkip ? "Cancelled before start" : undefined,
1389
+ truncated: false,
1390
+ durationMs: 0,
1391
+ tokens: 0,
1392
+ output: "",
1393
+ stderr: entry.message,
1394
+ error: entry.message,
1395
+ duplicateDisposition: entry.duplicateDisposition,
1396
+ } as SingleResult),
1397
+ );
1184
1398
  if (startedJobs.length === 0) {
1185
- const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
1399
+ const failureText = `Failed to start background task jobs: ${failedSchedules.map(entry => `${entry.task.id}: ${entry.message}`).join("; ")}`;
1186
1400
  return {
1187
1401
  content: [{ type: "text", text: failureText }],
1188
- details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
1402
+ details: { projectAgentsDir: null, results: scheduleFailureReceipts, totalDurationMs: 0 },
1189
1403
  };
1190
1404
  }
1191
1405
 
@@ -1233,7 +1447,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1233
1447
  ],
1234
1448
  details: {
1235
1449
  projectAgentsDir: null,
1236
- results: [],
1450
+ results: scheduleFailureReceipts,
1237
1451
  totalDurationMs: 0,
1238
1452
  progress: getProgressSnapshot(),
1239
1453
  async: { state: asyncState, jobId: startedJobs[0].jobId, type: "task" },
@@ -1705,27 +1919,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1705
1919
  if (taskRepositoryBinding.relativeSubdir) {
1706
1920
  assertPathUnderRepositoryBinding(taskRepositoryBinding, ".");
1707
1921
  }
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
1922
  if (!isIsolated) {
1730
1923
  await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, taskRepositoryBinding);
1731
1924
  const result = await runSubprocess({
@@ -1780,32 +1973,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1780
1973
  parentTelemetry: this.session.getTelemetry?.(),
1781
1974
  forkContextSeed,
1782
1975
  });
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
1976
  return {
1804
1977
  ...result,
1805
1978
  ...(forkContext ? { forkContext } : {}),
1806
1979
  forkContextAdvisory,
1807
1980
  repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1808
- ...(reviewSource ? { reviewSource } : {}),
1809
1981
  };
1810
1982
  }
1811
1983
 
@@ -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. */
package/src/tools/ask.ts CHANGED
@@ -1613,7 +1613,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1613
1613
  signal,
1614
1614
  initialSelection,
1615
1615
  navigation: options?.navigation,
1616
- scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
1616
+ scrollTitleRows: DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
1617
1617
  otherOptionLabel,
1618
1618
  autoSelectOnTimeout: !intentContract(q.deepInterview) && !intentReview(q.deepInterview),
1619
1619
  clarificationOptionLabel,