@sema-agent/core 1.451.0 → 1.452.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/observer.js +3 -0
- package/dist/agents/subagent.js +51 -12
- package/dist/core/ask-question.js +5 -5
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +5 -1
- package/dist/core/exec-output-tail.d.ts +18 -1
- package/dist/core/exec-output-tail.js +38 -5
- package/dist/core/lsp-session.d.ts +1 -0
- package/dist/core/lsp-session.js +24 -6
- package/dist/core/lsp.d.ts +10 -0
- package/dist/core/lsp.js +63 -6
- package/dist/core/mailbox-store.d.ts +3 -2
- package/dist/core/mailbox-store.js +19 -4
- package/dist/core/memory.js +1 -1
- package/dist/core/runner/prepare-task.js +10 -1
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/session-reconcile.js +5 -2
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-registry.d.ts +15 -9
- package/dist/core/task-registry.js +290 -53
- package/dist/core/tool-result-store.js +2 -2
- package/dist/core/tools.d.ts +5 -0
- package/dist/core/tools.js +3 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/workflow-journal-store.d.ts +2 -0
- package/dist/core/workflow-journal-store.js +14 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
- package/dist/engine/execution-env/node-execution-env.js +130 -20
- package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
- package/dist/engine/lsp/node-lsp-manager.js +22 -5
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
- package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +29 -6
- package/dist/orchestration/workflow.d.ts +9 -0
- package/dist/orchestration/workflow.js +80 -5
- package/dist/stores/cc/mailbox-store.js +6 -1
- package/dist/stores/file/background-agent-store.js +3 -2
- package/dist/stores/file/mailbox-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.js +9 -7
- package/dist/tools/fs/encoding.d.ts +5 -0
- package/dist/tools/fs/encoding.js +6 -0
- package/dist/tools/fs/index.js +184 -120
- package/dist/tools/fs/notebook.d.ts +43 -0
- package/dist/tools/fs/notebook.js +141 -0
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/search.js +141 -12
- package/dist/tools/gitea-issue.js +4 -2
- package/dist/tools/monitor.js +12 -8
- package/dist/tools/scheduler-tools.js +16 -16
- package/dist/tools/task-list.js +34 -12
- package/dist/tools/web.d.ts +2 -0
- package/dist/tools/web.js +105 -19
- package/dist/tools/worktree.js +14 -14
- package/package.json +1 -1
package/dist/tools/task-list.js
CHANGED
|
@@ -326,22 +326,41 @@ export function createTaskListTools(store) {
|
|
|
326
326
|
t.metadata[k] = v;
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
|
+
const ignoredDeps = [];
|
|
330
|
+
let linkedBlocks = false;
|
|
331
|
+
let linkedBlockedBy = false;
|
|
329
332
|
for (const b of a.addBlocks ?? []) {
|
|
330
|
-
if (!t.blocks.includes(b))
|
|
331
|
-
t.blocks.push(b);
|
|
332
333
|
const other = b === id ? t : await tx.get(b);
|
|
333
|
-
if (
|
|
334
|
+
if (!other) {
|
|
335
|
+
if (!ignoredDeps.includes(b))
|
|
336
|
+
ignoredDeps.push(b);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!t.blocks.includes(b)) {
|
|
340
|
+
t.blocks.push(b);
|
|
341
|
+
linkedBlocks = true;
|
|
342
|
+
}
|
|
343
|
+
if (!other.blockedBy.includes(id)) {
|
|
334
344
|
other.blockedBy.push(id);
|
|
345
|
+
linkedBlocks = true;
|
|
335
346
|
if (other !== t)
|
|
336
347
|
await tx.set(b, other);
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
350
|
for (const b of a.addBlockedBy ?? []) {
|
|
340
|
-
if (!t.blockedBy.includes(b))
|
|
341
|
-
t.blockedBy.push(b);
|
|
342
351
|
const other = b === id ? t : await tx.get(b);
|
|
343
|
-
if (
|
|
352
|
+
if (!other) {
|
|
353
|
+
if (!ignoredDeps.includes(b))
|
|
354
|
+
ignoredDeps.push(b);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!t.blockedBy.includes(b)) {
|
|
358
|
+
t.blockedBy.push(b);
|
|
359
|
+
linkedBlockedBy = true;
|
|
360
|
+
}
|
|
361
|
+
if (!other.blocks.includes(id)) {
|
|
344
362
|
other.blocks.push(id);
|
|
363
|
+
linkedBlockedBy = true;
|
|
345
364
|
if (other !== t)
|
|
346
365
|
await tx.set(b, other);
|
|
347
366
|
}
|
|
@@ -354,10 +373,13 @@ export function createTaskListTools(store) {
|
|
|
354
373
|
a.activeForm !== undefined ? "activeForm" : "",
|
|
355
374
|
a.owner !== undefined ? "owner" : "",
|
|
356
375
|
a.metadata ? "metadata" : "",
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
linkedBlocks ? "blocks" : "",
|
|
377
|
+
linkedBlockedBy ? "blockedBy" : "",
|
|
359
378
|
].filter(Boolean);
|
|
360
|
-
|
|
379
|
+
const ignoredNote = ignoredDeps.length
|
|
380
|
+
? ` (ignored: no such task ${ignoredDeps.map((x) => `#${x}`).join(", ")})`
|
|
381
|
+
: "";
|
|
382
|
+
return { content: `Updated task #${id}${changed.length ? ` ${changed.join(", ")}` : ""}${ignoredNote}`, details: { type: "task", task: snapTask(t) } };
|
|
361
383
|
}),
|
|
362
384
|
};
|
|
363
385
|
const taskList = {
|
|
@@ -393,12 +415,12 @@ export function createTaskListTools(store) {
|
|
|
393
415
|
if (all.length === 0) {
|
|
394
416
|
return { content: "The task list is empty.", details: { type: "task-list", tasks: [] } };
|
|
395
417
|
}
|
|
396
|
-
const
|
|
418
|
+
const openIds = new Set(all.filter((x) => x.status !== "completed").map((x) => x.id));
|
|
397
419
|
const lines = all.map((t) => {
|
|
398
|
-
const liveBlockers = t.blockedBy.filter((b) =>
|
|
420
|
+
const liveBlockers = t.blockedBy.filter((b) => openIds.has(b));
|
|
399
421
|
return `#${t.id} [${t.status}] ${t.subject}${t.owner ? ` (owner: ${t.owner})` : ""}${liveBlockers.length ? ` (blocked by: ${liveBlockers.join(", ")})` : ""}`;
|
|
400
422
|
});
|
|
401
|
-
const open =
|
|
423
|
+
const open = openIds.size;
|
|
402
424
|
return {
|
|
403
425
|
content: `${lines.join("\n")}\n(${all.length} total, ${open} open)`,
|
|
404
426
|
details: { type: "task-list", tasks: all.map(snapTask) },
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -25,5 +25,7 @@ export interface WebSearchConfig {
|
|
|
25
25
|
snippet: string;
|
|
26
26
|
}>>;
|
|
27
27
|
maxResults?: number;
|
|
28
|
+
timeoutMs?: number;
|
|
28
29
|
}
|
|
30
|
+
export declare function clipCodePoints(s: string, max: number): string;
|
|
29
31
|
export declare function createWebSearchTool(config: WebSearchConfig): ToolSpec;
|
package/dist/tools/web.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { defineTool } from "../core/tools.js";
|
|
2
|
+
import { defineTool, errorResult } from "../core/tools.js";
|
|
3
3
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
4
|
+
import { redactSecrets } from "../core/untrusted-egress.js";
|
|
4
5
|
import { isPrivateHost } from "../core/runner/image.js";
|
|
5
6
|
const MAX_REDIRECTS = 10;
|
|
6
7
|
const MAX_URL_LENGTH = 2048;
|
|
@@ -229,10 +230,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
229
230
|
const { url, prompt } = args;
|
|
230
231
|
const startedAt = Date.now();
|
|
231
232
|
let urlForDetails = url.length > MAX_URL_LENGTH ? url.slice(0, MAX_URL_LENGTH) : url;
|
|
232
|
-
const failure = (message, extra = {}) => ({
|
|
233
|
-
content: message,
|
|
234
|
-
details: { type: "web-fetch", url: urlForDetails, bytes: 0, durationMs: Date.now() - startedAt, ...extra },
|
|
235
|
-
});
|
|
233
|
+
const failure = (message, extra = {}) => errorResult(message, { type: "web-fetch", url: urlForDetails, bytes: 0, durationMs: Date.now() - startedAt, ...extra });
|
|
236
234
|
if (url.length > MAX_URL_LENGTH) {
|
|
237
235
|
return failure(`Error (WebFetch): URL is ${url.length} characters — the limit is ${MAX_URL_LENGTH}.`);
|
|
238
236
|
}
|
|
@@ -381,6 +379,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
381
379
|
return {
|
|
382
380
|
content: `Error (WebFetch): the request was interrupted (aborted) before completion${finalResponse ? " while reading the response body" : ""}. No content was retrieved.`,
|
|
383
381
|
details: { ...errDetails, aborted: true },
|
|
382
|
+
isError: true,
|
|
384
383
|
};
|
|
385
384
|
}
|
|
386
385
|
if (timedOut) {
|
|
@@ -391,11 +390,13 @@ export function webFetchToolSpec(config = {}) {
|
|
|
391
390
|
? `Error (WebFetch): the server responded (HTTP ${connectedMeta.status}) to a redirect hop, but the request stalled while following the redirect — timed out after ${timeoutMs}ms. The host IS reachable; retry only if the stall was likely transient, or fetch the redirect target directly.`
|
|
392
391
|
: `Error (WebFetch): timed out after ${timeoutMs}ms while connecting — the host did not respond or the connection stalled before any response arrived. Do not retry the same URL; continue without it or try a different source.`,
|
|
393
392
|
details: { ...errDetails, timedOut: true },
|
|
393
|
+
isError: true,
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
return {
|
|
397
397
|
content: `Error (WebFetch): request failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
398
398
|
details: errDetails,
|
|
399
|
+
isError: true,
|
|
399
400
|
};
|
|
400
401
|
}
|
|
401
402
|
finally {
|
|
@@ -447,6 +448,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
447
448
|
durationMs: Date.now() - startedAt,
|
|
448
449
|
...transferStateDetails,
|
|
449
450
|
},
|
|
451
|
+
isError: true,
|
|
450
452
|
};
|
|
451
453
|
}
|
|
452
454
|
if (bodyBytes) {
|
|
@@ -474,6 +476,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
474
476
|
durationMs: Date.now() - startedAt,
|
|
475
477
|
...transferStateDetails,
|
|
476
478
|
},
|
|
479
|
+
isError: true,
|
|
477
480
|
};
|
|
478
481
|
}
|
|
479
482
|
}
|
|
@@ -571,20 +574,36 @@ export function makeWebFetchSummarizer(brain, model) {
|
|
|
571
574
|
return text;
|
|
572
575
|
};
|
|
573
576
|
}
|
|
577
|
+
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
|
|
578
|
+
const SEARCH_ERROR_EXCERPT_CHARS = 2048;
|
|
579
|
+
const SEARCH_TITLE_MAX_CHARS = 300;
|
|
580
|
+
const SEARCH_SNIPPET_MAX_CHARS = 1_000;
|
|
581
|
+
const SEARCH_BODY_MAX_CHARS = 100_000;
|
|
582
|
+
export function clipCodePoints(s, max) {
|
|
583
|
+
const cps = [...s];
|
|
584
|
+
return cps.length > max ? cps.slice(0, max).join("") + "…" : s;
|
|
585
|
+
}
|
|
574
586
|
function hostMatchesDomain(host, domain) {
|
|
575
|
-
const
|
|
576
|
-
|
|
587
|
+
const norm = (s) => s.toLowerCase().replace(/^\*\./, "").replace(/^\.+|\.+$/g, "");
|
|
588
|
+
const h = norm(host);
|
|
589
|
+
const d = norm(domain);
|
|
590
|
+
if (!d || !h)
|
|
577
591
|
return false;
|
|
578
|
-
return
|
|
592
|
+
return h === d || h.endsWith("." + d);
|
|
579
593
|
}
|
|
580
594
|
function webSearchResultAllowed(url, allowed, blocked) {
|
|
581
595
|
let host;
|
|
582
596
|
try {
|
|
583
|
-
|
|
597
|
+
const u = new URL(url);
|
|
598
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
599
|
+
return false;
|
|
600
|
+
host = u.hostname.toLowerCase();
|
|
584
601
|
}
|
|
585
602
|
catch {
|
|
586
|
-
return
|
|
603
|
+
return false;
|
|
587
604
|
}
|
|
605
|
+
if (!host)
|
|
606
|
+
return false;
|
|
588
607
|
if (blocked && blocked.some((d) => hostMatchesDomain(host, d)))
|
|
589
608
|
return false;
|
|
590
609
|
if (allowed && allowed.length && !allowed.some((d) => hostMatchesDomain(host, d)))
|
|
@@ -612,27 +631,94 @@ export function createWebSearchTool(config) {
|
|
|
612
631
|
effect: "read",
|
|
613
632
|
execute: async (args, ctx) => {
|
|
614
633
|
const { query, allowed_domains, blocked_domains } = args;
|
|
634
|
+
const startedAt = Date.now();
|
|
635
|
+
const failCard = (extra = {}) => ({
|
|
636
|
+
type: "web-search",
|
|
637
|
+
query,
|
|
638
|
+
results: [],
|
|
639
|
+
durationSeconds: (Date.now() - startedAt) / 1000,
|
|
640
|
+
searchCount: 0,
|
|
641
|
+
...extra,
|
|
642
|
+
});
|
|
615
643
|
if (allowed_domains?.length && blocked_domains?.length) {
|
|
616
|
-
return "Error: Cannot specify both allowed_domains and blocked_domains in the same request";
|
|
644
|
+
return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard());
|
|
617
645
|
}
|
|
618
|
-
const
|
|
646
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
|
|
647
|
+
const ac = new AbortController();
|
|
648
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
649
|
+
const onOuterAbort = () => ac.abort();
|
|
650
|
+
if (ctx.signal?.aborted)
|
|
651
|
+
ac.abort();
|
|
652
|
+
else
|
|
653
|
+
ctx.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
654
|
+
const TIMED_OUT = Symbol("websearch-timeout");
|
|
655
|
+
const abortedFrame = () => errorResult("Error (WebSearch): the search was interrupted (aborted) before completion. No results were retrieved.", failCard({ aborted: true }));
|
|
656
|
+
const timedOutFrame = () => errorResult(`Error (WebSearch): the search backend did not respond within ${timeoutMs}ms. The tool stopped waiting; the backend may still be executing the request.`, failCard({ timedOut: true }));
|
|
619
657
|
let results;
|
|
620
658
|
try {
|
|
621
|
-
|
|
659
|
+
const work = Promise.resolve(config.search(query, ac.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains }));
|
|
660
|
+
work.catch(() => { });
|
|
661
|
+
let fireDeadline;
|
|
662
|
+
const deadline = new Promise((res) => {
|
|
663
|
+
fireDeadline = () => res(TIMED_OUT);
|
|
664
|
+
});
|
|
665
|
+
const deadlineTimer = setTimeout(() => fireDeadline(), timeoutMs);
|
|
666
|
+
deadlineTimer.unref?.();
|
|
667
|
+
try {
|
|
668
|
+
const raced = await Promise.race([work, deadline]);
|
|
669
|
+
if (raced === TIMED_OUT) {
|
|
670
|
+
return ctx.signal?.aborted ? abortedFrame() : timedOutFrame();
|
|
671
|
+
}
|
|
672
|
+
results = raced;
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
clearTimeout(deadlineTimer);
|
|
676
|
+
}
|
|
622
677
|
}
|
|
623
678
|
catch (e) {
|
|
624
|
-
|
|
679
|
+
if (ctx.signal?.aborted)
|
|
680
|
+
return abortedFrame();
|
|
681
|
+
if (ac.signal.aborted)
|
|
682
|
+
return timedOutFrame();
|
|
683
|
+
const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
|
|
684
|
+
const headline = "Error (WebSearch): the search backend failed.";
|
|
685
|
+
return errorResult(msg
|
|
686
|
+
? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}`
|
|
687
|
+
: headline, failCard());
|
|
625
688
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
689
|
+
finally {
|
|
690
|
+
clearTimeout(timer);
|
|
691
|
+
ctx.signal?.removeEventListener("abort", onOuterAbort);
|
|
692
|
+
}
|
|
693
|
+
const usable = results.filter((r) => webSearchResultAllowed(r.url));
|
|
694
|
+
const droppedUnusable = results.length - usable.length;
|
|
695
|
+
results = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains)).slice(0, max);
|
|
696
|
+
let clipped = false;
|
|
697
|
+
const shown = results.map((r) => {
|
|
698
|
+
const title = clipCodePoints(r.title, SEARCH_TITLE_MAX_CHARS);
|
|
699
|
+
const url = clipCodePoints(r.url, MAX_URL_LENGTH);
|
|
700
|
+
const snippet = clipCodePoints(r.snippet, SEARCH_SNIPPET_MAX_CHARS);
|
|
701
|
+
if (title !== r.title || url !== r.url || snippet !== r.snippet)
|
|
702
|
+
clipped = true;
|
|
703
|
+
return { title, url, snippet };
|
|
704
|
+
});
|
|
705
|
+
const body = shown.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join("\n");
|
|
706
|
+
if (body.length > SEARCH_BODY_MAX_CHARS)
|
|
707
|
+
clipped = true;
|
|
708
|
+
const fenced = delimitUntrusted(`WebSearch ${JSON.stringify(query)}`, body || "(no results)", SEARCH_BODY_MAX_CHARS);
|
|
709
|
+
const dropNote = droppedUnusable > 0
|
|
710
|
+
? `\n\n[WebSearch: ${droppedUnusable} result(s) returned by the backend were dropped — their URL was not a usable http(s) link]`
|
|
711
|
+
: "";
|
|
712
|
+
const clipNote = clipped
|
|
713
|
+
? "\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.]"
|
|
714
|
+
: "";
|
|
715
|
+
const modelText = `${fenced}${dropNote}${clipNote}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
|
|
630
716
|
return {
|
|
631
717
|
content: modelText,
|
|
632
718
|
details: {
|
|
633
719
|
type: "web-search",
|
|
634
720
|
query,
|
|
635
|
-
results: [{ tool_use_id: ctx.toolCallId, content:
|
|
721
|
+
results: [{ tool_use_id: ctx.toolCallId, content: shown.map((r) => ({ title: r.title, url: r.url })) }],
|
|
636
722
|
durationSeconds: (Date.now() - startedAt) / 1000,
|
|
637
723
|
searchCount: 1,
|
|
638
724
|
},
|
package/dist/tools/worktree.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { defineTool } from "../core/tools.js";
|
|
3
|
+
import { defineTool, errorResult } from "../core/tools.js";
|
|
4
4
|
import { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "../core/git-worktree-env.js";
|
|
5
5
|
const ENTER_DESCRIPTION = "Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by " +
|
|
6
6
|
"project instructions. This tool creates an isolated git worktree and switches the current session into it.\n" +
|
|
@@ -113,20 +113,20 @@ export function createWorktreeTools(env, opts) {
|
|
|
113
113
|
execute: async (args) => {
|
|
114
114
|
const { name, path, baseRef } = args;
|
|
115
115
|
if (path !== undefined && name !== undefined) {
|
|
116
|
-
return "Error (EnterWorktree): `name` and `path` are mutually exclusive — `path` enters an existing worktree, `name` creates a new one.";
|
|
116
|
+
return errorResult("Error (EnterWorktree): `name` and `path` are mutually exclusive — `path` enters an existing worktree, `name` creates a new one.");
|
|
117
117
|
}
|
|
118
118
|
if (path !== undefined && baseRef !== undefined) {
|
|
119
|
-
return "Error (EnterWorktree): `baseRef` only applies when creating a new worktree — an existing worktree entered via `path` keeps whatever state it has.";
|
|
119
|
+
return errorResult("Error (EnterWorktree): `baseRef` only applies when creating a new worktree — an existing worktree entered via `path` keeps whatever state it has.");
|
|
120
120
|
}
|
|
121
121
|
if (session.current && path === undefined) {
|
|
122
|
-
return (`Error (EnterWorktree): already in a worktree session (${session.current.worktreeDir}). ` +
|
|
122
|
+
return errorResult(`Error (EnterWorktree): already in a worktree session (${session.current.worktreeDir}). ` +
|
|
123
123
|
"Use ExitWorktree to leave it before creating a new worktree, or pass `path` to switch into an " +
|
|
124
124
|
"existing worktree of this repository.");
|
|
125
125
|
}
|
|
126
126
|
const probe = await env.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
|
|
127
127
|
if (!probe.ok || probe.value.exitCode !== 0) {
|
|
128
128
|
const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : String(probe.error);
|
|
129
|
-
return (`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
|
|
129
|
+
return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
|
|
130
130
|
`${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
|
|
131
131
|
"so worktree isolation is unavailable here.");
|
|
132
132
|
}
|
|
@@ -134,31 +134,31 @@ export function createWorktreeTools(env, opts) {
|
|
|
134
134
|
const candidate = resolve(cwdRef.current, path);
|
|
135
135
|
const top = await env.exec("git rev-parse --show-toplevel", { cwd: candidate });
|
|
136
136
|
if (!top.ok || top.value.exitCode !== 0) {
|
|
137
|
-
return `Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory)
|
|
137
|
+
return errorResult(`Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory).`);
|
|
138
138
|
}
|
|
139
139
|
const target = top.value.stdout.trim();
|
|
140
140
|
const list = await env.exec("git worktree list --porcelain", { cwd: repoRoot });
|
|
141
141
|
if (!list.ok || list.value.exitCode !== 0) {
|
|
142
|
-
return "Error (EnterWorktree): could not read `git worktree list` for this repository.";
|
|
142
|
+
return errorResult("Error (EnterWorktree): could not read `git worktree list` for this repository.");
|
|
143
143
|
}
|
|
144
144
|
const registered = list.value.stdout
|
|
145
145
|
.split("\n")
|
|
146
146
|
.filter((l) => l.startsWith("worktree "))
|
|
147
147
|
.map((l) => l.slice("worktree ".length).trim());
|
|
148
148
|
if (!registered.includes(target)) {
|
|
149
|
-
return (`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
|
|
149
|
+
return errorResult(`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
|
|
150
150
|
"(it does not appear in `git worktree list`).");
|
|
151
151
|
}
|
|
152
152
|
if (target === registered[0]) {
|
|
153
|
-
return `Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter
|
|
153
|
+
return errorResult(`Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter.`);
|
|
154
154
|
}
|
|
155
155
|
if (target !== repoRoot && !target.startsWith(repoRoot.endsWith("/") ? repoRoot : `${repoRoot}/`)) {
|
|
156
|
-
return (`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
|
|
156
|
+
return errorResult(`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
|
|
157
157
|
"file containment stays anchored at the task root, so it cannot be entered from this session.");
|
|
158
158
|
}
|
|
159
159
|
const head = await env.exec("git rev-parse HEAD", { cwd: target });
|
|
160
160
|
if (!head.ok || head.value.exitCode !== 0) {
|
|
161
|
-
return `Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}
|
|
161
|
+
return errorResult(`Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`);
|
|
162
162
|
}
|
|
163
163
|
const previous = session.current;
|
|
164
164
|
const originalCwd = previous ? previous.originalCwd : cwdRef.current;
|
|
@@ -172,7 +172,7 @@ export function createWorktreeTools(env, opts) {
|
|
|
172
172
|
}
|
|
173
173
|
const base = await resolveBaseCommit(env, repoRoot, baseRef ?? "fresh");
|
|
174
174
|
if ("error" in base)
|
|
175
|
-
return `Error (EnterWorktree): ${base.error}
|
|
175
|
+
return errorResult(`Error (EnterWorktree): ${base.error}.`);
|
|
176
176
|
let createdDir;
|
|
177
177
|
try {
|
|
178
178
|
const { worktreeDir } = await addWorktree(env, {
|
|
@@ -187,7 +187,7 @@ export function createWorktreeTools(env, opts) {
|
|
|
187
187
|
createdDir = worktreeDir;
|
|
188
188
|
}
|
|
189
189
|
catch (e) {
|
|
190
|
-
return `Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}
|
|
190
|
+
return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
|
|
191
191
|
}
|
|
192
192
|
session.current = {
|
|
193
193
|
worktreeDir: createdDir,
|
|
@@ -227,7 +227,7 @@ export function createWorktreeTools(env, opts) {
|
|
|
227
227
|
const discardChanges = raw.discardChanges ?? raw.discard_changes;
|
|
228
228
|
const wantsRemove = raw.action === "remove";
|
|
229
229
|
if (keep === true && discardChanges === true) {
|
|
230
|
-
return "Error (ExitWorktree): `keep: true` and `discardChanges: true` contradict each other — pick one.";
|
|
230
|
+
return errorResult("Error (ExitWorktree): `keep: true` and `discardChanges: true` contradict each other — pick one.");
|
|
231
231
|
}
|
|
232
232
|
if (!session.current) {
|
|
233
233
|
return ("No-op: there is no active EnterWorktree session to exit. This tool only operates on the " +
|