@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
@@ -13,6 +13,8 @@ import { readAudit } from "../accounts/store.js";
13
13
  import { log } from "../core/bus.js";
14
14
  import { HarnessError, invalid } from "../core/errors.js";
15
15
  import { newId, now } from "../core/fs.js";
16
+ import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
17
+ import { resolveWorkspaceRoot } from "../core/workspace.js";
16
18
  import { prependSearchPath, searchPath } from "../core/path.js";
17
19
  import { dispatch } from "../tools/dispatch.js";
18
20
  import { materialise } from "../variant/materialise.js";
@@ -20,8 +22,14 @@ import * as componentLibrary from "../variant/components.js";
20
22
  import * as variantStore from "../variant/store.js";
21
23
  import { compile, copyToRun, record, render, saveForSession } from "../prompt/compile.js";
22
24
  import { isActiveStatus } from "../sessions/contracts.js";
23
- import { findRun, loadRun, saveRun, updateRun } from "../sessions/runStore.js";
24
- import { CHECKLIST_WRITE } from "../tools/checklist.js";
25
+ import { findRun, loadRun, readEvents, recordedSubagents, saveRun, updateRun } from "../sessions/runStore.js";
26
+ import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "../sessions/workspaceBoundary.js";
27
+ import { TODO_WRITE } from "../tools/todo.js";
28
+ import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
29
+ import { harnessTools } from "../tools/harnessTools.js";
30
+ import { HARNESS_PLUGINS } from "../plugins/builtin.js";
31
+ import * as attachments from "../plugins/attachments.js";
32
+ import { FIRST_PARTY_PLUGINS } from "../plugins/firstParty.js";
25
33
  import { createProfile, listProfiles, profileDirs, profileSection, removeProfile, resolveProfile } from "../agents/profiles.js";
26
34
  import { SESSION_NAME } from "../tools/sessionName.js";
27
35
  import { contributionId } from "../plugins/identity.js";
@@ -34,7 +42,7 @@ import { TerminalController } from "./controller.js";
34
42
  import { terminalCommandFiles } from "./launch.js";
35
43
  import { SessionSocket, socketPath } from "./socket.js";
36
44
  import { harnessLogInfo, readHarnessLogChunk } from "./logs.js";
37
- import { listTerminalTrajectoryRuns, readTerminalTrajectory } from "./trajectory.js";
45
+ import { listTerminalTrajectoryRuns, readTerminalTrajectory, latestTerminalTodo } from "./trajectory.js";
38
46
  import { BrowserControl } from "./browserControl.js";
39
47
  /** Identity of the exact resource decision that this terminal was prepared with. */
40
48
  function resourceIdentity(loading) {
@@ -90,12 +98,61 @@ export class Terminals {
90
98
  }
91
99
  return this.childProfiles.get(record.id) ?? null;
92
100
  }
