@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.16

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 (88) hide show
  1. package/dist/capability-control/service.d.ts +2 -0
  2. package/dist/capability-control/service.js +4 -2
  3. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  4. package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
  5. package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
  6. package/dist/cc-orchestrator/session-history.js +2 -2
  7. package/dist/cli/agent-server-stdio.js +7 -2
  8. package/dist/context/manager.d.ts +14 -2
  9. package/dist/context/manager.js +83 -8
  10. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  11. package/dist/credentials/inject-credential-tool.js +29 -10
  12. package/dist/credentials/use-credential-tool.d.ts +1 -0
  13. package/dist/credentials/use-credential-tool.js +3 -0
  14. package/dist/engine/engine.d.ts +9 -0
  15. package/dist/engine/engine.js +115 -25
  16. package/dist/engine/turn-loop.d.ts +3 -1
  17. package/dist/engine/turn-loop.js +11 -2
  18. package/dist/engine/types.d.ts +8 -0
  19. package/dist/git/worktree/crud.d.ts +69 -0
  20. package/dist/git/worktree/crud.js +206 -0
  21. package/dist/git/worktree/diff.d.ts +14 -0
  22. package/dist/git/worktree/diff.js +82 -0
  23. package/dist/git/worktree/git-exec.d.ts +7 -0
  24. package/dist/git/worktree/git-exec.js +51 -0
  25. package/dist/git/worktree/index.d.ts +5 -0
  26. package/dist/git/worktree/index.js +5 -0
  27. package/dist/git/worktree/query.d.ts +42 -0
  28. package/dist/git/worktree/query.js +121 -0
  29. package/dist/git/worktree/slug.d.ts +11 -0
  30. package/dist/git/worktree/slug.js +58 -0
  31. package/dist/git/worktree.d.ts +1 -127
  32. package/dist/git/worktree.js +5 -464
  33. package/dist/index.d.ts +28 -27
  34. package/dist/index.js +24 -24
  35. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  36. package/dist/plugins/installer/checkUpdate.js +4 -2
  37. package/dist/plugins/installer/install.js +2 -0
  38. package/dist/plugins/installer/parseSource.d.ts +4 -1
  39. package/dist/plugins/installer/parseSource.js +28 -10
  40. package/dist/plugins/installer/update.d.ts +4 -1
  41. package/dist/plugins/installer/update.js +5 -3
  42. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  43. package/dist/plugins/parseMarketplaceInput.js +4 -3
  44. package/dist/preset/index.d.ts +1 -0
  45. package/dist/preset/index.js +6 -0
  46. package/dist/prompt/sections/base.md +1 -1
  47. package/dist/protocol/chat-session-manager.js +7 -0
  48. package/dist/protocol/server.d.ts +10 -0
  49. package/dist/protocol/server.js +90 -5
  50. package/dist/protocol/types.d.ts +6 -0
  51. package/dist/protocol/types.js +2 -0
  52. package/dist/runtime/background-shell.d.ts +1 -0
  53. package/dist/runtime/background-shell.js +23 -13
  54. package/dist/services/auto-dream.d.ts +15 -4
  55. package/dist/services/auto-dream.js +20 -20
  56. package/dist/services/dream-consolidation.d.ts +2 -3
  57. package/dist/services/dream-consolidation.js +65 -15
  58. package/dist/services/extract-memories.d.ts +14 -5
  59. package/dist/services/extract-memories.js +20 -3
  60. package/dist/services/global-dream-promotion.d.ts +23 -0
  61. package/dist/services/global-dream-promotion.js +112 -0
  62. package/dist/services/memory-orchestrator.js +347 -34
  63. package/dist/session/memory.d.ts +61 -18
  64. package/dist/session/memory.js +342 -79
  65. package/dist/session/session-manager.d.ts +1 -1
  66. package/dist/session/session-manager.js +6 -6
  67. package/dist/settings/manager.js +43 -12
  68. package/dist/settings/schema.d.ts +21 -0
  69. package/dist/settings/schema.js +12 -0
  70. package/dist/skills/scanner.d.ts +3 -2
  71. package/dist/skills/scanner.js +12 -9
  72. package/dist/tool-system/builtin/background-jobs.d.ts +2 -0
  73. package/dist/tool-system/builtin/background-jobs.js +4 -0
  74. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  75. package/dist/tool-system/builtin/background-work.js +51 -5
  76. package/dist/tool-system/builtin/index.js +18 -8
  77. package/dist/tool-system/builtin/memory.js +40 -8
  78. package/dist/tool-system/builtin/worktree.d.ts +2 -0
  79. package/dist/tool-system/builtin/worktree.js +59 -11
  80. package/dist/tool-system/context.d.ts +11 -1
  81. package/dist/tool-system/mcp-manager.d.ts +8 -5
  82. package/dist/tool-system/mcp-manager.js +85 -55
  83. package/dist/tool-system/path-policy.d.ts +2 -0
  84. package/dist/tool-system/path-policy.js +28 -12
  85. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  86. package/dist/tool-system/workspace-bridge.js +1 -0
  87. package/dist/types.d.ts +20 -0
  88. package/package.json +1 -1
