@xingular/harness 1.3.1-unstable.0 → 1.3.1-unstable.2

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 (116) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +16 -12
  3. package/builtin/skills/plugin-authoring/SKILL.md +34 -0
  4. package/dist/cli.js +2334 -1715
  5. package/dist/sdk/agents/profiles.d.ts +13 -4
  6. package/dist/sdk/agents/profiles.js +59 -7
  7. package/dist/sdk/annotations/bridge.d.ts +2 -0
  8. package/dist/sdk/annotations/bridge.js +4 -0
  9. package/dist/sdk/annotations/capture.d.ts +44 -2
  10. package/dist/sdk/annotations/capture.js +104 -3
  11. package/dist/sdk/annotations/delivery.d.ts +24 -17
  12. package/dist/sdk/annotations/delivery.js +82 -49
  13. package/dist/sdk/annotations/inspect.js +7 -6
  14. package/dist/sdk/annotations/pageScript.js +4 -4
  15. package/dist/sdk/annotations/part.d.ts +0 -2
  16. package/dist/sdk/annotations/part.js +4 -33
  17. package/dist/sdk/annotations/queue.d.ts +2 -2
  18. package/dist/sdk/annotations/queue.js +12 -7
  19. package/dist/sdk/annotations/serverMethods.js +11 -7
  20. package/dist/sdk/annotations/store.d.ts +13 -21
  21. package/dist/sdk/annotations/store.js +47 -37
  22. package/dist/sdk/commands/list.js +2 -0
  23. package/dist/sdk/core/links.d.ts +51 -0
  24. package/dist/sdk/core/links.js +155 -0
  25. package/dist/sdk/harness.d.ts +5 -0
  26. package/dist/sdk/harness.js +11 -6
  27. package/dist/sdk/index.d.ts +1 -0
  28. package/dist/sdk/index.js +1 -0
  29. package/dist/sdk/plugins/builtin.js +11 -6
  30. package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
  31. package/dist/sdk/plugins/builtinRegistry.js +9 -3
  32. package/dist/sdk/plugins/discover.d.ts +7 -1
  33. package/dist/sdk/plugins/discover.js +41 -2
  34. package/dist/sdk/plugins/identity.js +1 -1
  35. package/dist/sdk/plugins/inventory.js +1 -1
  36. package/dist/sdk/plugins/manifest.d.ts +2 -0
  37. package/dist/sdk/plugins/manifest.js +21 -4
  38. package/dist/sdk/plugins/plugins.d.ts +2 -0
  39. package/dist/sdk/plugins/plugins.js +1 -0
  40. package/dist/sdk/plugins/proposals.d.ts +17 -0
  41. package/dist/sdk/plugins/proposals.js +147 -0
  42. package/dist/sdk/plugins/types.d.ts +1 -1
  43. package/dist/sdk/plugins/types.js +1 -1
  44. package/dist/sdk/prompt/compile.d.ts +11 -2
  45. package/dist/sdk/prompt/compile.js +32 -4
  46. package/dist/sdk/prompt/standard.d.ts +1 -1
  47. package/dist/sdk/prompt/standard.js +1 -1
  48. package/dist/sdk/server/methods.js +10 -0
  49. package/dist/sdk/sessions/sessionState.d.ts +13 -0
  50. package/dist/sdk/sessions/sessionState.js +31 -0
  51. package/dist/sdk/sessions/sessions.d.ts +7 -0
  52. package/dist/sdk/sessions/sessions.js +45 -5
  53. package/dist/sdk/sessions/subagentService.js +3 -3
  54. package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
  55. package/dist/sdk/terminal/opentui/theme.js +1 -0
  56. package/dist/sdk/terminal/piExtension.js +71 -2
  57. package/dist/sdk/terminal/terminals.d.ts +9 -0
  58. package/dist/sdk/terminal/terminals.js +46 -14
  59. package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
  60. package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
  61. package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
  62. package/dist/sdk/terminal/tui/annotateSend.js +40 -0
  63. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  64. package/dist/sdk/terminal/tui/commandPages.js +2 -7
  65. package/dist/sdk/terminal/tui/commands.js +4 -2
  66. package/dist/sdk/terminal/tui/dialogs.js +1 -0
  67. package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
  68. package/dist/sdk/terminal/tui/keybindings.js +1 -1
  69. package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
  70. package/dist/sdk/terminal/tui/pageOverlay.js +12 -1
  71. package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
  72. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
  73. package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
  74. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +6 -0
  75. package/dist/sdk/terminal/tui/pages/pluginsPage.js +99 -1
  76. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
  77. package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
  78. package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
  79. package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
  80. package/dist/sdk/tools/defaultSkills.d.ts +4 -0
  81. package/dist/sdk/tools/defaultSkills.js +20 -0
  82. package/dist/sdk/tools/definitions.d.ts +3 -1
  83. package/dist/sdk/tools/definitions.js +17 -1
  84. package/dist/sdk/tools/dispatch.js +12 -0
  85. package/dist/sdk/tools/harnessTools.js +1 -1
  86. package/dist/sdk/tools/sessionName.d.ts +7 -0
  87. package/dist/sdk/tools/sessionName.js +53 -8
  88. package/dist/sdk/world/world.d.ts +0 -2
  89. package/dist/terminal-opentui.js +2019 -1269
  90. package/dist/terminal-pi.js +106 -7
  91. package/docs/README.md +3 -0
  92. package/docs/architecture/02-sessions-and-runs.md +11 -5
  93. package/docs/architecture/03-variants.md +1 -3
  94. package/docs/architecture/04-prompt.md +16 -7
  95. package/docs/architecture/06-plugins-and-capabilities.md +36 -4
  96. package/docs/architecture/07-tools-and-gate.md +20 -5
  97. package/docs/architecture/10-server-protocol.md +52 -7
  98. package/docs/architecture/11-terminal.md +50 -19
  99. package/docs/architecture/12-storage.md +1 -0
  100. package/docs/architecture/13-world-tools.md +8 -6
  101. package/docs/architecture/README.md +3 -1
  102. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
  103. package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
  104. package/docs/architecture/proposals/README.md +1 -0
  105. package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
  106. package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
  107. package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
  108. package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
  109. package/docs/parity.md +7 -4
  110. package/docs/releases/1.3.1-unstable.1.md +8 -0
  111. package/docs/releases/1.3.1-unstable.2.md +11 -0
  112. package/docs/releases/README.md +7 -4
  113. package/docs/terminal-tui.md +62 -23
  114. package/package.json +1 -1
  115. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
  116. package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
