@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
package/src/harness.ts CHANGED
@@ -1,6 +1,7 @@
1
- // The harness, assembled: Pi sessions on isolated accounts, a compiled prompt, a memory
2
- // lifecycle, tools, connected apps, and skills. An application embeds one of these and adds
3
- // its own prompt sections, tools, and policy; nothing here knows what that application is.
1
+ // The harness, assembled: Pi sessions on isolated accounts, versioned harness trees that shape
2
+ // them, refinement that improves those trees from evidence, a compiled prompt, project notes,
3
+ // tools, and connected apps. An application embeds one of these and adds its own prompt
4
+ // sections, tools, and policy; nothing here knows what that application is.
4
5
  import { Subagents } from './sessions/subagentService'
5
6
  import { mkdirSync } from 'node:fs'
6
7
  import { join } from 'node:path'
@@ -15,18 +16,15 @@ import { DEFAULT_PRODUCT_NAME, EMPTY_DIRECTORY, type Directory, type HarnessEnv
15
16
  import { HarnessError } from './core/errors'
16
17
  import { readJsonOr } from './core/fs'
17
18
  import { closeIndex } from './core/index'
18
- import { projectsOrNone } from './core/projects'
19
19
  import { ConnectedApps } from './mcp/service'
20
20
  import type { AppOAuthClients } from './mcp/types'
21
21
  import { migrateConnectionScopes } from './mcp/scopeMigration'
22
- import type { MaintainerFactory } from './memory/maintainer'
23
- import { Maintenance } from './memory/maintenance'
24
- import * as migrate from './memory/migrate'
25
- import { writeIndex } from './memory/proposals'
26
- import * as recall from './memory/recall'
27
- import * as store from './memory/store'
28
- import { countPages } from './memory/wiki'
29
- import * as wikiIndex from './memory/wikiIndex'
22
+ import * as recall from './history/recall'
23
+ import type { RefineHost } from './refine/host'
24
+ import { Refinements } from './refine/service'
25
+ import { configureBuiltins } from './tree/components'
26
+ import { TreeTrust } from './tree/trust'
27
+ import * as notes from './wiki/notes'
30
28
  import * as pluginApi from './plugins/api'
31
29
  import * as attachments from './plugins/attachments'
32
30
  import { HARNESS_PLUGINS } from './plugins/builtin'
@@ -40,7 +38,6 @@ import { RunRegistry } from './sessions/registry'
40
38
  import { markInterruptedRuns } from './sessions/runStore'
41
39
  import type { SessionFactory } from './sessions/runner'
42
40
  import { Sessions } from './sessions/sessions'
43
- import * as registry from './skills/registry'
44
41
  import { Terminals, type TerminalAssets } from './terminal/terminals'
45
42
 
46
43
  export interface HarnessOptions {
@@ -57,19 +54,13 @@ export interface HarnessOptions {
57
54
  policy?: ToolPolicy
58
55
  /** Replaces Pi, for tests. */
59
56
  sessionFactory?: SessionFactory
60
- maintainerFactory?: MaintainerFactory
61
- /** Delegate maintenance execution to the embedding host through durable requests. */
62
- externalMaintenance?: boolean
63
- maintenanceExcludedTriggers?: string[]
64
- /** Queue consolidation after every settled run (the default). False leaves memory to explicit proposals and scheduled merges. */
65
- maintenanceRunTrigger?: boolean
66
- /** How long the maintenance queue stays empty before full memory blocks are condensed. */
67
- idleMs?: number
57
+ /** Where refinement executions work and with what; without it, a temporary copy of the task's source and Pi's own tools. */
58
+ refineHost?: RefineHost
68
59
  /** Where the built commands a terminal session runs are, when they are not beside this package. */
69
60
  assets?: TerminalAssets
70
61
  /** Optional legacy executable override. The default is the Harness-owned terminal frontend. */
71
62
  piProgram?: () => string | null
72
- /** Folders of built-in skills, prompts, and rules, each laid out like `registry/`. */
63
+ /** Folders of built-in skills, prompts, and rules for the component library, each laid out like `registry/`. */
73
64
  builtins?: string[]
74
65
  /**
75
66
  * The application's own parts, described as plugins: which tools, prompt sections and tool
@@ -90,7 +81,10 @@ export interface HarnessOptions {
90
81
  /** What starting on a data root found and repaired. */
91
82
  export interface Recovery {
92
83
  interruptedRuns: number
84
+ /** Notes in the Wiki. */
93
85
  reindexedPages: number
86
+ /** Where an earlier Wiki's memory, facts and revisions were moved, when they were. */
87
+ legacyWiki?: string | null
94
88
  /** Set when the storage migration failed; the old data is left as it was. */
95
89
  migrationError: string | null
96
90
  }
@@ -133,7 +127,11 @@ export class Harness {
133
127
  applicationPlugins: ReadonlySet<string> = new Set()
134
128
  readonly subagents: Subagents
135
129
  readonly sessions: Sessions
136
- readonly maintenance: Maintenance
130
+ /** Which plugin code in harness trees a person trusted. */
131
+ readonly trust: TreeTrust
132
+ /** Refinement: improving a harness tree from evidence of how it ran. */
133
+ readonly refine: Refinements
134
+ refineHost: RefineHost | null
137
135
  readonly terminals: Terminals
138
136
  /** Resolves when what was loaded in the background at construction is in place. */
139
137
  readonly ready: Promise<void>
@@ -147,7 +145,9 @@ export class Harness {
147
145
  this.env = new Env(options.dataRoot || null, options.directory ?? EMPTY_DIRECTORY, options.productName ?? DEFAULT_PRODUCT_NAME, this.bus)
148
146
  this.hub = new Hub(this, options.hub)
149
147
  this.policy = options.policy ?? {}
150
- registry.configureBuiltins(options.builtins ?? [])
148
+ configureBuiltins(options.builtins ?? [])
149
+ this.trust = new TreeTrust(this.stateDir)
150
+ this.refineHost = options.refineHost ?? null
151
151
  const secrets = options.secrets ?? fileSecretStore(join(this.stateDir, 'secrets.json'))
152
152
  this.accounts = new Accounts({ dir: join(this.stateDir, 'accounts'), secrets, bus: this.bus })
153
153
  this.approvals = new Approvals(this.stateDir, this.bus)
@@ -200,10 +200,11 @@ export class Harness {
200
200
  broker: this.apps,
201
201
  plugins: this.plugins,
202
202
  resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }),
203
- factory: options.sessionFactory
203
+ factory: options.sessionFactory,
204
+ trust: this.trust
204
205
  })
205
- this.maintenance = new Maintenance({ env: this.env, bus: this.bus, accounts: this.accounts, factory: options.maintainerFactory, idleMs: options.idleMs, external: options.externalMaintenance, excludedTriggers: options.maintenanceExcludedTriggers, runTrigger: options.maintenanceRunTrigger })
206
- this.terminals = new Terminals({ subagents: this.subagents, resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }), env: this.env, stateDir: this.stateDir, accounts: this.accounts, runs: this.runs, approvals: this.approvals, apps: this.apps, policy: this.policy, bus: this.bus, plugins: this.plugins, pluginControl: { list: (scope) => pluginApi.list(this, scope), attach: (params, on) => pluginApi.attach(this, params, on), trust: (scope) => pluginApi.trust(this, scope), untrust: (scope) => pluginApi.untrust(this, scope) }, assets: options.assets, program: options.piProgram, hub: this.hub })
206
+ this.refine = new Refinements({ root: () => this.env.root, bus: this.bus, trust: this.trust, host: () => this.refineHost, runtime: { root: () => this.env.root, runs: this.runs, run: (input) => this.sessions.run(input) } })
207
+ this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }), env: this.env, stateDir: this.stateDir, accounts: this.accounts, runs: this.runs, approvals: this.approvals, apps: this.apps, policy: this.policy, bus: this.bus, plugins: this.plugins, pluginControl: { list: (scope) => pluginApi.list(this, scope), attach: (params, on) => pluginApi.attach(this, params, on), trust: (scope) => pluginApi.trust(this, scope), untrust: (scope) => pluginApi.untrust(this, scope) }, assets: options.assets, program: options.piProgram, hub: this.hub })
207
208
  this.ready = this.apps.start().catch((error) => log('connected apps', error instanceof Error ? error.message : String(error)))
