@xingular/harness 1.1.1 → 1.2.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 (223) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +28 -25
  3. package/dist/cli.js +6675 -2501
  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 +15 -1
  11. package/dist/sdk/agents/planResearch.d.ts +5 -0
  12. package/dist/sdk/agents/planResearch.js +19 -0
  13. package/dist/sdk/agents/profiles.d.ts +2 -1
  14. package/dist/sdk/agents/profiles.js +9 -4
  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/cli.js +5 -4
  26. package/dist/sdk/commands/list.d.ts +3 -0
  27. package/dist/sdk/commands/list.js +75 -0
  28. package/dist/sdk/commands/registry.d.ts +250 -0
  29. package/dist/sdk/commands/registry.js +47 -0
  30. package/dist/sdk/commands/routes.d.ts +16 -0
  31. package/dist/sdk/commands/routes.js +33 -0
  32. package/dist/sdk/core/commandBudget.d.ts +23 -0
  33. package/dist/sdk/core/commandBudget.js +57 -0
  34. package/dist/sdk/core/index.js +17 -1
  35. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  36. package/dist/sdk/core/interactiveProfile.js +143 -0
  37. package/dist/sdk/core/workspace.d.ts +13 -0
  38. package/dist/sdk/core/workspace.js +88 -0
  39. package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
  40. package/dist/sdk/core/workspaceBashTool.js +36 -0
  41. package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
  42. package/dist/sdk/core/workspaceSandbox.js +320 -0
  43. package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
  44. package/dist/sdk/core/workspaceWriteOperations.js +206 -0
  45. package/dist/sdk/doctor/types.d.ts +2 -2
  46. package/dist/sdk/harness.d.ts +7 -0
  47. package/dist/sdk/harness.js +42 -2
  48. package/dist/sdk/index.d.ts +6 -1
  49. package/dist/sdk/index.js +3 -0
  50. package/dist/sdk/pi/runtime.d.ts +2 -1
  51. package/dist/sdk/pi/runtime.js +2 -2
  52. package/dist/sdk/plugins/builtin.js +29 -12
  53. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  54. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  55. package/dist/sdk/plugins/catalogue.js +2 -1
  56. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  57. package/dist/sdk/plugins/commandPrefix.js +21 -0
  58. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  59. package/dist/sdk/plugins/firstParty.js +8 -2
  60. package/dist/sdk/plugins/inventory.js +4 -3
  61. package/dist/sdk/plugins/manifest.d.ts +2 -0
  62. package/dist/sdk/plugins/manifest.js +2 -1
  63. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  64. package/dist/sdk/plugins/surfaces.js +11 -0
  65. package/dist/sdk/plugins/types.d.ts +15 -0
  66. package/dist/sdk/prompt/compile.d.ts +14 -0
  67. package/dist/sdk/prompt/compile.js +65 -29
  68. package/dist/sdk/prompt/standard.d.ts +17 -0
  69. package/dist/sdk/prompt/standard.js +21 -0
  70. package/dist/sdk/server/methods.js +18 -0
  71. package/dist/sdk/server/remote.js +36 -11
  72. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  73. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  74. package/dist/sdk/sessions/continuation.d.ts +3 -0
  75. package/dist/sdk/sessions/continuation.js +5 -1
  76. package/dist/sdk/sessions/contracts.d.ts +168 -0
  77. package/dist/sdk/sessions/contracts.js +59 -1
  78. package/dist/sdk/sessions/control.d.ts +72 -0
  79. package/dist/sdk/sessions/control.js +158 -0
  80. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  81. package/dist/sdk/sessions/initialBash.js +7 -0
  82. package/dist/sdk/sessions/planMode.d.ts +14 -3
  83. package/dist/sdk/sessions/planMode.js +73 -16
  84. package/dist/sdk/sessions/registry.d.ts +1 -1
  85. package/dist/sdk/sessions/registry.js +2 -2
  86. package/dist/sdk/sessions/runStore.d.ts +14 -0
  87. package/dist/sdk/sessions/runStore.js +35 -0
  88. package/dist/sdk/sessions/runner.d.ts +40 -2
  89. package/dist/sdk/sessions/runner.js +161 -13
  90. package/dist/sdk/sessions/sessions.d.ts +23 -3
  91. package/dist/sdk/sessions/sessions.js +202 -17
  92. package/dist/sdk/sessions/subagentService.d.ts +9 -0
  93. package/dist/sdk/sessions/subagentService.js +63 -4
  94. package/dist/sdk/sessions/workSlice.d.ts +103 -0
  95. package/dist/sdk/sessions/workSlice.js +297 -0
  96. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  97. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  98. package/dist/sdk/terminal/browserControl.js +2 -1
  99. package/dist/sdk/terminal/controller.d.ts +11 -3
  100. package/dist/sdk/terminal/controller.js +40 -13
  101. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  102. package/dist/sdk/terminal/opentui/theme.js +87 -0
  103. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  104. package/dist/sdk/terminal/piExtension.js +154 -54
  105. package/dist/sdk/terminal/standalone.d.ts +7 -0
  106. package/dist/sdk/terminal/standalone.js +72 -13
  107. package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
  108. package/dist/sdk/terminal/subagentMonitor.js +32 -1
  109. package/dist/sdk/terminal/terminals.d.ts +19 -2
  110. package/dist/sdk/terminal/terminals.js +245 -35
  111. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  112. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  113. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  114. package/dist/sdk/terminal/tui/commandPages.js +3 -0
  115. package/dist/sdk/terminal/tui/commands.d.ts +2 -0
  116. package/dist/sdk/terminal/tui/commands.js +205 -124
  117. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  118. package/dist/sdk/terminal/tui/list.js +96 -14
  119. package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
  120. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  121. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  122. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
  124. package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
  125. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  126. package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
  127. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  128. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  129. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  130. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  131. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  132. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  133. package/dist/sdk/terminal/tui/runtime.js +23 -3
  134. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  135. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  136. package/dist/sdk/tools/checklist.js +1 -0
  137. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  138. package/dist/sdk/tools/checkpoint.js +154 -0
  139. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  140. package/dist/sdk/tools/getCapabilities.js +34 -0
  141. package/dist/sdk/tools/spawnSubagent.js +4 -3
  142. package/dist/sdk/usage/analytics.d.ts +20 -0
  143. package/dist/sdk/usage/analytics.js +151 -0
  144. package/dist/sdk/usage/ledger.d.ts +38 -0
  145. package/dist/sdk/usage/ledger.js +142 -0
  146. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  147. package/dist/sdk/usage/limits/adapters.js +349 -0
  148. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  149. package/dist/sdk/usage/limits/capture.js +47 -0
  150. package/dist/sdk/usage/limits/http.d.ts +12 -0
  151. package/dist/sdk/usage/limits/http.js +51 -0
  152. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  153. package/dist/sdk/usage/limits/normalize.js +211 -0
  154. package/dist/sdk/usage/limits/service.d.ts +30 -0
  155. package/dist/sdk/usage/limits/service.js +176 -0
  156. package/dist/sdk/usage/limits/types.d.ts +65 -0
  157. package/dist/sdk/usage/limits/types.js +1 -0
  158. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  159. package/dist/sdk/usage/serverMethods.js +22 -0
  160. package/dist/sdk/usage/service.d.ts +32 -0
  161. package/dist/sdk/usage/service.js +66 -0
  162. package/dist/sdk/usage/settings.d.ts +7 -0
  163. package/dist/sdk/usage/settings.js +28 -0
  164. package/dist/sdk/usage/types.d.ts +60 -0
  165. package/dist/sdk/usage/types.js +4 -0
  166. package/dist/terminal-opentui.js +5207 -1883
  167. package/dist/terminal-pi.js +1658 -271
  168. package/docs/architecture/02-sessions-and-runs.md +128 -26
  169. package/docs/architecture/04-prompt.md +50 -12
  170. package/docs/architecture/06-plugins-and-capabilities.md +24 -7
  171. package/docs/architecture/07-tools-and-gate.md +104 -24
  172. package/docs/architecture/08-memory-and-history.md +4 -2
  173. package/docs/architecture/09-accounts-and-connectors.md +31 -1
  174. package/docs/architecture/10-server-protocol.md +58 -3
  175. package/docs/architecture/11-terminal.md +105 -42
  176. package/docs/architecture/12-storage.md +30 -2
  177. package/docs/architecture/13-world-tools.md +22 -1
  178. package/docs/architecture/14-surfaces-and-parity.md +54 -7
  179. package/docs/architecture/README.md +3 -1
  180. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  181. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  182. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  183. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  184. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  185. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  186. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  187. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  188. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  189. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  190. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  191. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  192. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
  193. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  194. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  195. package/docs/architecture/proposals/README.md +14 -0
  196. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  197. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  198. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  199. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  200. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  201. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  202. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  203. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  204. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  205. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  206. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  207. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  208. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  209. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  210. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  211. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  212. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  213. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  214. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  215. package/docs/parity.md +80 -24
  216. package/docs/releases/1.1.1.md +28 -0
  217. package/docs/releases/1.2.0.md +49 -0
  218. package/docs/releases/4.0.0.md +10 -9
  219. package/docs/releases/README.md +35 -30
  220. package/docs/study/00-index.md +3 -0
  221. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  222. package/docs/terminal-tui.md +100 -59
  223. package/package.json +4 -3
