@gajae-code/coding-agent 0.11.7 → 0.11.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 (180) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/config/keybindings.d.ts +33 -3
  4. package/dist/types/config/model-profile-activation.d.ts +4 -3
  5. package/dist/types/config/model-profile-contract.d.ts +38 -0
  6. package/dist/types/config/settings-schema.d.ts +15 -2
  7. package/dist/types/edit/modes/patch.d.ts +13 -0
  8. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  9. package/dist/types/eval/py/executor.d.ts +1 -1
  10. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  11. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  12. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  13. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  14. package/dist/types/main.d.ts +8 -0
  15. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  16. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  19. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  20. package/dist/types/modes/interactive-mode.d.ts +5 -2
  21. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  22. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  24. package/dist/types/modes/types.d.ts +3 -0
  25. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  26. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  27. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  28. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  29. package/dist/types/sdk/broker/broker.d.ts +2 -0
  30. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  31. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  32. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  33. package/dist/types/sdk/bus/config.d.ts +2 -0
  34. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  35. package/dist/types/sdk/bus/index.d.ts +25 -0
  36. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  37. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  38. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
  39. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  42. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  43. package/dist/types/sdk/host/host.d.ts +1 -1
  44. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  45. package/dist/types/sdk/index.d.ts +2 -0
  46. package/dist/types/sdk/prompt-status.d.ts +88 -0
  47. package/dist/types/sdk/session-directory.d.ts +1 -1
  48. package/dist/types/sdk/startup-capability.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +2 -0
  50. package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
  51. package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
  52. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  53. package/dist/types/session/session-storage.d.ts +18 -0
  54. package/dist/types/task/index.d.ts +3 -1
  55. package/dist/types/task/receipt.d.ts +2 -0
  56. package/dist/types/task/types.d.ts +94 -0
  57. package/dist/types/tools/ask.d.ts +454 -174
  58. package/dist/types/tools/image-gen.d.ts +47 -1
  59. package/dist/types/tools/index.d.ts +4 -0
  60. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  61. package/package.json +8 -8
  62. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  63. package/scripts/dogfood-repository-binding.ts +209 -0
  64. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  65. package/scripts/run-test-manifest.ts +4 -4
  66. package/src/cli/args.ts +2 -92
  67. package/src/cli/notify-cli.ts +5 -38
  68. package/src/cli.ts +18 -0
  69. package/src/commands/sdk.ts +2 -2
  70. package/src/config/keybindings.ts +157 -24
  71. package/src/config/model-profile-activation.ts +7 -18
  72. package/src/config/model-profile-contract.ts +161 -0
  73. package/src/config/model-registry.ts +11 -1
  74. package/src/config/settings-schema.ts +17 -2
  75. package/src/coordinator-mcp/model-preset.ts +21 -74
  76. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  77. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  78. package/src/edit/index.ts +3 -6
  79. package/src/edit/modes/patch.ts +38 -1
  80. package/src/edit/modes/replace.ts +42 -0
  81. package/src/edit/path-mutation-lock.ts +112 -0
  82. package/src/eval/py/executor.ts +30 -11
  83. package/src/eval/py/prelude.py +2 -2
  84. package/src/eval/py/tool-bridge.ts +34 -18
  85. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  86. package/src/gjc-runtime/launch-tmux.ts +1 -1
  87. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  88. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  89. package/src/gjc-runtime/repository-binding.ts +267 -0
  90. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  91. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  92. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  93. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  94. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  95. package/src/internal-urls/docs-index.generated.ts +6 -5
  96. package/src/internal-urls/local-protocol.ts +33 -16
  97. package/src/internal-urls/mcp-protocol.ts +73 -11
  98. package/src/main.ts +25 -10
  99. package/src/modes/DESIGN.md +5 -0
  100. package/src/modes/acp/acp-agent.ts +50 -16
  101. package/src/modes/components/model-selector.ts +42 -8
  102. package/src/modes/components/plan-preview-overlay.ts +1 -1
  103. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  104. package/src/modes/components/tool-status-header.ts +20 -4
  105. package/src/modes/components/welcome.ts +22 -18
  106. package/src/modes/controllers/command-controller.ts +97 -54
  107. package/src/modes/controllers/event-controller.ts +10 -0
  108. package/src/modes/controllers/input-controller.ts +7 -2
  109. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  110. package/src/modes/controllers/selector-controller.ts +46 -54
  111. package/src/modes/interactive-mode.ts +41 -27
  112. package/src/modes/prompt-action-autocomplete.ts +32 -13
  113. package/src/modes/prompt-suggestion-controller.ts +96 -0
  114. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  115. package/src/modes/types.ts +3 -0
  116. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  117. package/src/modes/utils/ui-helpers.ts +5 -3
  118. package/src/prompts/agents/architect.md +0 -1
  119. package/src/prompts/agents/critic.md +0 -1
  120. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  121. package/src/prompts/system/system-prompt.md +6 -4
  122. package/src/prompts/tools/cron.md +3 -1
  123. package/src/runtime-mcp/manager.ts +51 -53
  124. package/src/runtime-mcp/oauth-flow.ts +0 -72
  125. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  126. package/src/runtime-mcp/transports/http.ts +13 -6
  127. package/src/sdk/broker/broker.ts +34 -20
  128. package/src/sdk/broker/lifecycle.ts +185 -36
  129. package/src/sdk/bus/chat-daemon-control.ts +18 -5
  130. package/src/sdk/bus/config-commands.ts +2 -2
  131. package/src/sdk/bus/config.ts +9 -1
  132. package/src/sdk/bus/conversation-store.ts +40 -6
  133. package/src/sdk/bus/index.ts +240 -65
  134. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  135. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  136. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  137. package/src/sdk/bus/notification-orchestration.ts +12 -0
  138. package/src/sdk/bus/notification-service.ts +27 -2
  139. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  140. package/src/sdk/bus/recent-activity.ts +90 -26
  141. package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
  142. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  143. package/src/sdk/bus/telegram-daemon.ts +835 -110
  144. package/src/sdk/bus/telegram-reference.ts +20 -2
  145. package/src/sdk/cli/session-cli.ts +15 -2
  146. package/src/sdk/host/control/dispatch.ts +1 -1
  147. package/src/sdk/host/control/operations.ts +1 -1
  148. package/src/sdk/host/host.ts +2 -6
  149. package/src/sdk/host/query/handlers.ts +70 -5
  150. package/src/sdk/index.ts +12 -0
  151. package/src/sdk/prompt-status.ts +97 -0
  152. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  153. package/src/sdk/protocol/operation-registry.ts +18 -4
  154. package/src/sdk/session-directory.ts +5 -1
  155. package/src/sdk/session.ts +2 -0
  156. package/src/sdk/startup-capability.ts +9 -1
  157. package/src/session/agent-session.ts +52 -5
  158. package/src/session/blob-store.ts +7 -2
  159. package/src/session/internal/managed-session-scope.ts +854 -84
  160. package/src/session/internal/managed-session-storage.ts +251 -199
  161. package/src/session/internal/native-publish-outcome.ts +266 -0
  162. package/src/session/session-manager.ts +310 -327
  163. package/src/session/session-storage.ts +128 -11
  164. package/src/task/gjc-command.ts +2 -2
  165. package/src/task/index.ts +121 -15
  166. package/src/task/receipt.ts +3 -0
  167. package/src/task/types.ts +34 -3
  168. package/src/tools/ask.ts +198 -88
  169. package/src/tools/ast-edit.ts +2 -2
  170. package/src/tools/bash-pty-selection.ts +2 -2
  171. package/src/tools/browser/screenshot-format.ts +6 -1
  172. package/src/tools/cron.ts +1 -1
  173. package/src/tools/image-gen.ts +178 -6
  174. package/src/tools/index.ts +4 -0
  175. package/src/tools/render-utils.ts +7 -1
  176. package/src/utils/edit-mode.ts +2 -2
  177. package/src/utils/prompt-suggestion.ts +265 -0
  178. package/src/utils/sixel.ts +3 -3
  179. package/src/web/insane/url-guard.ts +3 -1
  180. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -259,6 +259,19 @@ export interface VerifiedSessionDeleteTarget {
259
259
  detachedArtifactsPath?: string;
260
260
  /** Identity-bound quarantine path retained when transcript unlink deferred cleanup. */
261
261
  detachedTranscriptPath?: string;
262
+ /** Native-retained publisher successor observed during transcript cleanup. */
263
+ retainedTranscriptSuccessorPath?: string;
264
+ /** Native-retained exchange placeholder observed during transcript cleanup. */
265
+ retainedTranscriptPlaceholderPath?: string;
266
+ /** Native-retained transcript entry whose identity could not be verified. */
267
+ retainedTranscriptUnknownPath?: string;
268
+ /** Native-retained publisher successor observed during cleanup. */
269
+ retainedArtifactsSuccessorPath?: string;
270
+ /** Native-retained exchange placeholder observed during cleanup. */
271
+ retainedArtifactsPlaceholderPath?: string;
272
+ /** Native-retained entry whose identity could not be verified. */
273
+ retainedArtifactsUnknownPath?: string;
274
+
262
275
  /** Caller-published, no-replace quarantine pathname for the next artifact detach. */
263
276
  plannedArtifactsPath?: string;
264
277
  /** Caller-published, no-replace quarantine pathname for the next transcript detach. */
@@ -288,6 +301,9 @@ export type VerifiedSessionDeleteResult =
288
301
  artifactsTree: NativeDirectoryTreeSnapshot;
289
302
  /** Transcript identity (unchanged) for retry binding. */
290
303
  transcriptIdentity: SessionStorageFileIdentity;
304
+ retainedSuccessorPath?: string;
305
+ retainedPlaceholderPath?: string;
306
+ retainedUnknownPath?: string;
291
307
  }
