@xingular/harness 1.1.1 → 1.3.1-unstable.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +36 -28
  3. package/dist/cli.js +10837 -6159
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +18 -4
  11. package/dist/sdk/agents/planResearch.d.ts +27 -0
  12. package/dist/sdk/agents/planResearch.js +82 -0
  13. package/dist/sdk/agents/profiles.d.ts +5 -1
  14. package/dist/sdk/agents/profiles.js +21 -5
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/cli.js +5 -4
  27. package/dist/sdk/commands/list.d.ts +3 -0
  28. package/dist/sdk/commands/list.js +75 -0
  29. package/dist/sdk/commands/registry.d.ts +258 -0
  30. package/dist/sdk/commands/registry.js +48 -0
  31. package/dist/sdk/commands/routes.d.ts +16 -0
  32. package/dist/sdk/commands/routes.js +33 -0
  33. package/dist/sdk/core/commandBudget.d.ts +23 -0
  34. package/dist/sdk/core/commandBudget.js +57 -0
  35. package/dist/sdk/core/index.js +17 -1
  36. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  37. package/dist/sdk/core/interactiveProfile.js +143 -0
  38. package/dist/sdk/core/shell.d.ts +20 -0
  39. package/dist/sdk/core/shell.js +73 -0
  40. package/dist/sdk/core/workspace.d.ts +13 -0
  41. package/dist/sdk/core/workspace.js +88 -0
  42. package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
  43. package/dist/sdk/core/workspaceBashTool.js +50 -0
  44. package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
  45. package/dist/sdk/core/workspaceSandbox.js +360 -0
  46. package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
  47. package/dist/sdk/core/workspaceWriteOperations.js +224 -0
  48. package/dist/sdk/doctor/checks.js +71 -12
  49. package/dist/sdk/doctor/types.d.ts +2 -2
  50. package/dist/sdk/harness.d.ts +7 -0
  51. package/dist/sdk/harness.js +42 -2
  52. package/dist/sdk/hub/hub.js +5 -3
  53. package/dist/sdk/index.d.ts +8 -3
  54. package/dist/sdk/index.js +4 -1
  55. package/dist/sdk/pi/runtime.d.ts +2 -1
  56. package/dist/sdk/pi/runtime.js +2 -2
  57. package/dist/sdk/plugins/builtin.js +32 -15
  58. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  59. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  60. package/dist/sdk/plugins/catalogue.js +2 -1
  61. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  62. package/dist/sdk/plugins/commandPrefix.js +21 -0
  63. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  64. package/dist/sdk/plugins/firstParty.js +8 -2
  65. package/dist/sdk/plugins/install.d.ts +6 -2
  66. package/dist/sdk/plugins/install.js +29 -5
  67. package/dist/sdk/plugins/inventory.js +6 -5
  68. package/dist/sdk/plugins/manifest.d.ts +2 -0
  69. package/dist/sdk/plugins/manifest.js +2 -1
  70. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  71. package/dist/sdk/plugins/surfaces.js +11 -0
  72. package/dist/sdk/plugins/types.d.ts +15 -0
  73. package/dist/sdk/prompt/compile.d.ts +14 -0
  74. package/dist/sdk/prompt/compile.js +65 -31
  75. package/dist/sdk/prompt/standard.d.ts +17 -0
  76. package/dist/sdk/prompt/standard.js +21 -0
  77. package/dist/sdk/server/methods.js +22 -2
  78. package/dist/sdk/server/remote.js +36 -11
  79. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  80. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  81. package/dist/sdk/sessions/continuation.d.ts +7 -2
  82. package/dist/sdk/sessions/continuation.js +11 -6
  83. package/dist/sdk/sessions/contracts.d.ts +190 -4
  84. package/dist/sdk/sessions/contracts.js +71 -8
  85. package/dist/sdk/sessions/control.d.ts +72 -0
  86. package/dist/sdk/sessions/control.js +158 -0
  87. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  88. package/dist/sdk/sessions/initialBash.js +7 -0
  89. package/dist/sdk/sessions/planMode.d.ts +14 -3
  90. package/dist/sdk/sessions/planMode.js +74 -17
  91. package/dist/sdk/sessions/registry.d.ts +1 -1
  92. package/dist/sdk/sessions/registry.js +2 -2
  93. package/dist/sdk/sessions/runStore.d.ts +14 -0
  94. package/dist/sdk/sessions/runStore.js +38 -1
  95. package/dist/sdk/sessions/runner.d.ts +40 -2
  96. package/dist/sdk/sessions/runner.js +161 -13
  97. package/dist/sdk/sessions/sessions.d.ts +23 -3
  98. package/dist/sdk/sessions/sessions.js +225 -22
  99. package/dist/sdk/sessions/subagentService.d.ts +24 -0
  100. package/dist/sdk/sessions/subagentService.js +225 -26
  101. package/dist/sdk/sessions/subagents.d.ts +10 -1
  102. package/dist/sdk/sessions/subagents.js +4 -0
  103. package/dist/sdk/sessions/workSlice.d.ts +106 -0
  104. package/dist/sdk/sessions/workSlice.js +329 -0
  105. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  106. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  107. package/dist/sdk/terminal/browserControl.js +2 -1
  108. package/dist/sdk/terminal/controller.d.ts +31 -3
  109. package/dist/sdk/terminal/controller.js +81 -29
  110. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  111. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  112. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  113. package/dist/sdk/terminal/opentui/model.js +482 -0
  114. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  115. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  116. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  117. package/dist/sdk/terminal/opentui/theme.js +88 -0
  118. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  119. package/dist/sdk/terminal/piExtension.js +177 -63
  120. package/dist/sdk/terminal/standalone.d.ts +7 -0
  121. package/dist/sdk/terminal/standalone.js +72 -13
  122. package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
  123. package/dist/sdk/terminal/subagentMonitor.js +61 -9
  124. package/dist/sdk/terminal/terminals.d.ts +26 -2
  125. package/dist/sdk/terminal/terminals.js +325 -43
  126. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  127. package/dist/sdk/terminal/trajectory.js +8 -0
  128. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  129. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  130. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  131. package/dist/sdk/terminal/tui/chrome.js +13 -0
  132. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  133. package/dist/sdk/terminal/tui/commandPages.js +17 -4
  134. package/dist/sdk/terminal/tui/commands.d.ts +6 -0
  135. package/dist/sdk/terminal/tui/commands.js +213 -124
  136. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  137. package/dist/sdk/terminal/tui/list.js +96 -14
  138. package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
  139. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  140. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  141. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  142. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  143. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  144. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  145. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
  146. package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
  147. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  148. package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
  149. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  150. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  151. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  152. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  153. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  154. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  155. package/dist/sdk/terminal/tui/runtime.js +23 -3
  156. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  157. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  158. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  159. package/dist/sdk/tools/checkpoint.js +154 -0
  160. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  161. package/dist/sdk/tools/getCapabilities.js +34 -0
  162. package/dist/sdk/tools/sessionName.d.ts +2 -0
  163. package/dist/sdk/tools/sessionName.js +5 -3
  164. package/dist/sdk/tools/spawnSubagent.js +11 -8
  165. package/dist/sdk/tools/todo.d.ts +15 -0
  166. package/dist/sdk/tools/todo.js +64 -0
  167. package/dist/sdk/usage/analytics.d.ts +20 -0
  168. package/dist/sdk/usage/analytics.js +151 -0
  169. package/dist/sdk/usage/ledger.d.ts +38 -0
  170. package/dist/sdk/usage/ledger.js +142 -0
  171. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  172. package/dist/sdk/usage/limits/adapters.js +349 -0
  173. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  174. package/dist/sdk/usage/limits/capture.js +47 -0
  175. package/dist/sdk/usage/limits/http.d.ts +12 -0
  176. package/dist/sdk/usage/limits/http.js +51 -0
  177. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  178. package/dist/sdk/usage/limits/normalize.js +211 -0
  179. package/dist/sdk/usage/limits/service.d.ts +30 -0
  180. package/dist/sdk/usage/limits/service.js +176 -0
  181. package/dist/sdk/usage/limits/types.d.ts +65 -0
  182. package/dist/sdk/usage/limits/types.js +1 -0
  183. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  184. package/dist/sdk/usage/serverMethods.js +22 -0
  185. package/dist/sdk/usage/service.d.ts +32 -0
  186. package/dist/sdk/usage/service.js +66 -0
  187. package/dist/sdk/usage/settings.d.ts +7 -0
  188. package/dist/sdk/usage/settings.js +28 -0
  189. package/dist/sdk/usage/types.d.ts +60 -0
  190. package/dist/sdk/usage/types.js +4 -0
  191. package/dist/terminal-opentui.js +9204 -4812
  192. package/dist/terminal-pi.js +1848 -272
  193. package/docs/architecture/00-overview.md +1 -1
  194. package/docs/architecture/01-extension-points.md +1 -1
  195. package/docs/architecture/02-sessions-and-runs.md +169 -36
  196. package/docs/architecture/04-prompt.md +55 -15
  197. package/docs/architecture/06-plugins-and-capabilities.md +36 -15
  198. package/docs/architecture/07-tools-and-gate.md +131 -25
  199. package/docs/architecture/08-memory-and-history.md +4 -2
  200. package/docs/architecture/09-accounts-and-connectors.md +39 -3
  201. package/docs/architecture/10-server-protocol.md +71 -9
  202. package/docs/architecture/11-terminal.md +148 -60
  203. package/docs/architecture/12-storage.md +30 -2
  204. package/docs/architecture/13-world-tools.md +22 -1
  205. package/docs/architecture/14-surfaces-and-parity.md +58 -8
  206. package/docs/architecture/README.md +20 -17
  207. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  208. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  209. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  210. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  211. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  212. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  213. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  214. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  215. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  216. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  217. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  218. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  219. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  220. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
  221. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  222. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  223. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  224. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  225. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  226. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  227. package/docs/architecture/proposals/README.md +24 -6
  228. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  229. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  230. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  231. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  232. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  233. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  234. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  235. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  236. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  237. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  238. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  239. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  240. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  241. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  242. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  243. package/docs/doctor.md +14 -1
  244. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  245. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  246. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  247. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  248. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  249. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  250. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  251. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  252. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  253. package/docs/parity.md +83 -27
  254. package/docs/releases/1.1.1.md +28 -0
  255. package/docs/releases/1.2.0.md +49 -0
  256. package/docs/releases/1.3.0.md +21 -0
  257. package/docs/releases/4.0.0.md +10 -9
  258. package/docs/releases/README.md +35 -30
  259. package/docs/study/00-index.md +3 -0
  260. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  261. package/docs/terminal-tui.md +162 -82
  262. package/package.json +4 -3
  263. package/dist/sdk/tools/checklist.d.ts +0 -9
  264. package/dist/sdk/tools/checklist.js +0 -54
