@xingular/harness 1.1.1 → 1.2.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 +13 -0
- package/README.md +28 -25
- package/dist/cli.js +6675 -2501
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +15 -1
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +2 -1
- package/dist/sdk/agents/profiles.js +9 -4
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +29 -12
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -29
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +18 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +168 -0
- package/dist/sdk/sessions/contracts.js +59 -1
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +73 -16
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +35 -0
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +202 -17
- package/dist/sdk/sessions/subagentService.d.ts +9 -0
- package/dist/sdk/sessions/subagentService.js +63 -4
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +11 -3
- package/dist/sdk/terminal/controller.js +40 -13
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +154 -54
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
- package/dist/sdk/terminal/subagentMonitor.js +32 -1
- package/dist/sdk/terminal/terminals.d.ts +19 -2
- package/dist/sdk/terminal/terminals.js +245 -35
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +205 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checklist.js +1 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/spawnSubagent.js +4 -3
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +5207 -1883
- package/dist/terminal-pi.js +1658 -271
- package/docs/architecture/02-sessions-and-runs.md +128 -26
- package/docs/architecture/04-prompt.md +50 -12
- package/docs/architecture/06-plugins-and-capabilities.md +24 -7
- package/docs/architecture/07-tools-and-gate.md +104 -24
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +31 -1
- package/docs/architecture/10-server-protocol.md +58 -3
- package/docs/architecture/11-terminal.md +105 -42
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +54 -7
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +14 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +80 -24
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +100 -59
- package/package.json +4 -3
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { constants as fsConstants, realpathSync, statSync } from 'node:fs';
|
|
5
|
+
import { access, readFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { isWithinWorkspace, resolveWritePath, verifyWorkspaceRoot } from "./workspace.js";
|
|
8
|
+
import { workspaceSandboxCommand } from "./workspaceSandbox.js";
|
|
9
|
+
const writeAuthorization = new AsyncLocalStorage();
|
|
10
|
+
const atomicWriteScript = `
|
|
11
|
+
const fs = require('node:fs')
|
|
12
|
+
const path = require('node:path')
|
|
13
|
+
const crypto = require('node:crypto')
|
|
14
|
+
const input = JSON.parse(fs.readFileSync(0, 'utf8'))
|
|
15
|
+
const target = input.path
|
|
16
|
+
const parent = path.dirname(target)
|
|
17
|
+
fs.mkdirSync(parent, { recursive: true })
|
|
18
|
+
let mode = 0o666
|
|
19
|
+
let preserveMode = false
|
|
20
|
+
try {
|
|
21
|
+
const existing = fs.lstatSync(target)
|
|
22
|
+
if (existing.isSymbolicLink() || !existing.isFile()) throw new Error('The write target is not a regular file.')
|
|
23
|
+
fs.accessSync(target, fs.constants.W_OK)
|
|
24
|
+
if (input.expectedContentHash) {
|
|
25
|
+
const current = fs.readFileSync(target)
|
|
26
|
+
if (crypto.createHash('sha256').update(current).digest('hex') !== input.expectedContentHash) {
|
|
27
|
+
throw new Error('The file changed after the edit tool read it; retry against the current contents.')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
mode = existing.mode & 0o7777
|
|
31
|
+
preserveMode = true
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error && error.code !== 'ENOENT') throw error
|
|
34
|
+
}
|
|
35
|
+
const temporary = path.join(parent, '.' + path.basename(target) + '.' + process.pid + '.' + crypto.randomBytes(8).toString('hex') + '.tmp')
|
|
36
|
+
let descriptor
|
|
37
|
+
try {
|
|
38
|
+
descriptor = fs.openSync(temporary, 'wx', mode)
|
|
39
|
+
fs.writeFileSync(descriptor, input.content, 'utf8')
|
|
40
|
+
if (preserveMode) fs.fchmodSync(descriptor, mode)
|
|
41
|
+
fs.fsyncSync(descriptor)
|
|
42
|
+
fs.closeSync(descriptor)
|
|
43
|
+
descriptor = undefined
|
|
44
|
+
fs.renameSync(temporary, target)
|
|
45
|
+
try {
|
|
46
|
+
const directory = fs.openSync(parent, 'r')
|
|
47
|
+
try { fs.fsyncSync(directory) } finally { fs.closeSync(directory) }
|
|
48
|
+
} catch {}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
51
|
+
try { fs.unlinkSync(temporary) } catch {}
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
function shellQuote(value) {
|
|
56
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
57
|
+
}
|
|
58
|
+
function nearestExistingDirectory(path) {
|
|
59
|
+
let current = dirname(path);
|
|
60
|
+
while (true) {
|
|
61
|
+
try {
|
|
62
|
+
const canonical = realpathSync(current);
|
|
63
|
+
if (!statSync(canonical).isDirectory())
|
|
64
|
+
throw new Error(`Write scope is not a directory: ${canonical}`);
|
|
65
|
+
return canonical;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code !== 'ENOENT')
|
|
69
|
+
throw error;
|
|
70
|
+
const parent = dirname(current);
|
|
71
|
+
if (parent === current)
|
|
72
|
+
throw new Error(`Could not find a writable parent for ${path}.`);
|
|
73
|
+
current = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function killProcess(child) {
|
|
78
|
+
if (child.pid && process.platform !== 'win32') {
|
|
79
|
+
try {
|
|
80
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Fall back to the direct child if it has already left its process group.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
child.kill('SIGKILL');
|
|
88
|
+
}
|
|
89
|
+
async function runAtomicWrite(authorization, path, content) {
|
|
90
|
+
const root = verifyWorkspaceRoot(authorization.workspaceRoot);
|
|
91
|
+
const canonicalPath = resolveWritePath(path, authorization.cwd);
|
|
92
|
+
if (canonicalPath !== authorization.path)
|
|
93
|
+
throw new Error('The write target changed after workspace approval; the write was blocked.');
|
|
94
|
+
const outside = !isWithinWorkspace(root, canonicalPath);
|
|
95
|
+
if (outside !== authorization.outside || (outside && !authorization.authorized)) {
|
|
96
|
+
throw new Error('The write authorization no longer matches the active workspace target.');
|
|
97
|
+
}
|
|
98
|
+
const additionalRoots = outside ? [nearestExistingDirectory(canonicalPath)] : [];
|
|
99
|
+
const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
|
|
100
|
+
const spec = workspaceSandboxCommand(process.platform, root, root, command, additionalRoots);
|
|
101
|
+
const env = {
|
|
102
|
+
PATH: '/usr/bin:/bin',
|
|
103
|
+
...(process.env.HOME ? { HOME: process.env.HOME } : {})
|
|
104
|
+
};
|
|
105
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
106
|
+
const child = spawn(spec.executable, spec.args, {
|
|
107
|
+
cwd: root,
|
|
108
|
+
detached: process.platform !== 'win32',
|
|
109
|
+
env,
|
|
110
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
|
+
windowsHide: true
|
|
112
|
+
});
|
|
113
|
+
const output = [];
|
|
114
|
+
let outputBytes = 0;
|
|
115
|
+
let settled = false;
|
|
116
|
+
const timeout = setTimeout(() => {
|
|
117
|
+
killProcess(child);
|
|
118
|
+
finish(new Error('Workspace-contained atomic write timed out.'));
|
|
119
|
+
}, 30_000);
|
|
120
|
+
const capture = (chunk) => {
|
|
121
|
+
if (outputBytes >= 8_192)
|
|
122
|
+
return;
|
|
123
|
+
const remaining = 8_192 - outputBytes;
|
|
124
|
+
output.push(chunk.subarray(0, remaining));
|
|
125
|
+
outputBytes += Math.min(chunk.length, remaining);
|
|
126
|
+
};
|
|
127
|
+
const finish = (error) => {
|
|
128
|
+
if (settled)
|
|
129
|
+
return;
|
|
130
|
+
settled = true;
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
authorization.signal?.removeEventListener('abort', abort);
|
|
133
|
+
if (error)
|
|
134
|
+
rejectPromise(error);
|
|
135
|
+
else
|
|
136
|
+
resolvePromise();
|
|
137
|
+
};
|
|
138
|
+
const abort = () => {
|
|
139
|
+
killProcess(child);
|
|
140
|
+
finish(new Error('Workspace-contained atomic write was aborted.'));
|
|
141
|
+
};
|
|
142
|
+
authorization.signal?.addEventListener('abort', abort, { once: true });
|
|
143
|
+
if (authorization.signal?.aborted) {
|
|
144
|
+
abort();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
child.stdout?.on('data', capture);
|
|
148
|
+
child.stderr?.on('data', capture);
|
|
149
|
+
child.once('error', (error) => finish(error));
|
|
150
|
+
child.once('close', (code) => {
|
|
151
|
+
if (code === 0) {
|
|
152
|
+
finish();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const detail = Buffer.concat(output).toString('utf8').trim();
|
|
156
|
+
finish(new Error(`Workspace-contained atomic write failed${detail ? `: ${detail.slice(-2000)}` : ` with exit code ${code ?? 'unknown'}`}.`));
|
|
157
|
+
});
|
|
158
|
+
child.stdin?.on('error', () => { });
|
|
159
|
+
child.stdin?.end(JSON.stringify({
|
|
160
|
+
path: canonicalPath,
|
|
161
|
+
content,
|
|
162
|
+
...(authorization.expectedContentHash ? { expectedContentHash: authorization.expectedContentHash } : {})
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function writeWorkspaceFile(path, content) {
|
|
167
|
+
const authorization = writeAuthorization.getStore();
|
|
168
|
+
if (!authorization?.authorized)
|
|
169
|
+
throw new Error('The file write has no current workspace authorization.');
|
|
170
|
+
await runAtomicWrite(authorization, path, content);
|
|
171
|
+
}
|
|
172
|
+
export const workspaceWriteOperations = {
|
|
173
|
+
mkdir: async (_directory) => { },
|
|
174
|
+
writeFile: writeWorkspaceFile
|
|
175
|
+
};
|
|
176
|
+
export const workspaceEditOperations = {
|
|
177
|
+
access: async (path) => access(path, fsConstants.R_OK | fsConstants.W_OK),
|
|
178
|
+
readFile: async (path) => {
|
|
179
|
+
const content = await readFile(path);
|
|
180
|
+
const authorization = writeAuthorization.getStore();
|
|
181
|
+
if (authorization && resolve(path) === authorization.path) {
|
|
182
|
+
authorization.expectedContentHash = createHash('sha256').update(content).digest('hex');
|
|
183
|
+
}
|
|
184
|
+
return content;
|
|
185
|
+
},
|
|
186
|
+
writeFile: writeWorkspaceFile
|
|
187
|
+
};
|
|
188
|
+
/** Binds one policy-approved path and abort signal to Pi's native write/edit operation. */
|
|
189
|
+
export function withWorkspaceWriteAuthorization(tool, authorizations) {
|
|
190
|
+
const native = tool;
|
|
191
|
+
const execute = native.execute.bind(tool);
|
|
192
|
+
return {
|
|
193
|
+
...tool,
|
|
194
|
+
execute: (...args) => {
|
|
195
|
+
const toolCallId = String(args[0] ?? '');
|
|
196
|
+
const authorization = authorizations.get(toolCallId);
|
|
197
|
+
authorizations.delete(toolCallId);
|
|
198
|
+
if (!authorization?.authorized)
|
|
199
|
+
throw new Error('The file write has no current workspace authorization.');
|
|
200
|
+
const signal = args[2];
|
|
201
|
+
if (signal && typeof signal === 'object' && 'aborted' in signal)
|
|
202
|
+
authorization.signal = signal;
|
|
203
|
+
return writeAuthorization.run(authorization, () => execute(...args));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -11,9 +11,9 @@ export declare const findingSchema: z.ZodObject<{
|
|
|
11
11
|
status: z.ZodEnum<{
|
|
12
12
|
disabled: "disabled";
|
|
13
13
|
warning: "warning";
|
|
14
|
+
blocked: "blocked";
|
|
14
15
|
pass: "pass";
|
|
15
16
|
fail: "fail";
|
|
16
|
-
blocked: "blocked";
|
|
17
17
|
unverified: "unverified";
|
|
18
18
|
}>;
|
|
19
19
|
verification: z.ZodEnum<{
|
|
@@ -119,9 +119,9 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
119
119
|
status: z.ZodEnum<{
|
|
120
120
|
disabled: "disabled";
|
|
121
121
|
warning: "warning";
|
|
122
|
+
blocked: "blocked";
|
|
122
123
|
pass: "pass";
|
|
123
124
|
fail: "fail";
|
|
124
|
-
blocked: "blocked";
|
|
125
125
|
unverified: "unverified";
|
|
126
126
|
}>;
|
|
127
127
|
verification: z.ZodEnum<{
|
package/dist/sdk/harness.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { Sessions } from "./sessions/sessions.js";
|
|
|
24
24
|
import { SideQuestions } from "./sessions/sideQuestions.js";
|
|
25
25
|
import { Terminals, type TerminalAssets } from "./terminal/terminals.js";
|
|
26
26
|
import { Worktrees } from "./worktrees/service.js";
|
|
27
|
+
import { Usage } from "./usage/service.js";
|
|
28
|
+
import { UsageLimits } from "./usage/limits/service.js";
|
|
27
29
|
import { AnnotationCapture } from "./annotations/capture.js";
|
|
28
30
|
import { NoteDeliveries } from "./annotations/delivery.js";
|
|
29
31
|
import type { WorktreesOptions } from "./worktrees/types.js";
|
|
@@ -89,6 +91,8 @@ declare class Env implements HarnessEnv {
|
|
|
89
91
|
get root(): string;
|
|
90
92
|
}
|
|
91
93
|
export declare class Harness {
|
|
94
|
+
/** The periodic liveness check, started once with recovery (PROP-0041). */
|
|
95
|
+
private staleRuns?;
|
|
92
96
|
private readonly browserEnabled;
|
|
93
97
|
readonly hub: Hub;
|
|
94
98
|
readonly bus: Bus;
|
|
@@ -120,6 +124,9 @@ export declare class Harness {
|
|
|
120
124
|
refineHost: RefineHost | null;
|
|
121
125
|
readonly terminals: Terminals;
|
|
122
126
|
readonly doctor: Doctor;
|
|
127
|
+
/** Usage analytics over the derived ledger, and the provider limits last reported. */
|
|
128
|
+
readonly usage: Usage;
|
|
129
|
+
readonly usageLimits: UsageLimits;
|
|
123
130
|
/** Managed git worktrees sessions can work in (PROP-0016). */
|
|
124
131
|
readonly worktrees: Worktrees;
|
|
125
132
|
/** The terminal's visible browser for capturing page notes (PROP-0017). */
|
package/dist/sdk/harness.js
CHANGED
|
@@ -36,11 +36,14 @@ import { Hooks } from "./plugins/hooks.js";
|
|
|
36
36
|
import { packageFolders, Plugins } from "./plugins/plugins.js";
|
|
37
37
|
import { approvalKey, Approvals } from "./sessions/approvals.js";
|
|
38
38
|
import { RunRegistry } from "./sessions/registry.js";
|
|
39
|
-
import { loadRun, markInterruptedRuns } from "./sessions/runStore.js";
|
|
39
|
+
import { loadRun, markInterruptedRuns, reconcileStaleRuns } from "./sessions/runStore.js";
|
|
40
40
|
import { Sessions } from "./sessions/sessions.js";
|
|
41
41
|
import { SideQuestions } from "./sessions/sideQuestions.js";
|
|
42
42
|
import { Terminals } from "./terminal/terminals.js";
|
|
43
43
|
import { Worktrees } from "./worktrees/service.js";
|
|
44
|
+
import { Usage } from "./usage/service.js";
|
|
45
|
+
import { UsageLimits } from "./usage/limits/service.js";
|
|
46
|
+
import { indexRunUsage, syncUsage } from "./usage/ledger.js";
|
|
44
47
|
import { AnnotationCapture } from "./annotations/capture.js";
|
|
45
48
|
import { NoteDeliveries } from "./annotations/delivery.js";
|
|
46
49
|
class Env {
|
|
@@ -61,6 +64,8 @@ class Env {
|
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
export class Harness {
|
|
67
|
+
/** The periodic liveness check, started once with recovery (PROP-0041). */
|
|
68
|
+
staleRuns;
|
|
64
69
|
browserEnabled;
|
|
65
70
|
hub;
|
|
66
71
|
bus = new Bus();
|
|
@@ -92,6 +97,9 @@ export class Harness {
|
|
|
92
97
|
refineHost;
|
|
93
98
|
terminals;
|
|
94
99
|
doctor;
|
|
100
|
+
/** Usage analytics over the derived ledger, and the provider limits last reported. */
|
|
101
|
+
usage;
|
|
102
|
+
usageLimits;
|
|
95
103
|
/** Managed git worktrees sessions can work in (PROP-0016). */
|
|
96
104
|
worktrees;
|
|
97
105
|
/** The terminal's visible browser for capturing page notes (PROP-0017). */
|
|
@@ -116,6 +124,10 @@ export class Harness {
|
|
|
116
124
|
const secrets = options.secrets ?? fileSecretStore(join(this.stateDir, 'secrets.json'));
|
|
117
125
|
this.accounts = new Accounts({ dir: join(this.stateDir, 'accounts'), secrets, bus: this.bus });
|
|
118
126
|
this.approvals = new Approvals(this.stateDir, this.bus);
|
|
127
|
+
this.usageLimits = new UsageLimits({ accounts: this.accounts, root: () => this.env.dataRoot });
|
|
128
|
+
// Response headers are the free, always-fresh limit signal; the account runtime reports them here.
|
|
129
|
+
this.accounts.observeModels = (models, accountId) => this.usageLimits.observeModels(models, accountId);
|
|
130
|
+
this.usage = new Usage({ root: () => this.env.root, accounts: this.accounts });
|
|
119
131
|
this.runs = new RunRegistry({
|
|
120
132
|
root: () => this.env.root,
|
|
121
133
|
automatic: async (run, target, operation, standing) => {
|
|
@@ -171,6 +183,7 @@ export class Harness {
|
|
|
171
183
|
broker: this.apps,
|
|
172
184
|
plugins: this.plugins,
|
|
173
185
|
resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }),
|
|
186
|
+
fullPermission: (run) => this.approvals.isAuto(approvalKey(run)),
|
|
174
187
|
factory: options.sessionFactory,
|
|
175
188
|
trust: this.trust,
|
|
176
189
|
worktrees: this.worktrees
|
|
@@ -187,7 +200,7 @@ export class Harness {
|
|
|
187
200
|
this.refine = new Refinements({ root: () => this.env.root, bus: this.bus, trust: this.trust, host: () => this.refineHost, runtime: refineRuntime });
|
|
188
201
|
this.lessons = new Lessons({ root: () => this.env.root, runtime: refineRuntime });
|
|
189
202
|
this.pageNotes = new NoteDeliveries({ root: () => this.env.root, bus: this.bus, terminals: () => this.terminals ?? null });
|
|
190
|
-
this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, sideQuestions: this.sideQuestions, 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), configure: (params) => pluginApi.configure(this, params), install: params => pluginInstall.install(this, params), remove: params => pluginInstall.remove(this, params) }, assets: options.assets, hub: this.hub, world: this.world, doctor: this.doctor, worktrees: this.worktrees, noteCapture: this.noteCapture, pageNotes: this.pageNotes });
|
|
203
|
+
this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, sideQuestions: this.sideQuestions, 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, usage: this.usage, usageLimits: this.usageLimits, 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), configure: (params) => pluginApi.configure(this, params), install: params => pluginInstall.install(this, params), remove: params => pluginInstall.remove(this, params) }, assets: options.assets, hub: this.hub, world: this.world, doctor: this.doctor, worktrees: this.worktrees, noteCapture: this.noteCapture, pageNotes: this.pageNotes });
|
|
191
204
|
this.ready = this.apps.start().catch((error) => log('connected apps', error instanceof Error ? error.message : String(error)));
|
|
192
205
|
this.runs.listen((event, run) => {
|
|
193
206
|
this.accounts.onRunEvent(event, run);
|
|
@@ -197,6 +210,10 @@ export class Harness {
|
|
|
197
210
|
if (run?.trigger === 'doctor')
|
|
198
211
|
return;
|
|
199
212
|
const runEnded = event.type === 'completed' || event.type === 'failed' || (event.type === 'status.changed' && event.payload.status === 'interrupted');
|
|
213
|
+
// The usage ledger is derived from the same recorded events; a failure never blocks the run.
|
|
214
|
+
// An interrupted run is indexed too, so its spent tokens are not lost until the next start.
|
|
215
|
+
if (runEnded)
|
|
216
|
+
indexRunUsage(this.env.root, event.runId);
|
|
200
217
|
if (runEnded && run?.sessionId) {
|
|
201
218
|
// Notes sent with "Proceed now" while this session was working go out now; the terminal
|
|
202
219
|
// needs a moment to see its run as ended.
|
|
@@ -294,18 +311,41 @@ export class Harness {
|
|
|
294
311
|
catch (error) {
|
|
295
312
|
log('recall', error instanceof Error ? error.message : String(error));
|
|
296
313
|
}
|
|
314
|
+
try {
|
|
315
|
+
// Usage rows come from the same run events; the ledger can always be rebuilt from them.
|
|
316
|
+
syncUsage(root);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
log('usage ledger', error instanceof Error ? error.message : String(error));
|
|
320
|
+
}
|
|
297
321
|
this.recovery = { interruptedRuns: interruptedRunIds.length, interruptedRunIds, reindexedPages: safeCount(root), migrationError, legacyWiki };
|
|
322
|
+
if (!this.staleRuns) {
|
|
323
|
+
// A reply that stops emitting heartbeats while its process lives is settled here, not left
|
|
324
|
+
// `running` for the next restart (PROP-0041).
|
|
325
|
+
this.staleRuns = setInterval(() => {
|
|
326
|
+
try {
|
|
327
|
+
reconcileStaleRuns(root);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
log('stale run check', error instanceof Error ? error.message : String(error));
|
|
331
|
+
}
|
|
332
|
+
}, 60_000);
|
|
333
|
+
this.staleRuns.unref?.();
|
|
334
|
+
}
|
|
298
335
|
return this.recovery;
|
|
299
336
|
}
|
|
300
337
|
/** Rebuilds the history index from the run records. Returns the notes in the Wiki. */
|
|
301
338
|
rebuildIndex() {
|
|
302
339
|
recall.sync(this.env.root);
|
|
340
|
+
syncUsage(this.env.root);
|
|
303
341
|
const count = safeCount(this.env.root);
|
|
304
342
|
this.recovery = { ...this.recovery, reindexedPages: count };
|
|
305
343
|
return count;
|
|
306
344
|
}
|
|
307
345
|
/** Stops live runs and refinements and releases the index. */
|
|
308
346
|
async close() {
|
|
347
|
+
if (this.staleRuns)
|
|
348
|
+
clearInterval(this.staleRuns);
|
|
309
349
|
await this.doctor.close();
|
|
310
350
|
this.hub.close();
|
|
311
351
|
await this.refine.close();
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export type { ApprovalFacts, ApprovalPolicy, AutomaticReason } from "./sessions/
|
|
|
21
21
|
export type { ScopeDecision, ToolCall, ToolPolicy } from "./sessions/policy.js";
|
|
22
22
|
export type { ActiveRun, ApprovalDecision, ApprovalInput, QuestionAnswer, QuestionInput } from "./sessions/registry.js";
|
|
23
23
|
export type { RunFields, SessionRunInput } from "./sessions/sessions.js";
|
|
24
|
+
export type { SessionCommand } from "./sessions/runner.js";
|
|
25
|
+
export type { SessionRef, SessionTree, SessionTreeEntry } from "./sessions/control.js";
|
|
26
|
+
export { CORE_COMMANDS, coreCommand, type CoreCommand, type CoreCommandGroup, type CoreCommandKind, type CoreCommandName, type ListedCommand } from "./commands/registry.js";
|
|
27
|
+
export { STANDARD_CONNECTIONS, type StandardSession } from "./prompt/standard.js";
|
|
24
28
|
export { SideQuestions, SIDE_QUESTION_INSTRUCTION, buildSideQuestionText, type SideQuestionContext, type SideQuestionRead, type SideQuestionStartInput } from "./sessions/sideQuestions.js";
|
|
25
29
|
export type { Effort } from "./sessions/runner.js";
|
|
26
30
|
export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE, type HarnessCall, type HarnessToolDefinition } from "./tools/definitions.js";
|
|
@@ -56,7 +60,8 @@ export { HARNESS_PLUGINS } from "./plugins/builtin.js";
|
|
|
56
60
|
export { FIRST_PARTY_PLUGINS } from "./plugins/firstParty.js";
|
|
57
61
|
export type { Found, Resolver } from "./plugins/discover.js";
|
|
58
62
|
export type { Loading, Registered } from "./plugins/plugins.js";
|
|
59
|
-
export { CONTRIBUTION_KINDS, PLUGIN_SOURCES, type ConnectorTemplate, type Contribution, type ContributionKind, type ContributionState, type Delivery, type Inventory, type InventoryItem, type Plugin, type PluginDescriptor, type PluginManage, type PluginSetting, type PluginSource } from "./plugins/types.js";
|
|
63
|
+
export { CONTRIBUTION_KINDS, PLUGIN_SOURCES, type ConnectorTemplate, type Contribution, type ContributionKind, type ContributionState, type Delivery, type Inventory, type InventoryItem, type Plugin, type PluginDescriptor, type PluginManage, type PluginSetting, type PluginSource, type PluginSurfaces } from "./plugins/types.js";
|
|
64
|
+
export { describePlugin, surfaceBadge } from "./plugins/surfaces.js";
|
|
60
65
|
export { methods } from "./server/methods.js";
|
|
61
66
|
export { remoteDirectory, remotePolicy, remoteSecrets, remoteTools } from "./server/remote.js";
|
|
62
67
|
export { PROTOCOL_VERSION, RpcPeer, stdioTransport, type Handler, type Transport } from "./server/rpc.js";
|
package/dist/sdk/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export { fileSecretStore, memorySecretStore, secretKey } from "./accounts/secret
|
|
|
20
20
|
export { dispatch as harnessCall } from "./tools/dispatch.js";
|
|
21
21
|
export { compile, render, SECTION_ORDER, CONTEXT_MESSAGE_TYPE, CONTEXT_SNAPSHOT_KEY } from "./prompt/compile.js";
|
|
22
22
|
export { runEventSchema, runRecordSchema, isActiveStatus } from "./sessions/contracts.js";
|
|
23
|
+
export { CORE_COMMANDS, coreCommand } from "./commands/registry.js";
|
|
24
|
+
export { STANDARD_CONNECTIONS } from "./prompt/standard.js";
|
|
23
25
|
export { SideQuestions, SIDE_QUESTION_INSTRUCTION, buildSideQuestionText } from "./sessions/sideQuestions.js";
|
|
24
26
|
export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE } from "./tools/definitions.js";
|
|
25
27
|
export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
|
|
@@ -42,6 +44,7 @@ export { attach as attachPlugin, inventoryFor as pluginInventory, list as listPl
|
|
|
42
44
|
export { HARNESS_PLUGINS } from "./plugins/builtin.js";
|
|
43
45
|
export { FIRST_PARTY_PLUGINS } from "./plugins/firstParty.js";
|
|
44
46
|
export { CONTRIBUTION_KINDS, PLUGIN_SOURCES } from "./plugins/types.js";
|
|
47
|
+
export { describePlugin, surfaceBadge } from "./plugins/surfaces.js";
|
|
45
48
|
export { methods } from "./server/methods.js";
|
|
46
49
|
export { remoteDirectory, remotePolicy, remoteSecrets, remoteTools } from "./server/remote.js";
|
|
47
50
|
export { PROTOCOL_VERSION, RpcPeer, stdioTransport } from "./server/rpc.js";
|
package/dist/sdk/pi/runtime.d.ts
CHANGED
package/dist/sdk/pi/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
-
export const PINNED_PI_VERSION = '1.
|
|
4
|
-
const HTTP_DISPATCHER_CHUNK = 'dist/bundle/chunks/chunk-
|
|
3
|
+
export const PINNED_PI_VERSION = '1.4.0';
|
|
4
|
+
export const HTTP_DISPATCHER_CHUNK = 'dist/bundle/chunks/chunk-MM3LKT73.js';
|
|
5
5
|
export function assertPiRuntimeVersion(version) {
|
|
6
6
|
if (version !== PINNED_PI_VERSION)
|
|
7
7
|
throw new Error(`The Harness Pi runtime requires Pi ${PINNED_PI_VERSION}; found ${version}. Install the Harness package's pinned dependencies.`);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { CONNECTION_TOOLS, NATIVE_GROUP } from "../prompt/compile.js";
|
|
3
3
|
import { ASK_USER, askUserTool } from "../tools/askUser.js";
|
|
4
4
|
import { CHECKLIST_WRITE, checklistTool } from "../tools/checklist.js";
|
|
5
|
+
import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
|
|
6
|
+
import { sliceOf } from "../sessions/workSlice.js";
|
|
5
7
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS } from "../sessions/subagents.js";
|
|
6
8
|
import { subagentTools } from "../tools/spawnSubagent.js";
|
|
7
9
|
import { SESSION_NAME } from "../tools/sessionName.js";
|
|
@@ -25,6 +27,9 @@ import { subagentServerMethods } from "../sessions/subagentServerMethods.js";
|
|
|
25
27
|
import { sideQuestionServerMethods } from "../sessions/sideQuestionServerMethods.js";
|
|
26
28
|
import { connectedAppServerMethods } from "../mcp/serverMethods.js";
|
|
27
29
|
import { harnessTools } from "../tools/harnessTools.js";
|
|
30
|
+
import { GET_CAPABILITIES } from "../tools/getCapabilities.js";
|
|
31
|
+
import { USAGE_SETTINGS } from "../usage/settings.js";
|
|
32
|
+
import { usageServerMethods } from "../usage/serverMethods.js";
|
|
28
33
|
import { createBuiltinPluginRegistry } from "./builtinRegistry.js";
|
|
29
34
|
const tools = (definitions) => definitions.map((tool) => ({ name: tool.name, description: tool.label }));
|
|
30
35
|
const toolContribution = (definitions, toolOrder) => ({
|
|
@@ -75,6 +80,7 @@ const WIKI_PLUGIN = {
|
|
|
75
80
|
const HISTORY_PLUGIN = { id: 'history', title: 'History', description: 'Search earlier conversations and inspect recorded run trajectories.', required: 'Earlier runs are how an agent picks up where work stopped.', tools: tools([...RECALL_TOOLS, ...TRAJECTORY_TOOLS]), toolGroups: ['History'] };
|
|
76
81
|
const ASK_USER_PLUGIN = { id: 'ask-user', title: 'Ask the user', description: 'Lets the agent stop and ask a question with choices.', required: 'An agent must be able to stop and ask you.', tools: [{ name: ASK_USER }] };
|
|
77
82
|
const CHECKLIST_PLUGIN = { id: 'checklist', title: 'Checklist', description: 'Lets the agent keep a checklist of the steps of its work, recorded with the run so progress can be shown and picked up again.', required: 'Progress of longer work is kept through it.', tools: [{ name: CHECKLIST_WRITE, description: 'Write the checklist' }] };
|
|
83
|
+
const CHECKPOINT_PLUGIN = { id: 'checkpoint', title: 'Checkpoint', description: 'Lets a bounded reply record a handoff at a work-slice boundary so the next slice starts from it.', required: 'A reply that stops at its budget leaves the state the next one resumes from.', tools: [{ name: CHECKPOINT_WRITE, description: 'Write the handoff checkpoint' }] };
|
|
78
84
|
const CONNECTED_APPS_PLUGIN = {
|
|
79
85
|
id: 'connected-apps',
|
|
80
86
|
title: 'Connected apps',
|
|
@@ -88,6 +94,13 @@ const DOCTOR_PLUGIN = { id: 'doctor', title: 'Doctor', description: 'Local readi
|
|
|
88
94
|
const WORKTREES_PLUGIN = { id: 'worktrees', title: 'Worktrees', description: 'Create and manage isolated git worktrees, move changes between them, and restore removed worktrees.', required: 'Managed worktrees are available to every local terminal.' };
|
|
89
95
|
const HUB_PLUGIN = { id: 'hub', title: 'Hub', description: 'Browse and install reviewed skills, plugins and documents from configured registries.', required: 'Reviewed Harness resources are available to every host.' };
|
|
90
96
|
const REFINEMENT_PLUGIN = { id: 'refinement', title: 'Refinement', description: 'Improve variants from recorded evidence through isolated refinement runs and reviewed candidates.', required: 'Variant refinement is available to every host.' };
|
|
97
|
+
const CAPABILITIES_PLUGIN = { id: 'capabilities', title: 'Capability discovery', description: 'Inspect the callable tools of the current run, including their input schemas. Discovery never grants access.', required: 'Every interactive session can inspect its own registered capabilities.', tools: [{ name: GET_CAPABILITIES, description: 'Inspect current capabilities' }] };
|
|
98
|
+
const USAGE_PLUGIN = {
|
|
99
|
+
id: 'usage',
|
|
100
|
+
title: 'Usage',
|
|
101
|
+
description: 'Token, cost and latency totals from recorded runs, and the plan windows, balances or rate limits each provider reports.',
|
|
102
|
+
settings: [...USAGE_SETTINGS]
|
|
103
|
+
};
|
|
91
104
|
const SIDE_QUESTIONS_PLUGIN = {
|
|
92
105
|
id: 'side-questions',
|
|
93
106
|
title: 'Side questions',
|
|
@@ -107,7 +120,6 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
107
120
|
tools: context => context.allowedTools.includes(SPAWN_SUBAGENT)
|
|
108
121
|
? subagentTools(context.subagents, context.runId).filter(tool => context.allowedTools.includes(tool.name))
|
|
109
122
|
: [],
|
|
110
|
-
commands: [{ name: 'subagents', description: 'Watch current subagent tasks and activity' }, { name: 'agents', description: 'Browse and edit agent profiles' }],
|
|
111
123
|
serverMethods: subagentServerMethods
|
|
112
124
|
},
|
|
113
125
|
{ descriptor: VARIANT_PLUGIN, ...toolContribution(SKILL_TOOLS, 30) },
|
|
@@ -115,11 +127,11 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
115
127
|
{
|
|
116
128
|
descriptor: HISTORY_PLUGIN,
|
|
117
129
|
...toolContribution([...RECALL_TOOLS, ...TRAJECTORY_TOOLS], 10),
|
|
118
|
-
commands: [{ name: 'trajectory', description: 'Inspect the recorded runs of this session' }],
|
|
119
130
|
serverMethods: historyServerMethods
|
|
120
131
|
},
|
|
121
132
|
{ descriptor: ASK_USER_PLUGIN, toolOrder: 40, tools: context => context.harnessTools && context.allowedTools.includes(ASK_USER) ? [askUserTool(context.run)] : [] },
|
|
122
133
|
{ descriptor: CHECKLIST_PLUGIN, toolOrder: 50, tools: context => context.harnessTools ? [checklistTool(context.run)] : [] },
|
|
134
|
+
{ descriptor: CHECKPOINT_PLUGIN, toolOrder: 51, tools: context => context.harnessTools && context.allowedTools.includes(CHECKPOINT_WRITE) ? [checkpointTool(context.run, () => sliceOf(context.env.root, context.runId))] : [] },
|
|
123
135
|
{
|
|
124
136
|
descriptor: CONNECTED_APPS_PLUGIN,
|
|
125
137
|
toolOrder: 0,
|
|
@@ -141,23 +153,28 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
141
153
|
},
|
|
142
154
|
{
|
|
143
155
|
descriptor: ANNOTATIONS_PLUGIN,
|
|
156
|
+
commandPrefix: 'annotate',
|
|
157
|
+
defaultCommand: 'annotate',
|
|
144
158
|
commands: [
|
|
145
|
-
{ name: 'notes', description: '
|
|
146
|
-
{ name: 'annotate', description: 'Inspect a page or app, comment on components, send fixes
|
|
159
|
+
{ name: 'notes', description: 'Review web annotations saved from pages and apps' },
|
|
160
|
+
{ name: 'annotate', description: 'Inspect a page or app, comment on components, and send fixes: /annotate:annotate [url | --app <devtools address> | --bridge]' }
|
|
147
161
|
],
|
|
148
162
|
createWorldPart: ({ options }) => annotationsPart({ root: options.root, changed: options.notesChanged }),
|
|
149
163
|
serverMethods: annotationServerMethods
|
|
150
164
|
},
|
|
165
|
+
{ descriptor: CAPABILITIES_PLUGIN },
|
|
151
166
|
{
|
|
152
|
-
descriptor:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
serverMethods:
|
|
167
|
+
descriptor: USAGE_PLUGIN,
|
|
168
|
+
commandPrefix: 'usage',
|
|
169
|
+
defaultCommand: 'analysis',
|
|
170
|
+
commands: [
|
|
171
|
+
{ name: 'analysis', description: 'Token, cost and latency totals: /usage:analysis [today | 7d | 30d | all]' },
|
|
172
|
+
{ name: 'limit', description: 'Provider plan windows, balances and rate limits: /usage:limit' }
|
|
173
|
+
],
|
|
174
|
+
serverMethods: usageServerMethods
|
|
160
175
|
},
|
|
176
|
+
{ descriptor: DOCTOR_PLUGIN, serverMethods: doctorServerMethods },
|
|
177
|
+
{ descriptor: WORKTREES_PLUGIN, serverMethods: worktreeServerMethods },
|
|
161
178
|
...FIRST_PARTY_PLUGINS.map(descriptor => ({ descriptor }))
|
|
162
179
|
]);
|
|
163
180
|
export const HARNESS_PLUGINS = BUILTIN_PLUGIN_REGISTRY.descriptors();
|
|
@@ -10,9 +10,10 @@ import type { ActiveRun } from "../sessions/registry.js";
|
|
|
10
10
|
import type { PluginDescriptor } from "./types.js";
|
|
11
11
|
export interface BuiltinPluginCommand {
|
|
12
12
|
pluginId: string;
|
|
13
|
+
prefix: string;
|
|
13
14
|
name: string;
|
|
14
15
|
description: string;
|
|
15
|
-
|
|
16
|
+
legacyBare: boolean;
|
|
16
17
|
}
|
|
17
18
|
export interface BuiltinWorldContext {
|
|
18
19
|
options: WorldOptions;
|
|
@@ -33,10 +34,14 @@ export interface BuiltinToolContext {
|
|
|
33
34
|
}
|
|
34
35
|
export interface BuiltinPluginModule {
|
|
35
36
|
descriptor: PluginDescriptor;
|
|
37
|
+
/** Stable terminal command namespace; defaults to the plugin id. */
|
|
38
|
+
commandPrefix?: string;
|
|
39
|
+
/** Command invoked by the namespace root, such as `/annotate:`. */
|
|
40
|
+
defaultCommand?: string;
|
|
36
41
|
/** Stable order for model tools, independent of descriptor display order. */
|
|
37
42
|
toolOrder?: number;
|
|
38
43
|
tools?: (context: BuiltinToolContext) => ToolSpec[];
|
|
39
|
-
commands?: readonly Omit<BuiltinPluginCommand, 'pluginId' | '
|
|
44
|
+
commands?: readonly Omit<BuiltinPluginCommand, 'pluginId' | 'prefix' | 'legacyBare'>[];
|
|
40
45
|
createWorldPart?: (context: BuiltinWorldContext) => WorldPart | null;
|
|
41
46
|
serverMethods?: (harness: Harness) => Record<string, Handler>;
|
|
42
47
|
}
|
|
@@ -44,7 +49,9 @@ export interface BuiltinPluginRegistry {
|
|
|
44
49
|
readonly modules: readonly BuiltinPluginModule[];
|
|
45
50
|
descriptors(): PluginDescriptor[];
|
|
46
51
|
commands(): BuiltinPluginCommand[];
|
|
47
|
-
command(name: string): BuiltinPluginCommand | undefined;
|
|
52
|
+
command(prefix: string, name: string): BuiltinPluginCommand | undefined;
|
|
53
|
+
commandsNamed(name: string): BuiltinPluginCommand[];
|
|
54
|
+
defaultCommand(prefix: string): BuiltinPluginCommand | undefined;
|
|
48
55
|
tools(context: BuiltinToolContext): ToolSpec[];
|
|
49
56
|
worldParts(context: BuiltinWorldContext): WorldPart[];
|
|
50
57
|
serverMethods(harness: Harness): Record<string, Handler>;
|
|
@@ -1,33 +1,55 @@
|
|
|
1
1
|
/** One checked registry for the Harness-owned plugin modules and their surface contributions. */
|
|
2
2
|
export function createBuiltinPluginRegistry(modules) {
|
|
3
3
|
const pluginIds = new Set();
|
|
4
|
-
const
|
|
4
|
+
const prefixOwners = new Map();
|
|
5
|
+
const routeKeys = new Set();
|
|
5
6
|
for (const module of modules) {
|
|
6
7
|
const id = module.descriptor.id;
|
|
7
8
|
if (pluginIds.has(id))
|
|
8
9
|
throw new Error(`Duplicate built-in plugin id: ${id}`);
|
|
9
10
|
pluginIds.add(id);
|
|
11
|
+
const prefix = module.commandPrefix ?? id;
|
|
12
|
+
if (!/^[a-z][a-z0-9-]*$/.test(prefix) || prefix === 'plugin' || prefix === 'skill')
|
|
13
|
+
throw new Error(`Invalid built-in plugin command prefix: ${prefix}`);
|
|
14
|
+
if ((module.commands?.length || module.defaultCommand) && prefixOwners.has(prefix)) {
|
|
15
|
+
throw new Error(`Duplicate built-in plugin command prefix: ${prefix}`);
|
|
16
|
+
}
|
|
17
|
+
if (module.commands?.length || module.defaultCommand)
|
|
18
|
+
prefixOwners.set(prefix, id);
|
|
10
19
|
for (const command of module.commands ?? []) {
|
|
11
|
-
if (!/^[a-z][a-z0-9-]
|
|
20
|
+
if (!/^[a-z][a-z0-9-]*$/.test(command.name))
|
|
12
21
|
throw new Error(`Invalid built-in plugin command: ${command.name}`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
const key = `${prefix}:${command.name}`;
|
|
23
|
+
if (routeKeys.has(key))
|
|
24
|
+
throw new Error(`Duplicate built-in plugin command: ${key}`);
|
|
25
|
+
routeKeys.add(key);
|
|
26
|
+
}
|
|
27
|
+
if (module.defaultCommand && !module.commands?.some(command => command.name === module.defaultCommand)) {
|
|
28
|
+
throw new Error(`Unknown default command for built-in plugin ${id}: ${module.defaultCommand}`);
|
|
16
29
|
}
|
|
17
30
|
}
|
|
18
31
|
const commands = modules.flatMap(module => (module.commands ?? []).map(command => ({
|
|
19
32
|
pluginId: module.descriptor.id,
|
|
33
|
+
prefix: module.commandPrefix ?? module.descriptor.id,
|
|
20
34
|
...command,
|
|
21
|
-
|
|
35
|
+
legacyBare: !!module.descriptor.required
|
|
22
36
|
})));
|
|
23
|
-
const
|
|
37
|
+
const byRoute = new Map(commands.map(command => [`${command.prefix}:${command.name}`, command]));
|
|
38
|
+
const byName = new Map();
|
|
39
|
+
for (const command of commands)
|
|
40
|
+
byName.set(command.name, [...(byName.get(command.name) ?? []), command]);
|
|
41
|
+
const defaults = new Map(modules.flatMap(module => module.defaultCommand
|
|
42
|
+
? [[module.commandPrefix ?? module.descriptor.id, byRoute.get(`${module.commandPrefix ?? module.descriptor.id}:${module.defaultCommand}`)]]
|
|
43
|
+
: []));
|
|
24
44
|
const toolModules = modules.map((module, index) => ({ module, index })).filter(({ module }) => module.tools)
|
|
25
45
|
.sort((left, right) => (left.module.toolOrder ?? left.index) - (right.module.toolOrder ?? right.index));
|
|
26
46
|
return {
|
|
27
47
|
modules,
|
|
28
48
|
descriptors: () => modules.map(module => module.descriptor),
|
|
29
49
|
commands: () => [...commands],
|
|
30
|
-
command: name =>
|
|
50
|
+
command: (prefix, name) => byRoute.get(`${prefix}:${name}`),
|
|
51
|
+
commandsNamed: name => [...(byName.get(name) ?? [])],
|
|
52
|
+
defaultCommand: prefix => defaults.get(prefix),
|
|
31
53
|
tools: context => toolModules.flatMap(({ module }) => module.tools(context)),
|
|
32
54
|
worldParts: context => modules.flatMap(module => {
|
|
33
55
|
const part = module.createWorldPart?.(context);
|