@@ -18,15 +18,17 @@ import { FIRST_PARTY_PLUGINS, firstPartyExtensions } from "../plugins/firstParty
18
18
  import { imagePruning } from "../world/images.js";
19
19
  import { materialise } from "../variant/materialise.js";
20
20
  import { ASK_USER } from "../tools/askUser.js";
21
+ import { defaultSkillPaths } from "../tools/defaultSkills.js";
21
22
  import { TODO_WRITE } from "../tools/todo.js";
22
23
  import { CHECKPOINT_WRITE, checkpointOf } from "../tools/checkpoint.js";
23
- import { SESSION_NAME, sessionNameTool } from "../tools/sessionName.js";
24
+ import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
24
25
  import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
25
26
  import { GET_CAPABILITIES, getCapabilitiesTool } from "../tools/getCapabilities.js";
26
27
  import { isActiveStatus } from "./contracts.js";
27
28
  import { SPAWN_SUBAGENT, WAIT_SUBAGENTS, subagentDepth, subagentHistory, subagentNesting } from "./subagents.js";
28
29
  import { selectHistory } from "./history.js";
29
30
  import { runLineageJoin } from "./lineageJoin.js";
31
+ import { latestSessionName } from "./sessionState.js";
30
32
  import { compactionCheckpoint, COMPACTION_CHECKPOINT_EXTENSION } from "./compactionCheckpoint.js";
31
33
  import { createSliceState, parseWorkSlice, sliceOf, workSliceGuard, WorkSliceError, WORK_SLICE_EXTENSION } from "./workSlice.js";
32
34
  import { piSessionFactory, runTurn, summarize } from "./runner.js";
@@ -468,6 +470,28 @@ export class Sessions {
468
470
  updateRun(root, runId, (record) => Object.assign(record, { status: 'failed', endedAt: now(), terminalReason: message }));
469
471
  log(`session ${runId} did not start: ${message}`, code);
470
472
  };
473
+ // Plugins are resolved once per run: for the profiles the prompt lists, then for what Pi loads.
474
+ const { plugins } = this.options;
475
+ let pluginLoading = null;
476
+ const loadPlugins = plugins
477
+ ? () => (pluginLoading ??= plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }))
478
+ : null;
479
+ // A person's main session sees its current name and whether this run's todo is empty. A
480
+ // command, child, side question or unattended run has no such state to show.
481
+ const stateEligible = !input.command
482
+ && !input.run.relation
483
+ && (input.depth ?? 0) === 0
484
+ && !input.automatic
485
+ && (input.run.trigger == null || input.run.trigger === 'user');
486
+ const existingName = stateEligible ? latestSessionName(historyFile) : null;
487
+ const sessionState = stateEligible
488
+ ? {
489
+ name: existingName,
490
+ todo: [],
491
+ ...(continuation?.todo.length ? { previousTodo: continuation.todo } : {}),
492
+ firstTurn: !historyFile
493
+ }
494
+ : undefined;
471
495
  let system;
