@xingular/harness 1.0.0 → 1.1.1

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 (257) hide show
  1. package/CHANGELOG.md +33 -5
  2. package/README.md +70 -19
  3. package/dist/cli.js +10321 -5785
  4. package/dist/sdk/agents/builtin.d.ts +2 -0
  5. package/dist/sdk/agents/builtin.js +34 -0
  6. package/dist/sdk/agents/narrow.d.ts +13 -0
  7. package/dist/sdk/agents/narrow.js +29 -0
  8. package/dist/sdk/agents/profiles.d.ts +80 -0
  9. package/dist/sdk/agents/profiles.js +221 -0
  10. package/dist/sdk/annotations/bridge.d.ts +42 -0
  11. package/dist/sdk/annotations/bridge.js +264 -0
  12. package/dist/sdk/annotations/capture.d.ts +116 -0
  13. package/dist/sdk/annotations/capture.js +308 -0
  14. package/dist/sdk/annotations/delivery.d.ts +72 -0
  15. package/dist/sdk/annotations/delivery.js +130 -0
  16. package/dist/sdk/annotations/inspect.d.ts +10 -0
  17. package/dist/sdk/annotations/inspect.js +33 -0
  18. package/dist/sdk/annotations/isolated.d.ts +6 -0
  19. package/dist/sdk/annotations/isolated.js +51 -0
  20. package/dist/sdk/annotations/pageScript.d.ts +28 -0
  21. package/dist/sdk/annotations/pageScript.js +570 -0
  22. package/dist/sdk/annotations/part.d.ts +10 -0
  23. package/dist/sdk/annotations/part.js +144 -0
  24. package/dist/sdk/annotations/queue.d.ts +4 -0
  25. package/dist/sdk/annotations/queue.js +40 -0
  26. package/dist/sdk/annotations/serverMethods.d.ts +3 -0
  27. package/dist/sdk/annotations/serverMethods.js +62 -0
  28. package/dist/sdk/annotations/store.d.ts +160 -0
  29. package/dist/sdk/annotations/store.js +386 -0
  30. package/dist/sdk/browser/index.d.ts +14 -3
  31. package/dist/sdk/browser/index.js +11 -4
  32. package/dist/sdk/browser/serverMethods.d.ts +3 -0
  33. package/dist/sdk/browser/serverMethods.js +7 -0
  34. package/dist/sdk/browser/session.d.ts +1 -0
  35. package/dist/sdk/browser/session.js +45 -9
  36. package/dist/sdk/cli.js +19 -11
  37. package/dist/sdk/core/bus.js +1 -1
  38. package/dist/sdk/core/env.d.ts +1 -1
  39. package/dist/sdk/core/env.js +1 -1
  40. package/dist/sdk/doctor/serverMethods.d.ts +3 -0
  41. package/dist/sdk/doctor/serverMethods.js +8 -0
  42. package/dist/sdk/harness.d.ts +15 -0
  43. package/dist/sdk/harness.js +44 -5
  44. package/dist/sdk/history/recall.js +8 -8
  45. package/dist/sdk/history/serverMethods.d.ts +3 -0
  46. package/dist/sdk/history/serverMethods.js +8 -0
  47. package/dist/sdk/hub/cli.d.ts +1 -1
  48. package/dist/sdk/hub/cli.js +1 -1
  49. package/dist/sdk/hub/schema.d.ts +1 -1
  50. package/dist/sdk/hub/serverMethods.d.ts +3 -0
  51. package/dist/sdk/hub/serverMethods.js +11 -0
  52. package/dist/sdk/index.d.ts +10 -0
  53. package/dist/sdk/index.js +7 -0
  54. package/dist/sdk/mcp/connections.js +14 -7
  55. package/dist/sdk/mcp/serverMethods.d.ts +3 -0
  56. package/dist/sdk/mcp/serverMethods.js +22 -0
  57. package/dist/sdk/plugins/builtin.d.ts +1 -0
  58. package/dist/sdk/plugins/builtin.js +128 -33
  59. package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
  60. package/dist/sdk/plugins/builtinRegistry.js +49 -0
  61. package/dist/sdk/plugins/cli.d.ts +1 -1
  62. package/dist/sdk/plugins/cli.js +4 -3
  63. package/dist/sdk/prompt/compile.d.ts +5 -2
  64. package/dist/sdk/prompt/compile.js +25 -8
  65. package/dist/sdk/refine/cli.d.ts +1 -1
  66. package/dist/sdk/refine/cli.js +2 -2
  67. package/dist/sdk/refine/serverMethods.d.ts +3 -0
  68. package/dist/sdk/refine/serverMethods.js +20 -0
  69. package/dist/sdk/server/methods.js +9 -86
  70. package/dist/sdk/server/params.d.ts +4 -0
  71. package/dist/sdk/server/params.js +11 -0
  72. package/dist/sdk/sessions/contracts.d.ts +17 -0
  73. package/dist/sdk/sessions/contracts.js +42 -3
  74. package/dist/sdk/sessions/planMode.d.ts +5 -2
  75. package/dist/sdk/sessions/planMode.js +12 -3
  76. package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
  77. package/dist/sdk/sessions/reasoningSummaries.js +3 -3
  78. package/dist/sdk/sessions/registry.d.ts +1 -0
  79. package/dist/sdk/sessions/registry.js +4 -1
  80. package/dist/sdk/sessions/runIndex.js +7 -7
  81. package/dist/sdk/sessions/runStore.js +3 -1
  82. package/dist/sdk/sessions/runner.js +2 -2
  83. package/dist/sdk/sessions/sessions.d.ts +17 -5
  84. package/dist/sdk/sessions/sessions.js +79 -12
  85. package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
  86. package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
  87. package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
  88. package/dist/sdk/sessions/sideQuestions.js +290 -0
  89. package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
  90. package/dist/sdk/sessions/subagentServerMethods.js +41 -0
  91. package/dist/sdk/sessions/subagentService.d.ts +13 -2
  92. package/dist/sdk/sessions/subagentService.js +34 -16
  93. package/dist/sdk/sessions/subagents.d.ts +9 -1
  94. package/dist/sdk/sessions/subagents.js +19 -0
  95. package/dist/sdk/terminal/browser.d.ts +62 -0
  96. package/dist/sdk/terminal/browser.js +41 -0
  97. package/dist/sdk/terminal/browserControl.d.ts +29 -0
  98. package/dist/sdk/terminal/browserControl.js +186 -0
  99. package/dist/sdk/terminal/commandHelp.d.ts +17 -0
  100. package/dist/sdk/terminal/commandHelp.js +20 -0
  101. package/dist/sdk/terminal/controller.d.ts +7 -1
  102. package/dist/sdk/terminal/controller.js +34 -2
  103. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
  104. package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
  105. package/dist/sdk/terminal/piExtension.js +33 -48
  106. package/dist/sdk/terminal/socket.d.ts +6 -3
  107. package/dist/sdk/terminal/socket.js +40 -9
  108. package/dist/sdk/terminal/standalone.d.ts +2 -0
  109. package/dist/sdk/terminal/standalone.js +50 -10
  110. package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
  111. package/dist/sdk/terminal/subagentMonitor.js +121 -0
  112. package/dist/sdk/terminal/terminals.d.ts +42 -6
  113. package/dist/sdk/terminal/terminals.js +359 -18
  114. package/dist/sdk/terminal/trajectory.js +2 -2
  115. package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
  116. package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
  117. package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
  118. package/dist/sdk/terminal/tui/commandPages.js +54 -6
  119. package/dist/sdk/terminal/tui/commands.d.ts +8 -4
  120. package/dist/sdk/terminal/tui/commands.js +181 -58
  121. package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
  122. package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
  123. package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
  124. package/dist/sdk/terminal/tui/dialogs.js +9 -2
  125. package/dist/sdk/terminal/tui/modelPicker.js +1 -6
  126. package/dist/sdk/terminal/tui/page.d.ts +3 -0
  127. package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
  128. package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
  129. package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
  130. package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
  131. package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
  132. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
  133. package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
  134. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
  135. package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
  136. package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
  137. package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
  138. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
  139. package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
  140. package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
  141. package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
  142. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
  143. package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
  144. package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
  145. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
  146. package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
  147. package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
  148. package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
  149. package/dist/sdk/terminal/tui/planMode.js +1 -1
  150. package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
  151. package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
  152. package/dist/sdk/terminal/tui/questions.d.ts +2 -0
  153. package/dist/sdk/terminal/tui/runtime.js +4 -2
  154. package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
  155. package/dist/sdk/terminal/tui/settingsModel.js +32 -2
  156. package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
  157. package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
  158. package/dist/sdk/terminal/tui/ui.d.ts +2 -0
  159. package/dist/sdk/terminal/tui/ui.js +1 -0
  160. package/dist/sdk/tools/askUser.js +26 -8
  161. package/dist/sdk/tools/checklist.js +2 -2
  162. package/dist/sdk/tools/harnessTools.d.ts +2 -0
  163. package/dist/sdk/tools/harnessTools.js +1 -1
  164. package/dist/sdk/tools/spawnSubagent.js +13 -4
  165. package/dist/sdk/web/part.d.ts +10 -0
  166. package/dist/sdk/web/part.js +6 -0
  167. package/dist/sdk/web/serverMethods.d.ts +3 -0
  168. package/dist/sdk/web/serverMethods.js +7 -0
  169. package/dist/sdk/wiki/serverMethods.d.ts +3 -0
  170. package/dist/sdk/wiki/serverMethods.js +26 -0
  171. package/dist/sdk/worktrees/cli.d.ts +11 -0
  172. package/dist/sdk/worktrees/cli.js +146 -0
  173. package/dist/sdk/worktrees/copy.d.ts +9 -0
  174. package/dist/sdk/worktrees/copy.js +111 -0
  175. package/dist/sdk/worktrees/git.d.ts +16 -0
  176. package/dist/sdk/worktrees/git.js +81 -0
  177. package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
  178. package/dist/sdk/worktrees/serverMethods.js +30 -0
  179. package/dist/sdk/worktrees/service.d.ts +73 -0
  180. package/dist/sdk/worktrees/service.js +483 -0
  181. package/dist/sdk/worktrees/setup.d.ts +23 -0
  182. package/dist/sdk/worktrees/setup.js +65 -0
  183. package/dist/sdk/worktrees/types.d.ts +91 -0
  184. package/dist/sdk/worktrees/types.js +1 -0
  185. package/dist/sdk/world/cli.d.ts +2 -2
  186. package/dist/sdk/world/cli.js +6 -6
  187. package/dist/sdk/world/world.d.ts +3 -1
  188. package/dist/sdk/world/world.js +8 -9
  189. package/dist/terminal-opentui.js +14540 -7374
  190. package/dist/terminal-pi.js +285 -666
  191. package/docs/README.md +22 -2
  192. package/docs/architecture/00-overview.md +28 -16
  193. package/docs/architecture/02-sessions-and-runs.md +72 -15
  194. package/docs/architecture/03-variants.md +10 -2
  195. package/docs/architecture/04-prompt.md +26 -6
  196. package/docs/architecture/06-plugins-and-capabilities.md +39 -3
  197. package/docs/architecture/07-tools-and-gate.md +21 -6
  198. package/docs/architecture/09-accounts-and-connectors.md +8 -0
  199. package/docs/architecture/10-server-protocol.md +111 -2
  200. package/docs/architecture/11-terminal.md +185 -5
  201. package/docs/architecture/12-storage.md +15 -1
  202. package/docs/architecture/13-world-tools.md +28 -6
  203. package/docs/architecture/14-surfaces-and-parity.md +89 -0
  204. package/docs/architecture/README.md +15 -4
  205. package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
  206. package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
  207. package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
  208. package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
  209. package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
  210. package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
  211. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
  212. package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
  213. package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
  214. package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
  215. package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
  216. package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
  217. package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
  218. package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
  219. package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
  220. package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
  221. package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
  222. package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
  223. package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
  224. package/docs/architecture/proposals/README.md +25 -0
  225. package/docs/decisions/ADR-0009-xharness-command.md +6 -5
  226. package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
  227. package/docs/decisions/ADR-0015-resource-browser.md +28 -0
  228. package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
  229. package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
  230. package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
  231. package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
  232. package/docs/decisions/ADR-0020-page-annotations.md +37 -0
  233. package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
  234. package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
  235. package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
  236. package/docs/decisions/ADR-0024-xagent-command.md +40 -0
  237. package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
  238. package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
  239. package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
  240. package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
  241. package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
  242. package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
  243. package/docs/doctor.md +10 -4
  244. package/docs/features/README.md +26 -0
  245. package/docs/features/interfaces/README.md +1 -1
  246. package/docs/future-work.md +48 -0
  247. package/docs/parity.md +78 -0
  248. package/docs/releases/1.0.0.md +2 -2
  249. package/docs/releases/4.0.0.md +46 -0
  250. package/docs/releases/README.md +20 -15
  251. package/docs/study/00-index.md +5 -0
  252. package/docs/study/autonomous-engineering-sources.md +277 -0
  253. package/docs/study/autonomy-sources.md +147 -0
  254. package/docs/study/proactive-autonomy.md +408 -0
  255. package/docs/study/tui-resource-browser.md +78 -0
  256. package/docs/terminal-tui.md +325 -93
  257. package/package.json +3 -3
