@xingular/harness 1.1.1 → 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 (264) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +36 -28
  3. package/dist/cli.js +10837 -6159
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +18 -4
  11. package/dist/sdk/agents/planResearch.d.ts +27 -0
  12. package/dist/sdk/agents/planResearch.js +82 -0
  13. package/dist/sdk/agents/profiles.d.ts +5 -1
  14. package/dist/sdk/agents/profiles.js +21 -5
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/cli.js +5 -4
  27. package/dist/sdk/commands/list.d.ts +3 -0
  28. package/dist/sdk/commands/list.js +75 -0
  29. package/dist/sdk/commands/registry.d.ts +258 -0
  30. package/dist/sdk/commands/registry.js +48 -0
  31. package/dist/sdk/commands/routes.d.ts +16 -0
  32. package/dist/sdk/commands/routes.js +33 -0
  33. package/dist/sdk/core/commandBudget.d.ts +23 -0
  34. package/dist/sdk/core/commandBudget.js +57 -0
  35. package/dist/sdk/core/index.js +17 -1
  36. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  37. package/dist/sdk/core/interactiveProfile.js +143 -0
  38. package/dist/sdk/core/shell.d.ts +20 -0
  39. package/dist/sdk/core/shell.js +73 -0
  40. package/dist/sdk/core/workspace.d.ts +13 -0
  41. package/dist/sdk/core/workspace.js +88 -0
  42. package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
  43. package/dist/sdk/core/workspaceBashTool.js +50 -0
  44. package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
  45. package/dist/sdk/core/workspaceSandbox.js +360 -0
  46. package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
  47. package/dist/sdk/core/workspaceWriteOperations.js +224 -0
  48. package/dist/sdk/doctor/checks.js +71 -12
  49. package/dist/sdk/doctor/types.d.ts +2 -2
  50. package/dist/sdk/harness.d.ts +7 -0
  51. package/dist/sdk/harness.js +42 -2
  52. package/dist/sdk/hub/hub.js +5 -3
  53. package/dist/sdk/index.d.ts +8 -3
  54. package/dist/sdk/index.js +4 -1
  55. package/dist/sdk/pi/runtime.d.ts +2 -1
  56. package/dist/sdk/pi/runtime.js +2 -2
  57. package/dist/sdk/plugins/builtin.js +32 -15
  58. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  59. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  60. package/dist/sdk/plugins/catalogue.js +2 -1
  61. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  62. package/dist/sdk/plugins/commandPrefix.js +21 -0
  63. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  64. package/dist/sdk/plugins/firstParty.js +8 -2
  65. package/dist/sdk/plugins/install.d.ts +6 -2
  66. package/dist/sdk/plugins/install.js +29 -5
  67. package/dist/sdk/plugins/inventory.js +6 -5
  68. package/dist/sdk/plugins/manifest.d.ts +2 -0
  69. package/dist/sdk/plugins/manifest.js +2 -1
  70. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  71. package/dist/sdk/plugins/surfaces.js +11 -0
  72. package/dist/sdk/plugins/types.d.ts +15 -0
  73. package/dist/sdk/prompt/compile.d.ts +14 -0
  74. package/dist/sdk/prompt/compile.js +65 -31
  75. package/dist/sdk/prompt/standard.d.ts +17 -0
  76. package/dist/sdk/prompt/standard.js +21 -0
  77. package/dist/sdk/server/methods.js +22 -2
  78. package/dist/sdk/server/remote.js +36 -11
  79. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  80. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  81. package/dist/sdk/sessions/continuation.d.ts +7 -2
  82. package/dist/sdk/sessions/continuation.js +11 -6
  83. package/dist/sdk/sessions/contracts.d.ts +190 -4
  84. package/dist/sdk/sessions/contracts.js +71 -8
  85. package/dist/sdk/sessions/control.d.ts +72 -0
  86. package/dist/sdk/sessions/control.js +158 -0
  87. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  88. package/dist/sdk/sessions/initialBash.js +7 -0
  89. package/dist/sdk/sessions/planMode.d.ts +14 -3
  90. package/dist/sdk/sessions/planMode.js +74 -17
  91. package/dist/sdk/sessions/registry.d.ts +1 -1
  92. package/dist/sdk/sessions/registry.js +2 -2
  93. package/dist/sdk/sessions/runStore.d.ts +14 -0
  94. package/dist/sdk/sessions/runStore.js +38 -1
  95. package/dist/sdk/sessions/runner.d.ts +40 -2
  96. package/dist/sdk/sessions/runner.js +161 -13
  97. package/dist/sdk/sessions/sessions.d.ts +23 -3
  98. package/dist/sdk/sessions/sessions.js +225 -22
  99. package/dist/sdk/sessions/subagentService.d.ts +24 -0
  100. package/dist/sdk/sessions/subagentService.js +225 -26
  101. package/dist/sdk/sessions/subagents.d.ts +10 -1
  102. package/dist/sdk/sessions/subagents.js +4 -0
  103. package/dist/sdk/sessions/workSlice.d.ts +106 -0
  104. package/dist/sdk/sessions/workSlice.js +329 -0
  105. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  106. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  107. package/dist/sdk/terminal/browserControl.js +2 -1
  108. package/dist/sdk/terminal/controller.d.ts +31 -3
  109. package/dist/sdk/terminal/controller.js +81 -29
  110. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  111. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  112. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  113. package/dist/sdk/terminal/opentui/model.js +482 -0
  114. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  115. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  116. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  117. package/dist/sdk/terminal/opentui/theme.js +88 -0
  118. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  119. package/dist/sdk/terminal/piExtension.js +177 -63
  120. package/dist/sdk/terminal/standalone.d.ts +7 -0
  121. package/dist/sdk/terminal/standalone.js +72 -13
  122. package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
  123. package/dist/sdk/terminal/subagentMonitor.js +61 -9
  124. package/dist/sdk/terminal/terminals.d.ts +26 -2
  125. package/dist/sdk/terminal/terminals.js +325 -43
  126. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  127. package/dist/sdk/terminal/trajectory.js +8 -0
  128. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  129. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  130. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  131. package/dist/sdk/terminal/tui/chrome.js +13 -0
  132. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  133. package/dist/sdk/terminal/tui/commandPages.js +17 -4
  134. package/dist/sdk/terminal/tui/commands.d.ts +6 -0
  135. package/dist/sdk/terminal/tui/commands.js +213 -124
  136. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  137. package/dist/sdk/terminal/tui/list.js +96 -14
  138. package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
  139. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  140. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  141. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  142. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  143. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  144. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  145. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
  146. package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
  147. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  148. package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
  149. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  150. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  151. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  152. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  153. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  154. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  155. package/dist/sdk/terminal/tui/runtime.js +23 -3
  156. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  157. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  158. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  159. package/dist/sdk/tools/checkpoint.js +154 -0
  160. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  161. package/dist/sdk/tools/getCapabilities.js +34 -0
  162. package/dist/sdk/tools/sessionName.d.ts +2 -0
  163. package/dist/sdk/tools/sessionName.js +5 -3
  164. package/dist/sdk/tools/spawnSubagent.js +11 -8
  165. package/dist/sdk/tools/todo.d.ts +15 -0
  166. package/dist/sdk/tools/todo.js +64 -0
  167. package/dist/sdk/usage/analytics.d.ts +20 -0
  168. package/dist/sdk/usage/analytics.js +151 -0
  169. package/dist/sdk/usage/ledger.d.ts +38 -0
  170. package/dist/sdk/usage/ledger.js +142 -0
  171. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  172. package/dist/sdk/usage/limits/adapters.js +349 -0
  173. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  174. package/dist/sdk/usage/limits/capture.js +47 -0
  175. package/dist/sdk/usage/limits/http.d.ts +12 -0
  176. package/dist/sdk/usage/limits/http.js +51 -0
  177. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  178. package/dist/sdk/usage/limits/normalize.js +211 -0
  179. package/dist/sdk/usage/limits/service.d.ts +30 -0
  180. package/dist/sdk/usage/limits/service.js +176 -0
  181. package/dist/sdk/usage/limits/types.d.ts +65 -0
  182. package/dist/sdk/usage/limits/types.js +1 -0
  183. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  184. package/dist/sdk/usage/serverMethods.js +22 -0
  185. package/dist/sdk/usage/service.d.ts +32 -0
  186. package/dist/sdk/usage/service.js +66 -0
  187. package/dist/sdk/usage/settings.d.ts +7 -0
  188. package/dist/sdk/usage/settings.js +28 -0
  189. package/dist/sdk/usage/types.d.ts +60 -0
  190. package/dist/sdk/usage/types.js +4 -0
  191. package/dist/terminal-opentui.js +9204 -4812
  192. package/dist/terminal-pi.js +1848 -272
  193. package/docs/architecture/00-overview.md +1 -1
  194. package/docs/architecture/01-extension-points.md +1 -1
  195. package/docs/architecture/02-sessions-and-runs.md +169 -36
  196. package/docs/architecture/04-prompt.md +55 -15
  197. package/docs/architecture/06-plugins-and-capabilities.md +36 -15
  198. package/docs/architecture/07-tools-and-gate.md +131 -25
  199. package/docs/architecture/08-memory-and-history.md +4 -2
  200. package/docs/architecture/09-accounts-and-connectors.md +39 -3
  201. package/docs/architecture/10-server-protocol.md +71 -9
  202. package/docs/architecture/11-terminal.md +148 -60
  203. package/docs/architecture/12-storage.md +30 -2
  204. package/docs/architecture/13-world-tools.md +22 -1
  205. package/docs/architecture/14-surfaces-and-parity.md +58 -8
  206. package/docs/architecture/README.md +20 -17
  207. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  208. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  209. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  210. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  211. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  212. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  213. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  214. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  215. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  216. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  217. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  218. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  219. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  220. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
  221. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  222. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  223. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  224. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  225. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  226. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  227. package/docs/architecture/proposals/README.md +24 -6
  228. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  229. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  230. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  231. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  232. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  233. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  234. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  235. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  236. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  237. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  238. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  239. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  240. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  241. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  242. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  243. package/docs/doctor.md +14 -1
  244. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  245. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  246. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  247. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  248. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  249. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  250. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  251. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  252. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  253. package/docs/parity.md +83 -27
  254. package/docs/releases/1.1.1.md +28 -0
  255. package/docs/releases/1.2.0.md +49 -0
  256. package/docs/releases/1.3.0.md +21 -0
  257. package/docs/releases/4.0.0.md +10 -9
  258. package/docs/releases/README.md +35 -30
  259. package/docs/study/00-index.md +3 -0
  260. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  261. package/docs/terminal-tui.md +162 -82
  262. package/package.json +4 -3
  263. package/dist/sdk/tools/checklist.d.ts +0 -9
  264. package/dist/sdk/tools/checklist.js +0 -54