@@ -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";
@@ -16,17 +19,43 @@ import { imagePruning } from "../world/images.js";
16
19
  import { materialise } from "../variant/materialise.js";
17
20
  import { ASK_USER } from "../tools/askUser.js";
18
21
  import { CHECKLIST_WRITE } from "../tools/checklist.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] : []), CHECKLIST_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,
@@ -310,11 +472,11 @@ export class Sessions {
310
472
  if (!existsSync(input.sessionDir))
311
473
  mkdirSync(input.sessionDir, { recursive: true });
312
474
  const launch = input.launch ?? {};
313
- const plan = planSection(input.plan === true, historyFile);
475
+ const plan = planSection(input.plan === true, historyFile, input.run.relation === 'subagent');
314
476
  const delivered = historyFile ? deliveredSnapshot(historyFile) : null;
315
477
  const resumed = continuationSection(continuation, delivered?.some((entry) => entry.id === CONTINUATION_SECTION_ID) ?? false);
316
478
  // 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;
479
+ const profiles = capabilities.tools.includes(SPAWN_SUBAGENT) ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true) : null;
318
480
  if (profiles && variant)
319
481
  variant = { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) };
320
482
  const own = [plan, resumed, profiles].filter((section) => section !== null);
@@ -331,6 +493,9 @@ export class Sessions {
331
493
  fail(error);
332
494
  throw error;
333
495
  }
