@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.
Files changed (223) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +28 -25
  3. package/dist/cli.js +6675 -2501
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +15 -1
  11. package/dist/sdk/agents/planResearch.d.ts +5 -0
  12. package/dist/sdk/agents/planResearch.js +19 -0
  13. package/dist/sdk/agents/profiles.d.ts +2 -1
  14. package/dist/sdk/agents/profiles.js +9 -4
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/cli.js +5 -4
  26. package/dist/sdk/commands/list.d.ts +3 -0
  27. package/dist/sdk/commands/list.js +75 -0
  28. package/dist/sdk/commands/registry.d.ts +250 -0
  29. package/dist/sdk/commands/registry.js +47 -0
  30. package/dist/sdk/commands/routes.d.ts +16 -0
  31. package/dist/sdk/commands/routes.js +33 -0
  32. package/dist/sdk/core/commandBudget.d.ts +23 -0
  33. package/dist/sdk/core/commandBudget.js +57 -0
  34. package/dist/sdk/core/index.js +17 -1
  35. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  36. package/dist/sdk/core/interactiveProfile.js +143 -0
  37. package/dist/sdk/core/workspace.d.ts +13 -0
  38. package/dist/sdk/core/workspace.js +88 -0
  39. package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
  40. package/dist/sdk/core/workspaceBashTool.js +36 -0
  41. package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
  42. package/dist/sdk/core/workspaceSandbox.js +320 -0
  43. package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
  44. package/dist/sdk/core/workspaceWriteOperations.js +206 -0
  45. package/dist/sdk/doctor/types.d.ts +2 -2
  46. package/dist/sdk/harness.d.ts +7 -0
  47. package/dist/sdk/harness.js +42 -2
  48. package/dist/sdk/index.d.ts +6 -1
  49. package/dist/sdk/index.js +3 -0
  50. package/dist/sdk/pi/runtime.d.ts +2 -1
  51. package/dist/sdk/pi/runtime.js +2 -2
  52. package/dist/sdk/plugins/builtin.js +29 -12
  53. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  54. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  55. package/dist/sdk/plugins/catalogue.js +2 -1
  56. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  57. package/dist/sdk/plugins/commandPrefix.js +21 -0
  58. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  59. package/dist/sdk/plugins/firstParty.js +8 -2
  60. package/dist/sdk/plugins/inventory.js +4 -3
  61. package/dist/sdk/plugins/manifest.d.ts +2 -0
  62. package/dist/sdk/plugins/manifest.js +2 -1
  63. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  64. package/dist/sdk/plugins/surfaces.js +11 -0
  65. package/dist/sdk/plugins/types.d.ts +15 -0
  66. package/dist/sdk/prompt/compile.d.ts +14 -0
  67. package/dist/sdk/prompt/compile.js +65 -29
  68. package/dist/sdk/prompt/standard.d.ts +17 -0
  69. package/dist/sdk/prompt/standard.js +21 -0
  70. package/dist/sdk/server/methods.js +18 -0
  71. package/dist/sdk/server/remote.js +36 -11
  72. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  73. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  74. package/dist/sdk/sessions/continuation.d.ts +3 -0
  75. package/dist/sdk/sessions/continuation.js +5 -1
  76. package/dist/sdk/sessions/contracts.d.ts +168 -0
  77. package/dist/sdk/sessions/contracts.js +59 -1
  78. package/dist/sdk/sessions/control.d.ts +72 -0
  79. package/dist/sdk/sessions/control.js +158 -0
  80. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  81. package/dist/sdk/sessions/initialBash.js +7 -0
  82. package/dist/sdk/sessions/planMode.d.ts +14 -3
  83. package/dist/sdk/sessions/planMode.js +73 -16
  84. package/dist/sdk/sessions/registry.d.ts +1 -1
  85. package/dist/sdk/sessions/registry.js +2 -2
  86. package/dist/sdk/sessions/runStore.d.ts +14 -0
  87. package/dist/sdk/sessions/runStore.js +35 -0
  88. package/dist/sdk/sessions/runner.d.ts +40 -2
  89. package/dist/sdk/sessions/runner.js +161 -13
  90. package/dist/sdk/sessions/sessions.d.ts +23 -3
  91. package/dist/sdk/sessions/sessions.js +202 -17
  92. package/dist/sdk/sessions/subagentService.d.ts +9 -0
  93. package/dist/sdk/sessions/subagentService.js +63 -4
  94. package/dist/sdk/sessions/workSlice.d.ts +103 -0
  95. package/dist/sdk/sessions/workSlice.js +297 -0
  96. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  97. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  98. package/dist/sdk/terminal/browserControl.js +2 -1
  99. package/dist/sdk/terminal/controller.d.ts +11 -3
  100. package/dist/sdk/terminal/controller.js +40 -13
  101. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  102. package/dist/sdk/terminal/opentui/theme.js +87 -0
  103. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  104. package/dist/sdk/terminal/piExtension.js +154 -54
  105. package/dist/sdk/terminal/standalone.d.ts +7 -0
  106. package/dist/sdk/terminal/standalone.js +72 -13
  107. package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
  108. package/dist/sdk/terminal/subagentMonitor.js +32 -1
  109. package/dist/sdk/terminal/terminals.d.ts +19 -2
  110. package/dist/sdk/terminal/terminals.js +245 -35
  111. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  112. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  113. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  114. package/dist/sdk/terminal/tui/commandPages.js +3 -0
  115. package/dist/sdk/terminal/tui/commands.d.ts +2 -0
  116. package/dist/sdk/terminal/tui/commands.js +205 -124
  117. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  118. package/dist/sdk/terminal/tui/list.js +96 -14
  119. package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
  120. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  121. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  122. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
  124. package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
  125. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  126. package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
  127. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  128. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  129. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  130. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  131. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  132. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  133. package/dist/sdk/terminal/tui/runtime.js +23 -3
  134. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  135. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  136. package/dist/sdk/tools/checklist.js +1 -0
  137. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  138. package/dist/sdk/tools/checkpoint.js +154 -0
  139. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  140. package/dist/sdk/tools/getCapabilities.js +34 -0
  141. package/dist/sdk/tools/spawnSubagent.js +4 -3
  142. package/dist/sdk/usage/analytics.d.ts +20 -0
  143. package/dist/sdk/usage/analytics.js +151 -0
  144. package/dist/sdk/usage/ledger.d.ts +38 -0
  145. package/dist/sdk/usage/ledger.js +142 -0
  146. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  147. package/dist/sdk/usage/limits/adapters.js +349 -0
  148. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  149. package/dist/sdk/usage/limits/capture.js +47 -0
  150. package/dist/sdk/usage/limits/http.d.ts +12 -0
  151. package/dist/sdk/usage/limits/http.js +51 -0
  152. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  153. package/dist/sdk/usage/limits/normalize.js +211 -0
  154. package/dist/sdk/usage/limits/service.d.ts +30 -0
  155. package/dist/sdk/usage/limits/service.js +176 -0
  156. package/dist/sdk/usage/limits/types.d.ts +65 -0
  157. package/dist/sdk/usage/limits/types.js +1 -0
  158. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  159. package/dist/sdk/usage/serverMethods.js +22 -0
  160. package/dist/sdk/usage/service.d.ts +32 -0
  161. package/dist/sdk/usage/service.js +66 -0
  162. package/dist/sdk/usage/settings.d.ts +7 -0
  163. package/dist/sdk/usage/settings.js +28 -0
  164. package/dist/sdk/usage/types.d.ts +60 -0
  165. package/dist/sdk/usage/types.js +4 -0
  166. package/dist/terminal-opentui.js +5207 -1883
  167. package/dist/terminal-pi.js +1658 -271
  168. package/docs/architecture/02-sessions-and-runs.md +128 -26
  169. package/docs/architecture/04-prompt.md +50 -12
  170. package/docs/architecture/06-plugins-and-capabilities.md +24 -7
  171. package/docs/architecture/07-tools-and-gate.md +104 -24
  172. package/docs/architecture/08-memory-and-history.md +4 -2
  173. package/docs/architecture/09-accounts-and-connectors.md +31 -1
  174. package/docs/architecture/10-server-protocol.md +58 -3
  175. package/docs/architecture/11-terminal.md +105 -42
  176. package/docs/architecture/12-storage.md +30 -2
  177. package/docs/architecture/13-world-tools.md +22 -1
  178. package/docs/architecture/14-surfaces-and-parity.md +54 -7
  179. package/docs/architecture/README.md +3 -1
  180. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  181. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  182. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  183. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  184. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  185. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  186. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  187. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  188. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  189. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  190. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  191. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  192. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
  193. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  194. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  195. package/docs/architecture/proposals/README.md +14 -0
  196. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  197. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  198. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  199. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  200. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  201. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  202. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  203. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  204. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  205. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  206. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  207. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  208. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  209. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  210. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  211. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  212. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  213. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  214. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  215. package/docs/parity.md +80 -24
  216. package/docs/releases/1.1.1.md +28 -0
  217. package/docs/releases/1.2.0.md +49 -0
  218. package/docs/releases/4.0.0.md +10 -9
  219. package/docs/releases/README.md +35 -30
  220. package/docs/study/00-index.md +3 -0
  221. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  222. package/docs/terminal-tui.md +100 -59
  223. 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<{
@@ -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). */
@@ -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();
@@ -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";
@@ -1,4 +1,5 @@
1
- export declare const PINNED_PI_VERSION = "1.2.0";
1
+ export declare const PINNED_PI_VERSION = "1.4.0";
2
+ export declare const HTTP_DISPATCHER_CHUNK = "dist/bundle/chunks/chunk-MM3LKT73.js";
2
3
  type PiRuntimeModule = {
3
4
  VERSION: string;
4
5
  getPackageDir(): string;
@@ -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.2.0';
4
- const HTTP_DISPATCHER_CHUNK = 'dist/bundle/chunks/chunk-TJXZSVTD.js';
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: 'Page notes: keep notes on web pages and share them with this session' },
146
- { name: 'annotate', description: 'Inspect a page or app, comment on components, send fixes to the agent: /plugin:annotate [url | --app <devtools address> | --bridge]' }
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: DOCTOR_PLUGIN,
153
- commands: [{ name: 'doctor', description: 'Check account, model, tools and runtime readiness' }],
154
- serverMethods: doctorServerMethods
155
- },
156
- {
157
- descriptor: WORKTREES_PLUGIN,
158
- commands: [{ name: 'worktree', description: 'Work in a managed git worktree: new, switch, hand off, remove' }],
159
- serverMethods: worktreeServerMethods
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
- namespace: 'unprefixed' | 'plugin';
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' | 'namespace'>[];
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 commandNames = new Set();
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-]*$/i.test(command.name))
20
+ if (!/^[a-z][a-z0-9-]*$/.test(command.name))
12
21
  throw new Error(`Invalid built-in plugin command: ${command.name}`);
13
- if (commandNames.has(command.name))
14
- throw new Error(`Duplicate built-in plugin command: ${command.name}`);
15
- commandNames.add(command.name);
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
- namespace: module.descriptor.required ? 'unprefixed' : 'plugin'
35
+ legacyBare: !!module.descriptor.required
22
36
  })));
23
- const byCommand = new Map(commands.map(command => [command.name, command]));
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 => byCommand.get(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);