@@ -1,12 +1,15 @@
1
1
  // Starting a session turn: the account, the run record, the compiled prompt, the tools and
2
2
  // their gate, then Pi. Everything before Pi answers is synchronous with the caller; the reply
3
3
  // itself streams as run events.
4
+ import { randomUUID } from 'node:crypto';
4
5
  import { existsSync, mkdirSync } from 'node:fs';
5
- import { join } from 'node:path';
6
+ import { isAbsolute, join } from 'node:path';
6
7
  import { log } from "../core/bus.js";
7
8
  import { HarnessError, invalid } from "../core/errors.js";
9
+ import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
8
10
  import { capabilitySet as parseCapabilitySet, freezeRun } from "../capabilities/runSnapshot.js";
9
11
  import { now, writeJson } from "../core/fs.js";
12
+ import { resolveWorkspaceRoot, verifyWorkspaceRoot } from "../core/workspace.js";
10
13
  import { takeChars } from "../core/text.js";
11
14
  import { BUILTIN_PLUGIN_REGISTRY } from "../plugins/builtin.js";
12
15
  import { sessionLaunch } from "../prompt/compile.js";
@@ -15,18 +18,44 @@ import { FIRST_PARTY_PLUGINS, firstPartyExtensions } from "../plugins/firstParty
15
18
  import { imagePruning } from "../world/images.js";