@@ -5,8 +5,8 @@
5
5
  // reply leaves it open for the next request. Each terminal session holds a random token bound to
6
6
  // its caller, its connected-apps grant, and its controller; the token stops working when the
7
7
  // terminal ends.
8
- import { createHash, randomBytes } from 'node:crypto';
9
- import { chmodSync, mkdirSync, rmSync, statSync } from 'node:fs';
8
+ import { randomBytes } from 'node:crypto';
9
+ import { chmodSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
10
10
  import { createServer } from 'node:net';
11
11
  import { tmpdir } from 'node:os';
12
12
  import { dirname, join } from 'node:path';
@@ -17,21 +17,50 @@ const MAX_REQUEST_BYTES = 1024 * 1024;
17
17
  /** macOS refuses a socket path longer than 103 bytes. */
18
18
  const MAX_SOCKET_PATH = 100;
19
19
  /**
20
- * Windows has no socket files for Node's `net`: it listens on a named pipe, which lives in a
21
- * machine-wide namespace. A random name keeps another local process from guessing it and standing
22
- * in front of the session; the token still decides what a caller may do.
20
+ * Each Harness process listens on its own path. Windows has no socket files for Node's `net`: it
21
+ * listens on a named pipe in a machine-wide namespace, and a random name keeps another local process
22
+ * from guessing it. On Unix the file is named for this process, because several Harness processes
23
+ * may share one state folder: closing a server removes its socket file, so a shared name would let
24
+ * one process delete, and the next one take over, the path another's sessions still call. The token
25
+ * still decides what a caller may do.
23
26
  */
24
27
  export function socketPath(stateDir) {
25
28
  if (process.platform === 'win32')
26
29
  return `\\\\.\\pipe\\xingular-harness-${randomBytes(12).toString('hex')}`;
27
- const path = join(stateDir, 'run', 'harness.sock');
30
+ const path = join(stateDir, 'run', `harness-${process.pid}-${randomBytes(8).toString('hex')}.sock`);
28
31
  if (Buffer.byteLength(path) <= MAX_SOCKET_PATH)
29
32
  return path;
30
- return join(tmpdir(), `xh-${createHash('sha256').update(path).digest('hex').slice(0, 16)}.sock`);
33
+ return join(tmpdir(), `xh-${process.pid}-${randomBytes(8).toString('hex')}.sock`);
34
+ }
35
+ const OWN_SOCKET = /^harness-(\d+)-[0-9a-f]+\.sock$/;
36
+ /** Removes socket files left in `folder` by Harness processes that are no longer running. */
37
+ function pruneDeadSockets(folder) {
38
+ let names;
39
+ try {
40
+ names = readdirSync(folder);
41
+ }
42
+ catch {
43
+ return;
44
+ }
45
+ for (const name of names) {
46
+ const pid = Number(OWN_SOCKET.exec(name)?.[1]);
47
+ if (!pid || pid === process.pid || isRunning(pid))
48
+ continue;
49
+ rmSync(join(folder, name), { force: true });
50
+ }
51
+ }
52
+ function isRunning(pid) {
53
+ try {
54
+ process.kill(pid, 0);
55
+ return true;
56
+ }
57
+ catch (error) {
58
+ return error.code === 'EPERM';
59
+ }
31
60
  }
32
61
  function alternateSocketPath(path) {
33
62
  const candidate = join(dirname(path), `harness-${process.pid}-${randomBytes(8).toString('hex')}.sock`);
34
- return Buffer.byteLength(candidate) <= MAX_SOCKET_PATH ? candidate : join(tmpdir(), `xh-${randomBytes(16).toString('hex')}.sock`);
63
+ return Buffer.byteLength(candidate) <= MAX_SOCKET_PATH ? candidate : join(tmpdir(), `xh-${process.pid}-${randomBytes(8).toString('hex')}.sock`);
35
64
  }
36
65
  /** A named pipe has no folder, file mode, or owner to check or clean up. */
37
66
  const isPipePath = (path) => path.startsWith('\\\\.\\pipe\\');
@@ -216,8 +245,10 @@ export class SessionSocket {
216
245
  }
217
246
  /** Binds the usual path, or an instance-specific path if another Harness owns it. */
218
247
  async bindAvailablePath() {
219
- if (!isPipePath(this.path))
248
+ if (!isPipePath(this.path)) {
220
249
  mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
250
+ pruneDeadSockets(dirname(this.path));
251
+ }
221
252
  try {
222
253
  await this.bind(this.path);
223
254
  }
@@ -3,6 +3,8 @@ import { type Harness, type HarnessOptions } from "../harness.js";
3
3
  export interface StandalonePaths {
4
4
  dataRoot: string;
5
5
  stateDir: string;
6
+ /** `--worktree [name]`: start in a new managed worktree (true) or the named one. */
7
+ worktree?: string | true | null;
6
8
  }
7
9
  export interface StandaloneDependencies {
8
10
  home?: string;
@@ -5,10 +5,11 @@ import { appendFileSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { isAbsolute, join, resolve } from 'node:path';
7
7
  import { setLogSink } from "../core/bus.js";
8
+ import { DEFAULT_PRODUCT_NAME } from "../core/env.js";
8
9
  import { createHarness } from "../harness.js";
9
10
  export function standaloneArguments(argv) {
10
11
  const command = argv[0];
11
- if (!command || command.startsWith('--') || command === '-h')
12
+ if (!command || command.startsWith('-'))
12
13
  return argv;
13
14
  return command === 'terminal' ? argv.slice(1) : null;
14
15
  }
@@ -21,6 +22,16 @@ export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd
21
22
  const given = new Set();
22
23
  for (let index = 0; index < argv.length; index += 1) {
23
24
  const argument = argv[index];
25
+ if (argument === '--worktree' || argument === '-w') {
26
+ const name = argv[index + 1];
27
+ if (name && !name.startsWith('-')) {
28
+ paths.worktree = name;
29
+ index += 1;
30
+ }
31
+ else
32
+ paths.worktree = true;
33
+ continue;
34
+ }
24
35
  if (argument !== '--data' && argument !== '--state')
25
36
  throw new Error(`Unknown standalone option: ${argument}`);
26
37
  const value = argv[index + 1];
@@ -99,14 +110,32 @@ function spawnFrontend(launch, cwd, spawn = nodeSpawn) {
99
110
  child.once('exit', (code, signal) => finish(() => resolveExit(exitCode(code, signal))));
100
111
  });
101
112
  }
113
+ /**
114
+ * `--worktree`: the named worktree of the repository in `cwd`, or a new managed one. Older managed
115
+ * worktrees beyond the kept number are pruned (unsaved work is kept as a snapshot).
116
+ */
117
+ async function enterWorktree(harness, cwd, wanted) {
118
+ const name = wanted === true ? undefined : wanted;
119
+ const existing = name ? (await harness.worktrees.list(cwd)).find((record) => record.name === name) : undefined;
120
+ if (existing)
121
+ return existing;
122
+ const created = await harness.worktrees.create({ repo: cwd, ...(name ? { name } : {}) });
123
+ process.stdout.write(`Working in worktree ${created.worktree.name} (${created.worktree.path}).\n`);
124
+ if (created.setup.state === 'untrusted')
125
+ process.stdout.write('This repository has a setup script you have not trusted; read .xharness/worktree-setup, then run: xagent worktree trust-setup\n');
126
+ if (created.setup.state === 'failed')
127
+ process.stdout.write(`The worktree setup script failed; see ${created.setup.log}.\n`);
128
+ await harness.worktrees.prune({ repo: cwd }).catch(() => undefined);
129
+ return created.worktree;
130
+ }
102
131
  export async function runStandaloneTerminal(argv, dependencies = {}) {
103
132
  if (!(dependencies.stdinIsTTY ?? process.stdin.isTTY) || !(dependencies.stdoutIsTTY ?? process.stdout.isTTY)) {
104
- throw new Error('The interactive Harness terminal needs a TTY. Run it from a terminal, or use `xharness serve` for a host process.');
133
+ throw new Error('The interactive Harness terminal needs a TTY. Run it from a terminal, or use `xagent serve` for a host process.');
105
134
  }
106
- const cwd = resolve(dependencies.cwd ?? process.cwd());
135
+ let cwd = resolve(dependencies.cwd ?? process.cwd());
107
136
  const paths = parseStandaloneOptions(argv, dependencies.home, cwd);
108
137
  const makeHarness = dependencies.createHarness ?? createHarness;
109
- const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: 'Xingular Harness' });
138
+ const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: DEFAULT_PRODUCT_NAME });
110
139
  const newSessionId = dependencies.createSessionId ?? randomUUID;
