@xingular/harness 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +28 -25
  3. package/dist/cli.js +6675 -2501
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +15 -1
  11. package/dist/sdk/agents/planResearch.d.ts +5 -0
  12. package/dist/sdk/agents/planResearch.js +19 -0
  13. package/dist/sdk/agents/profiles.d.ts +2 -1
  14. package/dist/sdk/agents/profiles.js +9 -4
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/cli.js +5 -4
  26. package/dist/sdk/commands/list.d.ts +3 -0
  27. package/dist/sdk/commands/list.js +75 -0
  28. package/dist/sdk/commands/registry.d.ts +250 -0
  29. package/dist/sdk/commands/registry.js +47 -0
  30. package/dist/sdk/commands/routes.d.ts +16 -0
  31. package/dist/sdk/commands/routes.js +33 -0
  32. package/dist/sdk/core/commandBudget.d.ts +23 -0
  33. package/dist/sdk/core/commandBudget.js +57 -0
  34. package/dist/sdk/core/index.js +17 -1
  35. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  36. package/dist/sdk/core/interactiveProfile.js +143 -0
  37. package/dist/sdk/core/workspace.d.ts +13 -0
  38. package/dist/sdk/core/workspace.js +88 -0
  39. package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
  40. package/dist/sdk/core/workspaceBashTool.js +36 -0
  41. package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
  42. package/dist/sdk/core/workspaceSandbox.js +320 -0
  43. package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
  44. package/dist/sdk/core/workspaceWriteOperations.js +206 -0
  45. package/dist/sdk/doctor/types.d.ts +2 -2
  46. package/dist/sdk/harness.d.ts +7 -0
  47. package/dist/sdk/harness.js +42 -2
  48. package/dist/sdk/index.d.ts +6 -1
  49. package/dist/sdk/index.js +3 -0
  50. package/dist/sdk/pi/runtime.d.ts +2 -1
  51. package/dist/sdk/pi/runtime.js +2 -2
  52. package/dist/sdk/plugins/builtin.js +29 -12
  53. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  54. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  55. package/dist/sdk/plugins/catalogue.js +2 -1
  56. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  57. package/dist/sdk/plugins/commandPrefix.js +21 -0
  58. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  59. package/dist/sdk/plugins/firstParty.js +8 -2
  60. package/dist/sdk/plugins/inventory.js +4 -3
  61. package/dist/sdk/plugins/manifest.d.ts +2 -0
  62. package/dist/sdk/plugins/manifest.js +2 -1
  63. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  64. package/dist/sdk/plugins/surfaces.js +11 -0
  65. package/dist/sdk/plugins/types.d.ts +15 -0
  66. package/dist/sdk/prompt/compile.d.ts +14 -0
  67. package/dist/sdk/prompt/compile.js +65 -29
  68. package/dist/sdk/prompt/standard.d.ts +17 -0
  69. package/dist/sdk/prompt/standard.js +21 -0
  70. package/dist/sdk/server/methods.js +18 -0
  71. package/dist/sdk/server/remote.js +36 -11
  72. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  73. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  74. package/dist/sdk/sessions/continuation.d.ts +3 -0
  75. package/dist/sdk/sessions/continuation.js +5 -1
  76. package/dist/sdk/sessions/contracts.d.ts +168 -0
  77. package/dist/sdk/sessions/contracts.js +59 -1
  78. package/dist/sdk/sessions/control.d.ts +72 -0
  79. package/dist/sdk/sessions/control.js +158 -0
  80. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  81. package/dist/sdk/sessions/initialBash.js +7 -0
  82. package/dist/sdk/sessions/planMode.d.ts +14 -3
  83. package/dist/sdk/sessions/planMode.js +73 -16
  84. package/dist/sdk/sessions/registry.d.ts +1 -1
  85. package/dist/sdk/sessions/registry.js +2 -2
  86. package/dist/sdk/sessions/runStore.d.ts +14 -0
  87. package/dist/sdk/sessions/runStore.js +35 -0
  88. package/dist/sdk/sessions/runner.d.ts +40 -2
  89. package/dist/sdk/sessions/runner.js +161 -13
  90. package/dist/sdk/sessions/sessions.d.ts +23 -3
  91. package/dist/sdk/sessions/sessions.js +202 -17
  92. package/dist/sdk/sessions/subagentService.d.ts +9 -0
  93. package/dist/sdk/sessions/subagentService.js +63 -4
  94. package/dist/sdk/sessions/workSlice.d.ts +103 -0
  95. package/dist/sdk/sessions/workSlice.js +297 -0
  96. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  97. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  98. package/dist/sdk/terminal/browserControl.js +2 -1
  99. package/dist/sdk/terminal/controller.d.ts +11 -3
  100. package/dist/sdk/terminal/controller.js +40 -13
  101. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  102. package/dist/sdk/terminal/opentui/theme.js +87 -0
  103. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  104. package/dist/sdk/terminal/piExtension.js +154 -54
  105. package/dist/sdk/terminal/standalone.d.ts +7 -0
  106. package/dist/sdk/terminal/standalone.js +72 -13
  107. package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
  108. package/dist/sdk/terminal/subagentMonitor.js +32 -1
  109. package/dist/sdk/terminal/terminals.d.ts +19 -2
  110. package/dist/sdk/terminal/terminals.js +245 -35
  111. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  112. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  113. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  114. package/dist/sdk/terminal/tui/commandPages.js +3 -0
  115. package/dist/sdk/terminal/tui/commands.d.ts +2 -0
  116. package/dist/sdk/terminal/tui/commands.js +205 -124
  117. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  118. package/dist/sdk/terminal/tui/list.js +96 -14
  119. package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
  120. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  121. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  122. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
  124. package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
  125. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  126. package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
  127. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  128. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  129. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  130. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  131. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  132. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  133. package/dist/sdk/terminal/tui/runtime.js +23 -3
  134. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  135. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  136. package/dist/sdk/tools/checklist.js +1 -0
  137. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  138. package/dist/sdk/tools/checkpoint.js +154 -0
  139. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  140. package/dist/sdk/tools/getCapabilities.js +34 -0
  141. package/dist/sdk/tools/spawnSubagent.js +4 -3
  142. package/dist/sdk/usage/analytics.d.ts +20 -0
  143. package/dist/sdk/usage/analytics.js +151 -0
  144. package/dist/sdk/usage/ledger.d.ts +38 -0
  145. package/dist/sdk/usage/ledger.js +142 -0
  146. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  147. package/dist/sdk/usage/limits/adapters.js +349 -0
  148. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  149. package/dist/sdk/usage/limits/capture.js +47 -0
  150. package/dist/sdk/usage/limits/http.d.ts +12 -0
  151. package/dist/sdk/usage/limits/http.js +51 -0
  152. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  153. package/dist/sdk/usage/limits/normalize.js +211 -0
  154. package/dist/sdk/usage/limits/service.d.ts +30 -0
  155. package/dist/sdk/usage/limits/service.js +176 -0
  156. package/dist/sdk/usage/limits/types.d.ts +65 -0
  157. package/dist/sdk/usage/limits/types.js +1 -0
  158. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  159. package/dist/sdk/usage/serverMethods.js +22 -0
  160. package/dist/sdk/usage/service.d.ts +32 -0
  161. package/dist/sdk/usage/service.js +66 -0
  162. package/dist/sdk/usage/settings.d.ts +7 -0
  163. package/dist/sdk/usage/settings.js +28 -0
  164. package/dist/sdk/usage/types.d.ts +60 -0
  165. package/dist/sdk/usage/types.js +4 -0
  166. package/dist/terminal-opentui.js +5207 -1883
  167. package/dist/terminal-pi.js +1658 -271
  168. package/docs/architecture/02-sessions-and-runs.md +128 -26
  169. package/docs/architecture/04-prompt.md +50 -12
  170. package/docs/architecture/06-plugins-and-capabilities.md +24 -7
  171. package/docs/architecture/07-tools-and-gate.md +104 -24
  172. package/docs/architecture/08-memory-and-history.md +4 -2
  173. package/docs/architecture/09-accounts-and-connectors.md +31 -1
  174. package/docs/architecture/10-server-protocol.md +58 -3
  175. package/docs/architecture/11-terminal.md +105 -42
  176. package/docs/architecture/12-storage.md +30 -2
  177. package/docs/architecture/13-world-tools.md +22 -1
  178. package/docs/architecture/14-surfaces-and-parity.md +54 -7
  179. package/docs/architecture/README.md +3 -1
  180. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  181. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  182. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  183. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  184. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  185. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  186. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  187. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  188. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  189. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  190. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  191. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  192. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
  193. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  194. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  195. package/docs/architecture/proposals/README.md +14 -0
  196. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  197. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  198. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  199. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  200. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  201. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  202. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  203. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  204. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  205. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  206. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  207. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  208. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  209. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  210. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  211. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  212. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  213. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  214. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  215. package/docs/parity.md +80 -24
  216. package/docs/releases/1.1.1.md +28 -0
  217. package/docs/releases/1.2.0.md +49 -0
  218. package/docs/releases/4.0.0.md +10 -9
  219. package/docs/releases/README.md +35 -30
  220. package/docs/study/00-index.md +3 -0
  221. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  222. package/docs/terminal-tui.md +100 -59
  223. package/package.json +4 -3
