@xingular/harness 1.2.0 → 1.3.1-unstable.1
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 +63 -0
- package/README.md +26 -17
- package/builtin/skills/plugin-authoring/SKILL.md +34 -0
- package/dist/cli.js +7362 -6239
- 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 +16 -4
- package/dist/sdk/agents/profiles.js +75 -12
- package/dist/sdk/annotations/bridge.d.ts +2 -0
- package/dist/sdk/annotations/bridge.js +4 -0
- package/dist/sdk/annotations/capture.d.ts +44 -2
- package/dist/sdk/annotations/capture.js +104 -3
- package/dist/sdk/annotations/delivery.d.ts +24 -17
- package/dist/sdk/annotations/delivery.js +82 -49
- package/dist/sdk/annotations/inspect.js +7 -6
- package/dist/sdk/annotations/pageScript.js +4 -4
- package/dist/sdk/annotations/part.d.ts +0 -2
- package/dist/sdk/annotations/part.js +4 -33
- package/dist/sdk/annotations/queue.d.ts +2 -2
- package/dist/sdk/annotations/queue.js +12 -7
- package/dist/sdk/annotations/serverMethods.js +11 -7
- package/dist/sdk/annotations/store.d.ts +13 -21
- package/dist/sdk/annotations/store.js +47 -37
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/list.js +2 -0
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/links.d.ts +51 -0
- package/dist/sdk/core/links.js +155 -0
- 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/harness.d.ts +5 -0
- package/dist/sdk/harness.js +11 -6
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/plugins/builtin.js +14 -9
- package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
- package/dist/sdk/plugins/builtinRegistry.js +9 -3
- package/dist/sdk/plugins/discover.d.ts +7 -1
- package/dist/sdk/plugins/discover.js +41 -2
- package/dist/sdk/plugins/identity.js +1 -1
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +3 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +21 -4
- package/dist/sdk/plugins/plugins.d.ts +2 -0
- package/dist/sdk/plugins/plugins.js +1 -0
- package/dist/sdk/plugins/proposals.d.ts +17 -0
- package/dist/sdk/plugins/proposals.js +147 -0
- package/dist/sdk/plugins/types.d.ts +1 -1
- package/dist/sdk/plugins/types.js +1 -1
- package/dist/sdk/prompt/compile.d.ts +13 -4
- package/dist/sdk/prompt/compile.js +46 -20
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/server/methods.js +14 -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/sessionState.d.ts +13 -0
- package/dist/sdk/sessions/sessionState.js +31 -0
- package/dist/sdk/sessions/sessions.d.ts +7 -0
- package/dist/sdk/sessions/sessions.js +73 -15
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +195 -55
- 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.d.ts +2 -0
- package/dist/sdk/terminal/opentui/theme.js +2 -0
- package/dist/sdk/terminal/piExtension.js +98 -15
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +16 -0
- package/dist/sdk/terminal/terminals.js +130 -26
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
- package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
- package/dist/sdk/terminal/tui/annotateSend.js +40 -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.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +16 -11
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +12 -2
- package/dist/sdk/terminal/tui/dialogs.js +1 -0
- package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
- package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
- package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
- 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 +14 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
- package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
- package/dist/sdk/tools/defaultSkills.d.ts +4 -0
- package/dist/sdk/tools/defaultSkills.js +20 -0
- package/dist/sdk/tools/definitions.d.ts +3 -1
- package/dist/sdk/tools/definitions.js +17 -1
- package/dist/sdk/tools/dispatch.js +12 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/sessionName.d.ts +9 -0
- package/dist/sdk/tools/sessionName.js +58 -11
- 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/sdk/world/world.d.ts +0 -2
- package/dist/terminal-opentui.js +3331 -1584
- package/dist/terminal-pi.js +421 -133
- package/docs/README.md +3 -0
- 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 +73 -36
- package/docs/architecture/03-variants.md +1 -3
- package/docs/architecture/04-prompt.md +24 -13
- package/docs/architecture/06-plugins-and-capabilities.md +46 -10
- package/docs/architecture/07-tools-and-gate.md +49 -8
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +68 -16
- package/docs/architecture/11-terminal.md +94 -45
- package/docs/architecture/12-storage.md +1 -0
- package/docs/architecture/13-world-tools.md +8 -6
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +20 -18
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
- 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/PROP-0048-simple-annotate.md +85 -0
- package/docs/architecture/proposals/README.md +11 -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/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
- package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
- package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
- package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +16 -13
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/1.3.1-unstable.1.md +8 -0
- package/docs/releases/README.md +9 -6
- package/docs/terminal-tui.md +127 -65
- package/package.json +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
- package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
- 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,20 +1078,47 @@ 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.";
|
|
1082
|
+
function normalizeSessionName(value) {
|
|
1083
|
+
if (typeof value !== "string") throw invalid("Send name as a short sentence of at most six words.");
|
|
1084
|
+
if (/[\x00-\x08\x0e-\x1f\x7f-\x9f]/u.test(value)) {
|
|
1085
|
+
throw invalid("Session names cannot contain terminal escapes or control characters.");
|
|
1086
|
+
}
|
|
1087
|
+
const name = value.replace(/\s+/gu, " ").trim();
|
|
1088
|
+
if (!name || name.split(" ").length > 6) throw invalid("Use a non-empty session name of at most six words.");
|
|
1089
|
+
return name;
|
|
1090
|
+
}
|
|
1091
|
+
var CONVERSATIONAL = /^(?:hi|hello|hey|yo|thanks|thank you|ok|okay|sure|great|nice|cool|sounds good|got it|good morning|good afternoon|good evening|how are you|what'?s up)[.!?]*$/iu;
|
|
1092
|
+
var LEADING = /^(?:please|can you|could you|would you|i want you to|i'd like you to|help me|let'?s|go ahead and)[,\s]+/iu;
|
|
1093
|
+
function deriveSessionName(text4) {
|
|
1094
|
+
if (typeof text4 !== "string") return null;
|
|
1095
|
+
const input = text4.trim();
|
|
1096
|
+
if (!input || input.startsWith("!") || input.startsWith("```") || /^https?:\/\//iu.test(input)) return null;
|
|
1097
|
+
if (CONVERSATIONAL.test(input)) return null;
|
|
1098
|
+
if (/^continue\b/iu.test(input)) return "Continue previous work";
|
|
1099
|
+
const sentence = input.split(/[.!?](?:\s|$)/u)[0] ?? input;
|
|
1100
|
+
const cleaned = sentence.replace(/```[\s\S]*?```/gu, " ").replace(/[`*_~#>]/gu, " ").replace(/[:;,]+/gu, " ").replace(LEADING, "").replace(/\s+/gu, " ").trim();
|
|
1101
|
+
if (!/[a-z]/iu.test(cleaned)) return null;
|
|
1102
|
+
const words = cleaned.split(" ").filter(Boolean).slice(0, 6);
|
|
1103
|
+
if (!words.length) return null;
|
|
1104
|
+
const name = words.join(" ").replace(/^[\p{P}\p{S}]+/u, "").replace(/[\p{P}\p{S}]+$/u, "").trim();
|
|
1105
|
+
if (!name) return null;
|
|
1106
|
+
const capitalized = name[0].toUpperCase() + name.slice(1);
|
|
1107
|
+
try {
|
|
1108
|
+
return normalizeSessionName(capitalized);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1061
1113
|
function sessionNameTool(current) {
|
|
1062
1114
|
return {
|
|
1063
1115
|
name: SESSION_NAME,
|
|
1064
1116
|
label: "Name the session",
|
|
1065
|
-
description:
|
|
1117
|
+
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
1118
|
parameters: (Type3) => Type3.Object({ name: Type3.String({ description: "One short sentence describing the objective, at most six words" }) }),
|
|
1067
1119
|
planModeSafe: () => true,
|
|
1068
1120
|
execute: async (input) => {
|
|
1069
|
-
|
|
1070
|
-
if (/[\x00-\x08\x0e-\x1f\x7f-\x9f]/u.test(input.name)) {
|
|
1071
|
-
throw invalid("Session names cannot contain terminal escapes or control characters.");
|
|
1072
|
-
}
|
|
1073
|
-
const name = input.name.replace(/\s+/gu, " ").trim();
|
|
1074
|
-
if (!name || name.split(" ").length > 6) throw invalid("Use a non-empty session name of at most six words.");
|
|
1121
|
+
const name = normalizeSessionName(input.name);
|
|
1075
1122
|
const session = current();
|
|
1076
1123
|
if (!session) throw invalid("The native session is unavailable; try naming it again once it is ready.");
|
|
1077
1124
|
if (session.sessionName !== name) session.setSessionName(name);
|
|
@@ -1118,12 +1165,13 @@ var WORLD_GUIDANCE = [
|
|
|
1118
1165
|
].join("\n");
|
|
1119
1166
|
|
|
1120
1167
|
// src/prompt/compile.ts
|
|
1121
|
-
var
|
|
1168
|
+
var SESSION_STATE_BUDGET = 1e3;
|
|
1169
|
+
var SECTION_ORDER = { harnessIdentity: 5, system: 25, sessionState: 30, guardrails: 40, memory: 45, references: 50, subagents: 64, scripts: 66, wiki: 80, tools: 90, reporting: 95 };
|
|
1122
1170
|
function clip2(text4, limit) {
|
|
1123
1171
|
return charCount(text4) <= limit ? text4 : `${takeChars(text4, limit)}\u2026`;
|
|
1124
1172
|
}
|
|
1125
1173
|
function renderCompactionCheckpoint(input) {
|
|
1126
|
-
if (!input.
|
|
1174
|
+
if (!input.todo.length && !input.planInstructions) return null;
|
|
1127
1175
|
const parts = [
|
|
1128
1176
|
"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
1177
|
input.goal.trim() ? `## Current request
|
|
@@ -1133,18 +1181,18 @@ ${clip2(input.goal.trim(), 1e3)}` : "",
|
|
|
1133
1181
|
|
|
1134
1182
|
${clip2(input.planInstructions, 4e3)}` : ""
|
|
1135
1183
|
].filter(Boolean);
|
|
1136
|
-
if (input.
|
|
1137
|
-
const active = input.
|
|
1138
|
-
const completed = input.
|
|
1139
|
-
const
|
|
1184
|
+
if (input.todo.length) {
|
|
1185
|
+
const active = input.todo.filter((item) => item.status !== "completed");
|
|
1186
|
+
const completed = input.todo.filter((item) => item.status === "completed");
|
|
1187
|
+
const todo = [
|
|
1140
1188
|
active.length ? `### In progress and pending
|
|
1141
|
-
${clip2(
|
|
1189
|
+
${clip2(describeTodo(active), 2500)}` : "",
|
|
1142
1190
|
completed.length ? `### Completed (${completed.length})
|
|
1143
|
-
${clip2(
|
|
1191
|
+
${clip2(describeTodo(completed), 500)}` : ""
|
|
1144
1192
|
].filter(Boolean).join("\n\n");
|
|
1145
|
-
parts.push(`## Current
|
|
1193
|
+
parts.push(`## Current todo list
|
|
1146
1194
|
|
|
1147
|
-
${
|
|
1195
|
+
${todo}`);
|
|
1148
1196
|
}
|
|
1149
1197
|
const body = clip2(parts.join("\n\n"), 8e3);
|
|
1150
1198
|
return `[Harness context update \u2014 not a user message]
|
|
@@ -1153,7 +1201,19 @@ ${checklist}`);
|
|
|
1153
1201
|
|
|
1154
1202
|
${body}`;
|
|
1155
1203
|
}
|
|
1204
|
+
function sessionStateText(state, options) {
|
|
1205
|
+
const parts = [];
|
|
1206
|
+
if (options.name) parts.push(`Name: ${state.name ? JSON.stringify(state.name) : "unnamed"}.`);
|
|
1207
|
+
if (options.todo) {
|
|
1208
|
+
parts.push(`Todo for this run: ${describeTodo(state.todo)}`);
|
|
1209
|
+
if (state.previousTodo?.length) parts.push(`Previous run's todo (context, not this run's list): ${describeTodo(state.previousTodo)}`);
|
|
1210
|
+
}
|
|
1211
|
+
if (!parts.length) return null;
|
|
1212
|
+
parts.push(state.firstTurn ? "This is the first turn in this native session." : "This run continues the conversation; the todo list resets for each run.");
|
|
1213
|
+
return clip2(parts.join("\n"), SESSION_STATE_BUDGET);
|
|
1214
|
+
}
|
|
1156
1215
|
var BUILT_IN_DYNAMIC = [
|
|
1216
|
+
{ id: "session-state", order: SECTION_ORDER.sessionState },
|
|
1157
1217
|
{ id: "references", order: SECTION_ORDER.references },
|
|
1158
1218
|
{ id: "wiki", order: SECTION_ORDER.wiki }
|
|
1159
1219
|
];
|
|
@@ -1165,7 +1225,7 @@ var PLAN_READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1165
1225
|
"find",
|
|
1166
1226
|
"ls",
|
|
1167
1227
|
SESSION_NAME,
|
|
1168
|
-
|
|
1228
|
+
TODO_WRITE,
|
|
1169
1229
|
ASK_USER,
|
|
1170
1230
|
"ask_user_question",
|
|
1171
1231
|
GET_CAPABILITIES,
|
|
@@ -1264,12 +1324,12 @@ function planRefusal(tool, readOnly, args, planModeSafe) {
|
|
|
1264
1324
|
|
|
1265
1325
|
// src/sessions/compactionCheckpoint.ts
|
|
1266
1326
|
var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
|
|
1267
|
-
function
|
|
1268
|
-
if (!Array.isArray(value) || value.length >
|
|
1327
|
+
function validatedTodo(value) {
|
|
1328
|
+
if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS) return null;
|
|
1269
1329
|
const items = [];
|
|
1270
1330
|
for (const valueItem of value) {
|
|
1271
1331
|
const item = valueItem;
|
|
1272
|
-
const parsed =
|
|
1332
|
+
const parsed = todoItemSchema.safeParse({
|
|
1273
1333
|
text: typeof item?.text === "string" ? item.text.trim() : item?.text,
|
|
1274
1334
|
status: item?.status
|
|
1275
1335
|
});
|
|
@@ -1281,26 +1341,26 @@ function validatedChecklist(value) {
|
|
|
1281
1341
|
function compactionCheckpoint(options = {}) {
|
|
1282
1342
|
return (pi) => {
|
|
1283
1343
|
let goal = "";
|
|
1284
|
-
let
|
|
1344
|
+
let todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
1285
1345
|
let lastCompactionKey = null;
|
|
1286
1346
|
let compacted = false;
|
|
1287
1347
|
const reset = () => {
|
|
1288
1348
|
goal = "";
|
|
1289
|
-
|
|
1349
|
+
todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
1290
1350
|
lastCompactionKey = null;
|
|
1291
1351
|
compacted = false;
|
|
1292
1352
|
};
|
|
1293
1353
|
pi.on("before_agent_start", (event) => {
|
|
1294
1354
|
if (event.messageType !== "user") return;
|
|
1295
1355
|
goal = event.prompt;
|
|
1296
|
-
|
|
1356
|
+
todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
1297
1357
|
lastCompactionKey = null;
|
|
1298
1358
|
compacted = false;
|
|
1299
1359
|
});
|
|
1300
1360
|
pi.on("tool_result", (event) => {
|
|
1301
|
-
if (event.toolName !==
|
|
1302
|
-
const items =
|
|
1303
|
-
if (items)
|
|
1361
|
+
if (event.toolName !== TODO_WRITE || event.isError) return;
|
|
1362
|
+
const items = validatedTodo(event.input.items);
|
|
1363
|
+
if (items) todo = items;
|
|
1304
1364
|
});
|
|
1305
1365
|
pi.on("context", (event) => {
|
|
1306
1366
|
const summary = event.messages.findLast((message) => message.role === "compactionSummary");
|
|
@@ -1314,7 +1374,7 @@ function compactionCheckpoint(options = {}) {
|
|
|
1314
1374
|
compacted = false;
|
|
1315
1375
|
}
|
|
1316
1376
|
if (!compacted) return;
|
|
1317
|
-
const content = renderCompactionCheckpoint({ goal,
|
|
1377
|
+
const content = renderCompactionCheckpoint({ goal, todo, planInstructions });
|
|
1318
1378
|
if (!content) return;
|
|
1319
1379
|
return {
|
|
1320
1380
|
messages: [...event.messages, {
|
|
@@ -1428,23 +1488,150 @@ function inspectWorkspaceWrite(workspaceRoot, cwd, tool, input) {
|
|
|
1428
1488
|
// src/core/workspaceBashTool.ts
|
|
1429
1489
|
import { Type } from "typebox";
|
|
1430
1490
|
|
|
1491
|
+
// src/core/shell.ts
|
|
1492
|
+
import { accessSync as accessSync2, constants as constants2, statSync as statSync3 } from "node:fs";
|
|
1493
|
+
import { win32 } from "node:path";
|
|
1494
|
+
|
|
1495
|
+
// src/core/path.ts
|
|
1496
|
+
import { execFileSync } from "node:child_process";
|
|
1497
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
1498
|
+
import { homedir as homedir2 } from "node:os";
|
|
1499
|
+
import { delimiter, extname, isAbsolute as isAbsolute2, join as join2 } from "node:path";
|
|
1500
|
+
var resolved = null;
|
|
1501
|
+
function searchPath() {
|
|
1502
|
+
const platform = process.platform;
|
|
1503
|
+
const pathKey = pathVariableName(process.env);
|
|
1504
|
+
const inherited = process.env[pathKey] ?? "";
|
|
1505
|
+
if (resolved?.platform === platform && resolved.inherited === inherited) return resolved.path;
|
|
1506
|
+
const separator = pathSeparator(platform);
|
|
1507
|
+
const entries = [];
|
|
1508
|
+
if (platform !== "win32") {
|
|
1509
|
+
try {
|
|
1510
|
+
const output = execFileSync(process.env.SHELL || "/bin/zsh", ["-ilc", 'printf %s "$PATH"'], { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
1511
|
+
entries.push(...(output.split("\n").filter(Boolean).at(-1) ?? "").split(separator));
|
|
1512
|
+
} catch {
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
entries.push(...inherited.split(separator));
|
|
1516
|
+
if (platform !== "win32") {
|
|
1517
|
+
const home = homedir2();
|
|
1518
|
+
if (home) entries.push(...[".local/bin", ".bun/bin", ".npm-global/bin", ".cargo/bin"].map((folder) => join2(home, folder)));
|
|
1519
|
+
entries.push("/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin");
|
|
1520
|
+
}
|
|
1521
|
+
const path = [...new Set(entries.filter(Boolean))].join(separator);
|
|
1522
|
+
resolved = { platform, inherited, path };
|
|
1523
|
+
return path;
|
|
1524
|
+
}
|
|
1525
|
+
function pathVariableName(environment) {
|
|
1526
|
+
return Object.keys(environment).find((name) => name.toLowerCase() === "path") ?? "PATH";
|
|
1527
|
+
}
|
|
1528
|
+
function pathSeparator(platform) {
|
|
1529
|
+
return platform === "win32" ? ";" : delimiter;
|
|
1530
|
+
}
|
|
1531
|
+
function isExecutable(path) {
|
|
1532
|
+
try {
|
|
1533
|
+
accessSync(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
1534
|
+
return statSync2(path).isFile();
|
|
1535
|
+
} catch {
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function findBinary(name) {
|
|
1540
|
+
const platform = process.platform;
|
|
1541
|
+
const suffixes = platform === "win32" && !extname(name) ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [];
|
|
1542
|
+
const names = [name, ...suffixes.map((suffix) => `${name}${suffix}`)];
|
|
1543
|
+
if (isAbsolute2(name)) {
|
|
1544
|
+
for (const candidate of names) if (isExecutable(candidate)) return candidate;
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
for (const directory of searchPath().split(pathSeparator(platform))) {
|
|
1548
|
+
for (const candidateName of names) {
|
|
1549
|
+
const candidate = join2(directory, candidateName);
|
|
1550
|
+
if (isExecutable(candidate)) return candidate;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/core/shell.ts
|
|
1557
|
+
var POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"];
|
|
1558
|
+
var POWERSHELL_UTF8 = "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
|
|
1559
|
+
var cached;
|
|
1560
|
+
function hostShell() {
|
|
1561
|
+
if (cached === void 0) cached = resolveShell(process.platform);
|
|
1562
|
+
return cached;
|
|
1563
|
+
}
|
|
1564
|
+
function resolveShell(platform, lookup = {}) {
|
|
1565
|
+
const env = lookup.env ?? process.env;
|
|
1566
|
+
const exists = lookup.exists ?? isFile;
|
|
1567
|
+
const onPath = lookup.onPath ?? findBinary;
|
|
1568
|
+
if (platform !== "win32") {
|
|
1569
|
+
const executable = ["/bin/bash", "/usr/bin/bash"].find((candidate) => exists(candidate));
|
|
1570
|
+
return executable ? { kind: "bash", executable, args: ["-c"] } : null;
|
|
1571
|
+
}
|
|
1572
|
+
const gitBash = [env.ProgramFiles, env["ProgramFiles(x86)"]].filter((folder) => Boolean(folder)).map((folder) => win32.join(folder, "Git", "bin", "bash.exe")).find((candidate) => exists(candidate));
|
|
1573
|
+
if (gitBash) return { kind: "bash", executable: gitBash, args: ["-c"] };
|
|
1574
|
+
const bashOnPath = onPath("bash.exe");
|
|
1575
|
+
if (bashOnPath && !isWslLauncher(bashOnPath, env)) return { kind: "bash", executable: bashOnPath, args: ["-c"] };
|
|
1576
|
+
const existing = (folder, ...parts) => {
|
|
1577
|
+
if (!folder) return null;
|
|
1578
|
+
const candidate = win32.join(folder, ...parts);
|
|
1579
|
+
return exists(candidate) ? candidate : null;
|
|
1580
|
+
};
|
|
1581
|
+
const powershell = onPath("pwsh.exe") ?? existing(env.ProgramFiles, "PowerShell", "7", "pwsh.exe") ?? onPath("powershell.exe") ?? existing(env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
1582
|
+
return powershell ? { kind: "powershell", executable: powershell, args: POWERSHELL_ARGS } : null;
|
|
1583
|
+
}
|
|
1584
|
+
function shellArguments(shell, command) {
|
|
1585
|
+
return [...shell.args, shell.kind === "powershell" ? `${POWERSHELL_UTF8}${command}` : command];
|
|
1586
|
+
}
|
|
1587
|
+
function shellLabel(shell) {
|
|
1588
|
+
if (shell.kind === "powershell") return "PowerShell";
|
|
1589
|
+
return /\\git\\/iu.test(shell.executable) ? "Git Bash" : "Bash";
|
|
1590
|
+
}
|
|
1591
|
+
function isWslLauncher(path, env) {
|
|
1592
|
+
const normalized = win32.normalize(path).toLowerCase();
|
|
1593
|
+
const systemRoot = env.SystemRoot;
|
|
1594
|
+
if (systemRoot) {
|
|
1595
|
+
return ["System32", "Sysnative"].some((folder) => normalized === win32.join(systemRoot, folder, "bash.exe").toLowerCase());
|
|
1596
|
+
}
|
|
1597
|
+
return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/u.test(normalized);
|
|
1598
|
+
}
|
|
1599
|
+
function isFile(path) {
|
|
1600
|
+
try {
|
|
1601
|
+
accessSync2(path, constants2.F_OK);
|
|
1602
|
+
return statSync3(path).isFile();
|
|
1603
|
+
} catch {
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1431
1608
|
// src/core/workspaceSandbox.ts
|
|
1432
1609
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1433
1610
|
import { spawn } from "node:child_process";
|
|
1434
1611
|
import { randomUUID } from "node:crypto";
|
|
1435
|
-
import { accessSync, constants, existsSync, lstatSync as lstatSync2, mkdtempSync, realpathSync as realpathSync2, readdirSync, rmSync, statSync as
|
|
1612
|
+
import { accessSync as accessSync3, constants as constants3, existsSync, lstatSync as lstatSync2, mkdtempSync, realpathSync as realpathSync2, readdirSync, rmSync, statSync as statSync4 } from "node:fs";
|
|
1436
1613
|
import { tmpdir } from "node:os";
|
|
1437
|
-
import { join as
|
|
1614
|
+
import { join as join3 } from "node:path";
|
|
1615
|
+
import { spawnSync } from "node:child_process";
|
|
1438
1616
|
var MARKER_PREFIX = "__XINGULAR_WORKSPACE_SANDBOX_STARTED_";
|
|
1439
1617
|
var bashWriteScope = new AsyncLocalStorage();
|
|
1440
1618
|
function runWithWorkspaceWriteScope(scope, action) {
|
|
1441
1619
|
return bashWriteScope.run(scope, action);
|
|
1442
1620
|
}
|
|
1621
|
+
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.";
|
|
1622
|
+
var SANDBOX_BACKENDS = {
|
|
1623
|
+
darwin: ["/usr/bin/sandbox-exec"],
|
|
1624
|
+
linux: ["/usr/bin/bwrap", "/bin/bwrap"]
|
|
1625
|
+
};
|
|
1626
|
+
function workspaceSandboxBackend(platform = process.platform) {
|
|
1627
|
+
const candidates = SANDBOX_BACKENDS[platform];
|
|
1628
|
+
return candidates ? systemExecutable(candidates) : null;
|
|
1629
|
+
}
|
|
1443
1630
|
function systemExecutable(candidates) {
|
|
1444
1631
|
for (const candidate of candidates) {
|
|
1445
1632
|
try {
|
|
1446
|
-
|
|
1447
|
-
if (
|
|
1633
|
+
accessSync3(candidate, constants3.X_OK);
|
|
1634
|
+
if (statSync4(candidate).isFile()) return candidate;
|
|
1448
1635
|
} catch {
|
|
1449
1636
|
}
|
|
1450
1637
|
}
|
|
@@ -1455,6 +1642,16 @@ function shellPath() {
|
|
|
1455
1642
|
if (!shell) throw new Error("The workspace write-boundary sandbox requires Bash, but no Bash executable was found.");
|
|
1456
1643
|
return shell;
|
|
1457
1644
|
}
|
|
1645
|
+
function requireShell(resolve3) {
|
|
1646
|
+
const shell = resolve3();
|
|
1647
|
+
if (!shell) {
|
|
1648
|
+
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.");
|
|
1649
|
+
}
|
|
1650
|
+
return shell;
|
|
1651
|
+
}
|
|
1652
|
+
function unconfinedCommand(shell, command) {
|
|
1653
|
+
return { executable: shell.executable, args: shellArguments(shell, command), marker: "" };
|
|
1654
|
+
}
|
|
1458
1655
|
function sbplString(value) {
|
|
1459
1656
|
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n")}"`;
|
|
1460
1657
|
}
|
|
@@ -1496,12 +1693,12 @@ ${command}`;
|
|
|
1496
1693
|
if (readOnlyTempRoot !== null) return readOnlySandboxCommand(platform, realpathSync2(readOnlyTempRoot), cwd, shell, wrappedCommand, marker);
|
|
1497
1694
|
const writeRoots = [...new Set([workspaceRoot, ...additionalWriteRoots].map((root) => realpathSync2(root)))].sort((left, right) => left.length - right.length);
|
|
1498
1695
|
if (platform === "darwin") {
|
|
1499
|
-
const executable =
|
|
1696
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1500
1697
|
if (!executable) throw new Error("Workspace write-boundary sandbox unavailable: sandbox-exec was not found. Bash was not run.");
|
|
1501
1698
|
return { executable, args: ["-p", macSandboxProfile(writeRoots), shell, "-c", wrappedCommand], marker };
|
|
1502
1699
|
}
|
|
1503
1700
|
if (platform === "linux") {
|
|
1504
|
-
const executable =
|
|
1701
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1505
1702
|
if (!executable) throw new Error("Workspace write-boundary sandbox unavailable: install bubblewrap (bwrap). Bash was not run.");
|
|
1506
1703
|
return {
|
|
1507
1704
|
executable,
|
|
@@ -1530,12 +1727,12 @@ ${command}`;
|
|
|
1530
1727
|
}
|
|
1531
1728
|
function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand, marker) {
|
|
1532
1729
|
if (platform === "darwin") {
|
|
1533
|
-
const executable =
|
|
1730
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1534
1731
|
if (!executable) throw new Error("Read-only Bash sandbox unavailable: sandbox-exec was not found. Bash was not run.");
|
|
1535
1732
|
return { executable, args: ["-p", macReadOnlyProfile(tempRoot), shell, "-c", wrappedCommand], marker };
|
|
1536
1733
|
}
|
|
1537
1734
|
if (platform === "linux") {
|
|
1538
|
-
const executable =
|
|
1735
|
+
const executable = workspaceSandboxBackend(platform);
|
|
1539
1736
|
if (!executable) throw new Error("Read-only Bash sandbox unavailable: install bubblewrap (bwrap). Bash was not run.");
|
|
1540
1737
|
return {
|
|
1541
1738
|
executable,
|
|
@@ -1570,7 +1767,11 @@ function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand,
|
|
|
1570
1767
|
throw new Error(`Read-only Bash sandbox unavailable on ${platform}. Bash was not run.`);
|
|
1571
1768
|
}
|
|
1572
1769
|
function killProcess(child) {
|
|
1573
|
-
if (child.pid && process.platform
|
|
1770
|
+
if (child.pid && process.platform === "win32") {
|
|
1771
|
+
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (child.pid) {
|
|
1574
1775
|
try {
|
|
1575
1776
|
process.kill(-child.pid, "SIGKILL");
|
|
1576
1777
|
return;
|
|
@@ -1691,11 +1892,13 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1691
1892
|
const canonicalCwd = realpathSync2(cwd);
|
|
1692
1893
|
if (!isWithinWorkspace(root, canonicalCwd)) throw new Error("The Bash working directory is outside the active project workspace.");
|
|
1693
1894
|
const scope = options.workspaceWriteScope?.() ?? bashWriteScope.getStore() ?? "workspace";
|
|
1895
|
+
const platform = options.platform ?? process.platform;
|
|
1896
|
+
const shell = options.shell ?? hostShell;
|
|
1694
1897
|
if (scope === "read-only") {
|
|
1695
|
-
|
|
1696
|
-
const tempRoot = realpathSync2(mkdtempSync(
|
|
1898
|
+
if (platform === "win32") throw new Error("Plan-mode Bash needs a read-only sandbox, which Windows does not have. Bash was not run.");
|
|
1899
|
+
const tempRoot = realpathSync2(mkdtempSync(join3(tmpdir(), "harness-read-only-bash-")));
|
|
1697
1900
|
try {
|
|
1698
|
-
const spec2 = workspaceSandboxCommand(
|
|
1901
|
+
const spec2 = workspaceSandboxCommand(platform, root, canonicalCwd, command, [], tempRoot);
|
|
1699
1902
|
const env = { ...execOptions.env ?? process.env, TMPDIR: tempRoot };
|
|
1700
1903
|
const result2 = await runProcess(spec2, canonicalCwd, { ...execOptions, env }, spec2.marker);
|
|
1701
1904
|
return { exitCode: result2.exitCode };
|
|
@@ -1710,11 +1913,12 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1710
1913
|
bypass = false;
|
|
1711
1914
|
}
|
|
1712
1915
|
if (bypass) {
|
|
1713
|
-
const result2 = await runProcess(
|
|
1916
|
+
const result2 = await runProcess(unconfinedCommand(requireShell(shell), command), canonicalCwd, execOptions, null);
|
|
1714
1917
|
return { exitCode: result2.exitCode };
|
|
1715
1918
|
}
|
|
1716
|
-
if (scope === "outside") {
|
|
1717
|
-
const
|
|
1919
|
+
if (scope === "outside" || platform === "win32") {
|
|
1920
|
+
const resolved2 = requireShell(shell);
|
|
1921
|
+
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
1922
|
let approved = false;
|
|
1719
1923
|
try {
|
|
1720
1924
|
approved = await options.requestWorkspaceWriteScope?.({ command, cwd: canonicalCwd, workspaceRoot: root, reason }) ?? false;
|
|
@@ -1722,12 +1926,11 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1722
1926
|
approved = false;
|
|
1723
1927
|
}
|
|
1724
1928
|
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(
|
|
1929
|
+
if (scope === "outside") execOptions.onData(Buffer.from("\n[Outside-workspace writes approved for this command; running once.]\n"));
|
|
1930
|
+
const result2 = await runProcess(unconfinedCommand(resolved2, command), canonicalCwd, execOptions, null);
|
|
1727
1931
|
return { exitCode: result2.exitCode };
|
|
1728
1932
|
}
|
|
1729
1933
|
assertNoExternalHardlinks(root);
|
|
1730
|
-
const platform = options.platform ?? process.platform;
|
|
1731
1934
|
const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command);
|
|
1732
1935
|
const result = await runProcess(spec, canonicalCwd, execOptions, spec.marker);
|
|
1733
1936
|
return { exitCode: result.exitCode };
|
|
@@ -1736,12 +1939,17 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
1736
1939
|
}
|
|
1737
1940
|
|
|
1738
1941
|
// src/core/workspaceBashTool.ts
|
|
1739
|
-
function
|
|
1942
|
+
function createWorkspaceShellTool(pi, cwd, operations, shell = hostShell(), platform = process.platform) {
|
|
1943
|
+
const native = shell?.kind === "powershell" ? { ...pi.createPowerShellTool(cwd, { operations }), name: "bash" } : pi.createBashTool(cwd, { operations });
|
|
1944
|
+
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.` : "";
|
|
1945
|
+
return withWorkspaceWriteScope(native, note);
|
|
1946
|
+
}
|
|
1947
|
+
function withWorkspaceWriteScope(tool, note = "") {
|
|
1740
1948
|
const native = tool;
|
|
1741
1949
|
const execute = native.execute.bind(tool);
|
|
1742
1950
|
return {
|
|
1743
1951
|
...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.`,
|
|
1952
|
+
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
1953
|
parameters: Type.Object({
|
|
1746
1954
|
command: Type.String({ description: "Shell command to execute" }),
|
|
1747
1955
|
timeout: Type.Optional(Type.Number({ description: `Timeout in seconds (default ${DEFAULT_COMMAND_TIMEOUT_SECONDS})` })),
|
|
@@ -1767,7 +1975,7 @@ function withWorkspaceWriteScope(tool) {
|
|
|
1767
1975
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
1768
1976
|
import { spawn as spawn2 } from "node:child_process";
|
|
1769
1977
|
import { createHash as createHash2 } from "node:crypto";
|
|
1770
|
-
import { constants as fsConstants, realpathSync as realpathSync3, statSync as
|
|
1978
|
+
import { constants as fsConstants, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
|
|
1771
1979
|
import { access, readFile } from "node:fs/promises";
|
|
1772
1980
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
1773
1981
|
var writeAuthorization = new AsyncLocalStorage2();
|
|
@@ -1816,6 +2024,22 @@ try {
|
|
|
1816
2024
|
throw error
|
|
1817
2025
|
}
|
|
1818
2026
|
`;
|
|
2027
|
+
function atomicWriteLaunch(platform, root, additionalRoots) {
|
|
2028
|
+
if (platform === "win32") {
|
|
2029
|
+
const env = {};
|
|
2030
|
+
for (const name of ["SystemRoot", "windir", "TEMP", "TMP", "USERPROFILE"]) {
|
|
2031
|
+
if (process.env[name]) env[name] = process.env[name];
|
|
2032
|
+
}
|
|
2033
|
+
return { executable: process.execPath, args: ["-e", atomicWriteScript], env };
|
|
2034
|
+
}
|
|
2035
|
+
const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
|
|
2036
|
+
const spec = workspaceSandboxCommand(platform, root, root, command, additionalRoots);
|
|
2037
|
+
return {
|
|
2038
|
+
executable: spec.executable,
|
|
2039
|
+
args: spec.args,
|
|
2040
|
+
env: { PATH: "/usr/bin:/bin", ...process.env.HOME ? { HOME: process.env.HOME } : {} }
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
1819
2043
|
function shellQuote(value) {
|
|
1820
2044
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1821
2045
|
}
|
|
@@ -1824,7 +2048,7 @@ function nearestExistingDirectory(path) {
|
|
|
1824
2048
|
while (true) {
|
|
1825
2049
|
try {
|
|
1826
2050
|
const canonical2 = realpathSync3(current);
|
|
1827
|
-
if (!
|
|
2051
|
+
if (!statSync5(canonical2).isDirectory()) throw new Error(`Write scope is not a directory: ${canonical2}`);
|
|
1828
2052
|
return canonical2;
|
|
1829
2053
|
} catch (error) {
|
|
1830
2054
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -1853,17 +2077,12 @@ async function runAtomicWrite(authorization, path, content) {
|
|
|
1853
2077
|
throw new Error("The write authorization no longer matches the active workspace target.");
|
|
1854
2078
|
}
|
|
1855
2079
|
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
|
-
};
|
|
2080
|
+
const launch = atomicWriteLaunch(process.platform, root, additionalRoots);
|
|
1862
2081
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
1863
|
-
const child = spawn2(
|
|
2082
|
+
const child = spawn2(launch.executable, launch.args, {
|
|
1864
2083
|
cwd: root,
|
|
1865
2084
|
detached: process.platform !== "win32",
|
|
1866
|
-
env,
|
|
2085
|
+
env: launch.env,
|
|
1867
2086
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1868
2087
|
windowsHide: true
|
|
1869
2088
|
});
|
|
@@ -2051,6 +2270,7 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2051
2270
|
let context = null;
|
|
2052
2271
|
let nativeSubagents = false;
|
|
2053
2272
|
let subagentParent = false;
|
|
2273
|
+
let stoppedChildren = null;
|
|
2054
2274
|
let timer = null;
|
|
2055
2275
|
let polling = false;
|
|
2056
2276
|
let failure;
|
|
@@ -2063,6 +2283,10 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2063
2283
|
const fileTools = /* @__PURE__ */ new Map();
|
|
2064
2284
|
const writeAuthorizations = /* @__PURE__ */ new Map();
|
|
2065
2285
|
let workspaceRoot = null;
|
|
2286
|
+
let sessionState = null;
|
|
2287
|
+
let stateDelivered = false;
|
|
2288
|
+
let autoNameTried = false;
|
|
2289
|
+
let lastTodo = [];
|
|
2066
2290
|
let stage = "Tool discovery";
|
|
2067
2291
|
try {
|
|
2068
2292
|
await terminalCall("startup", { phase: stage });
|
|
@@ -2132,21 +2356,38 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2132
2356
|
},
|
|
2133
2357
|
workspaceWriteScope: () => planRun?.active ? "read-only" : void 0
|
|
2134
2358
|
});
|
|
2135
|
-
pi.registerTool(
|
|
2359
|
+
pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
|
|
2136
2360
|
pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
|
|
2137
2361
|
pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
|
|
2362
|
+
pi.on("context", (event) => {
|
|
2363
|
+
if (!sessionState || stateDelivered) return;
|
|
2364
|
+
stateDelivered = true;
|
|
2365
|
+
return {
|
|
2366
|
+
messages: [...event.messages, {
|
|
2367
|
+
role: "custom",
|
|
2368
|
+
customType: "xingular-session-state",
|
|
2369
|
+
content: sessionState,
|
|
2370
|
+
display: false,
|
|
2371
|
+
timestamp: Date.now()
|
|
2372
|
+
}]
|
|
2373
|
+
};
|
|
2374
|
+
});
|
|
2138
2375
|
compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
|
|
2139
2376
|
const slicePolicy = parseWorkSlice(void 0);
|
|
2140
2377
|
const sliceState = createSliceState(slicePolicy);
|
|
2141
|
-
if (slicePolicy.enabled) workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
|
|
2378
|
+
if (slicePolicy.enabled) workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
|
|
2142
2379
|
pi.on("context", (event) => {
|
|
2143
2380
|
const messages = pruneToolImages(event.messages);
|
|
2144
2381
|
return messages === event.messages ? void 0 : { messages };
|
|
2145
2382
|
});
|
|
2146
2383
|
pi.on("session_start", async (_event, ctx) => {
|
|
2147
2384
|
context = ctx;
|
|
2385
|
+
sessionState = null;
|
|
2386
|
+
stateDelivered = false;
|
|
2387
|
+
autoNameTried = false;
|
|
2388
|
+
lastTodo = [];
|
|
2148
2389
|
if (timer) clearInterval(timer);
|
|
2149
|
-
await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile() });
|
|
2390
|
+
await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
|
|
2150
2391
|
timer = setInterval(() => {
|
|
2151
2392
|
if (polling || !context) return;
|
|
2152
2393
|
polling = true;
|
|
@@ -2170,13 +2411,16 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2170
2411
|
usage = emptyUsage();
|
|
2171
2412
|
reasoningTokens = void 0;
|
|
2172
2413
|
costReported = false;
|
|
2173
|
-
const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
|
|
2414
|
+
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
2415
|
workspaceRoot = typeof begun.workspaceRoot === "string" ? begun.workspaceRoot : null;
|
|
2175
2416
|
if (!workspaceRoot) throw new Error("The active project workspace could not be resolved; the turn was not started.");
|
|
2417
|
+
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
2418
|
+
stoppedChildren = null;
|
|
2176
2419
|
if (nativeSubagents) {
|
|
2177
|
-
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
2178
2420
|
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
2421
|
if (planRun && Array.isArray(snapshot.connectors)) planRun.connectors = snapshot.connectors;
|
|
2422
|
+
if (planRun && snapshot.subagentProfiles) planRun.subagentProfiles = snapshot.subagentProfiles;
|
|
2423
|
+
stoppedChildren = snapshot.notice ?? null;
|
|
2180
2424
|
subagentParent = true;
|
|
2181
2425
|
}
|
|
2182
2426
|
if (planRun && event.messageType === "user") {
|
|
@@ -2232,6 +2476,44 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2232
2476
|
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
2477
|
return { message: { customType: "xingular-initial-bash", content: instruction, display: false, details: {} } };
|
|
2234
2478
|
});
|
|
2479
|
+
pi.on("before_agent_start", async () => {
|
|
2480
|
+
const notice = stoppedChildren;
|
|
2481
|
+
stoppedChildren = null;
|
|
2482
|
+
if (notice) return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
|
|
2483
|
+
});
|
|
2484
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
2485
|
+
if (event.messageType !== "user") return;
|
|
2486
|
+
const branch = ctx.sessionManager.getBranch?.() ?? [];
|
|
2487
|
+
const hasUser = branch.some((entry) => entry.type === "message" && entry.message?.role === "user");
|
|
2488
|
+
let name = typeof pi.getSessionName === "function" ? pi.getSessionName() ?? null : null;
|
|
2489
|
+
if (!name && !autoNameTried && pi.setSessionName) {
|
|
2490
|
+
const candidate = deriveSessionName(event.prompt);
|
|
2491
|
+
if (candidate) {
|
|
2492
|
+
autoNameTried = true;
|
|
2493
|
+
try {
|
|
2494
|
+
const policy = await controlCall("terminal.policy", { tool: SESSION_NAME, args: { name: candidate } });
|
|
2495
|
+
if (policy.allowed && !policy.requiresApproval) {
|
|
2496
|
+
pi.setSessionName(candidate);
|
|
2497
|
+
name = candidate;
|
|
2498
|
+
}
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const state = {
|
|
2504
|
+
name,
|
|
2505
|
+
todo: [],
|
|
2506
|
+
...lastTodo.length ? { previousTodo: lastTodo } : {},
|
|
2507
|
+
firstTurn: !hasUser
|
|
2508
|
+
};
|
|
2509
|
+
lastTodo = [];
|
|
2510
|
+
const active = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : null;
|
|
2511
|
+
sessionState = sessionStateText(state, {
|
|
2512
|
+
name: active ? active.includes(SESSION_NAME) : true,
|
|
2513
|
+
todo: active ? active.includes(TODO_WRITE) : true
|
|
2514
|
+
});
|
|
2515
|
+
stateDelivered = false;
|
|
2516
|
+
});
|
|
2235
2517
|
const deliverChildren = async (event, ctx, settle) => {
|
|
2236
2518
|
if (!nativeSubagents || settle && event.outcome !== "completed" || ctx.signal?.aborted) return;
|
|
2237
2519
|
const result = await withParentCancellation(ctx.signal, () => controlCall("terminal.subagents.delivery", { history: subagentHistory([...ctx.sessionManager.getBranch(), ...event.entries]), settle }, 2147483647));
|
|
@@ -2371,6 +2653,10 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2371
2653
|
if (timer) clearInterval(timer);
|
|
2372
2654
|
timer = null;
|
|
2373
2655
|
context = null;
|
|
2656
|
+
sessionState = null;
|
|
2657
|
+
stateDelivered = false;
|
|
2658
|
+
autoNameTried = false;
|
|
2659
|
+
lastTodo = [];
|
|
2374
2660
|
await terminalCall("shutdown").catch(() => {
|
|
2375
2661
|
});
|
|
2376
2662
|
});
|
|
@@ -2391,18 +2677,20 @@ async function piExtension_default(pi, planRun, getSession) {
|
|
|
2391
2677
|
}
|
|
2392
2678
|
});
|
|
2393
2679
|
let recorded = Promise.resolve();
|
|
2394
|
-
const
|
|
2395
|
-
recorded = terminalCall("
|
|
2680
|
+
const todo = todoTool({ emit: (_type, payload) => {
|
|
2681
|
+
recorded = terminalCall("todo", payload);
|
|
2396
2682
|
} });
|
|
2397
2683
|
pi.registerTool({
|
|
2398
|
-
name:
|
|
2399
|
-
label:
|
|
2400
|
-
description:
|
|
2401
|
-
parameters: Type2.Unsafe(
|
|
2684
|
+
name: todo.name,
|
|
2685
|
+
label: todo.label,
|
|
2686
|
+
description: todo.description,
|
|
2687
|
+
parameters: Type2.Unsafe(todo.parameters(Type2)),
|
|
2402
2688
|
async execute(_id, input) {
|
|
2403
2689
|
recorded = Promise.resolve();
|
|
2404
|
-
const result = await
|
|
2690
|
+
const result = await todo.execute(input);
|
|
2405
2691
|
await recorded;
|
|
2692
|
+
const items = input.items;
|
|
2693
|
+
if (Array.isArray(items)) lastTodo = items;
|
|
2406
2694
|
return { content: [{ type: "text", text: typeof result === "string" ? result : result.text }], details: void 0 };
|
|
2407
2695
|
}
|
|
2408
2696
|
});
|