@xingular/harness 1.2.0 → 1.3.1-unstable.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/CHANGELOG.md +35 -0
- package/README.md +10 -5
- package/dist/cli.js +5560 -5056
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +4 -1
- package/dist/sdk/agents/profiles.js +17 -6
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/plugins/builtin.js +3 -3
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +2 -2
- package/dist/sdk/prompt/compile.d.ts +2 -2
- package/dist/sdk/prompt/compile.js +14 -16
- package/dist/sdk/server/methods.js +4 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.js +29 -11
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +194 -54
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +28 -14
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +7 -0
- package/dist/sdk/terminal/terminals.js +86 -14
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.js +14 -4
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +8 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/terminal-opentui.js +1416 -348
- package/dist/terminal-pi.js +315 -126
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +65 -34
- package/docs/architecture/04-prompt.md +8 -6
- package/docs/architecture/06-plugins-and-capabilities.md +10 -6
- package/docs/architecture/07-tools-and-gate.md +31 -5
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +16 -9
- package/docs/architecture/11-terminal.md +51 -26
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +18 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/README.md +10 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +9 -9
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/README.md +2 -2
- package/docs/terminal-tui.md +85 -46
- package/package.json +1 -1
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
package/dist/terminal-pi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/terminal/piExtension.ts
|
|
2
|
-
import { createBashTool, createEditTool, createWriteTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type as Type2 } from "typebox";
|
|
4
4
|
|
|
5
5
|
// src/terminal/client.ts
|
|
@@ -199,6 +199,8 @@ var runEventTypeSchema = z.enum([
|
|
|
199
199
|
"extension.notice",
|
|
200
200
|
"reaction.added",
|
|
201
201
|
"agent.message",
|
|
202
|
+
"todo.updated",
|
|
203
|
+
// Written before the rename; accepted so stored run logs keep their lists. Never written now.
|
|
202
204
|
"checklist.updated",
|
|
203
205
|
"checkpoint",
|
|
204
206
|
"heartbeat",
|
|
@@ -336,9 +338,9 @@ var extensionNoticePayload = z.object({
|
|
|
336
338
|
text: z.string(),
|
|
337
339
|
extension: z.string().optional()
|
|
338
340
|
});
|
|
339
|
-
var
|
|
340
|
-
var
|
|
341
|
-
var
|
|
341
|
+
var todoItemSchema = z.object({ text: z.string().min(1).max(500), status: z.enum(["pending", "in_progress", "completed"]) }).strict();
|
|
342
|
+
var MAX_TODO_ITEMS = 50;
|
|
343
|
+
var todoPayload = z.object({ items: z.array(todoItemSchema).max(MAX_TODO_ITEMS) }).strict();
|
|
342
344
|
var MAX_CHECKPOINT_SUMMARY = 1200;
|
|
343
345
|
var MAX_CHECKPOINT_DONE = 20;
|
|
344
346
|
var MAX_CHECKPOINT_EVIDENCE = 5;
|
|
@@ -469,7 +471,8 @@ var runEventPayloadSchemas = {
|
|
|
469
471
|
"extension.notice": extensionNoticePayload,
|
|
470
472
|
"reaction.added": reactionAddedPayload,
|
|
471
473
|
"agent.message": agentMessagePayload,
|
|
472
|
-
"
|
|
474
|
+
"todo.updated": todoPayload,
|
|
475
|
+
"checklist.updated": todoPayload,
|
|
473
476
|
checkpoint: checkpointPayload,
|
|
474
477
|
heartbeat: heartbeatPayload,
|
|
475
478
|
"artifact.added": artifactPayload,
|
|
@@ -526,7 +529,9 @@ var runRecordSchema = z.object({
|
|
|
526
529
|
variantScope: text.optional(),
|
|
527
530
|
variantVersion: text.optional(),
|
|
528
531
|
profile: text.optional(),
|
|
529
|
-
title: text.optional()
|
|
532
|
+
title: text.optional(),
|
|
533
|
+
model: text.optional(),
|
|
534
|
+
effort: text.optional()
|
|
530
535
|
});
|
|
531
536
|
|
|
532
537
|
// src/core/index.ts
|
|
@@ -565,7 +570,7 @@ var PLAN_RESEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
565
570
|
"grep",
|
|
566
571
|
"find",
|
|
567
572
|
"ls",
|
|
568
|
-
"
|
|
573
|
+
"todo_write",
|
|
569
574
|
"session_name",
|
|
570
575
|
"history_search",
|
|
571
576
|
"history_read",
|
|
@@ -653,38 +658,39 @@ function askUserTool(run2) {
|
|
|
653
658
|
};
|
|
654
659
|
}
|
|
655
660
|
|
|
656
|
-
// src/tools/
|
|
657
|
-
var
|
|
661
|
+
// src/tools/todo.ts
|
|
662
|
+
var TODO_WRITE = "todo_write";
|
|
663
|
+
var TODO_GUIDANCE = "If the work has several distinct steps or will take more than one tool call, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. Before your final reply, write one final update: mark every fully completed step completed and leave unfinished or blocked steps accurate. Never mark unfinished work completed. The person follows your work from it, and a subagent\u2019s todo list shows as its progress in the person\u2019s list of subagents.";
|
|
658
664
|
var MARK = { pending: "[ ]", in_progress: "[~]", completed: "[x]" };
|
|
659
|
-
function
|
|
660
|
-
if (!items.length) return "The
|
|
665
|
+
function describeTodo(items) {
|
|
666
|
+
if (!items.length) return "The todo list is empty.";
|
|
661
667
|
return items.map((item) => `${MARK[item.status]} ${item.text}`).join("\n");
|
|
662
668
|
}
|
|
663
|
-
function
|
|
669
|
+
function todoTool(run2) {
|
|
664
670
|
return {
|
|
665
|
-
name:
|
|
666
|
-
label: "Write the
|
|
667
|
-
description:
|
|
671
|
+
name: TODO_WRITE,
|
|
672
|
+
label: "Write the todo list",
|
|
673
|
+
description: `Keep a todo list of the steps of the current work. The user watches this list to follow your progress. ${TODO_GUIDANCE} Send the whole list every time; it replaces the previous one. Do not use it for a single, simple step or for conversation. Statuses: pending, in_progress, completed. Keep exactly one step in_progress while you work, mark a step completed as soon as it is done (not in batches), and add steps you discover. Only mark a step completed when it is fully done; if something blocks it, keep it in_progress and add a step for the blocker.`,
|
|
668
674
|
parameters: (Type3) => Type3.Object({
|
|
669
675
|
items: Type3.Array(Type3.Object({
|
|
670
676
|
text: Type3.String({ description: "One step, as a short imperative line" }),
|
|
671
677
|
status: Type3.Union([Type3.Literal("pending"), Type3.Literal("in_progress"), Type3.Literal("completed")])
|
|
672
|
-
}), { maxItems:
|
|
678
|
+
}), { maxItems: MAX_TODO_ITEMS })
|
|
673
679
|
}),
|
|
674
680
|
readOnly: true,
|
|
675
681
|
planModeSafe: () => true,
|
|
676
682
|
execute: async (input) => {
|
|
677
|
-
if (!Array.isArray(input.items)) return "Send items: the whole
|
|
678
|
-
if (input.items.length >
|
|
683
|
+
if (!Array.isArray(input.items)) return "Send items: the whole todo list as an array.";
|
|
684
|
+
if (input.items.length > MAX_TODO_ITEMS) return `Keep the todo list to ${MAX_TODO_ITEMS} steps or fewer.`;
|
|
679
685
|
const items = [];
|
|
680
686
|
for (const raw of input.items) {
|
|
681
687
|
const entry = raw;
|
|
682
|
-
const parsed =
|
|
688
|
+
const parsed = todoItemSchema.safeParse({ text: typeof entry?.text === "string" ? entry.text.trim() : entry?.text, status: entry?.status });
|
|
683
689
|
if (!parsed.success) return "Every step needs text of 1 to 500 characters and a status of pending, in_progress or completed.";
|
|
684
690
|
items.push(parsed.data);
|
|
685
691
|
}
|
|
686
|
-
run2.emit("
|
|
687
|
-
return
|
|
692
|
+
run2.emit("todo.updated", { items });
|
|
693
|
+
return describeTodo(items);
|
|
688
694
|
}
|
|
689
695
|
};
|
|
690
696
|
}
|
|
@@ -798,14 +804,15 @@ function checkpointTool(run2, slice) {
|
|
|
798
804
|
import { createHash } from "node:crypto";
|
|
799
805
|
var DEFAULT_SLICE_POLICY = {
|
|
800
806
|
enabled: true,
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
|
|
807
|
+
// A chapter of a task, not an interruption every few edits: roughly 80 minutes of ordinary work
|
|
808
|
+
// (about 15 s a call), with 60 minutes of wall clock as the ceiling, whichever comes first. A task
|
|
809
|
+
// still gets eight slices before it returns to the person.
|
|
810
|
+
maxToolCalls: 320,
|
|
811
|
+
maxMinutes: 60,
|
|
812
|
+
// No token cap: the tool calls and the clock are the bound. Counting new input, cache writes and
|
|
813
|
+
// output made a few large reads end a reply before useful work (see the "Confirm plan model
|
|
814
|
+
// support" thread), so a cap is available to whoever asks for one and off unless they do.
|
|
815
|
+
maxNewTokens: 0,
|
|
809
816
|
maxSlices: 8,
|
|
810
817
|
continue: true
|
|
811
818
|
};
|
|
@@ -884,42 +891,54 @@ function canonical(value, depth = 0) {
|
|
|
884
891
|
}
|
|
885
892
|
var signature = (name, input) => `${name}\0${createHash("sha256").update(canonical(input ?? null)).digest("hex")}`;
|
|
886
893
|
var clip = (value, limit) => value.length <= limit ? value : `${value.slice(0, limit - 1)}\u2026`;
|
|
887
|
-
function handoffRequest(limit, state) {
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
return clip(
|
|
895
|
-
`You are repeating work: ${reason}. Do not repeat it. Choose a different approach, read what the earlier attempt returned, or call checkpoint_write with the blocker so the next slice can decide.`,
|
|
896
|
-
600
|
|
897
|
-
);
|
|
894
|
+
function handoffRequest(limit, state, checkpointAvailable = true) {
|
|
895
|
+
const handoff = checkpointAvailable ? "Call checkpoint_write now with what is done, the single next step, anything blocking, the files you touched and the checks that matter. Then tell the user in a few lines where you stopped." : "Stop using tools and return a concise partial research report with findings, blockers and the next useful research step. The Harness records an evidence-derived checkpoint.";
|
|
896
|
+
return clip(`This reply reached its work-slice limit (${limit}; step ${state.steps}). ${handoff} Do not call other tools.`, 600);
|
|
897
|
+
}
|
|
898
|
+
function stalledNote(reason, checkpointAvailable = true) {
|
|
899
|
+
const handoff = checkpointAvailable ? "Choose a different approach, read what the earlier attempt returned, or call checkpoint_write with the blocker so the next slice can decide." : "Do not use more tools. Return concise findings and blockers in your final response; the Harness records an evidence-derived checkpoint.";
|
|
900
|
+
return clip(`You are repeating work: ${reason}. Do not repeat it. ${handoff}`, 600);
|
|
898
901
|
}
|
|
899
902
|
function workSliceGuard(options) {
|
|
900
903
|
const { policy, state } = options;
|
|
901
904
|
const calls = /* @__PURE__ */ new Map();
|
|
905
|
+
const checkCalls = /* @__PURE__ */ new Set();
|
|
902
906
|
const edits = /* @__PURE__ */ new Map();
|
|
903
907
|
const failures = /* @__PURE__ */ new Map();
|
|
904
|
-
let stepsSinceProgress = 0;
|
|
905
908
|
let nudged = false;
|
|
909
|
+
const checkpointAvailable = () => typeof options.checkpointAvailable === "function" ? options.checkpointAvailable() : options.checkpointAvailable !== false;
|
|
906
910
|
const resetRepetition = () => {
|
|
907
911
|
calls.clear();
|
|
912
|
+
checkCalls.clear();
|
|
908
913
|
edits.clear();
|
|
909
914
|
failures.clear();
|
|
910
|
-
stepsSinceProgress = 0;
|
|
911
915
|
nudged = false;
|
|
912
916
|
};
|
|
913
917
|
const end = (kind, limit) => {
|
|
914
918
|
state.endedBy = kind;
|
|
915
919
|
state.endedByLimit = limit;
|
|
916
920
|
options.onEnd?.(kind, limit);
|
|
917
|
-
const
|
|
921
|
+
const canCheckpoint = checkpointAvailable();
|
|
922
|
+
const reason = kind === "stuck" ? stalledNote(limit, canCheckpoint) : kind === "compaction-thrash" ? handoffRequest("three context compactions in one reply", state, canCheckpoint) : handoffRequest(limit, state, canCheckpoint);
|
|
918
923
|
return { block: true, terminate: true, reason };
|
|
919
924
|
};
|
|
925
|
+
const nudge = (reason) => {
|
|
926
|
+
if (nudged) return end("stuck", reason);
|
|
927
|
+
nudged = true;
|
|
928
|
+
const canCheckpoint = checkpointAvailable();
|
|
929
|
+
if (!canCheckpoint) {
|
|
930
|
+
state.endedBy = "stuck";
|
|
931
|
+
state.endedByLimit = reason;
|
|
932
|
+
}
|
|
933
|
+
return { block: true, reason: stalledNote(reason, canCheckpoint) };
|
|
934
|
+
};
|
|
920
935
|
const repeated = (name, input) => {
|
|
921
936
|
if (POLLING_TOOLS.has(name)) return null;
|
|
922
937
|
const key = signature(name, input);
|
|
938
|
+
if (name === "bash") {
|
|
939
|
+
const command = input && typeof input === "object" ? String(input.command ?? "") : "";
|
|
940
|
+
if (command && looksLikeCheck(command)) checkCalls.add(key);
|
|
941
|
+
}
|
|
923
942
|
const count = (calls.get(key) ?? 0) + 1;
|
|
924
943
|
calls.set(key, count);
|
|
925
944
|
if (count < 3) return null;
|
|
@@ -945,9 +964,18 @@ function workSliceGuard(options) {
|
|
|
945
964
|
pi.on("tool_result", (event) => {
|
|
946
965
|
if (event.toolName === "checkpoint_write" && !event.isError) {
|
|
947
966
|
state.checkpointWritten = true;
|
|
948
|
-
stepsSinceProgress = 0;
|
|
949
967
|
nudged = false;
|
|
950
968
|
}
|
|
969
|
+
if ((event.toolName === "edit" || event.toolName === "write") && event.isError === false) {
|
|
970
|
+
const path = event.input && typeof event.input === "object" ? String(event.input.path ?? "") : "";
|
|
971
|
+
if (path) {
|
|
972
|
+
edits.set(path, (edits.get(path) ?? 0) + 1);
|
|
973
|
+
failures.clear();
|
|
974
|
+
for (const key of checkCalls) calls.delete(key);
|
|
975
|
+
checkCalls.clear();
|
|
976
|
+
nudged = false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
951
979
|
if (event.toolName !== "bash") return;
|
|
952
980
|
const command = event.input && typeof event.input === "object" ? String(event.input.command ?? "") : "";
|
|
953
981
|
if (!command || !looksLikeCheck(command)) return;
|
|
@@ -958,16 +986,18 @@ function workSliceGuard(options) {
|
|
|
958
986
|
failures.delete(command);
|
|
959
987
|
edits.clear();
|
|
960
988
|
calls.clear();
|
|
961
|
-
|
|
989
|
+
checkCalls.clear();
|
|
962
990
|
nudged = false;
|
|
963
991
|
});
|
|
964
992
|
pi.on("tool_call", (event) => {
|
|
965
993
|
const name = event.toolName;
|
|
966
994
|
state.steps += 1;
|
|
967
995
|
state.lastTool = name;
|
|
968
|
-
const
|
|
996
|
+
const canCheckpoint = checkpointAvailable();
|
|
997
|
+
const handoff = canCheckpoint && HANDOFF_TOOLS.has(name);
|
|
969
998
|
if (state.endedBy) {
|
|
970
|
-
|
|
999
|
+
const reason2 = state.checkpointWritten ? "This reply is over. Its checkpoint is recorded; nothing else runs." : "This reply is over. The Harness will record an evidence-derived checkpoint; nothing else runs.";
|
|
1000
|
+
return handoff ? void 0 : { block: true, terminate: true, reason: reason2 };
|
|
971
1001
|
}
|
|
972
1002
|
if (state.compactions >= 3) {
|
|
973
1003
|
if (handoff) {
|
|
@@ -982,34 +1012,24 @@ function workSliceGuard(options) {
|
|
|
982
1012
|
state.endedBy = "budget";
|
|
983
1013
|
state.endedByLimit = limit;
|
|
984
1014
|
state.refusedAt ??= state.steps;
|
|
985
|
-
|
|
986
|
-
return handoff ? void 0 : { block: true, reason: handoffRequest(limit, state) };
|
|
1015
|
+
return handoff ? void 0 : { block: true, reason: handoffRequest(limit, state, canCheckpoint) };
|
|
987
1016
|
}
|
|
988
1017
|
if (handoff) return void 0;
|
|
1018
|
+
if (name === "todo_write") return void 0;
|
|
989
1019
|
const edit = name === "edit" || name === "write";
|
|
990
1020
|
const path = edit && event.input && typeof event.input === "object" ? String(event.input.path ?? "") : "";
|
|
991
1021
|
if (edit && path) {
|
|
992
|
-
const count =
|
|
993
|
-
edits.set(path, count);
|
|
994
|
-
stepsSinceProgress = 0;
|
|
1022
|
+
const count = edits.get(path) ?? 0;
|
|
995
1023
|
if (count >= 4) {
|
|
996
1024
|
const reason2 = `${path} was edited ${count} times with no passing check between`;
|
|
997
|
-
|
|
998
|
-
nudged = true;
|
|
999
|
-
return { block: true, reason: stalledNote(reason2) };
|
|
1025
|
+
return nudge(reason2);
|
|
1000
1026
|
}
|
|
1001
|
-
} else {
|
|
1002
|
-
stepsSinceProgress += 1;
|
|
1003
1027
|
}
|
|
1004
1028
|
const repeat = repeated(name, event.input);
|
|
1005
1029
|
const failure = failing(name, event.input);
|
|
1006
|
-
const
|
|
1007
|
-
const reason = repeat ?? failure ?? noProgress;
|
|
1030
|
+
const reason = repeat ?? failure;
|
|
1008
1031
|
if (!reason) return void 0;
|
|
1009
|
-
|
|
1010
|
-
nudged = true;
|
|
1011
|
-
stepsSinceProgress = 0;
|
|
1012
|
-
return { block: true, reason: stalledNote(reason) };
|
|
1032
|
+
return nudge(reason);
|
|
1013
1033
|
});
|
|
1014
1034
|
};
|
|
1015
1035
|
}
|
|
@@ -1058,11 +1078,12 @@ async function withCommandBudget(run2, abort, seconds) {
|
|
|
1058
1078
|
|
|
1059
1079
|
// src/tools/sessionName.ts
|
|
1060
1080
|
var SESSION_NAME = "session_name";
|
|
1081
|
+
var SESSION_GUIDANCE = "If the request needs tools or changes state \u2014 not a greeting, thanks, or a short conversational reply \u2014 call session_name once before your first other tool call, naming the work from the request. Use one short imperative sentence of at most six words. Revise the name only when the overall objective changes; keep the current name when it does not.";
|
|
1061
1082
|
function sessionNameTool(current) {
|
|
1062
1083
|
return {
|
|
1063
1084
|
name: SESSION_NAME,
|
|
1064
1085
|
label: "Name the session",
|
|
1065
|
-
description:
|
|
1086
|
+
description: `Set or revise this session\u2019s name. ${SESSION_GUIDANCE} For example, "Add web browser integrations". You may revise a manually assigned name. This only names your own session.`,
|
|
1066
1087
|
parameters: (Type3) => Type3.Object({ name: Type3.String({ description: "One short sentence describing the objective, at most six words" }) }),
|
|
1067
1088
|
planModeSafe: () => true,
|
|
1068
1089
|
execute: async (input) => {
|
|
@@ -1123,7 +1144,7 @@ function clip2(text4, limit) {
|
|
|
1123
1144
|
return charCount(text4) <= limit ? text4 : `${takeChars(text4, limit)}\u2026`;
|
|
1124
1145
|
}
|
|
1125
1146
|
function renderCompactionCheckpoint(input) {
|
|
1126
|
-
if (!input.
|
|
1147
|
+
if (!input.todo.length && !input.planInstructions) return null;
|
|
1127
1148
|
const parts = [
|
|
1128
1149
|
"This is current state from the active run after context compaction, not a new user request. Continue the same request. Do not infer that omitted details or decisions were settled.",
|
|
1129
1150
|
input.goal.trim() ? `## Current request
|
|
@@ -1133,18 +1154,18 @@ ${clip2(input.goal.trim(), 1e3)}` : "",
|
|
|
1133
1154
|
|
|
1134
1155
|
${clip2(input.planInstructions, 4e3)}` : ""
|
|
1135
1156
|
].filter(Boolean);
|
|
1136
|
-
if (input.
|
|
1137
|
-
const active = input.
|
|
1138
|
-
const completed = input.
|
|
1139
|
-
const
|
|
1157
|
+
if (input.todo.length) {
|
|
1158
|
+
const active = input.todo.filter((item) => item.status !== "completed");
|
|
1159
|
+
const completed = input.todo.filter((item) => item.status === "completed");
|
|
1160
|
+
const todo = [
|
|
1140
1161
|
active.length ? `### In progress and pending
|
|
1141
|
-
${clip2(
|
|
1162
|
+
${clip2(describeTodo(active), 2500)}` : "",
|
|
1142
1163
|
completed.length ? `### Completed (${completed.length})
|
|
1143
|
-
${clip2(
|
|
1164
|
+
${clip2(describeTodo(completed), 500)}` : ""
|
|
1144
1165
|
].filter(Boolean).join("\n\n");
|
|
1145
|
-
parts.push(`## Current
|
|
1166
|
+
parts.push(`## Current todo list
|
|
1146
1167
|
|
|
1147
|
-
${
|
|
1168
|
+
${todo}`);
|
|
1148
1169
|
}
|
|
1149
1170
|
const body = clip2(parts.join("\n\n"), 8e3);
|
|
1150
1171
|
return `[Harness context update \u2014 not a user message]
|
|
@@ -1165,7 +1186,7 @@ var PLAN_READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1165
1186
|
"find",
|
|
1166
1187
|
"ls",
|
|
1167
1188
|
SESSION_NAME,
|
|
1168
|
-
|
|
1189
|
+
TODO_WRITE,
|
|
1169
1190
|
ASK_USER,
|
|
1170
1191
|
"ask_user_question",
|
|
1171
1192
|
GET_CAPABILITIES,
|
|
@@ -1264,12 +1285,12 @@ function planRefusal(tool, readOnly, args, planModeSafe) {
|
|
|
1264
1285
|
|
|
1265
1286
|
// src/sessions/compactionCheckpoint.ts
|
|
1266
1287
|
var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
|
|
1267
|
-
function
|
|
1268
|
-
if (!Array.isArray(value) || value.length >
|
|
1288
|
+
function validatedTodo(value) {
|
|
1289
|
+
if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS) return null;
|
|
1269
1290
|
const items = [];
|
|
1270
1291
|
for (const valueItem of value) {
|
|
1271
1292
|
const item = valueItem;
|
|
1272
|
-
const parsed =
|
|
1293
|
+
const parsed = todoItemSchema.safeParse({
|
|
1273
1294
|
text: typeof item?.text === "string" ? item.text.trim() : item?.text,
|
|
1274
1295
|
status: item?.status
|
|
1275
1296
|
});
|
|
@@ -1281,26 +1302,26 @@ function validatedChecklist(value) {
|
|
|
1281
1302
|
function compactionCheckpoint(options = {}) {
|
|
1282
1303
|
return (pi) => {
|
|
1283
1304
|
let goal = "";
|
|
1284
|
-
let
|
|
1305
|
+
let todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
1285
1306
|
let lastCompactionKey = null;
|
|
1286
1307
|
let compacted = false;
|
|
1287
1308
|
const reset = () => {
|
|
1288
1309
|
goal = "";
|
|
1289
|
-
|
|
1310
|
+
todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
1290
1311
|
lastCompactionKey = null;
|
|
1291
1312
|
compacted = false;
|
|
1292
1313
|
};
|
|
1293
1314
|
pi.on("before_agent_start", (event) => {
|
|
1294
1315
|
if (event.messageType !== "user") return;
|
|
1295
1316
|
goal = event.prompt;
|
|
1296
|
-
|
|
1317
|
+
todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
1297
1318
|
lastCompactionKey = null;
|
|
1298
1319
|
compacted = false;
|
|
1299
1320
|
});
|
|
1300
1321
|
pi.on("tool_result", (event) => {
|
|
1301
|
-
if (event.toolName !==
|
|
1302
|
-
const items =
|
|
1303
|
-
if (items)
|
|
1322
|
+
if (event.toolName !== TODO_WRITE || event.isError) return;
|
|
1323
|
+
const items = validatedTodo(event.input.items);
|
|
1324
|
+
if (items) todo = items;
|
|
1304
1325
|
});
|
|
1305
1326
|
pi.on("context", (event) => {
|
|
1306
1327
|
const summary = event.messages.findLast((message) => message.role === "compactionSummary");
|
|
@@ -1314,7 +1335,7 @@ function compactionCheckpoint(options = {}) {
|
|
|
1314
1335
|
compacted = false;
|
|
1315
1336
|
}
|
|
1316
1337
|
if (!compacted) return;
|
|
1317
|
-
const content = renderCompactionCheckpoint({ goal,
|
|
1338
|
+
const content = renderCompactionCheckpoint({ goal, todo, planInstructions });
|
|
1318
1339
|
if (!content) return;
|
|
1319
1340
|
return {
|
|
1320
1341
|
messages: [...event.messages, {
|
|
@@ -1428,23 +1449,150 @@ function inspectWorkspaceWrite(workspaceRoot, cwd, tool, input) {
|
|
|
1428
1449
|
// src/core/workspaceBashTool.ts
|
|
1429
1450
|
import { Type } from "typebox";
|
|
1430
1451
|
|
|
1452
|
+
// src/core/shell.ts
|
|
1453
|
+
import { accessSync as accessSync2, constants as constants2, statSync as statSync3 } from "node:fs";
|
|
1454
|
+
import { win32 } from "node:path";
|
|
1455
|
+
|
|
1456
|
+
// src/core/path.ts
|
|
1457
|
+
import { execFileSync } from "node:child_process";
|
|
1458
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
1459
|
+
import { homedir as homedir2 } from "node:os";
|
|
1460
|
+
import { delimiter, extname, isAbsolute as isAbsolute2, join as join2 } from "node:path";
|
|
1461
|
+
var resolved = null;
|
|
1462
|
+
function searchPath() {
|
|
1463
|
+
const platform = process.platform;
|
|
1464
|
+
const pathKey = pathVariableName(process.env);
|
|
1465
|
+
const inherited = process.env[pathKey] ?? "";
|
|
1466
|
+
if (resolved?.platform === platform && resolved.inherited === inherited) return resolved.path;
|
|
1467
|
+
const separator = pathSeparator(platform);
|
|
1468
|
+
const entries = [];
|
|
1469
|
+
if (platform !== "win32") {
|
|
1470
|
+
try {
|
|
1471
|
+
const output = execFileSync(process.env.SHELL || "/bin/zsh", ["-ilc", 'printf %s "$PATH"'], { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
1472
|
+
entries.push(...(output.split("\n").filter(Boolean).at(-1) ?? "").split(separator));
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
entries.push(...inherited.split(separator));
|
|
1477
|
+
if (platform !== "win32") {
|
|
1478
|
+
const home = homedir2();
|
|
1479
|
+
if (home) entries.push(...[".local/bin", ".bun/bin", ".npm-global/bin", ".cargo/bin"].map((folder) => join2(home, folder)));
|
|
1480
|
+
entries.push("/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin");
|
|
1481
|
+
}
|
|
1482
|
+
const path = [...new Set(entries.filter(Boolean))].join(separator);
|
|
1483
|
+
resolved = { platform, inherited, path };
|
|
1484
|
+
return path;
|
|
1485
|
+
}
|
|
1486
|
+
function pathVariableName(environment) {
|
|
1487
|
+
return Object.keys(environment).find((name) => name.toLowerCase() === "path") ?? "PATH";
|
|
1488
|
+
}
|
|
1489
|
+
function pathSeparator(platform) {
|
|
1490
|
+
return platform === "win32" ? ";" : delimiter;
|
|
1491
|
+
}
|
|
1492
|
+
function isExecutable(path) {
|
|
1493
|
+
try {
|
|
1494
|
+
accessSync(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
1495
|
+
return statSync2(path).isFile();
|
|
1496
|
+
} catch {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function findBinary(name) {
|
|
1501
|
+
const platform = process.platform;
|
|
1502
|
+
const suffixes = platform === "win32" && !extname(name) ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [];
|
|
1503
|
+
const names = [name, ...suffixes.map((suffix) => `${name}${suffix}`)];
|
|
1504
|
+
if (isAbsolute2(name)) {
|
|
1505
|
+
for (const candidate of names) if (isExecutable(candidate)) return candidate;
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
for (const directory of searchPath().split(pathSeparator(platform))) {
|
|
1509
|
+
for (const candidateName of names) {
|
|
1510
|
+
const candidate = join2(directory, candidateName);
|
|
1511
|
+
if (isExecutable(candidate)) return candidate;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/core/shell.ts
|
|
1518
|
+
var POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"];
|
|
1519
|
+
var POWERSHELL_UTF8 = "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
|
|
1520
|
+
var cached;
|
|
1521
|
+
function hostShell() {
|
|
1522
|
+
if (cached === void 0) cached = resolveShell(process.platform);
|
|
1523
|
+
return cached;
|
|
1524
|
+
}
|
|
1525
|
+
function resolveShell(platform, lookup = {}) {
|
|
1526
|
+
const env = lookup.env ?? process.env;
|
|
1527
|
+
const exists = lookup.exists ?? isFile;
|
|
1528
|
+
const onPath = lookup.onPath ?? findBinary;
|
|
1529
|
+
if (platform !== "win32") {
|
|
1530
|
+
const executable = ["/bin/bash", "/usr/bin/bash"].find((candidate) => exists(candidate));
|
|
1531
|
+
return executable ? { kind: "bash", executable, args: ["-c"] } : null;
|
|
1532
|
+
}
|
|
1533
|
+
const gitBash = [env.ProgramFiles, env["ProgramFiles(x86)"]].filter((folder) => Boolean(folder)).map((folder) => win32.join(folder, "Git", "bin", "bash.exe")).find((candidate) => exists(candidate));
|
|
1534
|
+
if (gitBash) return { kind: "bash", executable: gitBash, args: ["-c"] };
|
|
1535
|
+
const bashOnPath = onPath("bash.exe");
|
|
1536
|
+
if (bashOnPath && !isWslLauncher(bashOnPath, env)) return { kind: "bash", executable: bashOnPath, args: ["-c"] };
|
|
1537
|
+
const existing = (folder, ...parts) => {
|
|
1538
|
+
if (!folder) return null;
|
|
1539
|
+
const candidate = win32.join(folder, ...parts);
|
|
1540
|
+
return exists(candidate) ? candidate : null;
|
|
1541
|
+
};
|
|
1542
|
+
const powershell = onPath("pwsh.exe") ?? existing(env.ProgramFiles, "PowerShell", "7", "pwsh.exe") ?? onPath("powershell.exe") ?? existing(env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
1543
|
+
return powershell ? { kind: "powershell", executable: powershell, args: POWERSHELL_ARGS } : null;
|
|
1544
|
+
}
|
|
1545
|
+
function shellArguments(shell, command) {
|
|
1546
|
+
return [...shell.args, shell.kind === "powershell" ? `${POWERSHELL_UTF8}${command}` : command];
|
|
1547
|
+
}
|
|
1548
|
+
function shellLabel(shell) {
|
|
1549
|
+
if (shell.kind === "powershell") return "PowerShell";
|
|
1550
|
+
return /\\git\\/iu.test(shell.executable) ? "Git Bash" : "Bash";
|
|
1551
|
+
}
|
|
1552
|
+
function isWslLauncher(path, env) {
|
|
1553
|
+
const normalized = win32.normalize(path).toLowerCase();
|
|
1554
|
+
const systemRoot = env.SystemRoot;
|
|
1555
|
+
if (systemRoot) {
|
|
1556
|
+
return ["System32", "Sysnative"].some((folder) => normalized === win32.join(systemRoot, folder, "bash.exe").toLowerCase());
|
|
1557
|
+
}
|
|
1558
|
+
return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/u.test(normalized);
|
|
1559
|
+
}
|
|
1560
|
+
function isFile(path) {
|
|
1561
|
+
try {
|
|
1562
|
+
accessSync2(path, constants2.F_OK);
|
|
1563
|
+
return statSync3(path).isFile();
|
|
1564
|
+
} catch {
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1431
1569
|
// src/core/workspaceSandbox.ts
|
|
1432
1570
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1433
1571
|
import { spawn } from "node:child_process";
|
|
1434
1572
|
import { randomUUID } from "node:crypto";
|
|
1435
|
-
import { accessSync, constants, existsSync, lstatSync as lstatSync2, mkdtempSync, realpathSync as realpathSync2, readdirSync, rmSync, statSync as
|
|
1573
|
+
import { accessSync as accessSync3, constants as constants3, existsSync, lstatSync as lstatSync2, mkdtempSync, realpathSync as realpathSync2, readdirSync, rmSync, statSync as statSync4 } from "node:fs";
|
|
1436
1574
|
import { tmpdir } from "node:os";
|
|
1437
|
-
import { join as
|
|
1575
|
+
import { join as join3 } from "node:path";
|
|
1576
|
+
import { spawnSync } from "node:child_process";
|
|
1438
1577
|
var MARKER_PREFIX = "__XINGULAR_WORKSPACE_SANDBOX_STARTED_";
|
|
1439
1578
|
var bashWriteScope = new AsyncLocalStorage();
|
|
1440
1579
|
function runWithWorkspaceWriteScope(scope, action) {
|
|
1441
1580
|
return bashWriteScope.run(scope, action);
|
|
1442
1581
|
}
|
|
1582
|
+
var WINDOWS_UNSANDBOXED_REASON = "Windows has no workspace write-boundary sandbox, so this command would run without one. It will not run until this one-call request is approved.";
|
|
1583
|
+
var SANDBOX_BACKENDS = {
|
|
1584
|
+
darwin: ["/usr/bin/sandbox-exec"],
|
|
1585
|
+
linux: ["/usr/bin/bwrap", "/bin/bwrap"]
|
|
1586
|
+
};
|
|
1587
|
+
function workspaceSandboxBackend(platform = process.platform) {
|
|
1588
|
+
const candidates = SANDBOX_BACKENDS[platform];
|
|
1589
|
+
return candidates ? systemExecutable(candidates) : null;
|
|
1590
|
+
}
|
|
1443
1591
|
function systemExecutable(candidates) {
|
|
1444
1592
|
for (const candidate of candidates) {
|
|
1445
1593
|
try {
|
|
1446
|
-
|
|
1447
|
-
if (
|
|
1594
|
+
accessSync3(candidate, constants3.X_OK);
|
|
1595
|
+
if (statSync4(candidate).isFile()) return candidate;
|
|
1448
1596
|
} catch {
|
|
1449
1597
|
}
|
|
1450
1598
|
}
|
|
@@ -1455,6 +1603,16 @@ function shellPath() {
|
|
|
1455
1603
|
if (!shell) throw new Error("The workspace write-boundary sandbox requires Bash, but no Bash executable was found.");
|
|
1456
1604
|
return shell;
|
|
1457
1605
|
}
|
|
1606
|
+
function requireShell(resolve3) {
|
|
1607
|
+
const shell = resolve3();
|
|
1608
|
+
if (!shell) {
|
|
1609
|
+
throw new Error(process.platform === "win32" ? "No shell was found: neither Git Bash nor PowerShell is available. Bash was not run." : "No Bash executable was found. Bash was not run.");
|
|
1610
|
+
}
|
|
1611
|
+
return shell;
|
|
1612
|
+
}
|
|
1613
|
+
function unconfinedCommand(shell, command) {
|
|
1614
|
+
return { executable: shell.executable, args: shellArguments(shell, command), marker: "" };
|
|
1615
|
+
}
|
|
1458
1616
|
function sbplString(value) {
|
|
1459
1617
|
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n")}"`;
|
|
1460
1618
|
}
|
|
@@ -1496,12 +1654,12 @@ ${command}`;
|
|
|
1496
1654
|
if (readOnlyTempRoot !== null) return readOnlySandboxCommand(platform, realpathSync2(readOnlyTempRoot), cwd, shell, wrappedCommand, marker);
|
|
1497
1655
|
const writeRoots = [...new Set([workspaceRoot, ...additionalWriteRoots].map((root) => realpathSync2(root)))].sort((left, right) => left.length - right.length);
|
|
1498
1656
|
if (platform === "darwin") {
|
|
1499
|
-
const executable =
|
|
1657
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1500
1658
|
if (!executable) throw new Error("Workspace write-boundary sandbox unavailable: sandbox-exec was not found. Bash was not run.");
|
|
1501
1659
|
return { executable, args: ["-p", macSandboxProfile(writeRoots), shell, "-c", wrappedCommand], marker };
|
|
1502
1660
|
}
|
|
1503
1661
|
if (platform === "linux") {
|
|
1504
|
-
const executable =
|
|
1662
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1505
1663
|
if (!executable) throw new Error("Workspace write-boundary sandbox unavailable: install bubblewrap (bwrap). Bash was not run.");
|
|
1506
1664
|
return {
|
|
1507
1665
|
executable,
|
|
@@ -1530,12 +1688,12 @@ ${command}`;
|
|
|
1530
1688
|
}
|
|
1531
1689
|
function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand, marker) {
|
|
1532
1690
|
if (platform === "darwin") {
|
|
1533
|
-
const executable =
|
|
1691
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1534
1692
|
if (!executable) throw new Error("Read-only Bash sandbox unavailable: sandbox-exec was not found. Bash was not run.");
|
|
1535
1693
|
return { executable, args: ["-p", macReadOnlyProfile(tempRoot), shell, "-c", wrappedCommand], marker };
|
|
1536
1694
|
}
|
|
1537
1695
|
if (platform === "linux") {
|
|
1538
|
-
const executable =
|
|
1696
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1539
1697
|
if (!executable) throw new Error("Read-only Bash sandbox unavailable: install bubblewrap (bwrap). Bash was not run.");
|
|
1540
1698
|
return {
|
|
1541
1699
|
executable,
|
|
@@ -1570,7 +1728,11 @@ function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand,
|
|
|
1570
1728
|
throw new Error(`Read-only Bash sandbox unavailable on ${platform}. Bash was not run.`);
|
|
1571
1729
|
}
|
|
1572
1730
|
function killProcess(child) {
|
|
1573
|
-
if (child.pid && process.platform
|
|
1731
|
+
if (child.pid && process.platform === "win32") {
|
|
1732
|
+
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (child.pid) {
|
|
1574
1736
|
try {
|
|
1575
1737
|
process.kill(-child.pid, "SIGKILL");
|
|
1576
1738
|
return;
|
|
@@ -1691,11 +1853,13 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1691
1853
|
const canonicalCwd = realpathSync2(cwd);
|
|
1692
1854
|
if (!isWithinWorkspace(root, canonicalCwd)) throw new Error("The Bash working directory is outside the active project workspace.");
|
|
1693
1855
|
const scope = options.workspaceWriteScope?.() ?? bashWriteScope.getStore() ?? "workspace";
|
|
1856
|
+
const platform = options.platform ?? process.platform;
|
|
1857
|
+
const shell = options.shell ?? hostShell;
|
|
1694
1858
|
if (scope === "read-only") {
|
|
1695
|
-
|
|
1696
|
-
const tempRoot = realpathSync2(mkdtempSync(
|
|
1859
|
+
if (platform === "win32") throw new Error("Plan-mode Bash needs a read-only sandbox, which Windows does not have. Bash was not run.");
|
|
1860
|
+
const tempRoot = realpathSync2(mkdtempSync(join3(tmpdir(), "harness-read-only-bash-")));
|
|
1697
1861
|
try {
|
|
1698
|
-
const spec2 = workspaceSandboxCommand(
|
|
1862
|
+
const spec2 = workspaceSandboxCommand(platform, root, canonicalCwd, command, [], tempRoot);
|
|
1699
1863
|
const env = { ...execOptions.env ?? process.env, TMPDIR: tempRoot };
|
|
1700
1864
|
const result2 = await runProcess(spec2, canonicalCwd, { ...execOptions, env }, spec2.marker);
|
|
1701
1865
|
return { exitCode: result2.exitCode };
|
|
@@ -1710,11 +1874,12 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1710
1874
|
bypass = false;
|
|
1711
1875
|
}
|
|
1712
1876
|
if (bypass) {
|
|
1713
|
-
const result2 = await runProcess(
|
|
1877
|
+
const result2 = await runProcess(unconfinedCommand(requireShell(shell), command), canonicalCwd, execOptions, null);
|
|
1714
1878
|
return { exitCode: result2.exitCode };
|
|
1715
1879
|
}
|
|
1716
|
-
if (scope === "outside") {
|
|
1717
|
-
const
|
|
1880
|
+
if (scope === "outside" || platform === "win32") {
|
|
1881
|
+
const resolved2 = requireShell(shell);
|
|
1882
|
+
const reason = scope === "outside" ? "This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved." : WINDOWS_UNSANDBOXED_REASON;
|
|
1718
1883
|
let approved = false;
|
|
1719
1884
|
try {
|
|
1720
1885
|
approved = await options.requestWorkspaceWriteScope?.({ command, cwd: canonicalCwd, workspaceRoot: root, reason }) ?? false;
|
|
@@ -1722,12 +1887,11 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1722
1887
|
approved = false;
|
|
1723
1888
|
}
|
|
1724
1889
|
if (!approved) throw new Error(`${reason} Bash was not run.`);
|
|
1725
|
-
execOptions.onData(Buffer.from("\n[Outside-workspace writes approved for this command; running once.]\n"));
|
|
1726
|
-
const result2 = await runProcess(
|
|
1890
|
+
if (scope === "outside") execOptions.onData(Buffer.from("\n[Outside-workspace writes approved for this command; running once.]\n"));
|
|
1891
|
+
const result2 = await runProcess(unconfinedCommand(resolved2, command), canonicalCwd, execOptions, null);
|
|
1727
1892
|
return { exitCode: result2.exitCode };
|
|
1728
1893
|
}
|
|
1729
1894
|
assertNoExternalHardlinks(root);
|
|
1730
|
-
const platform = options.platform ?? process.platform;
|
|
1731
1895
|
const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command);
|
|
1732
1896
|
const result = await runProcess(spec, canonicalCwd, execOptions, spec.marker);
|
|
1733
1897
|
return { exitCode: result.exitCode };
|
|
@@ -1736,12 +1900,17 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1736
1900
|
}
|
|
1737
1901
|
|
|
1738
1902
|
// src/core/workspaceBashTool.ts
|
|
1739
|
-
function
|
|
1903
|
+
function createWorkspaceShellTool(pi, cwd, operations, shell = hostShell(), platform = process.platform) {
|
|
1904
|
+
const native = shell?.kind === "powershell" ? { ...pi.createPowerShellTool(cwd, { operations }), name: "bash" } : pi.createBashTool(cwd, { operations });
|
|
1905
|
+
const note = platform === "win32" && shell ? ` On this machine commands run in ${shellLabel(shell)}. Windows has no workspace sandbox, so each command asks for approval unless Full Permission is on, and plan mode cannot run commands.` : "";
|
|
1906
|
+
return withWorkspaceWriteScope(native, note);
|
|
1907
|
+
}
|
|
1908
|
+
function withWorkspaceWriteScope(tool, note = "") {
|
|
1740
1909
|
const native = tool;
|
|
1741
1910
|
const execute = native.execute.bind(tool);
|
|
1742
1911
|
return {
|
|
1743
1912
|
...tool,
|
|
1744
|
-
description: `${native.description} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
|
|
1913
|
+
description: `${native.description}${note} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
|
|
1745
1914
|
parameters: Type.Object({
|
|
1746
1915
|
command: Type.String({ description: "Shell command to execute" }),
|
|
1747
1916
|
timeout: Type.Optional(Type.Number({ description: `Timeout in seconds (default ${DEFAULT_COMMAND_TIMEOUT_SECONDS})` })),
|
|
@@ -1767,7 +1936,7 @@ function withWorkspaceWriteScope(tool) {
|
|
|
1767
1936
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
1768
1937
|
import { spawn as spawn2 } from "node:child_process";
|
|
1769
1938
|
import { createHash as createHash2 } from "node:crypto";
|
|
1770
|
-
import { constants as fsConstants, realpathSync as realpathSync3, statSync as
|
|
1939
|
+
import { constants as fsConstants, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
|
|
1771
1940
|
import { access, readFile } from "node:fs/promises";
|
|
1772
1941
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
1773
1942
|
var writeAuthorization = new AsyncLocalStorage2();
|
|
@@ -1816,6 +1985,22 @@ try {
|
|
|
1816
1985
|
throw error
|
|
1817
1986
|
}
|
|
1818
1987
|
`;
|
|
1988
|
+
function atomicWriteLaunch(platform, root, additionalRoots) {
|
|
1989
|
+
if (platform === "win32") {
|
|
1990
|
+
const env = {};
|
|
1991
|
+
for (const name of ["SystemRoot", "windir", "TEMP", "TMP", "USERPROFILE"]) {
|
|
1992
|
+
if (process.env[name]) env[name] = process.env[name];
|
|
1993
|
+
}
|
|
1994
|
+
return { executable: process.execPath, args: ["-e", atomicWriteScript], env };
|
|
1995
|
+
}
|
|
1996
|
+
const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
|
|
1997
|
+
const spec = workspaceSandboxCommand(platform, root, root, command, additionalRoots);
|
|
1998
|
+
return {
|
|
1999
|
+
executable: spec.executable,
|
|
2000
|
+
args: spec.args,
|
|
2001
|
+
env: { PATH: "/usr/bin:/bin", ...process.env.HOME ? { HOME: process.env.HOME } : {} }
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
1819
2004
|
function shellQuote(value) {
|
|
1820
2005
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1821
2006
|
}
|
|
@@ -1824,7 +2009,7 @@ function nearestExistingDirectory(path) {
|
|
|
1824
2009
|
while (true) {
|
|
1825
2010
|
try {
|
|
1826
2011
|
const canonical2 = realpathSync3(current);
|
|
1827
|
-
if (!
|
|
2012
|
+
if (!statSync5(canonical2).isDirectory()) throw new Error(`Write scope is not a directory: ${canonical2}`);
|
|
1828
2013
|
return canonical2;
|
|
1829
2014
|
} catch (error) {
|
|
1830
2015
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -1853,17 +2038,12 @@ async function runAtomicWrite(authorization, path, content) {
|
|
|
1853
2038
|
throw new Error("The write authorization no longer matches the active workspace target.");
|
|
1854
2039
|
}
|
|
1855
2040
|
const additionalRoots = outside ? [nearestExistingDirectory(canonicalPath)] : [];
|
|
1856
|
-
const
|
|
1857
|
-
const spec = workspaceSandboxCommand(process.platform, root, root, command, additionalRoots);
|
|
1858
|
-
const env = {
|
|
1859
|
-
PATH: "/usr/bin:/bin",
|
|
1860
|
-
...process.env.HOME ? { HOME: process.env.HOME } : {}
|
|
1861
|
-
};
|
|
2041
|
+
const launch = atomicWriteLaunch(process.platform, root, additionalRoots);
|
|
1862
2042
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
1863
|
-
const child = spawn2(
|
|
2043
|
+
const child = spawn2(launch.executable, launch.args, {
|
|
1864
2044
|
cwd: root,
|
|
1865
2045
|
detached: process.platform !== "win32",
|
|
1866
|
-
env,
|
|
2046
|
+
env: launch.env,
|
|
1867
2047
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1868
2048
|
windowsHide: true
|
|
1869
2049
|
});
|
|
@@ -2051,6 +2231,7 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2051
2231
|
let context = null;
|
|
2052
2232
|
let nativeSubagents = false;
|
|
2053
2233
|
let subagentParent = false;
|
|
2234
|
+
let stoppedChildren = null;
|
|
2054
2235
|
let timer = null;
|
|
2055
2236
|
let polling = false;
|
|
2056
2237
|
let failure;
|
|
@@ -2132,13 +2313,13 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2132
2313
|
},
|
|
2133
2314
|
workspaceWriteScope: () => planRun?.active ? "read-only" : void 0
|
|
2134
2315
|
});
|
|
2135
|
-
pi.registerTool(
|
|
2316
|
+
pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
|
|
2136
2317
|
pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
|
|
2137
2318
|
pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
|
|
2138
2319
|
compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
|
|
2139
2320
|
const slicePolicy = parseWorkSlice(void 0);
|
|
2140
2321
|
const sliceState = createSliceState(slicePolicy);
|
|
2141
|
-
if (slicePolicy.enabled) workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
|
|
2322
|
+
if (slicePolicy.enabled) workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
|
|
2142
2323
|
pi.on("context", (event) => {
|
|
2143
2324
|
const messages = pruneToolImages(event.messages);
|
|
2144
2325
|
return messages === event.messages ? void 0 : { messages };
|
|
@@ -2146,7 +2327,7 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2146
2327
|
pi.on("session_start", async (_event, ctx) => {
|
|
2147
2328
|
context = ctx;
|
|
2148
2329
|
if (timer) clearInterval(timer);
|
|
2149
|
-
await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile() });
|
|
2330
|
+
await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
|
|
2150
2331
|
timer = setInterval(() => {
|
|
2151
2332
|
if (polling || !context) return;
|
|
2152
2333
|
polling = true;
|
|
@@ -2170,13 +2351,16 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2170
2351
|
usage = emptyUsage();
|
|
2171
2352
|
reasoningTokens = void 0;
|
|
2172
2353
|
costReported = false;
|
|
2173
|
-
const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
|
|
2354
|
+
const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "", model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel?.() ?? null });
|
|
2174
2355
|
workspaceRoot = typeof begun.workspaceRoot === "string" ? begun.workspaceRoot : null;
|
|
2175
2356
|
if (!workspaceRoot) throw new Error("The active project workspace could not be resolved; the turn was not started.");
|
|
2357
|
+
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
2358
|
+
stoppedChildren = null;
|
|
2176
2359
|
if (nativeSubagents) {
|
|
2177
|
-
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
2178
2360
|
const snapshot = await controlCall("terminal.subagents.register", { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), plan: planRun?.active === true, history: subagentHistory(ctx.sessionManager.getBranch()) });
|
|
2179
2361
|
if (planRun && Array.isArray(snapshot.connectors)) planRun.connectors = snapshot.connectors;
|
|
2362
|
+
if (planRun && snapshot.subagentProfiles) planRun.subagentProfiles = snapshot.subagentProfiles;
|
|
2363
|
+
stoppedChildren = snapshot.notice ?? null;
|
|
2180
2364
|
subagentParent = true;
|
|
2181
2365
|
}
|
|
2182
2366
|
if (planRun && event.messageType === "user") {
|
|
@@ -2232,6 +2416,11 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2232
2416
|
const instruction = refusal ? `The user's initial Bash command was not run. Reason: ${refusal}. Do not claim that it ran; explain the limitation and ask how to proceed.` : "The user\u2019s initial Bash command has already run. Read its native Bash result and explain what it shows. Do not run the same command again unless the user explicitly asks.";
|
|
2233
2417
|
return { message: { customType: "xingular-initial-bash", content: instruction, display: false, details: {} } };
|
|
2234
2418
|
});
|
|
2419
|
+
pi.on("before_agent_start", async () => {
|
|
2420
|
+
const notice = stoppedChildren;
|
|
2421
|
+
stoppedChildren = null;
|
|
2422
|
+
if (notice) return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
|
|
2423
|
+
});
|
|
2235
2424
|
const deliverChildren = async (event, ctx, settle) => {
|
|
2236
2425
|
if (!nativeSubagents || settle && event.outcome !== "completed" || ctx.signal?.aborted) return;
|
|
2237
2426
|
const result = await withParentCancellation(ctx.signal, () => controlCall("terminal.subagents.delivery", { history: subagentHistory([...ctx.sessionManager.getBranch(), ...event.entries]), settle }, 2147483647));
|
|
@@ -2391,17 +2580,17 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2391
2580
|
}
|
|
2392
2581
|
});
|
|
2393
2582
|
let recorded = Promise.resolve();
|
|
2394
|
-
const
|
|
2395
|
-
recorded = terminalCall("
|
|
2583
|
+
const todo = todoTool({ emit: (_type, payload) => {
|
|
2584
|
+
recorded = terminalCall("todo", payload);
|
|
2396
2585
|
} });
|
|
2397
2586
|
pi.registerTool({
|
|
2398
|
-
name:
|
|
2399
|
-
label:
|
|
2400
|
-
description:
|
|
2401
|
-
parameters: Type2.Unsafe(
|
|
2587
|
+
name: todo.name,
|
|
2588
|
+
label: todo.label,
|
|
2589
|
+
description: todo.description,
|
|
2590
|
+
parameters: Type2.Unsafe(todo.parameters(Type2)),
|
|
2402
2591
|
async execute(_id, input) {
|
|
2403
2592
|
recorded = Promise.resolve();
|
|
2404
|
-
const result = await
|
|
2593
|
+
const result = await todo.execute(input);
|
|
2405
2594
|
await recorded;
|
|
2406
2595
|
return { content: [{ type: "text", text: typeof result === "string" ? result : result.text }], details: void 0 };
|
|
2407
2596
|
}
|