16
19
  import { materialise } from "../variant/materialise.js";
17
20
  import { ASK_USER } from "../tools/askUser.js";
18
- import { CHECKLIST_WRITE } from "../tools/checklist.js";
21
+ import { TODO_WRITE } from "../tools/todo.js";
22
+ import { CHECKPOINT_WRITE, checkpointOf } from "../tools/checkpoint.js";
19
23
  import { SESSION_NAME, sessionNameTool } from "../tools/sessionName.js";
20
24
  import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
25
+ import { GET_CAPABILITIES, getCapabilitiesTool } from "../tools/getCapabilities.js";
26
+ import { isActiveStatus } from "./contracts.js";
21
27
  import { SPAWN_SUBAGENT, WAIT_SUBAGENTS, subagentDepth, subagentHistory, subagentNesting } from "./subagents.js";
22
28
  import { selectHistory } from "./history.js";
23
29
  import { runLineageJoin } from "./lineageJoin.js";
30
+ import { compactionCheckpoint, COMPACTION_CHECKPOINT_EXTENSION } from "./compactionCheckpoint.js";
31
+ import { createSliceState, parseWorkSlice, sliceOf, workSliceGuard, WorkSliceError, WORK_SLICE_EXTENSION } from "./workSlice.js";
24
32
  import { piSessionFactory, runTurn, summarize } from "./runner.js";