208
209
  this.runs.listen((event, run) => {
209
210
  this.accounts.onRunEvent(event, run)
@@ -212,8 +213,6 @@ export class Harness {
212
213
  if ((event.type === 'completed' || event.type === 'failed') && run) {
213
214
  this.hooks.emit({ type: 'history.run.ended', runId: run.id, sessionId: run.sessionId, status: run.status, goal: run.goal ?? null, agentId: run.agentId ?? null, projectId: run.projectId ?? null, startedAt: run.startedAt, endedAt: run.endedAt ?? null, at: event.timestamp })
214
215
  }
215
- if (event.type === 'completed' || event.type === 'failed' || (event.type === 'status.changed' && (event.payload as { status?: string }).status === 'interrupted')) this.maintenance.finishRun(event.runId, event.type !== 'completed')
216
- this.maintenance.onRunEvent(event)
217
216
  })
218
217
  }
219
218
 
@@ -230,27 +229,28 @@ export class Harness {
230
229
  }
231
230
 
232
231
  /**
233
- * Opens a data root: brings its Wiki to the current layout, settles runs that never ended,
234
- * and refreshes the index. `agentIds` are the agents that get their own memory blocks when an
235
- * older Wiki is migrated.
232
+ * Opens a data root: moves an earlier Wiki's memory and facts aside (once), lays out the Wiki,
233
+ * settles runs that never ended, and refreshes the history index. `agentIds` is accepted for
234
+ * hosts written against earlier versions and is not used.
236
235
  */
