@gajae-code/coding-agent 0.11.7 → 0.11.9

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 (213) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/cli.d.ts +1 -0
  4. package/dist/types/config/keybindings.d.ts +33 -3
  5. package/dist/types/config/model-profile-activation.d.ts +4 -3
  6. package/dist/types/config/model-profile-contract.d.ts +38 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -3
  8. package/dist/types/config/settings-schema.d.ts +15 -2
  9. package/dist/types/edit/modes/patch.d.ts +13 -0
  10. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  11. package/dist/types/eval/py/executor.d.ts +1 -1
  12. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  14. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  17. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  19. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  20. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  21. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  22. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  23. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  24. package/dist/types/main.d.ts +8 -0
  25. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  26. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  27. package/dist/types/modes/components/welcome.d.ts +4 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  30. package/dist/types/modes/interactive-mode.d.ts +5 -2
  31. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  32. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  33. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  34. package/dist/types/modes/types.d.ts +3 -0
  35. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  40. package/dist/types/sdk/broker/broker.d.ts +2 -0
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  43. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  44. package/dist/types/sdk/bus/config.d.ts +2 -0
  45. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  46. package/dist/types/sdk/bus/index.d.ts +25 -0
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  52. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  53. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  54. package/dist/types/sdk/host/host.d.ts +1 -1
  55. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  56. package/dist/types/sdk/index.d.ts +2 -0
  57. package/dist/types/sdk/prompt-status.d.ts +88 -0
  58. package/dist/types/sdk/session-directory.d.ts +1 -1
  59. package/dist/types/sdk/startup-capability.d.ts +3 -0
  60. package/dist/types/session/agent-session.d.ts +2 -0
  61. package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
  62. package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
  63. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  64. package/dist/types/session/session-manager.d.ts +5 -1
  65. package/dist/types/session/session-storage.d.ts +31 -0
  66. package/dist/types/skill-state/active-state.d.ts +4 -0
  67. package/dist/types/task/index.d.ts +3 -1
  68. package/dist/types/task/receipt.d.ts +2 -0
  69. package/dist/types/task/types.d.ts +94 -0
  70. package/dist/types/tools/ask.d.ts +454 -174
  71. package/dist/types/tools/image-gen.d.ts +47 -1
  72. package/dist/types/tools/index.d.ts +4 -0
  73. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  74. package/package.json +8 -8
  75. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  76. package/scripts/dogfood-repository-binding.ts +209 -0
  77. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  78. package/scripts/run-test-manifest.ts +4 -4
  79. package/src/cli/args.ts +2 -92
  80. package/src/cli/mcp-cli.ts +4 -21
  81. package/src/cli/notify-cli.ts +5 -38
  82. package/src/cli.ts +50 -0
  83. package/src/commands/deep-interview.ts +9 -4
  84. package/src/commands/sdk.ts +2 -2
  85. package/src/commit/agentic/index.ts +3 -3
  86. package/src/commit/map-reduce/index.ts +2 -2
  87. package/src/config/keybindings.ts +157 -24
  88. package/src/config/model-profile-activation.ts +7 -18
  89. package/src/config/model-profile-contract.ts +161 -0
  90. package/src/config/model-profiles.ts +2 -3
  91. package/src/config/model-registry.ts +11 -1
  92. package/src/config/settings-schema.ts +17 -2
  93. package/src/coordinator-mcp/model-preset.ts +21 -74
  94. package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
  95. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  96. package/src/edit/index.ts +3 -6
  97. package/src/edit/modes/patch.ts +38 -1
  98. package/src/edit/modes/replace.ts +42 -0
  99. package/src/edit/path-mutation-lock.ts +112 -0
  100. package/src/eval/py/executor.ts +30 -11
  101. package/src/eval/py/prelude.py +2 -2
  102. package/src/eval/py/tool-bridge.ts +34 -18
  103. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  104. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  105. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  106. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  107. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  108. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  109. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  110. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  111. package/src/gjc-runtime/launch-tmux.ts +1 -1
  112. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  113. package/src/gjc-runtime/ralplan-runtime.ts +78 -4
  114. package/src/gjc-runtime/repository-binding.ts +267 -0
  115. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  116. package/src/gjc-runtime/state-writer.ts +166 -2
  117. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  118. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  119. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  120. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  121. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  122. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  123. package/src/internal-urls/docs-index.generated.ts +8 -6
  124. package/src/internal-urls/local-protocol.ts +33 -16
  125. package/src/internal-urls/mcp-protocol.ts +73 -11
  126. package/src/main.ts +25 -10
  127. package/src/modes/DESIGN.md +5 -0
  128. package/src/modes/acp/acp-agent.ts +50 -16
  129. package/src/modes/components/model-selector.ts +42 -8
  130. package/src/modes/components/plan-preview-overlay.ts +1 -1
  131. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  132. package/src/modes/components/tool-status-header.ts +20 -4
  133. package/src/modes/components/welcome.ts +32 -20
  134. package/src/modes/controllers/command-controller.ts +97 -54
  135. package/src/modes/controllers/event-controller.ts +10 -0
  136. package/src/modes/controllers/input-controller.ts +7 -2
  137. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  138. package/src/modes/controllers/selector-controller.ts +46 -54
  139. package/src/modes/interactive-mode.ts +70 -36
  140. package/src/modes/prompt-action-autocomplete.ts +32 -13
  141. package/src/modes/prompt-suggestion-controller.ts +96 -0
  142. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  143. package/src/modes/types.ts +3 -0
  144. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  145. package/src/modes/utils/ui-helpers.ts +5 -3
  146. package/src/prompts/agents/architect.md +0 -1
  147. package/src/prompts/agents/critic.md +0 -1
  148. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  149. package/src/prompts/system/system-prompt.md +7 -4
  150. package/src/prompts/tools/cron.md +3 -1
  151. package/src/runtime-mcp/json-rpc.ts +7 -4
  152. package/src/runtime-mcp/manager.ts +51 -53
  153. package/src/runtime-mcp/oauth-flow.ts +0 -72
  154. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  155. package/src/runtime-mcp/redaction.ts +30 -0
  156. package/src/runtime-mcp/transports/http.ts +13 -6
  157. package/src/sdk/broker/broker.ts +34 -20
  158. package/src/sdk/broker/lifecycle.ts +185 -36
  159. package/src/sdk/broker/session-index.ts +11 -1
  160. package/src/sdk/bus/chat-daemon-control.ts +20 -5
  161. package/src/sdk/bus/config-commands.ts +2 -2
  162. package/src/sdk/bus/config.ts +9 -1
  163. package/src/sdk/bus/conversation-store.ts +40 -6
  164. package/src/sdk/bus/index.ts +240 -65
  165. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  166. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  167. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  168. package/src/sdk/bus/notification-orchestration.ts +12 -0
  169. package/src/sdk/bus/notification-service.ts +27 -2
  170. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  171. package/src/sdk/bus/recent-activity.ts +90 -26
  172. package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
  173. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  174. package/src/sdk/bus/telegram-daemon.ts +853 -112
  175. package/src/sdk/bus/telegram-reference.ts +20 -2
  176. package/src/sdk/cli/session-cli.ts +15 -2
  177. package/src/sdk/host/control/dispatch.ts +1 -1
  178. package/src/sdk/host/control/operations.ts +1 -1
  179. package/src/sdk/host/host.ts +2 -6
  180. package/src/sdk/host/query/handlers.ts +70 -5
  181. package/src/sdk/index.ts +12 -0
  182. package/src/sdk/prompt-status.ts +97 -0
  183. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  184. package/src/sdk/protocol/operation-registry.ts +18 -4
  185. package/src/sdk/session-directory.ts +5 -1
  186. package/src/sdk/session.ts +2 -0
  187. package/src/sdk/startup-capability.ts +9 -1
  188. package/src/session/agent-session.ts +138 -5
  189. package/src/session/blob-store.ts +7 -2
  190. package/src/session/internal/managed-session-scope.ts +998 -133
  191. package/src/session/internal/managed-session-storage.ts +268 -203
  192. package/src/session/internal/native-publish-outcome.ts +266 -0
  193. package/src/session/session-manager.ts +406 -360
  194. package/src/session/session-storage.ts +151 -11
  195. package/src/skill-state/active-state.ts +10 -0
  196. package/src/task/executor.ts +13 -5
  197. package/src/task/gjc-command.ts +2 -2
  198. package/src/task/index.ts +121 -15
  199. package/src/task/receipt.ts +3 -0
  200. package/src/task/types.ts +34 -3
  201. package/src/tools/ask.ts +199 -88
  202. package/src/tools/ast-edit.ts +2 -2
  203. package/src/tools/bash-pty-selection.ts +2 -2
  204. package/src/tools/browser/screenshot-format.ts +6 -1
  205. package/src/tools/cron.ts +1 -1
  206. package/src/tools/image-gen.ts +178 -6
  207. package/src/tools/index.ts +4 -0
  208. package/src/tools/render-utils.ts +7 -1
  209. package/src/utils/edit-mode.ts +2 -2
  210. package/src/utils/prompt-suggestion.ts +265 -0
  211. package/src/utils/sixel.ts +3 -3
  212. package/src/web/insane/url-guard.ts +3 -1
  213. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -167,6 +167,8 @@ export interface SessionStorage {
167
167
  readSnapshotSync?(path: string): SessionStorageSnapshot;
168
168
  statSync(path: string): SessionStorageStat;
169
169
  listFilesSync(dir: string, pattern: string): string[];
170
+ /** List matching files with mtimes without issuing one JavaScript stat call per path. */
171
+ listFilesByMtime?(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>>;
170
172
  /**
171
173
  * Strict directory scan that never suppresses scan/root errors. Used by strict
172
174
  * authorization inventory; the forgiving {@link listFilesSync} stays display-only.
@@ -259,6 +261,19 @@ export interface VerifiedSessionDeleteTarget {
259
261
  detachedArtifactsPath?: string;
260
262
  /** Identity-bound quarantine path retained when transcript unlink deferred cleanup. */
261
263
  detachedTranscriptPath?: string;
264
+ /** Native-retained publisher successor observed during transcript cleanup. */
265
+ retainedTranscriptSuccessorPath?: string;
266
+ /** Native-retained exchange placeholder observed during transcript cleanup. */
267
+ retainedTranscriptPlaceholderPath?: string;
268
+ /** Native-retained transcript entry whose identity could not be verified. */
269
+ retainedTranscriptUnknownPath?: string;
270
+ /** Native-retained publisher successor observed during cleanup. */
271
+ retainedArtifactsSuccessorPath?: string;
272
+ /** Native-retained exchange placeholder observed during cleanup. */
273
+ retainedArtifactsPlaceholderPath?: string;
274
+ /** Native-retained entry whose identity could not be verified. */
275
+ retainedArtifactsUnknownPath?: string;
276
+
262
277
  /** Caller-published, no-replace quarantine pathname for the next artifact detach. */
263
278
  plannedArtifactsPath?: string;
264
279
  /** Caller-published, no-replace quarantine pathname for the next transcript detach. */
@@ -288,6 +303,9 @@ export type VerifiedSessionDeleteResult =
288
303
  artifactsTree: NativeDirectoryTreeSnapshot;
289
304
  /** Transcript identity (unchanged) for retry binding. */
290
305
  transcriptIdentity: SessionStorageFileIdentity;
306
+ retainedSuccessorPath?: string;
307
+ retainedPlaceholderPath?: string;
308
+ retainedUnknownPath?: string;
291
309
  }
292
310
  | {
293
311
  kind: "cleanup_pending";
@@ -297,6 +315,9 @@ export type VerifiedSessionDeleteResult =
297
315
  transcriptIdentity: SessionStorageFileIdentity;
298
316
  /** Optional identity-bound transcript quarantine path for restart cleanup. */
299
317
  detachedTranscriptPath?: string;
318
+ retainedSuccessorPath?: string;
319
+ retainedPlaceholderPath?: string;
320
+ retainedUnknownPath?: string;
300
321
  };
301
322
 
302
323
  /** Default OS-close dispatcher: a direct `fs.closeSync`. */
@@ -306,7 +327,22 @@ const defaultCloseAdapter: SessionStorageWriterCloseAdapter = {
306
327
  },
307
328
  };