25
- import { loadRun, runDir, saveRun, updateRun } from "./runStore.js";
26
- import { planRefusal, planSection } from "./planMode.js";
33
+ import { loadRun, readEvents, runDir, saveRun, updateRun } from "./runStore.js";
34
+ import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "./workspaceBoundary.js";
35
+ import { PLAN_INSTRUCTIONS, PLAN_RESEARCH_SUBAGENT_INSTRUCTIONS, planRefusal, planSection } from "./planMode.js";
27
36
  import { CONTINUATION_SECTION_ID, continuationOf, continuationSection } from "./continuation.js";
28
37
  import { deliveredSnapshot } from "../prompt/compile.js";
29
38
  import { profileSection } from "../agents/profiles.js";
39
+ /** A session command acts on a saved conversation and is never combined with a request of its own. */
40
+ function checkCommand(command, historyFile, input) {
41
+ if (!historyFile)
42
+ throw invalid('There is no saved conversation for this command yet.');
43
+ if (input.plan || input.continueFrom || input.automatic || input.initialBashCommand != null || input.images?.length) {
44
+ throw invalid('A session command runs on its own, without plan mode, a continuation, Bash or images.');
45
+ }
46
+ switch (command?.name) {
47
+ case 'compact': return;
48
+ case 'navigate':
49
+ if (typeof command.entryId !== 'string' || !command.entryId)
50
+ throw invalid('navigate needs the entry to go to.');
51
+ return;
52
+ case 'export':
53
+ if (typeof command.path !== 'string' || !isAbsolute(command.path) || !command.path.endsWith('.html'))
54
+ throw invalid('export needs an absolute .html path.');
55
+ return;
56
+ default: throw invalid('Unknown session command.');
57
+ }
58
+ }
30
59
  const CONTINUE_TEXT = 'Continue from where you stopped.';
31
60
  /** Fills a continuing run's input from the run it continues, refusing anything that would differ. */