@@ -0,0 +1,87 @@
1
+ // OpenCode's terminal palette, carried by the Harness's own product name.
2
+ import { SELECTION_COLORS } from "../tui/selection.js";
3
+ export const opentuiTheme = {
4
+ background: '#0b0b0d',
5
+ panel: '#111114',
6
+ scrim: '#0b0b0d99',
7
+ border: '#2a2a31',
8
+ borderFocus: '#3a3a44',
9
+ text: '#e6e6e9',
10
+ muted: '#8b8b95',
11
+ dim: '#5c5c66',
12
+ selectionBg: SELECTION_COLORS.dark.background,
13
+ selectionText: SELECTION_COLORS.dark.text,
14
+ selectionMuted: SELECTION_COLORS.dark.muted,
15
+ questionSelectedBg: '#193322',
16
+ questionActionBg: '#35754a',
17
+ questionActionText: '#f7fff8',
18
+ primary: '#fab283',
19
+ accent: '#8ab4f8',
20
+ success: '#a6da95',
21
+ warning: '#eed49f',
22
+ error: '#ed8796',
23
+ bashMode: '#00ff88',
24
+ planMode: '#8ab4f8',
25
+ attachment: '#5ea1ff',
26
+ purple: '#c6a0f6',
27
+ cyan: '#8bd5ca'
28
+ };
29
+ /** The colour of an agent profile's name (PROP-0029); an unknown name falls back to the accent. */
30
+ export function profileColor(theme, name) {
31
+ switch (name) {
32
+ case 'blue': return theme.accent;
33
+ case 'green': return theme.success;
34
+ case 'yellow': return theme.warning;
35
+ case 'red': return theme.error;
36
+ case 'magenta': return '#f5bde6';
37
+ case 'cyan': return theme.cyan;
38
+ case 'orange': return theme.primary;
39
+ case 'purple': return theme.purple;
40
+ default: return theme.accent;
41
+ }
42
+ }
43
+ export function blendHexColor(from, to, amount) {
44
+ const start = /^#([\da-f]{6})$/iu.exec(from)?.[1];
45
+ const end = /^#([\da-f]{6})$/iu.exec(to)?.[1];
46
+ if (!start || !end)
47
+ return from;
48
+ const weight = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 0;
49
+ const channels = [0, 2, 4].map(offset => {
50
+ const first = Number.parseInt(start.slice(offset, offset + 2), 16);
51
+ const second = Number.parseInt(end.slice(offset, offset + 2), 16);
52
+ return Math.round(first + (second - first) * weight).toString(16).padStart(2, '0');
53
+ });
54
+ return `#${channels.join('')}`;
55
+ }
56
+ export function composerRuleColor(theme, mode) {
57
+ if (mode.bash)
58
+ return theme.bashMode;
59
+ if (mode.plan)
60
+ return theme.planMode;
61
+ return mode.overlay ? theme.border : theme.dim;
62
+ }
63
+ export function noticeColor(theme, type) {
64
+ if (type === 'error')
65
+ return theme.error;
66
+ if (type === 'warning')
67
+ return theme.warning;
68
+ return theme.muted;
69
+ }
70
+ export function toolColor(theme, status) {
71
+ if (status === 'error')
72
+ return theme.error;
73
+ if (status === 'running')
74
+ return theme.warning;
75
+ if (status === 'pending')
76
+ return theme.dim;
77
+ return theme.success;
78
+ }
79
+ export function thinkingLevelColor(theme, level) {
80
+ if (level === 'off')
81
+ return theme.dim;
82
+ if (level === 'minimal' || level === 'low')
83
+ return theme.muted;
84
+ if (level === 'medium')
85
+ return theme.accent;
86
+ return theme.primary;
87
+ }
@@ -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, 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
12
  import { checklistTool } from "../tools/checklist.js";