292
308
  | {
293
309
  kind: "cleanup_pending";
@@ -297,6 +313,9 @@ export type VerifiedSessionDeleteResult =
297
313
  transcriptIdentity: SessionStorageFileIdentity;
298
314
  /** Optional identity-bound transcript quarantine path for restart cleanup. */
299
315
  detachedTranscriptPath?: string;
316
+ retainedSuccessorPath?: string;
317
+ retainedPlaceholderPath?: string;
318
+ retainedUnknownPath?: string;
300
319
  };
301
320
 
302
321
  /** Default OS-close dispatcher: a direct `fs.closeSync`. */
@@ -306,7 +325,22 @@ const defaultCloseAdapter: SessionStorageWriterCloseAdapter = {
306
325
  },
307
326
  };
308
327
 
309
- type NativeExactUnlinkResult = { ok: true; detachedPath?: string } | { ok: false; code: string; detachedPath?: string };
328
+ type NativeExactUnlinkResult =
329
+ | {
330
+ ok: true;
331
+ detachedPath?: string;
332
+ retainedSuccessorPath?: string;
333
+ retainedPlaceholderPath?: string;
334
+ retainedUnknownPath?: string;
335
+ }
336
+ | {
337
+ ok: false;
338
+ code: string;
339
+ detachedPath?: string;
340
+ retainedSuccessorPath?: string;
341
+ retainedPlaceholderPath?: string;
342
+ retainedUnknownPath?: string;
343
+ };
310
344
  type NativeExactUnlink = (
311
345
  path: string,
312
346
  identity: {
@@ -766,7 +800,13 @@ export class FileSessionStorage implements SessionStorage {
766
800
  expectedArtifactsIdentity,
767
801
  expectedArtifactsTree,
768
802
  detachedArtifactsPath,
803
+ retainedArtifactsSuccessorPath,
804
+ retainedArtifactsPlaceholderPath,
805
+ retainedArtifactsUnknownPath,
769
806
  detachedTranscriptPath,
807
+ retainedTranscriptSuccessorPath,
808
+ retainedTranscriptPlaceholderPath,
809
+ retainedTranscriptUnknownPath,
770
810
  plannedArtifactsPath,
771
811
  plannedTranscriptPath,
772
812
  artifactsRemoved,
@@ -847,7 +887,8 @@ export class FileSessionStorage implements SessionStorage {
847
887
  if (
848
888
  !expectedArtifactsIdentity ||
849
889
  path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
850
- !path.basename(detachedArtifactsPath).startsWith(".gjc-delete-")
890
+ (!path.basename(detachedArtifactsPath).startsWith(".gjc-delete-") &&
891
+ !detachedArtifactsPath.endsWith(".removing"))
851
892
  ) {
852
893
  throw new SessionDeleteVerificationError("artifacts", "Detached artifact cleanup evidence is invalid");
853
894
  }
@@ -882,17 +923,20 @@ export class FileSessionStorage implements SessionStorage {
882
923
  artifactsIdentity: expectedArtifactsIdentity,
883
924
  detachedArtifactsPath: retainedRoot,
884
925
  artifactsTree: expectedArtifactsTree,
926
+ ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
927
+ ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
928
+ : {}),
929
+ ...((removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath)
930
+ ? { retainedPlaceholderPath: removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath }
931
+ : {}),
932
+ ...((removal.retainedUnknownPath ?? retainedArtifactsUnknownPath)
933
+ ? { retainedUnknownPath: removal.retainedUnknownPath ?? retainedArtifactsUnknownPath }
934
+ : {}),
885
935
  transcriptIdentity,
886
936
  };
887
937
  }
888
938
  }
889
939
 
890
- if (artifactsRemoved && (detachedArtifactsPath || expectedArtifactsIdentity)) {
891
- throw new SessionDeleteVerificationError(
892
- "artifacts",
893
- "Artifact phase receipt conflicts with pending artifact cleanup",
894
- );
895
- }
896
940
  const artifactsDir = transcriptPath.slice(0, -6);
897
941
  const artifactsIdentity = this.#optionalDirectoryIdentity(artifactsDir);
898
942
  if (artifactsRemoved && artifactsIdentity) {
@@ -947,12 +991,48 @@ export class FileSessionStorage implements SessionStorage {
947
991
  directory: true,
948
992
  quarantineName: path.basename(plannedArtifactsPath),
949
993
  });
950
- if (!detach.ok || !detach.detachedPath) {
994
+ if (!detach.detachedPath) {
951
995
  throw new SessionDeleteVerificationError(
952
996
  "artifacts",
953
997
  `Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
954
998
  );
955
999
  }
1000
+ if (!detach.ok && process.platform !== "win32") {
1001
+ let descriptor: number | undefined;
1002
+ try {
1003
+ descriptor = fs.openSync(
1004
+ path.dirname(transcriptPath),
1005
+ fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
1006
+ );
1007
+ const durableParent = fs.fstatSync(descriptor, { bigint: true });
1008
+ if (
1009
+ !durableParent.isDirectory() ||
1010
+ durableParent.dev !== parentIdentity.dev ||
1011
+ durableParent.ino !== parentIdentity.ino
1012
+ )
1013
+ throw new Error("parent_changed");
1014
+ fs.fsyncSync(descriptor);
1015
+ } catch (error) {
1016
+ throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
1017
+ } finally {
1018
+ if (descriptor !== undefined) fs.closeSync(descriptor);
1019
+ }
1020
+ }
1021
+ if (!detach.ok) {
1022
+ return {
1023
+ kind: "cleanup_pending",
1024
+ phase: "artifacts",
1025
+ error: new SessionDeleteVerificationError("artifacts", `Exact artifact detach retained: ${detach.code}`),
1026
+ artifactsIdentity,
1027
+ detachedArtifactsPath: detach.detachedPath,
1028
+ artifactsTree,
1029
+ ...(detach.retainedSuccessorPath ? { retainedSuccessorPath: detach.retainedSuccessorPath } : {}),
1030
+ ...(detach.retainedPlaceholderPath ? { retainedPlaceholderPath: detach.retainedPlaceholderPath } : {}),
1031
+ ...(detach.retainedUnknownPath ? { retainedUnknownPath: detach.retainedUnknownPath } : {}),
1032
+
1033
+ transcriptIdentity,
1034
+ };
1035
+ }
956
1036
  const removal = removeDirectoryTreeExact(detach.detachedPath, artifactsTree);
957
1037
  if (!removal.ok) {
958
1038
  const retainedRoot = removal.detachedPath ?? detach.detachedPath;
@@ -971,6 +1051,15 @@ export class FileSessionStorage implements SessionStorage {
971
1051
  artifactsIdentity,
972
1052
  detachedArtifactsPath: retainedRoot,
973
1053
  artifactsTree,
1054
+ ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
1055
+ ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
1056
+ : {}),
1057
+ ...((removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath)
1058
+ ? { retainedPlaceholderPath: removal.retainedPlaceholderPath ?? retainedArtifactsPlaceholderPath }
1059
+ : {}),
1060
+ ...((removal.retainedUnknownPath ?? retainedArtifactsUnknownPath)
1061
+ ? { retainedUnknownPath: removal.retainedUnknownPath ?? retainedArtifactsUnknownPath }
1062
+ : {}),
974
1063
  transcriptIdentity,
975
1064
  };
976
1065
  }
@@ -1010,13 +1099,27 @@ export class FileSessionStorage implements SessionStorage {
1010
1099
  });
1011
1100
  if (!deletion.ok) {
1012
1101
  const error = exactUnlinkFailure(deletion);
1013
- if (error.kind === "identity" || error.kind === "symlink") throw error;
1102
+ const retainedAuthority =
1103
+ deletion.detachedPath ||
1104
+ deletion.retainedSuccessorPath ||
1105
+ deletion.retainedPlaceholderPath ||
1106
+ deletion.retainedUnknownPath;
1107
+ if ((error.kind === "identity" || error.kind === "symlink") && !retainedAuthority) throw error;
1014
1108
  return {
1015
1109
  kind: "cleanup_pending",
1016
1110
  phase: "transcript",
1017
1111
  error,
1018
1112
  transcriptIdentity,
1019
1113
  detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
1114
+ ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
1115
+ ? { retainedSuccessorPath: deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath }
1116
+ : {}),
1117
+ ...((deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath)
1118
+ ? { retainedPlaceholderPath: deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath }
1119
+ : {}),
1120
+ ...((deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath)
1121
+ ? { retainedUnknownPath: deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath }
1122
+ : {}),
1020
1123
  };
1021
1124
  }
1022
1125
  return { kind: "deleted" };
@@ -1058,13 +1161,27 @@ export class FileSessionStorage implements SessionStorage {
1058
1161
  });
1059
1162
  if (!deletion.ok) {
1060
1163
  const error = exactUnlinkFailure(deletion);
1061
- if (error.kind === "identity" || error.kind === "symlink") throw error;
1164
+ const retainedAuthority =
1165
+ deletion.detachedPath ||
1166
+ deletion.retainedSuccessorPath ||
1167
+ deletion.retainedPlaceholderPath ||
1168
+ deletion.retainedUnknownPath;
1169
+ if ((error.kind === "identity" || error.kind === "symlink") && !retainedAuthority) throw error;
1062
1170
  return {
1063
1171
  kind: "cleanup_pending",
1064
1172
  phase: "transcript",
1065
1173
  error,
1066
1174
  transcriptIdentity,
1067
1175
  detachedTranscriptPath: deletion.detachedPath,
1176
+ ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
1177
+ ? { retainedSuccessorPath: deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath }
1178
+ : {}),
1179
+ ...((deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath)
1180
+ ? { retainedPlaceholderPath: deletion.retainedPlaceholderPath ?? retainedTranscriptPlaceholderPath }
1181
+ : {}),
1182
+ ...((deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath)
1183
+ ? { retainedUnknownPath: deletion.retainedUnknownPath ?? retainedTranscriptUnknownPath }
1184
+ : {}),
1068
1185
  };
1069
1186
  }
1070
1187
  return { kind: "deleted" };
@@ -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
  }
package/src/task/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Usage } from "@gajae-code/ai";
3
- import { $env } from "@gajae-code/utils";
3
+ import { $pickenv } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
5
  import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
6
6
  import type { TaskResultReceipt } from "./receipt";
@@ -27,10 +27,10 @@ const parseNumber = (value: string | undefined, defaultValue: number): number =>
27
27
  };
28
28
 
29
29
  /** Maximum output bytes per agent */
30
- export const MAX_OUTPUT_BYTES = parseNumber($env.PI_TASK_MAX_OUTPUT_BYTES, 500_000);
30
+ export const MAX_OUTPUT_BYTES = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"), 500_000);
31
31
 
32
32
  /** Maximum output lines per agent */
33
- export const MAX_OUTPUT_LINES = parseNumber($env.PI_TASK_MAX_OUTPUT_LINES, 5000);
33
+ export const MAX_OUTPUT_LINES = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"), 5000);
34
34
 
35
35
  /** EventBus channel for raw subagent events */
36
36
  export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
@@ -74,6 +74,19 @@ const spawnPlanSchema = z
74
74
  })
75
75
  .describe("justification required before spawning more than four tasks");
76
76
 
77
+ const repositoryBindingSchema = z
78
+ .object({
79
+ schema: z.literal("gjc.repository_binding.v1"),
80
+ worktreeRoot: z.string().min(1).describe("canonical git worktree root"),
81
+ commonDir: z.string().min(1).nullable().describe("git common dir, or null outside a git checkout"),
82
+ relativeSubdir: z.string().min(1).optional().describe("optional repo-relative subdirectory; not an absolute cwd"),
83
+ displayPath: z.string().min(1).optional().describe("human-facing path; never used for authority"),
84
+ head: z.string().min(1).optional(),
85
+ branch: z.string().min(1).optional(),
86
+ })
87
+ .strict()
88
+ .describe("authoritative repository identity for multi-repo fail-closed spawn");
89
+
77
90
  const createTaskItemSchema = (_contextEnabled: boolean) =>
78
91
  z.object({
79
92
  id: z.string().max(48).refine(isValidTaskId, TASK_ID_DESCRIPTION).describe("filesystem-safe task identifier"),
@@ -85,6 +98,11 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
85
98
  .describe(
86
99
  "fork-context mode: none/omitted copies no parent context; receipt copies a minimal receipt-sized snapshot; last-turn copies only the latest exchange; bounded copies the bounded default snapshot; full copies a larger sanitized snapshot up to the configured/model token cap",
87
100
  ),
101
+ repositoryBinding: repositoryBindingSchema
102
+ .optional()
103
+ .describe(
104
+ "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
105
+ ),
88
106
  });
89
107
 
90
108
  /** Single task item for parallel execution (default shape with context enabled). */
@@ -353,6 +371,19 @@ export interface SingleResult {
353
371
  * never changes the actual mode selection).
354
372
  */
355
373
  forkContextAdvisory?: { recommendedMode: ForkContextMode; reasons: string[] };
374
+ /**
375
+ * Resolved repository identity used for this task after pre-discovery stamping
376
+ * and fail-closed validation (#2901).
377
+ */
378
+ repositoryBinding?: {
379
+ schema: "gjc.repository_binding.v1";
380
+ worktreeRoot: string;
381
+ commonDir: string | null;
382
+ relativeSubdir?: string;
383
+ displayPath?: string;
384
+ head?: string;
385
+ branch?: string;
386
+ };
356
387
  }
357
388
 
358
389
  /** True only for complete, factual five-bucket cost accounting. */