wave-agent-sdk 0.19.7 → 0.19.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
  2. package/builtin/plugins/sdd/hooks/hooks.json +14 -0
  3. package/builtin/plugins/sdd/scripts/session-start.js +24 -0
  4. package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
  5. package/builtin/plugins/sdd/skills/specify/SKILL.md +48 -0
  6. package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
  7. package/dist/agent.d.ts +8 -0
  8. package/dist/agent.js +30 -10
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts +18 -0
  12. package/dist/managers/aiManager.js +155 -46
  13. package/dist/managers/permissionManager.d.ts +7 -0
  14. package/dist/managers/permissionManager.js +102 -142
  15. package/dist/managers/pluginManager.d.ts +7 -0
  16. package/dist/managers/pluginManager.js +31 -0
  17. package/dist/managers/subagentManager.js +6 -0
  18. package/dist/prompts/index.d.ts +12 -1
  19. package/dist/prompts/index.js +133 -45
  20. package/dist/services/aiService.d.ts +1 -17
  21. package/dist/services/aiService.js +3 -85
  22. package/dist/services/configurationService.d.ts +6 -0
  23. package/dist/services/configurationService.js +31 -0
  24. package/dist/services/remoteSettingsService.js +2 -0
  25. package/dist/services/session.d.ts +3 -1
  26. package/dist/services/session.js +12 -4
  27. package/dist/services/taskManager.d.ts +1 -0
  28. package/dist/services/taskManager.js +34 -5
  29. package/dist/tools/editTool.js +24 -10
  30. package/dist/tools/enterWorktreeTool.js +2 -1
  31. package/dist/tools/grepTool.js +8 -2
  32. package/dist/tools/writeTool.js +36 -0
  33. package/dist/types/configuration.d.ts +5 -0
  34. package/dist/types/permissions.d.ts +0 -2
  35. package/dist/types/processes.d.ts +27 -0
  36. package/dist/types/workflow.d.ts +1 -1
  37. package/dist/utils/bashParser.d.ts +25 -0
  38. package/dist/utils/bashParser.js +103 -0
  39. package/dist/utils/configPaths.d.ts +4 -0
  40. package/dist/utils/configPaths.js +6 -0
  41. package/dist/utils/containerSetup.js +0 -9
  42. package/dist/utils/fileSearch.js +4 -2
  43. package/dist/utils/worktreeSession.d.ts +1 -1
  44. package/dist/utils/worktreeSession.js +1 -1
  45. package/dist/utils/worktreeUtils.d.ts +7 -1
  46. package/dist/utils/worktreeUtils.js +10 -4
  47. package/dist/workflow/types.d.ts +5 -0
  48. package/package.json +1 -1
  49. package/src/agent.ts +29 -10
  50. package/src/index.ts +1 -0
  51. package/src/managers/aiManager.ts +219 -61
  52. package/src/managers/permissionManager.ts +116 -168
  53. package/src/managers/pluginManager.ts +29 -0
  54. package/src/managers/subagentManager.ts +6 -0
  55. package/src/prompts/index.ts +144 -37
  56. package/src/services/aiService.ts +9 -128
  57. package/src/services/configurationService.ts +37 -0
  58. package/src/services/remoteSettingsService.ts +1 -0
  59. package/src/services/session.ts +18 -4
  60. package/src/services/taskManager.ts +46 -7
  61. package/src/tools/editTool.ts +29 -11
  62. package/src/tools/enterWorktreeTool.ts +2 -1
  63. package/src/tools/grepTool.ts +11 -2
  64. package/src/tools/writeTool.ts +43 -0
  65. package/src/types/configuration.ts +5 -0
  66. package/src/types/permissions.ts +0 -2
  67. package/src/types/processes.ts +29 -0
  68. package/src/types/workflow.ts +1 -0
  69. package/src/utils/bashParser.ts +106 -0
  70. package/src/utils/configPaths.ts +7 -0
  71. package/src/utils/containerSetup.ts +0 -11
  72. package/src/utils/fileSearch.ts +6 -2
  73. package/src/utils/worktreeSession.ts +1 -1
  74. package/src/utils/worktreeUtils.ts +14 -4
  75. package/src/workflow/types.ts +6 -0