13
+ import { 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 { withWorkspaceWriteScope } 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;
@@ -42,6 +58,8 @@ export default async function (pi, planRun, getSession) {
42
58
  let requestStartedAt = null;
43
59
  const toolStartedAt = new Map();
44
60
  const fileTools = new Map();
61
+ const writeAuthorizations = new Map();
62
+ let workspaceRoot = null;
45
63
  // The harness's own tools for this session (connected apps and what the application offers).
46
64
  let stage = 'Tool discovery';
47
65
  try {
@@ -62,8 +80,19 @@ export default async function (pi, planRun, getSession) {
62
80
  }
63
81
  });
64
82
  }
83
+ // The harness's own extensions a person switched on, as a structured session loads them inline.
84
+ const own = await socketCall(connection, 'firstParty.list', {}, 15_000)
85
+ .then((reply) => reply?.plugins ?? [], () => []);
86
+ for (const plugin of own) {
87
+ if (plugin.id === OUTPUT_TRIM)
88
+ outputTrim(plugin.settings)(pi);
89
+ if (plugin.id === BUDGET_GUARD)
90
+ budgetGuard(plugin.settings)(pi);
91
+ }
65
92
  const native = await controlCall('terminal.subagents.tools');
66
93
  nativeSubagents = native.tools.length > 0;