111
140
  let current = { sessionId: newSessionId(), accountId: null, nativeSessionId: null };
112
141
  // The frontend draws on this terminal, so the host's diagnostics go to a file the person can read instead.
@@ -123,17 +152,28 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
123
152
  await harness.ready;
124
153
  if (harness.accounts.overview().accounts.length === 0)
125
154
  harness.accounts.create('Default', 'subscription');
126
- const cwdHash = createHash('sha256').update(cwd).digest('hex');
127
- const sessionFolder = join(paths.dataRoot, 'sessions', 'terminal', cwdHash);
155
+ let worktreeId = null;
156
+ if (paths.worktree) {
157
+ const worktree = await enterWorktree(harness, cwd, paths.worktree);
158
+ cwd = worktree.path;
159
+ worktreeId = worktree.id;
160
+ }
128
161
  for (;;) {
129
- const launch = await harness.terminals.prepare({ ...current, cwd, sessionFolder });
162
+ 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 } } : {}) });
130
164
  const code = await spawnFrontend(launch, cwd, dependencies.spawn);
131
- // `/accounts` asks for a switch and ends the frontend. The conversation resumes on the chosen account.
165
+ // `/accounts` and `/worktree` ask for a switch and end the frontend. An account switch resumes
166
+ // the conversation; a folder switch starts a new one there.
132
167
  const next = harness.terminals.takeRelaunch(current.sessionId);
