@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0

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 (193) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +4 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +11 -1
  8. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  9. package/dist/cc-orchestrator/codex-session-history.js +65 -5
  10. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  11. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  13. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  14. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  15. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  16. package/dist/cc-orchestrator/external-agent-driver.js +265 -50
  17. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  18. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  19. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  20. package/dist/cc-orchestrator/session-history.js +98 -15
  21. package/dist/cli/agent-server-stdio.js +9 -2
  22. package/dist/context/compaction.d.ts +8 -1
  23. package/dist/context/compaction.js +49 -4
  24. package/dist/context/manager.d.ts +16 -2
  25. package/dist/context/manager.js +103 -19
  26. package/dist/credentials/access.d.ts +57 -0
  27. package/dist/credentials/access.js +185 -0
  28. package/dist/credentials/index.d.ts +3 -1
  29. package/dist/credentials/index.js +2 -0
  30. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  31. package/dist/credentials/inject-credential-tool.js +30 -11
  32. package/dist/credentials/oauth.d.ts +20 -0
  33. package/dist/credentials/oauth.js +114 -0
  34. package/dist/credentials/store.d.ts +1 -0
  35. package/dist/credentials/store.js +3 -1
  36. package/dist/credentials/types.d.ts +49 -3
  37. package/dist/credentials/use-credential-tool.d.ts +9 -1
  38. package/dist/credentials/use-credential-tool.js +58 -45
  39. package/dist/engine/engine.d.ts +23 -3
  40. package/dist/engine/engine.js +380 -255
  41. package/dist/engine/goal.d.ts +17 -0
  42. package/dist/engine/goal.js +16 -6
  43. package/dist/engine/image-policy.d.ts +6 -0
  44. package/dist/engine/image-policy.js +17 -6
  45. package/dist/engine/input-attachments.d.ts +13 -0
  46. package/dist/engine/input-attachments.js +398 -0
  47. package/dist/engine/model-facade.d.ts +5 -2
  48. package/dist/engine/model-facade.js +4 -4
  49. package/dist/engine/parse-task.d.ts +10 -0
  50. package/dist/engine/parse-task.js +5 -0
  51. package/dist/engine/run-image-input.d.ts +22 -0
  52. package/dist/engine/run-image-input.js +195 -0
  53. package/dist/engine/steer-queue.d.ts +3 -1
  54. package/dist/engine/steer-queue.js +10 -2
  55. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  56. package/dist/engine/streaming-tool-queue.js +11 -7
  57. package/dist/engine/turn-loop.d.ts +37 -2
  58. package/dist/engine/turn-loop.js +228 -43
  59. package/dist/engine/types.d.ts +8 -0
  60. package/dist/git/worktree/crud.d.ts +69 -0
  61. package/dist/git/worktree/crud.js +206 -0
  62. package/dist/git/worktree/diff.d.ts +14 -0
  63. package/dist/git/worktree/diff.js +82 -0
  64. package/dist/git/worktree/git-exec.d.ts +7 -0
  65. package/dist/git/worktree/git-exec.js +51 -0
  66. package/dist/git/worktree/index.d.ts +5 -0
  67. package/dist/git/worktree/index.js +5 -0
  68. package/dist/git/worktree/query.d.ts +42 -0
  69. package/dist/git/worktree/query.js +121 -0
  70. package/dist/git/worktree/slug.d.ts +11 -0
  71. package/dist/git/worktree/slug.js +58 -0
  72. package/dist/git/worktree.d.ts +1 -84
  73. package/dist/git/worktree.js +5 -230
  74. package/dist/hooks/goal-stop-hook.d.ts +33 -1
  75. package/dist/hooks/goal-stop-hook.js +202 -34
  76. package/dist/index.d.ts +31 -28
  77. package/dist/index.js +27 -25
  78. package/dist/logging/logger.js +6 -6
  79. package/dist/logging/sanitize-messages.d.ts +10 -2
  80. package/dist/logging/sanitize-messages.js +21 -6
  81. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  82. package/dist/plugins/installer/checkUpdate.js +4 -2
  83. package/dist/plugins/installer/install.js +2 -0
  84. package/dist/plugins/installer/installFromSource.js +9 -1
  85. package/dist/plugins/installer/parseSource.d.ts +4 -1
  86. package/dist/plugins/installer/parseSource.js +28 -10
  87. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  88. package/dist/plugins/installer/sourcePath.js +50 -0
  89. package/dist/plugins/installer/update.d.ts +4 -1
  90. package/dist/plugins/installer/update.js +5 -3
  91. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  92. package/dist/plugins/parseMarketplaceInput.js +4 -3
  93. package/dist/plugins/pluginInstaller.js +24 -22
  94. package/dist/preset/index.d.ts +1 -0
  95. package/dist/preset/index.js +30 -13
  96. package/dist/prompt/sections/base.md +1 -1
  97. package/dist/protocol/chat-session-manager.d.ts +2 -0
  98. package/dist/protocol/chat-session-manager.js +38 -2
  99. package/dist/protocol/chat-session.d.ts +3 -0
  100. package/dist/protocol/chat-session.js +1 -0
  101. package/dist/protocol/client.d.ts +9 -4
  102. package/dist/protocol/client.js +18 -1
  103. package/dist/protocol/server.d.ts +30 -0
  104. package/dist/protocol/server.js +247 -47
  105. package/dist/protocol/types.d.ts +49 -0
  106. package/dist/protocol/types.js +6 -0
  107. package/dist/run/FileRunStore.js +10 -1
  108. package/dist/run/Heartbeat.js +12 -0
  109. package/dist/run/RunApprovalBackend.d.ts +3 -0
  110. package/dist/run/RunApprovalBackend.js +41 -6
  111. package/dist/run/RunLock.js +2 -0
  112. package/dist/run/RunManager.d.ts +2 -0
  113. package/dist/run/RunManager.js +64 -24
  114. package/dist/run/ids.d.ts +2 -0
  115. package/dist/run/ids.js +23 -0
  116. package/dist/runtime/background-shell.d.ts +1 -0
  117. package/dist/runtime/background-shell.js +23 -13
  118. package/dist/runtime/spawn-common.js +10 -0
  119. package/dist/services/auto-dream.d.ts +15 -4
  120. package/dist/services/auto-dream.js +20 -20
  121. package/dist/services/dream-consolidation.d.ts +2 -3
  122. package/dist/services/dream-consolidation.js +65 -15
  123. package/dist/services/extract-memories.d.ts +14 -5
  124. package/dist/services/extract-memories.js +20 -3
  125. package/dist/services/global-dream-promotion.d.ts +23 -0
  126. package/dist/services/global-dream-promotion.js +112 -0
  127. package/dist/services/memory-orchestrator.js +347 -34
  128. package/dist/session/memory.d.ts +61 -18
  129. package/dist/session/memory.js +342 -79
  130. package/dist/session/session-manager.d.ts +35 -1
  131. package/dist/session/session-manager.js +224 -4
  132. package/dist/session/transcript.d.ts +1 -1
  133. package/dist/session/transcript.js +17 -5
  134. package/dist/settings/manager.d.ts +1 -0
  135. package/dist/settings/manager.js +87 -37
  136. package/dist/settings/schema-export.d.ts +2 -3
  137. package/dist/settings/schema-export.js +2 -3
  138. package/dist/settings/schema.d.ts +21 -0
  139. package/dist/settings/schema.js +12 -0
  140. package/dist/skills/scanner.d.ts +3 -2
  141. package/dist/skills/scanner.js +12 -9
  142. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  143. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  144. package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
  145. package/dist/tool-system/builtin/background-jobs.js +116 -7
  146. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  147. package/dist/tool-system/builtin/background-work.js +51 -5
  148. package/dist/tool-system/builtin/config.d.ts +2 -1
  149. package/dist/tool-system/builtin/config.js +16 -11
  150. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  151. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  152. package/dist/tool-system/builtin/cron.d.ts +1 -2
  153. package/dist/tool-system/builtin/cron.js +9 -7
  154. package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
  155. package/dist/tool-system/builtin/drive-claude-code.js +589 -48
  156. package/dist/tool-system/builtin/edit.js +5 -2
  157. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  158. package/dist/tool-system/builtin/generate-video.js +13 -4
  159. package/dist/tool-system/builtin/index.d.ts +8 -3
  160. package/dist/tool-system/builtin/index.js +50 -22
  161. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  162. package/dist/tool-system/builtin/lsp.js +6 -3
  163. package/dist/tool-system/builtin/memory.js +40 -8
  164. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  165. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  166. package/dist/tool-system/builtin/powershell.js +11 -7
  167. package/dist/tool-system/builtin/read.js +118 -6
  168. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  169. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  170. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  171. package/dist/tool-system/builtin/sleep.js +1 -22
  172. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  173. package/dist/tool-system/builtin/view-image.js +109 -19
  174. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  175. package/dist/tool-system/builtin/worktree.js +297 -74
  176. package/dist/tool-system/builtin/write.js +5 -3
  177. package/dist/tool-system/context.d.ts +41 -1
  178. package/dist/tool-system/executor.d.ts +1 -5
  179. package/dist/tool-system/executor.js +94 -115
  180. package/dist/tool-system/mcp-manager.d.ts +31 -6
  181. package/dist/tool-system/mcp-manager.js +193 -78
  182. package/dist/tool-system/path-policy.d.ts +2 -0
  183. package/dist/tool-system/path-policy.js +41 -12
  184. package/dist/tool-system/permission.d.ts +28 -7
  185. package/dist/tool-system/permission.js +130 -49
  186. package/dist/tool-system/registry.js +11 -4
  187. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  188. package/dist/tool-system/tool-result-redaction.js +48 -0
  189. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  190. package/dist/tool-system/workspace-bridge.js +1 -0
  191. package/dist/types.d.ts +78 -11
  192. package/dist/utils/toolDisplay.js +1 -1
  193. package/package.json +4 -3
