@xingular/harness 1.2.0 → 1.3.1-unstable.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 (186) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +26 -17
  3. package/builtin/skills/plugin-authoring/SKILL.md +34 -0
  4. package/dist/cli.js +7362 -6239
  5. package/dist/sdk/agents/builtin.js +3 -3
  6. package/dist/sdk/agents/planResearch.d.ts +23 -1
  7. package/dist/sdk/agents/planResearch.js +71 -8
  8. package/dist/sdk/agents/profiles.d.ts +16 -4
  9. package/dist/sdk/agents/profiles.js +75 -12
  10. package/dist/sdk/annotations/bridge.d.ts +2 -0
  11. package/dist/sdk/annotations/bridge.js +4 -0
  12. package/dist/sdk/annotations/capture.d.ts +44 -2
  13. package/dist/sdk/annotations/capture.js +104 -3
  14. package/dist/sdk/annotations/delivery.d.ts +24 -17
  15. package/dist/sdk/annotations/delivery.js +82 -49
  16. package/dist/sdk/annotations/inspect.js +7 -6
  17. package/dist/sdk/annotations/pageScript.js +4 -4
  18. package/dist/sdk/annotations/part.d.ts +0 -2
  19. package/dist/sdk/annotations/part.js +4 -33
  20. package/dist/sdk/annotations/queue.d.ts +2 -2
  21. package/dist/sdk/annotations/queue.js +12 -7
  22. package/dist/sdk/annotations/serverMethods.js +11 -7
  23. package/dist/sdk/annotations/store.d.ts +13 -21
  24. package/dist/sdk/annotations/store.js +47 -37
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/commands/list.js +2 -0
  27. package/dist/sdk/commands/registry.d.ts +11 -3
  28. package/dist/sdk/commands/registry.js +3 -2
  29. package/dist/sdk/core/links.d.ts +51 -0
  30. package/dist/sdk/core/links.js +155 -0
  31. package/dist/sdk/core/shell.d.ts +20 -0
  32. package/dist/sdk/core/shell.js +73 -0
  33. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  34. package/dist/sdk/core/workspaceBashTool.js +16 -2
  35. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  36. package/dist/sdk/core/workspaceSandbox.js +52 -12
  37. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  38. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  39. package/dist/sdk/doctor/checks.js +71 -12
  40. package/dist/sdk/harness.d.ts +5 -0
  41. package/dist/sdk/harness.js +11 -6
  42. package/dist/sdk/hub/hub.js +5 -3
  43. package/dist/sdk/index.d.ts +3 -2
  44. package/dist/sdk/index.js +2 -1
  45. package/dist/sdk/plugins/builtin.js +14 -9
  46. package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
  47. package/dist/sdk/plugins/builtinRegistry.js +9 -3
  48. package/dist/sdk/plugins/discover.d.ts +7 -1
  49. package/dist/sdk/plugins/discover.js +41 -2
  50. package/dist/sdk/plugins/identity.js +1 -1
  51. package/dist/sdk/plugins/install.d.ts +6 -2
  52. package/dist/sdk/plugins/install.js +29 -5
  53. package/dist/sdk/plugins/inventory.js +3 -3
  54. package/dist/sdk/plugins/manifest.d.ts +2 -0
  55. package/dist/sdk/plugins/manifest.js +21 -4
  56. package/dist/sdk/plugins/plugins.d.ts +2 -0
  57. package/dist/sdk/plugins/plugins.js +1 -0
  58. package/dist/sdk/plugins/proposals.d.ts +17 -0
  59. package/dist/sdk/plugins/proposals.js +147 -0
  60. package/dist/sdk/plugins/types.d.ts +1 -1
  61. package/dist/sdk/plugins/types.js +1 -1
  62. package/dist/sdk/prompt/compile.d.ts +13 -4
  63. package/dist/sdk/prompt/compile.js +46 -20
  64. package/dist/sdk/prompt/standard.d.ts +1 -1
  65. package/dist/sdk/prompt/standard.js +1 -1
  66. package/dist/sdk/server/methods.js +14 -2
  67. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  68. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  69. package/dist/sdk/sessions/continuation.d.ts +4 -2
  70. package/dist/sdk/sessions/continuation.js +7 -6
  71. package/dist/sdk/sessions/contracts.d.ts +22 -4
  72. package/dist/sdk/sessions/contracts.js +12 -7
  73. package/dist/sdk/sessions/planMode.js +2 -2
  74. package/dist/sdk/sessions/runStore.js +3 -1
  75. package/dist/sdk/sessions/runner.js +2 -2
  76. package/dist/sdk/sessions/sessionState.d.ts +13 -0
  77. package/dist/sdk/sessions/sessionState.js +31 -0
  78. package/dist/sdk/sessions/sessions.d.ts +7 -0
  79. package/dist/sdk/sessions/sessions.js +73 -15
  80. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  81. package/dist/sdk/sessions/subagentService.js +195 -55
  82. package/dist/sdk/sessions/subagents.d.ts +10 -1
  83. package/dist/sdk/sessions/subagents.js +4 -0
  84. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  85. package/dist/sdk/sessions/workSlice.js +77 -45
  86. package/dist/sdk/terminal/controller.d.ts +20 -0
  87. package/dist/sdk/terminal/controller.js +43 -18
  88. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  89. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  90. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  91. package/dist/sdk/terminal/opentui/model.js +482 -0
  92. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  93. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  94. package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
  95. package/dist/sdk/terminal/opentui/theme.js +2 -0
  96. package/dist/sdk/terminal/piExtension.js +98 -15
  97. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  98. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  99. package/dist/sdk/terminal/terminals.d.ts +16 -0
  100. package/dist/sdk/terminal/terminals.js +130 -26
  101. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  102. package/dist/sdk/terminal/trajectory.js +8 -0
  103. package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
  104. package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
  105. package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
  106. package/dist/sdk/terminal/tui/annotateSend.js +40 -0
  107. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  108. package/dist/sdk/terminal/tui/chrome.js +13 -0
  109. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  110. package/dist/sdk/terminal/tui/commandPages.js +16 -11
  111. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  112. package/dist/sdk/terminal/tui/commands.js +12 -2
  113. package/dist/sdk/terminal/tui/dialogs.js +1 -0
  114. package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
  115. package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
  116. package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
  117. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
  118. package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
  119. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  120. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  121. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  122. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
  124. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  125. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
  126. package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
  127. package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
  128. package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
  129. package/dist/sdk/tools/defaultSkills.d.ts +4 -0
  130. package/dist/sdk/tools/defaultSkills.js +20 -0
  131. package/dist/sdk/tools/definitions.d.ts +3 -1
  132. package/dist/sdk/tools/definitions.js +17 -1
  133. package/dist/sdk/tools/dispatch.js +12 -0
  134. package/dist/sdk/tools/harnessTools.js +1 -1
  135. package/dist/sdk/tools/sessionName.d.ts +9 -0
  136. package/dist/sdk/tools/sessionName.js +58 -11
  137. package/dist/sdk/tools/spawnSubagent.js +9 -7
  138. package/dist/sdk/tools/todo.d.ts +15 -0
  139. package/dist/sdk/tools/todo.js +64 -0
  140. package/dist/sdk/world/world.d.ts +0 -2
  141. package/dist/terminal-opentui.js +3331 -1584
  142. package/dist/terminal-pi.js +421 -133
  143. package/docs/README.md +3 -0
  144. package/docs/architecture/00-overview.md +1 -1
  145. package/docs/architecture/01-extension-points.md +1 -1
  146. package/docs/architecture/02-sessions-and-runs.md +73 -36
  147. package/docs/architecture/03-variants.md +1 -3
  148. package/docs/architecture/04-prompt.md +24 -13
  149. package/docs/architecture/06-plugins-and-capabilities.md +46 -10
  150. package/docs/architecture/07-tools-and-gate.md +49 -8
  151. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  152. package/docs/architecture/10-server-protocol.md +68 -16
  153. package/docs/architecture/11-terminal.md +94 -45
  154. package/docs/architecture/12-storage.md +1 -0
  155. package/docs/architecture/13-world-tools.md +8 -6
  156. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  157. package/docs/architecture/README.md +20 -18
  158. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  159. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
  160. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  161. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  162. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  163. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  164. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  165. package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
  166. package/docs/architecture/proposals/README.md +11 -6
  167. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  168. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  169. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  170. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  171. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  172. package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
  173. package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
  174. package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
  175. package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
  176. package/docs/doctor.md +14 -1
  177. package/docs/parity.md +16 -13
  178. package/docs/releases/1.3.0.md +21 -0
  179. package/docs/releases/1.3.1-unstable.1.md +8 -0
  180. package/docs/releases/README.md +9 -6
  181. package/docs/terminal-tui.md +127 -65
  182. package/package.json +1 -1
  183. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
  184. package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
  185. package/dist/sdk/tools/checklist.d.ts +0 -9
  186. package/dist/sdk/tools/checklist.js +0 -55