496
+ // A reply is bounded only when its checkpoint tool is part of the frozen capability set.
497
+ const sliceEnabled = slicePolicy.enabled && capabilities.tools.includes(CHECKPOINT_WRITE);
498
+ const sliceState = createSliceState(slicePolicy, sliceOf(root, runId));
334
499
  const run = this.options.runs.start(input.run.sessionId, runId);
335
500
  if (run.finished)
336
501
  return { runId, nativeSessionId: null };
@@ -353,6 +518,11 @@ export class Sessions {
353
518
  });
354
519
  try {
355
520
  const tools = [
521
+ ...(harnessToolsEnabled && allow.includes(GET_CAPABILITIES) ? [getCapabilitiesTool(() => ({
522
+ capabilities,
523
+ active: pi?.getActiveToolNames?.() ?? [],
524
+ registered: pi?.getAllTools?.() ?? []
525
+ }))] : []),
356
526
  ...BUILTIN_PLUGIN_REGISTRY.tools({
357
527
  env: this.options.env,
358
528
  caller,
@@ -379,12 +549,15 @@ export class Sessions {
379
549
  }
380
550
  }
381
551
  const readOnly = new Set(tools.filter((tool) => tool.readOnly).map((tool) => tool.name));
552
+ const checkpointPlanInstructions = input.plan
553
+ ? () => input.run.relation === 'subagent' ? PLAN_RESEARCH_SUBAGENT_INSTRUCTIONS : PLAN_INSTRUCTIONS
554
+ : undefined;
382
555
  const planModeSafety = new Map();
383
556
  for (const tool of tools)
384
557
  if (tool.planModeSafe)
385
558
  planModeSafety.set(tool.name, tool.planModeSafe);
386
559
  const gate = (() => {
387
- const baseGate = this.gate(run, caller, new Set((input.tools.gated ?? []).filter((name) => capabilities.tools.includes(name))));
560
+ const baseGate = this.gate(run, caller, new Set((input.tools.gated ?? []).filter((name) => capabilities.tools.includes(name))), false, input.automatic === true);
388
561
  return {
389
562
  ...baseGate,
390
563
  check: (name, args) => {
@@ -401,7 +574,9 @@ export class Sessions {
401
574
  ...(input.initialBashContext !== undefined && input.initialBashContext !== null ? { initialBashContext: input.initialBashContext } : {}),
402
575
  gate,
403
576
  restrictTools: input.run.relation === 'subagent',
577
+ ...(input.plan ? { plan: true } : {}),
404
578
  cwd: input.cwd,
579
+ workspaceRoot,
405
580
  sessionDir: input.sessionDir,
406
581
  historyFile,
407
582
  model: input.model ?? null,
@@ -414,10 +589,20 @@ export class Sessions {
414
589
  account: context,
415
590
  resources: { ids: [], skillPaths: variant?.skillPaths ?? [], promptPaths: [] },
416
591
  variantExtensions: variant?.extensionPaths ?? [],
417
- inline: [...firstPartyFor(root, capabilities.contributions), runLineageJoin(root, runId), this.options.subagents.extension(runId), { name: 'image-pruning', factory: imagePruning() }],
592
+ inline: [
593
+ ...firstPartyFor(root, capabilities.contributions),
594
+ runLineageJoin(root, runId),
595
+ this.options.subagents.extension(runId),
596
+ { name: 'image-pruning', factory: imagePruning() },
597
+ { name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialChecklist: continuation?.checklist, planInstructions: checkpointPlanInstructions }) },
598
+ ...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState }) }] : [])
599
+ ],
418
600
  ...(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
601
  system,
420
- update
602
+ ...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
603
+ // A command sends no request of its own, so the context update waits for the next turn.
604
+ update: input.command ? null : update,
605
+ ...(input.command ? { command: input.command } : {})
421
606
  };
422
607
  pi = await this.factory({
423
608
  params,
@@ -445,7 +630,7 @@ export class Sessions {
445
630
  if (pi.model?.id && pi.model.provider)
446
631
  this.options.subagents.resolvedModel(runId, `${pi.model.provider}/${pi.model.id}`, pi.thinkingLevel);
447
632
  params.settle = () => this.options.subagents.verify(runId, opened.sessionManager ? subagentHistory(opened.sessionManager.getBranch()) : undefined);
448
- void runTurn(run, pi, params);
633
+ void runTurn(run, pi, params).then(() => (sliceEnabled && !input.command ? this.continueSlice({ root, runId, source: input, policy: slicePolicy }) : undefined));
449
634
  return { runId, nativeSessionId: pi.sessionId };
450
635
  }
451
636
  catch (error) {
@@ -15,6 +15,10 @@ export type SpawnedSubagent = Started & {
15
15
  agent?: string;
16
16
  unavailableTools?: string[];
17
17
  };
18
+ export interface SubagentRuntimeSettings {
19
+ model: string | null;
20
+ effort: Effort | null;
21
+ }
18
22
  export interface SubagentServiceOptions {
19
23
  /** The plugin descriptors, so a profile's plugin list can drop the tools those plugins own. */
20
24
  descriptors?: () => readonly PluginDescriptor[];
@@ -30,11 +34,16 @@ export declare class Subagents {
30
34
  private readonly launch;
31
35
  private readonly options;
32
36
  private readonly parents;
37
+ private readonly runtimeSettingsByRun;
33
38
  constructor(env: HarnessEnv, runs: RunRegistry, launch: (input: SessionRunInput) => Promise<Started>, options?: SubagentServiceOptions);
34
39
  /** Internal adapter contract: context comes from a live run, never from model arguments. */
35
40
  register(input: SessionRunInput, accountId: string, capabilities: CapabilitySet, deferUntilPrompt?: boolean): void;
41
+ runtimeSettings(runId: string): SubagentRuntimeSettings | null;
42
+ private rememberRuntimeSettings;
36
43
  resolvedModel(runId: string, model: string, effort?: Effort): void;
37
44
  private parent;
45
+ /** The plan-mode per-call gate and spawn implementation share this eligibility check. */
46
+ planSpawnRefusal(parentRunId: string, args: unknown): string | null;
38
47
  spawn(request: SubagentInput): Promise<SpawnedSubagent>;
39
48
  private selected;
40
49
  results(selection: SubagentSelection): SubagentResult[];
@@ -6,6 +6,7 @@ import { now } from "../core/fs.js";
6
6
  import { takeChars } from "../core/text.js";
7
7
  import { isActiveStatus } from "./contracts.js";
8
8
  import { narrowForProfile } from "../agents/narrow.js";
9
+ import { PLAN_RESEARCH_EVIDENCE_TOOLS, PLAN_RESEARCH_TOOLS, planResearchProfile } from "../agents/planResearch.js";
9
10
  import { resolveProfile } from "../agents/profiles.js";
10
11
  import { findRun, loadRun, readEvents, recordedSubagents, recordSubagent, saveRun } from "./runStore.js";
11
12
  import { SPAWN_SUBAGENT, SUBAGENT_RELATION, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, briefTitle, subagentHistory, subagentTitle } from "./subagents.js";
@@ -27,6 +28,7 @@ export class Subagents {
27
28
  launch;
28
29
  options;
29
30
  parents = new Map();
31
+ runtimeSettingsByRun = new Map();
30
32
  constructor(env, runs, launch, options = {}) {
31
33
  this.env = env;
32
34
  this.runs = runs;
@@ -66,12 +68,26 @@ export class Subagents {
66
68
  started();
67
69
  this.parents.set(input.run.id, { ready, started, input: structuredClone({ ...input, text: '', images: undefined, requiresImageInput: false, preamble: null, resume: null }), accountId, capabilities: ceiling, stopped: new AbortController(), recovered: new Set(), launching: new Map() });
68
70
  }
71
+ runtimeSettings(runId) {
72
+ return this.runtimeSettingsByRun.get(runId) ?? null;
73
+ }
74
+ rememberRuntimeSettings(runId, settings) {
75
+ this.runtimeSettingsByRun.set(runId, settings);
76
+ if (this.runtimeSettingsByRun.size > 500) {
77
+ const oldest = this.runtimeSettingsByRun.keys().next().value;
78
+ if (oldest !== undefined)
79
+ this.runtimeSettingsByRun.delete(oldest);
80
+ }
81
+ }
69
82
  resolvedModel(runId, model, effort) {
70
83
  const parent = this.parents.get(runId);
71
84
  if (parent) {
72
85
  parent.input.model = model;
73
86
  parent.input.effort = effort ?? parent.input.effort;
74
87
  }
88
+ const settings = this.runtimeSettingsByRun.get(runId);
89
+ if (settings)
90
+ this.rememberRuntimeSettings(runId, { model, effort: effort ?? settings.effort });
75
91
  }
76
92
  parent(runId) {
77
93
  const parent = this.parents.get(runId);
@@ -79,6 +95,34 @@ export class Subagents {
79
95
  throw new HarnessError('run_not_active', 'The parent run is not accepting subagents.');
80
96
  return parent;
81
97
  }
98
+ /** The plan-mode per-call gate and spawn implementation share this eligibility check. */
99
+ planSpawnRefusal(parentRunId, args) {
100
+ const parent = this.parents.get(parentRunId);
101
+ if (!parent || !this.runs.active(parentRunId))
102
+ return 'The parent run is not accepting subagents.';
103
+ if (!parent.input.plan)
104
+ return 'Read-only research delegation is available only during plan mode.';
105
+ if (!parent.capabilities.tools.includes(SPAWN_SUBAGENT))
106
+ return 'Subagents are not enabled for this run.';
107
+ if (!args || typeof args !== 'object' || Array.isArray(args))
108
+ return 'Plan-mode delegation requires a named, eligible read-only research profile.';
109
+ const request = args;
110
+ if (typeof request.agent !== 'string' || !request.agent.trim())
111
+ return 'Plan-mode delegation requires a named, eligible read-only research profile.';
112
+ const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null);
113
+ if (!profile)
114
+ return `There is no agent profile named ${request.agent.trim()}.`;
115
+ if (!planResearchProfile(profile))
116
+ return 'This profile is not eligible for plan-mode research; use an explicit read-only research profile.';
117
+ if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some(name => typeof name !== 'string' || !PLAN_RESEARCH_TOOLS.has(name) || !profile.tools.includes(name) || !parent.capabilities.tools.includes(name)))) {
118
+ return 'Plan-mode subagent tools must be plan-safe and included in the selected profile and parent capabilities.';
119
+ }
120
+ const requested = Array.isArray(request.tools) ? request.tools : null;
121
+ const available = profile.tools.filter(name => parent.capabilities.tools.includes(name) && (!requested || requested.includes(name)));
122
+ if (!available.some(name => PLAN_RESEARCH_EVIDENCE_TOOLS.has(name)))
123
+ return 'The selected profile has no available read-only research tools in this run.';
124
+ return null;
125
+ }
82
126
  async spawn(request) {
83
127
  const parent = this.parent(request.parentRunId);
84
128
  const { input, accountId, capabilities } = parent;
@@ -97,6 +141,11 @@ export class Subagents {
97
141
  const outside = request.tools?.filter((name) => !capabilities.tools.includes(name)) ?? [];
98
142
  if (outside.length)
99
143
  throw invalid(`A subagent cannot have tools you do not have: ${outside.join(', ')}.`);
144
+ if (input.plan) {
145
+ const refusal = this.planSpawnRefusal(request.parentRunId, request);
146
+ if (refusal)
147
+ throw invalid(refusal);
148
+ }
100
149
  await until(parent.ready, parent.stopped.signal);
101
150
  let variant = input.variant ?? null;
102
151
  let launch = input.launch;
@@ -122,20 +171,30 @@ export class Subagents {
122
171
  effort = profile.effort;
123
172
  launch = { ...(input.launch ?? {}), sections: [...(input.launch?.sections ?? []).filter((section) => section.id !== 'subagent'), { id: 'subagent', title: `Your role: ${profile.name}`, body: profile.instructions || profile.description, budget: 16_000, order: 22 }] };
124
173
  }
174
+ if (input.plan) {
175
+ const refusal = this.planSpawnRefusal(request.parentRunId, request);
176
+ if (refusal)
177
+ throw invalid(refusal);
178
+ }
125
179
  const narrowed = narrowForProfile(capabilities, profile, request.tools, this.options.descriptors?.() ?? []);
126
- const tools = narrowed.capabilities.tools;
180
+ const childCapabilities = input.plan
181
+ ? { ...narrowed.capabilities, tools: narrowed.capabilities.tools.filter(name => PLAN_RESEARCH_TOOLS.has(name)), connectors: [] }
182
+ : narrowed.capabilities;
183
+ const tools = childCapabilities.tools;
127
184
  const runId = randomUUID();
128
185
  const sessionId = randomUUID();
186
+ this.rememberRuntimeSettings(runId, { model: model ?? null, effort: effort ?? null });
129
187
  recordSubagent(this.env.root, request.parentRunId, runId);
130
188
  const child = {
131
189
  ...input,
132
- run: { ...input.run, id: runId, sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, goal: takeChars(brief, 4000), title, attachmentCount: 0, resumedFromRunId: null, handoffId: null, profile: profile?.name ?? null },
190
+ run: { ...input.run, ...(input.plan ? { worktreeId: null } : {}), id: runId, sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, goal: takeChars(brief, 4000), title, attachmentCount: 0, resumedFromRunId: null, handoffId: null, profile: profile?.name ?? null },
133
191
  accountId, poolId: null, sessionDir: join(input.sessionDir, 'subagents', runId), resume: null,
134
192
  text: brief, images: undefined, requiresImageInput: false, preamble: null,
135
193
  caller: { ...input.caller, runId: undefined, sessionId },
136
194
  tools: { allow: tools, gated: (input.tools.gated ?? []).filter((name) => tools.includes(name)) },
137
- capabilities: narrowed.capabilities, depth: (input.depth ?? 0) + 1,
138
- variant, launch, model, effort
195
+ capabilities: childCapabilities, depth: (input.depth ?? 0) + 1,
196
+ variant, launch, model, effort,
197
+ ...(input.plan ? { connections: false, subagents: null } : {})
139
198
  };
140
199
  const started = this.launch(child).catch((error) => {
141
200
  if (!findRun(this.env.root, runId)) {
@@ -0,0 +1,103 @@
1
+ import type { Checkpoint } from "./contracts.js";
2
+ export declare const WORK_SLICE_EXTENSION = "work-slice";
3
+ export interface WorkSliceInput {
4
+ /** False turns the guard off for this run: no budget, no stall notes, no continuation. */
5
+ enabled?: boolean;
6
+ /** Tool calls in one reply; 0 disables this limit. */
7
+ maxToolCalls?: number;
8
+ /** Minutes in one reply; 0 disables this limit. */
9
+ maxMinutes?: number;
10
+ /** New tokens in one reply (input + cache read + output); 0 disables this limit. */
11
+ maxNewTokens?: number;
12
+ /** Slices one task may use before it is handed back to the person; 0 disables the cap. */
13
+ maxSlices?: number;
14
+ /** Start the next slice from the checkpoint without asking. */
15
+ continue?: boolean;
16
+ }
17
+ export interface SlicePolicy {
18
+ enabled: boolean;
19
+ maxToolCalls: number;
20
+ maxMinutes: number;
21
+ maxNewTokens: number;
22
+ maxSlices: number;
23
+ continue: boolean;
24
+ }
25
+ /** The foundation defaults: enough for one meaningful step of work, short enough to report. */
26
+ export declare const DEFAULT_SLICE_POLICY: SlicePolicy;
27
+ export declare class WorkSliceError extends Error {
28
+ }
29
+ /**
30
+ * The policy a run asked for, or the defaults. `XINGULAR_WORK_SLICE=off` at launch turns the guard
31
+ * off for every run in this process; nothing is persisted, so restarting without it restores the
32
+ * defaults.
33
+ */
34
+ export declare function parseWorkSlice(input: unknown): SlicePolicy;
35
+ export type SliceEnd = 'budget' | 'stuck' | 'compaction-thrash';
36
+ /** What a live reply tracks for its slice; the runner reads it after the reply. */
37
+ export interface SliceState {
38
+ slice: number;
39
+ steps: number;
40
+ tokens: number;
41
+ startedAt: number;
42
+ lastTool: string | null;
43
+ compactions: number;
44
+ checkpointWritten: boolean;
45
+ /** Set when the guard ended the reply instead of the model finishing it. */
46
+ endedBy: SliceEnd | null;
47
+ /** The limit that ended it, for the record. */
48
+ endedByLimit: string | null;
49
+ /** Steps past which every non-exempt tool call is refused, once one was refused. */
50
+ refusedAt: number | null;
51
+ }
52
+ export declare function createSliceState(_policy: SlicePolicy, slice?: number, now?: number): SliceState;
53
+ /** Counters start again for each reply; a terminal session keeps one guard across replies. */
54
+ export declare function resetSliceState(state: SliceState, now?: number): void;
55
+ /** The first limit this reply has passed, named as a person would say it. */
56
+ export declare function sliceLimitReached(policy: SlicePolicy, state: SliceState, now?: number): string | null;
57
+ /** Commands that read as a check, for "no progress" and "still failing" rules. */
58
+ export declare function looksLikeCheck(command: string): boolean;
59
+ interface ToolCall {
60
+ toolName: string;
61
+ input?: unknown;
62
+ }
63
+ interface ToolResult {
64
+ toolName: string;
65
+ isError?: boolean;
66
+ input?: unknown;
67
+ }
68
+ /** The part of Pi's extension API this guard uses. Structural, so the module needs no Pi import. */
69
+ interface GuardApi {
70
+ on(event: 'agent_start', handler: () => unknown): void;
71
+ on(event: 'tool_call', handler: (event: ToolCall) => unknown): void;
72
+ on(event: 'tool_result', handler: (event: ToolResult) => unknown): void;
73
+ on(event: 'message_end', handler: (event: {
74
+ message: {
75
+ role: string;
76
+ usage?: {
77
+ input?: number;
78
+ output?: number;
79
+ cacheRead?: number;
80
+ cacheWrite?: number;
81
+ };
82
+ };
83
+ }) => unknown): void;
84
+ }
85
+ export interface WorkSliceGuardOptions {
86
+ policy: SlicePolicy;
87
+ state: SliceState;
88
+ /** Called once the guard has decided the reply must stop; the runner ends it. */
89
+ onEnd?: (end: SliceEnd, limit: string) => void;
90
+ }
91
+ /**
92
+ * Bounds one reply and names repeated work. The first refusal asks for a checkpoint; a tool call
93
+ * after that refusal ends the reply. A repeated call or failing command is named once, then ends it.
94
+ */
95
+ export declare function workSliceGuard(options: WorkSliceGuardOptions): (pi: GuardApi) => void;
96
+ /**
97
+ * Which slice of its session a run is, from 1. Counts the recorded continuation chain, so it
98
+ * survives a restart and needs no extra field on the run record.
99
+ */
100
+ export declare function sliceOf(root: string, runId: string, limit?: number): number;
101
+ /** The reason a checkpoint the harness derives (no model handoff) records. */
102
+ export declare function derivedReason(end: SliceEnd | null): Checkpoint['reason'];
103
+ export {};