@@ -1,6 +1,11 @@
1
- import type { AgentSession, ExtensionAPI } from '@earendil-works/pi-coding-agent';
1
+ import { type AgentSession, type ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  export interface TerminalPlanRunState {
3
3
  active: boolean;
4
4
  allowedTools: ReadonlySet<string>;
5
+ connectors?: string[];
6
+ subagentProfiles?: {
7
+ normal: string;
8
+ plan: string;
9
+ };
5
10
  }
6
11
  export default function (pi: ExtensionAPI, planRun?: TerminalPlanRunState, getSession?: () => AgentSession): Promise<void>;
@@ -1,13 +1,29 @@
1
+ // Loaded by the `pi` command in a terminal session (`--extension`). It reports what the native
2
+ // session does to the harness, asks before each tool call, takes instructions the harness
3
+ // queues for it, and gives the session the harness's own tools. It runs in the `pi` process,
4
+ // so everything it knows about the harness comes through the session socket.
5
+ import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from '@earendil-works/pi-coding-agent';
1
6
  import { Type } from 'typebox';
2
- import { QUESTION_SKETCH_MAX_LENGTH, QUESTION_SKETCH_PATTERN, questionSketchError } from "../sessions/contracts.js";
3
7
  import { connectionFromEnv, socketCall, terminalCall } from "./client.js";
4
8
  import { controlCall } from "./control.js";
5
- import { SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
9
+ import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
10
+ import { PLAN_RESEARCH_TOOLS } from "../agents/planResearch.js";
6
11
  import { askUserTool } from "../tools/askUser.js";
7
- import { checklistTool } from "../tools/checklist.js";
12
+ import { todoTool } from "../tools/todo.js";
13
+ import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
14
+ import { createSliceState, parseWorkSlice, workSliceGuard } from "../sessions/workSlice.js";
15
+ import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
8
16
  import { sessionNameTool } from "../tools/sessionName.js";
17
+ import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
9
18
  import { PLAN_INSTRUCTIONS, planRefusal } from "../sessions/planMode.js";
19
+ import { compactionCheckpoint } from "../sessions/compactionCheckpoint.js";
10
20
  import { pruneToolImages } from "../world/images.js";
21
+ import { parseInitialBashCommand } from "../sessions/initialBash.js";
22
+ import { inspectWorkspaceWrite } from "../sessions/workspaceBoundary.js";
23
+ import { createWorkspaceShellTool } from "../core/workspaceBashTool.js";
24
+ import { createWorkspaceBashOperations, runWithWorkspaceWriteScope } from "../core/workspaceSandbox.js";
25
+ import { withWorkspaceWriteAuthorization, workspaceEditOperations, workspaceWriteOperations } from "../core/workspaceWriteOperations.js";
26
+ import { BUDGET_GUARD, OUTPUT_TRIM, budgetGuard, outputTrim } from "../plugins/firstParty.js";
11
27
  /** How much text a tool handed back to the model. */
12
28
  function resultSize(result) {
13
29
  const content = result?.content;
@@ -32,6 +48,7 @@ export default async function (pi, planRun, getSession) {
32
48
  let context = null;
33
49
  let nativeSubagents = false;
34
50
  let subagentParent = false;
51
+ let stoppedChildren = null;
35
52
  let timer = null;
36
53
  let polling = false;
37
54
  let failure;
@@ -42,6 +59,8 @@ export default async function (pi, planRun, getSession) {
42
59
  let requestStartedAt = null;
43
60
  const toolStartedAt = new Map();
44
61
  const fileTools = new Map();
62
+ const writeAuthorizations = new Map();
63
+ let workspaceRoot = null;
45
64
  // The harness's own tools for this session (connected apps and what the application offers).
46
65
  let stage = 'Tool discovery';
47
66
  try {
@@ -62,8 +81,19 @@ export default async function (pi, planRun, getSession) {
62
81
  }
63
82
  });
64
83
  }
84
+ // The harness's own extensions a person switched on, as a structured session loads them inline.
85
+ const own = await socketCall(connection, 'firstParty.list', {}, 15_000)
86
+ .then((reply) => reply?.plugins ?? [], () => []);
87
+ for (const plugin of own) {
88
+ if (plugin.id === OUTPUT_TRIM)
89
+ outputTrim(plugin.settings)(pi);
90
+ if (plugin.id === BUDGET_GUARD)
91
+ budgetGuard(plugin.settings)(pi);
92
+ }
65
93
  const native = await controlCall('terminal.subagents.tools');
66
94
  nativeSubagents = native.tools.length > 0;
95
+ if (planRun && native.profiles)
96
+ planRun.subagentProfiles = native.profiles;
67
97
  for (const tool of native.tools)
68
98
  pi.registerTool({
69
99
  name: tool.name, label: tool.label, description: tool.description, parameters: Type.Unsafe(tool.parameters),
@@ -77,6 +107,46 @@ export default async function (pi, planRun, getSession) {
77
107
  await terminalCall('startupFailed', { reason: `${stage} failed: ${error instanceof Error ? error.message : 'Unavailable'}` }).catch(() => { });
78
108
  throw error;
79
109
  }
110
+ pi.registerTool({
111
+ ...CAPABILITY_DISCOVERY,
112
+ parameters: Type.Unsafe(CAPABILITY_DISCOVERY.parameters),
113
+ async execute(_id, input) {
114
+ const active = pi.getActiveTools();
115
+ const tools = pi.getAllTools();
116
+ const selected = planRun?.active ? [...planRun.allowedTools] : active;
117
+ const name = input && typeof input === 'object' && typeof input.tool === 'string'
118
+ ? input.tool
119
+ : undefined;
120
+ const text = JSON.stringify(describeCapabilities({
121
+ capabilities: { version: 1, tools: selected, connectors: planRun?.connectors ?? [], contributions: [] },
122
+ active,
123
+ registered: tools
124
+ }, name));
125
+ return { content: [{ type: 'text', text }], details: undefined };
126
+ }
127
+ });
128
+ const workspaceBash = createWorkspaceBashOperations(() => workspaceRoot, {
129
+ bypassWorkspaceBoundary: async (request) => {
130
+ const result = await controlCall('terminal.workspaceBoundaryBypass', { tool: 'bash', args: { command: request.command, cwd: request.cwd, workspaceRoot: request.workspaceRoot } });
131
+ return result.bypass;
132
+ },
133
+ requestWorkspaceWriteScope: async (request) => {
134
+ const result = await controlCall('terminal.workspaceEscape', { tool: 'bash', args: { command: request.command, workspaceWriteScope: 'outside' }, reason: request.reason });
135
+ return result.approved;
136
+ },
137
+ workspaceWriteScope: () => planRun?.active ? 'read-only' : undefined
138
+ });
139
+ pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
140
+ pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
141
+ pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
142
+ compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
143
+ // A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
144
+ // person decides whether to continue.
145
+ const slicePolicy = parseWorkSlice(undefined);
146
+ const sliceState = createSliceState(slicePolicy);
147
+ // `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
148
+ if (slicePolicy.enabled)
149
+ workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
80
150
  // Only the latest screenshots stay in what the model sees (world/images.ts).
81
151
  pi.on('context', (event) => {
82
152
  const messages = pruneToolImages(event.messages);
@@ -86,7 +156,8 @@ export default async function (pi, planRun, getSession) {
86
156
  context = ctx;
87
157
  if (timer)
88
158
  clearInterval(timer);
89
- await terminalCall('ready', { nativeSessionId: ctx.sessionManager.getSessionFile() });
159
+ // The branch's parent runs let the controller show the latest one's rows again on reopen.
160
+ await terminalCall('ready', { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
90
161
  timer = setInterval(() => {
91
162
  if (polling || !context)
92
163
  return;
@@ -119,10 +190,20 @@ export default async function (pi, planRun, getSession) {
119
190
  reasoningTokens = undefined;
120
191
  costReported = false;
121
192
  // A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
122
- const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' });
193
+ const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '', model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel?.() ?? null });
194
+ workspaceRoot = typeof begun.workspaceRoot === 'string' ? begun.workspaceRoot : null;
195
+ if (!workspaceRoot)
196
+ throw new Error('The active project workspace could not be resolved; the turn was not started.');
197
+ // Every turn marks its run on the branch, so a reopened conversation finds its latest one.
198
+ pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
199
+ stoppedChildren = null;
123
200
  if (nativeSubagents) {
124
- pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
125
- await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), history: subagentHistory(ctx.sessionManager.getBranch()) });
201
+ const snapshot = await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), plan: planRun?.active === true, history: subagentHistory(ctx.sessionManager.getBranch()) });
202
+ if (planRun && Array.isArray(snapshot.connectors))
203
+ planRun.connectors = snapshot.connectors;
204
+ if (planRun && snapshot.subagentProfiles)
205
+ planRun.subagentProfiles = snapshot.subagentProfiles;
206
+ stoppedChildren = snapshot.notice ?? null;
126
207
  subagentParent = true;
127
208
  }
128
209
  if (planRun && event.messageType === 'user') {
@@ -130,24 +211,31 @@ export default async function (pi, planRun, getSession) {
130
211
  event.systemPromptOptions.sections.plan = PLAN_INSTRUCTIONS;
131
212
  else
132
213
  delete event.systemPromptOptions.sections.plan;
214
+ if (planRun.subagentProfiles)
215
+ event.systemPromptOptions.sections.subagents = planRun.active ? planRun.subagentProfiles.plan : planRun.subagentProfiles.normal;
133
216
  }
134
217
  const bang = event.messageType === 'user' ? /^\s*!\s*([\s\S]+)$/u.exec(event.prompt) : null;
135
218
  if (!bang)
136
219
  return;
137
- const command = bang[1];
220
+ const initial = parseInitialBashCommand(bang[1]);
221
+ const command = initial.command;
222
+ const args = { command, ...(initial.workspaceWriteScope === 'outside' ? { workspaceWriteScope: 'outside' } : {}) };
138
223
  const id = `initial-bash:${begun.runId}`;
139
224
  const started = Date.now();
140
- await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify({ command }) });
225
+ await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify(args) });
141
226
  let refusal;
142
227
  let result;
143
- if (planRun?.active) {
228
+ if (!command) {
229
+ refusal = 'Enter a Bash command after !outside.';
230
+ }
231
+ else if (planRun?.active) {
144
232
  refusal = 'Bash commands are disabled while a plan request is running.';
145
233
  }
146
234
  else {
147
235
  try {
148
- const policy = await terminalCall('policy', { tool: 'bash', args: { command } });
236
+ const policy = await terminalCall('policy', { tool: 'bash', args });
149
237
  if (policy.requiresApproval) {
150
- const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args: { command } });
238
+ const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args });
151
239
  if (approval.decision !== 'approved')
152
240
  refusal = 'The user denied this Bash command.';
153
241
  }
@@ -159,7 +247,7 @@ export default async function (pi, planRun, getSession) {
159
247
  if (!session)
160
248
  refusal = 'The native session is unavailable.';
161
249
  else
162
- result = await session.executeBash(command, undefined, { id });
250
+ result = await runWithWorkspaceWriteScope(initial.workspaceWriteScope, () => withCommandBudget(() => session.executeBash(command, undefined, { id, operations: workspaceBash }), () => session.abortBash(), DEFAULT_COMMAND_TIMEOUT_SECONDS));
163
251
  }
164
252
  }