94
+ if (planRun && native.profiles)
95
+ planRun.subagentProfiles = native.profiles;
67
96
  for (const tool of native.tools)
68
97
  pi.registerTool({
69
98
  name: tool.name, label: tool.label, description: tool.description, parameters: Type.Unsafe(tool.parameters),
@@ -77,6 +106,46 @@ export default async function (pi, planRun, getSession) {
77
106
  await terminalCall('startupFailed', { reason: `${stage} failed: ${error instanceof Error ? error.message : 'Unavailable'}` }).catch(() => { });
78
107
  throw error;
79
108
  }
109
+ pi.registerTool({
110
+ ...CAPABILITY_DISCOVERY,
111
+ parameters: Type.Unsafe(CAPABILITY_DISCOVERY.parameters),
112
+ async execute(_id, input) {
113
+ const active = pi.getActiveTools();
114
+ const tools = pi.getAllTools();
115
+ const selected = planRun?.active ? [...planRun.allowedTools] : active;
116
+ const name = input && typeof input === 'object' && typeof input.tool === 'string'
117
+ ? input.tool
118
+ : undefined;
119
+ const text = JSON.stringify(describeCapabilities({
120
+ capabilities: { version: 1, tools: selected, connectors: planRun?.connectors ?? [], contributions: [] },
121
+ active,
122
+ registered: tools
123
+ }, name));
124
+ return { content: [{ type: 'text', text }], details: undefined };
125
+ }
126
+ });
127
+ const workspaceBash = createWorkspaceBashOperations(() => workspaceRoot, {
128
+ bypassWorkspaceBoundary: async (request) => {
129
+ const result = await controlCall('terminal.workspaceBoundaryBypass', { tool: 'bash', args: { command: request.command, cwd: request.cwd, workspaceRoot: request.workspaceRoot } });
130
+ return result.bypass;
131
+ },
132
+ requestWorkspaceWriteScope: async (request) => {
133
+ const result = await controlCall('terminal.workspaceEscape', { tool: 'bash', args: { command: request.command, workspaceWriteScope: 'outside' }, reason: request.reason });
134
+ return result.approved;
135
+ },
136
+ workspaceWriteScope: () => planRun?.active ? 'read-only' : undefined
137
+ });
138
+ pi.registerTool(withWorkspaceWriteScope(createBashTool(process.cwd(), { operations: workspaceBash })));
139
+ pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
140
+ pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
141
+ compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
142
+ // A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
143
+ // person decides whether to continue.
144
+ const slicePolicy = parseWorkSlice(undefined);
145
+ const sliceState = createSliceState(slicePolicy);
146
+ // `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
147
+ if (slicePolicy.enabled)
148
+ workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
80
149
  // Only the latest screenshots stay in what the model sees (world/images.ts).
81
150
  pi.on('context', (event) => {
82
151
  const messages = pruneToolImages(event.messages);
@@ -120,9 +189,14 @@ export default async function (pi, planRun, getSession) {
120
189
  costReported = false;
121
190
  // A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
122
191
  const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' });
192
+ workspaceRoot = typeof begun.workspaceRoot === 'string' ? begun.workspaceRoot : null;
193
+ if (!workspaceRoot)
194
+ throw new Error('The active project workspace could not be resolved; the turn was not started.');
123
195
  if (nativeSubagents) {
124
196
  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()) });
197
+ 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()) });
198
+ if (planRun && Array.isArray(snapshot.connectors))
199
+ planRun.connectors = snapshot.connectors;
126
200
  subagentParent = true;
