@xingular/harness 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +70 -37
  4. package/dist/cli.js +6572 -6700
  5. package/dist/terminal-pi.js +51 -38
  6. package/docs/terminal-tui.md +50 -12
  7. package/package.json +2 -2
  8. package/src/accounts/accounts.ts +4 -3
  9. package/src/capabilities/api.ts +1 -1
  10. package/src/cli.ts +7 -3
  11. package/src/core/bus.test.ts +15 -1
  12. package/src/core/bus.ts +39 -0
  13. package/src/core/caller.ts +34 -0
  14. package/src/core/fs.ts +17 -1
  15. package/src/harness.ts +55 -66
  16. package/src/{memory → history}/recall.test.ts +1 -1
  17. package/src/{memory → history}/recall.ts +1 -1
  18. package/src/{memory → history}/trajectory.test.ts +4 -4
  19. package/src/{memory → history}/trajectory.ts +1 -1
  20. package/src/hub/hub.test.ts +4 -5
  21. package/src/hub/hub.ts +2 -2
  22. package/src/index.ts +28 -14
  23. package/src/mcp/connections.ts +1 -1
  24. package/src/mcp/service.ts +1 -1
  25. package/src/pi/proxy.test.ts +68 -0
  26. package/src/pi/runtime.test.ts +61 -0
  27. package/src/pi/runtime.ts +50 -0
  28. package/src/plugins/api.ts +11 -9
  29. package/src/plugins/builtin.ts +22 -40
  30. package/src/plugins/catalogue.test.ts +13 -41
  31. package/src/plugins/catalogue.ts +3 -32
  32. package/src/plugins/discover.ts +2 -1
  33. package/src/plugins/firstParty.test.ts +1 -1
  34. package/src/plugins/hooks.test.ts +21 -71
  35. package/src/plugins/hooks.ts +6 -7
  36. package/src/plugins/identity.ts +16 -2
  37. package/src/plugins/install.test.ts +1 -1
  38. package/src/plugins/install.ts +2 -1
  39. package/src/plugins/inventory.ts +19 -18
  40. package/src/plugins/plugins.ts +38 -2
  41. package/src/prompt/compile.test.ts +82 -193
  42. package/src/prompt/compile.ts +98 -363
  43. package/src/refine/cli.ts +114 -0
  44. package/src/refine/demo.ts +124 -0
  45. package/src/refine/evidence.ts +349 -0
  46. package/src/refine/host.ts +102 -0
  47. package/src/refine/ledger.ts +104 -0
  48. package/src/refine/library.ts +192 -0
  49. package/src/refine/pi.test.ts +90 -0
  50. package/src/refine/pi.ts +218 -0
  51. package/src/refine/prompts.ts +430 -0
  52. package/src/refine/recursion.ts +353 -0
  53. package/src/refine/refine.test.ts +140 -0
  54. package/src/refine/runner.ts +205 -0
  55. package/src/refine/selection.ts +291 -0
  56. package/src/refine/service.ts +194 -0
  57. package/src/refine/trajectory.ts +71 -0
  58. package/src/refine/types.ts +91 -0
  59. package/src/server/methods.ts +137 -123
  60. package/src/server/server.test.ts +37 -30
  61. package/src/sessions/contracts.ts +7 -2
  62. package/src/sessions/conversation.test.ts +2 -27
  63. package/src/sessions/planMode.test.ts +8 -1
  64. package/src/sessions/planMode.ts +17 -9
  65. package/src/sessions/policy.ts +1 -1
  66. package/src/sessions/pricing.ts +3 -1
  67. package/src/sessions/questionBatches.test.ts +15 -3
  68. package/src/sessions/registry.ts +13 -1
  69. package/src/sessions/runStore.ts +3 -1
  70. package/src/sessions/runner.ts +10 -5
  71. package/src/sessions/sessions.test.ts +60 -13
  72. package/src/sessions/sessions.ts +43 -18
  73. package/src/sessions/subagentService.test.ts +22 -1
  74. package/src/sessions/subagentService.ts +14 -1
  75. package/src/sessions/subagents.ts +2 -0
  76. package/src/terminal/cli.ts +24 -48
  77. package/src/terminal/logs.test.ts +26 -0
  78. package/src/terminal/logs.ts +67 -0
  79. package/src/terminal/piExtension.lifecycle.test.ts +1 -1
  80. package/src/terminal/piExtension.ts +3 -3
  81. package/src/terminal/pluginControl.test.ts +7 -8
  82. package/src/terminal/socket.ts +3 -3
  83. package/src/terminal/terminal.test.ts +41 -39
  84. package/src/terminal/terminals.ts +31 -13
  85. package/src/terminal/tui/application.test.ts +58 -0
  86. package/src/terminal/tui/application.ts +15 -4
  87. package/src/terminal/tui/clipboardFiles.test.ts +94 -0
  88. package/src/terminal/tui/clipboardFiles.ts +86 -0
  89. package/src/terminal/tui/commands.test.ts +13 -1
  90. package/src/terminal/tui/commands.ts +16 -0
  91. package/src/terminal/tui/logPanel.test.ts +61 -0
  92. package/src/terminal/tui/logPanel.ts +175 -0
  93. package/src/terminal/tui/piAdapter.ts +7 -4
  94. package/src/terminal/tui/questionPicker.ts +168 -0
  95. package/src/terminal/tui/questions.test.ts +86 -19
  96. package/src/terminal/tui/questions.ts +27 -22
  97. package/src/terminal/tui/runtime.ts +2 -0
  98. package/src/tools/askUser.test.ts +23 -2
  99. package/src/tools/askUser.ts +15 -5
  100. package/src/tools/definitions.ts +49 -132
  101. package/src/tools/dispatch.ts +78 -0
  102. package/src/tools/harnessTools.ts +23 -0
  103. package/src/tools/spawnSubagent.ts +3 -3
  104. package/src/tools/types.ts +3 -1
  105. package/src/tree/components.ts +221 -0
  106. package/src/tree/materialise.ts +122 -0
  107. package/src/tree/seed.ts +123 -0
  108. package/src/tree/store.ts +193 -0
  109. package/src/tree/surface.ts +135 -0
  110. package/src/tree/tree.test.ts +126 -0
  111. package/src/tree/tree.ts +346 -0
  112. package/src/tree/trust.ts +86 -0
  113. package/src/web/tools.ts +27 -5
  114. package/src/web/web.test.ts +26 -1
  115. package/src/wiki/notes.ts +313 -0
  116. package/src/memory/api.test.ts +0 -82
  117. package/src/memory/api.ts +0 -303
  118. package/src/memory/blocks.test.ts +0 -78
  119. package/src/memory/blocks.ts +0 -408
  120. package/src/memory/caller.ts +0 -69
  121. package/src/memory/curation.test.ts +0 -206
  122. package/src/memory/curation.ts +0 -64
  123. package/src/memory/dispatch.ts +0 -64
  124. package/src/memory/facts.test.ts +0 -41
  125. package/src/memory/facts.ts +0 -248
  126. package/src/memory/graph.ts +0 -103
  127. package/src/memory/health.test.ts +0 -130
  128. package/src/memory/health.ts +0 -74
  129. package/src/memory/maintainer.ts +0 -272
  130. package/src/memory/maintenance.test.ts +0 -192
  131. package/src/memory/maintenance.ts +0 -679
  132. package/src/memory/migrate.ts +0 -79
  133. package/src/memory/operations.ts +0 -150
  134. package/src/memory/proposals.test.ts +0 -168
  135. package/src/memory/proposals.ts +0 -584
  136. package/src/memory/references.ts +0 -77
  137. package/src/memory/runContext.ts +0 -93
  138. package/src/memory/store.test.ts +0 -66
  139. package/src/memory/store.ts +0 -385
  140. package/src/memory/wiki.test.ts +0 -47
  141. package/src/memory/wiki.ts +0 -423
  142. package/src/memory/wikiIndex.test.ts +0 -88
  143. package/src/memory/wikiIndex.ts +0 -117
  144. package/src/plugins/proposal.ts +0 -121
  145. package/src/skills/registry.test.ts +0 -104
  146. package/src/skills/registry.ts +0 -413
  147. package/src/tools/maintenance.ts +0 -29
  148. package/src/tools/memoryTools.ts +0 -22
  149. package/src/tools/pluginPropose.ts +0 -41
  150. package/src/tools/registryWrite.test.ts +0 -101
  151. package/src/tools/registryWrite.ts +0 -182
  152. /package/src/{memory → core}/secrets.ts +0 -0