@@ -42,12 +42,12 @@ import { MCPManager } from "../tool-system/mcp-manager.js";
42
42
  import { SettingsManager, userHome } from "../settings/manager.js";
43
43
  import { CredentialStore } from "../credentials/store.js";
44
44
  import { isFeatureEnabled, resolveFeatureFlags, } from "../settings/feature-flags.js";
45
- import { effectiveDisabledList, effectiveBuiltinLists, } from "../capability-control/overlay.js";
45
+ import { effectiveDisabledList, effectiveBuiltinLists } from "../capability-control/overlay.js";
46
46
  import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
47
47
  import { FileHistory } from "../session/file-history.js";
48
48
  import { patchBackupTargets } from "../tool-system/builtin/apply-patch/backup-targets.js";
49
49
  import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
50
- import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
50
+ import { resolveAgentPreset, resolveBuiltinToolNames } from "../preset/index.js";
51
51
  import { ModelPool } from "../llm/model-pool.js";
52
52
  import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
53
53
  import { defaultCacheDir } from "../llm/model-cache.js";
@@ -445,6 +445,7 @@ export class Engine {
445
445
  new ToolRegistry({
446
446
  builtinTools: resolveBuiltinToolNames({
447
447
  preset: this.preset.name,
448
+ host: config.builtinToolHost,
448
449
  enabledBuiltinTools: builtinLists.enabledBuiltinTools,
449
450
  disabledBuiltinTools: builtinLists.disabledBuiltinTools,
450
451
  }),
@@ -635,6 +636,10 @@ export class Engine {
635
636
  setBrowserBridge(bridge) {
636
637
  this.config.browserBridge = bridge;
637
638
  }
639
+ /** Inject the host-backed workspace bridge after construction. */
640
+ setWorkspaceBridge(bridge) {
641
+ this.config.workspaceBridge = bridge;
642
+ }
638
643
  /**
639
644
  * Queue a user message to be spliced into the in-flight run for `sessionId`
640
645
  * at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
@@ -736,7 +741,7 @@ export class Engine {
736
741
  */
737
742
  async run(task, options) {
738
743
  const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
739
- ? this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
744
+ ? await this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
740
745
  : undefined;
741
746
  if (workspaceResume && !workspaceResume.ok) {
742
747
  return {
@@ -971,6 +976,7 @@ export class Engine {
971
976
  preset: this.preset.name,
972
977
  enabledBuiltinTools: childEnabled,
973
978
  disabledBuiltinTools: childDisabled,
979
+ builtinToolHost: this.config.builtinToolHost,
974
980
  customSystemPrompt: this.config.customSystemPrompt,
975
981
  appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
976
982
  undefined,
@@ -1311,22 +1317,49 @@ export class Engine {
1311
1317
  messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
1312
1318
  }
1313
1319
  }
1314
- // Rough token estimate of the full prompt so the UI's ctx bar isn't 0%
1315
- // before the first real usage_update arrives. The authoritative count
1320
+ const contextManager = new ContextManager({
1321
+ maxTokens: this.resolveMaxContextTokens(),
1322
+ // Drop undefined fields so they don't clobber ContextManager defaults
1323
+ // (spread of `{x: undefined}` would override the default with undefined).
1324
+ ...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
1325
+ });
1326
+ this.lastContextManager = contextManager;
1327
+ const persistedContextAnchor = session.state.contextUsageAnchor;
1328
+ const contextAnchorCompatible = persistedContextAnchor !== undefined &&
1329
+ (persistedContextAnchor.provider === undefined ||
1330
+ persistedContextAnchor.provider === this.config.llm.provider) &&
1331
+ (persistedContextAnchor.model === undefined ||
1332
+ persistedContextAnchor.model === this.config.llm.model) &&
1333
+ (persistedContextAnchor.messageCount <= messages.length ||
1334
+ persistedContextAnchor.estimateAtAnchor !== undefined);
1335
+ if (contextAnchorCompatible) {
1336
+ contextManager.seedActualUsage(persistedContextAnchor);
1337
+ }
1338
+ // Best-effort token estimate of the full prompt so the UI's ctx bar isn't
1339
+ // 0% before the first real usage_update arrives. The authoritative count
1316
1340
  // comes from `usage.promptTokens` after the first LLM response — this is
1317
- // just a display-friendly approximation for the first frame.
1341
+ // just a display-friendly approximation for the first frame, annotated
1342
+ // with source/confidence so consumers don't treat heuristics as truth.
1318
1343
  //
1319
1344
  // Only seed once per (process, sid). On subsequent turns the UI already
1320
- // shows the previous turn's accurate ctx; overwriting it with this rough
1321
- // char/4 estimate would make the bar visibly drop on every submit.
1345
+ // shows the previous turn's accurate ctx; overwriting it with a fresh
1346
+ // best-effort estimate would make the bar visibly drop on every submit.
1322
1347
  const sid = session.state.sessionId;
1323
1348
  const needsCtxSeed = !this.ctxSeedSent.has(sid);
1324
- const roughPromptTokens = needsCtxSeed
1325
- ? messages.reduce((sum, m) => {
1326
- const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1327
- return sum + Math.ceil(text.length / 4);
1328
- }, 0)
1329
- : 0;
1349
+ const ctxSeed = needsCtxSeed
1350
+ ? (() => {
1351
+ const checked = contextManager.checkLimits(messages);
1352
+ return {
1353
+ tokens: checked.tokens,
1354
+ source: checked.promptTokensSource,
1355
+ confidence: checked.promptTokensConfidence,
1356
+ };
1357
+ })()
1358
+ : {
1359
+ tokens: 0,
1360
+ source: "heuristic_estimate",
1361
+ confidence: "low",
1362
+ };
1330
1363
  if (needsCtxSeed)
1331
1364
  this.ctxSeedSent.add(sid);
1332
1365
  // Tell the client the sid *now* instead of waiting for run() to resolve.
@@ -1335,7 +1368,9 @@ export class Engine {
1335
1368
  options?.onStream?.({
1336
1369
  type: "session_started",
1337
1370
  sessionId: sid,
1338
- promptTokens: roughPromptTokens,
1371
+ promptTokens: ctxSeed.tokens,
1372
+ promptTokensSource: ctxSeed.source,
1373
+ promptTokensConfidence: ctxSeed.confidence,
1339
1374
  });
1340
1375
  // Replay the last TodoWrite snapshot on resume so the UI's pinned
1341
1376
  // task panel re-hydrates without the LLM needing to call TodoWrite
@@ -1383,13 +1418,6 @@ export class Engine {
1383
1418
  // Wire abort signal for cascading cancellation + per-Engine ToolContext
1384
1419
  toolExecutor.setSignal(options?.signal);
1385
1420
  toolExecutor.setContext(toolCtx);
1386
- const contextManager = new ContextManager({
1387
- maxTokens: this.resolveMaxContextTokens(),
1388
- // Drop undefined fields so they don't clobber ContextManager defaults
1389
- // (spread of `{x: undefined}` would override the default with undefined).
1390
- ...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
1391
- });
1392
- this.lastContextManager = contextManager;
1393
1421
  const { disabledSkills, disabledPlugins } = this.readDisabledLists();
1394
1422
  const promptComposer = new PromptComposer({
1395
1423
  cwd,
@@ -1451,6 +1479,7 @@ export class Engine {
1451
1479
  (normalizeGoal(options?.goal) !== undefined ||
1452
1480
  session.state.activeGoal !== undefined ||
1453
1481
  normalizeGoal(this.config.goal) !== undefined),
1482
+ settingsScope: this.config.settingsScope ?? "project",
1454
1483
  };
1455
1484
  toolCtx.toolVisibility = toolVisibility;
1456
1485
  // #7: per-turn project builtin override. The toolRegistry's builtin tool
@@ -1761,6 +1790,13 @@ export class Engine {
1761
1790
  consumeSteer: (source) => this.consumeSteer(sid, source),
1762
1791
  claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
1763
1792
  recordCumulativeUsage,
1793
+ recordContextUsageAnchor: (anchor) => {
1794
+ session.state.contextUsageAnchor = {
1795
+ ...anchor,
1796
+ provider: this.config.llm.provider,
1797
+ model: this.config.llm.model,
1798
+ };
1799
+ },
1764
1800
  // Clear the persisted goal for a self-reported completion / confirmed
1765
1801
  // cancel. Clears the in-RAM session's activeGoal (so THIS run's later
1766
1802
  // turns don't re-arm) AND persists it, and drops the in-flight stop
@@ -1820,6 +1856,8 @@ export class Engine {
1820
1856
  options?.onStream?.({
1821
1857
  type: "usage_update",
1822
1858
  promptTokens: cumulative.cumulativePromptTokens,
1859
+ promptTokensSource: "session_cumulative",
1860
+ promptTokensConfidence: "high",
1823
1861
  cumulativePromptTokens: cumulative.cumulativePromptTokens,
1824
1862
  cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
1825
1863
  cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
@@ -2359,7 +2397,6 @@ export class Engine {
2359
2397
  refreshRuntimeConfig(patch, version) {
2360
2398
  if (version <= this.lastAppliedConfigVersion)
2361
2399
  return;
2362
- const prevServers = this.config.mcpServers ?? {};
2363
2400
  const prevPresetName = this.preset.name;
2364
2401
  this.config = { ...this.config, ...patch };
2365
2402
  // #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
@@ -2370,11 +2407,17 @@ export class Engine {
2370
2407
  // The builtin tool SET is ctor-frozen and may be shared via runtime — we
2371
2408
  // do NOT rebuild it here. If the new preset implies a different builtin
2372
2409
  // tool set, that part of the change only lands on session restart.
2373
- const prevTools = resolveBuiltinToolNames({ preset: prevPresetName })
2410
+ const prevTools = resolveBuiltinToolNames({
2411
+ preset: prevPresetName,
2412
+ host: this.config.builtinToolHost,
2413
+ })
2374
2414
  .slice()
2375
2415
  .sort()
2376
2416
  .join(",");
2377
- const nextTools = resolveBuiltinToolNames({ preset: nextPreset.name })
2417
+ const nextTools = resolveBuiltinToolNames({
2418
+ preset: nextPreset.name,
2419
+ host: this.config.builtinToolHost,
2420
+ })
2378
2421
  .slice()
2379
2422
  .sort()
2380
2423
  .join(",");
@@ -2461,6 +2504,41 @@ export class Engine {
2461
2504
  }
2462
2505
  return had;
2463
2506
  }
2507
+ /**
2508
+ * Reset a session's workspace pointer back to its main root. If the session is
2509
+ * actively running, mutate that live SessionBundle first so the run's next
2510
+ * saveState cannot resurrect a stale worktree pointer.
2511
+ */
2512
+ releaseSessionWorkspace(sessionId) {
2513
+ if (!sessionId || !this.sessionManager.exists(sessionId))
2514
+ return null;
2515
+ const mainRoot = this.sessionManager.readCwd(sessionId) ??
2516
+ (this.activeRunSession?.state.sessionId === sessionId
2517
+ ? this.activeRunSession.state.cwd
2518
+ : undefined);
2519
+ if (!mainRoot)
2520
+ return null;
2521
+ const workspace = { root: mainRoot, kind: "main" };
2522
+ if (this.activeRunSession?.state.sessionId === sessionId) {
2523
+ this.activeRunSession.state.workspace = workspace;
2524
+ }
2525
+ try {
2526
+ const bundle = this.activeRunSession?.state.sessionId === sessionId
2527
+ ? this.activeRunSession
2528
+ : this.sessionManager.resume(sessionId);
2529
+ bundle.state.workspace = workspace;
2530
+ this.sessionManager.saveState(bundle.state);
2531
+ }
2532
+ catch {
2533
+ try {
2534
+ this.sessionManager.setSessionWorkspace(sessionId, workspace);
2535
+ }
2536
+ catch {
2537
+ return null;
2538
+ }
2539
+ }
2540
+ return workspace;
2541
+ }
2464
2542
  injectContext(sessionId, content) {
2465
2543
  const session = this.sessionManager.resume(sessionId);
2466
2544
  session.transcript.appendMessage("assistant", content);
@@ -2954,6 +3032,17 @@ export class Engine {
2954
3032
  return undefined;
2955
3033
  }
2956
3034
  }
3035
+ readWorktreeBranchPrefix(cwd) {
3036
+ if (this.config.isSubAgent === true || !cwd)
3037
+ return undefined;
3038
+ try {
3039
+ const settings = this.getSettingsManager().get();
3040
+ return settings.worktree?.branchPrefix;
3041
+ }
3042
+ catch {
3043
+ return undefined;
3044
+ }
3045
+ }
2957
3046
  async resolveWorktreeSetupSandbox(cwd) {
2958
3047
  if (!cwd)
2959
3048
  return undefined;
@@ -2978,6 +3067,7 @@ export class Engine {
2978
3067
  toolRegistry: this.toolRegistry,
2979
3068
  askUser: this.config.askUser,
2980
3069
  browser: this.config.browserBridge,
3070
+ workspace: this.config.workspaceBridge,
2981
3071
  injectCredentialToBrowser: this.config.injectCredentialToBrowser,
2982
3072
  isSubAgent: this.config.isSubAgent === true,
2983
3073
  // Credential tools narrow their disk reads to this scope: a project/
@@ -4,7 +4,7 @@
4
4
  * Following Claude Code's po_() pattern:
5
5
  * pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
6
6
  */
7
- import type { Message, StreamCallback, TerminalReason, ContentBlock, ToolResult, TokenUsage } from "../types.js";
7
+ import type { Message, StreamCallback, TerminalReason, ContentBlock, ToolResult, TokenUsage, ContextUsageAnchor } from "../types.js";
8
8
  import type { SteerItem } from "./steer-queue.js";
9
9
  import { ModelFacade } from "./model-facade.js";
10
10
  import { ToolExecutor } from "../tool-system/executor.js";
@@ -88,6 +88,8 @@ export interface TurnLoopDeps {
88
88
  * Returns the updated counters so usage_update can carry both metric scopes.
89
89
  */
90
90
  recordCumulativeUsage?: (usage: TokenUsage) => CumulativeUsageCounters;
91
+ /** Persist the latest context-estimation anchor derived from provider usage. */
92
+ recordContextUsageAnchor?: (anchor: ContextUsageAnchor) => void;
91
93
  /**
92
94
  * Reads/clears any user messages queued for THIS session via the steering
93
95
  * channel (Engine.enqueueSteer) while a run is in flight. Consumed at the top
@@ -281,7 +281,12 @@ export class TurnLoop {
281
281
  if (ctx === this.lastCtxEmit)
282
282
  return;
283
283
  this.lastCtxEmit = ctx;
284
- this.config.onStream({ type: "usage_update", promptTokens: ctx });
284
+ this.config.onStream({
285
+ type: "usage_update",
286
+ promptTokens: ctx,
287
+ promptTokensSource: overhead > 0 ? "calibrated_estimate" : "heuristic_estimate",
288
+ promptTokensConfidence: overhead > 0 ? "medium" : "low",
289
+ });
285
290
  }
286
291
  recordResponseUsage(usage) {
287
292
  this.currentTurnUsage = addTokenUsage(this.currentTurnUsage, usage);
@@ -320,6 +325,8 @@ export class TurnLoop {
320
325
  this.config.onStream({
321
326
  type: "usage_update",
322
327
  promptTokens,
328
+ promptTokensSource: "provider_usage",
329
+ promptTokensConfidence: "high",
323
330
  ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
324
331
  ...(usage.cacheCreationTokens !== undefined
325
332
  ? { cacheCreationTokens: usage.cacheCreationTokens }
@@ -553,7 +560,9 @@ export class TurnLoop {
553
560
  // compaction decisions use hybrid (actual + delta) estimation rather than
554
561
  // pure heuristics. Without this the manager falls back to char/4 estimates.
555
562
  if (response.usage?.promptTokens !== undefined) {
556
- this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
563
+ const anchor = this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
564
+ if (anchor)
565
+ this.deps.recordContextUsageAnchor?.(anchor);
557
566
  }
558
567
  messages = this.markPendingImagesConsumed(messages);
559
568
  // Truncation that cut off a TOOL CALL: the model overflowed
@@ -75,6 +75,8 @@ export interface EngineConfig {
75
75
  /** Browser automation bridge (browser_* tools). Wired by the host (desktop)
76
76
  * after construction via setBrowserBridge. Undefined → tools degrade. */
77
77
  browserBridge?: import("../tool-system/browser-bridge.js").BrowserBridge;
78
+ /** Host-backed workspace switch bridge (desktop only). */
79
+ workspaceBridge?: import("../tool-system/workspace-bridge.js").WorkspaceBridge;
78
80
  /** Inject a cookie credential into the built-in browser (InjectCredential
79
81
  * tool). Wired by the host after construction via setInjectCredential.
80
82
  * Undefined → the tool degrades with a clear "no browser" error. */
@@ -127,6 +129,12 @@ export interface EngineConfig {
127
129
  * automation shown; tui hidden). Sub-agents override to "subagent".
128
130
  */
129
131
  origin?: SessionOrigin;
132
+ /**
133
+ * Host-specific builtin tool surface. Desktop replaces the legacy core
134
+ * worktree tools with SwitchSessionWorkspace because Electron main owns the
135
+ * authoritative workspace switch service and UI refresh path.
136
+ */
137
+ builtinToolHost?: "desktop";
130
138
  /**
131
139
  * Optional shared EngineRuntime providing pre-constructed shared resources
132
140
  * (modelPool, toolRegistry, etc.). When provided, Engine uses these instead
@@ -0,0 +1,69 @@
1
+ import type { SandboxBackend } from "../../tool-system/sandbox/index.js";
2
+ export interface WorktreeSession {
3
+ originalCwd: string;
4
+ worktreePath: string;
5
+ worktreeName: string;
6
+ worktreeBranch: string;
7
+ originalBranch?: string;
8
+ sessionId: string;
9
+ createdAt: number;
10
+ }
11
+ /** Per-platform setup/cleanup scripts (a project's localEnvironment). */
12
+ export interface PlatformScripts {
13
+ default?: string;
14
+ macos?: string;
15
+ linux?: string;
16
+ windows?: string;
17
+ }
18
+ export interface CreateWorktreeOptions {
19
+ prefix?: string;
20
+ }
21
+ /**
22
+ * Pick the setup/cleanup script for the running platform, falling back to
23
+ * `default`. Empty/whitespace-only scripts are treated as absent so a project
24
+ * can leave a platform key blank without spawning an empty shell. `platform`
25
+ * defaults to `process.platform` so callers usually omit it; tests pass a
26
+ * fixed value.
27
+ */
28
+ export declare function selectPlatformScript(scripts: PlatformScripts | undefined, platform?: NodeJS.Platform): string | undefined;
29
+ /**
30
+ * Create an isolated git worktree for an agent session.
31
+ */
32
+ export declare function createWorktree(cwd: string, slug: string, sessionId: string, opts?: CreateWorktreeOptions): Promise<WorktreeSession>;
33
+ export interface WorktreeSetupResult {
34
+ /** True if no setup script was configured for this platform (nothing ran). */
35
+ skipped: boolean;
36
+ /** True if a script ran and exited 0. */
37
+ ok: boolean;
38
+ /** Combined stdout/stderr, for surfacing to the user on failure. */
39
+ output: string;
40
+ /** Exit code when a script ran; undefined when skipped. */
41
+ exitCode?: number | null;
42
+ }
43
+ /**
44
+ * Run a project's `localEnvironment.setupScripts` once, in the freshly-created
45
+ * worktree's root, right after `git worktree add`.
46
+ */
47
+ export declare function runWorktreeSetup(worktreePath: string, script: string | undefined, opts?: {
48
+ sandbox?: SandboxBackend;
49
+ shellEnv?: Record<string, string>;
50
+ timeoutMs?: number;
51
+ signal?: AbortSignal;
52
+ }): Promise<WorktreeSetupResult>;
53
+ export interface RemoveWorktreeResult {
54
+ /** True once `git worktree remove` completed and the directory is gone. */
55
+ dirRemoved: boolean;
56
+ /** The branch targeted for deletion when removeBranch=true. */
57
+ branch?: string;
58
+ /** True when removeBranch=true and branch deletion completed. */
59
+ branchDeleted?: boolean;
60
+ /** Non-empty when the worktree directory is gone but branch deletion failed. */
61
+ branchError?: string;
62
+ }
63
+ export interface RemoveWorktreeOptions {
64
+ prefix?: string;
65
+ }
66
+ /**
67
+ * Remove a worktree and optionally its branch.
68
+ */
69
+ export declare function removeWorktree(worktreePath: string, removeBranch?: boolean, opts?: RemoveWorktreeOptions): RemoveWorktreeResult;
@@ -0,0 +1,206 @@
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, symlinkSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { safeSpawnShell } from "../../runtime/safe-spawn.js";
4
+ import { buildSandboxEnv, defaultShellBinary, mergeShellEnv } from "../../runtime/spawn-common.js";
5
+ import { execGit, execGitSync, gitErrorMessage } from "./git-exec.js";
6
+ import { assertBranchNotCheckedOut, currentBranch, findGitRoot } from "./query.js";
7
+ import { applyPrefix, isManagedWorktreeBranch, validateWorktreeSlug } from "./slug.js";
8
+ /**
9
+ * Pick the setup/cleanup script for the running platform, falling back to
10
+ * `default`. Empty/whitespace-only scripts are treated as absent so a project
11
+ * can leave a platform key blank without spawning an empty shell. `platform`
12
+ * defaults to `process.platform` so callers usually omit it; tests pass a
13
+ * fixed value.
14
+ */
15
+ export function selectPlatformScript(scripts, platform = process.platform) {
16
+ if (!scripts)
17
+ return undefined;
18
+ const key = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux";
19
+ // A blank platform key shouldn't shadow a real `default` — fall through.
20
+ const platformScript = scripts[key]?.trim() ? scripts[key] : undefined;
21
+ const candidate = platformScript ?? scripts.default;
22
+ const trimmed = candidate?.trim();
23
+ return trimmed ? candidate : undefined;
24
+ }
25
+ /**
26
+ * Create an isolated git worktree for an agent session.
27
+ */
28
+ export async function createWorktree(cwd, slug, sessionId, opts = {}) {
29
+ validateWorktreeSlug(slug);
30
+ const gitRoot = await findGitRoot(cwd);
31
+ const branchName = applyPrefix(opts.prefix, slug, sessionId);
32
+ const worktreePath = resolve(gitRoot, "..", `.worktrees/${slug}-${sessionId.slice(0, 8)}`);
33
+ await assertBranchNotCheckedOut(gitRoot, branchName);
34
+ const originalBranch = await currentBranch(gitRoot);
35
+ // The argv form keeps branchName/worktreePath as literal positional
36
+ // arguments even if a future caller bypasses validateWorktreeSlug.
37
+ await execGit(gitRoot, ["worktree", "add", "-b", branchName, worktreePath], 30000);
38
+ // Symlink large directories to avoid disk bloat.
39
+ symlinkLargeDirectories(gitRoot, worktreePath);
40
+ return {
41
+ originalCwd: cwd,
42
+ worktreePath,
43
+ worktreeName: slug,
44
+ worktreeBranch: branchName,
45
+ originalBranch,
46
+ sessionId,
47
+ createdAt: Date.now(),
48
+ };
49
+ }
50
+ /**
51
+ * Run a project's `localEnvironment.setupScripts` once, in the freshly-created
52
+ * worktree's root, right after `git worktree add`.
53
+ */
54
+ export async function runWorktreeSetup(worktreePath, script, opts = {}) {
55
+ const trimmed = script?.trim();
56
+ if (!trimmed)
57
+ return { skipped: true, ok: true, output: "" };
58
+ const shell = defaultShellBinary();
59
+ const backend = opts.sandbox;
60
+ const baseEnv = backend && backend.name !== "off" ? buildSandboxEnv() : { ...process.env };
61
+ const env = mergeShellEnv(baseEnv, opts.shellEnv);
62
+ const result = await safeSpawnShell(trimmed, {
63
+ cwd: worktreePath,
64
+ env,
65
+ timeoutMs: opts.timeoutMs ?? 120_000,
66
+ maxOutputBytes: 1024 * 1024,
67
+ sandbox: backend,
68
+ shell,
69
+ signal: opts.signal,
70
+ });
71
+ const parts = [];
72
+ if (result.stdout)
73
+ parts.push(result.stdout);
74
+ if (result.stderr)
75
+ parts.push(result.stderr);
76
+ const output = parts.join("\n").trim();
77
+ if (result.aborted)
78
+ return { skipped: false, ok: false, output: output || "setup aborted" };
79
+ if (result.timedOut)
80
+ return { skipped: false, ok: false, output: output || "setup timed out" };
81
+ if (result.spawnFailed) {
82
+ return { skipped: false, ok: false, output: result.error ?? "failed to spawn setup script" };
83
+ }
84
+ return { skipped: false, ok: result.exitCode === 0, output, exitCode: result.exitCode };
85
+ }
86
+ /**
87
+ * Remove a worktree and optionally its branch.
88
+ */
89
+ export function removeWorktree(worktreePath, removeBranch = false, opts = {}) {
90
+ // The MAIN repo root, not the worktree's own toplevel. `git rev-parse
91
+ // --show-toplevel` from inside a worktree returns the worktree path, which
92
+ // is about to be deleted; the branch-delete must run from the main repo,
93
+ // which outlives the worktree. Derive it from the common git dir.
94
+ let mainRoot;
95
+ try {
96
+ const commonDir = execGitSync(worktreePath, ["rev-parse", "--path-format=absolute", "--git-common-dir"], 5000).trim();
97
+ mainRoot = dirname(commonDir);
98
+ }
99
+ catch (err) {
100
+ throw new Error(`failed to inspect worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
101
+ cause: err,
102
+ });
103
+ }
104
+ // Capture the worktree's branch BEFORE removing the worktree — afterwards
105
+ // the directory is gone and `git branch --show-current` in it would fail.
106
+ let branch = "";
107
+ if (removeBranch) {
108
+ try {
109
+ branch = execGitSync(worktreePath, ["branch", "--show-current"], 5000).trim();
110
+ }
111
+ catch (err) {
112
+ throw new Error(`failed to determine branch for worktree ${worktreePath}: ${gitErrorMessage(err)}`, { cause: err });
113
+ }
114
+ if (!branch) {
115
+ throw new Error(`failed to determine branch for worktree ${worktreePath}`);
116
+ }
117
+ if (!isManagedWorktreeBranch(branch, opts.prefix)) {
118
+ throw new Error(`refusing to delete non-CodeShell worktree branch ${branch}`);
119
+ }
120
+ }
121
+ try {
122
+ execGitSync(mainRoot, ["worktree", "remove", worktreePath, "--force"], 30000);
123
+ }
124
+ catch (err) {
125
+ throw new Error(`failed to remove worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
126
+ cause: err,
127
+ });
128
+ }
129
+ if (removeBranch) {
130
+ try {
131
+ execGitSync(mainRoot, ["branch", "-D", branch], 10000);
132
+ }
133
+ catch (err) {
134
+ return {
135
+ dirRemoved: true,
136
+ branch,
137
+ branchDeleted: false,
138
+ branchError: gitErrorMessage(err),
139
+ };
140
+ }
141
+ }
142
+ return removeBranch ? { dirRemoved: true, branch, branchDeleted: true } : { dirRemoved: true };
143
+ }
144
+ /**
145
+ * Symlink large directories (node_modules, .venv, etc.) from main repo to
146
+ * worktree. Also links each monorepo workspace package's private
147
+ * `node_modules` (e.g. `packages/desktop/node_modules`, where bun keeps
148
+ * un-hoisted deps like electron) so a fresh worktree can build/run the whole
149
+ * workspace, not just the packages whose deps happen to be hoisted to the root.
150
+ */
151
+ function symlinkLargeDirectories(sourceRoot, worktreePath) {
152
+ const largeDirs = ["node_modules", ".venv", "vendor", ".pnpm-store"];
153
+ for (const dir of largeDirs) {
154
+ linkDir(join(sourceRoot, dir), join(worktreePath, dir));
155
+ }
156
+ // Monorepo workspace packages keep private node_modules that are NOT hoisted
157
+ // to the root. Link `<pkg>/node_modules` for each package under `packages/`.
158
+ const packagesDir = join(sourceRoot, "packages");
159
+ if (!dirExists(packagesDir))
160
+ return;
161
+ let pkgNames;
162
+ try {
163
+ pkgNames = readdirSync(packagesDir);
164
+ }
165
+ catch {
166
+ return;
167
+ }
168
+ for (const pkg of pkgNames) {
169
+ const source = join(packagesDir, pkg, "node_modules");
170
+ if (!dirExists(source))
171
+ continue;
172
+ const targetPkgDir = join(worktreePath, "packages", pkg);
173
+ // The worktree only has package dirs that exist in this branch's tree; a
174
+ // package present in the main repo but not checked out here is skipped.
175
+ if (!dirExists(targetPkgDir)) {
176
+ try {
177
+ mkdirSync(targetPkgDir, { recursive: true });
178
+ }
179
+ catch {
180
+ continue;
181
+ }
182
+ }
183
+ linkDir(source, join(targetPkgDir, "node_modules"));
184
+ }
185
+ }
186
+ function dirExists(path) {
187
+ try {
188
+ return lstatSync(path).isDirectory();
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ function linkDir(source, target) {
195
+ // Windows directory symlinks need admin/Developer Mode and throw EPERM for a
196
+ // normal user; NTFS junctions don't and behave the same for our purpose.
197
+ const linkType = process.platform === "win32" ? "junction" : "dir";
198
+ if (dirExists(source) && !existsSync(target)) {
199
+ try {
200
+ symlinkSync(source, target, linkType);
201
+ }
202
+ catch {
203
+ // Symlink/junction might fail on some systems — non-fatal.
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,14 @@
1
+ export interface WorktreeDiffSummary {
2
+ /** Best-effort base used for the branch comparison, usually main/master. */
3
+ baseRef?: string;
4
+ /** Unique files changed either by commits ahead of base or by uncommitted changes. */
5
+ changedFiles: number;
6
+ /** Commits reachable from HEAD but not from baseRef. */
7
+ aheadCommits: number;
8
+ /** True when `git status --porcelain` reports local changes. */
9
+ hasUncommittedChanges: boolean;
10
+ }
11
+ export declare function getWorktreeDiff(worktreePath: string, baseRef?: string): Promise<WorktreeDiffSummary>;
12
+ export declare function worktreeHasUncommittedChanges(worktreePath: string): Promise<boolean>;
13
+ export declare function worktreeHasUncommittedOrAheadChanges(worktreePath: string, baseRef?: string): Promise<boolean>;
14
+ export declare function findComparisonBaseRef(cwd: string): Promise<string | undefined>;