127
201
  }
128
202
  if (planRun && event.messageType === 'user') {
@@ -130,24 +204,31 @@ export default async function (pi, planRun, getSession) {
130
204
  event.systemPromptOptions.sections.plan = PLAN_INSTRUCTIONS;
131
205
  else
132
206
  delete event.systemPromptOptions.sections.plan;
207
+ if (planRun.subagentProfiles)
208
+ event.systemPromptOptions.sections.subagents = planRun.active ? planRun.subagentProfiles.plan : planRun.subagentProfiles.normal;
133
209
  }
134
210
  const bang = event.messageType === 'user' ? /^\s*!\s*([\s\S]+)$/u.exec(event.prompt) : null;
135
211
  if (!bang)
136
212
  return;
137
- const command = bang[1];
213
+ const initial = parseInitialBashCommand(bang[1]);
214
+ const command = initial.command;
215
+ const args = { command, ...(initial.workspaceWriteScope === 'outside' ? { workspaceWriteScope: 'outside' } : {}) };
138
216
  const id = `initial-bash:${begun.runId}`;
139
217
  const started = Date.now();
140
- await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify({ command }) });
218
+ await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify(args) });
141
219
  let refusal;
142
220
  let result;
143
- if (planRun?.active) {
221
+ if (!command) {
222
+ refusal = 'Enter a Bash command after !outside.';
223
+ }
224
+ else if (planRun?.active) {
144
225
  refusal = 'Bash commands are disabled while a plan request is running.';
145
226
  }
146
227
  else {
147
228
  try {
148
- const policy = await terminalCall('policy', { tool: 'bash', args: { command } });
229
+ const policy = await terminalCall('policy', { tool: 'bash', args });
149
230
  if (policy.requiresApproval) {
150
- const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args: { command } });
231
+ const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args });
151
232
  if (approval.decision !== 'approved')