@@ -24,7 +24,6 @@ import * as fs from "fs";
24
24
  import * as path from "path";
25
25
 
26
26
  import {
27
- COMPACT_MESSAGES_SYSTEM_PROMPT,
28
27
  WEB_CONTENT_SYSTEM_PROMPT,
29
28
  BTW_SYSTEM_PROMPT,
30
29
  type SystemPromptBlock,
@@ -169,6 +168,12 @@ export interface CallAgentOptions {
169
168
  | "required"
170
169
  | { type: "function"; function: { name: string } }; // Force tool selection
171
170
 
171
+ // Force SSE streaming independent of callback presence. Long-running
172
+ // non-interactive calls (e.g. the compaction fork) need streaming so a
173
+ // slow reasoning model isn't killed by a gateway idle timeout before the
174
+ // first byte arrives.
175
+ stream?: boolean;
176
+
172
177
  // NEW: Streaming callbacks
173
178
  onContentUpdate?: (content: string) => void;
174
179
  onToolUpdate?: (toolCall: {
@@ -319,11 +324,9 @@ export async function callAgent(
319
324
  });
320
325
 
321
326
  // Determine if streaming is needed
322
- const isStreaming = !!(
323
- onContentUpdate ||
324
- onToolUpdate ||
325
- onReasoningUpdate
326
- );
327
+ const isStreaming =
328
+ options.stream === true ||
329
+ !!(onContentUpdate || onToolUpdate || onReasoningUpdate);
327
330
 
328
331
  // Prepare API call parameters
329
332
  createParams = {
@@ -787,128 +790,6 @@ async function processStreamingResponse(
787
790
  return result;
788
791
  }
789
792
 
790
- export interface CompactMessagesOptions {
791
- // Resolved configuration
792
- gatewayConfig: GatewayConfig;
793
- modelConfig: ModelConfig;
794
-
795
- // Existing parameters
796
- messages: ChatCompletionMessageParam[];
797
- abortSignal?: AbortSignal;
798
- model?: string;
799
- customInstructions?: string;
800
- }
801
-
802
- export interface CompactMessagesResult {
803
- content: string;
804
- usage?: {
805
- prompt_tokens: number;
806
- completion_tokens: number;
807
- total_tokens: number;
808
- };
809
- }
810
-
811
- export async function compactMessages(
812
- options: CompactMessagesOptions,
813
- ): Promise<CompactMessagesResult> {
814
- const { gatewayConfig, modelConfig, messages, abortSignal } = options;
815
-
816
- // Validate model config at call time
817
- validateModelConfig(modelConfig);
818
-
819
- // Apply global 1 QPS rate limit
820
- if (
821
- process.env.NODE_ENV !== "test" ||
822
- modelConfig.model === "rate-limit-test"
823
- ) {
824
- await acquireSlot(abortSignal);
825
- }
826
-
827
- // Strip images from messages before compact API call to reduce token usage
828
- const cleanedMessages = messages.map((msg) => {
829
- // Handle user/assistant messages with array content
830
- if (Array.isArray(msg.content)) {
831
- const textParts = msg.content.filter(
832
- (part) => part.type === "text",
833
- ) as import("openai/resources.js").ChatCompletionContentPartText[];
834
- const text = textParts.map((p) => p.text).join("\n");
835
- return { ...msg, content: text || "(empty message)" };
836
- }
837
- return msg;
838
- });
839
-
840
- // Create OpenAI client with injected configuration
841
- const openai = new OpenAIClient({
842
- apiKey: gatewayConfig.apiKey,
843
- baseURL: gatewayConfig.baseURL,
844
- defaultHeaders: gatewayConfig.defaultHeaders,
845
- fetchOptions: gatewayConfig.fetchOptions,
846
- fetch: gatewayConfig.fetch,
847
- });
848
-
849
- // When a fast model override is provided, use the fast model's options
850
- // (if configured); otherwise fall back to the agent model's options.
851
- const activeExtraParams = options.model
852
- ? modelConfig.fastModelOptions || {}
853
- : modelConfig.options || {};
854
-
855
- const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
856
- temperature: 0.1,
857
- max_tokens: 8192,
858
- ...activeExtraParams,
859
- });
860
-
861
- try {
862
- const response = await openai.chat.completions.create(
863
- {
864
- ...openaiModelConfig,
865
- messages: [
866
- {
867
- role: "system",
868
- content: COMPACT_MESSAGES_SYSTEM_PROMPT,
869
- },
870
- ...cleanedMessages,
871
- {
872
- role: "user",
873
- content: options.customInstructions
874
- ? `Please create a detailed summary of the conversation so far. Pay special attention to these instructions: ${options.customInstructions}`
875
- : `Please create a detailed summary of the conversation so far.`,
876
- },
877
- ],
878
- },
879
- {
880
- signal: abortSignal,
881
- },
882
- );
883
-
884
- const content = response.choices[0]?.message?.content?.trim();
885
- if (!content) {
886
- throw new Error(
887
- "Failed to compact conversation history: Empty response from AI",
888
- );
889
- }
890
- const usage = response.usage
891
- ? {
892
- prompt_tokens: response.usage.prompt_tokens,
893
- completion_tokens: response.usage.completion_tokens,
894
- total_tokens: response.usage.total_tokens,
895
- }
896
- : undefined;
897
-
898
- return {
899
- content,
900
- usage,
901
- };
902
- } catch (error) {
903
- if ((error as Error).name === "AbortError") {
904
- logger.info("Compaction request was aborted");
905
- throw new Error("Compaction request was aborted");
906
- }
907
- logger.error("Failed to compact messages:", error);
908
- throw error;
909
- }
910
- }
911
-
912
793
  export interface ProcessWebContentOptions {
913
794
  // Resolved configuration
914
795
  gatewayConfig: GatewayConfig;
@@ -320,6 +320,23 @@ export class ConfigurationService {
320
320
  }
321
321
  }
322
322
 
323
+ // Validate worktree if present
324
+ if (config.worktree !== undefined) {
325
+ if (typeof config.worktree !== "object" || config.worktree === null) {
326
+ result.isValid = false;
327
+ result.errors.push("worktree configuration must be an object");
328
+ } else if (
329
+ config.worktree.baseRef !== undefined &&
330
+ config.worktree.baseRef !== "fresh" &&
331
+ config.worktree.baseRef !== "head"
332
+ ) {
333
+ result.isValid = false;
334
+ result.errors.push(
335
+ `Invalid worktree.baseRef: "${config.worktree.baseRef}". Must be "fresh" or "head".`,
336
+ );
337
+ }
338
+ }
339
+
323
340
  return result;
324
341
  }
325
342
 
@@ -640,6 +657,19 @@ export class ConfigurationService {
640
657
  return true;
641
658
  }
642
659
 
660
+ /**
661
+ * Resolves worktree base ref with fallbacks
662
+ * Resolution priority: settings.json > default ("fresh")
663
+ * @returns Resolved worktree base ref
664
+ */
665
+ resolveWorktreeBaseRef(): "fresh" | "head" {
666
+ const baseRef = this.currentConfiguration?.worktree?.baseRef;
667
+ if (baseRef === "head") {
668
+ return "head";
669
+ }
670
+ return "fresh";
671
+ }
672
+
643
673
  /**
644
674
  * Resolves auto-memory extraction frequency with fallbacks
645
675
  * Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
@@ -1172,6 +1202,7 @@ export function loadWaveConfigFromFile(
1172
1202
  : undefined,
1173
1203
  models: config.models || undefined,
1174
1204
  marketplaces: config.marketplaces || undefined,
1205
+ worktree: config.worktree || undefined,
1175
1206
  };
1176
1207
  } catch (error) {
1177
1208
  if (error instanceof SyntaxError) {
@@ -1320,6 +1351,11 @@ export function loadMergedWaveConfig(
1320
1351
  Object.assign(mergedConfig.marketplaces, config.marketplaces);
1321
1352
  }
1322
1353
 
1354
+ // Merge worktree (last one wins)
1355
+ if (config.worktree !== undefined) {
1356
+ mergedConfig.worktree = config.worktree;
1357
+ }
1358
+
1323
1359
  // Merge models
1324
1360
  if (config.models) {
1325
1361
  if (!mergedConfig.models) mergedConfig.models = {};
@@ -1363,5 +1399,6 @@ export function loadMergedWaveConfig(
1363
1399
  mergedConfig.models && Object.keys(mergedConfig.models).length > 0
1364
1400
  ? mergedConfig.models
1365
1401
  : undefined,
1402
+ worktree: mergedConfig.worktree,
1366
1403
  };
1367
1404
  }
@@ -326,6 +326,7 @@ export function mergeRemoteSettings(
326
326
  result.autoMemoryEnabled = remote.autoMemoryEnabled;
327
327
  if (remote.autoMemoryFrequency !== undefined)
328
328
  result.autoMemoryFrequency = remote.autoMemoryFrequency;
329
+ if (remote.worktree !== undefined) result.worktree = remote.worktree;
329
330
  if (remote.models !== undefined) result.models = remote.models;
330
331
  if (remote.marketplaces !== undefined)
331
332
  result.marketplaces = remote.marketplaces;
@@ -825,7 +825,9 @@ export async function handleSessionRestoration(
825
825
  /**
826
826
  * Load the full message thread for a session.
827
827
  * With append-only compaction, all messages are in a single file.
828
- * Returns the active messages (post-compact boundary).
828
+ * Unlike loadSessionFromJsonl, this returns every message in the file,
829
+ * including those before the last compact boundary — rewind needs the
830
+ * complete history to allow rewinding past compaction points.
829
831
  * @param currentSessionId - The ID of the current session
830
832
  * @param workdir - Working directory for the session
831
833
  * @returns Promise that resolves to an array of all messages in the thread
@@ -834,7 +836,19 @@ export async function loadFullMessageThread(
834
836
  currentSessionId: string,
835
837
  workdir: string,
836
838
  ): Promise<{ messages: Message[]; sessionIds: string[] }> {
837
- const sessionData = await loadSessionFromJsonl(currentSessionId, workdir);
838
- if (!sessionData) return { messages: [], sessionIds: [] };
839
- return { messages: sessionData.messages, sessionIds: [currentSessionId] };
839
+ const jsonlHandler = new JsonlHandler();
840
+ const filePath = await generateSessionFilePath(
841
+ currentSessionId,
842
+ workdir,
843
+ "main",
844
+ );
845
+
846
+ try {
847
+ await fs.access(filePath);
848
+ } catch {
849
+ return { messages: [], sessionIds: [] };
850
+ }
851
+
852
+ const messages = await jsonlHandler.read(filePath);
853
+ return { messages, sessionIds: [currentSessionId] };
840
854
  }
@@ -71,6 +71,7 @@ export class TaskManager extends EventEmitter {
71
71
  let lockHandle;
72
72
  const maxRetries = 100;
73
73
  const retryDelay = process.env.NODE_ENV === "test" ? 1 : 100;
74
+ const staleThreshold = 10000;
74
75
 
75
76
  await this.ensureSessionDir();
76
77
 
@@ -79,16 +80,41 @@ export class TaskManager extends EventEmitter {
79
80
  lockHandle = await fs.open(lockPath, "wx");
80
81
  break;
81
82
  } catch (error) {
82
- if ((error as NodeJS.ErrnoException).code === "EEXIST") {
83
- if (i === maxRetries - 1) {
84
- throw new Error(
85
- `Could not acquire lock for task list ${this.taskListId} after ${maxRetries} retries`,
86
- );
83
+ const code = (error as NodeJS.ErrnoException).code;
84
+
85
+ // Only EEXIST (lock held) and EPERM (Windows pending-delete window)
86
+ // are transient lock-contention errors. EACCES/ENOENT are real failures
87
+ // and must not be retried as lock competition.
88
+ if (code !== "EEXIST" && code !== "EPERM") {
89
+ throw error;
90
+ }
91
+
92
+ if (i === maxRetries - 1) {
93
+ throw new Error(
94
+ `Could not acquire lock for task list ${this.taskListId} after ${maxRetries} retries`,
95
+ );
96
+ }
97
+
98
+ // Stale recovery: if the lock holder crashed without releasing, the
99
+ // lock file remains forever. Check mtime — if older than the threshold,
100
+ // remove it. Aligns with proper-lockfile's default 10s stale detection.
101
+ // The lock file is empty (no PID content), so mtime is the only signal.
102
+ if (
103
+ code === "EEXIST" &&
104
+ (await this.isLockStale(lockPath, staleThreshold))
105
+ ) {
106
+ logger.warn(
107
+ `TaskManager: removing stale lock for task list ${this.taskListId}`,
108
+ );
109
+ try {
110
+ await fs.unlink(lockPath);
111
+ } catch {
112
+ // Another waiter may have already removed it — retry anyway
87
113
  }
88
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
89
114
  continue;
90
115
  }
91
- throw error;
116
+
117
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
92
118
  }
93
119
  }
94
120
 
@@ -109,6 +135,19 @@ export class TaskManager extends EventEmitter {
109
135
  }
110
136
  }
111
137
 
138
+ private async isLockStale(
139
+ lockPath: string,
140
+ threshold: number,
141
+ ): Promise<boolean> {
142
+ try {
143
+ const stats = await fs.stat(lockPath);
144
+ return Date.now() - stats.mtimeMs > threshold;
145
+ } catch {
146
+ // Lock was removed between our EEXIST and stat — not stale, just retry
147
+ return false;
148
+ }
149
+ }
150
+
112
151
  private validateTask(task: Task): void {
113
152
  if (!task.id || typeof task.id !== "string")
114
153
  throw new Error("Invalid task ID");
@@ -114,9 +114,14 @@ Usage:
114
114
 
115
115
  // Enforce read-before-edit: the file must have been read or written first.
116
116
  // readFileState is populated by Read, Write, and Edit tools — single source
117
- // of truth, aligned with Claude Code's readFileState approach.
117
+ // of truth, aligned with Claude Code's readFileState approach. Skipped in
118
+ // plan mode: permissionManager enforces a plan-file-only gate whose denial
119
+ // message must surface instead of being masked by a read-state rejection.
118
120
  const resolvedPath = resolvePath(filePath, context.workdir);
119
- if (!context.readFileState?.has(resolvedPath)) {
121
+ if (
122
+ context.permissionMode !== "plan" &&
123
+ !context.readFileState?.has(resolvedPath)
124
+ ) {
120
125
  return {
121
126
  success: false,
122
127
  content: "",
@@ -137,18 +142,31 @@ Usage:
137
142
  };
138
143
  }
139
144
 
140
- // Staleness check: file must not have been modified since last Read
141
- if (context.readFileState) {
145
+ // Staleness check (aligned with Claude Code): only flag when the file got
146
+ // newer since last read. For full reads, a content-hash fallback avoids
147
+ // false positives when mtime changed but content didn't (git checkout,
148
+ // editor round-trip save, cloud sync, antivirus). Partial reads get no
149
+ // fallback since only a slice was cached. Skipped in plan mode (see
150
+ // read-before-edit note above) so the plan-file-only denial wins.
151
+ if (context.permissionMode !== "plan" && context.readFileState) {
142
152
  const state = context.readFileState.get(resolvedPath);
143
153
  if (state) {
144
154
  const currentStats = await stat(resolvedPath);
145
- if (currentStats.mtime.getTime() !== state.mtime) {
146
- return {
147
- success: false,
148
- content: "",
149
- error:
150
- "File has been unexpectedly modified since last read. Read it again before editing it.",
151
- };
155
+ if (currentStats.mtime.getTime() > state.mtime) {
156
+ const isFullRead =
157
+ state.offset === undefined && state.limit === undefined;
158
+ const contentUnchanged =
159
+ isFullRead &&
160
+ createHash("sha256").update(originalContent).digest("hex") ===
161
+ state.hash;
162
+ if (!contentUnchanged) {
163
+ return {
164
+ success: false,
165
+ content: "",
166
+ error:
167
+ "File has been unexpectedly modified since last read. Read it again before editing it.",
168
+ };
169
+ }
152
170
  }
153
171
  }
154
172
  }
@@ -102,7 +102,8 @@ export const enterWorktreeTool: ToolPlugin = {
102
102
  }
103
103
 
104
104
  // Create the worktree (captures originalHeadCommit internally)
105
- const worktreeInfo = createWorktree(name, mainRepoRoot);
105
+ const baseRef = context.aiManager?.getWorktreeBaseRef?.();
106
+ const worktreeInfo = createWorktree(name, mainRepoRoot, { baseRef });
106
107
 
107
108
  // Build session state
108
109
  const session: WorktreeSession = {
@@ -2,6 +2,7 @@ import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
2
  import { spawn } from "child_process";
3
3
  import { rgPath } from "../utils/ripgrep.js";
4
4
  import { getDisplayPath } from "../utils/path.js";
5
+ import { logger } from "../utils/globalLogger.js";
5
6
  import {
6
7
  GREP_TOOL_NAME,
7
8
  BASH_TOOL_NAME,
@@ -235,8 +236,10 @@ export const grepTool: ToolPlugin = {
235
236
 
236
237
  const result = await executeCommand(rgPath, rgArgs, workdir);
237
238
 
238
- if (result.error && result.exitCode !== 1) {
239
- // rg returns 1 for no matches, not an error
239
+ // Only a process-level spawn failure (exitCode null) is a hard error.
240
+ // rg exit 2 means some files were unreadable (e.g. device-name files
241
+ // like "nul" on Windows); stdout still holds usable partial results.
242
+ if (result.exitCode === null) {
240
243
  return {
241
244
  success: false,
242
245
  content: "",
@@ -244,6 +247,12 @@ export const grepTool: ToolPlugin = {
244
247
  };
245
248
  }
246
249
 
250
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
251
+ logger.debug(
252
+ `ripgrep exited with code ${result.exitCode}, keeping partial results: ${result.stderr.trim()}`,
253
+ );
254
+ }
255
+
247
256
  const output = result.stdout.trim();
248
257
  if (!output) {
249
258
  return {
@@ -86,6 +86,49 @@ Usage:
86
86
  isExistingFile = false;
87
87
  }
88
88
 
89
+ // Read-before-write + staleness guards (aligned with Claude Code).
90
+ // Only enforced for existing files when readFileState is available
91
+ // (production always injects it; new-file creation always bypasses).
92
+ // Grep does not register a file as read, so Grep-then-Write on an
93
+ // existing file is still rejected. Staleness uses the same `>` + full-
94
+ // read content-hash fallback as editTool to avoid false positives from
95
+ // git checkout / editor round-trip save / cloud sync / antivirus.
96
+ // Plan mode is excluded: it has its own stricter write gate (plan-file-
97
+ // only, enforced in permissionManager) whose denial message must surface
98
+ // instead of being masked by a read-state rejection.
99
+ if (
100
+ isExistingFile &&
101
+ context.readFileState &&
102
+ context.permissionMode !== "plan"
103
+ ) {
104
+ const state = context.readFileState.get(resolvedPath);
105
+ if (!state) {
106
+ return {
107
+ success: false,
108
+ content: "",
109
+ error:
110
+ "File has not been read yet. Read it first before writing to it.",
111
+ };
112
+ }
113
+ const currentStats = await stat(resolvedPath);
114
+ if (currentStats.mtime.getTime() > state.mtime) {
115
+ const isFullRead =
116
+ state.offset === undefined && state.limit === undefined;
117
+ const contentUnchanged =
118
+ isFullRead &&
119
+ createHash("sha256").update(originalContent).digest("hex") ===
120
+ state.hash;
121
+ if (!contentUnchanged) {
122
+ return {
123
+ success: false,
124
+ content: "",
125
+ error:
126
+ "File has been unexpectedly modified since last read. Read it again before writing to it.",
127
+ };
128
+ }
129
+ }
130
+ }
131
+
89
132
  // Check if overwriting existing file but content is the same
90
133
  if (isExistingFile && originalContent === content) {
91
134
  return {
@@ -54,6 +54,11 @@ export interface WaveConfiguration {
54
54
  monitoring?: {
55
55
  telemetry?: Partial<TelemetryConfig>;
56
56
  };
57
+ /** Worktree configuration */
58
+ worktree?: {
59
+ /** Base ref for new worktrees: "fresh" (origin/<default-branch>, default) | "head" (local HEAD) */
60
+ baseRef?: "fresh" | "head";
61
+ };
57
62
  }