@@ -1,122 +1,345 @@
1
1
  /**
2
- * EnterWorktree / ExitWorktree tools — create and manage isolated git worktrees.
2
+ * EnterWorktree / ExitWorktree tools — switch a session between git workspaces.
3
3
  */
4
- import { createWorktree, removeWorktree, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, } from "../../git/worktree.js";
5
- // Global worktree state
6
- let _activeWorktree;
7
- export function getActiveWorktree() {
8
- return _activeWorktree;
9
- }
10
- export const enterWorktreeToolDef = {
11
- name: "EnterWorktree",
12
- description: "Create an isolated git worktree for safe code modifications. " +
13
- "The worktree is a separate copy of the repository on a new branch. " +
14
- "Changes made in the worktree do not affect the main working directory. " +
15
- "Use this when you need to make experimental changes or work in isolation.",
4
+ import { existsSync } from "node:fs";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, } from "../../git/worktree.js";
7
+ export const switchSessionWorkspaceToolDef = {
8
+ name: "SwitchSessionWorkspace",
9
+ description: "Switch this current conversation session into or out of a workspace through the host UI path. " +
10
+ "Use this when you need isolated or parallel work in a git worktree, when you need to move " +
11
+ "this conversation to an existing worktree path/branch, or when you are done and should return " +
12
+ "this current conversation to main. This is the correct way to move THIS conversation's session " +
13
+ "into or out of a worktree on desktop.",
16
14
  inputSchema: {
17
15
  type: "object",
18
16
  properties: {
19
- slug: {
17
+ target: {
20
18
  type: "string",
21
- description: "A short identifier for the worktree (alphanumeric, dots, dashes only, max 64 chars). " +
22
- "Example: 'fix-auth-bug', 'refactor-api'",
19
+ description: "Workspace target: 'main', a new slug, an existing worktree path, or an existing branch name.",
23
20
  },
24
21
  },
25
- required: ["slug"],
22
+ required: ["target"],
26
23
  },
27
24
  };
28
- export async function enterWorktreeTool(args, ctx) {
29
- const slug = args.slug;
30
- if (!slug)
31
- return "Error: slug is required";
32
- if (_activeWorktree) {
33
- return `Error: Already in a worktree at ${_activeWorktree.worktreePath}. Exit it first with ExitWorktree.`;
25
+ export async function switchSessionWorkspaceTool(args, ctx) {
26
+ const target = stringArg(args.target);
27
+ if (!target)
28
+ return "Error: target is required";
29
+ const bridge = ctx?.workspace;
30
+ if (!bridge) {
31
+ return ("SwitchSessionWorkspace is not available in this host. " +
32
+ "Use the host's supported workspace controls instead.");
34
33
  }
35
34
  try {
36
- validateWorktreeSlug(slug);
35
+ const workspace = await bridge.switch(target);
36
+ ctx?.setSessionWorkspace?.(workspace);
37
+ const details = workspace.kind === "worktree" && workspace.worktree
38
+ ? `\n Branch: ${workspace.worktree.branch}`
39
+ : "";
40
+ return (`Switched session workspace:\n` +
41
+ ` Path: ${workspace.root}` +
42
+ details +
43
+ `\n\n` +
44
+ nextTurnNotice(workspace.root, ctx?.cwd ?? workspace.root));
37
45
  }
38
46
  catch (err) {
39
- return `Error: ${err.message}`;
47
+ return `Error switching workspace: ${err.message}`;
40
48
  }
49
+ }
50
+ export const enterWorktreeToolDef = {
51
+ name: "EnterWorktree",
52
+ description: "Switch the current session workspace. Target can be a new worktree slug, " +
53
+ "an existing worktree path or branch, or 'main' to return to the main repository. " +
54
+ "Switching leaves the previous worktree on disk.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ target: {
59
+ type: "string",
60
+ description: "Workspace target: 'main', a new slug (alphanumeric, dots, dashes, underscores), " +
61
+ "or an existing worktree path/branch to switch to.",
62
+ },
63
+ slug: {
64
+ type: "string",
65
+ description: "Deprecated alias for target. Prefer target.",
66
+ },
67
+ },
68
+ required: [],
69
+ },
70
+ };
71
+ export async function enterWorktreeTool(args, ctx) {
72
+ const target = stringArg(args.target) ?? stringArg(args.slug);
73
+ if (!target)
74
+ return "Error: target is required";
75
+ const resolved = sessionServices(ctx);
76
+ if (!resolved.ok)
77
+ return resolved.error;
78
+ const { sessionId, sessionManager } = resolved;
79
+ const mainRoot = sessionManager.readCwd(sessionId) ?? ctx?.cwd ?? process.cwd();
80
+ const fromWorkspace = sessionManager.getSessionWorkspace(sessionId) ?? {
81
+ root: mainRoot,
82
+ kind: "main",
83
+ };
84
+ const fromRoot = fromWorkspace.root;
85
+ const currentTurnRoot = ctx?.cwd ?? fromRoot;
86
+ const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
41
87
  try {
42
- // Use a placeholder session ID (the agent should set this properly)
43
- const sessionId = args.__sessionId ?? `wt-${Date.now()}`;
44
- const cwd = args.__cwd ?? ctx?.cwd ?? process.cwd();
45
- _activeWorktree = createWorktree(cwd, slug, sessionId);
46
- // Run the project's localEnvironment.setupScripts once in the new
47
- // worktree root (Beta decision 2026-06-08: setup belongs to the worktree
48
- // lifecycle, not the conversation). Failure warns-but-continues — a broken
49
- // setup script must not strand the agent outside a worktree it just made.
50
- let setupNote = "";
51
- const setupScripts = ctx?.engine?.readWorktreeSetupScripts(cwd);
52
- const script = selectPlatformScript(setupScripts);
53
- if (script) {
54
- const setup = await runWorktreeSetup(_activeWorktree.worktreePath, script, {
55
- sandbox: ctx?.sandbox,
56
- shellEnv: ctx?.shellEnv,
57
- signal: ctx?.signal,
58
- });
59
- if (setup.ok) {
60
- setupNote = `\n\nRan setup script (exit 0).${setup.output ? `\n${truncate(setup.output)}` : ""}`;
61
- }
62
- else {
63
- setupNote =
64
- `\n\n⚠️ Setup script failed (exit ${setup.exitCode ?? "?"}) — continuing anyway. ` +
65
- `You may need to run setup manually.${setup.output ? `\n${truncate(setup.output)}` : ""}`;
66
- }
88
+ if (target === "main") {
89
+ const workspace = { root: mainRoot, kind: "main" };
90
+ persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
91
+ sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
92
+ return (`Switched to main workspace:\n` +
93
+ ` Path: ${mainRoot}\n` +
94
+ ` From: ${fromRoot}\n\n` +
95
+ nextTurnNotice(mainRoot, currentTurnRoot));
67
96
  }
68
- return (`Worktree created:\n` +
69
- ` Path: ${_activeWorktree.worktreePath}\n` +
70
- ` Branch: ${_activeWorktree.worktreeBranch}\n` +
71
- ` From: ${_activeWorktree.originalBranch ?? "HEAD"}\n\n` +
72
- `You are now working in an isolated copy. Changes here won't affect the main repo.` +
97
+ const selected = await resolveWorktreeTarget({
98
+ target,
99
+ cwd: ctx?.cwd ?? mainRoot,
100
+ mainRoot,
101
+ sessionId,
102
+ currentWorkspace: fromWorkspace,
103
+ branchPrefix,
104
+ });
105
+ const workspace = toSessionWorkspace(selected, fromWorkspace);
106
+ persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
107
+ sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
108
+ const setupNote = selected.created
109
+ ? await runSetupIfConfigured(selected.session.worktreePath, mainRoot, ctx)
110
+ : "";
111
+ const verb = selected.created ? "Worktree created and switched" : "Switched to worktree";
112
+ return (`${verb}:\n` +
113
+ ` Path: ${workspace.worktree.path}\n` +
114
+ ` Branch: ${workspace.worktree.branch}\n` +
115
+ ` From: ${selected.from}\n` +
116
+ ` Previous: ${fromRoot}\n\n` +
117
+ nextTurnNotice(workspace.root, currentTurnRoot) +
73
118
  setupNote);
74
119
  }
75
120
  catch (err) {
76
- return `Error creating worktree: ${err.message}`;
121
+ return `Error switching worktree: ${err.message}`;
77
122
  }
78
123
  }
79
124
  /** Keep setup output from bloating the tool result — head+tail-ish trim. */
80
125
  function truncate(s, max = 2000) {
81
126
  return s.length <= max ? s : `${s.slice(0, max)}\n…(${s.length - max} more chars)`;
82
127
  }
128
+ function persistSessionWorkspace(sessionManager, sessionId, workspace, ctx) {
129
+ sessionManager.setSessionWorkspace(sessionId, workspace);
130
+ ctx?.setSessionWorkspace?.(workspace);
131
+ }
132
+ function nextTurnNotice(nextRoot, currentTurnRoot) {
133
+ return (`Workspace switched to ${nextRoot}. This takes effect on the next turn - ` +
134
+ `file/shell/sandbox tools in the CURRENT turn still target ${currentTurnRoot}.`);
135
+ }
83
136
  export const exitWorktreeToolDef = {
84
137
  name: "ExitWorktree",
85
- description: "Exit the current worktree and return to the main working directory. " +
86
- "Choose whether to keep or discard the changes made in the worktree.",
138
+ description: "Switch the current session back to the main working directory. " +
139
+ "Choose whether to keep the worktree, detach it while preserving the branch, " +
140
+ "or discard it entirely.",
87
141
  inputSchema: {
88
142
  type: "object",
89
143
  properties: {
90
144
  action: {
91
145
  type: "string",
92
- enum: ["keep", "discard"],
93
- description: "'keep' preserves the worktree branch for later merging. " +
94
- "'discard' removes the worktree and its branch entirely.",
146
+ enum: ["keep", "detach", "discard"],
147
+ description: "'keep' preserves the worktree directory and branch for later. " +
148
+ "'detach' removes the directory but keeps the branch. " +
149
+ "'discard' removes the directory and its branch entirely. " +
150
+ "If omitted, a clean worktree is detached automatically; a dirty worktree requires keep/discard.",
95
151
  },
96
152
  },
97
- required: ["action"],
153
+ required: [],
98
154
  },
99
155
  };
100
- export async function exitWorktreeTool(args) {
101
- if (!_activeWorktree) {
156
+ export async function exitWorktreeTool(args, ctx) {
157
+ const resolved = sessionServices(ctx);
158
+ if (!resolved.ok)
159
+ return resolved.error;
160
+ const { sessionId, sessionManager } = resolved;
161
+ const workspace = sessionManager.getSessionWorkspace(sessionId);
162
+ if (!workspace || workspace.kind !== "worktree" || !workspace.worktree) {
102
163
  return "Not currently in a worktree.";
103
164
  }
104
- const action = args.action;
105
- const session = _activeWorktree;
106
- _activeWorktree = undefined;
165
+ const requested = args.action;
166
+ if (requested !== undefined &&
167
+ requested !== "keep" &&
168
+ requested !== "detach" &&
169
+ requested !== "discard") {
170
+ return `Error: unknown action "${requested}" (expected keep, detach, or discard).`;
171
+ }
172
+ const hasPendingChanges = existsSync(workspace.worktree.path) &&
173
+ (await worktreeHasUncommittedOrAheadChanges(workspace.worktree.path, workspace.worktree.baseRef));
174
+ const action = requested ?? (hasPendingChanges ? undefined : "detach");
175
+ if (!action) {
176
+ return (`Error: worktree has uncommitted changes or new commits. Choose action "keep" to preserve ` +
177
+ `the directory or "discard" to delete the worktree and branch.`);
178
+ }
179
+ if (action === "detach" && hasPendingChanges) {
180
+ return (`Error: detach would drop uncommitted changes or new commits. Choose action "keep" to preserve ` +
181
+ `the directory or "discard" to delete the worktree and branch.`);
182
+ }
183
+ const mainRoot = sessionManager.readCwd(sessionId) ?? workspace.root;
184
+ const currentTurnRoot = ctx?.cwd ?? workspace.root;
185
+ const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
107
186
  try {
187
+ let removal;
188
+ if (action === "discard" || action === "detach") {
189
+ const otherOwners = await otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, workspace.worktree.path);
190
+ if (otherOwners.length > 0) {
191
+ const mainWorkspace = { root: mainRoot, kind: "main" };
192
+ persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
193
+ sessionManager.recordWorkspaceHandoff(sessionId, workspace, mainWorkspace);
194
+ return sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot);
195
+ }
196
+ }
108
197
  if (action === "discard") {
109
- removeWorktree(session.worktreePath, true);
110
- return `Worktree removed and branch ${session.worktreeBranch} deleted. Back to ${session.originalCwd}.`;
198
+ removal = removeWorktree(workspace.worktree.path, true, { prefix: branchPrefix });
199
+ }
200
+ else if (action === "detach") {
201
+ removal = removeWorktree(workspace.worktree.path, false);
202
+ }
203
+ const mainWorkspace = { root: mainRoot, kind: "main" };
204
+ persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
205
+ sessionManager.recordWorkspaceHandoff(sessionId, workspace, mainWorkspace);
206
+ if (action === "keep") {
207
+ return (`Worktree preserved at ${workspace.worktree.path}. ` +
208
+ `Branch ${workspace.worktree.branch} preserved.\n` +
209
+ `Back to ${mainRoot} starting next turn.\n` +
210
+ nextTurnNotice(mainRoot, currentTurnRoot));
111
211
  }
112
- else {
113
- removeWorktree(session.worktreePath, false);
114
- return (`Worktree removed. Branch ${session.worktreeBranch} preserved.\n` +
115
- `To merge: git merge ${session.worktreeBranch}\n` +
116
- `Back to ${session.originalCwd}.`);
212
+ if (action === "discard") {
213
+ if (removal?.branchDeleted === false) {
214
+ const branch = removal.branch ?? workspace.worktree.branch;
215
+ return (`WARNING: worktree removed; branch ${branch} could not be deleted: ` +
216
+ `${removal.branchError ?? "unknown error"}\n` +
217
+ `Delete it manually with git branch -D ${branch}.\n` +
218
+ `Back to ${mainRoot} starting next turn.\n` +
219
+ nextTurnNotice(mainRoot, currentTurnRoot));
220
+ }
221
+ return (`Worktree removed and branch ${workspace.worktree.branch} deleted. ` +
222
+ `Back to ${mainRoot} starting next turn.\n` +
223
+ nextTurnNotice(mainRoot, currentTurnRoot));
117
224
  }
225
+ return (`Worktree removed${requested ? "" : " (auto-detached clean worktree)"}. ` +
226
+ `Branch ${workspace.worktree.branch} preserved.\n` +
227
+ `To merge: git merge ${workspace.worktree.branch}\n` +
228
+ `Back to ${mainRoot} starting next turn.\n` +
229
+ nextTurnNotice(mainRoot, currentTurnRoot));
118
230
  }
119
231
  catch (err) {
120
232
  return `Error exiting worktree: ${err.message}`;
121
233
  }
122
234
  }
235
+ function sessionServices(ctx) {
236
+ const sessionId = ctx?.sessionId;
237
+ if (!sessionId)
238
+ return { ok: false, error: "Error: worktree tools require a sessionId." };
239
+ const sessionManager = ctx?.engine?.getSessionManager?.();
240
+ if (!sessionManager) {
241
+ return { ok: false, error: "Error: worktree tools require a session manager." };
242
+ }
243
+ return { ok: true, sessionId, sessionManager };
244
+ }
245
+ async function otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, worktreePath) {
246
+ const workspaceOwners = sessionManager
247
+ .list(Number.MAX_SAFE_INTEGER)
248
+ .map((session) => ({ sessionId: session.sessionId, workspace: session.workspace }))
249
+ .filter((owner) => owner.workspace !== undefined);
250
+ const entry = (await listWorktrees(mainRoot, {
251
+ currentSessionId: sessionId,
252
+ workspaceOwners,
253
+ })).find((worktree) => resolve(worktree.path) === resolve(worktreePath));
254
+ return (entry?.occupiedBySessionIds ?? []).filter((owner) => owner !== sessionId);
255
+ }
256
+ function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot) {
257
+ return (`Error: this worktree is also in use by session(s) ${otherOwners.join(", ")}; ` +
258
+ `switching to main, but removal has been skipped.\n` +
259
+ `Back to ${mainRoot} starting next turn.\n` +
260
+ nextTurnNotice(mainRoot, currentTurnRoot));
261
+ }
262
+ async function resolveWorktreeTarget(opts) {
263
+ const entries = await listWorktrees(opts.mainRoot);
264
+ const pathTarget = pathLike(opts.target) ? resolvePathTarget(opts.target, opts.cwd) : undefined;
265
+ const branchTarget = normalizeBranchName(opts.target);
266
+ const match = entries.find((entry) => {
267
+ if (pathTarget && resolve(entry.path) === pathTarget)
268
+ return true;
269
+ return entry.branch === branchTarget;
270
+ });
271
+ if (match) {
272
+ return {
273
+ created: false,
274
+ session: {
275
+ originalCwd: opts.mainRoot,
276
+ worktreePath: match.path,
277
+ worktreeName: match.path.split(/[\\/]/).pop() ?? match.branch,
278
+ worktreeBranch: match.branch,
279
+ originalBranch: await currentBranch(opts.mainRoot),
280
+ sessionId: opts.sessionId,
281
+ createdAt: Date.now(),
282
+ },
283
+ from: opts.currentWorkspace.root,
284
+ };
285
+ }
286
+ if (pathTarget) {
287
+ throw new Error(`no existing worktree found at ${opts.target}`);
288
+ }
289
+ validateWorktreeSlug(opts.target);
290
+ const created = await createWorktree(opts.mainRoot, opts.target, opts.sessionId, {
291
+ prefix: opts.branchPrefix,
292
+ });
293
+ return { created: true, session: created, from: created.originalBranch ?? "HEAD" };
294
+ }
295
+ function toSessionWorkspace(selected, currentWorkspace) {
296
+ const previous = currentWorkspace.kind === "worktree" &&
297
+ currentWorkspace.worktree &&
298
+ resolve(currentWorkspace.worktree.path) === resolve(selected.session.worktreePath)
299
+ ? currentWorkspace.worktree
300
+ : undefined;
301
+ return {
302
+ root: selected.session.worktreePath,
303
+ kind: "worktree",
304
+ worktree: {
305
+ path: selected.session.worktreePath,
306
+ branch: selected.session.worktreeBranch,
307
+ baseRef: previous?.baseRef ?? selected.session.originalBranch ?? "HEAD",
308
+ createdBy: "codeshell",
309
+ },
310
+ };
311
+ }
312
+ async function runSetupIfConfigured(worktreePath, mainRoot, ctx) {
313
+ const setupScripts = ctx?.engine?.readWorktreeSetupScripts(mainRoot);
314
+ const script = selectPlatformScript(setupScripts);
315
+ if (!script)
316
+ return "";
317
+ const setupSandbox = ctx?.engine?.resolveWorktreeSetupSandbox
318
+ ? await ctx.engine.resolveWorktreeSetupSandbox(worktreePath)
319
+ : ctx?.sandbox;
320
+ const setupShellEnv = ctx?.engine?.readWorktreeSetupShellEnv
321
+ ? ctx.engine.readWorktreeSetupShellEnv(worktreePath)
322
+ : ctx?.shellEnv;
323
+ const setup = await runWorktreeSetup(worktreePath, script, {
324
+ sandbox: setupSandbox,
325
+ shellEnv: setupShellEnv,
326
+ signal: ctx?.signal,
327
+ });
328
+ if (setup.ok) {
329
+ return `\n\nRan setup script (exit 0).${setup.output ? `\n${truncate(setup.output)}` : ""}`;
330
+ }
331
+ return (`\n\n⚠️ Setup script failed (exit ${setup.exitCode ?? "?"}) — continuing anyway. ` +
332
+ `You may need to run setup manually.${setup.output ? `\n${truncate(setup.output)}` : ""}`);
333
+ }
334
+ function stringArg(value) {
335
+ return typeof value === "string" && value.length > 0 ? value : undefined;
336
+ }
337
+ function pathLike(target) {
338
+ return (isAbsolute(target) || target.startsWith(".") || target.includes("/") || target.includes("\\"));
339
+ }
340
+ function resolvePathTarget(target, cwd) {
341
+ return resolve(cwd, target);
342
+ }
343
+ function normalizeBranchName(branch) {
344
+ return branch.replace(/^refs\/heads\//, "");
345
+ }
@@ -2,7 +2,7 @@
2
2
  * Built-in Write file tool.
3
3
  */
4
4
  import { writeFile, mkdir } from "node:fs/promises";
5
- import { dirname } from "node:path";
5
+ import { dirname, isAbsolute, resolve } from "node:path";
6
6
  import { fileCache } from "./file-cache.js";
7
7
  export const writeToolDef = {
8
8
  name: "Write",
@@ -18,12 +18,14 @@ export const writeToolDef = {
18
18
  },
19
19
  };
20
20
  export async function writeTool(args, ctx) {
21
- const filePath = args.file_path;
21
+ const rawPath = args.file_path;
22
22
  const content = args.content;
23
- if (!filePath)
23
+ if (!rawPath)
24
24
  return "Error: file_path is required";
25
25
  if (content === undefined)
26
26
  return "Error: content is required";
27
+ const cwd = ctx?.cwd ?? process.cwd();
28
+ const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
27
29
  try {
28
30
  await mkdir(dirname(filePath), { recursive: true });
29
31
  await writeFile(filePath, content, "utf-8");
@@ -18,6 +18,8 @@ import type { ToolRegistry } from "./registry.js";
18
18
  import type { AgentPresetName } from "../preset/index.js";
19
19
  import type { SandboxBackend } from "./sandbox/index.js";
20
20
  import type { HookRegistry } from "../hooks/registry.js";
21
+ import type { SessionManager } from "../session/session-manager.js";
22
+ import type { SessionWorkspace } from "../types.js";
21
23
  /**
22
24
  * Narrow view of the owning Engine that tools are allowed to call back into.
23
25
  * Defined here (in the low-level tool-system) rather than importing the
@@ -38,6 +40,14 @@ export interface ToolRuntimeHost {
38
40
  linux?: string;
39
41
  windows?: string;
40
42
  } | undefined;
43
+ /** Branch prefix used for CodeShell-managed worktree branches. */
44
+ readWorktreeBranchPrefix?(cwd?: string): string | undefined;
45
+ /** Resolve a setup-only sandbox for a newly-created worktree root. */
46
+ resolveWorktreeSetupSandbox?(cwd: string): Promise<SandboxBackend | undefined>;
47
+ /** Resolve setup-only shell env for a newly-created worktree root. */
48
+ readWorktreeSetupShellEnv?(cwd?: string): Record<string, string> | undefined;
49
+ /** Session state store used by session-scoped tools such as worktree switching. */
50
+ getSessionManager?(): SessionManager;
41
51
  }
42
52
  /** One choice in a multiple-choice AskUserQuestion. */
43
53
  export interface AskUserChoice {
@@ -172,10 +182,24 @@ export interface SubAgentSpawner {
172
182
  export interface ToolVisibilityContext {
173
183
  cwd: string;
174
184
  hasGoal: boolean;
185
+ settingsScope?: import("../settings/manager.js").SettingsScope;
186
+ }
187
+ export interface ExternalFileChangesRecord {
188
+ jobId: string;
189
+ description: string;
190
+ cli: "claude" | "codex";
191
+ cwd: string;
192
+ status: "completed" | "failed" | "cancelled";
193
+ changedFiles: string[];
194
+ originClientMessageId?: string;
175
195
  }
176
196
  export interface ToolContext {
177
197
  /** Active working directory for this Engine. */
178
198
  cwd: string;
199
+ /** Mutate the owning live context cwd. Worktree switching intentionally does not use this. */
200
+ setCwd?(cwd: string): void;
201
+ /** Mutate the owning live session state after a session workspace switch. */
202
+ setSessionWorkspace?(workspace: SessionWorkspace): void;
179
203
  /** LLM credentials/endpoint for tools that need to make their own calls. */
180
204
  llmConfig: LLMConfig;
181
205
  /** Active model pool (Arena reads this to pick participants). */
@@ -246,6 +270,15 @@ export interface ToolContext {
246
270
  * narrow context, or standalone tool tests. (B2 — Gate 1, standard §S3.)
247
271
  */
248
272
  sessionId?: string;
273
+ /** Stable id of the real user turn that owns async work launched here. */
274
+ originClientMessageId?: string;
275
+ /**
276
+ * Persist file attribution from an async external DriveAgent completion in
277
+ * the owning CodeShell transcript. The callback closes over the live parent
278
+ * transcript, so a background job can record its files after the run that
279
+ * launched it has already returned.
280
+ */
281
+ recordExternalFileChanges?: (record: ExternalFileChangesRecord) => void;
249
282
  /**
250
283
  * Skill names the user has hidden from the LLM (full namespaced names
251
284
  * for plugin skills, e.g. "docs:pdf"). The skill builtin tool uses
@@ -336,6 +369,13 @@ export interface ToolContext {
336
369
  * docs/superpowers/specs/2026-06-16-browser-automation-mvp.md.
337
370
  */
338
371
  browser?: import("./browser-bridge.js").BrowserBridge;
372
+ /**
373
+ * Host-backed workspace switch bridge. Desktop wires this to Electron main so
374
+ * the model's SwitchSessionWorkspace tool goes through the same path as the UI
375
+ * switcher. Undefined outside hosts that can switch this conversation's
376
+ * session workspace.
377
+ */
378
+ workspace?: import("./workspace-bridge.js").WorkspaceBridge;
339
379
  /**
340
380
  * Inject a stored cookie credential into the built-in browser (restore its
341
381
  * login state so the AI can drive the page as that account). The host
@@ -347,7 +387,7 @@ export interface ToolContext {
347
387
  injectCredentialToBrowser?: InjectCredentialFn;
348
388
  }
349
389
  /** Inject a cookie credential into the built-in browser (host-implemented). */
350
- export type InjectCredentialFn = (credentialId: string) => Promise<{
390
+ export type InjectCredentialFn = (credentialId: string, credentialScope?: "full" | "project") => Promise<{
351
391
  ok: boolean;
352
392
  count?: number;
353
393
  error?: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tool executor — orchestrates permission checks, hooks, and execution.
3
3
  */
4
- import type { ToolCall, ToolResult, Message } from "../types.js";
4
+ import type { ToolCall, ToolResult } from "../types.js";
5
5
  import type { HookRegistry } from "../hooks/registry.js";
6
6
  import { ToolRegistry } from "./registry.js";
7
7
  import { PermissionClassifier } from "./permission.js";
@@ -42,9 +42,5 @@ export declare class ToolExecutor {
42
42
  private resolvePolicyOperation;
43
43
  private enforceDeclaredPathPolicy;
44
44
  private resolvePathPolicyTargets;
45
- /**
46
- * Convert tool results into Message entries for the transcript.
47
- */
48
- resultsToMessages(toolCalls: ToolCall[], results: ToolResult[]): Message[];
49
45
  }
50
46
  export {};