472
496
  let update;
473
497
  try {
@@ -483,14 +507,17 @@ export class Sessions {
483
507
  contributions: capabilities.contributions,
484
508
  descriptors: BUILTIN_PLUGIN_REGISTRY.descriptors()
485
509
  } : undefined;
510
+ if (capabilities.tools.includes(SPAWN_SUBAGENT) && loadPlugins && !input.automatic)
511
+ input.packageProfiles = (await loadPlugins()).profilePaths;
486
512
  const profiles = capabilities.tools.includes(SPAWN_SUBAGENT)
487
- ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true, researchContext)
513
+ ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true, researchContext, input.packageProfiles ?? [])
488
514
  : null;
489
515
  if (profiles && variant)
490
516
  variant = { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) };
491
517
  const own = [plan, resumed, profiles].filter((section) => section !== null);
492
518
  const effectiveLaunch = {
493
519
  ...launch,
520
+ ...(sessionState ? { sessionState } : {}),
494
521
  sections: own.length ? [...(launch.sections ?? []).filter((section) => !own.some((entry) => entry.id === section.id)), ...own] : launch.sections,
495
522
  tools: capabilities.tools,
496
523
  connectionTools: (launch.connectionTools ?? []).filter((name) => capabilities.tools.includes(name)),
@@ -516,7 +543,6 @@ export class Sessions {
516
543
  // What every session has, after the tools the caller switched on and in a fixed order.
517
544
  const allow = capabilities.tools;
518
545
  const session = { run, caller, allow, depth, cwd: input.cwd, capabilities };
519
- const { plugins } = this.options;
520
546
  let pi;
521
547
  const naming = sessionNameTool(() => {
522
548
  const current = pi;
@@ -598,7 +624,7 @@ export class Sessions {
598
624
  preamble: input.preamble ?? null,
599
625
  automatic: input.automatic,
600
626
  account: context,
601
- resources: { ids: [], skillPaths: variant?.skillPaths ?? [], promptPaths: [] },
627
+ resources: { ids: [], skillPaths: [...(variant?.skillPaths ?? []), ...defaultSkillPaths(variant?.dir)], promptPaths: [] },
602
628
  variantExtensions: variant?.extensionPaths ?? [],
603
629
  inline: [
604
630
  ...firstPartyFor(root, capabilities.contributions),
@@ -608,7 +634,7 @@ export class Sessions {
608
634
  { name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialTodo: continuation?.todo, planInstructions: checkpointPlanInstructions }) },
609
635
  ...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable }) }] : [])
610
636
  ],
611
- ...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
637
+ ...(plugins && loadPlugins ? { plugins: { loading: loadPlugins, loaded: (extensions) => plugins.loaded(extensions) } } : {}),
612
638
  system,
613
639
  ...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
614
640
  // A command sends no request of its own, so the context update waits for the next turn.
@@ -635,6 +661,20 @@ export class Sessions {
635
661
  run.finish('failed', { code: 'native_resume_unavailable', message: 'The saved conversation could not be restored.' });
636
662
  throw new HarnessError('native_resume_unavailable', 'The saved conversation could not be restored.');
637
663
  }
664
+ // The fallback is a name, not a task step: a refused or failed name never fails the run.
665
+ if (stateEligible && input.autoName !== false && !existingName && !pi.sessionName
666
+ && pi.setSessionName && capabilities.tools.includes(SESSION_NAME)) {
667
+ const name = deriveSessionName(input.text);
668
+ if (name) {
669
+ try {
670
+ if (await gate.check(SESSION_NAME, { name }) === null)
671
+ pi.setSessionName(name);
672
+ }
673
+ catch {
674
+ // Session naming is metadata; leave the session unnamed.
675
+ }
676
+ }
677
+ }
638
678
  const opened = pi;
639
679
  const openedModel = pi.model?.id && pi.model.provider ? `${pi.model.provider}/${pi.model.id}` : null;
640
680
  updateRun(root, runId, (record) => {
@@ -122,7 +122,7 @@ export class Subagents {
122
122
  if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some(name => typeof name !== 'string'))) {
123
123
  return { refusal: 'The optional tools field must be an array of tool names. Omit it to inherit the selected profile’s safe research intersection.' };
124
124
  }
125
- const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null);
125
+ const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null, parent.input.packageProfiles);
126
126
  if (!profile)