308
329
 
309
- type NativeExactUnlinkResult = { ok: true; detachedPath?: string } | { ok: false; code: string; detachedPath?: string };
330
+ type NativeExactUnlinkResult =
331
+ | {
332
+ ok: true;
333
+ detachedPath?: string;
334
+ retainedSuccessorPath?: string;
335
+ retainedPlaceholderPath?: string;
336
+ retainedUnknownPath?: string;
337
+ }
338
+ | {
339
+ ok: false;
340
+ code: string;
341
+ detachedPath?: string;
342
+ retainedSuccessorPath?: string;
343
+ retainedPlaceholderPath?: string;
344
+ retainedUnknownPath?: string;
345
+ };
310
346
  type NativeExactUnlink = (
311
347
  path: string,
312
348
  identity: {
@@ -671,6 +707,21 @@ export class FileSessionStorage implements SessionStorage {
671
707
  return [];
672
708
  }
673
709
  }
710
+ async listFilesByMtime(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>> {
711
+ const result = await native.glob({
712
+ path: dir,
713
+ pattern,
714
+ fileType: native.FileType.File,
715
+ recursive: false,
716
+ hidden: false,
717
+ gitignore: false,
718
+ sortByMtime: true,
719
+ });
720
+ return result.matches.map(match => ({
721
+ path: path.join(dir, match.path),
722
+ mtimeMs: match.mtime ?? 0,
723
+ }));
724
+ }
674
725
 
675
726
  listFilesStrictSync(dir: string, pattern: string): string[] {
676
727
  // Strict: never suppress scan/root errors. Authorization inventory depends on
@@ -766,7 +817,13 @@ export class FileSessionStorage implements SessionStorage {
766
817
  expectedArtifactsIdentity,
767
818
  expectedArtifactsTree,
768
819
  detachedArtifactsPath,
820
+ retainedArtifactsSuccessorPath,
821
+ retainedArtifactsPlaceholderPath,
822
+ retainedArtifactsUnknownPath,
769
823
  detachedTranscriptPath,
824
+ retainedTranscriptSuccessorPath,
825
+ retainedTranscriptPlaceholderPath,
826
+ retainedTranscriptUnknownPath,
770
827
  plannedArtifactsPath,
771
828
  plannedTranscriptPath,
772
829
  artifactsRemoved,
@@ -847,7 +904,8 @@ export class FileSessionStorage implements SessionStorage {
847
904
  if (
848
905
  !expectedArtifactsIdentity ||
849
906
  path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
850
- !path.basename(detachedArtifactsPath).startsWith(".gjc-delete-")
907
+ (!path.basename(detachedArtifactsPath).startsWith(".gjc-delete-") &&
908
+ !detachedArtifactsPath.endsWith(".removing"))
851
909
  ) {
852
910
  throw new SessionDeleteVerificationError("artifacts", "Detached artifact cleanup evidence is invalid");
853
911
  }
@@ -882,17 +940,20 @@ export class FileSessionStorage implements SessionStorage {
882
940
  artifactsIdentity: expectedArtifactsIdentity,
883
941
  detachedArtifactsPath: retainedRoot,
884
942
  artifactsTree: expectedArtifactsTree,
943
+ ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
944
+ ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
945
+ : {}),
946
+ ...((removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath)
947
+ ? { retainedPlaceholderPath: removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath }
948
+ : {}),
949
+ ...((removal.retainedUnknownPath ?? retainedArtifactsUnknownPath)
950
+ ? { retainedUnknownPath: removal.retainedUnknownPath ?? retainedArtifactsUnknownPath }
951
+ : {}),
885
952
  transcriptIdentity,
886
953
  };
887
954
  }
888
955
  }
889
956
 
890
- if (artifactsRemoved && (detachedArtifactsPath || expectedArtifactsIdentity)) {
891
- throw new SessionDeleteVerificationError(
892
- "artifacts",
893
- "Artifact phase receipt conflicts with pending artifact cleanup",
894
- );
895
- }
896
957
  const artifactsDir = transcriptPath.slice(0, -6);
897
958
  const artifactsIdentity = this.#optionalDirectoryIdentity(artifactsDir);
898
959
  if (artifactsRemoved && artifactsIdentity) {
@@ -947,12 +1008,48 @@ export class FileSessionStorage implements SessionStorage {
947
1008
  directory: true,
948
1009
  quarantineName: path.basename(plannedArtifactsPath),
949
1010
  });
950
- if (!detach.ok || !detach.detachedPath) {
1011
+ if (!detach.detachedPath) {
951
1012
  throw new SessionDeleteVerificationError(
952
1013
  "artifacts",
953
1014
  `Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
954
1015
  );
955
1016
  }
1017
+ if (!detach.ok && process.platform !== "win32") {
1018
+ let descriptor: number | undefined;
1019
+ try {
1020
+ descriptor = fs.openSync(
1021
+ path.dirname(transcriptPath),
1022
+ fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
1023
+ );
1024
+ const durableParent = fs.fstatSync(descriptor, { bigint: true });
1025
+ if (
1026
+ !durableParent.isDirectory() ||
1027
+ durableParent.dev !== parentIdentity.dev ||
1028
+ durableParent.ino !== parentIdentity.ino
1029
+ )
1030
+ throw new Error("parent_changed");
1031
+ fs.fsyncSync(descriptor);
1032
+ } catch (error) {
1033
+ throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
1034
+ } finally {
1035
+ if (descriptor !== undefined) fs.closeSync(descriptor);
1036
+ }
1037
+ }
1038
+ if (!detach.ok) {
1039
+ return {
1040
+ kind: "cleanup_pending",
1041
+ phase: "artifacts",
1042
+ error: new SessionDeleteVerificationError("artifacts", `Exact artifact detach retained: ${detach.code}`),
1043
+ artifactsIdentity,
1044
+ detachedArtifactsPath: detach.detachedPath,
1045
+ artifactsTree,
1046
+ ...(detach.retainedSuccessorPath ? { retainedSuccessorPath: detach.retainedSuccessorPath } : {}),
1047
+ ...(detach.retainedPlaceholderPath ? { retainedPlaceholderPath: detach.retainedPlaceholderPath } : {}),
1048
+ ...(detach.retainedUnknownPath ? { retainedUnknownPath: detach.retainedUnknownPath } : {}),
1049
+
1050
+ transcriptIdentity,
1051
+ };
1052
+ }
956
1053
  const removal = removeDirectoryTreeExact(detach.detachedPath, artifactsTree);
957
1054
  if (!removal.ok) {
958
1055
  const retainedRoot = removal.detachedPath ?? detach.detachedPath;
@@ -971,6 +1068,15 @@ export class FileSessionStorage implements SessionStorage {
971
1068
  artifactsIdentity,
972
1069
  detachedArtifactsPath: retainedRoot,
973
1070
  artifactsTree,
1071
+ ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
1072
+ ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
1073
+ : {}),
1074
+ ...((removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath)
1075
+ ? { retainedPlaceholderPath: removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath }
1076
+ : {}),
1077
+ ...((removal.retainedUnknownPath ?? retainedArtifactsUnknownPath)
1078
+ ? { retainedUnknownPath: removal.retainedUnknownPath ?? retainedArtifactsUnknownPath }
1079
+ : {}),
974
1080
  transcriptIdentity,
975
1081
  };
976
1082
  }
@@ -1010,13 +1116,27 @@ export class FileSessionStorage implements SessionStorage {
1010
1116
  });
1011
1117
  if (!deletion.ok) {
1012
1118
  const error = exactUnlinkFailure(deletion);
1013
- if (error.kind === "identity" || error.kind === "symlink") throw error;
1119
+ const retainedAuthority =
1120
+ deletion.detachedPath ||
1121
+ deletion.retainedSuccessorPath ||
1122
+ deletion.retainedPlaceholderPath ||
1123
+ deletion.retainedUnknownPath;
1124
+ if ((error.kind === "identity" || error.kind === "symlink") && !retainedAuthority) throw error;
1014
1125
  return {
1015
1126
  kind: "cleanup_pending",
1016
1127
  phase: "transcript",
1017
1128
  error,
1018
1129
  transcriptIdentity,
1019
1130
  detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
1131
+ ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
1132
+ ? { retainedSuccessorPath: deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath }
1133
+ : {}),
1134
+ ...((deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath)
1135
+ ? { retainedPlaceholderPath: deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath }
1136
+ : {}),
1137
+ ...((deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath)
1138
+ ? { retainedUnknownPath: deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath }
1139
+ : {}),
1020
1140
  };
1021
1141
  }
1022
1142
  return { kind: "deleted" };
@@ -1058,13 +1178,27 @@ export class FileSessionStorage implements SessionStorage {
1058
1178
  });
1059
1179
  if (!deletion.ok) {
1060
1180
  const error = exactUnlinkFailure(deletion);
1061
- if (error.kind === "identity" || error.kind === "symlink") throw error;
1181
+ const retainedAuthority =
1182
+ deletion.detachedPath ||
1183
+ deletion.retainedSuccessorPath ||
1184
+ deletion.retainedPlaceholderPath ||
1185
+ deletion.retainedUnknownPath;
1186
+ if ((error.kind === "identity" || error.kind === "symlink") && !retainedAuthority) throw error;
1062
1187
  return {
1063
1188
  kind: "cleanup_pending",
1064
1189
  phase: "transcript",
1065
1190
  error,
1066
1191
  transcriptIdentity,
1067
1192
  detachedTranscriptPath: deletion.detachedPath,
1193
+ ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
1194
+ ? { retainedSuccessorPath: deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath }
1195
+ : {}),
1196
+ ...((deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath)
1197
+ ? { retainedPlaceholderPath: deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath }
1198
+ : {}),
1199
+ ...((deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath)
1200
+ ? { retainedUnknownPath: deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath }
1201
+ : {}),
1068
1202
  };
1069
1203
  }
1070
1204
  return { kind: "deleted" };
@@ -1353,6 +1487,12 @@ export class MemorySessionStorage implements SessionStorage {
1353
1487
  }
1354
1488
  return files;
1355
1489
  }
1490
+ listFilesByMtime(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>> {
1491
+ const files = this.listFilesSync(dir, pattern)
1492
+ .map(path => ({ path, mtimeMs: this.statSync(path).mtimeMs }))
1493
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
1494
+ return Promise.resolve(files);
1495
+ }
1356
1496
  listFilesStrictSync(dir: string, pattern: string): string[] {
1357
1497
  // In-memory scan never suppresses; identical to the display scan.
1358
1498
  return this.listFilesSync(dir, pattern);
@@ -71,6 +71,8 @@ export interface SkillActiveEntry {
71
71
  handoff_at?: string;
72
72
  active_subskills?: ActiveSubskillEntry[];
73
73
  source_state_revision?: number;
74
+ /** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
75
+ committed_mode_state_revision?: number;
74
76
  }
75
77
 
76
78
  export interface SkillActiveState {
@@ -114,6 +116,8 @@ export interface SyncSkillActiveStateOptions {
114
116
  handoff_at?: string;
115
117
  active_subskills?: ActiveSubskillEntry[];
116
118
  sourceRevision?: number;
119
+ /** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
120
+ committedModeRevision?: number;
117
121
  }
118
122
 
119
123
  const HUD_TEXT_LIMIT = 80;
@@ -785,6 +789,9 @@ async function persistActiveEntry(
785
789
  await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
786
790
  cwd,
787
791
  audit: activeStateWriterAudit("write-active-entry", sessionScope),
792
+ ...(typeof entry.committed_mode_state_revision === "number"
793
+ ? { orderingRevision: entry.committed_mode_state_revision }
794
+ : {}),
788
795
  });
789
796
  }
790
797
  }
@@ -863,6 +870,9 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
863
870
  ? { active_subskills: preservedActiveSubskills }
864
871
  : {}),
865
872
  ...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
873
+ ...(typeof options.committedModeRevision === "number"
874
+ ? { committed_mode_state_revision: options.committedModeRevision }
875
+ : {}),
866
876
  };
867
877
  const sessionScope = { sessionId: options.sessionId };
868
878
  await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
@@ -486,11 +486,12 @@ function buildSchemaViolationOutcome(
486
486
  missing.length > 0
487
487
  ? `schema_violation: missing required fields: ${missing.join(", ")}`
488
488
  : `schema_violation: ${failure.message}`;
489
+ const dataPreview = previewOffendingData(data);
489
490
  const payload = {
490
491
  error: "schema_violation",
491
492
  message: failure.message,
492
493
  missingRequired: missing,
493
- data: previewOffendingData(data),
494
+ data,
494
495
  };
495
496
  let rawOutput: string;
496
497
  try {
@@ -498,7 +499,7 @@ function buildSchemaViolationOutcome(
498
499
  } catch {
499
500
  rawOutput = `{"error":"schema_violation","message":${JSON.stringify(headline)}}`;
500
501
  }
501
- return { rawOutput, stderr: headline, exitCode: 1 };
502
+ return { rawOutput, stderr: `${headline}. Offending data preview: ${dataPreview}`, exitCode: 1 };
502
503
  }
503
504
 
504
505
  function buildPlaceholderYieldOutcome(
@@ -541,9 +542,16 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
541
542
  const completeData = normalizeCompleteData(submitData, reportFindings);
542
543
  const { validator, error: schemaError } = buildOutputValidator(outputSchema);
543
544
  if (schemaError) {
544
- rawOutput = `{"error":"schema_violation","message":"invalid output schema: ${schemaError.replace(/"/g, '\\"')}"}`;
545
- stderr = `schema_violation: invalid output schema: ${schemaError}`;
546
- exitCode = 1;
545
+ const outcome = buildSchemaViolationOutcome(
546
+ {
547
+ message: `invalid output schema: ${schemaError}`,
548
+ missingRequired: [],
549
+ },
550
+ completeData,
551
+ );
552
+ rawOutput = outcome.rawOutput;
553
+ stderr = outcome.stderr;
554
+ exitCode = outcome.exitCode;
547
555
  } else {
548
556
  const placeholderPath = findPlaceholderYieldPath(completeData);
549
557
  if (placeholderPath) {
@@ -1,6 +1,6 @@
1
1
  import process from "node:process";
2
2
 
3
- import { $env } from "@gajae-code/utils";
3
+ import { $pickenv } from "@gajae-code/utils";
4
4
 
5
5
  interface GjcCommand {
6
6
  cmd: string;
@@ -12,7 +12,7 @@ const DEFAULT_CMD = process.platform === "win32" ? "gjc.cmd" : "gjc";
12
12
  const DEFAULT_SHELL = process.platform === "win32";
13
13
 
14
14
  export function resolveGjcCommand(): GjcCommand {
15
- const envCmd = $env.PI_SUBPROCESS_CMD;
15
+ const envCmd = $pickenv("GJC_SUBPROCESS_CMD", "PI_SUBPROCESS_CMD");
16
16
  if (envCmd?.trim()) {
17
17
  return { cmd: envCmd, args: [], shell: DEFAULT_SHELL };
18
18
  }
package/src/task/index.ts CHANGED
@@ -17,7 +17,7 @@ import * as os from "node:os";
17
17
  import path from "node:path";
18
18
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
19
19
  import type { Model, Usage } from "@gajae-code/ai";
20
- import { $env, prompt, Snowflake } from "@gajae-code/utils";
20
+ import { $pickenv, prompt, Snowflake } from "@gajae-code/utils";
21
21
  import type { ToolSession } from "..";
22
22
  import { AsyncJobManager, OwnerSubagentShutdownError, type ResumeRunner } from "../async";
23
23
  import { resolveAgentModelPatterns } from "../config/model-resolver";
@@ -43,6 +43,15 @@ import {
43
43
 
44
44
  // Import review tools for side effects (registers subagent tool handlers)
45
45
  import "../tools/review";
46
+ import {
47
+ assertExecutionRootMatchesRepositoryBinding,
48
+ assertPathUnderRepositoryBinding,
49
+ captureRepositoryBinding,
50
+ publicRepositoryBinding,
51
+ type RepositoryBinding,
52
+ RepositoryBindingError,
53
+ resolveTaskRepositoryBinding,
54
+ } from "../gjc-runtime/repository-binding";
46
55
  import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
47
56
  import { generateCommitMessage } from "../utils/commit-message-generator";
48
57
  import * as git from "../utils/git";
@@ -128,6 +137,43 @@ function addUsageTotals(target: Usage, usage: Partial<Usage>): void {
128
137
  target.cost.total += cost.total;
129
138
  }
130
139
 
140
+ /**
141
+ * Stamp/resolve repository authority for every task before agent discovery or spawn.
142
+ * Omitted bindings inherit the session worktree; declared ones must match it.
143
+ */
144
+ async function resolveTaskItemsWithRepositoryBindings(
145
+ cwd: string,
146
+ tasks: readonly TaskItem[],
147
+ ): Promise<{ tasks: TaskItem[]; error?: string }> {
148
+ const resolved: TaskItem[] = [];
149
+ for (const task of tasks) {
150
+ try {
151
+ const binding = await resolveTaskRepositoryBinding(cwd, task.repositoryBinding);
152
+ if (binding.relativeSubdir) {
153
+ assertPathUnderRepositoryBinding(binding, ".");
154
+ }
155
+ resolved.push({
156
+ ...task,
157
+ repositoryBinding: binding,
158
+ });
159
+ } catch (error) {
160
+ const id = task.id?.trim() ? task.id : "(missing-id)";
161
+ if (error instanceof RepositoryBindingError) {
162
+ return { tasks: [], error: `Task "${id}" repository binding rejected: ${error.message}` };
163
+ }
164
+ return {
165
+ tasks: [],
166
+ error: `Task "${id}" repository binding rejected: ${error instanceof Error ? error.message : String(error)}`,
167
+ };
168
+ }
169
+ }
170
+ return { tasks: resolved };
171
+ }
172
+
173
+ function repositoryBindingFromTask(task: TaskItem): RepositoryBinding | undefined {
174
+ return task.repositoryBinding ? publicRepositoryBinding(task.repositoryBinding as RepositoryBinding) : undefined;
175
+ }
176
+
131
177
  function validateTaskIdsForScheduling(tasks: readonly TaskItem[]): string | undefined {
132
178
  const invalid: string[] = [];
133
179
  for (let i = 0; i < tasks.length; i++) {
@@ -399,12 +445,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
399
445
  this.session.getSessionSpawns() ?? "*",
400
446
  );
401
447
  }
448
+ readonly #sessionRepositoryBinding: RepositoryBinding;
449
+
402
450
  private constructor(
403
451
  private readonly session: ToolSession,
404
452
  discoveredAgents: AgentDefinition[],
453
+ sessionRepositoryBinding: RepositoryBinding,
405
454
  ) {
406
- this.#blockedAgent = $env.PI_BLOCKED_AGENT;
455
+ this.#blockedAgent = $pickenv("GJC_BLOCKED_AGENT", "PI_BLOCKED_AGENT");
407
456
  this.#discoveredAgents = discoveredAgents;
457
+ this.#sessionRepositoryBinding = sessionRepositoryBinding;
408
458
  }
409
459
 
410
460
  #getTaskSimpleMode(): TaskSimpleMode {
@@ -412,11 +462,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
412
462
  }
413
463
 
414
464
  /**
415
- * Create a TaskTool instance with async agent discovery.
465
+ * Create a TaskTool instance.
466
+ * Repository authority is captured from session cwd *before* agent discovery so
467
+ * multi-repo workspaces fail closed prior to context/discovery (#2901).
416
468
  */
417
469
  static async create(session: ToolSession): Promise<TaskTool> {
470
+ const sessionRepositoryBinding = await captureRepositoryBinding(session.cwd, { displayPath: session.cwd });
471
+ // Authority check before discovery: session cwd must resolve to a stable binding.
472
+ await assertExecutionRootMatchesRepositoryBinding(session.cwd, sessionRepositoryBinding);
418
473
  const { agents } = await discoverAgents(session.cwd);
419
- return new TaskTool(session, agents);
474
+ return new TaskTool(session, agents, publicRepositoryBinding(sessionRepositoryBinding));
420
475
  }
421
476
 
422
477
  async execute(
@@ -432,13 +487,32 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
432
487
  return createTaskModeError(validationError);
433
488
  }
434
489
 
435
- const taskItems = params.tasks ?? [];
436
- const taskIdValidationError = validateTaskIdsForScheduling(taskItems);
490
+ // Re-verify session authority before using any discovered agents or scheduling work.
491
+ try {
492
+ await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, this.#sessionRepositoryBinding);
493
+ } catch (error) {
494
+ const message =
495
+ error instanceof RepositoryBindingError
496
+ ? error.message
497
+ : error instanceof Error
498
+ ? error.message
499
+ : String(error);
500
+ return createTaskModeError(`Session repository binding rejected before task discovery: ${message}`);
501
+ }
502
+
503
+ const rawTaskItems = params.tasks ?? [];
504
+ const taskIdValidationError = validateTaskIdsForScheduling(rawTaskItems);
437
505
  if (taskIdValidationError) {
438
506
  return createTaskModeError(taskIdValidationError);
439
507
  }
508
+ const bindingResolution = await resolveTaskItemsWithRepositoryBindings(this.session.cwd, rawTaskItems);
509
+ if (bindingResolution.error) {
510
+ return createTaskModeError(bindingResolution.error);
511
+ }
512
+ const taskItems = bindingResolution.tasks;
513
+ const paramsWithBindings: TaskParams = { ...params, tasks: taskItems };
440
514
  if (taskItems.length === 0) {
441
- return this.#executeSync(_toolCallId, params, signal, onUpdate);
515
+ return this.#executeSync(_toolCallId, paramsWithBindings, signal, onUpdate);
442
516
  }
443
517
  const agent = getAgent(this.#discoveredAgents, params.agent);
444
518
  if (!agent) {
@@ -926,13 +1000,31 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
926
1000
  },
927
1001
  ): Promise<AgentToolResult<TaskToolDetails>> {
928
1002
  const startTime = Date.now();
1003
+ // Pre-discovery authority: session cwd must still match the capture from create().
1004
+ try {
1005
+ await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, this.#sessionRepositoryBinding);
1006
+ } catch (error) {
1007
+ const message =
1008
+ error instanceof RepositoryBindingError
1009
+ ? error.message
1010
+ : error instanceof Error
1011
+ ? error.message
1012
+ : String(error);
1013
+ return createTaskModeError(`Session repository binding rejected before task discovery: ${message}`);
1014
+ }
1015
+ const bindingResolution = await resolveTaskItemsWithRepositoryBindings(this.session.cwd, params.tasks ?? []);
1016
+ if (bindingResolution.error) {
1017
+ return createTaskModeError(bindingResolution.error);
1018
+ }
1019
+ const boundParams: TaskParams = { ...params, tasks: bindingResolution.tasks };
1020
+
929
1021
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
930
- const { agent: agentName, context, schema: outputSchema } = params;
1022
+ const { agent: agentName, context, schema: outputSchema } = boundParams;
931
1023
  const simpleMode = this.#getTaskSimpleMode();
932
1024
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
933
1025
  const sharedContext = contextEnabled ? context?.trim() : undefined;
934
1026
  const isolationMode = this.session.settings.get("task.isolation.mode");
935
- const isolationRequested = "isolated" in params ? params.isolated === true : false;
1027
+ const isolationRequested = "isolated" in boundParams ? boundParams.isolated === true : false;
936
1028
  const isIsolated = isolationMode !== "none" && isolationRequested;
937
1029
  const mergeMode = this.session.settings.get("task.isolation.merge");
938
1030
  const commitStyle = this.session.settings.get("task.isolation.commits");
@@ -940,7 +1032,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
940
1032
  const taskDepth = this.session.taskDepth ?? 0;
941
1033
  const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
942
1034
 
943
- if (isolationMode === "none" && "isolated" in params) {
1035
+ if (isolationMode === "none" && "isolated" in boundParams) {
944
1036
  return {
945
1037
  content: [
946
1038
  {
@@ -1000,7 +1092,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1000
1092
  }
1001
1093
 
1002
1094
  const forkContextValidationError = validateForkContextRequests(
1003
- params.tasks ?? [],
1095
+ boundParams.tasks ?? [],
1004
1096
  agent,
1005
1097
  this.session.settings.get("task.forkContext.enabled"),
1006
1098
  );
@@ -1039,7 +1131,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1039
1131
  : (effectiveAgent.output ?? this.session.outputSchema);
1040
1132
 
1041
1133
  // Handle empty or missing tasks
1042
- if (!params.tasks || params.tasks.length === 0) {
1134
+ if (!boundParams.tasks || boundParams.tasks.length === 0) {
1043
1135
  return {
1044
1136
  content: [
1045
1137
  {
@@ -1057,7 +1149,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1057
1149
  };
1058
1150
  }
1059
1151
 
1060
- const tasks = params.tasks;
1152
+ const tasks = boundParams.tasks;
1061
1153
  const missingTaskIndexes: number[] = [];
1062
1154
  const idIndexes = new Map<string, number[]>();
1063
1155
 
@@ -1173,7 +1265,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1173
1265
  const emitProgress = () => {
1174
1266
  const progress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
1175
1267
  onUpdate?.({
1176
- content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
1268
+ content: [{ type: "text", text: `Running ${tasks.length} agents...` }],
1177
1269
  details: {
1178
1270
  projectAgentsDir,
1179
1271
  results: [],
@@ -1342,7 +1434,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1342
1434
  const taskSessionFile = managedPersistence
1343
1435
  ? null
1344
1436
  : (overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null);
1437
+ const taskRepositoryBinding = repositoryBindingFromTask(task) ?? this.#sessionRepositoryBinding;
1438
+ // Declared paths (relativeSubdir) must stay under the bound root before spawn.
1439
+ if (taskRepositoryBinding.relativeSubdir) {
1440
+ assertPathUnderRepositoryBinding(taskRepositoryBinding, ".");
1441
+ }
1345
1442
  if (!isIsolated) {
1443
+ await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, taskRepositoryBinding);
1346
1444
  const result = await runSubprocess({
1347
1445
  cwd: this.session.cwd,
1348
1446
  agent: effectiveAgent,
@@ -1393,7 +1491,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1393
1491
  parentTelemetry: this.session.getTelemetry?.(),
1394
1492
  forkContextSeed,
1395
1493
  });
1396
- return { ...result, ...(forkContext ? { forkContext } : {}), forkContextAdvisory };
1494
+ return {
1495
+ ...result,
1496
+ ...(forkContext ? { forkContext } : {}),
1497
+ forkContextAdvisory,
1498
+ repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1499
+ };
1397
1500
  }
1398
1501
 
1399
1502
  const taskStart = Date.now();
@@ -1406,6 +1509,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1406
1509
 
1407
1510
  isolationHandle = await ensureIsolation(repoRoot, task.id, preferredIsolationBackend);
1408
1511
  const isolationDir = isolationHandle.mergedDir;
1512
+ // Isolated worktrees must preserve the source repository identity (#2901).
1513
+ await assertExecutionRootMatchesRepositoryBinding(isolationDir, taskRepositoryBinding);
1409
1514
 
1410
1515
  const result = await runSubprocess({
1411
1516
  cwd: this.session.cwd,
@@ -1462,6 +1567,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1462
1567
  ...result,
1463
1568
  ...(forkContext ? { forkContext } : {}),
1464
1569
  forkContextAdvisory,
1570
+ repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1465
1571
  };
1466
1572
  if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
1467
1573
  try {
@@ -48,6 +48,8 @@ export interface TaskResultReceipt {
48
48
  extractedToolCounts?: Record<string, number>;
49
49
  forkContext?: SingleResult["forkContext"];
50
50
  forkContextAdvisory?: SingleResult["forkContextAdvisory"];
51
+ /** Resolved repository identity for this delegated lane (#2901). */
52
+ repositoryBinding?: SingleResult["repositoryBinding"];
51
53
  roi?: TaskRoi;
52
54
  }
53
55
 
@@ -262,6 +264,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
262
264
  extractedToolCounts,
263
265
  forkContext: raw.forkContext,
264
266
  forkContextAdvisory: raw.forkContextAdvisory,
267
+ repositoryBinding: raw.repositoryBinding,
265
268
  roi: buildTaskRoi(raw),
266
269
  };
267
270
  }