133
168
  if (!next)
134
169
  return code;
135
- const label = harness.accounts.find(next.accountId).label;
136
- process.stdout.write(`Switching this session to account ${label}.\n`);
170
+ if (next.cwd) {
171
+ process.stdout.write(`Opening ${next.cwd}.\n`);
172
+ cwd = next.cwd;
173
+ worktreeId = next.worktreeId ?? null;
174
+ }
175
+ else
176
+ process.stdout.write(`Switching this session to account ${harness.accounts.find(next.accountId).label}.\n`);
137
177
  await harness.terminals.dispose(current.sessionId);
138
178
  current = { sessionId: newSessionId(), accountId: next.accountId, nativeSessionId: next.nativeSessionId };
139
179
  }
@@ -0,0 +1,38 @@
1
+ import type { RunEvent, RunStatus } from "../sessions/contracts.js";
2
+ /** The agent profile a child started from, as the roster shows it (PROP-0029). */
3
+ export interface TerminalSubagentProfile {
4
+ name: string;
5
+ color: string;
6
+ }
7
+ export interface TerminalSubagentRun {
8
+ id: string;
9
+ parentRunId: string | null;
10
+ relation: 'subagent';
11
+ /** The short name the parent gave the child (PROP-0030). */
12
+ title: string;
13
+ goal: string | null;
14
+ status: RunStatus;
15
+ startedAt: string;
16
+ endedAt: string | null;
17
+ profile?: TerminalSubagentProfile | null;
18
+ }
19
+ export interface TerminalSubagentActivity extends TerminalSubagentRun {
20
+ activity: string;
21
+ /** How far the child's own checklist is: counts only, never the items. */
22
+ checklist: {
23
+ done: number;
24
+ total: number;
25
+ } | null;
26
+ }
27
+ export declare const ACTIVE_SUBAGENT_STATUSES: Set<"queued" | "running" | "waiting_for_approval" | "waiting_for_input" | "interrupted" | "completed" | "failed">;
28
+ export declare function terminalSubagentDuration(activity: TerminalSubagentRun, now?: number): string;
29
+ export declare function terminalSubagentTone(status: RunStatus): 'success' | 'warning' | 'error' | 'muted';
30
+ /** Volatile, read-only activity projection for the active terminal parent run. */
31
+ export declare class SubagentMonitor {
32
+ private parentRunId;
33
+ private readonly activities;
34
+ begin(parentRunId: string): void;
35
+ observe(parentRunId: string, run: TerminalSubagentRun, event: RunEvent): void;
36
+ snapshot(parentRunId: string): TerminalSubagentActivity[];
37
+ clear(parentRunId?: string): void;
38
+ }
@@ -0,0 +1,121 @@
1
+ export const ACTIVE_SUBAGENT_STATUSES = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
2
+ export function terminalSubagentDuration(activity, now = Date.now()) {
3
+ const start = Date.parse(activity.startedAt);
4
+ const end = activity.endedAt ? Date.parse(activity.endedAt) : now;
5
+ if (!Number.isFinite(start) || !Number.isFinite(end))
6
+ return '—';
7
+ const seconds = Math.max(0, Math.floor((end - start) / 1000));
8
+ if (seconds < 60)
9
+ return `${seconds}s`;
10
+ const minutes = Math.floor(seconds / 60);
11
+ if (minutes < 60)
12
+ return `${minutes}:${String(seconds % 60).padStart(2, '0')}`;
13
+ return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
14
+ }
15
+ export function terminalSubagentTone(status) {
16
+ if (status === 'completed')
17
+ return 'success';
18
+ if (status === 'failed')
19
+ return 'error';
20
+ if (ACTIVE_SUBAGENT_STATUSES.has(status))
21
+ return 'warning';
22
+ return 'muted';
23
+ }
24
+ function statusActivity(status) {
25
+ switch (status) {
26
+ case 'queued': return 'Queued';
27
+ case 'waiting_for_approval': return 'Waiting for approval';
28
+ case 'waiting_for_input': return 'Waiting for input';
29
+ case 'completed': return 'Completed';
30
+ case 'failed': return 'Failed';
31
+ case 'interrupted': return 'Interrupted';
32
+ case 'running': return 'Working';
33
+ }
34
+ }
35
+ function payloadOf(event) {
36
+ return event.payload && typeof event.payload === 'object' ? event.payload : {};
37
+ }
38
+ function stringOf(value) {
39
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
40
+ }
41
+ function latestTool(tools) {
42
+ return [...tools.values()].sort((left, right) => right.sequence - left.sequence)[0]?.text;
43
+ }
44
+ /** Volatile, read-only activity projection for the active terminal parent run. */
45
+ export class SubagentMonitor {
46
+ parentRunId = null;
47
+ activities = new Map();
48
+ begin(parentRunId) {
49
+ this.parentRunId = parentRunId;
50
+ this.activities.clear();
51
+ }
52
+ observe(parentRunId, run, event) {
53
+ if (this.parentRunId !== parentRunId || run.relation !== 'subagent')
54
+ return;
55
+ const row = this.activities.get(run.id) ?? {
56
+ id: run.id,
57
+ parentRunId: run.parentRunId,
58
+ relation: run.relation,
59
+ title: run.title,
60
+ goal: run.goal,
61
+ status: run.status,
62
+ startedAt: run.startedAt,
63
+ endedAt: run.endedAt,
64
+ activity: statusActivity(run.status),
65
+ checklist: null,
66
+ profile: run.profile ?? null,
67
+ tools: new Map(),
68
+ lastTool: null
69
+ };
70
+ row.parentRunId = run.parentRunId;
71
+ if (run.profile !== undefined)
72
+ row.profile = run.profile;
73
+ row.title = run.title;
74
+ row.goal = run.goal;
75
+ row.status = run.status;
76
+ row.startedAt = run.startedAt;
77
+ row.endedAt = run.endedAt;
78
+ const payload = payloadOf(event);
79
+ if (event.type === 'tool.started') {
80
+ const toolCallId = stringOf(payload.toolCallId);
81
+ const name = stringOf(payload.name) ?? 'Tool';
82
+ if (toolCallId)
83
+ row.tools.set(toolCallId, { sequence: event.sequence, text: name });
84
+ }
85
+ else if (event.type === 'tool.completed') {
86
+ const toolCallId = stringOf(payload.toolCallId);
87
+ const tool = toolCallId ? row.tools.get(toolCallId) : undefined;
88
+ if (tool && (!row.lastTool || tool.sequence >= row.lastTool.sequence))
89
+ row.lastTool = tool;
90
+ if (toolCallId)
91
+ row.tools.delete(toolCallId);
92
+ }
93
+ else if (event.type === 'message.delta') {
94
+ row.lastTool = null;
95
+ row.activity = 'Responding';
96
+ }
97
+ else if (event.type === 'checklist.updated') {
98
+ const items = Array.isArray(payload.items) ? payload.items : [];
99
+ row.checklist = items.length ? { done: items.filter(item => item?.status === 'completed').length, total: items.length } : null;
100
+ }
101
+ if (run.status !== 'running')
102
+ row.tools.clear();
103
+ row.activity = run.status === 'running'
104
+ ? latestTool(row.tools) ?? row.lastTool?.text ?? (event.type === 'message.delta' ? 'Responding' : 'Working')
105
+ : statusActivity(run.status);
106
+ this.activities.set(run.id, row);
107
+ }
108
+ snapshot(parentRunId) {
109
+ if (this.parentRunId !== parentRunId)
110
+ return [];
111
+ return [...this.activities.values()]
112
+ .sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
113
+ .map(({ tools: _tools, lastTool: _lastTool, ...activity }) => activity);
114
+ }
115
+ clear(parentRunId) {
116
+ if (parentRunId && this.parentRunId !== parentRunId)
117
+ return;
118
+ this.parentRunId = null;
119
+ this.activities.clear();
120
+ }
121
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CapabilitySet } from "../capabilities/types.js";
2
2
  import type { Subagents } from "../sessions/subagentService.js";