@@ -1,6 +1,6 @@
1
- // `xagent-wiki` and `xagent-mcp`: the harness's memory, Wiki, and connected apps as commands,
2
- // for a `pi` terminal session's shell. Each command maps onto one call to the session socket.
3
- import type { WikiCall } from '../tools/definitions'
1
+ // `xagent-wiki` and `xagent-mcp`: the harness's notes, skills, history and connected apps as
2
+ // commands, for a `pi` terminal session's shell. Each command maps onto one call to the session socket.
3
+ import type { HarnessCall } from '../tools/definitions'
4
4
  import { connectionFromEnv, socketCall } from './client'
5
5
 
6
6
  export const WIKI_COMMAND = 'xagent-wiki'
@@ -8,22 +8,14 @@ export const APPS_COMMAND = 'xagent-mcp'
8
8
 
9
9
  export const USAGE = `Usage: ${WIKI_COMMAND} <command> [arguments]
10
10
 
11
- entrypoint Your context pack: memory blocks, rules, skills, project map
12
- search <query> [--kind K] [--namespace NS]
13
- Search every namespace (K: page, fact, skill, rule, prompt)
14
- read <id or path> Read one page, fact, skill, rule, or prompt
15
- list [namespace] [--kind K] List namespaces and pages, or all entries of a kind
16
- skill [name] Load a skill, or list skills
17
- facts [query] [--namespace NS] Recall facts (also: memory)
11
+ search <query> [--project SLUG] Search the Wiki's project notes
12
+ read <path> Read one note, e.g. projects/<slug>/decisions
13
+ list [project slug | library] List notes
14
+ write <path> <text | -> Save a note (when this session may write the Wiki)
15
+ skill [name] Load one of your skills, or list them
18
16
  history <query> [--scope all] [--since YYYY-MM-DD]
19
17
  Search earlier conversations and runs (also: recall)
20
18
  history-read <run id> Read one earlier run (also: recall-read)
21
- block append <label> <text> Add to a memory block (user, project, or a shared block)
22
- block replace <label> --old <text> --new <text>
23
- Replace one exact passage in a memory block
24
- block rethink <label> <text> Rewrite a whole memory block
25
- propose <json | -> Record knowledge, e.g. {"kind":"fact","title":"…","body":"…",
26
- "type":"preference","sourceRefs":[{"kind":"run","ref":"current"}]}
27
19
  `
28
20
 
29
21
  export const APPS_USAGE = `Usage: ${APPS_COMMAND} <command>
@@ -33,15 +25,15 @@ export const APPS_USAGE = `Usage: ${APPS_COMMAND} <command>
33
25
  Use one of them