@@ -21,24 +21,26 @@ function mark(status, frame) {
21
21
  case 'running': return { mark: frame, tone: 'warning' };
22
22
  }
23
23
  }
24
- export function subagentTasks(checklist) {
25
- return checklist && checklist.total > 0 ? `${checklist.done} of ${checklist.total} tasks` : null;
24
+ export function subagentTasks(todo) {
25
+ return todo && todo.total > 0 ? `${todo.done} of ${todo.total} tasks` : null;
26
26
  }
27
27
  /**
28
28
  * The compact live roster shown under the status line: active children first, at most three
29
29
  * rows. The session name is cut first when the terminal is narrow.
30
30
  */
31
31
  export function subagentRoster(items, width, frame = getWorkingSpinnerFrames()[0] ?? '') {
32
- if (!items.length || width < 1)
32
+ // The status line already describes the main agent; its row is only on the /subagents page.
33
+ const children = items.filter(item => item.relation !== 'main');
34
+ if (!children.length || width < 1)
33
35
  return null;
34
- const ordered = [...items].sort((left, right) => {
36
+ const ordered = [...children].sort((left, right) => {
35
37
  const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
36
38
  return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
37
39
  });
38
40
  const rows = ordered.slice(0, MAX_VISIBLE_ROWS).map((item) => {
39
41
  const status = mark(item.status, frame);
40
42
  const profile = cut(safe(item.profile?.name ?? '') || 'subagent', Math.max(1, Math.min(24, width - 2)));
41
- const tasks = subagentTasks(item.checklist);
43
+ const tasks = subagentTasks(item.todo);
42
44
  const used = 2 + visibleWidth(profile) + GAP.length + (tasks ? GAP.length + tasks.length : 0);
43
45
  const title = cut(safe(item.title) || 'Subagent', Math.max(1, width - used));
44
46
  return { id: item.id, mark: status.mark, tone: status.tone, profile, color: item.profile?.color ?? null, title, tasks };
@@ -17,6 +17,8 @@ export interface OpenTuiTheme {
17
17
  questionActionText: string;
18
18
  primary: string;
19
19
  accent: string;
20
+ /** A link under the pointer, one step brighter than the link itself. */
21
+ linkHover: string;
20
22
  success: string;
21
23
  warning: string;
22
24
  error: string;
@@ -17,6 +17,7 @@ export const opentuiTheme = {
17
17
  questionActionText: '#f7fff8',
18
18
  primary: '#fab283',
19
19
  accent: '#8ab4f8',
20
+ linkHover: '#c9ddff',
20
21
  success: '#a6da95',
21
22
  warning: '#eed49f',
22
23
  error: '#ed8796',
@@ -37,6 +38,7 @@ export function profileColor(theme, name) {
37
38
  case 'cyan': return theme.cyan;
38
39
  case 'orange': return theme.primary;
39
40
  case 'purple': return theme.purple;
41
+ case 'main': return theme.primary;
40
42
  default: return theme.accent;
41
43
  }
42
44
  }
@@ -2,25 +2,26 @@
2
2
  // session does to the harness, asks before each tool call, takes instructions the harness
3
3
  // queues for it, and gives the session the harness's own tools. It runs in the `pi` process,
4
4
  // so everything it knows about the harness comes through the session socket.
5
- import { createBashTool, createEditTool, createWriteTool } from '@earendil-works/pi-coding-agent';
5
+ import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from '@earendil-works/pi-coding-agent';
6
6
  import { Type } from 'typebox';
7
7
  import { connectionFromEnv, socketCall, terminalCall } from "./client.js";
8
8
  import { controlCall } from "./control.js";
9
9
  import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
10
10
  import { PLAN_RESEARCH_TOOLS } from "../agents/planResearch.js";
11
11
  import { askUserTool } from "../tools/askUser.js";
12
- import { checklistTool } from "../tools/checklist.js";
13
- import { checkpointTool } from "../tools/checkpoint.js";
12
+ import { TODO_WRITE, todoTool } from "../tools/todo.js";
13
+ import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
14
14
  import { createSliceState, parseWorkSlice, workSliceGuard } from "../sessions/workSlice.js";
15
15
  import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
16
- import { sessionNameTool } from "../tools/sessionName.js";
16
+ import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
17
17
  import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
18
18
  import { PLAN_INSTRUCTIONS, planRefusal } from "../sessions/planMode.js";
19
19
  import { compactionCheckpoint } from "../sessions/compactionCheckpoint.js";
20
+ import { sessionStateText } from "../prompt/compile.js";
20
21
  import { pruneToolImages } from "../world/images.js";
21
22
  import { parseInitialBashCommand } from "../sessions/initialBash.js";
22
23
  import { inspectWorkspaceWrite } from "../sessions/workspaceBoundary.js";
23
- import { withWorkspaceWriteScope } from "../core/workspaceBashTool.js";
24
+ import { createWorkspaceShellTool } from "../core/workspaceBashTool.js";
24
25
  import { createWorkspaceBashOperations, runWithWorkspaceWriteScope } from "../core/workspaceSandbox.js";
25
26
  import { withWorkspaceWriteAuthorization, workspaceEditOperations, workspaceWriteOperations } from "../core/workspaceWriteOperations.js";
26
27
  import { BUDGET_GUARD, OUTPUT_TRIM, budgetGuard, outputTrim } from "../plugins/firstParty.js";
@@ -48,6 +49,7 @@ export default async function (pi, planRun, getSession) {
48
49
  let context = null;
49
50
  let nativeSubagents = false;
50
51
  let subagentParent = false;
52
+ let stoppedChildren = null;
51
53
  let timer = null;
52
54
  let polling = false;
53
55
  let failure;
@@ -60,6 +62,10 @@ export default async function (pi, planRun, getSession) {
60
62
  const fileTools = new Map();
61
63
  const writeAuthorizations = new Map();
62
64
  let workspaceRoot = null;
65
+ let sessionState = null;
66
+ let stateDelivered = false;
67
+ let autoNameTried = false;
68
+ let lastTodo = [];
63
69
  // The harness's own tools for this session (connected apps and what the application offers).
64
70
  let stage = 'Tool discovery';
65
71
  try {
@@ -135,9 +141,24 @@ export default async function (pi, planRun, getSession) {
135
141
  },
136
142
  workspaceWriteScope: () => planRun?.active ? 'read-only' : undefined
137
143
  });
138
- pi.registerTool(withWorkspaceWriteScope(createBashTool(process.cwd(), { operations: workspaceBash })));
144
+ pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
139
145
  pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
140
146
  pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
147
+ // The current name and this run's todo ride with each request; they never change the stable prefix.
148
+ pi.on('context', (event) => {
149
+ if (!sessionState || stateDelivered)
150
+ return;
151
+ stateDelivered = true;
152
+ return {
153
+ messages: [...event.messages, {
154
+ role: 'custom',
155
+ customType: 'xingular-session-state',
156
+ content: sessionState,
157
+ display: false,
158
+ timestamp: Date.now()
159
+ }]
160
+ };
161
+ });
141
162
  compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
142
163
  // A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
143
164
  // person decides whether to continue.
@@ -145,7 +166,7 @@ export default async function (pi, planRun, getSession) {
145
166
  const sliceState = createSliceState(slicePolicy);
146
167
  // `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
147
168
  if (slicePolicy.enabled)
148
- workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
169
+ workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
149
170
  // Only the latest screenshots stay in what the model sees (world/images.ts).
150
171
  pi.on('context', (event) => {
151
172
  const messages = pruneToolImages(event.messages);
@@ -153,9 +174,14 @@ export default async function (pi, planRun, getSession) {
153
174
  });
154
175
  pi.on('session_start', async (_event, ctx) => {
155
176
  context = ctx;
177
+ sessionState = null;
178
+ stateDelivered = false;
179
+ autoNameTried = false;
180
+ lastTodo = [];
156
181
  if (timer)
157
182
  clearInterval(timer);
158
- await terminalCall('ready', { nativeSessionId: ctx.sessionManager.getSessionFile() });
183
+ // The branch's parent runs let the controller show the latest one's rows again on reopen.
184
+ await terminalCall('ready', { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
159
185
  timer = setInterval(() => {
160
186
  if (polling || !context)
161
187
  return;
@@ -188,15 +214,20 @@ export default async function (pi, planRun, getSession) {
188
214
  reasoningTokens = undefined;
189
215
  costReported = false;
190
216
  // A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
191
- const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' });
217
+ 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 });
192
218
  workspaceRoot = typeof begun.workspaceRoot === 'string' ? begun.workspaceRoot : null;
193
219
  if (!workspaceRoot)
194
220
  throw new Error('The active project workspace could not be resolved; the turn was not started.');
221
+ // Every turn marks its run on the branch, so a reopened conversation finds its latest one.
222
+ pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
223
+ stoppedChildren = null;
195
224
  if (nativeSubagents) {
196
- pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
197
225
  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
226
  if (planRun && Array.isArray(snapshot.connectors))
199
227
  planRun.connectors = snapshot.connectors;
228
+ if (planRun && snapshot.subagentProfiles)
229
+ planRun.subagentProfiles = snapshot.subagentProfiles;
230
+ stoppedChildren = snapshot.notice ?? null;
200
231
  subagentParent = true;
201
232
  }
202
233
  if (planRun && event.messageType === 'user') {
@@ -260,6 +291,51 @@ export default async function (pi, planRun, getSession) {
260
291
  : '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.';
261
292
  return { message: { customType: 'xingular-initial-bash', content: instruction, display: false, details: {} } };
262
293
  });
294
+ // A second handler, so the notice joins any initial-Bash message rather than replacing it.
295
+ pi.on('before_agent_start', async () => {
296
+ const notice = stoppedChildren;
297
+ stoppedChildren = null;
298
+ if (notice)
299
+ return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
300
+ });
301
+ // A new user turn starts with an empty todo. The previous list is context, not this run's list.
302
+ // A substantive first turn also gets a provisional name when the terminal policy allows it.
303
+ pi.on('before_agent_start', async (event, ctx) => {
304
+ if (event.messageType !== 'user')
305
+ return;
306
+ const branch = ctx.sessionManager.getBranch?.() ?? [];
307
+ const hasUser = branch.some((entry) => entry.type === 'message' && entry.message?.role === 'user');
308
+ let name = typeof pi.getSessionName === 'function' ? pi.getSessionName() ?? null : null;
309
+ if (!name && !autoNameTried && pi.setSessionName) {
310
+ const candidate = deriveSessionName(event.prompt);
311
+ if (candidate) {
312
+ autoNameTried = true;
313
+ try {
314
+ const policy = await controlCall('terminal.policy', { tool: SESSION_NAME, args: { name: candidate } });
315
+ if (policy.allowed && !policy.requiresApproval) {
316
+ pi.setSessionName(candidate);
317
+ name = candidate;
318
+ }
319
+ }
320
+ catch {
321
+ // Session naming is metadata; a refused or failed name never fails the turn.
322
+ }
323
+ }
324
+ }
325
+ const state = {
326
+ name,
327
+ todo: [],
328
+ ...(lastTodo.length ? { previousTodo: lastTodo } : {}),
329
+ firstTurn: !hasUser
330
+ };
331
+ lastTodo = [];
332
+ const active = typeof pi.getActiveTools === 'function' ? pi.getActiveTools() : null;
333
+ sessionState = sessionStateText(state, {
334
+ name: active ? active.includes(SESSION_NAME) : true,
335
+ todo: active ? active.includes(TODO_WRITE) : true
336
+ });
337
+ stateDelivered = false;
338
+ });
263
339
  const deliverChildren = async (event, ctx, settle) => {
264
340
  if (!nativeSubagents || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
265
341
  return;
@@ -421,6 +497,10 @@ export default async function (pi, planRun, getSession) {
421
497
  clearInterval(timer);
422
498
  timer = null;
423
499
  context = null;
500
+ sessionState = null;
501
+ stateDelivered = false;
502
+ autoNameTried = false;
503
+ lastTodo = [];
424
504
  // This clears the controller's readiness while the next native session binds. The
425
505
  // process-level control claim survives replacement; only the session poller stops.
426
506
  await terminalCall('shutdown').catch(() => { });
@@ -437,16 +517,19 @@ export default async function (pi, planRun, getSession) {
437
517
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
438
518
  }
439
519
  });
440
- // The checklist is recorded on the harness run; the shared tool validates and the socket carries the event.
520
+ // The todo list is recorded on the harness run; the shared tool validates and the socket carries the event.
441
521
  let recorded = Promise.resolve();
442
- const checklist = checklistTool({ emit: (_type, payload) => { recorded = terminalCall('checklist', payload); } });
522
+ const todo = todoTool({ emit: (_type, payload) => { recorded = terminalCall('todo', payload); } });
443
523
  pi.registerTool({
444
- name: checklist.name, label: checklist.label, description: checklist.description,
445
- parameters: Type.Unsafe(checklist.parameters(Type)),
524
+ name: todo.name, label: todo.label, description: todo.description,
525
+ parameters: Type.Unsafe(todo.parameters(Type)),
446
526
  async execute(_id, input) {
447
527
  recorded = Promise.resolve();
448
- const result = await checklist.execute(input);
528
+ const result = await todo.execute(input);
449
529
  await recorded;
530
+ const items = input.items;
531
+ if (Array.isArray(items))
532
+ lastTodo = items;
450
533
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
451
534
  }
452
535
  });
@@ -5,10 +5,13 @@ export interface TerminalSubagentProfile {
5
5
  name: string;
6
6
  color: string;
7
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;
8
10
  export interface TerminalSubagentRun {
9
11
  id: string;
10
12
  parentRunId: string | null;
11
- relation: 'subagent';
13
+ /** `main` is the parent run itself; every other row is a child or nested child. */
14
+ relation: 'main' | 'subagent';
12
15
  /** The short name the parent gave the child (PROP-0030). */
13
16
  title: string;
14
17
  goal: string | null;
@@ -27,8 +30,8 @@ export interface TerminalSubagentActivity extends TerminalSubagentRun {
27
30
  activity: string;
28
31
  /** Cumulative run usage only; provider reasoning summaries and request details stay private. */
29
32
  usage: TerminalSubagentUsage | null;
30
- /** How far the child's own checklist is: counts only, never the items. */
31
- checklist: {
33
+ /** How far the child's own todo is: counts only, never the items. */
34
+ todo: {
32
35
  done: number;
33
36
  total: number;
34
37
  } | null;
@@ -41,7 +44,17 @@ export declare class SubagentMonitor {
41
44
  private parentRunId;
42
45
  private readonly activities;
43
46
  begin(parentRunId: string): void;
44
- 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. */
45
58
  snapshot(parentRunId: string): TerminalSubagentActivity[];
46
59
  clear(parentRunId?: string): void;
47
60
  }
@@ -1,4 +1,6 @@
1
1
  import { usagePayloadSchema } from "../sessions/contracts.js";
2
+ /** The main agent's row: the parent run itself, shown by name rather than by a profile. */
3
+ export const MAIN_PROFILE = { name: 'main', color: 'main' };
2
4
  export const ACTIVE_SUBAGENT_STATUSES = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
3
5
  export function terminalSubagentDuration(activity, now = Date.now()) {
4
6
  const start = Date.parse(activity.startedAt);
@@ -50,8 +52,11 @@ export class SubagentMonitor {
50
52
  this.parentRunId = parentRunId;
51
53
  this.activities.clear();
52
54
  }
55
+ /** Folds one event of the tracked parent (`main`) or one of its descendants; `null` seeds the row. */
53
56
  observe(parentRunId, run, event) {
54
- if (this.parentRunId !== parentRunId || run.relation !== 'subagent')
57
+ if (this.parentRunId !== parentRunId)
58
+ return;
59
+ if ((run.relation === 'main') !== (run.id === parentRunId))
55
60
  return;
56
61
  const row = this.activities.get(run.id) ?? {
57
62
  id: run.id,
@@ -63,7 +68,7 @@ export class SubagentMonitor {
63
68
  startedAt: run.startedAt,
64
69
  endedAt: run.endedAt,
65
70
  activity: statusActivity(run.status),
66
- checklist: null,
71
+ todo: null,
67
72
  profile: run.profile ?? null,
68
73
  accountLabel: run.accountLabel ?? null,
69
74
  model: run.model ?? null,
@@ -86,8 +91,11 @@ export class SubagentMonitor {
86
91
  row.status = run.status;
87
92
  row.startedAt = run.startedAt;
88
93
  row.endedAt = run.endedAt;
89
- const payload = payloadOf(event);
90
- if (event.type === 'tool.started') {
94
+ const payload = event ? payloadOf(event) : {};
95
+ if (!event) {
96
+ // A seeded row keeps what it already folded.
97
+ }
98
+ else if (event.type === 'tool.started') {
91
99
  const toolCallId = stringOf(payload.toolCallId);
92
100
  const name = stringOf(payload.name) ?? 'Tool';
93
101
  if (toolCallId)
@@ -125,22 +133,35 @@ export class SubagentMonitor {
125
133
  };
126
134
  }
127
135
  }
128
- else if (event.type === 'checklist.updated') {
136
+ else if (event.type === 'todo.updated' || event.type === 'checklist.updated') {
129
137
  const items = Array.isArray(payload.items) ? payload.items : [];
130
- row.checklist = items.length ? { done: items.filter(item => item?.status === 'completed').length, total: items.length } : null;
138
+ row.todo = items.length ? { done: items.filter(item => item?.status === 'completed').length, total: items.length } : null;
131
139
  }
132
140
  if (run.status !== 'running')
133
141
  row.tools.clear();
134
142
  row.activity = run.status === 'running'
135
- ? latestTool(row.tools) ?? row.lastTool?.text ?? (event.type === 'message.delta' ? 'Responding' : 'Working')
143
+ ? latestTool(row.tools) ?? row.lastTool?.text ?? (event?.type === 'message.delta' || (!event && row.activity === 'Responding') ? 'Responding' : 'Working')
136
144
  : statusActivity(run.status);
137
145
  this.activities.set(run.id, row);
138
146
  }
147
+ /**
148
+ * Rebuilds the rows of a parent run that is no longer live, such as the latest one of a reopened
149
+ * conversation, by folding each run's saved events. Rows keep their final status.
150
+ */
151
+ restore(parentRunId, runs) {
152
+ this.begin(parentRunId);
153
+ for (const { run, events } of runs) {
154
+ this.observe(parentRunId, run, null);
155
+ for (const event of events)
156
+ this.observe(parentRunId, run, event);
157
+ }
158
+ }
159
+ /** The main row first, then children in start order. */
139
160
  snapshot(parentRunId) {
140
161
  if (this.parentRunId !== parentRunId)
141
162
  return [];
142
163
  return [...this.activities.values()]
143
- .sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
164
+ .sort((left, right) => Number(right.relation === 'main') - Number(left.relation === 'main') || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
144
165
  .map(({ tools: _tools, lastTool: _lastTool, ...activity }) => activity);
145
166
  }
146
167
  clear(parentRunId) {
@@ -11,6 +11,7 @@ import type { CallerContext } from "../core/caller.js";
11
11
  import { type VariantChoice } from "../variant/materialise.js";
12
12
  import type { VariantTrust } from "../variant/trust.js";
13
13
  import type { AttachParams, PluginScope } from "../plugins/api.js";
14
+ import type { Proposal } from "../plugins/proposals.js";
14
15
  import type { Plugins } from "../plugins/plugins.js";
15
16
  import type { Plugin } from "../plugins/types.js";
16
17
  import type { Hub } from "../hub/hub.js";
@@ -131,6 +132,12 @@ export interface PluginControl {
131
132
  remove?(params: PluginScope & {
132
133
  pluginId: string;
133
134
  }): Promise<Plugin[]>;
135
+ proposals?: {
136
+ list(): Proposal[] | Promise<Proposal[]>;
137
+ preview(id: string): Proposal | Promise<Proposal>;
138
+ accept(id: string, hash: string): Proposal | Promise<Proposal>;
139
+ reject(id: string, hash: string): Proposal | Promise<Proposal>;
140
+ };
134
141
  }
135
142
  export declare class Terminals {
136
143
  private readonly options;
@@ -138,6 +145,8 @@ export declare class Terminals {
138
145
  private readonly diagnosticOwners;
139
146
  /** The variant each live session runs, for loading its skills. */
140
147
  private readonly variantDirs;
148
+ /** The subagent profile files each live session's packages bring, chosen when it started. */
149
+ private readonly packageProfiles;
141
150
  /** The world tools each session was given, so a call can only reach one of them. */
142
151
  private readonly worldNames;
143
152
  private readonly worldReadOnly;
@@ -150,6 +159,13 @@ export declare class Terminals {
150
159
  /** Each child's profile name and colour, looked up once per child run. */
151
160
  private readonly childProfiles;
152
161
  private subagentProfile;
162
+ /** Account, model and effort for a monitor row, from the run record. */
163
+ private monitorRuntime;
164
+ /**
165
+ * Shows the reopened conversation's latest parent run again: the last owner on its native branch
166
+ * that ran in this folder, with every descendant flat, as each ended.
167
+ */
168
+ private hydrateMonitor;
153
169
  constructor(options: TerminalsOptions);
154
170
  /** The commands a terminal session finds on PATH. Rewritten only when they change. */
155
171
  private ensureCommands;