@sema-agent/core 2.3.0 → 2.5.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.
- package/dist/agents/send-message-tool.d.ts +4 -0
- package/dist/agents/send-message-tool.js +37 -24
- package/dist/agents/subagent.js +279 -128
- package/dist/brain/errors.d.ts +1 -0
- package/dist/brain/errors.js +14 -0
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-compaction.d.ts +2 -0
- package/dist/core/auto-compaction.js +2 -1
- package/dist/core/context-edit.js +2 -1
- package/dist/core/mcp.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +5 -0
- package/dist/core/runner/prepare-task.js +41 -2
- package/dist/core/runner/runtask.js +39 -9
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +16 -5
- package/dist/core/runner/tool-output-projection.js +2 -1
- package/dist/core/session-reconcile.d.ts +1 -0
- package/dist/core/session-reconcile.js +40 -20
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
- package/dist/core/store-contracts/contract-harness.d.ts +6 -0
- package/dist/core/store-contracts/contract-harness.js +16 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
- package/dist/core/store-contracts/contract-kit-version.js +2 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
- package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
- package/dist/core/store-contracts/session-repo-contract.js +36 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-registry-agent.d.ts +6 -0
- package/dist/core/task-registry-agent.js +24 -1
- package/dist/core/task-registry-monitor.js +6 -6
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry-shared.js +1 -1
- package/dist/core/task-registry.d.ts +8 -0
- package/dist/core/task-registry.js +59 -4
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/types.d.ts +10 -3
- package/dist/engine/compaction/compaction.d.ts +5 -0
- package/dist/engine/compaction/compaction.js +68 -2
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +53 -3
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/messages.js +11 -3
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/engine/session/import-validate.js +30 -1
- package/dist/engine/session/session.js +7 -5
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/builtin-workflows.d.ts +1 -1
- package/dist/orchestration/builtin-workflows.js +11 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -1
- package/dist/orchestration/workflow-governance.js +24 -4
- package/dist/orchestration/workflow-primitives.js +7 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +41 -3
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +59 -22
- package/dist/tools/fs/fs-read.js +22 -11
- package/dist/tools/fs/fs-search-tools.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +20 -7
- package/dist/tools/fs/fs-shared.js +17 -3
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/index.d.ts +2 -0
- package/dist/tools/fs/index.js +7 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +10 -0
- package/dist/tools/fs/safety.js +15 -1
- package/dist/tools/monitor.js +18 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- package/package.json +1 -1
package/dist/tools/fs/index.js
CHANGED
|
@@ -35,7 +35,10 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
35
35
|
tools.push(createGrepTool(env, rootCanonical, additionalRoots), createGlobTool(env, rootCanonical, additionalRoots), createRepoMapTool(env, rootCanonical, additionalRoots));
|
|
36
36
|
if (includeShell) {
|
|
37
37
|
tools.push(readOnly
|
|
38
|
-
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), opts.execClamp
|
|
38
|
+
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), opts.execClamp, {
|
|
39
|
+
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
40
|
+
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
41
|
+
})
|
|
39
42
|
: createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
|
|
40
43
|
taskRegistry: opts.taskRegistry,
|
|
41
44
|
taskOwner: opts.taskOwner,
|
|
@@ -46,6 +49,9 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
46
49
|
execClamp: opts.execClamp,
|
|
47
50
|
...(opts.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: opts.autoBackgroundOnTimeout } : {}),
|
|
48
51
|
...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}),
|
|
52
|
+
...(additionalRoots !== undefined ? { additionalRoots } : {}),
|
|
53
|
+
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
54
|
+
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
49
55
|
}));
|
|
50
56
|
if (!readOnly && mountBackgroundTaskTools && hasBackgroundShell(env)) {
|
|
51
57
|
const sessionAxis = opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
-
import { resolveKey, violationText } from "./safety.js";
|
|
3
|
+
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
4
4
|
import { buildIgnore, walk, walkIsPartial } from "./search.js";
|
|
5
5
|
const DEFAULT_MAX_CHARS = 16_000;
|
|
6
6
|
const DEFAULT_MAX_FILES = 400;
|
|
@@ -101,7 +101,7 @@ export function createRepoMapTool(env, rootCanonical, additionalRoots) {
|
|
|
101
101
|
if (a.path !== undefined) {
|
|
102
102
|
const r = await resolveKey(env, rootCanonical, a.path, signal, rootCanonical, additionalRoots);
|
|
103
103
|
if (!r.ok)
|
|
104
|
-
return errorResult(violationText("RepoMap", r.violation));
|
|
104
|
+
return errorResult(violationText("RepoMap", r.violation), violationDetails(r.violation));
|
|
105
105
|
start = r.key;
|
|
106
106
|
}
|
|
107
107
|
const maxChars = Math.max(1, Math.floor(a.max_chars ?? DEFAULT_MAX_CHARS));
|
|
@@ -18,6 +18,8 @@ export declare function sha256(content: string): string;
|
|
|
18
18
|
export interface FsViolation {
|
|
19
19
|
code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid";
|
|
20
20
|
partialView?: true;
|
|
21
|
+
target?: string;
|
|
22
|
+
roots?: readonly string[];
|
|
21
23
|
message: string;
|
|
22
24
|
}
|
|
23
25
|
export declare function isBlockedDevicePath(key: string): boolean;
|
|
@@ -43,6 +45,14 @@ export declare function canonicalizeTarget(env: ExecutionEnv, path: string, sign
|
|
|
43
45
|
unresolvedSymlink?: true;
|
|
44
46
|
}>;
|
|
45
47
|
export declare function violationText(toolName: string, v: FsViolation): string;
|
|
48
|
+
export declare function violationDetails(v: FsViolation): {
|
|
49
|
+
type: "path_not_in_root";
|
|
50
|
+
code: "path_not_in_root";
|
|
51
|
+
target: string;
|
|
52
|
+
roots: readonly string[];
|
|
53
|
+
} | undefined;
|
|
54
|
+
export declare function withinAnyRoot(rootsCanonical: readonly string[], p: string): boolean;
|
|
55
|
+
export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s) and the deployment permits it, use the Bash tool \u2014 it is not confined by this fence, though every call remains subject to the deployment's approval policy. Or ask for the directory to be added to additionalDirectories.)";
|
|
46
56
|
export declare function requireRead(state: ReadFileState, key: string): FsViolation | undefined;
|
|
47
57
|
export declare const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused \u2014 read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
|
|
48
58
|
export declare const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view \u2014 the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -175,7 +175,12 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
175
175
|
const roots = [rootCanonical, ...(additionalRootsCanonical ?? [])];
|
|
176
176
|
return {
|
|
177
177
|
ok: false,
|
|
178
|
-
violation: {
|
|
178
|
+
violation: {
|
|
179
|
+
code: "path_not_in_root",
|
|
180
|
+
message: `path "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}resolves outside the allowed root${roots.length > 1 ? "s" : ""} (${roots.join(", ")}); access denied. ${PATH_NOT_IN_ROOT_ESCAPE_HINT}`,
|
|
181
|
+
target: key,
|
|
182
|
+
roots,
|
|
183
|
+
},
|
|
179
184
|
};
|
|
180
185
|
}
|
|
181
186
|
return { ok: true, key };
|
|
@@ -241,6 +246,15 @@ async function canonicalizeNewPath(env, abs, signal) {
|
|
|
241
246
|
export function violationText(toolName, v) {
|
|
242
247
|
return `Error (${toolName}): ${v.message}`;
|
|
243
248
|
}
|
|
249
|
+
export function violationDetails(v) {
|
|
250
|
+
return v.code === "path_not_in_root" && v.target !== undefined
|
|
251
|
+
? { type: "path_not_in_root", code: "path_not_in_root", target: v.target, roots: v.roots ?? [] }
|
|
252
|
+
: undefined;
|
|
253
|
+
}
|
|
254
|
+
export function withinAnyRoot(rootsCanonical, p) {
|
|
255
|
+
return rootsCanonical.some((r) => within(r, p));
|
|
256
|
+
}
|
|
257
|
+
export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s) and the deployment permits it, use the Bash tool — it is not confined by this fence, though every call remains subject to the deployment's approval policy. Or ask for the directory to be added to additionalDirectories.)";
|
|
244
258
|
export function requireRead(state, key) {
|
|
245
259
|
const entry = state.get(key);
|
|
246
260
|
if (entry === undefined || entry.isPartialView) {
|
package/dist/tools/monitor.js
CHANGED
|
@@ -91,17 +91,31 @@ export function createMonitorTool(env, opts) {
|
|
|
91
91
|
return errorResult(`Error (Monitor): the watch process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated.`);
|
|
92
92
|
}
|
|
93
93
|
const bgTimeout = env.backgroundCapabilities.defaultBgTimeoutSec;
|
|
94
|
+
const wasClamped = timeout_ms !== undefined && timeout_ms !== timeoutMs;
|
|
95
|
+
const clampNote = !isPersistent && wasClamped ? ` (clamped to ${timeoutMs}ms from your requested ${timeout_ms}ms)` : "";
|
|
96
|
+
const timeoutIgnoredNote = isPersistent && timeout_ms !== undefined ? ` (the timeout_ms you passed is ignored because persistent is true)` : "";
|
|
94
97
|
const lifetime = isPersistent
|
|
95
98
|
? typeof bgTimeout === "number"
|
|
96
|
-
? `It is persistent: no watch timeout of its own, but the execution environment kills background processes after ${bgTimeout}s; it runs until then, until you stop it, or until the session ends.`
|
|
97
|
-
:
|
|
98
|
-
: `It will be killed after ${timeoutMs}ms if still running.`;
|
|
99
|
-
|
|
99
|
+
? `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but the execution environment kills background processes after ${bgTimeout}s; it runs until then, until you stop it, or until the session ends.`
|
|
100
|
+
: `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but the execution environment's background time budget still applies; it runs until then, until you stop it, or until the session ends.`
|
|
101
|
+
: `It will be killed after ${timeoutMs}ms${clampNote} if still running.`;
|
|
102
|
+
const content = onNotify !== undefined
|
|
100
103
|
? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
|
|
101
104
|
`~200ms are batched); you'll get a final notification with the exit code when it ends — do not poll. ` +
|
|
102
105
|
`${lifetime} TaskOutput("${taskId}") reads the full spool (stdout+stderr); TaskStop("${taskId}") stops the watch.`
|
|
103
106
|
: `Monitoring in background; task_id=${taskId}. No notification channel is wired in this mount — read output with ` +
|
|
104
107
|
`TaskOutput("${taskId}") (re-readable spool, stdout+stderr) and stop with TaskStop("${taskId}"). ${lifetime}`;
|
|
108
|
+
return {
|
|
109
|
+
content,
|
|
110
|
+
details: {
|
|
111
|
+
type: "monitor-start",
|
|
112
|
+
taskId,
|
|
113
|
+
persistent: isPersistent,
|
|
114
|
+
...(isPersistent
|
|
115
|
+
? { timeoutIgnored: timeout_ms !== undefined }
|
|
116
|
+
: { timeoutMs, ...(timeout_ms !== undefined ? { requestedTimeoutMs: timeout_ms } : {}), wasClamped }),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
105
119
|
},
|
|
106
120
|
});
|
|
107
121
|
}
|
package/dist/tools/web.js
CHANGED
|
@@ -594,7 +594,7 @@ function classifySearchFailure(message) {
|
|
|
594
594
|
if (code === 408 || /\btimed? ?out\b|\betimedout\b/.test(m)) {
|
|
595
595
|
return { retryable: true, hint: "This looks like a timeout — a retry may succeed; a narrower query tends to return faster." };
|
|
596
596
|
}
|
|
597
|
-
if (/\
|
|
597
|
+
if (/\beconnrefused\b|\benotfound\b|\beai_again\b|\beconnreset\b|\bepipe\b|\bfetch failed\b|socket hang up|\bnetwork\b|\bdns\b/.test(m)) {
|
|
598
598
|
return { retryable: true, hint: "This is a network/transport fault, not a rejection of the query — one retry is reasonable; if it repeats, treat the search backend as unavailable and continue without it." };
|
|
599
599
|
}
|
|
600
600
|
if (code !== undefined && code >= 500 && code <= 599) {
|
|
@@ -751,6 +751,7 @@ export function createWebSearchTool(config) {
|
|
|
751
751
|
const domainFiltered = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains));
|
|
752
752
|
const droppedByDomain = usable.length - domainFiltered.length;
|
|
753
753
|
const kept = domainFiltered.slice(0, max);
|
|
754
|
+
const droppedByMax = domainFiltered.length - kept.length;
|
|
754
755
|
const schemeAssumed = kept.filter((r) => r.schemeAssumed).length;
|
|
755
756
|
let clipped = false;
|
|
756
757
|
const shown = kept.map((r) => {
|
|
@@ -771,6 +772,9 @@ export function createWebSearchTool(config) {
|
|
|
771
772
|
const domainNote = droppedByDomain > 0
|
|
772
773
|
? `\n\n[WebSearch: ${droppedByDomain} result(s) were dropped by the ${allowed_domains?.length ? "allowed_domains" : "blocked_domains"} filter passed with this call — the backend returned them, this tool removed them. Relax or drop the filter to see them.]`
|
|
773
774
|
: "";
|
|
775
|
+
const maxResultsNote = droppedByMax > 0
|
|
776
|
+
? `\n\n[WebSearch: showing ${kept.length} of ${domainFiltered.length} results — the rest were cut off by this deployment's maxResults limit (${max}).]`
|
|
777
|
+
: "";
|
|
774
778
|
const clipNote = clipped
|
|
775
779
|
? "\n\n[WebSearch: one or more result fields exceeded the display limits and were truncated — treat the text above as an excerpt, not the backend's full result.]"
|
|
776
780
|
: "";
|
|
@@ -780,7 +784,7 @@ export function createWebSearchTool(config) {
|
|
|
780
784
|
const tailReminder = shown.length > 0
|
|
781
785
|
? "REMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks."
|
|
782
786
|
: "REMINDER: this search returned ZERO usable sources — there is nothing above to cite. Do not fabricate sources, URLs or citations; tell the user the search returned no usable results (the notes above say why, when there is a why) and answer from what you already know, or try a different query.";
|
|
783
|
-
const modelText = `${fenced}${dropNote}${domainNote}${schemeNote}${clipNote}\n\n${tailReminder}`;
|
|
787
|
+
const modelText = `${fenced}${dropNote}${domainNote}${maxResultsNote}${schemeNote}${clipNote}\n\n${tailReminder}`;
|
|
784
788
|
return {
|
|
785
789
|
content: modelText,
|
|
786
790
|
details: {
|
package/dist/tools/worktree.js
CHANGED
|
@@ -164,11 +164,14 @@ export function createWorktreeTools(env, opts) {
|
|
|
164
164
|
const originalCwd = previous ? previous.originalCwd : cwdRef.current;
|
|
165
165
|
session.current = { worktreeDir: target, originalCwd, baseSha: head.value.stdout.trim(), entered: true };
|
|
166
166
|
cwdRef.current = target;
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
return {
|
|
168
|
+
content: `Switched into the existing worktree at ${target}${previous ? ` (the previous worktree at ${previous.worktreeDir} was left on disk, untouched)` : ""}. ` +
|
|
169
|
+
"The session's working directory is now this worktree — subsequent commands and relative paths " +
|
|
170
|
+
"operate there. Note this is a cwd switch, not a sandbox: file containment stays anchored at the " +
|
|
171
|
+
"task root. ExitWorktree will restore the original directory but will NOT remove a worktree " +
|
|
172
|
+
"entered via `path`.",
|
|
173
|
+
details: { type: "worktree", action: "enter", worktreeDir: target },
|
|
174
|
+
};
|
|
172
175
|
}
|
|
173
176
|
const base = await resolveBaseCommit(env, repoRoot, baseRef ?? "fresh");
|
|
174
177
|
if ("error" in base)
|
|
@@ -195,11 +198,14 @@ export function createWorktreeTools(env, opts) {
|
|
|
195
198
|
baseSha: base.sha,
|
|
196
199
|
};
|
|
197
200
|
cwdRef.current = createdDir;
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
return {
|
|
202
|
+
content: `Created worktree at ${createdDir} (base: ${base.label}${base.note ?? ""}). ` +
|
|
203
|
+
"The session's working directory is now the worktree — subsequent commands and relative paths " +
|
|
204
|
+
"operate there. Note this is a cwd switch, not a sandbox: file containment stays anchored at the " +
|
|
205
|
+
"task root, so the rest of the repository remains reachable. " +
|
|
206
|
+
"Use ExitWorktree to leave (an unchanged worktree is removed; a changed one is kept).",
|
|
207
|
+
details: { type: "worktree", action: "create", worktreeDir: createdDir, base: base.label },
|
|
208
|
+
};
|
|
203
209
|
},
|
|
204
210
|
});
|
|
205
211
|
const exit = defineTool({
|
|
@@ -245,16 +251,22 @@ export function createWorktreeTools(env, opts) {
|
|
|
245
251
|
cwdRef.current = restoredTo;
|
|
246
252
|
if (active.entered) {
|
|
247
253
|
const discardNote = discardChanges === true ? " (`discardChanges` does not apply to a worktree this session did not create)" : "";
|
|
248
|
-
return
|
|
249
|
-
`
|
|
250
|
-
|
|
254
|
+
return {
|
|
255
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
256
|
+
`The worktree at ${active.worktreeDir} was entered via \`path\` (not created by this session) and ` +
|
|
257
|
+
`was left on disk, untouched${discardNote}.`,
|
|
258
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true },
|
|
259
|
+
};
|
|
251
260
|
}
|
|
252
261
|
const wtThere = await env.exists(active.worktreeDir);
|
|
253
262
|
if (wtThere.ok && !wtThere.value) {
|
|
254
263
|
await pruneWorktrees(env, repoRoot).catch(() => undefined);
|
|
255
|
-
return
|
|
256
|
-
`
|
|
257
|
-
|
|
264
|
+
return {
|
|
265
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
266
|
+
`The worktree at ${active.worktreeDir} was already gone from disk (removed externally, not by this ` +
|
|
267
|
+
"session — any uncommitted work in it is lost); stale git worktree metadata was pruned.",
|
|
268
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: false },
|
|
269
|
+
};
|
|
258
270
|
}
|
|
259
271
|
let changed = true;
|
|
260
272
|
let changeDetail = "could not verify the worktree state — keeping it to be safe";
|
|
@@ -278,10 +290,13 @@ export function createWorktreeTools(env, opts) {
|
|
|
278
290
|
const removeHint = wantsRemove && keep !== true
|
|
279
291
|
? " To remove it anyway and permanently discard these changes, re-invoke with discard_changes: true."
|
|
280
292
|
: "";
|
|
281
|
-
return
|
|
282
|
-
`
|
|
283
|
-
|
|
284
|
-
|
|
293
|
+
return {
|
|
294
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
295
|
+
`The worktree was KEPT at ${active.worktreeDir} (${why}) — inspect or merge it there, and remove it ` +
|
|
296
|
+
"later with `git worktree remove` if no longer needed." +
|
|
297
|
+
removeHint,
|
|
298
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true },
|
|
299
|
+
};
|
|
285
300
|
}
|
|
286
301
|
await env.exec(`git worktree remove --force ${shq(active.worktreeDir)}`, { cwd: repoRoot }).catch(() => undefined);
|
|
287
302
|
await pruneWorktrees(env, repoRoot).catch(() => undefined);
|
|
@@ -291,12 +306,18 @@ export function createWorktreeTools(env, opts) {
|
|
|
291
306
|
: changeDetail;
|
|
292
307
|
const stateClause = changed ? `had ${detailForDiscard} (discarded — discardChanges was set)` : "had no changes";
|
|
293
308
|
if (stillThere.ok && stillThere.value) {
|
|
294
|
-
return
|
|
295
|
-
`
|
|
296
|
-
|
|
309
|
+
return {
|
|
310
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
311
|
+
`The worktree at ${active.worktreeDir} ${changed ? "was marked for discard" : "had no changes"} but could NOT be removed — it is still on disk; ` +
|
|
312
|
+
"remove it manually with `git worktree remove --force` if no longer needed.",
|
|
313
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: true },
|
|
314
|
+
};
|
|
297
315
|
}
|
|
298
|
-
return
|
|
299
|
-
`
|
|
316
|
+
return {
|
|
317
|
+
content: `Exited the worktree session; working directory restored to ${restoredTo}${restoreNote}. ` +
|
|
318
|
+
`The worktree at ${active.worktreeDir} ${stateClause} and was removed.`,
|
|
319
|
+
details: { type: "worktree", action: "exit", worktreeDir: active.worktreeDir, kept: false },
|
|
320
|
+
};
|
|
300
321
|
},
|
|
301
322
|
});
|
|
302
323
|
return [enter, exit];
|