@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.
- package/CHANGELOG.md +18 -0
- package/NOTICE.md +22 -0
- package/README.md +70 -37
- package/dist/cli.js +6572 -6700
- package/dist/terminal-pi.js +51 -38
- package/docs/terminal-tui.md +50 -12
- package/package.json +2 -2
- package/src/accounts/accounts.ts +4 -3
- package/src/capabilities/api.ts +1 -1
- package/src/cli.ts +7 -3
- package/src/core/bus.test.ts +15 -1
- package/src/core/bus.ts +39 -0
- package/src/core/caller.ts +34 -0
- package/src/core/fs.ts +17 -1
- package/src/harness.ts +55 -66
- package/src/{memory → history}/recall.test.ts +1 -1
- package/src/{memory → history}/recall.ts +1 -1
- package/src/{memory → history}/trajectory.test.ts +4 -4
- package/src/{memory → history}/trajectory.ts +1 -1
- package/src/hub/hub.test.ts +4 -5
- package/src/hub/hub.ts +2 -2
- package/src/index.ts +28 -14
- package/src/mcp/connections.ts +1 -1
- package/src/mcp/service.ts +1 -1
- package/src/pi/proxy.test.ts +68 -0
- package/src/pi/runtime.test.ts +61 -0
- package/src/pi/runtime.ts +50 -0
- package/src/plugins/api.ts +11 -9
- package/src/plugins/builtin.ts +22 -40
- package/src/plugins/catalogue.test.ts +13 -41
- package/src/plugins/catalogue.ts +3 -32
- package/src/plugins/discover.ts +2 -1
- package/src/plugins/firstParty.test.ts +1 -1
- package/src/plugins/hooks.test.ts +21 -71
- package/src/plugins/hooks.ts +6 -7
- package/src/plugins/identity.ts +16 -2
- package/src/plugins/install.test.ts +1 -1
- package/src/plugins/install.ts +2 -1
- package/src/plugins/inventory.ts +19 -18
- package/src/plugins/plugins.ts +38 -2
- package/src/prompt/compile.test.ts +82 -193
- package/src/prompt/compile.ts +98 -363
- package/src/refine/cli.ts +114 -0
- package/src/refine/demo.ts +124 -0
- package/src/refine/evidence.ts +349 -0
- package/src/refine/host.ts +102 -0
- package/src/refine/ledger.ts +104 -0
- package/src/refine/library.ts +192 -0
- package/src/refine/pi.test.ts +90 -0
- package/src/refine/pi.ts +218 -0
- package/src/refine/prompts.ts +430 -0
- package/src/refine/recursion.ts +353 -0
- package/src/refine/refine.test.ts +140 -0
- package/src/refine/runner.ts +205 -0
- package/src/refine/selection.ts +291 -0
- package/src/refine/service.ts +194 -0
- package/src/refine/trajectory.ts +71 -0
- package/src/refine/types.ts +91 -0
- package/src/server/methods.ts +137 -123
- package/src/server/server.test.ts +37 -30
- package/src/sessions/contracts.ts +7 -2
- package/src/sessions/conversation.test.ts +2 -27
- package/src/sessions/planMode.test.ts +8 -1
- package/src/sessions/planMode.ts +17 -9
- package/src/sessions/policy.ts +1 -1
- package/src/sessions/pricing.ts +3 -1
- package/src/sessions/questionBatches.test.ts +15 -3
- package/src/sessions/registry.ts +13 -1
- package/src/sessions/runStore.ts +3 -1
- package/src/sessions/runner.ts +10 -5
- package/src/sessions/sessions.test.ts +60 -13
- package/src/sessions/sessions.ts +43 -18
- package/src/sessions/subagentService.test.ts +22 -1
- package/src/sessions/subagentService.ts +14 -1
- package/src/sessions/subagents.ts +2 -0
- package/src/terminal/cli.ts +24 -48
- package/src/terminal/logs.test.ts +26 -0
- package/src/terminal/logs.ts +67 -0
- package/src/terminal/piExtension.lifecycle.test.ts +1 -1
- package/src/terminal/piExtension.ts +3 -3
- package/src/terminal/pluginControl.test.ts +7 -8
- package/src/terminal/socket.ts +3 -3
- package/src/terminal/terminal.test.ts +41 -39
- package/src/terminal/terminals.ts +31 -13
- package/src/terminal/tui/application.test.ts +58 -0
- package/src/terminal/tui/application.ts +15 -4
- package/src/terminal/tui/clipboardFiles.test.ts +94 -0
- package/src/terminal/tui/clipboardFiles.ts +86 -0
- package/src/terminal/tui/commands.test.ts +13 -1
- package/src/terminal/tui/commands.ts +16 -0
- package/src/terminal/tui/logPanel.test.ts +61 -0
- package/src/terminal/tui/logPanel.ts +175 -0
- package/src/terminal/tui/piAdapter.ts +7 -4
- package/src/terminal/tui/questionPicker.ts +168 -0
- package/src/terminal/tui/questions.test.ts +86 -19
- package/src/terminal/tui/questions.ts +27 -22
- package/src/terminal/tui/runtime.ts +2 -0
- package/src/tools/askUser.test.ts +23 -2
- package/src/tools/askUser.ts +15 -5
- package/src/tools/definitions.ts +49 -132
- package/src/tools/dispatch.ts +78 -0
- package/src/tools/harnessTools.ts +23 -0
- package/src/tools/spawnSubagent.ts +3 -3
- package/src/tools/types.ts +3 -1
- package/src/tree/components.ts +221 -0
- package/src/tree/materialise.ts +122 -0
- package/src/tree/seed.ts +123 -0
- package/src/tree/store.ts +193 -0
- package/src/tree/surface.ts +135 -0
- package/src/tree/tree.test.ts +126 -0
- package/src/tree/tree.ts +346 -0
- package/src/tree/trust.ts +86 -0
- package/src/web/tools.ts +27 -5
- package/src/web/web.test.ts +26 -1
- package/src/wiki/notes.ts +313 -0
- package/src/memory/api.test.ts +0 -82
- package/src/memory/api.ts +0 -303
- package/src/memory/blocks.test.ts +0 -78
- package/src/memory/blocks.ts +0 -408
- package/src/memory/caller.ts +0 -69
- package/src/memory/curation.test.ts +0 -206
- package/src/memory/curation.ts +0 -64
- package/src/memory/dispatch.ts +0 -64
- package/src/memory/facts.test.ts +0 -41
- package/src/memory/facts.ts +0 -248
- package/src/memory/graph.ts +0 -103
- package/src/memory/health.test.ts +0 -130
- package/src/memory/health.ts +0 -74
- package/src/memory/maintainer.ts +0 -272
- package/src/memory/maintenance.test.ts +0 -192
- package/src/memory/maintenance.ts +0 -679
- package/src/memory/migrate.ts +0 -79
- package/src/memory/operations.ts +0 -150
- package/src/memory/proposals.test.ts +0 -168
- package/src/memory/proposals.ts +0 -584
- package/src/memory/references.ts +0 -77
- package/src/memory/runContext.ts +0 -93
- package/src/memory/store.test.ts +0 -66
- package/src/memory/store.ts +0 -385
- package/src/memory/wiki.test.ts +0 -47
- package/src/memory/wiki.ts +0 -423
- package/src/memory/wikiIndex.test.ts +0 -88
- package/src/memory/wikiIndex.ts +0 -117
- package/src/plugins/proposal.ts +0 -121
- package/src/skills/registry.test.ts +0 -104
- package/src/skills/registry.ts +0 -413
- package/src/tools/maintenance.ts +0 -29
- package/src/tools/memoryTools.ts +0 -22
- package/src/tools/pluginPropose.ts +0 -41
- package/src/tools/registryWrite.test.ts +0 -101
- package/src/tools/registryWrite.ts +0 -182
- /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,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
27
|
-
import * as
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
234
|
-
* and refreshes the index. `agentIds`
|
|
235
|
-
*
|
|
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,
|
|
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(
|
|
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
|
|
246
|
+
let legacyWiki: string | null = null
|
|
248
247
|
try {
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
301
|
-
|
|
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.
|
|
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 '
|
|
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 '
|
|
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 {
|
|
12
|
-
import type { CallerContext } from '
|
|
13
|
-
import { dispatch } from '
|
|
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 =
|
|
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 '
|
|
11
|
+
import type { CallerContext } from '../core/caller'
|
|
12
12
|
|
|
13
13
|
const SEARCH_DEFAULT = 10
|
|
14
14
|
const SEARCH_MAX = 30
|
package/src/hub/hub.test.ts
CHANGED
|
@@ -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 '../
|
|
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.
|
|
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.
|
|
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.
|
|
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 '../
|
|
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.
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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,
|
|
8
|
-
//
|
|
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 './
|
|
24
|
-
export { dispatch as
|
|
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,
|
|
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
|
|
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
|
|
package/src/mcp/connections.ts
CHANGED
|
@@ -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 '../
|
|
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'
|
package/src/mcp/service.ts
CHANGED
|
@@ -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 '../
|
|
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
|
+
)
|