58
63
 
59
64
  /**
@@ -35,8 +35,6 @@ export interface PermissionDecision {
35
35
  newPermissionMode?: PermissionMode;
36
36
  /** Signal to persist a new allowed rule */
37
37
  newPermissionRule?: string;
38
- /** Signal to clear the conversation context and proceed with the plan */
39
- clearContext?: boolean;
40
38
  }
41
39
 
42
40
  /** Callback function for custom permission logic */
@@ -59,6 +59,35 @@ export type BackgroundTask =
59
59
  | BackgroundSubagent
60
60
  | BackgroundWorkflow;
61
61
 
62
+ /**
63
+ * Serializable summary of a BackgroundTask, used for notifications where the
64
+ * full stdout/stderr and non-serializable process/onStop fields must be
65
+ * stripped to control payload size. Output is fetched on demand via
66
+ * getBackgroundTaskOutput.
67
+ */
68
+ export interface BackgroundTaskSummary {
69
+ id: string;
70
+ type: BackgroundTaskType;
71
+ status: BackgroundTaskStatus;
72
+ startTime: number;
73
+ endTime?: number;
74
+ command?: string;
75
+ description?: string;
76
+ exitCode?: number;
77
+ runtime?: number;
78
+ outputPath?: string;
79
+ }
80
+
81
+ /** Output snapshot returned by getBackgroundTaskOutput. */
82
+ export interface BackgroundTaskOutput {
83
+ stdout: string;
84
+ stderr: string;
85
+ status: BackgroundTaskStatus;
86
+ outputPath?: string;
87
+ type: BackgroundTaskType;
88
+ exitCode?: number;
89
+ }
90
+
62
91
  export interface ForegroundTask {
63
92
  id: string;
64
93
  backgroundHandler: () => Promise<void>;
@@ -2,4 +2,5 @@ export type {
2
2
  WorkflowRun,
3
3
  WorkflowMeta,
4
4
  WorkflowPhaseState,
5
+ SerializableWorkflowRun,
5
6
  } from "../workflow/types.js";