3
+ import type { SideQuestions } from "../sessions/sideQuestions.js";
3
4
  import { type SubagentOptions } from "../sessions/subagents.js";
4
5
  import type { Accounts } from "../accounts/accounts.js";
5
6
  import type { Approvals } from "../sessions/approvals.js";
@@ -18,11 +19,14 @@ import type { RunRecord } from "../sessions/contracts.js";
18
19
  import type { ToolPolicy } from "../sessions/policy.js";
19
20
  import type { RunRegistry } from "../sessions/registry.js";
20
21
  import type { World } from "../world/world.js";
22
+ import type { Worktrees } from "../worktrees/service.js";
23
+ import type { AnnotationCapture } from "../annotations/capture.js";
24
+ import type { NoteDeliveries } from "../annotations/delivery.js";
21
25
  /** The files a terminal session needs on disk, outside this process. */
22
26
  export interface TerminalAssets {
23
27
  /** The Node binary that runs the commands. */
24
28
  node: string;
25
- /** The built `xharness` command (`dist/cli.js`). */
29
+ /** The built `xagent` command (`dist/cli.js`). */
26
30
  cli: string;
27
31
  /** The built Pi extension (`dist/terminal-pi.js`). */
28
32
  piExtension: string;
@@ -52,6 +56,15 @@ export interface TerminalPrepareInput {
52
56
  /** Fields copied onto each run this session records. */
53
57
  run?: Partial<Pick<RunRecord, 'mode' | 'worktreeId'>>;
54
58
  }
59
+ /** What a session asked to restart as: another account, or another folder (a worktree or the main checkout). */
60
+ export interface TerminalRelaunch {
61
+ accountId: string;
62
+ nativeSessionId: string | null;
63
+ /** Restart in this folder, as a new conversation there. */
64
+ cwd?: string;
65
+ /** The managed worktree the new folder belongs to; null for the main checkout. */
66
+ worktreeId?: string | null;
67
+ }
55
68
  export interface TerminalLaunch {
56
69
  program: string;
57
70
  args: string[];
@@ -65,6 +78,8 @@ export interface TerminalsOptions {
65
78
  /** Which plugin code in variants a person trusted. */
66
79
  trust?: VariantTrust | null;
67
80
  subagents?: Subagents;
81
+ /** Shared tool-free side-question runs. */
82
+ sideQuestions?: SideQuestions;
68
83
  resolveCapabilities?: (caller: CallerContext, tools: readonly string[], cwd: string) => Promise<CapabilitySet>;
69
84
  env: HarnessEnv;
70
85
  stateDir: string;
@@ -82,6 +97,12 @@ export interface TerminalsOptions {
82
97
  assets?: TerminalAssets;
83
98
  /** Web, browser and computer tools, offered when their plugins are on. */
84
99
  world?: World;
100
+ /** Managed worktrees: a session naming one works in it and holds it while it lives. */
101
+ worktrees?: Worktrees;
102
+ /** The visible browser a person captures page notes in. */
103
+ noteCapture?: AnnotationCapture;
104
+ /** Page notes on their way to a session (PROP-0018). */
105
+ pageNotes?: NoteDeliveries;
85
106
  }
86
107
  /** What `/plugins` in a terminal can do. Each takes the session's folder, account and project. */
87
108
  export interface PluginControl {
@@ -98,6 +119,12 @@ export interface PluginControl {
98
119
  pluginId: string;
99
120
  settings: Record<string, unknown>;
100
121
  }): Promise<Plugin[]>;
122
+ install?(params: PluginScope & {
123
+ source: string;
124
+ }): Promise<Plugin[]>;
125
+ remove?(params: PluginScope & {
126
+ pluginId: string;
127
+ }): Promise<Plugin[]>;
101
128
  }
102
129
  export declare class Terminals {
103
130
  private readonly options;
@@ -110,8 +137,13 @@ export declare class Terminals {
110
137
  private readonly worldReadOnly;
111
138
  private readonly socket;
112
139
  private readonly assets;
113
- /** A session's `/accounts` switch, taken once its Pi process has ended. */
140
+ /** A session's `/accounts` or `/worktree` switch, taken once its Pi process has ended. */
114
141
  private readonly relaunches;
142
+ /** The managed worktree each live session holds. */
143
+ private readonly worktreeLeases;
144
+ /** Each child's profile name and colour, looked up once per child run. */
145
+ private readonly childProfiles;
146
+ private subagentProfile;
115
147
  constructor(options: TerminalsOptions);
116
148
  /** The commands a terminal session finds on PATH. Rewritten only when they change. */
117
149
  private ensureCommands;
@@ -133,11 +165,14 @@ export declare class Terminals {
133
165
  * or removing an account is a person's choice, never the model's shell command.
134
166
  */
135
167
  private accountsCall;
168
+ /** `/agents`: the agent profiles this session's folder can use (PROP-0029). */
169
+ private agentsCall;
170
+ /** `/worktree`: the managed worktrees of the repository this session works in. */
171
+ private worktreesCall;
172
+ /** `/plugin:notes` and `/plugin:annotate`: the page notes a person keeps, and the visible browser they capture them in. */
173
+ private annotationsCall;
136
174
  /** The account a session asked to switch to. Taken once, after its Pi process has ended and before `dispose`. */
137
- takeRelaunch(sessionId: string): {
138
- accountId: string;
139
- nativeSessionId: string | null;
140
- } | null;
175
+ takeRelaunch(sessionId: string): TerminalRelaunch | null;
141
176
  private live;
142
177
  private historyDir;
143
178
  private runRecord;
@@ -147,6 +182,7 @@ export declare class Terminals {
147
182
  private subagentsCall;
148
183
  private componentsCall;
149
184
  private trajectoryCall;
185
+ private sideQuestionCall;
150
186
  /** The only terminal frontend: the Bun-run OpenTUI application. */
151
187
  private frontendCommand;
152
188
  /**