237
- open(dataRoot: string, agentIds: readonly string[] = []): Recovery {
236
+ open(dataRoot: string, _agentIds: readonly string[] = []): Recovery {
238
237
  if (this.hub.busy && this.env.dataRoot !== dataRoot) throw new HarnessError('hub_busy', 'Wait for the hub installation before changing the data location.')
239
238
  if (this.env.dataRoot && this.env.dataRoot !== dataRoot) closeIndex(this.env.dataRoot)
240
239
  this.env.dataRoot = dataRoot
241
240
  this.hub.watch(dataRoot)
242
241
  const root = dataRoot
243
- this.hooks.watch(store.wikiDir(root))
242
+ this.hooks.watch(notes.wikiDir(root))
244
243
  void this.hooks.reload()
245
- for (const folder of ['registry/skills', 'registry/prompts', 'registry/rules', 'config', 'runs']) mkdirSync(join(root, folder), { recursive: true })
244
+ for (const folder of ['registry/skills', 'registry/prompts', 'registry/rules', 'config', 'runs', 'harnesses']) mkdirSync(join(root, folder), { recursive: true })
246
245
  let migrationError: string | null = null
247
- let wikiMigrationError: string | null = null
246
+ let legacyWiki: string | null = null
248
247
  try {
249
- if (migrate.run(this.env, agentIds)) log(`started Wiki storage version ${migrate.WIKI_VERSION}; the earlier Wiki is kept as a backup in the data root`)
248
+ legacyWiki = notes.moveLegacy(root)
249
+ if (legacyWiki) log(`moved the earlier Wiki's memory, facts and revisions to ${legacyWiki}`)
250
+ notes.ensureLayout(root, this.env.productName)
250
251
  } catch (error) {
251
252
  migrationError = error instanceof Error ? error.message : String(error)
252
- wikiMigrationError = migrationError
253
- log('Wiki migration failed', migrationError)
253
+ log('Wiki layout', migrationError)
254
254
  }
255
255
  try {
256
256
  migrateConnectionScopes(root, this.stateDir)
@@ -259,55 +259,36 @@ export class Harness {
259
259
  migrationError = migrationError ? `${migrationError}; connector scope migration: ${detail}` : detail
260
260
  log('connector scope migration failed', detail)
261
261
  }
262
- if (!wikiMigrationError) {
263
- try {
264
- store.ensureLayout(root, this.env.productName)
265
- // Every project has its fixed pages, and the generated indexes match the files.
266
- for (const project of projectsOrNone(root)) store.ensureProjectNamespace(root, project.slug, project.name)
267
- writeIndex(store.wikiDir(root))
268
- } catch (error) {
269
- log('Wiki layout', error instanceof Error ? error.message : String(error))
270
- }
271
- }
272
262
  let interruptedRuns = 0
273
263
  try {
274
264
  interruptedRuns = markInterruptedRuns(root, `${this.env.productName} stopped before the run settled.`)
275
265
  } catch (error) {
276
266
  log('run recovery', error instanceof Error ? error.message : String(error))
277
267
  }
278
- let reindexedPages: number
279
- try {
280
- reindexedPages = wikiIndex.ensure(root)
281
- } catch (error) {
282
- log('index', error instanceof Error ? error.message : String(error))
283
- reindexedPages = countPages(root)
284
- }
285
268
  try {
286
- // Runs recorded while nothing was running, or before recall existed, become searchable.
269
+ // Runs recorded while nothing was running become searchable.
287
270
  recall.sync(root)
288
271
  } catch (error) {
289
272
  log('recall', error instanceof Error ? error.message : String(error))
290
273
  }
291
- this.recovery = { interruptedRuns, reindexedPages, migrationError }
292
- this.maintenance.start()
274
+ this.recovery = { interruptedRuns, reindexedPages: safeCount(root), migrationError, legacyWiki }
293
275
  return this.recovery
294
276
  }
295
277
 
296
- /** Deletes the index and rebuilds it from the files. Returns the pages indexed. */
278
+ /** Rebuilds the history index from the run records. Returns the notes in the Wiki. */
297
279
  rebuildIndex(): number {
298
- const pages = wikiIndex.rebuild(this.env.root)
299
280
  recall.sync(this.env.root)
300
- this.recovery = { ...this.recovery, reindexedPages: pages }
301
- return pages
281
+ const count = safeCount(this.env.root)
282
+ this.recovery = { ...this.recovery, reindexedPages: count }
283
+ return count
302
284
  }
303
285
 
304
- /** Stops live runs and releases the index. */
286
+ /** Stops live runs and refinements and releases the index. */
305
287
  async close(): Promise<void> {
306
288
  this.hub.close()
307
- this.maintenance.stop()
289
+ await this.refine.close()
308
290
  await this.terminals.close()
309
291
  await this.runs.interruptAll()
310
- await this.maintenance.settled()
311
292
  await this.hooks.settled().catch(() => undefined)
312
293
  this.hooks.close()
313
294
  await this.apps.close()
@@ -315,6 +296,14 @@ export class Harness {
315
296
  }
316
297
  }
317
298
 
299
+ function safeCount(root: string): number {
300
+ try {
301
+ return notes.paths(root).length
302
+ } catch {
303
+ return 0
304
+ }
305
+ }
306
+
318
307
  export function createHarness(options: HarnessOptions): Harness {
319
308
  const harness = new Harness(options)
320
309
  if (options.dataRoot) harness.open(options.dataRoot)
@@ -9,7 +9,7 @@ import { newId, writeJson } from '../core/fs'
9
9
  import { closeIndex } from '../core/index'
10
10
  import { runRecordSchema, type RunRecord } from '../sessions/contracts'
11
11
  import { parseEvent } from '../sessions/runStore'
12
- import type { CallerContext } from './caller'
12
+ import type { CallerContext } from '../core/caller'
13
13
  import { passages, read, readTool, search, searchTool, sync } from './recall'
14
14
 
15
15
  const run = (id: string, mode: string, session: string, project: string | null, goal: string): RunRecord =>
@@ -12,7 +12,7 @@ import { charCount, takeChars } from '../core/text'
12
12
  import type { RunEvent, RunRecord } from '../sessions/contracts'
13
13
  import { readAllRuns } from '../sessions/runIndex'
14
14
  import { findRun, readEvents } from '../sessions/runStore'
15
- import { isSession, projectOf, type CallerContext } from './caller'
15
+ import { isSession, projectOf, type CallerContext } from '../core/caller'
16
16
 
17
17
  const PASSAGE_LIMIT = 6000
18
18
  const READ_LIMIT = 12_000
@@ -8,9 +8,9 @@ import { closeIndex } from '../core/index'
8
8
  import { writeJson } from '../core/fs'
9
9
  import { runRecordSchema, type RunEvent, type RunRecord } from '../sessions/contracts'
10
10
  import { appendEvent, saveRun } from '../sessions/runStore'
11
- import { WIKI_AGENT_TOOLS } from '../tools/definitions'
12
- import type { CallerContext } from './caller'
13
- import { dispatch } from './dispatch'
11
+ import { HARNESS_TOOLS } from '../tools/definitions'
12
+ import type { CallerContext } from '../core/caller'
13
+ import { dispatch } from '../tools/dispatch'
14
14
  import { readTool, searchTool } from './trajectory'
15
15
 
16
16
  const roots: string[] = []
@@ -170,7 +170,7 @@ test('trajectory calls reject malformed filters and missing current runs', () =>
170
170
  test('the model tool registry exposes both trajectory calls through Wiki dispatch', () => {
171
171
  const { root, env } = setup()
172
172
  save(root, run('registered-run'))
173
- const calls = WIKI_AGENT_TOOLS.map((tool) => tool.call)
173
+ const calls = HARNESS_TOOLS.map((tool) => tool.call)
174
174
  expect(calls).toContain('trajectory.search')
175
175
  expect(calls).toContain('trajectory.read')
176
176
  expect(JSON.parse(dispatch(env, agent, 'trajectory.search', {})).runs[0].id).toBe('registered-run')
@@ -8,7 +8,7 @@ import { scrubSensitive } from '../mcp/connections'
8
8
  import { isActiveStatus, runStatusSchema, type RunEvent, type RunRecord } from '../sessions/contracts'
9
9
  import { readAllRuns } from '../sessions/runIndex'
10
10
  import { findRun, snapshot } from '../sessions/runStore'
11
- import type { CallerContext } from './caller'
11
+ import type { CallerContext } from '../core/caller'
12
12
 
13
13
  const SEARCH_DEFAULT = 10
14
14
  const SEARCH_MAX = 30
@@ -6,7 +6,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
6
6
  import { Bus } from '../core/bus'
7
7
  import { Harness } from '../harness'
8
8
  import { memorySecretStore } from '../accounts/secrets'
9
- import * as registry from '../skills/registry'
9
+ import * as registry from '../tree/components'
10
10
  import { Hub } from './hub'
11
11
  import { hubCatalogSchema, hubEntrySchema, type HubEntry } from './schema'
12
12
  import { runHubMenu } from '../terminal/hubMenu'
@@ -47,7 +47,7 @@ describe('Hub installation', () => {
47
47
  const { hub, root } = fixture([entry('registry', skill())])
48
48
  const receipt = await hub.install('example/review', '1.0.0')
49
49
  expect(receipt.resourceIds).toEqual(['skills/review'])
50
- expect(registry.enabledFor(root, null)).toEqual([])
50
+ expect(registry.list(root).map((item) => item.id)).toContain('skills/review')
51
51
  const content = readFileSync(join(root, 'registry', 'skills', 'review', 'SKILL.md'), 'utf8')
52
52
  const collision = { ...entry('registry', skill()), id: 'other/review' }
53
53
  const another = new Hub(({ dataRoot: root, env: { root }, stateDir: join(root, 'state'), bus: new Bus() }) as Harness, { catalog: { schemaVersion: 1, entries: [collision] } })
@@ -67,7 +67,7 @@ describe('Hub installation', () => {
67
67
  const { hub, root } = fixture([item])
68
68
  await hub.install(item.id, item.version)
69
69
  expect(readFileSync(join(root, 'registry', kind, 'review.md'), 'utf8')).toContain('Read evidence')
70
- expect(registry.enabledFor(root, null)).toEqual([])
70
+ expect(registry.list(root).map((entry) => entry.id)).toContain(`${kind}/review`)
71
71
  }
72
72
  })
73
73
  it('does not commit a download with the wrong digest', async () => {
@@ -140,12 +140,11 @@ describe('Hub installation', () => {
140
140
  it('can reinstall deleted resources and clears stale attachment grants', async () => {
141
141
  const { hub, root } = fixture([entry('registry', skill())])
142
142
  await hub.install('example/review', '1.0.0')
143
- registry.setEnabled(root, null, ['skills/review'])
144
143
  rmSync(join(root, 'registry', 'skills', 'review'), { recursive: true })
145
144
  expect((await hub.list()).installed[0]).toMatchObject({ state: 'incomplete' })
146
145
  await hub.uninstall('example/review', '1.0.0')
147
146
  await hub.install('example/review', '1.0.0')
148
- expect(registry.enabledFor(root, null)).toEqual([])
147
+ expect(registry.list(root).map((entry) => entry.id)).toEqual(['skills/review'])
149
148
  expect((await hub.list()).installed).toHaveLength(1)
150
149
  })
151
150
  it('preserves corrupt installation records rather than overwriting them', async () => {
package/src/hub/hub.ts CHANGED
@@ -8,7 +8,7 @@ import { atomicWrite, now, readJson, readJsonOr, writeJson } from '../core/fs'
8
8
  import type { Harness } from '../harness'
9
9
  import * as pluginApi from '../plugins/api'
10
10
  import * as packages from '../plugins/install'
11
- import * as registry from '../skills/registry'
11
+ import * as registry from '../tree/components'
12
12
  import * as attachments from '../plugins/attachments'
13
13
  import { version as harnessVersion } from '../../package.json'
14
14
  import bundled from './catalog.json'
@@ -244,7 +244,7 @@ export class Hub {
244
244
  const payload = JSON.parse(text)
245
245
  const previous = new Set(registry.list(root).map((item) => item.id))
246
246
  const imported = registry.importItems(root, delivery.registryKind, payload.items)
247
- receipt.resourceIds = imported.items.filter((item) => !previous.has(item.id)).map((item) => item.id)
247
+ receipt.resourceIds = imported.filter((item) => !previous.has(item.id)).map((item) => item.id)
248
248
  const choices = structuredClone(choicesBefore)
249
249
  const ids = new Set(receipt.resourceIds)
250
250
  for (const scope of [choices.global, ...Object.values(choices.agents), ...Object.values(choices.projects)]) {
package/src/index.ts CHANGED
@@ -1,11 +1,13 @@
1
- // @xingular/harness: a Pi agent harness. Multi-account Pi sessions, a compiled system prompt
2
- // with stable-prefix delivery, a memory lifecycle (Wiki, blocks, facts, recall, sleep-time
3
- // consolidation), tools with a gate and standing approvals, connected apps over MCP, and
4
- // skills (a person's own and built-in).
1
+ // @xingular/harness: a Pi agent harness built on recursive harnessing (Harness², Apache-2.0,
2
+ // Google LLC). Each agent runs on a versioned harness tree of eight components (system prompt,
3
+ // guardrails, memory, tool guidance, subagents, skills, scripts, plugins); refinement improves a
4
+ // tree from evidence through Propose–Probe–Compose, blind selection and an experience library.
5
+ // Around it: multi-account Pi sessions, a compiled prompt with stable-prefix delivery, project
6
+ // notes (the Wiki), history, tools with a gate and standing approvals, and connected apps.
5
7
  //
6
8
  // An application embeds it with `createHarness` and extends it through prompt sections, tool
7
- // providers, a tool policy, a secret store, a directory of its own names, and a description of
8
- // its own parts as plugins. Everything else is reachable by path, e.g. `@xingular/harness/memory/wiki`.
9
+ // providers, a tool policy, a secret store, a directory of its own names, a description of its
10
+ // own parts as plugins, and a refinement host. Everything else is reachable by path.
9
11
  export { createHarness, Harness, type HarnessOptions, type Recovery } from './harness'
10
12
  export { hubCatalogSchema, hubEntrySchema, hubOverviewSchema, hubReceiptSchema, type HubCatalog, type HubEntry, type HubOverview, type HubReceipt } from './hub/schema'
11
13
 
@@ -20,8 +22,8 @@ export type { AccountLoginSnapshot, ModelOption } from './accounts/accounts'
20
22
  export type { Capability, CapabilityCatalogue, CapabilityKind, CapabilityScope, CapabilitySet } from './capabilities/types'
21
23
  export type { RunCapabilitySnapshot } from './capabilities/runSnapshot'
22
24
 
23
- export type { CallerContext } from './memory/caller'
24
- export { dispatch as memoryCall } from './memory/dispatch'
25
+ export type { CallerContext } from './core/caller'
26
+ export { dispatch as harnessCall } from './tools/dispatch'
25
27
 
26
28
  export {
27
29
  compile,
@@ -30,7 +32,6 @@ export {
30
32
  CONTEXT_MESSAGE_TYPE,
31
33
  CONTEXT_SNAPSHOT_KEY,
32
34
  type ContextPack,
33
- type ContextProfile,
34
35
  type HostSection,
35
36
  type HostToolGroup,
36
37
  type Launch,
@@ -45,19 +46,32 @@ export type { ActiveRun, ApprovalDecision, ApprovalInput, QuestionAnswer, Questi
45
46
  export type { RunFields, SessionRunInput } from './sessions/sessions'
46
47
  export type { Effort } from './sessions/runner'
47
48
 
48
- export { describeTool, WIKI_TOOLS, WIKI_TOOL_NAMES, type WikiCall, type WikiToolDefinition } from './tools/definitions'
49
+ export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE, type HarnessCall, type HarnessToolDefinition } from './tools/definitions'
49
50
  export type { ToolProvider, ToolResult, ToolSession, ToolSpec } from './tools/types'
50
51
  export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from './tools/spawnSubagent'
51
- export { maintenanceTools, MAINTENANCE_PLUGIN, MAINTENANCE_TOOL_NAMES } from './tools/maintenance'
52
- export type { CurationJob } from './memory/curation'
53
52
  export { webTools, WEB_PLUGIN, WEB_SEARCH_KEY, type WebToolOptions } from './web/tools'
54
- export { registryTools, REGISTRY_WRITE, PLUGIN_WRITE, REGISTRY_WRITE_PLUGIN, type RegistryToolOptions } from './tools/registryWrite'
55
53
  export { safeRequest, type WebRequest, type WebResponse } from './web/safeRequest'
56
54
 
57
55
  export type { BrokeredTools, BrokerSession, BrokerTool } from './mcp/service'
58
56
  export type { Connection, ConnectionInput, AppOAuthClients } from './mcp/types'
59
57
 
60
- export { configureBuiltins, type RegistryItem, type RegistryState, type SkillBundleInput } from './skills/registry'
58
+ export { configureBuiltins, type ComponentItem, type SkillBundleInput } from './tree/components'
59
+
60
+ // The harness tree and refinement.
61
+ export { PI_SURFACE, componentOf, type ComponentGroup, type ComponentId, type ComponentInfo, type SurfaceSpec, type Tree } from './tree/surface'
62
+ export { validate as validateTree, autotrigger, diffSummary, TreeError, type TreeCensus, type TreeDiff } from './tree/tree'
63
+ export { fingerprint, type Head, type VersionRecord, type VersionSource } from './tree/store'
64
+ export { materialise, type HarnessChoice, type Materialised } from './tree/materialise'
65
+ export { seedTree, type SeedInput } from './tree/seed'
66
+ export type { TrustedPlugin } from './tree/trust'
67
+ export { DEFAULT_REFINE_HOST, defaultDeliverable, type RefineHost, type RolloutSession, type RolloutWorkspace } from './refine/host'
68
+ export { PROBE_TRIGGER, IMPROVER_TRIGGER, type ImproverSettings } from './refine/pi'
69
+ export type { CandidateView, StartInput as RefineStartInput } from './refine/service'
70
+ export type { RefineEvent, RefineStatus, RefineSummary } from './refine/runner'
71
+ export type { Executor, Improver, RecursionMode, RefineTask, Rollout } from './refine/types'
72
+ export type { WorkPrompts } from './refine/prompts'
73
+ export type { Experience } from './refine/library'
74
+ export type { Note, NoteSummary } from './wiki/notes'
61
75
 
62
76
  export type { TerminalAssets, TerminalLaunch, TerminalPrepareInput } from './terminal/terminals'
63
77
 
@@ -8,7 +8,7 @@ import type { Bus } from '../core/bus'
8
8
  import { HarnessError, invalid, notFound } from '../core/errors'
9
9
  import { newId, readJson, safeId, writeJson } from '../core/fs'
10
10
  import { findBinary, pathVariableName, searchPath } from '../core/path'
11
- import { isDir } from '../memory/store'
11
+ import { isDir } from '../core/fs'
12
12
  import type { RunEvent } from '../sessions/contracts'
13
13
  import * as attachments from '../plugins/attachments'
14
14
  import { connectionScopeMigrationComplete } from './scopeMigration'
@@ -11,7 +11,7 @@ import { HarnessError, invalid, notFound } from '../core/errors'
11
11
  import { newId, now, readJson, writeJson } from '../core/fs'
12
12
  import { charCount, takeChars } from '../core/text'
13
13
  import type { CapabilitySet } from '../capabilities/types'
14
- import type { CallerContext } from '../memory/caller'
14
+ import type { CallerContext } from '../core/caller'
15
15
  import type { ToolPolicy } from '../sessions/policy'
16
16
  import type { ActiveRun, RunRegistry } from '../sessions/registry'
17
17
  import { findRun, runDir } from '../sessions/runStore'
@@ -0,0 +1,68 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { createServer } from 'node:http'
3
+ import { promisify } from 'node:util'
4
+ import { afterEach, expect, it } from 'vitest'
5
+
6
+ const execFileAsync = promisify(execFile)
7
+ const servers: ReturnType<typeof createServer>[] = []
8
+ afterEach(async () => {
9
+ for (const server of servers.splice(0)) await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
10
+ })
11
+
12
+ it('routes Pi HTTP requests through the process proxy environment', async () => {
13
+ const requests: Array<{ url: string | undefined; host: string | undefined; tunnel: boolean }> = []
14
+ const proxy = createServer((request, response) => {
15
+ requests.push({ url: request.url, host: request.headers.host, tunnel: false })
16
+ response.writeHead(200, { 'content-type': 'text/plain' })
17
+ response.end('served by local proxy')
18
+ })
19
+ proxy.on('connect', (request, socket, head) => {
20
+ requests.push({ url: request.url, host: request.headers.host, tunnel: true })
21
+ socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
22
+ let received = head.toString('utf8')
23
+ const respond = (): void => {
24
+ if (!received.includes('\r\n\r\n')) return
25
+ const body = 'served by local proxy'
26
+ socket.end(`HTTP/1.1 200 OK\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`)
27
+ }
28
+ socket.on('data', chunk => {
29
+ received += chunk.toString('utf8')
30
+ respond()
31
+ })
32
+ respond()
33
+ })
34
+ servers.push(proxy)
35
+ await new Promise<void>((resolve, reject) => {
36
+ proxy.once('error', reject)
37
+ proxy.listen(0, '127.0.0.1', resolve)
38
+ })
39
+
40
+ const address = proxy.address()
41
+ if (!address || typeof address === 'string') throw new Error('The local proxy did not bind to a TCP port.')
42
+ const proxyUrl = `http://127.0.0.1:${address.port}`
43
+ const script = `
44
+ import { join } from 'node:path'
45
+ import { pathToFileURL } from 'node:url'
46
+
47
+ const pi = await import('@earendil-works/pi-coding-agent')
48
+ const dispatcherUrl = pathToFileURL(join(pi.getPackageDir(), 'dist/bundle/chunks/chunk-TJXZSVTD.js')).href
49
+ const { configureHttpDispatcher } = await import(dispatcherUrl)
50
+ configureHttpDispatcher()
51
+ const response = await fetch('http://127.0.0.2/dispatcher-check')
52
+ if (response.status !== 200 || await response.text() !== 'served by local proxy') throw new Error('Pi did not receive the local proxy response')
53
+ `
54
+ const env: NodeJS.ProcessEnv = {
55
+ ...(process.env.PATH ? { PATH: process.env.PATH } : {}),
56
+ HTTP_PROXY: proxyUrl,
57
+ HTTPS_PROXY: proxyUrl,
58
+ NO_PROXY: 'localhost,127.0.0.1'
59
+ }
60
+
61
+ await execFileAsync(process.execPath, ['--input-type=module', '-e', script], {
62
+ cwd: process.cwd(),
63
+ env,
64
+ timeout: 10_000
65
+ })
66
+
67
+ expect(requests).toEqual([{ url: '127.0.0.2:80', host: '127.0.0.2', tunnel: true }])
68
+ })
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { createPiLoader } from './runtime'
4
+
5
+ describe('Pi runtime initialization', () => {
6
+ it('configures Pi networking before returning the module and shares concurrent initialization', async () => {
7
+ const events: string[] = []
8
+ const pi = { VERSION: '1.2.0', getPackageDir: () => '/pinned/pi' }
9
+ const load = createPiLoader(
10
+ async () => {
11
+ events.push('import Pi')
12
+ return pi
13
+ },
14
+ async (packageDir) => {
15
+ events.push(`load dispatcher from ${packageDir}`)
16
+ return { configureHttpDispatcher: () => events.push('configure dispatcher') }
17
+ }
18
+ )
19
+
20
+ const [first, second] = await Promise.all([load(), load()])
21
+
22
+ expect(first).toBe(pi)
23
+ expect(second).toBe(pi)
24
+ expect(events).toEqual(['import Pi', 'load dispatcher from /pinned/pi', 'configure dispatcher'])
25
+ })
26
+
27
+ it('rejects a Pi version change before loading its private dispatcher', async () => {
28
+ const loadDispatcher = vi.fn(async () => ({ configureHttpDispatcher: vi.fn() }))
29
+ const load = createPiLoader(async () => ({ VERSION: '1.3.0', getPackageDir: () => '/other/pi' }), loadDispatcher)
30
+
31
+ await expect(load()).rejects.toThrow('requires Pi 1.2.0')
32
+ expect(loadDispatcher).not.toHaveBeenCalled()
33
+ })
34
+
35
+ it('reports when Pi no longer exposes its private dispatcher hook', async () => {
36
+ const load = createPiLoader(
37
+ async () => ({ VERSION: '1.2.0', getPackageDir: () => '/pinned/pi' }),
38
+ async () => ({})
39
+ )
40
+
41
+ await expect(load()).rejects.toThrow('pinned Pi 1.2.0 HTTP dispatcher hook')
42
+ })
43
+
44
+ it('can retry initialization after a transient dispatcher load failure', async () => {
45
+ const configure = vi.fn()
46
+ let attempts = 0
47
+ const load = createPiLoader(
48
+ async () => ({ VERSION: '1.2.0', getPackageDir: () => '/pinned/pi' }),
49
+ async () => {
50
+ attempts++
51
+ if (attempts === 1) throw new Error('temporary import failure')
52
+ return { configureHttpDispatcher: configure }
53
+ }
54
+ )
55
+
56
+ await expect(load()).rejects.toThrow('temporary import failure')
57
+ await expect(load()).resolves.toMatchObject({ VERSION: '1.2.0' })
58
+ expect(attempts).toBe(2)
59
+ expect(configure).toHaveBeenCalledOnce()
60
+ })
61
+ })
@@ -0,0 +1,50 @@
1
+ import { join } from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
3
+
4
+ export const PINNED_PI_VERSION = '1.2.0'
5
+ const HTTP_DISPATCHER_CHUNK = 'dist/bundle/chunks/chunk-TJXZSVTD.js'
6
+
7
+ type PiRuntimeModule = {
8
+ VERSION: string
9
+ getPackageDir(): string
10
+ }
11
+
12
+ type PiDispatcherModule = {
13
+ configureHttpDispatcher?: unknown
14
+ }
15
+
16
+ export function assertPiRuntimeVersion(version: string): void {
17
+ if (version !== PINNED_PI_VERSION) throw new Error(`The Harness Pi runtime requires Pi ${PINNED_PI_VERSION}; found ${version}. Install the Harness package's pinned dependencies.`)
18
+ }
19
+
20
+ /** Builds a cached Pi loader so initialization order and compatibility behavior can be verified. */
21
+ export function createPiLoader<T extends PiRuntimeModule>(
22
+ importPi: () => Promise<T>,
23
+ importDispatcher: (packageDir: string) => Promise<PiDispatcherModule>
24
+ ): () => Promise<T> {
25
+ let loading: Promise<T> | undefined
26
+
27
+ return () => {
28
+ if (!loading) {
29
+ loading = (async () => {
30
+ const pi = await importPi()
31
+ assertPiRuntimeVersion(pi.VERSION)
32
+ const dispatcher = await importDispatcher(pi.getPackageDir())
33
+ if (typeof dispatcher.configureHttpDispatcher !== 'function') {
34
+ throw new Error(`The Harness requires the pinned Pi ${PINNED_PI_VERSION} HTTP dispatcher hook; install the Harness package's pinned dependencies.`)
35
+ }
36
+ dispatcher.configureHttpDispatcher()
37
+ return pi
38
+ })().catch((error: unknown) => {
39
+ loading = undefined
40
+ throw error
41
+ })
42
+ }
43
+ return loading
44
+ }
45
+ }
46
+
47
+ export const loadPi = createPiLoader(
48
+ () => import('@earendil-works/pi-coding-agent'),
49
+ async (packageDir) => await import(pathToFileURL(join(packageDir, HTTP_DISPATCHER_CHUNK)).href) as PiDispatcherModule
50
+ )