34
26
  `
35
27
 
36
- export type CliCommand = { kind: 'call'; call: WikiCall; params: Record<string, unknown> } | { kind: 'help' } | { kind: 'stdin-propose' }
28
+ export type CliCommand = { kind: 'call'; call: HarnessCall; params: Record<string, unknown> } | { kind: 'help' } | { kind: 'stdin-write'; path: string }
37
29
 
38
30
  function options(args: string[]): { positional: string[]; flags: Record<string, string> } {
39
31
  const positional: string[] = []
40
32
  const flags: Record<string, string> = {}
41
33
  for (let index = 0; index < args.length; index += 1) {
42
- const arg = args[index]
34
+ const arg = args[index]!
43
35
  if (arg.startsWith('--') && index + 1 < args.length) {
44
- flags[arg.slice(2)] = args[index + 1]
36
+ flags[arg.slice(2)] = args[index + 1]!
45
37
  index += 1
46
38
  } else {
47
39
  positional.push(arg)
@@ -55,21 +47,23 @@ export function parseCli(argv: string[]): CliCommand {
55
47
  const { positional, flags } = options(rest)
56
48
  const joined = positional.join(' ').trim()
57
49
  switch (command) {
58
- case 'entrypoint':
59
- return { kind: 'call', call: 'wiki.entrypoint', params: {} }
60
50
  case 'search':
61
51
  if (!joined) return { kind: 'help' }
62
- return { kind: 'call', call: 'wiki.search', params: { query: joined, ...(flags.kind ? { kinds: [flags.kind] } : {}), ...(flags.namespace ? { namespace: flags.namespace } : {}) } }
52
+ return { kind: 'call', call: 'wiki.search', params: { query: joined, ...(flags.project ? { project: flags.project } : {}) } }
63
53
  case 'read':
64
54
  if (!joined) return { kind: 'help' }
65
- return { kind: 'call', call: 'wiki.read', params: { id: joined } }
55
+ return { kind: 'call', call: 'wiki.read', params: { path: joined } }
66
56
  case 'list':
67
- return { kind: 'call', call: 'wiki.list', params: { ...(joined ? { namespace: joined } : {}), ...(flags.kind ? { kind: flags.kind } : {}) } }
57
+ return { kind: 'call', call: 'wiki.list', params: joined ? { project: joined } : {} }
58
+ case 'write': {
59
+ const [path, ...words] = positional
60
+ if (!path) return { kind: 'help' }
61
+ if (words.join(' ').trim() === '-') return { kind: 'stdin-write', path }
62
+ if (!words.length) return { kind: 'help' }
63
+ return { kind: 'call', call: 'wiki.write', params: { path, text: words.join(' ') } }
64
+ }
68
65
  case 'skill':
69
- return { kind: 'call', call: 'wiki.skill', params: joined ? { name: joined } : {} }
70
- case 'facts':
71
- case 'memory':
72
- return { kind: 'call', call: 'wiki.memory', params: { ...(joined ? { query: joined } : {}), ...(flags.namespace ? { namespace: flags.namespace } : {}) } }
66
+ return { kind: 'call', call: 'skill.load', params: joined ? { name: joined } : {} }
73
67
  case 'history':
74
68
  case 'recall':
75
69
  if (!joined) return { kind: 'help' }
@@ -78,24 +72,6 @@ export function parseCli(argv: string[]): CliCommand {
78
72
  case 'recall-read':
79
73
  if (!joined) return { kind: 'help' }
80
74
  return { kind: 'call', call: 'recall.read', params: { run: joined } }
81
- case 'block': {
82
- const [action, label, ...words] = positional
83
- const body = words.join(' ').trim()
84
- if (action === 'append' && label && body) return { kind: 'call', call: 'memory.append', params: { block: label, text: body } }
85
- if (action === 'rethink' && label && body) return { kind: 'call', call: 'memory.rethink', params: { block: label, body } }
86
- if (action === 'replace' && label && flags.old !== undefined && flags.new !== undefined) return { kind: 'call', call: 'memory.replace', params: { block: label, old: flags.old, new: flags.new } }
87
- return { kind: 'help' }
88
- }
89
- case 'propose': {
90
- if (joined === '-') return { kind: 'stdin-propose' }
91
- try {
92
- const params = JSON.parse(joined) as unknown
93
- if (params && typeof params === 'object' && !Array.isArray(params)) return { kind: 'call', call: 'wiki.propose', params: params as Record<string, unknown> }
94
- } catch {
95
- // Falls through to help.
96
- }
97
- return { kind: 'help' }
98
- }
99
75
  default:
100
76
  return { kind: 'help' }
101
77
  }
@@ -119,8 +95,8 @@ export async function runWikiCli(argv: string[]): Promise<number> {
119
95
  try {
120
96
  const connection = connectionFromEnv()
121
97
  const result =
122
- command.kind === 'stdin-propose'
123
- ? await socketCall<string>(connection, 'wiki.propose', JSON.parse(await readStdin()) as Record<string, unknown>)
98
+ command.kind === 'stdin-write'
99
+ ? await socketCall<string>(connection, 'wiki.write', { path: command.path, text: await readStdin() })
124
100
  : await socketCall<string>(connection, command.call, command.params)
125
101
  process.stdout.write(`${typeof result === 'string' ? result : JSON.stringify(result)}\n`)
126
102
  return 0
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { LOG_READ_CHUNK_BYTES, readHarnessLogSnapshot } from './logs'
4
+
5
+ describe('terminal diagnostic log snapshots', () => {
6
+ it('reads the complete fixed-size snapshot in bounded byte chunks', async () => {
7
+ const original = `${'x'.repeat(LOG_READ_CHUNK_BYTES - 1)}🧠and the rest`
8
+ const bytes = Buffer.from(original)
9
+ const info = vi.fn(async () => ({ size: bytes.length }))
10
+ const chunk = vi.fn(async (snapshotSize: number, offset: number) => {
11
+ expect(snapshotSize).toBe(bytes.length)
12
+ const nextOffset = Math.min(bytes.length, offset + LOG_READ_CHUNK_BYTES)
13
+ return { data: bytes.subarray(offset, nextOffset).toString('base64'), nextOffset }
14
+ })
15
+
16
+ await expect(readHarnessLogSnapshot(info, chunk)).resolves.toBe(original)
17
+ expect(chunk).toHaveBeenCalledTimes(2)
18
+ expect(chunk.mock.calls.map(([, offset]) => offset)).toEqual([0, LOG_READ_CHUNK_BYTES])
19
+ })
20
+
21
+ it('returns an empty snapshot when this host has not captured process diagnostics', async () => {
22
+ const chunk = vi.fn()
23
+ await expect(readHarnessLogSnapshot(async () => ({ size: null }), chunk)).resolves.toBe('')
24
+ expect(chunk).not.toHaveBeenCalled()
25
+ })
26
+ })
@@ -0,0 +1,67 @@
1
+ import { closeSync, openSync, readSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export const LOG_READ_CHUNK_BYTES = 32 * 1024
5
+
6
+ export interface HarnessLogInfo {
7
+ size: number | null
8
+ }
9
+
10
+ export interface HarnessLogChunk {
11
+ data: string
12
+ nextOffset: number
13
+ }
14
+
15
+ /** Captures the byte length of the host-owned diagnostics file; callers never supply a path. */
16
+ export function harnessLogInfo(stateDir: string): HarnessLogInfo {
17
+ try {
18
+ const stat = statSync(join(stateDir, 'harness.log'))
19
+ if (!stat.isFile()) throw new Error('The Harness diagnostics path is not a regular file.')
20
+ return { size: stat.size }
21
+ } catch (error) {
22
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { size: null }
23
+ throw error
24
+ }
25
+ }
26
+
27
+ /** Reads one bounded range from the file size captured by `harnessLogInfo`. */
28
+ export function readHarnessLogChunk(stateDir: string, snapshotSize: number, offset: number): HarnessLogChunk {
29
+ if (!Number.isSafeInteger(snapshotSize) || snapshotSize < 0 || !Number.isSafeInteger(offset) || offset < 0 || offset >= snapshotSize) {
30
+ throw new Error('The Harness diagnostics snapshot range is invalid.')
31
+ }
32
+ const path = join(stateDir, 'harness.log')
33
+ const current = statSync(path)
34
+ if (!current.isFile() || current.size < snapshotSize) throw new Error('The Harness diagnostics file changed while it was being read.')
35
+ const length = Math.min(LOG_READ_CHUNK_BYTES, snapshotSize - offset)
36
+ const buffer = Buffer.allocUnsafe(length)
37
+ const descriptor = openSync(path, 'r')
38
+ try {
39
+ const bytesRead = readSync(descriptor, buffer, 0, length, offset)
40
+ if (bytesRead !== length) throw new Error('The Harness diagnostics file changed while it was being read.')
41
+ return { data: buffer.toString('base64'), nextOffset: offset + bytesRead }
42
+ } finally {
43
+ closeSync(descriptor)
44
+ }
45
+ }
46
+
47
+ /** Collects a fixed-size snapshot while keeping each socket reply bounded. */
48
+ export async function readHarnessLogSnapshot(
49
+ info: () => Promise<HarnessLogInfo>,
50
+ chunk: (snapshotSize: number, offset: number) => Promise<HarnessLogChunk>
51
+ ): Promise<string> {
52
+ const { size } = await info()
53
+ if (size === null || size === 0) return ''
54
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error('The Harness returned an invalid diagnostics file size.')
55
+ const buffers: Buffer[] = []
56
+ let offset = 0
57
+ while (offset < size) {
58
+ const page = await chunk(size, offset)
59
+ const buffer = Buffer.from(page.data, 'base64')
60
+ if (!buffer.length || page.nextOffset !== offset + buffer.length || page.nextOffset > size) {
61
+ throw new Error('The Harness returned an invalid diagnostics file page.')
62
+ }
63
+ buffers.push(buffer)
64
+ offset = page.nextOffset
65
+ }
66
+ return Buffer.concat(buffers).toString('utf8')
67
+ }
@@ -51,7 +51,7 @@ describe('terminal bridge lifecycle', () => {
51
51
  await terminalExtension(api, state)
52
52
  const policyCalls = transport.socket.mock.calls.filter(([, call]) => call === 'terminal.policy').length
53
53
  const result = await handlers.get('tool_call')!({ toolName: 'bash', toolCallId: 'call-1', input: { command: 'touch unsafe' } }, ctx)
54
- expect(result).toMatchObject({ block: true, reason: expect.stringContaining('Plan mode is read-only') })
54
+ expect(result).toMatchObject({ block: true, reason: expect.stringContaining('Plan mode is research-only') })
55
55
  expect(transport.socket.mock.calls.filter(([, call]) => call === 'terminal.policy')).toHaveLength(policyCalls)
56
56
  })
57
57
 
@@ -235,7 +235,7 @@ export default async function (pi: ExtensionAPI, planRun?: TerminalPlanRunState)
235
235
  })
236
236
  pi.on('tool_call', async (event) => {
237
237
  if (planRun?.active && !planRun.allowedTools.has(event.toolName)) {
238
- const reason = planRefusal(event.toolName, new Set()) ?? `Plan mode is read-only: ${event.toolName} is not available in this session.`
238
+ const reason = planRefusal(event.toolName, new Set()) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`
239
239
  return { block: true, reason }