32
61
  function continueInput(root, input) {
@@ -95,7 +124,7 @@ export class Sessions {
95
124
  diagnosticGate(run, caller) {
96
125
  return this.gate(run, caller, new Set(), true);
97
126
  }
98
- gate(run, caller, gated, diagnostic = false) {
127
+ gate(run, caller, gated, diagnostic = false, automatic = false) {
99
128
  const { policy } = this.options;
100
129
  return {
101
130
  ask: (names) => {
@@ -103,9 +132,19 @@ export class Sessions {
103
132
  gated.add(name);
104
133
  },
105
134
  check: async (tool, args) => {
106
- const call = { run: loadRun(this.env.root, run.runId), caller, tool, args };
107
- const scope = (await policy.scope?.(call)) ?? { allowed: true };
108
- if (!scope.allowed && (scope.automatic || !scope.requiresApproval))
135
+ let record;
136
+ let inspection;
137
+ try {
138
+ record = loadRun(this.env.root, run.runId);
139
+ inspection = inspectRunWorkspaceWrite(record, tool, args);
140
+ }
141
+ catch (error) {
142
+ return error instanceof Error ? error.message : 'The active project workspace could not be verified.';
143
+ }
144
+ const call = { run: record, caller, tool, args: inspection.input };
145
+ const hostScope = (await policy.scope?.(call)) ?? { allowed: true };
146
+ const scope = combineWorkspaceScope(hostScope, inspection);
147
+ if (!scope.allowed && (scope.automatic || (automatic && inspection.outside) || !scope.requiresApproval))
109
148
  return scope.reason ?? 'Outside the approved work scope.';
110
149
  try {
111
150
  await policy.authorize?.(call);
@@ -122,11 +161,11 @@ export class Sessions {
122
161
  return scope.reason ? `Refused in a refinement run: ${scope.reason}` : 'Refused in a refinement run: this call needs a person to approve it.';
123
162
  if (call.run.trigger === 'reflection')
124
163
  return scope.reason ? `Refused in a reflection run: ${scope.reason}` : 'Refused in a reflection run: this call needs a person to approve it.';
125
- // A call outside the scope is approved one at a time; a gated tool may be remembered, unless only a person may decide it.
164
+ const boundaryAction = inspection.outside ? `${inspection.reason} ${summarize(call.args, tool)}` : summarize(call.args);
126
165
  const decision = await run.requestApproval({
127
166
  target: tool,
128
- action: summarize(args),
129
- operation: { tool, args: (args && typeof args === 'object' ? args : {}) },
167
+ action: boundaryAction,
168
+ operation: { tool, args: (call.args && typeof call.args === 'object' ? call.args : {}) },
130
169
  rememberable: scope.allowed && !scope.humanOnly,
131
170
  standing: scope.humanOnly ? (scope.allowGlobalApproval ? 'switch' : 'none') : undefined
132
171
  });
@@ -139,6 +178,69 @@ export class Sessions {
139
178
  return error instanceof Error ? error.message : 'Tool was disabled while waiting.';
140
179
  }
141
180
  return null;
181
+ },
182
+ workspaceBoundaryBypass: async (tool, args) => {
183
+ if (diagnostic || automatic || tool.toLowerCase() !== 'bash')
184
+ return false;
185
+ const record = loadRun(this.env.root, run.runId);
186
+ if (record.trigger != null && record.trigger !== 'user')
187
+ return false;
188
+ if (!await this.options.fullPermission?.(record))
189
+ return false;
190
+ const call = { run: record, caller, tool, args };
191
+ const scope = (await policy.scope?.(call)) ?? { allowed: true };
192
+ if (scope.automatic || (!scope.allowed && !scope.requiresApproval) || (scope.humanOnly && !scope.allowGlobalApproval))
193
+ return false;
194
+ try {
195
+ await policy.authorize?.(call);
196
+ return true;
197
+ }
198
+ catch {
199
+ return false;
200
+ }
201
+ },
202
+ requestWorkspaceWriteScope: async (tool, args, reason) => {
203
+ if (diagnostic || automatic || tool.toLowerCase() !== 'bash')
204
+ return false;
205
+ const record = loadRun(this.env.root, run.runId);
206
+ if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
207
+ return false;
208
+ const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
209
+ if (operationArgs.workspaceWriteScope !== 'outside' || typeof operationArgs.command !== 'string' || !operationArgs.command)
210
+ return false;
211
+ const call = { run: record, caller, tool, args: operationArgs };
212
+ const scope = (await policy.scope?.(call)) ?? { allowed: true };
213
+ if (scope.automatic || (!scope.allowed && !scope.requiresApproval))
214
+ return false;
215
+ try {
216
+ await policy.authorize?.(call);
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ const decision = await run.requestApproval({
222
+ target: 'workspace-write',
223
+ action: `${reason}\n\nCommand to be run once after approval:\n${formatBashCommandForApproval(operationArgs.command)}`,
224
+ operation: { tool, args: operationArgs },
225
+ rememberable: false,
226
+ standing: 'none'
227
+ });
228
+ if (decision !== 'approved')
229
+ return false;
230
+ const currentRecord = loadRun(this.env.root, run.runId);
231
+ if (!isActiveStatus(currentRecord.status) || (currentRecord.trigger != null && currentRecord.trigger !== 'user'))
232
+ return false;
233
+ const currentCall = { run: currentRecord, caller, tool, args: operationArgs };
234
+ const currentScope = (await policy.scope?.(currentCall)) ?? { allowed: true };
235
+ if (currentScope.automatic || (!currentScope.allowed && !currentScope.requiresApproval))
236
+ return false;
237
+ try {
238
+ await policy.authorize?.(currentCall);
239
+ return true;
240
+ }
241
+ catch {
242
+ return false;
243
+ }
142
244
  }
143
245
  };
144
246
  }
@@ -195,6 +297,50 @@ export class Sessions {
195
297
  });
196
298
  return { release };
197
299
  }
300
+ /**
301
+ * Starts the next slice when a reply stopped at its work-slice boundary (PROP-0041). Never for
302
+ * unattended, plan, subagent, command or worktree runs, after a blocked or fruitless slice, or
303
+ * past the policy's slice cap. The earlier record is never reopened; this is a new run with the
304
+ * same pins, exactly as an application-asked continuation.
305
+ */
306
+ async continueSlice(input) {
307
+ const { root, runId, source, policy } = input;
308
+ if (!policy.enabled || !policy.continue || policy.maxSlices <= 0)
309
+ return;
310
+ if (source.automatic || source.plan || source.command || source.run.relation || source.run.worktreeId)
311
+ return;
312
+ if ((source.depth ?? 0) > 0)
313
+ return;
314
+ const record = loadRun(root, runId);
315
+ if (record.terminalReason !== 'slice_complete')
316
+ return;
317
+ const events = readEvents(root, runId);
318
+ const checkpoint = checkpointOf(events);
319
+ if (checkpoint?.blocked)
320
+ return;
321
+ const progressed = events.some((event) => event.type === 'file.changed')
322
+ || events.some((event) => {
323
+ const payload = event.payload;
324
+ return event.type === 'tool.completed' && payload.name === 'bash' && payload.ok === true;
325
+ })
326
+ || (checkpoint?.done.length ?? 0) > 0;
327
+ if (!progressed)
328
+ return;
329
+ if (sliceOf(root, runId) >= policy.maxSlices)
330
+ return;
331
+ const next = {
332
+ ...source,
333
+ run: { ...source.run, id: randomUUID(), resumedFromRunId: null, parentRunId: null, relation: null },
334
+ continueFrom: runId,
335
+ text: CONTINUE_TEXT
336
+ };
337
+ next.initialBashCommand = undefined;
338
+ next.initialBashContext = undefined;
339
+ next.images = undefined;
340
+ next.resume = null;
341
+ next.command = undefined;
342
+ await this.run(next).catch((error) => log(`the next work slice did not start: ${error instanceof Error ? error.message : String(error)}`));
343
+ }
198
344
  async start(input) {
199
345
  const { root } = this.env;
200
346
  if ((input.initialBashCommand !== undefined && input.initialBashCommand !== null) || (input.initialBashContext !== undefined && input.initialBashContext !== null)) {
@@ -222,8 +368,23 @@ export class Sessions {
222
368
  throw invalid('An initial Bash command is available only for a new interactive user turn.');
223
369
  }
224
370
  }
371
+ let slicePolicy;
372
+ try {
373
+ slicePolicy = parseWorkSlice(input.workSlice);
374
+ }
375
+ catch (error) {
376
+ if (error instanceof WorkSliceError)
377
+ throw invalid(error.message);
378
+ throw error;
379
+ }
225
380
  const continuation = input.continueFrom ? continueInput(root, input) : null;
381
+ const previousRun = continuation ? loadRun(root, continuation.runId) : null;
382
+ const workspaceRoot = previousRun?.workspaceRoot
383
+ ? verifyWorkspaceRoot(previousRun.workspaceRoot)
384
+ : resolveWorkspaceRoot(previousRun?.workingDirectory ?? input.cwd);
226
385
  const historyFile = selectHistory({ sessionDir: input.sessionDir, resume: input.resume });
386
+ if (input.command)
387
+ checkCommand(input.command, historyFile, input);
227
388
  const account = this.options.accounts.resolve(input.accountId, input.poolId);
228
389
  const context = this.options.accounts.context(account);
229
390
  const runId = input.run.id;
@@ -239,7 +400,7 @@ export class Sessions {
239
400
  const canAskUser = pinnedAskUser || (interactivePlan && harnessToolsEnabled);
240
401
  const requestedTools = [...new Set([
241
402
  ...input.tools.allow.filter((name) => name !== SPAWN_SUBAGENT && name !== WAIT_SUBAGENTS && (name !== ASK_USER || canAskUser)),
242
- ...(harnessToolsEnabled ? [...(canAskUser ? [ASK_USER] : []), CHECKLIST_WRITE, SESSION_NAME, ...DEFAULT_TOOL_NAMES] : []),
403
+ ...(harnessToolsEnabled ? [...(canAskUser ? [ASK_USER] : []), TODO_WRITE, CHECKPOINT_WRITE, SESSION_NAME, GET_CAPABILITIES, ...DEFAULT_TOOL_NAMES] : []),
243
404
  ...(broker?.toolNames ?? []),
244
405
  ...(canSpawn ? [SPAWN_SUBAGENT, WAIT_SUBAGENTS] : [])
245
406
  ])];
@@ -276,6 +437,7 @@ export class Sessions {
276
437
  sessionId: input.run.sessionId,
277
438
  nativeSessionId: null,
278
439
  workingDirectory: input.cwd,
440
+ workspaceRoot,
279
441
  worktreeId: input.run.worktreeId ?? null,
280
442
  goal: input.run.goal ?? takeChars(input.text, 4000),
281
443
  attachmentCount: input.run.attachmentCount ?? 0,
@@ -290,7 +452,9 @@ export class Sessions {
290
452
  variantScope: variant?.scope ?? null,
291
453
  variantVersion: variant?.fingerprint ?? null,
292
454
  ...(input.run.profile ? { profile: input.run.profile } : {}),
293
- ...(input.run.title ? { title: input.run.title } : {})
455
+ ...(input.run.title ? { title: input.run.title } : {}),
456
+ ...(input.model ? { model: input.model } : {}),
457
+ ...(input.effort ? { effort: input.effort } : {})
294
458
  });
295
459
  // What the session is, kept with the run so later consolidation knows which memory it had.
296
460
  try {
@@ -310,11 +474,18 @@ export class Sessions {
310
474
  if (!existsSync(input.sessionDir))
311
475
  mkdirSync(input.sessionDir, { recursive: true });
312
476
  const launch = input.launch ?? {};
313
- const plan = planSection(input.plan === true, historyFile);
477
+ const plan = planSection(input.plan === true, historyFile, input.run.relation === 'subagent');
314
478
  const delivered = historyFile ? deliveredSnapshot(historyFile) : null;
315
479
  const resumed = continuationSection(continuation, delivered?.some((entry) => entry.id === CONTINUATION_SECTION_ID) ?? false);
316
480
  // The profiles replace the variant's own subagent list, which they include.
317
- const profiles = capabilities.tools.includes(SPAWN_SUBAGENT) ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
481
+ const researchContext = input.plan === true ? {
482
+ frozenTools: capabilities.tools,
483
+ contributions: capabilities.contributions,
484
+ descriptors: BUILTIN_PLUGIN_REGISTRY.descriptors()
485
+ } : undefined;
486
+ const profiles = capabilities.tools.includes(SPAWN_SUBAGENT)
487
+ ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true, researchContext)
488
+ : null;
318
489
  if (profiles && variant)
319
490
  variant = { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) };
320
491
  const own = [plan, resumed, profiles].filter((section) => section !== null);
@@ -331,6 +502,11 @@ export class Sessions {
331
502
  fail(error);
332
503
  throw error;
333
504
  }
505
+ // Plan-research children are bounded too, but their read-only capability set omits checkpoint_write.
506
+ const checkpointAvailable = capabilities.tools.includes(CHECKPOINT_WRITE);
507
+ const planResearchChild = input.plan === true && input.run.relation === 'subagent';
508
+ const sliceEnabled = slicePolicy.enabled && (checkpointAvailable || planResearchChild);
509
+ const sliceState = createSliceState(slicePolicy, sliceOf(root, runId));
334
510
  const run = this.options.runs.start(input.run.sessionId, runId);
335
511
  if (run.finished)
336
512
  return { runId, nativeSessionId: null };
@@ -353,6 +529,11 @@ export class Sessions {
353
529
  });
354
530
  try {
355
531
  const tools = [
532
+ ...(harnessToolsEnabled && allow.includes(GET_CAPABILITIES) ? [getCapabilitiesTool(() => ({
533
+ capabilities,
534
+ active: pi?.getActiveToolNames?.() ?? [],
535
+ registered: pi?.getAllTools?.() ?? []
536
+ }))] : []),
356
537
  ...BUILTIN_PLUGIN_REGISTRY.tools({
357
538
  env: this.options.env,
358
539
  caller,
@@ -379,12 +560,15 @@ export class Sessions {
379
560
  }
380
561
  }
381
562
  const readOnly = new Set(tools.filter((tool) => tool.readOnly).map((tool) => tool.name));
563
+ const checkpointPlanInstructions = input.plan
564
+ ? () => input.run.relation === 'subagent' ? PLAN_RESEARCH_SUBAGENT_INSTRUCTIONS : PLAN_INSTRUCTIONS
565
+ : undefined;
382
566
  const planModeSafety = new Map();
383
567
  for (const tool of tools)
384
568
  if (tool.planModeSafe)
385
569
  planModeSafety.set(tool.name, tool.planModeSafe);
386
570
  const gate = (() => {
387
- const baseGate = this.gate(run, caller, new Set((input.tools.gated ?? []).filter((name) => capabilities.tools.includes(name))));
571
+ const baseGate = this.gate(run, caller, new Set((input.tools.gated ?? []).filter((name) => capabilities.tools.includes(name))), false, input.automatic === true);
388
572
  return {
389
573
  ...baseGate,
390
574
  check: (name, args) => {
@@ -401,7 +585,9 @@ export class Sessions {
401
585
  ...(input.initialBashContext !== undefined && input.initialBashContext !== null ? { initialBashContext: input.initialBashContext } : {}),
402
586
  gate,
403
587
  restrictTools: input.run.relation === 'subagent',
588
+ ...(input.plan ? { plan: true } : {}),
404
589
  cwd: input.cwd,
590
+ workspaceRoot,
405
591
  sessionDir: input.sessionDir,
406
592
  historyFile,
407
593
  model: input.model ?? null,
@@ -414,10 +600,20 @@ export class Sessions {
414
600
  account: context,
415
601
  resources: { ids: [], skillPaths: variant?.skillPaths ?? [], promptPaths: [] },
416
602
  variantExtensions: variant?.extensionPaths ?? [],
417
- inline: [...firstPartyFor(root, capabilities.contributions), runLineageJoin(root, runId), this.options.subagents.extension(runId), { name: 'image-pruning', factory: imagePruning() }],
603
+ inline: [
604
+ ...firstPartyFor(root, capabilities.contributions),
605
+ runLineageJoin(root, runId),
606
+ this.options.subagents.extension(runId),
607
+ { name: 'image-pruning', factory: imagePruning() },
608
+ { name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialTodo: continuation?.todo, planInstructions: checkpointPlanInstructions }) },
609
+ ...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable }) }] : [])
610
+ ],
418
611
  ...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
419
612
  system,
420
- update
613
+ ...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
614
+ // A command sends no request of its own, so the context update waits for the next turn.
615
+ update: input.command ? null : update,
616
+ ...(input.command ? { command: input.command } : {})
421
617
  };
422
618
  pi = await this.factory({
423
619
  params,
@@ -440,12 +636,19 @@ export class Sessions {
440
636
  throw new HarnessError('native_resume_unavailable', 'The saved conversation could not be restored.');
441
637
  }
442
638
  const opened = pi;
443
- updateRun(root, runId, (record) => void (record.nativeSessionId = opened.sessionId));
639
+ const openedModel = pi.model?.id && pi.model.provider ? `${pi.model.provider}/${pi.model.id}` : null;
640
+ updateRun(root, runId, (record) => {
641
+ record.nativeSessionId = opened.sessionId;
642
+ if (openedModel)
643
+ record.model = openedModel;
644
+ if (opened.thinkingLevel)
645
+ record.effort = opened.thinkingLevel;
646
+ });
444
647
  this.options.subagents.register(input, account.id, capabilities, Boolean(pi.sessionManager));
445
- if (pi.model?.id && pi.model.provider)
446
- this.options.subagents.resolvedModel(runId, `${pi.model.provider}/${pi.model.id}`, pi.thinkingLevel);
648
+ if (openedModel)
649
+ this.options.subagents.resolvedModel(runId, openedModel, pi.thinkingLevel);
447
650
  params.settle = () => this.options.subagents.verify(runId, opened.sessionManager ? subagentHistory(opened.sessionManager.getBranch()) : undefined);
448
- void runTurn(run, pi, params);
651
+ void runTurn(run, pi, params).then(() => (sliceEnabled && !input.command ? this.continueSlice({ root, runId, source: input, policy: slicePolicy }) : undefined));
449
652
  return { runId, nativeSessionId: pi.sessionId };
450
653
  }
451
654
  catch (error) {
@@ -14,7 +14,12 @@ type Started = {
14
14
  export type SpawnedSubagent = Started & {
15
15
  agent?: string;
16
16
  unavailableTools?: string[];
17
+ continued?: string;
17
18
  };
19
+ export interface SubagentRuntimeSettings {
20
+ model: string | null;
21
+ effort: Effort | null;
22
+ }
18
23
  export interface SubagentServiceOptions {
19
24
  /** The plugin descriptors, so a profile's plugin list can drop the tools those plugins own. */
20
25
  descriptors?: () => readonly PluginDescriptor[];
@@ -30,12 +35,31 @@ export declare class Subagents {
30
35
  private readonly launch;
31
36
  private readonly options;
32
37
  private readonly parents;
38
+ private readonly runtimeSettingsByRun;
33
39
  constructor(env: HarnessEnv, runs: RunRegistry, launch: (input: SessionRunInput) => Promise<Started>, options?: SubagentServiceOptions);
34
40
  /** Internal adapter contract: context comes from a live run, never from model arguments. */
35
41
  register(input: SessionRunInput, accountId: string, capabilities: CapabilitySet, deferUntilPrompt?: boolean): void;
42
+ runtimeSettings(runId: string): SubagentRuntimeSettings | null;
43
+ private rememberRuntimeSettings;
36
44
  resolvedModel(runId: string, model: string, effort?: Effort): void;
37
45
  private parent;
46
+ private planSpawnCheck;
47
+ /** The plan-mode per-call gate and spawn implementation share this eligibility check. */
48
+ planSpawnRefusal(parentRunId: string, args: unknown): string | null;
38
49
  spawn(request: SubagentInput): Promise<SpawnedSubagent>;
50
+ /** Records ownership, starts the child, and writes a failed record when it never opened. */
51
+ private launchChild;
52
+ /** The children this parent may read or continue: its own, and those recovered from its history. */
53
+ private owned;
54
+ /** The newest run in a continued child's chain, among the runs this parent owns. */
55
+ private latestLink;
56
+ /** The first run of a chain: its folder holds the child's history for every later link. */
57
+ private firstLink;
58
+ /**
59
+ * Continues a stopped child as a new child run with `continueFrom`: the same history, profile,
60
+ * account, variant version and capability set, which must still lie within the parent's.
61
+ */
62
+ private continueChild;
39
63
  private selected;
40
64
  results(selection: SubagentSelection): SubagentResult[];
41
65
  wait(selection: SubagentSelection, signal?: AbortSignal): Promise<SubagentResult[]>;