127
127
  return { refusal: `There is no agent profile named ${request.agent.trim()}.` };
128
128
  const eligibility = planResearchEligibility(profile, {
@@ -169,7 +169,7 @@ export class Subagents {
169
169
  if (request.agent !== undefined) {
170
170
  if (typeof request.agent !== 'string' || !request.agent.trim())
171
171
  throw invalid('agent must be the name of an agent profile.');
172
- profile = resolveProfile(this.env.root, input.cwd, request.agent.trim(), input.variant && 'dir' in input.variant ? input.variant.dir : null);
172
+ profile = resolveProfile(this.env.root, input.cwd, request.agent.trim(), input.variant && 'dir' in input.variant ? input.variant.dir : null, input.packageProfiles);
173
173
  if (!profile)
174
174
  throw invalid(`There is no agent profile named ${request.agent.trim()}.`);
175
175
  }
@@ -306,7 +306,7 @@ export class Subagents {
306
306
  throw invalid(`Subagent ${latest.id} needs capabilities you do not have in this run: ${missing.join(', ')}. Start a new subagent instead.`);
307
307
  let profile = null;
308
308
  if (latest.profile)
309
- profile = resolveProfile(root, input.cwd, latest.profile, input.variant && 'dir' in input.variant ? input.variant.dir : null);
309
+ profile = resolveProfile(root, input.cwd, latest.profile, input.variant && 'dir' in input.variant ? input.variant.dir : null, input.packageProfiles);
310
310
  if (input.plan) {
311
311
  // A plan run continues only a research child whose pinned tools still qualify.
312
312
  const check = latest.profile ? this.planSpawnCheck(request.parentRunId, { agent: latest.profile }) : { refusal: 'Plan mode can continue only a stopped research subagent that has a named profile.' };
@@ -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',
@@ -9,14 +9,15 @@ 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 { todoTool } from "../tools/todo.js";
12
+ import { TODO_WRITE, todoTool } from "../tools/todo.js";
13
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";
@@ -61,6 +62,10 @@ export default async function (pi, planRun, getSession) {
61
62
  const fileTools = new Map();
62
63
  const writeAuthorizations = new Map();
63
64
  let workspaceRoot = null;
65
+ let sessionState = null;
66
+ let stateDelivered = false;
67
+ let autoNameTried = false;
68
+ let lastTodo = [];
64
69
  // The harness's own tools for this session (connected apps and what the application offers).
65
70
  let stage = 'Tool discovery';
66
71
  try {
@@ -139,6 +144,21 @@ export default async function (pi, planRun, getSession) {
139
144
  pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
140
145
  pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
141
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
+ });
142
162
  compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
143
163
  // A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
144
164
  // person decides whether to continue.
@@ -154,6 +174,10 @@ export default async function (pi, planRun, getSession) {
154
174
  });
155
175
  pi.on('session_start', async (_event, ctx) => {
156
176
  context = ctx;
177
+ sessionState = null;
178
+ stateDelivered = false;
179
+ autoNameTried = false;
180
+ lastTodo = [];
157
181
  if (timer)
158
182
  clearInterval(timer);
159
183
  // The branch's parent runs let the controller show the latest one's rows again on reopen.
@@ -274,6 +298,44 @@ export default async function (pi, planRun, getSession) {
274
298
  if (notice)
275
299
  return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
276
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
+ });
277
339
  const deliverChildren = async (event, ctx, settle) => {
278
340
  if (!nativeSubagents || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
279
341
  return;
@@ -435,6 +497,10 @@ export default async function (pi, planRun, getSession) {
435
497
  clearInterval(timer);
436
498
  timer = null;
437
499
  context = null;
500
+ sessionState = null;
501
+ stateDelivered = false;
502
+ autoNameTried = false;
503
+ lastTodo = [];
438
504
  // This clears the controller's readiness while the next native session binds. The
439
505
  // process-level control claim survives replacement; only the session poller stops.
440
506
  await terminalCall('shutdown').catch(() => { });
@@ -461,6 +527,9 @@ export default async function (pi, planRun, getSession) {
461
527
  recorded = Promise.resolve();
462
528
  const result = await todo.execute(input);
463
529
  await recorded;
530
+ const items = input.items;
531
+ if (Array.isArray(items))
532
+ lastTodo = items;
464
533
  return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
465
534
  }
466
535
  });
@@ -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;
@@ -17,6 +17,7 @@ import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
17
17
  import { resolveWorkspaceRoot } from "../core/workspace.js";
18
18
  import { prependSearchPath, searchPath } from "../core/path.js";
19
19
  import { dispatch } from "../tools/dispatch.js";
20
+ import { defaultSkillPaths } from "../tools/defaultSkills.js";
20
21
  import { materialise } from "../variant/materialise.js";
21
22
  import * as componentLibrary from "../variant/components.js";
22
23
  import * as variantStore from "../variant/store.js";
@@ -50,7 +51,7 @@ function resourceIdentity(loading) {
50
51
  return 'null';
51
52
  return JSON.stringify({
52
53
  extensions: loading.extensionPaths, builtins: loading.builtins, skills: loading.skillPaths,
53
- prompts: loading.promptPaths, themes: loading.themePaths, detached: loading.detached,
54
+ prompts: loading.promptPaths, themes: loading.themePaths, profiles: loading.profilePaths, detached: loading.detached,
54
55
  project: loading.projectCode, packages: loading.packageCode
55
56
  });
56
57
  }
@@ -72,6 +73,8 @@ export class Terminals {
72
73
  diagnosticOwners = new Map();
73
74
  /** The variant each live session runs, for loading its skills. */
74
75
  variantDirs = new Map();
76
+ /** The subagent profile files each live session's packages bring, chosen when it started. */
77
+ packageProfiles = new Map();
75
78
  /** The world tools each session was given, so a call can only reach one of them. */
76
79
  worldNames = new Map();
77
80
  worldReadOnly = new Set();
@@ -358,6 +361,19 @@ export class Terminals {
358
361
  return params[name];
359
362
  };
360
363
  switch (call) {
364
+ case 'plugins.proposals.list':
365
+ if (!api.proposals)
366
+ throw invalid('Proposal review is unavailable in this host.');
367
+ return api.proposals.list();
368
+ case 'plugins.proposals.preview':
369
+ if (!api.proposals)
370
+ throw invalid('Proposal review is unavailable in this host.');
371
+ return api.proposals.preview(text('id'));
372
+ case 'plugins.proposals.accept':
373
+ case 'plugins.proposals.reject':
374
+ if (!api.proposals)
375
+ throw invalid('Proposal review is unavailable in this host.');
376
+ return api.proposals[call === 'plugins.proposals.accept' ? 'accept' : 'reject'](text('id'), text('hash'));
361
377
  case 'plugins.attach':
362
378
  case 'plugins.detach':
363
379
  return api.attach({ ...scope, id: text('id'), scope: params.everywhere === true || !projectId ? {} : { projectId } }, call === 'plugins.attach');
@@ -481,7 +497,7 @@ export class Terminals {
481
497
  return params.scope;
482
498
  };
483
499
  switch (call.slice('terminal.agents.'.length)) {
484
- case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null), dirs: profileDirs(root, cwd) };
500
+ case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null, this.packageProfiles.get(live.input.sessionId)), dirs: profileDirs(root, cwd) };
485
501
  case 'create': return createProfile(root, cwd, { name: name(), scope: scope(), from: typeof params.from === 'string' && params.from ? params.from : null });
486
502
  case 'remove':
487
503
  removeProfile(root, cwd, { name: name(), scope: scope() });
@@ -582,6 +598,9 @@ export class Terminals {
582
598
  if (call === 'terminal.usage.limits') {
583
599
  return params.refresh === true ? limits.refresh(live.account.id, { force: true }) : limits.read(live.account.id);
584
600
  }
601
+ if (call === 'terminal.usage.allLimits') {
602
+ return params.refresh === true ? limits.refresh() : limits.read();
603
+ }
585
604
  if (call === 'terminal.usage.headers') {
586
605
  if (typeof params.provider !== 'string' || !params.provider)
587
606
  throw invalid('A provider is required.');
@@ -636,14 +655,14 @@ export class Terminals {
636
655
  case 'read': return pageNotes.read(root, id());
637
656
  case 'create': {
638
657
  const note = pageNotes.create(root, { source: params.source, target: params.target, body: params.body, tags: params.tags });
639
- return params.share === true ? pageNotes.share(root, note.id, { sessionId }) : note;
658
+ return params.share === true ? needNotes().share(note.id, undefined, sessionId) : note;
640
659
  }
641
660
  case 'update': return pageNotes.update(root, id(), version(), (params.patch ?? {}));
642
661
  case 'delete':
643
662
  pageNotes.remove(root, id(), version());
644
663
  return {};
645
664
  // A terminal shares with, and withdraws from, its own session only.
646
- case 'share': return pageNotes.share(root, id(), { sessionId });
665
+ case 'share': return needNotes().share(id(), undefined, sessionId);
647
666
  case 'unshare': return pageNotes.unshare(root, id(), { sessionId });
648
667
  case 'open': return needCapture().open(sessionId, typeof params.url === 'string' && params.url ? params.url : null);
649
668
  case 'identity': return needCapture().identity(sessionId);
@@ -662,7 +681,7 @@ export class Terminals {
662
681
  const note = pageNotes.read(root, id());
663
682
  return needCapture().reveal(sessionId, note.source.url, note.target);
664
683
  }
665
- // ---- The inspector and the queue (PROP-0018); a terminal queues and sends to its own session.
684
+ // ---- The inspector and sending (PROP-0018, PROP-0042); a terminal shares with and sends to its own session.
666
685
  case 'target': return { ...needCapture().describe(sessionId), windows: await needCapture().windows(sessionId).catch(() => []) };
667
686
  case 'attach': {
668
687
  const kind = params.kind;
@@ -684,10 +703,18 @@ export class Terminals {
684
703
  await needCapture().stopInspect(sessionId);
685
704
  return {};
686
705
  case 'next': return inspectStep(needCapture(), needNotes(), sessionId, sessionId, 'terminal');
687
- case 'queue': return needNotes().queue(id(), version(), sessionId);
688
- case 'dequeue': return needNotes().dequeue(id(), version());
689
706
  case 'confirm': return needNotes().confirm(id(), version());
690
- case 'send': return needNotes().send(sessionId, { ids: Array.isArray(params.ids) ? params.ids.filter((entry) => typeof entry === 'string') : null });
707
+ case 'confirmAll': return { notes: needNotes().confirmAll(sessionId) };
708
+ case 'clear': return { notes: needNotes().clear(sessionId) };
709
+ case 'send': return needNotes().send(sessionId, {
710
+ ids: Array.isArray(params.ids) ? params.ids.filter((entry) => typeof entry === 'string') : null,
711
+ prompt: typeof params.prompt === 'string' ? params.prompt : null
712
+ });
713
+ case 'discover': return { apps: await needCapture().discover() };
714
+ case 'launch':
715
+ if (typeof params.path !== 'string' || !params.path.trim())
716
+ throw invalid('Give the path of the app to start.');
717
+ return needCapture().launchApp(sessionId, params.path, Array.isArray(params.args) ? params.args.filter((entry) => typeof entry === 'string') : []);
691
718
  default:
692
719
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
693
720
  }
@@ -886,11 +913,12 @@ export class Terminals {
886
913
  }
887
914
  if (call === 'terminal.subagents.tools') {
888
915
  const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
916
+ const packaged = this.packageProfiles.get(live.input.sessionId) ?? [];
889
917
  return {
890
918
  tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })),
891
919
  profiles: {
892
- normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
893
- plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true).body
920
+ normal: profileSection(this.options.env.root, live.input.cwd, variantDir, false, undefined, packaged).body,
921
+ plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true, undefined, packaged).body
894
922
  }
895
923
  };
896
924
  }
@@ -912,13 +940,14 @@ export class Terminals {
912
940
  const caller = { ...live.caller, runId: run.runId };
913
941
  const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
914
942
  const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
943
+ const packaged = this.packageProfiles.get(live.input.sessionId) ?? [];
915
944
  const subagentProfiles = {
916
- normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
945
+ normal: profileSection(this.options.env.root, live.input.cwd, variantDir, false, undefined, packaged).body,
917
946
  plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true, {
918
947
  frozenTools: capabilities.tools,
919
948
  contributions: capabilities.contributions,
920
949
  descriptors: HARNESS_PLUGINS
921
- }).body
950
+ }, packaged).body
922
951
  };
923
952
  freezeRun(this.options.env.root, run.runId, caller, capabilities);
924
953
  if (!service || live.input.subagents === null)
@@ -932,6 +961,7 @@ export class Terminals {
932
961
  connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
933
962
  plan: params.plan === true,
934
963
  variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
964
+ packageProfiles: packaged,
935
965
  capabilities
936
966
  }, live.account.id, capabilities);
937
967
  const history = params.history ?? { owners: [], receipts: [] };
@@ -1097,11 +1127,12 @@ export class Terminals {
1097
1127
  plugins: () => this.options.pluginControl?.list({ cwd: input.cwd, accountId: preparedAccount.id, projectId: input.projectId ?? null }) ?? Promise.resolve([]) });
1098
1128
  if (variant)
1099
1129
  this.variantDirs.set(sessionId, variant.dir);
1130
+ this.packageProfiles.set(sessionId, loading?.profilePaths ?? []);
1100
1131
  const worldTools = await this.worldToolNames(input, preparedAccount);
1101
1132
  const ownTools = await this.ownToolNames(input, preparedAccount);
1102
1133
  const launchTools = [...new Set([...(input.launch?.tools ?? []), TODO_WRITE, SESSION_NAME, ...ownTools, ...(broker ? apps.toolNames : []), ...worldTools])];
1103
1134
  // The profiles replace the variant's own subagent list, which they include.
1104
- const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
1135
+ const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null, false, undefined, loading?.profilePaths ?? []) : null;
1105
1136
  const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
