@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +10 -5
  3. package/dist/cli.js +5560 -5056
  4. package/dist/sdk/agents/builtin.js +3 -3
  5. package/dist/sdk/agents/planResearch.d.ts +23 -1
  6. package/dist/sdk/agents/planResearch.js +71 -8
  7. package/dist/sdk/agents/profiles.d.ts +4 -1
  8. package/dist/sdk/agents/profiles.js +17 -6
  9. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  10. package/dist/sdk/commands/registry.d.ts +11 -3
  11. package/dist/sdk/commands/registry.js +3 -2
  12. package/dist/sdk/core/shell.d.ts +20 -0
  13. package/dist/sdk/core/shell.js +73 -0
  14. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  15. package/dist/sdk/core/workspaceBashTool.js +16 -2
  16. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  17. package/dist/sdk/core/workspaceSandbox.js +52 -12
  18. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  19. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  20. package/dist/sdk/doctor/checks.js +71 -12
  21. package/dist/sdk/hub/hub.js +5 -3
  22. package/dist/sdk/index.d.ts +2 -2
  23. package/dist/sdk/index.js +1 -1
  24. package/dist/sdk/plugins/builtin.js +3 -3
  25. package/dist/sdk/plugins/install.d.ts +6 -2
  26. package/dist/sdk/plugins/install.js +29 -5
  27. package/dist/sdk/plugins/inventory.js +2 -2
  28. package/dist/sdk/prompt/compile.d.ts +2 -2
  29. package/dist/sdk/prompt/compile.js +14 -16
  30. package/dist/sdk/server/methods.js +4 -2
  31. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  32. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  33. package/dist/sdk/sessions/continuation.d.ts +4 -2
  34. package/dist/sdk/sessions/continuation.js +7 -6
  35. package/dist/sdk/sessions/contracts.d.ts +22 -4
  36. package/dist/sdk/sessions/contracts.js +12 -7
  37. package/dist/sdk/sessions/planMode.js +2 -2
  38. package/dist/sdk/sessions/runStore.js +3 -1
  39. package/dist/sdk/sessions/runner.js +2 -2
  40. package/dist/sdk/sessions/sessions.js +29 -11
  41. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  42. package/dist/sdk/sessions/subagentService.js +194 -54
  43. package/dist/sdk/sessions/subagents.d.ts +10 -1
  44. package/dist/sdk/sessions/subagents.js +4 -0
  45. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  46. package/dist/sdk/sessions/workSlice.js +77 -45
  47. package/dist/sdk/terminal/controller.d.ts +20 -0
  48. package/dist/sdk/terminal/controller.js +43 -18
  49. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  50. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  51. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  52. package/dist/sdk/terminal/opentui/model.js +482 -0
  53. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  54. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  55. package/dist/sdk/terminal/opentui/theme.js +1 -0
  56. package/dist/sdk/terminal/piExtension.js +28 -14
  57. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  58. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  59. package/dist/sdk/terminal/terminals.d.ts +7 -0
  60. package/dist/sdk/terminal/terminals.js +86 -14
  61. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  62. package/dist/sdk/terminal/trajectory.js +8 -0
  63. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  64. package/dist/sdk/terminal/tui/chrome.js +13 -0
  65. package/dist/sdk/terminal/tui/commandPages.js +14 -4
  66. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  67. package/dist/sdk/terminal/tui/commands.js +8 -0
  68. package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
  69. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  70. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  71. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  72. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
  73. package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
  74. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  75. package/dist/sdk/tools/sessionName.d.ts +2 -0
  76. package/dist/sdk/tools/sessionName.js +5 -3
  77. package/dist/sdk/tools/spawnSubagent.js +9 -7
  78. package/dist/sdk/tools/todo.d.ts +15 -0
  79. package/dist/sdk/tools/todo.js +64 -0
  80. package/dist/terminal-opentui.js +1416 -348
  81. package/dist/terminal-pi.js +315 -126
  82. package/docs/architecture/00-overview.md +1 -1
  83. package/docs/architecture/01-extension-points.md +1 -1
  84. package/docs/architecture/02-sessions-and-runs.md +65 -34
  85. package/docs/architecture/04-prompt.md +8 -6
  86. package/docs/architecture/06-plugins-and-capabilities.md +10 -6
  87. package/docs/architecture/07-tools-and-gate.md +31 -5
  88. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  89. package/docs/architecture/10-server-protocol.md +16 -9
  90. package/docs/architecture/11-terminal.md +51 -26
  91. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  92. package/docs/architecture/README.md +18 -17
  93. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  94. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  95. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  96. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  97. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  98. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  99. package/docs/architecture/proposals/README.md +10 -6
  100. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  101. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  102. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  103. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  104. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  105. package/docs/doctor.md +14 -1
  106. package/docs/parity.md +9 -9
  107. package/docs/releases/1.3.0.md +21 -0
  108. package/docs/releases/README.md +2 -2
  109. package/docs/terminal-tui.md +85 -46
  110. package/package.json +1 -1
  111. package/dist/sdk/tools/checklist.d.ts +0 -9
  112. package/dist/sdk/tools/checklist.js +0 -55