101
+ /** Account, model and effort for a monitor row, from the run record. */
102
+ monitorRuntime(live, record) {
103
+ let accountLabel = record.accountId === live.account.id ? live.account.label : null;
104
+ if (accountLabel === null && record.accountId) {
105
+ try {
106
+ accountLabel = this.options.accounts.find(record.accountId).label;
107
+ }
108
+ catch { }
109
+ }
110
+ return { accountLabel, model: record.model ?? null, effort: record.effort ?? null };
111
+ }
112
+ /**
113
+ * Shows the reopened conversation's latest parent run again: the last owner on its native branch
114
+ * that ran in this folder, with every descendant flat, as each ended.
115
+ */
116
+ hydrateMonitor(live, owners) {
117
+ const { root } = this.options.env;
118
+ const native = live.controller.nativeSessionId;
119
+ if (!native)
120
+ return;
121
+ for (const id of [...owners].reverse()) {
122
+ const record = findRun(root, id);
123
+ if (!record || record.relation || record.nativeSessionId !== native || record.workingDirectory !== live.input.cwd)
124
+ continue;
125
+ const children = [];
126
+ const seen = new Set([record.id]);
127
+ const visit = (parentId) => {
128
+ for (const childId of recordedSubagents(root, parentId)) {
129
+ if (seen.has(childId))
130
+ continue;
131
+ seen.add(childId);
132
+ const child = findRun(root, childId);
133
+ if (child?.relation !== 'subagent')
134
+ continue;
135
+ children.push(child);
136
+ visit(child.id);
137
+ }
138
+ };
139
+ visit(record.id);
140
+ const entry = (run, profile = null) => ({ record: run, events: readEvents(root, run.id), profile, runtime: this.monitorRuntime(live, run) });
141
+ live.controller.restoreMonitor(entry(record), children.map(child => entry(child, this.subagentProfile(child))));
142
+ return;
143
+ }
144
+ }
93
145
  constructor(options) {
94
146
  this.options = options;
95
147
  this.assets = options.assets ?? defaultAssets();
96
148
  options.runs.listen(event => {
97
- this.sessions.get(event.sessionId)?.controller.observe(event);
149
+ const session = this.sessions.get(event.sessionId);
150
+ session?.controller.observe(event);
98
151
  const record = findRun(options.env.root, event.runId);
152
+ if (record && !record.relation && session) {
153
+ session.controller.observeMain(record, event, this.monitorRuntime(session, record));
154
+ return;
155
+ }
99
156
  if (record?.relation !== 'subagent')
100
157
  return;
101
158
  let parentId = record.parentRunId;
@@ -108,7 +165,12 @@ export class Terminals {
108
165
  const owner = this.sessions.get(parent.sessionId);
109
166
  if (owner?.controller.activeRun?.runId === parent.id) {
110
167
  owner.controller.observe(event, true);
111
- owner.controller.observeSubagent(parent.id, record, event, this.subagentProfile(record));
168
+ const settings = options.subagents?.runtimeSettings(record.id);
169
+ owner.controller.observeSubagent(parent.id, record, event, this.subagentProfile(record), {
170
+ accountLabel: owner.account.label,
171
+ model: settings?.model ?? record.model ?? null,
172
+ effort: settings?.effort ?? record.effort ?? null
173
+ });
112
174
  break;
113
175
  }
114
176
  parentId = parent.parentRunId;
@@ -153,23 +215,48 @@ export class Terminals {
153
215
  }
154
216
  return bin;
155
217
  }
156
- /** The broker's tools, then the world tools whose plugins are on for this session. */
218
+ /**
219
+ * The harness's own History, Wiki and skill tools, then the broker's, then the world tools whose
220
+ * plugins are on for this session: the same tools a structured session has (INV-PAR-1).
221
+ */
157
222
  async tools(session, listing) {
158
223
  const brokered = this.options.apps.brokerTools(session);
159
224
  const live = this.sessions.get(session.sessionId);
160
- const { world } = this.options;
161
- if (!world || !live)
225
+ if (!live)
162
226
  return brokered;
227
+ const own = await this.ownTools(live, listing);
228
+ const { world } = this.options;
229
+ if (!world)
230
+ return [...own, ...brokered];
163
231
  const context = { run: live.controller.activeRun, caller: live.caller, attended: true, subagent: false, sessionKey: session.sessionId, cwd: live.input.cwd };
164
232
  if (!listing)
165
- return [...brokered, ...world.tools(context, (_plugin, tool) => this.worldNames.get(session.sessionId)?.has(tool) ?? false)];
233
+ return [...own, ...brokered, ...world.tools(context, (_plugin, tool) => this.worldNames.get(session.sessionId)?.has(tool) ?? false)];
166
234
  const on = await this.worldOn(live);
167
235
  const tools = world.tools(context, (plugin, tool) => on(plugin, tool));
168
236
  this.worldNames.set(session.sessionId, new Set(tools.map((tool) => tool.name)));
169
237
  for (const tool of tools)
170
238
  if (tool.readOnly)
171
239
  this.worldReadOnly.add(tool.name);
172
- return [...brokered, ...tools];
240
+ return [...own, ...brokered, ...tools];
241
+ }
242
+ ownNames = new Map();
243
+ async ownTools(live, listing) {
244
+ const sessionId = live.caller.sessionId;
245
+ const all = harnessTools(this.options.env, live.caller, { variantDir: this.variantDirs.get(sessionId) ?? null })
246
+ .filter((tool) => DEFAULT_TOOL_NAMES.includes(tool.name));
247
+ if (!listing)
248
+ return all.filter((tool) => this.ownNames.get(sessionId)?.has(tool.name) ?? false);
249
+ const on = await this.worldOn(live);
250
+ const owner = (tool) => HARNESS_PLUGINS.find((plugin) => plugin.tools?.some((entry) => entry.name === tool))?.id ?? null;
251
+ const tools = all.filter((tool) => {
252
+ const plugin = owner(tool.name);
253
+ return !plugin || on(plugin, tool.name);
254
+ });
255
+ this.ownNames.set(sessionId, new Set(tools.map((tool) => tool.name)));
256
+ for (const tool of tools)
257
+ if (tool.readOnly)
258
+ this.worldReadOnly.add(tool.name);
259
+ return tools;
173
260
  }
174
261
  /** The world tools a new session will have, named in its instructions. */
175
262
  async worldToolNames(input, account) {
@@ -181,6 +268,16 @@ export class Terminals {
181
268
  const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
182
269
  return world.plugins.flatMap((plugin) => (plugin.tools ?? []).map((tool) => tool.name).filter((name) => !on || on.has(contributionId(plugin.id, 'tool', name))));
183
270
  }
271
+ /** The harness's own History, Wiki and skill tools a new session will have. */
272
+ async ownToolNames(input, account) {
273
+ const list = this.options.pluginControl?.list;
274
+ const plugins = list ? await list({ cwd: input.cwd, accountId: account.id, projectId: input.projectId ?? null }) : null;
275
+ const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
276
+ return DEFAULT_TOOL_NAMES.filter((name) => {
277
+ const plugin = HARNESS_PLUGINS.find((entry) => entry.tools?.some((tool) => tool.name === name));
278
+ return !on || !plugin || on.has(contributionId(plugin.id, 'tool', name));
279
+ });
280
+ }
184
281
  /** Whether a world tool's plugin is switched on for a session's folder, account and project. */
185
282
  async worldOn(live) {
186
283
  const list = this.options.pluginControl?.list;
@@ -203,6 +300,17 @@ export class Terminals {
203
300
  const { Type } = await import('typebox');
204
301
  return { tools: (await this.tools(session, true)).map((tool) => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) };
205
302
  }
303
+ case 'firstParty.list': {
304
+ // The harness's own extensions switched on for this session, with their settings.
305
+ const live = this.sessions.get(session.sessionId);
306
+ const list = this.options.pluginControl?.list;
307
+ if (!live || !list)
308
+ return { plugins: [] };
309
+ const plugins = await list({ cwd: live.input.cwd, accountId: live.account.id, projectId: live.input.projectId ?? null });
310
+ const on = new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id)));
311
+ const settings = attachments.load(this.options.env.root).settings;
312
+ return { plugins: FIRST_PARTY_PLUGINS.filter((plugin) => plugin.code?.some((code) => on.has(`${plugin.id}/code/${code.name}`))).map((plugin) => ({ id: plugin.id, settings: settings[plugin.id] ?? {} })) };
313
+ }
206
314
  case 'tools.call': {
207
315
  const tool = (await this.tools(session, false)).find((candidate) => candidate.name === params.name);
208
316
  if (!tool)
@@ -451,7 +559,45 @@ export class Terminals {
451
559
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
452
560
  }
453
561
  }
454
- /** `/plugin:notes` and `/plugin:annotate`: the page notes a person keeps, and the visible browser they capture them in. */
562
+ /** `/usage:analysis` and `/usage:limit`: usage for this session's own account, project or session. */
563
+ async usageCall(live, call, params) {
564
+ const usage = this.options.usage;
565
+ const limits = this.options.usageLimits;
566
+ if (!usage || !limits)
567
+ throw invalid('Usage is unavailable in this host.');
568
+ if (call === 'terminal.usage.summary') {
569
+ const scope = params.scope === 'session' || params.scope === 'project' || params.scope === 'all' ? params.scope : 'account';
570
+ if (scope === 'project' && !live.input.projectId)
571
+ throw invalid('This session has no project to report usage for.');
572
+ const period = params.period === 'today' || params.period === '30d' || params.period === 'all' ? params.period : '7d';
573
+ return usage.summary({
574
+ scope,
575
+ sessionId: live.input.sessionId,
576
+ accountId: live.account.id,
577
+ projectId: live.input.projectId ?? null,
578
+ period,
579
+ timezoneOffsetMinutes: typeof params.timezoneOffsetMinutes === 'number' ? params.timezoneOffsetMinutes : 0
580
+ });
581
+ }
582
+ if (call === 'terminal.usage.limits') {
583
+ return params.refresh === true ? limits.refresh(live.account.id, { force: true }) : limits.read(live.account.id);
584
+ }
585
+ if (call === 'terminal.usage.headers') {
586
+ if (typeof params.provider !== 'string' || !params.provider)
587
+ throw invalid('A provider is required.');
588
+ if (typeof params.status !== 'number' || !Number.isFinite(params.status))
589
+ throw invalid('A response status is required.');
590
+ const headers = params.headers && typeof params.headers === 'object' && !Array.isArray(params.headers)
591
+ ? Object.fromEntries(Object.entries(params.headers).filter((entry) => typeof entry[1] === 'string'))
592
+ : {};
593
+ if (!Object.keys(headers).length)
594
+ return {};
595
+ limits.recordHeaders(live.account.id, params.provider, { status: params.status, headers });
596
+ return {};
597
+ }
598
+ throw invalid('Unknown usage operation.');
599
+ }
600
+ /** `/annotate:notes` and `/annotate:`: Web annotations and their visible capture browser. */
455
601
  async annotationsCall(live, call, params) {
456
602
  const root = this.options.env.root;
457
603
  const sessionId = live.input.sessionId;
@@ -578,6 +724,7 @@ export class Terminals {
578
724
  sessionId: input.sessionId,
579
725
  nativeSessionId: live.controller.nativeSessionId,
580
726
  workingDirectory: input.cwd,
727
+ workspaceRoot: resolveWorkspaceRoot(input.cwd),
581
728
  worktreeId: input.run?.worktreeId ?? null,
582
729
  goal: text,
583
730
  attachmentCount: 0,
@@ -586,11 +733,13 @@ export class Terminals {
586
733
  status: 'queued',
587
734
  exitCode: null,
588
735
  handoffId: null,
589
- accountId: live.account.id
736
+ accountId: live.account.id,
737
+ variantScope: live.variant?.scope ?? null,
738
+ variantVersion: live.variant?.fingerprint ?? null
590
739
  };
591
740
  }
592
741
  /** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
593
- begin(live, reservedRunId, text) {
742
+ begin(live, reservedRunId, text, launch = {}) {
594
743
  const { root } = this.options.env;
595
744
  const { sessionId } = live.input;
596
745
  if (this.options.runs.activeForSession(sessionId))
@@ -606,63 +755,174 @@ export class Terminals {
606
755
  saveRun(root, this.runRecord(live, runId, text || null));
607
756
  copyToRun(root, live.input.sessionFolder, runId);
608
757
  }
758
+ if (launch.model || launch.effort) {
759
+ updateRun(root, runId, (record) => {
760
+ if (launch.model)
761
+ record.model = launch.model;
762
+ if (launch.effort)
763
+ record.effort = launch.effort;
764
+ });
765
+ }
609
766
  return this.options.runs.start(sessionId, runId);
610
767
  }
611
768
  async decide(live, run, tool, args) {
612
769
  const record = loadRun(this.options.env.root, run.runId);
613
770
  if (!isActiveStatus(record.status))
614
771
  throw invalid(`The run is no longer active (its status is ${record.status}).`);
615
- const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args };
772
+ const name = tool.toLowerCase();
773
+ const workspaceRoot = record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
774
+ if (live.input.readOnly && ['bash', 'edit', 'write'].includes(name))
775
+ return { allowed: false, reason: 'This session is read-only.', workspaceRoot };
776
+ let inspection;
777
+ try {
778
+ inspection = inspectRunWorkspaceWrite(record, tool, args);
779
+ }
780
+ catch (error) {
781
+ return { allowed: false, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.', workspaceRoot };
782
+ }
783
+ const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args: inspection.input };
616
784
  const { policy } = this.options;
617
785
  if (policy.scope) {
618
- const scope = await policy.scope(call);
786
+ const scope = combineWorkspaceScope(await policy.scope(call), inspection);
619
787
  if (!scope.allowed && (scope.automatic || !scope.requiresApproval))
620
- return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.' };
788
+ return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.', workspaceRoot };
621
789
  try {
622
790
  await policy.authorize?.(call);
623
791
  }
624
792
  catch (error) {
625
- return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.' };
793
+ return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.', workspaceRoot };
626
794
  }
627
795
  const requiresApproval = !!scope.requiresApproval || (scope.allowed && !!scope.humanOnly);
628
796
  return {
629
797
  allowed: scope.allowed,
630
798
  requiresApproval,
631
799
  reason: scope.reason ?? null,
800
+ workspaceRoot,
632
801
  rememberable: scope.allowed && !scope.humanOnly,
633
802
  ...(scope.humanOnly ? { standing: scope.allowGlobalApproval ? 'switch' : 'none' } : {})
634
803
  };
635
804
  }
636
- // With no application policy: reads run, a read-only session refuses the rest, and anything else asks.
637
- const name = tool.toLowerCase();
638
- if (name === SESSION_NAME || name === CHECKLIST_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name))
639
- return { allowed: true };
640
- if (live.input.readOnly)
641
- return { allowed: false, reason: 'This session is read-only.' };
642
- return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.' };
805
+ if (inspection.outside)
806
+ return { allowed: false, requiresApproval: true, rememberable: false, standing: 'switch', reason: inspection.reason, workspaceRoot };
807
+ if (name === SESSION_NAME || name === TODO_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name) || ['bash', 'edit', 'write'].includes(name)) {
808
+ return { allowed: true, workspaceRoot };
809
+ }
810
+ return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.', workspaceRoot };
811
+ }
812
+ async workspaceEscape(live, run, tool, args, reason) {
813
+ if (tool !== 'bash' || live.input.readOnly)
814
+ return false;
815
+ const record = loadRun(this.options.env.root, run.runId);
816
+ if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
817
+ return false;
818
+ const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args };
819
+ const scope = (await this.options.policy.scope?.(call)) ?? { allowed: true };
820
+ if (scope.automatic || (!scope.allowed && !scope.requiresApproval))
821
+ return false;
822
+ try {
823
+ await this.options.policy.authorize?.(call);
824
+ }
825
+ catch {
826
+ return false;
827
+ }
828
+ const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
829
+ const command = typeof operationArgs.command === 'string' ? operationArgs.command : '';
830
+ if (operationArgs.workspaceWriteScope !== 'outside' || !command)
831
+ return false;
832
+ const workspaceRoot = record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
833
+ const decision = await run.requestApproval({
834
+ target: 'workspace-write',
835
+ action: `${reason} Workspace: ${workspaceRoot}. This exact command will run once with broader filesystem access.\n\nCommand:\n${formatBashCommandForApproval(command)}`,
836
+ operation: { tool, args: operationArgs },
837
+ rememberable: false,
838
+ standing: 'none'
839
+ });
840
+ if (decision !== 'approved')
841
+ return false;
842
+ const currentRecord = loadRun(this.options.env.root, run.runId);
843
+ if (!isActiveStatus(currentRecord.status) || (currentRecord.trigger != null && currentRecord.trigger !== 'user'))
844
+ return false;
845
+ const currentCall = { run: currentRecord, caller: { ...live.caller, runId: run.runId }, tool, args: operationArgs };
846
+ const currentScope = (await this.options.policy.scope?.(currentCall)) ?? { allowed: true };
847
+ if (currentScope.automatic || (!currentScope.allowed && !currentScope.requiresApproval))
848
+ return false;
849
+ try {
850
+ await this.options.policy.authorize?.(currentCall);
851
+ return true;
852
+ }
853
+ catch {
854
+ return false;
855
+ }
856
+ }
857
+ async workspaceBoundaryBypass(live, run, tool, args) {
858
+ if (tool !== 'bash' || live.input.readOnly || !this.options.approvals.isAuto('pi'))
859
+ return false;
860
+ const record = loadRun(this.options.env.root, run.runId);
861
+ if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
862
+ return false;
863
+ const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
864
+ if (typeof operationArgs.command !== 'string' || !operationArgs.command)
865
+ return false;
866
+ const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args: operationArgs };
867
+ try {
868
+ const scope = (await this.options.policy.scope?.(call)) ?? { allowed: true };
869
+ if (scope.automatic || (!scope.allowed && !scope.requiresApproval) || (scope.humanOnly && !scope.allowGlobalApproval))
870
+ return false;
871
+ await this.options.policy.authorize?.(call);
872
+ return true;
873
+ }
874
+ catch {
875
+ return false;
876
+ }
643
877
  }
644
878
  async subagentsCall(live, call, params) {
645
879
  if (call === 'terminal.subagents.monitor')
646
880
  return { children: live.controller.subagentSnapshot() };
647
881
  const service = this.options.subagents;
648
- if (!service || live.input.subagents === null) {
882
+ if ((!service || live.input.subagents === null) && call !== 'terminal.subagents.register') {
649
883
  if (call === 'terminal.subagents.tools')
650
884
  return { tools: [] };
651
885
  throw invalid('Subagents are disabled for this terminal.');
652
886
  }
653
- if (call === 'terminal.subagents.tools')
654
- return { tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) };
887
+ if (call === 'terminal.subagents.tools') {
888
+ const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
889
+ return {
890
+ tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })),
891
+ profiles: {
892
+ normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
893
+ plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true).body
894
+ }
895
+ };
896
+ }
655
897
  const run = live.controller.activeRun;
656
898
  if (!run)
657
899
  throw new HarnessError('run_not_active', 'This terminal has no active parent run.');
900
+ if (call === 'terminal.subagents.planSafe') {
901
+ const reason = service.planSpawnRefusal(run.runId, params.input);
902
+ return { allowed: reason === null, ...(reason ? { reason } : {}) };
903
+ }
658
904
  if (call === 'terminal.subagents.register') {
659
905
  if (!Array.isArray(params.tools) || params.tools.some((name) => typeof name !== 'string'))
660
906
  throw invalid('Native tool names are required.');
907
+ if (params.plan !== undefined && typeof params.plan !== 'boolean')
908
+ throw invalid('plan must be a boolean.');
661
909
  const readOnly = live.input.readOnly === true;
662
- const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, CHECKLIST_WRITE, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
663
- const tools = params.tools.filter((name) => !readOnly || readTools.has(name));
664
- const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(live.caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
665
- freezeRun(this.options.env.root, run.runId, live.caller, capabilities);
910
+ const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, TODO_WRITE, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
911
+ const tools = [...new Set(params.tools)].filter(name => (!readOnly || readTools.has(name)) && (params.plan === true || (name !== 'ask_user' && name !== 'ask_user_question')));
912
+ const caller = { ...live.caller, runId: run.runId };
913
+ const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
914
+ const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
915
+ const subagentProfiles = {
916
+ normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
917
+ plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true, {
918
+ frozenTools: capabilities.tools,
919
+ contributions: capabilities.contributions,
920
+ descriptors: HARNESS_PLUGINS
921
+ }).body
922
+ };
923
+ freezeRun(this.options.env.root, run.runId, caller, capabilities);
924
+ if (!service || live.input.subagents === null)
925
+ return { runId: run.runId, connectors: capabilities.connectors, subagentProfiles };
666
926
  service.register({
667
927
  run: { id: run.runId, sessionId: live.input.sessionId, mode: live.input.run?.mode ?? 'code', projectId: live.input.projectId, worktreeId: live.input.run?.worktreeId },
668
928
  cwd: live.input.cwd, sessionDir: this.historyDir(live.input), text: '', caller: live.caller,
@@ -670,11 +930,15 @@ export class Terminals {
670
930
  effort: typeof params.effort === 'string' ? params.effort : null,
671
931
  launch: live.input.launch, tools: { allow: capabilities.tools, gated: capabilities.tools.filter(name => !readTools.has(name)) },
672
932
  connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
673
- variant: this.variantDirs.has(live.input.sessionId) ? { dir: this.variantDirs.get(live.input.sessionId), scope: live.input.variant && 'scope' in live.input.variant ? live.input.variant.scope ?? null : null } : null,
933
+ plan: params.plan === true,
934
+ variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
674
935
  capabilities
675
936
  }, live.account.id, capabilities);
676
- service.restore(run.runId, params.history ?? { owners: [], receipts: [] });
677
- return { runId: run.runId };
937
+ const history = params.history ?? { owners: [], receipts: [] };
938
+ service.restore(run.runId, history);
939
+ // Children an earlier reply left stopped, reported before the model answers.
940
+ const notice = await service.delivery(run.runId, history, false);
941
+ return { runId: run.runId, connectors: capabilities.connectors, subagentProfiles, notice };
678
942
  }
679
943
  if (call === 'terminal.subagents.verify') {
680
944
  await service.verify(run.runId, params.history);
@@ -834,16 +1098,20 @@ export class Terminals {
834
1098
  if (variant)
835
1099
  this.variantDirs.set(sessionId, variant.dir);
836
1100
  const worldTools = await this.worldToolNames(input, preparedAccount);
837
- const launchTools = [...new Set([...(input.launch?.tools ?? []), CHECKLIST_WRITE, SESSION_NAME, ...worldTools])];
1101
+ const ownTools = await this.ownToolNames(input, preparedAccount);
1102
+ const launchTools = [...new Set([...(input.launch?.tools ?? []), TODO_WRITE, SESSION_NAME, ...ownTools, ...(broker ? apps.toolNames : []), ...worldTools])];
838
1103
  // The profiles replace the variant's own subagent list, which they include.
839
1104
  const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
840
1105
  const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
841
- const pack = compile(env, caller, { ...input.launch, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
1106
+ // A terminal is a person's own session: the same standard sections a host's session has.
1107
+ const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
1108
+ const pack = compile(env, caller, { ...input.launch, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
842
1109
  record(root, pack);
843
1110
  mkdirSync(this.historyDir(input), { recursive: true });
844
1111
  saveForSession(input.sessionFolder, pack);
845
1112
  const live = {
846
1113
  input,
1114
+ variant,
847
1115
  account: preparedAccount,
848
1116
  broker,
849
1117
  caller,
@@ -851,8 +1119,15 @@ export class Terminals {
851
1119
  controller: new TerminalController({
852
1120
  sessionId,
853
1121
  nativeSessionId: input.nativeSessionId ?? null,
854
- begin: ({ reservedRunId, text }) => this.begin(live, reservedRunId, text),
1122
+ begin: ({ reservedRunId, text, model, effort }) => this.begin(live, reservedRunId, text, { model, effort }),
1123
+ hydrate: (owners) => this.hydrateMonitor(live, owners),
855
1124
  policy: (run, tool, args) => this.decide(live, run, tool, args),
1125
+ workspaceRoot: run => {
1126
+ const record = loadRun(this.options.env.root, run.runId);
1127
+ return record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
1128
+ },
1129
+ workspaceEscape: (run, tool, args, reason) => this.workspaceEscape(live, run, tool, args, reason),
1130
+ workspaceBoundaryBypass: (run, tool, args) => this.workspaceBoundaryBypass(live, run, tool, args),
856
1131
  allowToolGlobally: target => { this.options.approvals.allowTool('pi', target); },
857
1132
  native: (nativeSessionId) => bus.emit({ type: 'terminal.native', sessionId, nativeSessionId }),
858
1133
  telemetry: (event) => bus.emit({ type: 'terminal.telemetry', sessionId, event }),
@@ -963,6 +1238,8 @@ export class Terminals {
963
1238
  return this.componentsCall(live, call, params);
964
1239
  if (call.startsWith('terminal.trajectory.'))
965
1240
  return this.trajectoryCall(live, call, params);
1241
+ if (call === 'terminal.todo')
1242
+ return latestTerminalTodo(this.options.env.root, live.input.sessionId, live.controller.nativeSessionId);
966
1243
  if (call === 'terminal.permissions.get') {
967
1244
  const policy = this.options.approvals.read();
968
1245
  return { full: this.options.approvals.isAuto('pi'), globalTools: policy.globalTools.pi ?? [] };
@@ -980,11 +1257,15 @@ export class Terminals {
980
1257
  : this.options.approvals.revokeTool('pi', params.tool);
981
1258
  return { globalTools: policy.globalTools.pi ?? [] };
982
1259
  }
1260
+ if (call.startsWith('terminal.usage.'))
1261
+ return this.usageCall(live, call, params);
983
1262
  if (call !== 'terminal.resources')
984
1263
  return live.controller.control(call, params);
985
1264
  const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
986
- if (resourceIdentity(current) !== resourceIdentity(loading))
1265
+ const currentVariant = variant ? materialise(root, { dir: variant.dir, scope: variant.scope }, { trust: this.options.trust ?? null }) : null;
1266
+ if (resourceIdentity(current) !== resourceIdentity(loading) || (variant && (currentVariant?.fingerprint !== variant.fingerprint || JSON.stringify(currentVariant.extensionPaths) !== JSON.stringify(variant.extensionPaths)))) {
987
1267
  throw new HarnessError('terminal_resources_changed', 'Trusted resources changed since this terminal was prepared. Review /plugins and reopen the terminal to load the new selection.');
1268
+ }
988
1269
  return {};
989
1270
  } }
990
1271
  });
@@ -993,16 +1274,17 @@ export class Terminals {
993
1274
  const args = [...command.argsPrefix, '--append-system-prompt', render(env, pack)];
994
1275
  if (input.model)
995
1276
  args.push('--model', input.model);
996
- if (loading) {
1277
+ {
997
1278
  // Pi's own discovery is off: the terminal loads what the harness found and nothing else.
998
1279
  args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes');
999
- for (const path of [...loading.extensionPaths, ...loading.builtins, ...(variant?.extensionPaths ?? [])])
1280
+ // The pi command's own extensions are not loaded: a host's session cannot have them (PROP-0039).
1281
+ for (const path of [...(loading?.extensionPaths ?? []), ...(variant?.extensionPaths ?? [])])
1000
1282
  args.push('--extension', path);
1001
- for (const path of [...(variant?.skillPaths ?? []), ...loading.skillPaths])
1283
+ for (const path of [...(variant?.skillPaths ?? []), ...(loading?.skillPaths ?? [])])
1002
1284
  args.push('--skill', path);
1003
- for (const path of loading.promptPaths)
1285
+ for (const path of loading?.promptPaths ?? [])
1004
1286
  args.push('--prompt-template', path);
1005
- for (const path of loading.themePaths)
1287
+ for (const path of loading?.themePaths ?? [])
1006
1288
  args.push('--theme', path);
1007
1289
  }
1008
1290
  // Last, so its gate sees a tool call as every other extension left it.
@@ -1,4 +1,4 @@
1
- import type { RunEvent, RunRecord } from "../sessions/contracts.js";
1
+ import type { RunEvent, RunRecord, TodoItem } from "../sessions/contracts.js";
2
2
  export type TerminalTrajectoryRun = Pick<RunRecord, 'id' | 'mode' | 'goal' | 'startedAt' | 'endedAt' | 'status'>;
3
3
  export interface TerminalTrajectorySnapshot {
4
4
  run: TerminalTrajectoryRun;
@@ -6,5 +6,10 @@ export interface TerminalTrajectorySnapshot {
6
6
  }
7
7
  /** The native conversation file is the terminal's boundary for this read-only view. */
8
8
  export declare function listTerminalTrajectoryRuns(root: string, sessionId: string, nativeSessionId: string | null): TerminalTrajectoryRun[];
9
+ /** The todo of the latest reply in this native conversation, for a host's /todo view. */
10
+ export declare function latestTerminalTodo(root: string, sessionId: string, nativeSessionId: string | null): {
11
+ runId: string;
12
+ todo: TodoItem[];
13
+ };
9
14
  /** Read a run only when it belongs to both this terminal and its current native conversation. */
10
15
  export declare function readTerminalTrajectory(root: string, sessionId: string, nativeSessionId: string | null, runId: string, afterSequence?: number): TerminalTrajectorySnapshot;
@@ -1,5 +1,6 @@
1
1
  import { notFound } from "../core/errors.js";
2
2
  import { findRun, listRuns, readEvents } from "../sessions/runStore.js";
3
+ import { todoOf } from "../tools/todo.js";
3
4
  function summary(run) {
4
5
  return {
5
6
  id: run.id,
@@ -18,6 +19,13 @@ export function listTerminalTrajectoryRuns(root, sessionId, nativeSessionId) {
18
19
  .filter(run => run.nativeSessionId === nativeSessionId || run.trigger === 'side-question' && run.sessionId === sessionId)
19
20
  .map(summary);
20
21
  }
22
+ /** The todo of the latest reply in this native conversation, for a host's /todo view. */
23
+ export function latestTerminalTodo(root, sessionId, nativeSessionId) {
24
+ if (!nativeSessionId)
25
+ return { runId: '', todo: [] };
26
+ const run = listRuns(root, { sessionId }).find(candidate => candidate.nativeSessionId === nativeSessionId && candidate.trigger !== 'side-question');
27
+ return run ? { runId: run.id, todo: todoOf(readEvents(root, run.id)) } : { runId: '', todo: [] };
28
+ }
21
29
  /** Read a run only when it belongs to both this terminal and its current native conversation. */
22
30
  export function readTerminalTrajectory(root, sessionId, nativeSessionId, runId, afterSequence) {
23
31
  if (!nativeSessionId || !Number.isSafeInteger(afterSequence ?? 0) || (afterSequence ?? 0) < 0)
@@ -1,3 +1,3 @@
1
1
  import type { AnnotateStart } from "./pages/annotatePage.js";
2
- /** `/plugin:annotate` accepts a page address, DevTools target, or page-bridge target. */
2
+ /** `/annotate:annotate` accepts a page address, DevTools target, or page-bridge target. */
3
3
  export declare function parseAnnotate(argument: string): AnnotateStart | string;
@@ -1,4 +1,4 @@
1
- /** `/plugin:annotate` accepts a page address, DevTools target, or page-bridge target. */
1
+ /** `/annotate:annotate` accepts a page address, DevTools target, or page-bridge target. */
2
2
  export function parseAnnotate(argument) {
3
3
  const words = argument.trim().split(/\s+/).filter(Boolean);
4
4
  if (!words.length)
@@ -6,7 +6,7 @@ export function parseAnnotate(argument) {
6
6
  if (words[0] === '--app') {
7
7
  const endpoint = words[1];
8
8
  if (!endpoint)
9
- return 'Give the app\'s DevTools address: /plugin:annotate --app http://127.0.0.1:9229';
9
+ return 'Give the app\'s DevTools address: /annotate:annotate --app http://127.0.0.1:9229';
10
10
  const address = /^\d+$/.test(endpoint) ? `http://127.0.0.1:${endpoint}` : endpoint;
11
11
  const name = words.slice(2).join(' ');
12
12
  return { kind: 'cdp', endpoint: address, ...(name ? { name } : {}) };
@@ -16,7 +16,7 @@ export function parseAnnotate(argument) {
16
16
  return { kind: 'bridge', ...(name ? { name } : {}) };
17
17
  }
18
18
  if (words.length > 1 || words[0].startsWith('--'))
19
- return 'Use /plugin:annotate [url], /plugin:annotate --app <DevTools address>, or /plugin:annotate --bridge.';
19
+ return 'Use /annotate:annotate [url], /annotate:annotate --app <DevTools address>, or /annotate:annotate --bridge.';
20
20
  const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(words[0]) ? words[0] : /^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(words[0]) ? `http://${words[0]}` : `https://${words[0]}`;
21
21
  return { kind: 'web', url };
22
22
  }