1106
1137
  // A terminal is a person's own session: the same standard sections a host's session has.
1107
1138
  const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
@@ -1280,7 +1311,7 @@ export class Terminals {
1280
1311
  // The pi command's own extensions are not loaded: a host's session cannot have them (PROP-0039).
1281
1312
  for (const path of [...(loading?.extensionPaths ?? []), ...(variant?.extensionPaths ?? [])])
1282
1313
  args.push('--extension', path);
1283
- for (const path of [...(variant?.skillPaths ?? []), ...(loading?.skillPaths ?? [])])
1314
+ for (const path of [...(variant?.skillPaths ?? []), ...defaultSkillPaths(variant?.dir), ...(loading?.skillPaths ?? [])])
1284
1315
  args.push('--skill', path);
1285
1316
  for (const path of loading?.promptPaths ?? [])
1286
1317
  args.push('--prompt-template', path);
@@ -1311,6 +1342,7 @@ export class Terminals {
1311
1342
  this.socket.revoke(token);
1312
1343
  this.sessions.delete(sessionId);
1313
1344
  this.variantDirs.delete(sessionId);
1345
+ this.packageProfiles.delete(sessionId);
1314
1346
  this.worldNames.delete(sessionId);
1315
1347
  if (this.worktreeLeases.get(sessionId) === acquiredWorktreeId)
1316
1348
  this.worktreeLeases.delete(sessionId);
@@ -1,3 +1,7 @@
1
1
  import type { AnnotateStart } from "./pages/annotatePage.js";
2
- /** `/annotate:annotate` accepts a page address, DevTools target, or page-bridge target. */
2
+ /** A typed page address: a bare host gets https, localhost gets http. */
3
+ export declare function normalizeAddress(text: string): string;
4
+ /** A typed DevTools port or loopback address. */
5
+ export declare function normalizeDevTools(text: string): string;
6
+ /** `/annotate:` accepts a page address, an app's DevTools port, or the page bridge. */
3
7
  export declare function parseAnnotate(argument: string): AnnotateStart | string;
@@ -1,4 +1,20 @@
1
- /** `/annotate:annotate` accepts a page address, DevTools target, or page-bridge target. */
1
+ /** A typed page address: a bare host gets https, localhost gets http. */
2
+ export function normalizeAddress(text) {
3
+ const word = text.trim();
4
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(word))
5
+ return word;
6
+ return /^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(word) ? `http://${word}` : `https://${word}`;
7
+ }
8
+ /** A typed DevTools port or loopback address. */
9
+ export function normalizeDevTools(text) {
10
+ const word = text.trim();
11
+ if (/^\d+$/.test(word))
12
+ return `http://127.0.0.1:${word}`;
13
+ if (/^(localhost|127\.0\.0\.1|\[::1\]):\d+$/.test(word))
14
+ return `http://${word}`;
15
+ return word;
16
+ }
17
+ /** `/annotate:` accepts a page address, an app's DevTools port, or the page bridge. */
2
18
  export function parseAnnotate(argument) {
3
19
  const words = argument.trim().split(/\s+/).filter(Boolean);
4
20
  if (!words.length)
@@ -6,17 +22,15 @@ export function parseAnnotate(argument) {
6
22
  if (words[0] === '--app') {
7
23
  const endpoint = words[1];
8
24
  if (!endpoint)
9
- return 'Give the app\'s DevTools address: /annotate:annotate --app http://127.0.0.1:9229';
10
- const address = /^\d+$/.test(endpoint) ? `http://127.0.0.1:${endpoint}` : endpoint;
25
+ return { kind: 'apps' };
11
26
  const name = words.slice(2).join(' ');
12
- return { kind: 'cdp', endpoint: address, ...(name ? { name } : {}) };
27
+ return { kind: 'cdp', endpoint: normalizeDevTools(endpoint), ...(name ? { name } : {}) };
13
28
  }
14
29
  if (words[0] === '--bridge') {
15
30
  const name = words.slice(1).join(' ');
16
31
  return { kind: 'bridge', ...(name ? { name } : {}) };
17
32
  }
18
33
  if (words.length > 1 || words[0].startsWith('--'))
19
- return 'Use /annotate:annotate [url], /annotate:annotate --app <DevTools address>, or /annotate:annotate --bridge.';
20
- const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(words[0]) ? words[0] : /^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(words[0]) ? `http://${words[0]}` : `https://${words[0]}`;
21
- return { kind: 'web', url };
34
+ return 'Use /annotate: [url], /annotate: --app [port], or /annotate: --bridge.';
35
+ return { kind: 'web', url: normalizeAddress(words[0]) };
22
36
  }
@@ -0,0 +1,30 @@
1
+ import type { MenuCall } from "../pluginMenu.js";
2
+ import type { Note } from "../../annotations/store.js";
3
+ type SendResult = {
4
+ state: 'sent';
5
+ ids: string[];
6
+ } | {
7
+ state: 'busy';
8
+ ids: string[];
9
+ } | {
10
+ state: 'host';
11
+ ids: string[];
12
+ } | {
13
+ state: 'empty';
14
+ ids: [];
15
+ unconfirmed?: number;
16
+ };
17
+ export interface AnnotateSendUi {
18
+ confirm(title: string, message: string): Promise<boolean>;
19
+ }
20
+ /** Unsent annotations of `sessionId`, oldest first. */
21
+ export declare function unsent(notes: readonly Note[], sessionId: string | null): Note[];
22
+ /**
23
+ * Sends every unsent annotation of this terminal's session, with `prompt` after them when given.
24
+ * Returns what to tell the person.
25
+ */
26
+ export declare function sendUnsent(call: MenuCall, ui: AnnotateSendUi, options?: {
27
+ prompt?: string;
28
+ }): Promise<string>;
29
+ export declare function describeSend(result: SendResult, withPrompt?: boolean): string;
30
+ export {};
@@ -0,0 +1,40 @@
1
+ const count = (n) => `${n} annotation${n === 1 ? '' : 's'}`;
2
+ /** Unsent annotations of `sessionId`, oldest first. */
3
+ export function unsent(notes, sessionId) {
4
+ return notes
5
+ .filter((note) => note.status === 'open' && note.delivery?.state === 'pending' && note.delivery.sessionId === sessionId)
6
+ .sort((left, right) => left.delivery.queuedAt.localeCompare(right.delivery.queuedAt));
7
+ }
8
+ /**
9
+ * Sends every unsent annotation of this terminal's session, with `prompt` after them when given.
10
+ * Returns what to tell the person.
11
+ */
12
+ export async function sendUnsent(call, ui, options = {}) {
13
+ const listed = await call('terminal.annotations.list');
14
+ const waiting = unsent(listed.notes, listed.sessionId);
15
+ if (!waiting.length) {
16
+ return options.prompt
17
+ ? 'No unsent annotations. Annotate something with /annotate: first, or send your request as a normal message.'
18
+ : 'Nothing to send. Annotate something first.';
19
+ }
20
+ const unconfirmed = waiting.filter((note) => note.delivery?.confirm);
21
+ if (unconfirmed.length) {
22
+ const yes = await ui.confirm(`Send ${count(waiting.length)} to the agent?`, `${count(unconfirmed.length)} came from the app's own page scripts, which could have written them. Check that the comments are yours.`);
23
+ if (!yes)
24
+ return 'Nothing was sent.';
25
+ await call('terminal.annotations.confirmAll');
26
+ }
27
+ const result = await call('terminal.annotations.send', options.prompt ? { prompt: options.prompt } : {});
28
+ return describeSend(result, !!options.prompt);
29
+ }
30
+ export function describeSend(result, withPrompt = false) {
31
+ if (result.state === 'sent')
32
+ return `Sent ${count(result.ids.length)} to the agent.`;
33
+ if (result.state === 'busy' && withPrompt)
34
+ return 'The agent is still working. Submit again when it finishes; your annotations stay unsent.';
35
+ if (result.state === 'busy')
36
+ return `The agent is working; ${count(result.ids.length)} go${result.ids.length === 1 ? 'es' : ''} out when it finishes.`;
37
+ if (result.state === 'host')
38
+ return `${count(result.ids.length)} handed to the app to send.`;
39
+ return result.unconfirmed ? `${count(result.unconfirmed)} still wait for your confirmation.` : 'Nothing to send.';
40
+ }
@@ -7,7 +7,7 @@ export declare function createCommandPage(host: TerminalCommandHost, id: Command
7
7
  runId?: string;
8
8
  url?: string;
9
9
  annotate?: AnnotateStart;
10
- usageTab?: 'analysis' | 'limit';
10
+ usageTab?: 'analysis' | 'limit' | 'accounts';
11
11
  usagePeriod?: 'today' | '7d' | '30d' | 'all';
12
12
  close?(): void;
13
13
  }): TerminalPage;