165
253
  catch (error) {
@@ -179,6 +267,13 @@ export default async function (pi, planRun, getSession) {
179
267
  : 'The user’s initial Bash command has already run. Read its native Bash result and explain what it shows. Do not run the same command again unless the user explicitly asks.';
180
268
  return { message: { customType: 'xingular-initial-bash', content: instruction, display: false, details: {} } };
181
269
  });
270
+ // A second handler, so the notice joins any initial-Bash message rather than replacing it.
271
+ pi.on('before_agent_start', async () => {
272
+ const notice = stoppedChildren;
273
+ stoppedChildren = null;
274
+ if (notice)
275
+ return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
276
+ });
182
277
  const deliverChildren = async (event, ctx, settle) => {
183
278
  if (!nativeSubagents || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
184
279
  return;
@@ -240,21 +335,61 @@ export default async function (pi, planRun, getSession) {
240
335
  pi.on('turn_start', async () => {
241
336
  requestStartedAt = Date.now();
242
337
  });
243
- pi.on('tool_call', async (event) => {
338
+ pi.on('tool_call', async (event, ctx) => {
244
339
  if (planRun?.active && !planRun.allowedTools.has(event.toolName)) {
245
- const reason = planRefusal(event.toolName, new Set()) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`;
340
+ const reason = planRefusal(event.toolName, new Set(), event.input) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`;
246
341
  return { block: true, reason };
247
342
  }
343
+ if (planRun?.active && event.toolName === SPAWN_SUBAGENT) {
344
+ try {
345
+ const eligibility = await controlCall('terminal.subagents.planSafe', { input: event.input });
346
+ if (!eligibility.allowed)
347
+ return { block: true, reason: eligibility.reason ?? 'This profile is not eligible for plan-mode research.' };
348
+ }
349
+ catch (error) {
350
+ return { block: true, reason: `Plan-mode research eligibility could not be verified: ${error instanceof Error ? error.message : 'controller unavailable'}` };
351
+ }
352
+ }
353
+ else if (planRun?.active) {
354
+ const reason = planRefusal(event.toolName, new Set(PLAN_RESEARCH_TOOLS), event.input);
355
+ if (reason)
356
+ return { block: true, reason };
357
+ }
358
+ let authorization;
359
+ if ((event.toolName === 'edit' || event.toolName === 'write') && typeof event.input.path === 'string') {
360
+ try {
361
+ if (!workspaceRoot)
362
+ throw new Error('The active project workspace is unavailable.');
363
+ const inspection = inspectWorkspaceWrite(workspaceRoot, ctx.cwd, event.toolName, event.input);
364
+ Object.assign(event.input, inspection.input);
365
+ if (!inspection.path)
366
+ throw new Error('The native file-write target could not be verified.');
367
+ authorization = {
368
+ workspaceRoot,
369
+ cwd: ctx.cwd,
370
+ path: inspection.path,
371
+ outside: inspection.outside,
372
+ authorized: false
373
+ };
374
+ }
375
+ catch (error) {
376
+ return { block: true, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.' };
377
+ }
378
+ }
248
379
  try {
249
380
  const policy = await controlCall('terminal.policy', { tool: event.toolName, args: event.input });
250
381
  if (policy.requiresApproval) {
251
382
  const approval = await controlCall('terminal.approval', { requestId: event.toolCallId, tool: event.toolName, args: event.input });
252
- if (approval.decision === 'approved')
253
- return;
254
- return { block: true, reason: 'The user denied this action. Ask how they would like to proceed.' };
383
+ if (approval.decision !== 'approved')
384
+ return { block: true, reason: 'The user denied this action. Ask how they would like to proceed.' };
255
385
  }
256
- if (!policy.allowed)
386
+ else if (!policy.allowed) {
257
387
  return { block: true, reason: policy.reason ?? 'Outside what this session may do.' };
388
+ }
389
+ if (authorization) {
390
+ authorization.authorized = true;
391
+ writeAuthorizations.set(event.toolCallId, authorization);
392
+ }
258
393
  }
259
394
  catch (error) {
260
395
  const detail = error instanceof Error ? error.message : String(error);
@@ -287,7 +422,10 @@ export default async function (pi, planRun, getSession) {
287
422
  }
288
423
  }
289
424
  subagentParent = false;
290
- await terminalCall('settled', { error: failure, aborted: event.aborted });
425
+ // A reply that stopped at its work-slice boundary is not a completed task: the record says so,
426
+ // so a host can offer the continuation instead of treating it as done (PROP-0041).
427
+ const reason = sliceState.endedBy ? sliceState.endedBy === 'stuck' ? 'stalled' : 'slice_complete' : undefined;
428
+ await terminalCall('settled', { error: failure, aborted: event.aborted, ...(reason ? { reason } : {}) });
291
429
  });
292
430
  pi.on('user_bash', async () => {
293
431
  throw new Error('Use the ! command composer so this action can be checked.');
@@ -313,19 +451,32 @@ export default async function (pi, planRun, getSession) {
313
451
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
314
452
  }
315
453
  });
316
- // The checklist is recorded on the harness run; the shared tool validates and the socket carries the event.
454
+ // The todo list is recorded on the harness run; the shared tool validates and the socket carries the event.
317
455
  let recorded = Promise.resolve();
318
- const checklist = checklistTool({ emit: (_type, payload) => { recorded = terminalCall('checklist', payload); } });
456
+ const todo = todoTool({ emit: (_type, payload) => { recorded = terminalCall('todo', payload); } });
319
457
  pi.registerTool({
320
- name: checklist.name, label: checklist.label, description: checklist.description,
321
- parameters: Type.Unsafe(checklist.parameters(Type)),
458
+ name: todo.name, label: todo.label, description: todo.description,
459
+ parameters: Type.Unsafe(todo.parameters(Type)),
322
460
  async execute(_id, input) {
323
461
  recorded = Promise.resolve();
324
- const result = await checklist.execute(input);
462
+ const result = await todo.execute(input);
325
463
  await recorded;
326
464
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
327
465
  }
328
466
  });
467
+ // A bounded reply records its handoff here; the terminal reply is its own slice.
468
+ let checkpointRecorded = Promise.resolve();
469
+ const checkpoint = checkpointTool({ emit: (_type, payload) => { checkpointRecorded = terminalCall('checkpoint', payload); } }, 1);
470
+ pi.registerTool({
471
+ name: checkpoint.name, label: checkpoint.label, description: checkpoint.description,
472
+ parameters: Type.Unsafe(checkpoint.parameters(Type)),
473
+ async execute(_id, input) {
474
+ checkpointRecorded = Promise.resolve();
475
+ const result = await checkpoint.execute(input);
476
+ await checkpointRecorded;
477
+ return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
478
+ }
479
+ });
329
480
  const ask = askUserTool({ requestAnswers: questions => terminalCall('questions', { questions }, 1_230_000) });
330
481
  pi.registerTool({
331
482
  name: ask.name, label: ask.label, description: ask.description,
@@ -335,43 +486,6 @@ export default async function (pi, planRun, getSession) {
335
486
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
336
487
  }
337
488
  });
338
- pi.registerTool({
339
- name: 'ask_user_question',
340
- label: 'Ask a question',
341
- description: 'Ask the user a question and wait for their answer. Include a concise 1–2 word summary label; if omitted, the terminal derives one from the question. Every question must include at least one choice and at least one choice must be recommended. Recommendations are visual only and never preselect answers; submitted choices keep their labels. Mark at most one choice recommended for single-choice questions or several when multiSelect is true. When a UI diagram helps compare choices, set uiSketch: true and include a detailed, bounded ASCII wireframe for every choice. Use ASCII tables and panel borders to show the affected element alongside relevant neighboring UI, with readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes, not a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels.',
342
- parameters: Type.Object({
343
- summary: Type.Optional(Type.String({ description: 'A concise label of one or two words' })),
344
- question: Type.String(),
345
- uiSketch: Type.Optional(Type.Literal(true, { description: 'Enable an ASCII sketch preview; when true, every choice must include a sketch.' })),
346
- options: Type.Array(Type.Object({
347
- label: Type.String(),
348
- description: Type.Optional(Type.String()),
349
- recommended: Type.Optional(Type.Boolean()),
350
- sketch: Type.Optional(Type.String({ minLength: 1, maxLength: QUESTION_SKETCH_MAX_LENGTH, pattern: QUESTION_SKETCH_PATTERN, description: 'Literal ASCII UI wireframe: 1–24 LF-separated lines, at most 80 printable ASCII characters per line. Include the affected element and neighboring UI using structured table/panel borders with fixed, matching outer-edge columns on every row. Required for every choice when uiSketch is true.' }))
351
- }), {
352
- minItems: 1,
353
- contains: Type.Object({ recommended: Type.Literal(true) })
354
- }),
355
- multiSelect: Type.Optional(Type.Boolean())
356
- }),
357
- async execute(id, input) {
358
- if (!Array.isArray(input.options) || input.options.length === 0)
359
- throw new Error('Every question needs at least one choice.');
360
- const recommendedCount = input.options.filter(option => option.recommended).length;
361
- if (recommendedCount === 0)
362
- throw new Error('Every question needs at least one recommended choice.');
363
- if (!input.multiSelect && recommendedCount > 1)
364
- throw new Error('A single-choice question may recommend at most one option.');
365
- const sketchError = questionSketchError(input.uiSketch, input.options);
366
- if (sketchError)
367
- throw new Error(sketchError);
368
- const summary = typeof input.summary === 'string' ? input.summary.trim() : '';
369
- if (summary && (summary.length > 100 || summary.split(/\s+/u).filter(Boolean).length > 2)) {
370
- throw new Error('The question summary must be a concise 1–2 word label of no more than 100 characters.');
371
- }
372
- const answer = await terminalCall('question', { requestId: id, ...input, ...(summary ? { summary } : {}) });
373
- return { content: [{ type: 'text', text: JSON.stringify(answer) }], details: undefined };
374
- }
375
- });
489
+ // One question tool, as in every structured session: the terminal has no ask_user_question alias (PROP-0039).
376
490
  await terminalCall('startup', { phase: 'Pi hooks registered' });
377
491
  }
@@ -1,10 +1,14 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
+ import type { SecretStore } from "../accounts/secrets.js";
3
+ import { type SecretBackendDescriptor } from "../core/interactiveProfile.js";
2
4
  import { type Harness, type HarnessOptions } from "../harness.js";
3
5
  export interface StandalonePaths {
4
6
  dataRoot: string;
5
7
  stateDir: string;
6
8
  /** `--worktree [name]`: start in a new managed worktree (true) or the named one. */
7
9
  worktree?: string | true | null;
10
+ /** `--variant <scope>`: run on that scope's head, as a host's agent of that scope does. */
11
+ variant?: string | null;
8
12
  }
9
13
  export interface StandaloneDependencies {
10
14
  home?: string;
@@ -13,6 +17,9 @@ export interface StandaloneDependencies {
13
17
  stdoutIsTTY?: boolean;
14
18
  createSessionId?: () => string;
15
19
  createHarness?: (options: HarnessOptions) => Harness;
20
+ hostSecretStore?: (descriptor: Extract<SecretBackendDescriptor, {
21
+ kind: 'host';
22
+ }>) => SecretStore | Promise<SecretStore>;
16
23
  spawn?: typeof nodeSpawn;
17
24
  }
18
25
  export declare function standaloneArguments(argv: string[]): string[] | null;
@@ -6,30 +6,39 @@ import { homedir } from 'node:os';
6
6
  import { isAbsolute, join, resolve } from 'node:path';
7
7
  import { setLogSink } from "../core/bus.js";
8
8
  import { DEFAULT_PRODUCT_NAME } from "../core/env.js";
9
+ import { interactiveProfileSecretStore, resolveInteractiveProfile } from "../core/interactiveProfile.js";
9
10
  import { createHarness } from "../harness.js";
11
+ import { seedVariant } from "../variant/seed.js";
12
+ import * as variantStore from "../variant/store.js";
10
13
  export function standaloneArguments(argv) {
11
14
  const command = argv[0];
12
15
  if (!command || command.startsWith('-'))
13
16
  return argv;
14
17
  return command === 'terminal' ? argv.slice(1) : null;
15
18
  }
16
- export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd()) {
17
- const root = join(home, '.xingular-harness');
18
- const paths = {
19
- dataRoot: join(root, 'data'),
20
- stateDir: join(root, 'state')
21
- };
19
+ function resolveStandaloneOptions(argv, home = homedir(), cwd = process.cwd()) {
20
+ const overrides = {};
22
21
  const given = new Set();
22
+ let worktree;
23
+ let variant;
23
24
  for (let index = 0; index < argv.length; index += 1) {
24
25
  const argument = argv[index];
25
26
  if (argument === '--worktree' || argument === '-w') {
26
27
  const name = argv[index + 1];
27
28
  if (name && !name.startsWith('-')) {
28
- paths.worktree = name;
29
+ worktree = name;
29
30
  index += 1;
30
31
  }
31
32
  else
32
- paths.worktree = true;
33
+ worktree = true;
34
+ continue;
35
+ }
36
+ if (argument === '--variant') {
37
+ const scope = argv[index + 1];
38
+ if (!scope || scope.startsWith('-'))
39
+ throw new Error('--variant needs a variant scope.');
40
+ variant = scope;
41
+ index += 1;
33
42
  continue;
34
43
  }
35
44
  if (argument !== '--data' && argument !== '--state')
@@ -37,7 +46,10 @@ export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd
37
46
  const value = argv[index + 1];
38
47
  if (!value || value.startsWith('--'))
39
48
  throw new Error(`${argument} needs a directory path.`);
40
- paths[argument === '--data' ? 'dataRoot' : 'stateDir'] = resolve(cwd, value);
49
+ if (argument === '--data')
50
+ overrides.dataRoot = resolve(cwd, value);
51
+ else
52
+ overrides.stateDir = resolve(cwd, value);
41
53
  given.add(argument);
42
54
  index += 1;
43
55
  }
@@ -46,7 +58,49 @@ export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd
46
58
  if (!isAbsolute(home) && !(given.has('--data') && given.has('--state'))) {
47
59
  throw new Error(`The home folder "${home}" is not an absolute path, so the default data and state folders cannot be placed. Set HOME to your home folder, or pass --data and --state with absolute folders.`);
48
60
  }
49
- return paths;
61
+ // `npm run dev` passes these roots explicitly. Keep its profile and credentials separate from
62
+ // the installed interactive profile, even when that production profile selects a host store.
63
+ const devRoot = isAbsolute(home) ? join(home, '.xingular-harness-dev') : null;
64
+ const devData = devRoot ? join(devRoot, 'data') : null;
65
+ const devState = devRoot ? join(devRoot, 'state') : null;
66
+ const development = !!devRoot && (overrides.dataRoot === devData || overrides.stateDir === devState);
67
+ const resolvedProfile = isAbsolute(home)
68
+ ? resolveInteractiveProfile({
69
+ home,
70
+ ...(development ? {
71
+ configPath: join(devRoot, 'profile.json'),
72
+ legacyDefaults: {
73
+ dataRoot: devData,
74
+ stateDir: devState,
75
+ defaultVariantScope: 'default',
76
+ secretBackend: { kind: 'file', path: join(devState, 'secrets.json') }
77
+ }
78
+ } : {}),
79
+ overrides
80
+ })
81
+ : {
82
+ version: 1,
83
+ dataRoot: overrides.dataRoot,
84
+ stateDir: overrides.stateDir,
85
+ defaultVariantScope: 'default',
86
+ secretBackend: { kind: 'file', path: join(overrides.stateDir, 'secrets.json') }
87
+ };
88
+ // The standalone development launch has no host adapter and must not use the App's Keychain.
89
+ const profile = development
90
+ ? { ...resolvedProfile, secretBackend: { kind: 'file', path: join(resolvedProfile.stateDir, 'secrets.json') } }
91
+ : resolvedProfile;
92
+ return {
93
+ profile,
94
+ paths: {
95
+ dataRoot: profile.dataRoot,
96
+ stateDir: profile.stateDir,
97
+ ...(worktree !== undefined ? { worktree } : {}),
98
+ ...(variant !== undefined ? { variant } : {})
99
+ }
100
+ };
101
+ }
102
+ export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd()) {
103
+ return resolveStandaloneOptions(argv, home, cwd).paths;
50
104
  }
51
105
  function processEnvironment(overrides) {
52
106
  const environment = { ...process.env };
@@ -133,9 +187,10 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
133
187
  throw new Error('The interactive Harness terminal needs a TTY. Run it from a terminal, or use `xagent serve` for a host process.');
134
188
  }
135
189
  let cwd = resolve(dependencies.cwd ?? process.cwd());
136
- const paths = parseStandaloneOptions(argv, dependencies.home, cwd);
190
+ const { paths, profile } = resolveStandaloneOptions(argv, dependencies.home, cwd);
191
+ const secrets = await interactiveProfileSecretStore(profile, dependencies.hostSecretStore);
137
192
  const makeHarness = dependencies.createHarness ?? createHarness;
138
- const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: DEFAULT_PRODUCT_NAME });
193
+ const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: DEFAULT_PRODUCT_NAME, secrets });
139
194
  const newSessionId = dependencies.createSessionId ?? randomUUID;
140
195
  let current = { sessionId: newSessionId(), accountId: null, nativeSessionId: null };
141
196
  // The frontend draws on this terminal, so the host's diagnostics go to a file the person can read instead.
@@ -150,6 +205,10 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
150
205
  });