@@ -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 checklistItemSchema = z.object({ text: z.string().min(1).max(500), status: z.enum(["pending", "in_progress", "completed"]) }).strict();
340
- var MAX_CHECKLIST_ITEMS = 50;
341
- var checklistPayload = z.object({ items: z.array(checklistItemSchema).max(MAX_CHECKLIST_ITEMS) }).strict();
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
- "checklist.updated": checklistPayload,
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
- "checklist_write",
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/checklist.ts
657
- var CHECKLIST_WRITE = "checklist_write";
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 describeChecklist(items) {
660
- if (!items.length) return "The checklist is empty.";
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 checklistTool(run2) {
669
+ function todoTool(run2) {
664
670
  return {
665
- name: CHECKLIST_WRITE,
666
- label: "Write the checklist",
667
- description: "Keep a checklist of the steps of the current work. The user watches this list to follow your progress, so write it early and keep it current. Send the whole list every time; it replaces the previous one. Use it whenever the work has several distinct steps or will take several tool calls, when the user gives several things to do, and when a subagent brief asks for more than one thing. 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. Before your final response, write one final checklist update: mark every fully completed step completed, and leave unfinished or blocked steps in their accurate pending or in_progress status. Never mark unfinished work completed. 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.",
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: MAX_CHECKLIST_ITEMS })
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 checklist as an array.";
678
- if (input.items.length > MAX_CHECKLIST_ITEMS) return `Keep the checklist to ${MAX_CHECKLIST_ITEMS} steps or fewer.`;
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 = checklistItemSchema.safeParse({ text: typeof entry?.text === "string" ? entry.text.trim() : entry?.text, status: entry?.status });
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("checklist.updated", { items });
687
- return describeChecklist(items);
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
- // Roughly 40 minutes of ordinary work (about 15 s a call), so a checkpoint is a chapter of a
802
- // task, not an interruption every few edits. A task still gets eight of them before the cap.
803
- maxToolCalls: 160,
804
- maxMinutes: 40,
805
- // A generous cost ceiling, counted on new input, cache writes and output only: heavy reading at
806
- // a large context can add tens of thousands of tokens a request, so a small cap stopped useful
807
- // work (see the "Confirm plan model support" thread). 320k is a safety net, not the bound.
808
- maxNewTokens: 32e4,
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 clip(
889
- `This reply reached its work-slice limit (${limit}; step ${state.steps}). 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. Do not call other tools.`,
890
- 600
891
- );
892
- }
893
- function stalledNote(reason) {
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 reason = kind === "stuck" ? stalledNote(limit) : kind === "compaction-thrash" ? handoffRequest("three context compactions in one reply", state) : handoffRequest(limit, state);
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
- stepsSinceProgress = 0;
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 handoff = HANDOFF_TOOLS.has(name);
996
+ const canCheckpoint = checkpointAvailable();
997
+ const handoff = canCheckpoint && HANDOFF_TOOLS.has(name);
969
998
  if (state.endedBy) {
970
- return handoff ? void 0 : { block: true, terminate: true, reason: "This reply is over. Its checkpoint is recorded; nothing else runs." };
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
- stepsSinceProgress = 0;
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 = (edits.get(path) ?? 0) + 1;
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
- if (nudged) return end("stuck", reason2);
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 noProgress = stepsSinceProgress >= 25 ? `${stepsSinceProgress} steps with no file change and no passing check` : null;
1007
- const reason = repeat ?? failure ?? noProgress;
1030
+ const reason = repeat ?? failure;
1008
1031
  if (!reason) return void 0;
1009
- if (nudged) return end("stuck", reason);
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: 'Set or revise this session\u2019s name. Name meaningful work early, then revise the name when the overall objective changes; greetings alone need no name. Use one short imperative sentence describing the objective, at most six words, such as "Add web browser integrations". You may revise a manually assigned name. Keep a useful current name when the objective is unchanged. This only names your own session.',
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.checklist.length && !input.planInstructions) return null;
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.checklist.length) {
1137
- const active = input.checklist.filter((item) => item.status !== "completed");
1138
- const completed = input.checklist.filter((item) => item.status === "completed");
1139
- const checklist = [
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(describeChecklist(active), 2500)}` : "",
1162
+ ${clip2(describeTodo(active), 2500)}` : "",
1142
1163
  completed.length ? `### Completed (${completed.length})
1143
- ${clip2(describeChecklist(completed), 500)}` : ""
1164
+ ${clip2(describeTodo(completed), 500)}` : ""
1144
1165
  ].filter(Boolean).join("\n\n");
1145
- parts.push(`## Current checklist
1166
+ parts.push(`## Current todo list
1146
1167
 
1147
- ${checklist}`);
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
- CHECKLIST_WRITE,
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 validatedChecklist(value) {
1268
- if (!Array.isArray(value) || value.length > MAX_CHECKLIST_ITEMS) return null;
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 = checklistItemSchema.safeParse({
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 checklist = validatedChecklist(options.initialChecklist ?? []) ?? [];
1305
+ let todo = validatedTodo(options.initialTodo ?? []) ?? [];
1285
1306
  let lastCompactionKey = null;
1286
1307
  let compacted = false;
1287
1308
  const reset = () => {
1288
1309
  goal = "";
1289
- checklist = validatedChecklist(options.initialChecklist ?? []) ?? [];
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
- checklist = validatedChecklist(options.initialChecklist ?? []) ?? [];
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 !== CHECKLIST_WRITE || event.isError) return;
1302
- const items = validatedChecklist(event.input.items);
1303
- if (items) checklist = 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, checklist, planInstructions });
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 statSync2 } from "node:fs";
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 join2 } from "node:path";
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
- accessSync(candidate, constants.X_OK);
1447
- if (statSync2(candidate).isFile()) return candidate;
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 = systemExecutable(["/usr/bin/sandbox-exec"]);
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 = systemExecutable(["/usr/bin/bwrap", "/bin/bwrap"]);
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 = systemExecutable(["/usr/bin/sandbox-exec"]);
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 = systemExecutable(["/usr/bin/bwrap", "/bin/bwrap"]);
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 !== "win32") {
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
- const platform2 = options.platform ?? process.platform;
1696
- const tempRoot = realpathSync2(mkdtempSync(join2(tmpdir(), "harness-read-only-bash-")));
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(platform2, root, canonicalCwd, command, [], tempRoot);
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({ executable: shellPath(), args: ["-c", command], marker: "" }, canonicalCwd, execOptions, null);
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 reason = "This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved.";
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({ executable: shellPath(), args: ["-c", command], marker: "" }, canonicalCwd, execOptions, null);
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 withWorkspaceWriteScope(tool) {
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 statSync3 } from "node:fs";
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 (!statSync3(canonical2).isDirectory()) throw new Error(`Write scope is not a directory: ${canonical2}`);
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 command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
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(spec.executable, spec.args, {
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(withWorkspaceWriteScope(createBashTool(process.cwd(), { operations: workspaceBash })));
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 checklist = checklistTool({ emit: (_type, payload) => {
2395
- recorded = terminalCall("checklist", payload);
2583
+ const todo = todoTool({ emit: (_type, payload) => {
2584
+ recorded = terminalCall("todo", payload);
2396
2585
  } });
2397
2586
  pi.registerTool({
2398
- name: checklist.name,
2399
- label: checklist.label,
2400
- description: checklist.description,
2401
- parameters: Type2.Unsafe(checklist.parameters(Type2)),
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 checklist.execute(input);
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
  }