240
240
  }
241
241
  try {
@@ -294,10 +294,10 @@ export default async function (pi: ExtensionAPI, planRun?: TerminalPlanRunState)
294
294
  pi.registerTool({
295
295
  name: 'ask_user_question',
296
296
  label: 'Ask a question',
297
- description: 'Ask the user a question and wait for their answer.',
297
+ description: 'Ask the user a question and wait for their answer. Recommendations are visual only and never preselect answers; submitted choices keep their labels. Mark options with recommended: true, at most one for single-choice questions or several when multiSelect is true.',
298
298
  parameters: Type.Object({
299
299
  question: Type.String(),
300
- options: Type.Optional(Type.Array(Type.Object({ label: Type.String(), description: Type.Optional(Type.String()) }))),
300
+ options: Type.Optional(Type.Array(Type.Object({ label: Type.String(), description: Type.Optional(Type.String()), recommended: Type.Optional(Type.Boolean()) }))),
301
301
  multiSelect: Type.Optional(Type.Boolean())
302
302
  }),
303
303
  async execute(id, input) {
@@ -80,21 +80,20 @@ describe('plugin control from a terminal session', () => {
80
80
  const { key } = await socketCall<{ key: string }>(connection, 'control.claim')
81
81
  const call = <T>(method: string, params?: Record<string, unknown>) => socketCall<T>(connection, method, params ?? {}, 5_000, key)
82
82
  const rows = menuRows(await call<Plugin[]>('plugins.list'), 'p1')
83
- expect(rows.map((row) => row.label)).toEqual(['● review-kit · code not trusted', '○ Run tests · instruction in Yours', '○ Budget guard · runs code', '○ Output trim · runs code'])
83
+ expect(rows.map((row) => row.label)).toEqual(['● review-kit · code not trusted', '○ Budget guard · runs code', '○ Output trim · runs code'])
84
84
  expect(rowActions(rows[0], 'p1')).toEqual(['Switch off for this project', 'Switch off everywhere', 'Trust its code', 'Back'])
85
85
  expect(rowActions(rows[1], null)).toEqual(['Switch on everywhere', 'Back'])
86
86
  // A part that ships with the harness needs no trust: it is the harness's own code.
87
- expect(rowActions(rows[2], 'p1')).toEqual(['Switch on for this project', 'Switch on everywhere', 'Back'])
87
+ expect(rowActions(rows[1], 'p1')).toEqual(['Switch on for this project', 'Switch on everywhere', 'Back'])
88
88
 
89
- // Declining the trust question leaves the code untrusted; then they trust it, switch a rule on, and leave.
90
- const { ui, shown, said } = person(['● review-kit · code not trusted', 'Trust its code', false, '● review-kit · code not trusted', 'Trust its code', true, '○ Run tests · instruction in Yours', 'Switch on for this project', 'Done'])
89
+ // Declining the trust question leaves the code untrusted; then they trust it and leave.
90
+ const { ui, shown, said } = person(['● review-kit · code not trusted', 'Trust its code', false, '● review-kit · code not trusted', 'Trust its code', true, 'Done'])
91
91
  await runPluginsMenu(ui, call, 'p1')
92
92
  expect(shown.find((line) => line.startsWith('Trust this code?'))).toContain('extensions/guard.ts')
93
93
  expect(shown.find((line) => line.startsWith('Trust this code?'))).toContain('can do anything this app can')
94
94
  expect(said).toEqual(['Saved. It applies when this session restarts.'])
95
95
  const after = await call<Plugin[]>('plugins.list')
96
96
  expect(after.find((plugin) => plugin.id === 'package:review-kit')).toMatchObject({ trust: 'trusted' })
97
- expect(after.flatMap((plugin) => plugin.contributions).find((entry) => entry.id === 'yours/rules/tests')).toMatchObject({ state: 'on', scope: 'App' })
98
97
  expect(harness.plugins.trust.list()).toHaveLength(1)
99
98
 
100
99
  // Leaving at once changes nothing and says nothing.
@@ -113,7 +112,7 @@ describe('the plugins command', () => {
113
112
  const last = (): string => lines.at(-1) ?? ''
114
113
 
115
114
  expect(await run(['list'])).toBe(0)
116
- expect(last()).toMatch(/^memory\s+required\s+Memory$/m)
115
+ expect(last()).toMatch(/^harness\s+required\s+Harness$/m)
117
116
  expect(await run(['install', join(base, 'kit')])).toBe(0)
118
117
  expect(last()).toContain('Installed, switched off.')
119
118
  expect(last()).toContain('trust its code with "plugins trust <id>"')
@@ -134,8 +133,8 @@ describe('the plugins command', () => {
134
133
  expect(last()).toContain('Code (trusted)')
135
134
  expect(last()).toContain('● skill audit (package:review-kit/skill/audit)')
136
135
 
137
- expect(await run(['detach', 'memory'])).toBe(1)
138
- expect(last()).toContain('Memory cannot be switched off.')
136
+ expect(await run(['detach', 'harness'])).toBe(1)
137
+ expect(last()).toContain('Harness cannot be switched off.')
139
138
  expect(await run(['untrust', 'package:review-kit'])).toBe(0)
140
139
  expect(await run(['remove', 'package:review-kit'])).toBe(0)
141
140
  expect(await run(['show', 'package:review-kit'])).toBe(1)
@@ -13,7 +13,7 @@ import { log } from '../core/bus'
13
13
  import { HarnessError, toErrorShape } from '../core/errors'
14
14
  import { newId } from '../core/fs'
15
15
  import type { BrokerSession } from '../mcp/service'
16
- import type { CallerContext } from '../memory/caller'
16
+ import type { CallerContext } from '../core/caller'
17
17
 
18
18
  /** What a token stands for. */
19
19
  export interface SocketGrant {
@@ -27,7 +27,7 @@ export interface SocketGrant {
27
27
  }
28
28
 
29
29
  export interface SocketHandlers {
30
- /** `wiki.*`, `memory.*`, `recall.*`. */
30
+ /** `wiki.*`, `skill.*`, `recall.*`, `trajectory.*`. */
31
31
  memory(caller: CallerContext, call: string, params: Record<string, unknown>): string
32
32
  /** `mcp.list`, `mcp.call`, `mcp.revision`, `tools.list`, `tools.call`. */
33
33
  apps(session: BrokerSession, call: string, params: Record<string, unknown>): Promise<unknown>
@@ -147,7 +147,7 @@ export class SessionSocket {
147
147
  }
148
148
  const call = typeof request.call === 'string' ? request.call : ''
149
149
  const params = (request.params && typeof request.params === 'object' ? request.params : {}) as Record<string, unknown>
150
- if (['wiki.', 'memory.', 'recall.'].some((prefix) => call.startsWith(prefix))) return { result: this.handlers.memory(grant.caller(), call, params) }
150
+ if (['wiki.', 'skill.', 'recall.', 'trajectory.'].some((prefix) => call.startsWith(prefix))) return { result: this.handlers.memory(grant.caller(), call, params) }
151
151
  if (call === 'tools.list' && !grant.broker) return { result: { tools: [] } }
152
152
  if (['mcp.', 'tools.'].some((prefix) => call.startsWith(prefix))) {
153
153
  if (!grant.broker) throw new HarnessError('mcp_unauthorized', 'Connected apps are not available to this session.')
@@ -15,6 +15,7 @@ import { rootWithProject, writeFile } from '../../tests/helpers'
15
15
  import { parseCli } from './cli'
16
16
  import { SOCKET_ENV, TOKEN_ENV, socketCall, type SocketConnection } from './client'
17
17
  import { SessionSocket } from './socket'
18
+ import { LOG_READ_CHUNK_BYTES } from './logs'
18
19
 
19
20
  const open: Harness[] = []
20
21
  afterEach(async () => {
@@ -106,11 +107,8 @@ describe('a pi terminal session', () => {
106
107
  expect(`${result.stdout}${result.stderr}`).toMatch(/usage|options|pi coding agent/i)
107
108
  })
108
109
 
109
- it('passes what the harness found as paths, the enabled registry items with them, and its own extension last', async () => {
110
+ it('passes what the harness found as paths, the harness tree\'s skills and trusted plugins with them, and its own extension last', async () => {
110
111
  const root = rootWithProject()
111
- writeFile(join(root, 'registry/skills/release/SKILL.md'), '# Release\n')
112
- writeFile(join(root, 'registry/prompts/review.md'), 'Review this.\n')
113
- writeFile(join(root, 'config/registry-enabled.json'), JSON.stringify({ p1: ['skills/release', 'prompts/review'] }))
114
112
  writeFile(join(root, '.pi/extensions/local.ts'), 'export default function () {}\n')
115
113
  const item = (kind: Found['kind'], path: string, origin: Found['origin'], enabled = true): Found => ({ kind, path, origin, project: origin === 'project', packageRoot: null, source: null, enabled })
116
114
  const found = [
@@ -125,12 +123,17 @@ describe('a pi terminal session', () => {
125
123
  const harness = createHarness({ dataRoot: root, stateDir: mkdtempSync(join(tmpdir(), 'hs-')), secrets: memorySecretStore(), piProgram: () => '/opt/pi/bin/pi', assets: { node: '/opt/node', cli: '/opt/harness/cli.js', piExtension: '/opt/harness/terminal-pi.js' }, resolver: async () => found })
126
124
  open.push(harness)
127
125
  const account = harness.accounts.create('Main', 'apiKey').accounts[0]
128
- const prepare = async () => (await harness.terminals.prepare({ sessionId: 'term-1', accountId: account.id, projectId: 'p1', cwd: root, sessionFolder: join(root, 'sessions', 'p1', 'term-1') })).args.slice(2)
126
+ const tree = { 'systemprompt.md': 'Ship releases.\n', 'guardrails.md': '- When cutting a release, load the `release` skill.\n', 'memory.md': '', 'tool_guidance.md': '', 'skills/release/SKILL.md': '---\nname: release\ndescription: cutting a release\n---\nTag it.\n', 'plugins/guard.ts': 'export default function (pi) {}\n' }
127
+ const { commit, versionPath, head } = await import('../tree/store')
128
+ commit(root, 'term-agent', tree, { kind: 'seed' })
129
+ harness.trust.grant(tree['plugins/guard.ts'], 'plugins/guard.ts', 'term-agent')
130
+ const version = versionPath(root, 'term-agent', head(root, 'term-agent')!.fingerprint)
131
+ const prepare = async () => (await harness.terminals.prepare({ sessionId: 'term-1', accountId: account.id, projectId: 'p1', cwd: root, sessionFolder: join(root, 'sessions', 'p1', 'term-1'), harness: { scope: 'term-agent' } })).args.slice(2)
129
132
 
130
133
  const loaded = [
131
- '--extension', '/shared/extensions/mine.ts', '--extension', 'builtin:mcp',
132
- '--skill', join(root, 'registry/skills/release/SKILL.md'), '--skill', '/shared/skills/alpha/SKILL.md',
133
- '--prompt-template', join(root, 'registry/prompts/review.md'), '--prompt-template', '/shared/prompts/p.md',
134
+ '--extension', '/shared/extensions/mine.ts', '--extension', 'builtin:mcp', '--extension', join(version, 'plugins/guard.ts'),
135
+ '--skill', join(version, 'skills'), '--skill', '/shared/skills/alpha/SKILL.md',
136
+ '--prompt-template', '/shared/prompts/p.md',
134
137
  '--theme', '/shared/themes/dark.json',
135
138
  '--extension', '/opt/harness/terminal-pi.js', '--session-dir', join(root, 'sessions', 'p1', 'term-1', 'pi')
136
139
  ]
@@ -190,24 +193,6 @@ describe('a pi terminal session', () => {
190
193
  expect(loadRun(root, 'reserved').status).toBe('interrupted')
191
194
  })
192
195
 
193
- it('sends a memory block that changed since the session started along with the next instruction', async () => {
194
- const { tell, harness, connection } = await setup()
195
- await tell('ready', {})
196
- await socketCall(connection, 'memory.append', { block: 'project', text: 'Prefers pnpm.' })
197
- const first = harness.terminals.send({ sessionId: 'term-1', text: 'Continue' })
198
- let delivered = ''
199
- await vi.waitFor(async () => {
200
- const { command } = await tell('poll')
201
- expect(command?.type).toBe('send')
202
- delivered = command.text
203
- })
204
- await tell('begin', { text: delivered })
205
- await first
206
- expect(delivered.startsWith('[Memory updated since this session')).toBe(true)
207
- expect(delivered).toContain('Prefers pnpm.')
208
- expect(delivered.endsWith('\n\nContinue')).toBe(true)
209
- })
210
-
211
196
  it('will not send over a draft the person is typing, and fails a delivery the native session never acknowledges', async () => {
212
197
  const { tell, harness, root } = await setup()
213
198
  await tell('ready', {})
@@ -388,11 +373,11 @@ describe('a pi terminal session', () => {
388
373
  expect(harness.terminals.state('term-1').ready).toBe(false)
389
374
  })
390
375
 
391
- it('serves memory and connected apps to the session over the socket, until the terminal ends', async () => {
376
+ it('serves the Wiki, skills and connected apps to the session over the socket, until the terminal ends', async () => {
392
377
  const { connection, harness, tell } = await setup()
393
378
  await tell('ready', {})
394
- expect(await socketCall<string>(connection, 'wiki.entrypoint')).toContain('# Xingular Harness context')
395
- expect(await socketCall<string>(connection, 'wiki.list')).toContain('library')
379
+ expect(await socketCall<string>(connection, 'skill.load')).toBe('You have no skills.')
380
+ expect(await socketCall<string>(connection, 'wiki.list')).toContain('home')
396
381
  const listed = await socketCall<{ tools: { name: string; parameters: { type: string } }[] }>(connection, 'tools.list')
397
382
  expect(listed.tools.map((tool) => tool.name)).toEqual(['mcp_list_tools', 'mcp_call_tool'])
398
383
  expect(listed.tools[0].parameters.type).toBe('object')
@@ -428,19 +413,17 @@ describe('the session socket', () => {
428
413
  })
429
414
 
430
415
  describe('xagent-wiki command', () => {
431
- it('maps commands onto memory and Wiki calls', () => {
432
- expect(parseCli(['search', 'deploy', 'steps', '--kind', 'page'])).toEqual({ kind: 'call', call: 'wiki.search', params: { query: 'deploy steps', kinds: ['page'] } })
433
- expect(parseCli(['read', '/projects/app/arch'])).toEqual({ kind: 'call', call: 'wiki.read', params: { id: '/projects/app/arch' } })
416
+ it('maps commands onto Wiki, skill and history calls', () => {
417
+ expect(parseCli(['search', 'deploy', 'steps', '--project', 'app'])).toEqual({ kind: 'call', call: 'wiki.search', params: { query: 'deploy steps', project: 'app' } })
418
+ expect(parseCli(['read', 'projects/app/arch'])).toEqual({ kind: 'call', call: 'wiki.read', params: { path: 'projects/app/arch' } })
434
419
  expect(parseCli(['list'])).toEqual({ kind: 'call', call: 'wiki.list', params: {} })
435
- expect(parseCli(['skill', 'review'])).toEqual({ kind: 'call', call: 'wiki.skill', params: { name: 'review' } })
436
- expect(parseCli(['memory', '--namespace', '/projects/app'])).toEqual({ kind: 'call', call: 'wiki.memory', params: { namespace: '/projects/app' } })
420
+ expect(parseCli(['list', 'library'])).toEqual({ kind: 'call', call: 'wiki.list', params: { project: 'library' } })
421
+ expect(parseCli(['write', 'library/notes', 'Use', 'pnpm.'])).toEqual({ kind: 'call', call: 'wiki.write', params: { path: 'library/notes', text: 'Use pnpm.' } })
422
+ expect(parseCli(['write', 'library/notes', '-'])).toEqual({ kind: 'stdin-write', path: 'library/notes' })
423
+ expect(parseCli(['skill', 'review'])).toEqual({ kind: 'call', call: 'skill.load', params: { name: 'review' } })
437
424
  expect(parseCli(['recall', 'the', 'outage', '--scope', 'all'])).toEqual({ kind: 'call', call: 'recall.search', params: { query: 'the outage', scope: 'all' } })
438
- expect(parseCli(['propose', '{"kind":"memory","title":"t","body":"b"}'])).toMatchObject({ kind: 'call', call: 'wiki.propose', params: { kind: 'memory' } })
439
- expect(parseCli(['propose', '-'])).toEqual({ kind: 'stdin-propose' })
440
- expect(parseCli(['block', 'append', 'project', 'Uses', 'pnpm.'])).toEqual({ kind: 'call', call: 'memory.append', params: { block: 'project', text: 'Uses pnpm.' } })
441
- expect(parseCli(['block', 'replace', 'project', '--old', 'npm', '--new', 'pnpm'])).toEqual({ kind: 'call', call: 'memory.replace', params: { block: 'project', old: 'npm', new: 'pnpm' } })
442
425
  expect(parseCli(['search'])).toEqual({ kind: 'help' })
443
- expect(parseCli(['nonsense'])).toEqual({ kind: 'help' })
426
+ expect(parseCli(['block', 'append', 'x', 'y'])).toEqual({ kind: 'help' })
444
427
  })
445
428
  })
446
429
 
@@ -467,6 +450,25 @@ describe('decisions in the Harness-owned frontend', () => {
467
450
  await expect(socketCall(connection, 'terminal.trajectory.list', {}, 1000, key)).resolves.toEqual({ runs: [] })
468
451
  })
469
452
 
453
+ it('serves a bounded diagnostic snapshot only through the private terminal control channel', async () => {
454
+ const { connection, harness } = await setup()
455
+ await expect(socketCall(connection, 'terminal.logs.info')).rejects.toMatchObject({ code: 'unauthorized' })
456
+ const { key } = await socketCall<{ key: string }>(connection, 'control.claim')
457
+ await expect(socketCall(connection, 'terminal.logs.info', {}, 1000, key)).resolves.toEqual({ size: null })
458
+ await expect(socketCall(connection, 'terminal.logs.read', { snapshotSize: 1, offset: 0 }, 1000, key)).rejects.toMatchObject({ code: 'invalid_request' })
459
+
460
+ const text = `${'a'.repeat(LOG_READ_CHUNK_BYTES - 1)}🧠tail`
461
+ writeFileSync(join(harness.stateDir, 'harness.log'), text)
462
+ const { size } = await socketCall<{ size: number }>(connection, 'terminal.logs.info', {}, 1000, key)
463
+ expect(size).toBe(Buffer.byteLength(text))
464
+ await expect(socketCall(connection, 'terminal.logs.read', { snapshotSize: size, offset: size }, 1000, key)).rejects.toMatchObject({ code: 'invalid_request' })
465
+ const first = await socketCall<{ data: string; nextOffset: number }>(connection, 'terminal.logs.read', { snapshotSize: size, offset: 0 }, 1000, key)
466
+ const second = await socketCall<{ data: string; nextOffset: number }>(connection, 'terminal.logs.read', { snapshotSize: size, offset: first.nextOffset }, 1000, key)
467
+ expect(first.nextOffset).toBe(LOG_READ_CHUNK_BYTES)
468
+ expect(second.nextOffset).toBe(size)
469
+ expect(Buffer.concat([Buffer.from(first.data, 'base64'), Buffer.from(second.data, 'base64')]).toString('utf8')).toBe(text)
470
+ })
471
+
470
472
  it('requires the private input-owner key for pending decisions and exact draft state', async () => {
471
473
  const { connection, harness, tell } = await setup()
472
474
  await tell('ready', { nativeSessionId: '/native.jsonl' })
@@ -24,25 +24,27 @@ import { HarnessError, invalid } from '../core/errors'
24
24
  import { newId, now } from '../core/fs'
25
25
  import { prependSearchPath, searchPath } from '../core/path'
26
26
  import type { BrokerSession, ConnectedApps } from '../mcp/service'
27
- import type { CallerContext } from '../memory/caller'
28
- import { dispatch } from '../memory/dispatch'
27
+ import type { CallerContext } from '../core/caller'
28
+ import { dispatch } from '../tools/dispatch'
29
+ import { materialise, type HarnessChoice } from '../tree/materialise'
30
+ import type { TreeTrust } from '../tree/trust'
29
31
  import type { AttachParams, PluginScope } from '../plugins/api'
30
32
  import type { Loading, Plugins } from '../plugins/plugins'
31
33
  import type { Plugin } from '../plugins/types'
32
34
  import type { Hub } from '../hub/hub'
33
- import { compile, copyToRun, memoryDelta, record, render, saveForSession, type Launch } from '../prompt/compile'
35
+ import { compile, copyToRun, record, render, saveForSession, type Launch } from '../prompt/compile'
34
36
  import type { RunRecord } from '../sessions/contracts'
35
37
  import { isActiveStatus } from '../sessions/contracts'
36
38
  import type { ToolPolicy } from '../sessions/policy'
37
39
  import type { ActiveRun, RunRegistry } from '../sessions/registry'
38
40
  import { findRun, loadRun, saveRun, updateRun } from '../sessions/runStore'
39
- import { resources } from '../skills/registry'
40
41
  import type { ToolSpec } from '../tools/types'
41
42
  import { APPS_COMMAND, WIKI_COMMAND } from './cli'
42
43
  import { SOCKET_ENV, TOKEN_ENV } from './client'
43
44
  import { TerminalController, type PolicyReply } from './controller'
44
45
  import { resolvePiCommand, terminalCommandFiles } from './launch'
45
46
  import { SessionSocket, socketPath } from './socket'
47
+ import { harnessLogInfo, readHarnessLogChunk } from './logs'
46
48
  import { listTerminalTrajectoryRuns, readTerminalTrajectory } from './trajectory'
47
49
 
48
50
  /** Identity of the exact resource decision that this terminal was prepared with. */
@@ -86,6 +88,8 @@ export interface TerminalPrepareInput {
86
88
  readOnly?: boolean
87
89
  /** What the application adds to the instructions, and which of its tools it describes. */
88
90
  launch?: Launch
91
+ /** The harness tree the session runs; its version is pinned for the session. */
92
+ harness?: HarnessChoice | null
89
93
  /** Give the session the connected-apps broker. Defaults to yes. */
90
94
  connections?: boolean
91
95
  /** Fields copied onto each run this session records. */
@@ -111,6 +115,8 @@ interface Live {
111
115
  }
112
116
 
113
117
  export interface TerminalsOptions {
118
+ /** Which plugin code in harness trees a person trusted. */
119
+ trust?: TreeTrust | null
114
120
  subagents?: Subagents
115
121
  resolveCapabilities?: (caller: CallerContext, tools: readonly string[], cwd: string) => Promise<CapabilitySet>
116
122
  env: HarnessEnv
@@ -143,6 +149,8 @@ const READ_ONLY_TOOLS = new Set(['read', 'grep', 'find', 'ls'])
143
149
 
144
150
  export class Terminals {
145
151
  private readonly sessions = new Map<string, Live>()
152
+ /** The harness tree each live session runs, for loading its skills. */
153
+ private readonly treeDirs = new Map<string, string>()
146
154
  private readonly socket: SessionSocket
147
155
  private readonly assets: TerminalAssets
148
156
  /** A session's `/accounts` switch, taken once its Pi process has ended. */
@@ -166,7 +174,7 @@ export class Terminals {
166
174
  }
167
175
  })
168
176
  this.socket = new SessionSocket(socketPath(options.stateDir), {
169
- memory: (caller, call, params) => dispatch(options.env, caller, call, params),
177
+ memory: (caller, call, params) => dispatch(options.env, caller, call, params, { treeDir: (caller.sessionId && this.treeDirs.get(caller.sessionId)) || null }),
170
178
  apps: (session, call, params) => this.appsCall(session, call, params),
171
179
  plugins: (grant, call, params, control) => this.pluginsCall(grant.sessionId, call, params, control),
172
180
  accounts: (grant, call, params, control) => this.accountsCall(grant.sessionId, call, params, control)
@@ -512,8 +520,10 @@ export class Terminals {
512
520
  const broker = input.connections === false ? null : apps.grant({ sessionId, runId: null, caller, readOnly: !!input.readOnly, depth: 0 })
513
521
  // The session keeps these instructions; each of its runs records a copy, and a memory
514
522
  // block that changes later is sent along as a note.
515
- const pack = compile(env, caller, { input: '', ...input.launch })
516
- record(root, caller, pack)
523
+ const tree = input.harness ? materialise(root, input.harness, { trust: this.options.trust ?? null }) : null
524
+ if (tree) this.treeDirs.set(sessionId, tree.dir)
525
+ const pack = compile(env, caller, input.launch ?? {}, tree)
526
+ record(root, pack)
517
527
  mkdirSync(this.historyDir(input), { recursive: true })
518
528
  saveForSession(input.sessionFolder, pack)
519
529
 
@@ -543,6 +553,16 @@ export class Terminals {
543
553
  broker,
544
554
  terminal: { accept: (event, data) => live.controller.accept(event, data), disconnected: () => live.controller.disconnected(), control: async (call, params) => {
545
555
  if (call.startsWith('terminal.subagents.')) return this.subagentsCall(live, call, params)
556
+ if (call === 'terminal.logs.info') return harnessLogInfo(this.options.stateDir)
557
+ if (call === 'terminal.logs.read') {
558
+ const { snapshotSize, offset } = params
559
+ if (typeof snapshotSize !== 'number' || !Number.isSafeInteger(snapshotSize) || snapshotSize < 0 || typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0 || offset >= snapshotSize) {
560
+ throw invalid('A valid diagnostics snapshot size and byte offset are required.')
561
+ }
562
+ const current = harnessLogInfo(this.options.stateDir).size
563
+ if (current === null || snapshotSize > current) throw invalid('The diagnostics snapshot is no longer available.')
564
+ return readHarnessLogChunk(this.options.stateDir, snapshotSize, offset)
565
+ }
546
566
  if (call.startsWith('terminal.trajectory.')) return this.trajectoryCall(live, call, params)
547
567
  if (call === 'terminal.permissions.get') {
548
568
  const policy = this.options.approvals.read()
@@ -573,10 +593,9 @@ export class Terminals {
573
593
  if (loading) {
574
594
  // Pi's own discovery is off: the terminal loads what the harness found and nothing else.
575
595
  args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes')
576
- const enabled = resources(root, input.projectId ?? null)
577
- for (const path of [...loading.extensionPaths, ...loading.builtins]) args.push('--extension', path)
578
- for (const path of [...enabled.skillPaths, ...loading.skillPaths]) args.push('--skill', path)
579
- for (const path of [...enabled.promptPaths, ...loading.promptPaths]) args.push('--prompt-template', path)
596
+ for (const path of [...loading.extensionPaths, ...loading.builtins, ...(tree?.extensionPaths ?? [])]) args.push('--extension', path)
597
+ for (const path of [...(tree?.skillPaths ?? []), ...loading.skillPaths]) args.push('--skill', path)
598
+ for (const path of loading.promptPaths) args.push('--prompt-template', path)
580
599
  for (const path of loading.themePaths) args.push('--theme', path)
581
600
  }
582
601
  // Last, so its gate sees a tool call as every other extension left it.
@@ -615,10 +634,9 @@ export class Terminals {
615
634
  const runId = input.runId || newId()
616
635
  if (findRun(root, runId)) throw invalid('This run already exists.')
617
636
  saveRun(root, this.runRecord(live, runId, input.text))
618
- const note = memoryDelta(env, live.input.sessionFolder, { ...live.caller, runId })
619
637
  copyToRun(root, live.input.sessionFolder, runId)
620
638
  try {
621
- await live.controller.send(runId, note ? `${note}\n\n${input.text}` : input.text)
639
+ await live.controller.send(runId, input.text)
622
640
  } catch (error) {
623
641
  updateRun(root, runId, (run) => {
624
642
  if (!isActiveStatus(run.status)) return