151
206
  try {
152
207
  await harness.ready;
208
+ const variantScope = paths.variant ?? profile.defaultVariantScope;
209
+ if (variantScope === profile.defaultVariantScope && !variantStore.head(paths.dataRoot, variantScope)) {
210
+ variantStore.seed(paths.dataRoot, variantScope, seedVariant(paths.dataRoot, {}), 'interactive profile default');
211
+ }
153
212
  if (harness.accounts.overview().accounts.length === 0)
154
213
  harness.accounts.create('Default', 'subscription');
155
214
  let worktreeId = null;
@@ -160,7 +219,7 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
160
219
  }
161
220
  for (;;) {
162
221
  const sessionFolder = join(paths.dataRoot, 'sessions', 'terminal', createHash('sha256').update(cwd).digest('hex'));
163
- const launch = await harness.terminals.prepare({ ...current, cwd, sessionFolder, ...(worktreeId ? { run: { worktreeId } } : {}) });
222
+ const launch = await harness.terminals.prepare({ ...current, cwd, sessionFolder, variant: { scope: variantScope }, ...(worktreeId ? { run: { worktreeId } } : {}) });
164
223
  const code = await spawnFrontend(launch, cwd, dependencies.spawn);
165
224
  // `/accounts` and `/worktree` ask for a switch and end the frontend. An account switch resumes
166
225
  // the conversation; a folder switch starts a new one there.
@@ -1,13 +1,17 @@
1
- import type { RunEvent, RunStatus } from "../sessions/contracts.js";
1
+ import { type RunEvent, type RunStatus, type UsagePayload } from "../sessions/contracts.js";
2
+ import type { Effort } from "../sessions/runner.js";
2
3
  /** The agent profile a child started from, as the roster shows it (PROP-0029). */
3
4
  export interface TerminalSubagentProfile {
4
5
  name: string;
5
6
  color: string;
6
7
  }
8
+ /** The main agent's row: the parent run itself, shown by name rather than by a profile. */
9
+ export declare const MAIN_PROFILE: TerminalSubagentProfile;
7
10
  export interface TerminalSubagentRun {
8
11
  id: string;
9
12
  parentRunId: string | null;
10
- relation: 'subagent';
13
+ /** `main` is the parent run itself; every other row is a child or nested child. */
14
+ relation: 'main' | 'subagent';
11
15
  /** The short name the parent gave the child (PROP-0030). */
12
16
  title: string;
13
17
  goal: string | null;
@@ -15,11 +19,19 @@ export interface TerminalSubagentRun {
15
19
  startedAt: string;
16
20
  endedAt: string | null;
17
21
  profile?: TerminalSubagentProfile | null;
22
+ /** The pinned account's display label; subagents never switch accounts. */
23
+ accountLabel?: string | null;
24
+ /** Effective launch selection, superseded by provider-reported usage when available. */
25
+ model?: string | null;
26
+ effort?: Effort | null;
18
27
  }
28
+ export type TerminalSubagentUsage = Partial<Pick<UsagePayload, 'provider' | 'model' | 'inputTokens' | 'outputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens' | 'costUsd' | 'costSource' | 'requests' | 'contextTokens' | 'contextWindow'>>;
19
29
  export interface TerminalSubagentActivity extends TerminalSubagentRun {
20
30
  activity: string;
21
- /** How far the child's own checklist is: counts only, never the items. */
22
- checklist: {
31
+ /** Cumulative run usage only; provider reasoning summaries and request details stay private. */
32
+ usage: TerminalSubagentUsage | null;
33
+ /** How far the child's own todo is: counts only, never the items. */
34
+ todo: {
23
35
  done: number;
24
36
  total: number;
25
37
  } | null;
@@ -27,12 +39,22 @@ export interface TerminalSubagentActivity extends TerminalSubagentRun {
27
39
  export declare const ACTIVE_SUBAGENT_STATUSES: Set<"queued" | "running" | "waiting_for_approval" | "waiting_for_input" | "interrupted" | "completed" | "failed">;
28
40
  export declare function terminalSubagentDuration(activity: TerminalSubagentRun, now?: number): string;
29
41
  export declare function terminalSubagentTone(status: RunStatus): 'success' | 'warning' | 'error' | 'muted';
30
- /** Volatile, read-only activity projection for the active terminal parent run. */
42
+ /** Volatile, read-only activity projection for the latest terminal parent run. */
31
43
  export declare class SubagentMonitor {
32
44
  private parentRunId;
33
45
  private readonly activities;
34
46
  begin(parentRunId: string): void;
35
- observe(parentRunId: string, run: TerminalSubagentRun, event: RunEvent): void;
47
+ /** Folds one event of the tracked parent (`main`) or one of its descendants; `null` seeds the row. */
48
+ observe(parentRunId: string, run: TerminalSubagentRun, event: RunEvent | null): void;
49
+ /**
50
+ * Rebuilds the rows of a parent run that is no longer live, such as the latest one of a reopened
51
+ * conversation, by folding each run's saved events. Rows keep their final status.
52
+ */
53
+ restore(parentRunId: string, runs: readonly {
54
+ run: TerminalSubagentRun;
55
+ events: readonly RunEvent[];
56
+ }[]): void;
57
+ /** The main row first, then children in start order. */
36
58
  snapshot(parentRunId: string): TerminalSubagentActivity[];
37
59
  clear(parentRunId?: string): void;
38
60
  }