152
233
  refusal = 'The user denied this Bash command.';
153
234
  }
@@ -159,7 +240,7 @@ export default async function (pi, planRun, getSession) {
159
240
  if (!session)
160
241
  refusal = 'The native session is unavailable.';
161
242
  else
162
- result = await session.executeBash(command, undefined, { id });
243
+ result = await runWithWorkspaceWriteScope(initial.workspaceWriteScope, () => withCommandBudget(() => session.executeBash(command, undefined, { id, operations: workspaceBash }), () => session.abortBash(), DEFAULT_COMMAND_TIMEOUT_SECONDS));
163
244
  }
164
245
  }
165
246
  catch (error) {
@@ -240,21 +321,61 @@ export default async function (pi, planRun, getSession) {
240
321
  pi.on('turn_start', async () => {
241
322
  requestStartedAt = Date.now();
242
323
  });
243
- pi.on('tool_call', async (event) => {
324
+ pi.on('tool_call', async (event, ctx) => {
244
325
  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.`;
326
+ const reason = planRefusal(event.toolName, new Set(), event.input) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`;
246
327
  return { block: true, reason };
247
328
  }
329
+ if (planRun?.active && event.toolName === SPAWN_SUBAGENT) {
330
+ try {
331
+ const eligibility = await controlCall('terminal.subagents.planSafe', { input: event.input });
332
+ if (!eligibility.allowed)
333
+ return { block: true, reason: eligibility.reason ?? 'This profile is not eligible for plan-mode research.' };
334
+ }
335
+ catch (error) {
336
+ return { block: true, reason: `Plan-mode research eligibility could not be verified: ${error instanceof Error ? error.message : 'controller unavailable'}` };
337
+ }
338
+ }
339
+ else if (planRun?.active) {
340
+ const reason = planRefusal(event.toolName, new Set(PLAN_RESEARCH_TOOLS), event.input);
341
+ if (reason)
342
+ return { block: true, reason };
343
+ }
344
+ let authorization;
345
+ if ((event.toolName === 'edit' || event.toolName === 'write') && typeof event.input.path === 'string') {
346
+ try {
347
+ if (!workspaceRoot)
348
+ throw new Error('The active project workspace is unavailable.');
349
+ const inspection = inspectWorkspaceWrite(workspaceRoot, ctx.cwd, event.toolName, event.input);
350
+ Object.assign(event.input, inspection.input);
351
+ if (!inspection.path)
352
+ throw new Error('The native file-write target could not be verified.');
353
+ authorization = {
354
+ workspaceRoot,
355
+ cwd: ctx.cwd,
356
+ path: inspection.path,
357
+ outside: inspection.outside,
358
+ authorized: false
359
+ };
360
+ }
361
+ catch (error) {
362
+ return { block: true, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.' };
363
+ }
364
+ }
248
365
  try {
249
366
  const policy = await controlCall('terminal.policy', { tool: event.toolName, args: event.input });
250
367
  if (policy.requiresApproval) {
251
368
  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.' };
369
+ if (approval.decision !== 'approved')
370
+ return { block: true, reason: 'The user denied this action. Ask how they would like to proceed.' };
255
371
  }
256
- if (!policy.allowed)
372
+ else if (!policy.allowed) {
257
373
  return { block: true, reason: policy.reason ?? 'Outside what this session may do.' };
374
+ }
375
+ if (authorization) {
376
+ authorization.authorized = true;
377
+ writeAuthorizations.set(event.toolCallId, authorization);
378
+ }
258
379
  }
259
380
  catch (error) {
260
381
  const detail = error instanceof Error ? error.message : String(error);
@@ -287,7 +408,10 @@ export default async function (pi, planRun, getSession) {
287
408
  }
288
409
  }
289
410
  subagentParent = false;
290
- await terminalCall('settled', { error: failure, aborted: event.aborted });
411
+ // A reply that stopped at its work-slice boundary is not a completed task: the record says so,
412
+ // so a host can offer the continuation instead of treating it as done (PROP-0041).
413
+ const reason = sliceState.endedBy ? sliceState.endedBy === 'stuck' ? 'stalled' : 'slice_complete' : undefined;
414
+ await terminalCall('settled', { error: failure, aborted: event.aborted, ...(reason ? { reason } : {}) });
291
415
  });
292
416
  pi.on('user_bash', async () => {
293
417
  throw new Error('Use the ! command composer so this action can be checked.');
@@ -326,6 +450,19 @@ export default async function (pi, planRun, getSession) {
326
450
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
327
451
  }
328
452
  });
453
+ // A bounded reply records its handoff here; the terminal reply is its own slice.
454
+ let checkpointRecorded = Promise.resolve();
455
+ const checkpoint = checkpointTool({ emit: (_type, payload) => { checkpointRecorded = terminalCall('checkpoint', payload); } }, 1);
456
+ pi.registerTool({
457
+ name: checkpoint.name, label: checkpoint.label, description: checkpoint.description,
458
+ parameters: Type.Unsafe(checkpoint.parameters(Type)),
459
+ async execute(_id, input) {
460
+ checkpointRecorded = Promise.resolve();
461
+ const result = await checkpoint.execute(input);
462
+ await checkpointRecorded;
463
+ return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
464
+ }
465
+ });
329
466
  const ask = askUserTool({ requestAnswers: questions => terminalCall('questions', { questions }, 1_230_000) });
330
467
  pi.registerTool({
331
468
  name: ask.name, label: ask.label, description: ask.description,
@@ -335,43 +472,6 @@ export default async function (pi, planRun, getSession) {
335
472
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
336
473
  }
337
474
  });
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
- });
475
+ // One question tool, as in every structured session: the terminal has no ask_user_question alias (PROP-0039).
376
476
  await terminalCall('startup', { phase: 'Pi hooks registered' });
377
477
  }
@@ -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,4 +1,5 @@
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;
@@ -15,9 +16,17 @@ export interface TerminalSubagentRun {
15
16
  startedAt: string;
16
17
  endedAt: string | null;
17
18
  profile?: TerminalSubagentProfile | null;
19
+ /** The pinned account's display label; subagents never switch accounts. */
20
+ accountLabel?: string | null;
21
+ /** Effective launch selection, superseded by provider-reported usage when available. */
22
+ model?: string | null;
23
+ effort?: Effort | null;
18
24
  }
25
+ export type TerminalSubagentUsage = Partial<Pick<UsagePayload, 'provider' | 'model' | 'inputTokens' | 'outputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens' | 'costUsd' | 'costSource' | 'requests' | 'contextTokens' | 'contextWindow'>>;
19
26
  export interface TerminalSubagentActivity extends TerminalSubagentRun {
20
27
  activity: string;
28
+ /** Cumulative run usage only; provider reasoning summaries and request details stay private. */
29
+ usage: TerminalSubagentUsage | null;
21
30
  /** How far the child's own checklist is: counts only, never the items. */
22
31
  checklist: {
23
32
  done: number;
@@ -27,7 +36,7 @@ export interface TerminalSubagentActivity extends TerminalSubagentRun {
27
36
  export declare const ACTIVE_SUBAGENT_STATUSES: Set<"queued" | "running" | "waiting_for_approval" | "waiting_for_input" | "interrupted" | "completed" | "failed">;
28
37
  export declare function terminalSubagentDuration(activity: TerminalSubagentRun, now?: number): string;
29
38
  export declare function terminalSubagentTone(status: RunStatus): 'success' | 'warning' | 'error' | 'muted';
30
- /** Volatile, read-only activity projection for the active terminal parent run. */
39
+ /** Volatile, read-only activity projection for the latest terminal parent run. */
31
40
  export declare class SubagentMonitor {
32
41
  private parentRunId;
33
42
  private readonly activities;