@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usagePayloadSchema } from "../sessions/contracts.js";
|
|
1
2
|
export const ACTIVE_SUBAGENT_STATUSES = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
|
|
2
3
|
export function terminalSubagentDuration(activity, now = Date.now()) {
|
|
3
4
|
const start = Date.parse(activity.startedAt);
|
|
@@ -41,7 +42,7 @@ function stringOf(value) {
|
|
|
41
42
|
function latestTool(tools) {
|
|
42
43
|
return [...tools.values()].sort((left, right) => right.sequence - left.sequence)[0]?.text;
|
|
43
44
|
}
|
|
44
|
-
/** Volatile, read-only activity projection for the
|
|
45
|
+
/** Volatile, read-only activity projection for the latest terminal parent run. */
|
|
45
46
|
export class SubagentMonitor {
|
|
46
47
|
parentRunId = null;
|
|
47
48
|
activities = new Map();
|
|
@@ -64,12 +65,22 @@ export class SubagentMonitor {
|
|
|
64
65
|
activity: statusActivity(run.status),
|
|
65
66
|
checklist: null,
|
|
66
67
|
profile: run.profile ?? null,
|
|
68
|
+
accountLabel: run.accountLabel ?? null,
|
|
69
|
+
model: run.model ?? null,
|
|
70
|
+
effort: run.effort ?? null,
|
|
71
|
+
usage: null,
|
|
67
72
|
tools: new Map(),
|
|
68
73
|
lastTool: null
|
|
69
74
|
};
|
|
70
75
|
row.parentRunId = run.parentRunId;
|
|
71
76
|
if (run.profile !== undefined)
|
|
72
77
|
row.profile = run.profile;
|
|
78
|
+
if (run.accountLabel !== undefined)
|
|
79
|
+
row.accountLabel = run.accountLabel;
|
|
80
|
+
if (run.model !== undefined && !row.usage?.model)
|
|
81
|
+
row.model = run.model;
|
|
82
|
+
if (run.effort !== undefined)
|
|
83
|
+
row.effort = run.effort;
|
|
73
84
|
row.title = run.title;
|
|
74
85
|
row.goal = run.goal;
|
|
75
86
|
row.status = run.status;
|
|
@@ -94,6 +105,26 @@ export class SubagentMonitor {
|
|
|
94
105
|
row.lastTool = null;
|
|
95
106
|
row.activity = 'Responding';
|
|
96
107
|
}
|
|
108
|
+
else if (event.type === 'usage.updated') {
|
|
109
|
+
const parsed = usagePayloadSchema.safeParse(payload);
|
|
110
|
+
if (parsed.success) {
|
|
111
|
+
const usage = parsed.data;
|
|
112
|
+
row.usage = {
|
|
113
|
+
...(usage.provider === undefined ? {} : { provider: usage.provider }),
|
|
114
|
+
...(usage.model === undefined ? {} : { model: usage.model }),
|
|
115
|
+
...(usage.inputTokens === undefined ? {} : { inputTokens: usage.inputTokens }),
|
|
116
|
+
...(usage.outputTokens === undefined ? {} : { outputTokens: usage.outputTokens }),
|
|
117
|
+
...(usage.cacheReadTokens === undefined ? {} : { cacheReadTokens: usage.cacheReadTokens }),
|
|
118
|
+
...(usage.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: usage.cacheWriteTokens }),
|
|
119
|
+
...(usage.reasoningTokens === undefined ? {} : { reasoningTokens: usage.reasoningTokens }),
|
|
120
|
+
...(usage.costUsd === undefined ? {} : { costUsd: usage.costUsd }),
|
|
121
|
+
...(usage.costSource === undefined ? {} : { costSource: usage.costSource }),
|
|
122
|
+
...(usage.requests === undefined ? {} : { requests: usage.requests }),
|
|
123
|
+
...(usage.contextTokens === undefined ? {} : { contextTokens: usage.contextTokens }),
|
|
124
|
+
...(usage.contextWindow === undefined ? {} : { contextWindow: usage.contextWindow })
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
97
128
|
else if (event.type === 'checklist.updated') {
|
|
98
129
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
99
130
|
row.checklist = items.length ? { done: items.filter(item => item?.status === 'completed').length, total: items.length } : null;
|
|
@@ -22,6 +22,8 @@ import type { World } from "../world/world.js";
|
|
|
22
22
|
import type { Worktrees } from "../worktrees/service.js";
|
|
23
23
|
import type { AnnotationCapture } from "../annotations/capture.js";
|
|
24
24
|
import type { NoteDeliveries } from "../annotations/delivery.js";
|
|
25
|
+
import type { Usage } from "../usage/service.js";
|
|
26
|
+
import type { UsageLimits } from "../usage/limits/service.js";
|
|
25
27
|
/** The files a terminal session needs on disk, outside this process. */
|
|
26
28
|
export interface TerminalAssets {
|
|
27
29
|
/** The Node binary that runs the commands. */
|
|
@@ -103,6 +105,10 @@ export interface TerminalsOptions {
|
|
|
103
105
|
noteCapture?: AnnotationCapture;
|
|
104
106
|
/** Page notes on their way to a session (PROP-0018). */
|
|
105
107
|
pageNotes?: NoteDeliveries;
|
|
108
|
+
/** Usage analytics over the derived ledger (`/usage:analysis`). */
|
|
109
|
+
usage?: Usage;
|
|
110
|
+
/** Provider plan windows, balances and rate limits (`/usage:limit`). */
|
|
111
|
+
usageLimits?: UsageLimits;
|
|
106
112
|
}
|
|
107
113
|
/** What `/plugins` in a terminal can do. Each takes the session's folder, account and project. */
|
|
108
114
|
export interface PluginControl {
|
|
@@ -147,10 +153,17 @@ export declare class Terminals {
|
|
|
147
153
|
constructor(options: TerminalsOptions);
|
|
148
154
|
/** The commands a terminal session finds on PATH. Rewritten only when they change. */
|
|
149
155
|
private ensureCommands;
|
|
150
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* The harness's own History, Wiki and skill tools, then the broker's, then the world tools whose
|
|
158
|
+
* plugins are on for this session: the same tools a structured session has (INV-PAR-1).
|
|
159
|
+
*/
|
|
151
160
|
private tools;
|
|
161
|
+
private readonly ownNames;
|
|
162
|
+
private ownTools;
|
|
152
163
|
/** The world tools a new session will have, named in its instructions. */
|
|
153
164
|
private worldToolNames;
|
|
165
|
+
/** The harness's own History, Wiki and skill tools a new session will have. */
|
|
166
|
+
private ownToolNames;
|
|
154
167
|
/** Whether a world tool's plugin is switched on for a session's folder, account and project. */
|
|
155
168
|
private worldOn;
|
|
156
169
|
private appsCall;
|
|
@@ -169,7 +182,9 @@ export declare class Terminals {
|
|
|
169
182
|
private agentsCall;
|
|
170
183
|
/** `/worktree`: the managed worktrees of the repository this session works in. */
|
|
171
184
|
private worktreesCall;
|
|
172
|
-
/** `/
|
|
185
|
+
/** `/usage:analysis` and `/usage:limit`: usage for this session's own account, project or session. */
|
|
186
|
+
private usageCall;
|
|
187
|
+
/** `/annotate:notes` and `/annotate:`: Web annotations and their visible capture browser. */
|
|
173
188
|
private annotationsCall;
|
|
174
189
|
/** The account a session asked to switch to. Taken once, after its Pi process has ended and before `dispose`. */
|
|
175
190
|
takeRelaunch(sessionId: string): TerminalRelaunch | null;
|
|
@@ -179,6 +194,8 @@ export declare class Terminals {
|
|
|
179
194
|
/** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
|
|
180
195
|
private begin;
|
|
181
196
|
private decide;
|
|
197
|
+
private workspaceEscape;
|
|
198
|
+
private workspaceBoundaryBypass;
|
|
182
199
|
private subagentsCall;
|
|
183
200
|
private componentsCall;
|
|
184
201
|
private trajectoryCall;
|
|
@@ -13,6 +13,8 @@ import { readAudit } from "../accounts/store.js";
|
|
|
13
13
|
import { log } from "../core/bus.js";
|
|
14
14
|
import { HarnessError, invalid } from "../core/errors.js";
|
|
15
15
|
import { newId, now } from "../core/fs.js";
|
|
16
|
+
import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
|
|
17
|
+
import { resolveWorkspaceRoot } from "../core/workspace.js";
|
|
16
18
|
import { prependSearchPath, searchPath } from "../core/path.js";
|
|
17
19
|
import { dispatch } from "../tools/dispatch.js";
|
|
18
20
|
import { materialise } from "../variant/materialise.js";
|
|
@@ -21,7 +23,13 @@ import * as variantStore from "../variant/store.js";
|
|
|
21
23
|
import { compile, copyToRun, record, render, saveForSession } from "../prompt/compile.js";
|
|
22
24
|
import { isActiveStatus } from "../sessions/contracts.js";
|
|
23
25
|
import { findRun, loadRun, saveRun, updateRun } from "../sessions/runStore.js";
|
|
26
|
+
import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
24
27
|
import { CHECKLIST_WRITE } from "../tools/checklist.js";
|
|
28
|
+
import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
|
|
29
|
+
import { harnessTools } from "../tools/harnessTools.js";
|
|
30
|
+
import { HARNESS_PLUGINS } from "../plugins/builtin.js";
|
|
31
|
+
import * as attachments from "../plugins/attachments.js";
|
|
32
|
+
import { FIRST_PARTY_PLUGINS } from "../plugins/firstParty.js";
|
|
25
33
|
import { createProfile, listProfiles, profileDirs, profileSection, removeProfile, resolveProfile } from "../agents/profiles.js";
|
|
26
34
|
import { SESSION_NAME } from "../tools/sessionName.js";
|
|
27
35
|
import { contributionId } from "../plugins/identity.js";
|
|
@@ -108,7 +116,12 @@ export class Terminals {
|
|
|
108
116
|
const owner = this.sessions.get(parent.sessionId);
|
|
109
117
|
if (owner?.controller.activeRun?.runId === parent.id) {
|
|
110
118
|
owner.controller.observe(event, true);
|
|
111
|
-
|
|
119
|
+
const settings = options.subagents?.runtimeSettings(record.id);
|
|
120
|
+
owner.controller.observeSubagent(parent.id, record, event, this.subagentProfile(record), {
|
|
121
|
+
accountLabel: owner.account.label,
|
|
122
|
+
model: settings?.model ?? null,
|
|
123
|
+
effort: settings?.effort ?? null
|
|
124
|
+
});
|
|
112
125
|
break;
|
|
113
126
|
}
|
|
114
127
|
parentId = parent.parentRunId;
|
|
@@ -153,23 +166,48 @@ export class Terminals {
|
|
|
153
166
|
}
|
|
154
167
|
return bin;
|
|
155
168
|
}
|
|
156
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* The harness's own History, Wiki and skill tools, then the broker's, then the world tools whose
|
|
171
|
+
* plugins are on for this session: the same tools a structured session has (INV-PAR-1).
|
|
172
|
+
*/
|
|
157
173
|
async tools(session, listing) {
|
|
158
174
|
const brokered = this.options.apps.brokerTools(session);
|
|
159
175
|
const live = this.sessions.get(session.sessionId);
|
|
160
|
-
|
|
161
|
-
if (!world || !live)
|
|
176
|
+
if (!live)
|
|
162
177
|
return brokered;
|
|
178
|
+
const own = await this.ownTools(live, listing);
|
|
179
|
+
const { world } = this.options;
|
|
180
|
+
if (!world)
|
|
181
|
+
return [...own, ...brokered];
|
|
163
182
|
const context = { run: live.controller.activeRun, caller: live.caller, attended: true, subagent: false, sessionKey: session.sessionId, cwd: live.input.cwd };
|
|
164
183
|
if (!listing)
|
|
165
|
-
return [...brokered, ...world.tools(context, (_plugin, tool) => this.worldNames.get(session.sessionId)?.has(tool) ?? false)];
|
|
184
|
+
return [...own, ...brokered, ...world.tools(context, (_plugin, tool) => this.worldNames.get(session.sessionId)?.has(tool) ?? false)];
|
|
166
185
|
const on = await this.worldOn(live);
|
|
167
186
|
const tools = world.tools(context, (plugin, tool) => on(plugin, tool));
|
|
168
187
|
this.worldNames.set(session.sessionId, new Set(tools.map((tool) => tool.name)));
|
|
169
188
|
for (const tool of tools)
|
|
170
189
|
if (tool.readOnly)
|
|
171
190
|
this.worldReadOnly.add(tool.name);
|
|
172
|
-
return [...brokered, ...tools];
|
|
191
|
+
return [...own, ...brokered, ...tools];
|
|
192
|
+
}
|
|
193
|
+
ownNames = new Map();
|
|
194
|
+
async ownTools(live, listing) {
|
|
195
|
+
const sessionId = live.caller.sessionId;
|
|
196
|
+
const all = harnessTools(this.options.env, live.caller, { variantDir: this.variantDirs.get(sessionId) ?? null })
|
|
197
|
+
.filter((tool) => DEFAULT_TOOL_NAMES.includes(tool.name));
|
|
198
|
+
if (!listing)
|
|
199
|
+
return all.filter((tool) => this.ownNames.get(sessionId)?.has(tool.name) ?? false);
|
|
200
|
+
const on = await this.worldOn(live);
|
|
201
|
+
const owner = (tool) => HARNESS_PLUGINS.find((plugin) => plugin.tools?.some((entry) => entry.name === tool))?.id ?? null;
|
|
202
|
+
const tools = all.filter((tool) => {
|
|
203
|
+
const plugin = owner(tool.name);
|
|
204
|
+
return !plugin || on(plugin, tool.name);
|
|
205
|
+
});
|
|
206
|
+
this.ownNames.set(sessionId, new Set(tools.map((tool) => tool.name)));
|
|
207
|
+
for (const tool of tools)
|
|
208
|
+
if (tool.readOnly)
|
|
209
|
+
this.worldReadOnly.add(tool.name);
|
|
210
|
+
return tools;
|
|
173
211
|
}
|
|
174
212
|
/** The world tools a new session will have, named in its instructions. */
|
|
175
213
|
async worldToolNames(input, account) {
|
|
@@ -181,6 +219,16 @@ export class Terminals {
|
|
|
181
219
|
const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
|
|
182
220
|
return world.plugins.flatMap((plugin) => (plugin.tools ?? []).map((tool) => tool.name).filter((name) => !on || on.has(contributionId(plugin.id, 'tool', name))));
|
|
183
221
|
}
|
|
222
|
+
/** The harness's own History, Wiki and skill tools a new session will have. */
|
|
223
|
+
async ownToolNames(input, account) {
|
|
224
|
+
const list = this.options.pluginControl?.list;
|
|
225
|
+
const plugins = list ? await list({ cwd: input.cwd, accountId: account.id, projectId: input.projectId ?? null }) : null;
|
|
226
|
+
const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
|
|
227
|
+
return DEFAULT_TOOL_NAMES.filter((name) => {
|
|
228
|
+
const plugin = HARNESS_PLUGINS.find((entry) => entry.tools?.some((tool) => tool.name === name));
|
|
229
|
+
return !on || !plugin || on.has(contributionId(plugin.id, 'tool', name));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
184
232
|
/** Whether a world tool's plugin is switched on for a session's folder, account and project. */
|
|
185
233
|
async worldOn(live) {
|
|
186
234
|
const list = this.options.pluginControl?.list;
|
|
@@ -203,6 +251,17 @@ export class Terminals {
|
|
|
203
251
|
const { Type } = await import('typebox');
|
|
204
252
|
return { tools: (await this.tools(session, true)).map((tool) => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) };
|
|
205
253
|
}
|
|
254
|
+
case 'firstParty.list': {
|
|
255
|
+
// The harness's own extensions switched on for this session, with their settings.
|
|
256
|
+
const live = this.sessions.get(session.sessionId);
|
|
257
|
+
const list = this.options.pluginControl?.list;
|
|
258
|
+
if (!live || !list)
|
|
259
|
+
return { plugins: [] };
|
|
260
|
+
const plugins = await list({ cwd: live.input.cwd, accountId: live.account.id, projectId: live.input.projectId ?? null });
|
|
261
|
+
const on = new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id)));
|
|
262
|
+
const settings = attachments.load(this.options.env.root).settings;
|
|
263
|
+
return { plugins: FIRST_PARTY_PLUGINS.filter((plugin) => plugin.code?.some((code) => on.has(`${plugin.id}/code/${code.name}`))).map((plugin) => ({ id: plugin.id, settings: settings[plugin.id] ?? {} })) };
|
|
264
|
+
}
|
|
206
265
|
case 'tools.call': {
|
|
207
266
|
const tool = (await this.tools(session, false)).find((candidate) => candidate.name === params.name);
|
|
208
267
|
if (!tool)
|
|
@@ -451,7 +510,45 @@ export class Terminals {
|
|
|
451
510
|
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
452
511
|
}
|
|
453
512
|
}
|
|
454
|
-
/** `/
|
|
513
|
+
/** `/usage:analysis` and `/usage:limit`: usage for this session's own account, project or session. */
|
|
514
|
+
async usageCall(live, call, params) {
|
|
515
|
+
const usage = this.options.usage;
|
|
516
|
+
const limits = this.options.usageLimits;
|
|
517
|
+
if (!usage || !limits)
|
|
518
|
+
throw invalid('Usage is unavailable in this host.');
|
|
519
|
+
if (call === 'terminal.usage.summary') {
|
|
520
|
+
const scope = params.scope === 'session' || params.scope === 'project' || params.scope === 'all' ? params.scope : 'account';
|
|
521
|
+
if (scope === 'project' && !live.input.projectId)
|
|
522
|
+
throw invalid('This session has no project to report usage for.');
|
|
523
|
+
const period = params.period === 'today' || params.period === '30d' || params.period === 'all' ? params.period : '7d';
|
|
524
|
+
return usage.summary({
|
|
525
|
+
scope,
|
|
526
|
+
sessionId: live.input.sessionId,
|
|
527
|
+
accountId: live.account.id,
|
|
528
|
+
projectId: live.input.projectId ?? null,
|
|
529
|
+
period,
|
|
530
|
+
timezoneOffsetMinutes: typeof params.timezoneOffsetMinutes === 'number' ? params.timezoneOffsetMinutes : 0
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (call === 'terminal.usage.limits') {
|
|
534
|
+
return params.refresh === true ? limits.refresh(live.account.id, { force: true }) : limits.read(live.account.id);
|
|
535
|
+
}
|
|
536
|
+
if (call === 'terminal.usage.headers') {
|
|
537
|
+
if (typeof params.provider !== 'string' || !params.provider)
|
|
538
|
+
throw invalid('A provider is required.');
|
|
539
|
+
if (typeof params.status !== 'number' || !Number.isFinite(params.status))
|
|
540
|
+
throw invalid('A response status is required.');
|
|
541
|
+
const headers = params.headers && typeof params.headers === 'object' && !Array.isArray(params.headers)
|
|
542
|
+
? Object.fromEntries(Object.entries(params.headers).filter((entry) => typeof entry[1] === 'string'))
|
|
543
|
+
: {};
|
|
544
|
+
if (!Object.keys(headers).length)
|
|
545
|
+
return {};
|
|
546
|
+
limits.recordHeaders(live.account.id, params.provider, { status: params.status, headers });
|
|
547
|
+
return {};
|
|
548
|
+
}
|
|
549
|
+
throw invalid('Unknown usage operation.');
|
|
550
|
+
}
|
|
551
|
+
/** `/annotate:notes` and `/annotate:`: Web annotations and their visible capture browser. */
|
|
455
552
|
async annotationsCall(live, call, params) {
|
|
456
553
|
const root = this.options.env.root;
|
|
457
554
|
const sessionId = live.input.sessionId;
|
|
@@ -578,6 +675,7 @@ export class Terminals {
|
|
|
578
675
|
sessionId: input.sessionId,
|
|
579
676
|
nativeSessionId: live.controller.nativeSessionId,
|
|
580
677
|
workingDirectory: input.cwd,
|
|
678
|
+
workspaceRoot: resolveWorkspaceRoot(input.cwd),
|
|
581
679
|
worktreeId: input.run?.worktreeId ?? null,
|
|
582
680
|
goal: text,
|
|
583
681
|
attachmentCount: 0,
|
|
@@ -586,7 +684,9 @@ export class Terminals {
|
|
|
586
684
|
status: 'queued',
|
|
587
685
|
exitCode: null,
|
|
588
686
|
handoffId: null,
|
|
589
|
-
accountId: live.account.id
|
|
687
|
+
accountId: live.account.id,
|
|
688
|
+
variantScope: live.variant?.scope ?? null,
|
|
689
|
+
variantVersion: live.variant?.fingerprint ?? null
|
|
590
690
|
};
|
|
591
691
|
}
|
|
592
692
|
/** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
|
|
@@ -612,57 +712,151 @@ export class Terminals {
|
|
|
612
712
|
const record = loadRun(this.options.env.root, run.runId);
|
|
613
713
|
if (!isActiveStatus(record.status))
|
|
614
714
|
throw invalid(`The run is no longer active (its status is ${record.status}).`);
|
|
615
|
-
const
|
|
715
|
+
const name = tool.toLowerCase();
|
|
716
|
+
const workspaceRoot = record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
|
|
717
|
+
if (live.input.readOnly && ['bash', 'edit', 'write'].includes(name))
|
|
718
|
+
return { allowed: false, reason: 'This session is read-only.', workspaceRoot };
|
|
719
|
+
let inspection;
|
|
720
|
+
try {
|
|
721
|
+
inspection = inspectRunWorkspaceWrite(record, tool, args);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
return { allowed: false, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.', workspaceRoot };
|
|
725
|
+
}
|
|
726
|
+
const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args: inspection.input };
|
|
616
727
|
const { policy } = this.options;
|
|
617
728
|
if (policy.scope) {
|
|
618
|
-
const scope = await policy.scope(call);
|
|
729
|
+
const scope = combineWorkspaceScope(await policy.scope(call), inspection);
|
|
619
730
|
if (!scope.allowed && (scope.automatic || !scope.requiresApproval))
|
|
620
|
-
return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.' };
|
|
731
|
+
return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.', workspaceRoot };
|
|
621
732
|
try {
|
|
622
733
|
await policy.authorize?.(call);
|
|
623
734
|
}
|
|
624
735
|
catch (error) {
|
|
625
|
-
return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.' };
|
|
736
|
+
return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.', workspaceRoot };
|
|
626
737
|
}
|
|
627
738
|
const requiresApproval = !!scope.requiresApproval || (scope.allowed && !!scope.humanOnly);
|
|
628
739
|
return {
|
|
629
740
|
allowed: scope.allowed,
|
|
630
741
|
requiresApproval,
|
|
631
742
|
reason: scope.reason ?? null,
|
|
743
|
+
workspaceRoot,
|
|
632
744
|
rememberable: scope.allowed && !scope.humanOnly,
|
|
633
745
|
...(scope.humanOnly ? { standing: scope.allowGlobalApproval ? 'switch' : 'none' } : {})
|
|
634
746
|
};
|
|
635
747
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (name === SESSION_NAME || name === CHECKLIST_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name))
|
|
639
|
-
return { allowed: true };
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
748
|
+
if (inspection.outside)
|
|
749
|
+
return { allowed: false, requiresApproval: true, rememberable: false, standing: 'switch', reason: inspection.reason, workspaceRoot };
|
|
750
|
+
if (name === SESSION_NAME || name === CHECKLIST_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name) || ['bash', 'edit', 'write'].includes(name)) {
|
|
751
|
+
return { allowed: true, workspaceRoot };
|
|
752
|
+
}
|
|
753
|
+
return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.', workspaceRoot };
|
|
754
|
+
}
|
|
755
|
+
async workspaceEscape(live, run, tool, args, reason) {
|
|
756
|
+
if (tool !== 'bash' || live.input.readOnly)
|
|
757
|
+
return false;
|
|
758
|
+
const record = loadRun(this.options.env.root, run.runId);
|
|
759
|
+
if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
|
|
760
|
+
return false;
|
|
761
|
+
const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args };
|
|
762
|
+
const scope = (await this.options.policy.scope?.(call)) ?? { allowed: true };
|
|
763
|
+
if (scope.automatic || (!scope.allowed && !scope.requiresApproval))
|
|
764
|
+
return false;
|
|
765
|
+
try {
|
|
766
|
+
await this.options.policy.authorize?.(call);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
|
|
772
|
+
const command = typeof operationArgs.command === 'string' ? operationArgs.command : '';
|
|
773
|
+
if (operationArgs.workspaceWriteScope !== 'outside' || !command)
|
|
774
|
+
return false;
|
|
775
|
+
const workspaceRoot = record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
|
|
776
|
+
const decision = await run.requestApproval({
|
|
777
|
+
target: 'workspace-write',
|
|
778
|
+
action: `${reason} Workspace: ${workspaceRoot}. This exact command will run once with broader filesystem access.\n\nCommand:\n${formatBashCommandForApproval(command)}`,
|
|
779
|
+
operation: { tool, args: operationArgs },
|
|
780
|
+
rememberable: false,
|
|
781
|
+
standing: 'none'
|
|
782
|
+
});
|
|
783
|
+
if (decision !== 'approved')
|
|
784
|
+
return false;
|
|
785
|
+
const currentRecord = loadRun(this.options.env.root, run.runId);
|
|
786
|
+
if (!isActiveStatus(currentRecord.status) || (currentRecord.trigger != null && currentRecord.trigger !== 'user'))
|
|
787
|
+
return false;
|
|
788
|
+
const currentCall = { run: currentRecord, caller: { ...live.caller, runId: run.runId }, tool, args: operationArgs };
|
|
789
|
+
const currentScope = (await this.options.policy.scope?.(currentCall)) ?? { allowed: true };
|
|
790
|
+
if (currentScope.automatic || (!currentScope.allowed && !currentScope.requiresApproval))
|
|
791
|
+
return false;
|
|
792
|
+
try {
|
|
793
|
+
await this.options.policy.authorize?.(currentCall);
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async workspaceBoundaryBypass(live, run, tool, args) {
|
|
801
|
+
if (tool !== 'bash' || live.input.readOnly || !this.options.approvals.isAuto('pi'))
|
|
802
|
+
return false;
|
|
803
|
+
const record = loadRun(this.options.env.root, run.runId);
|
|
804
|
+
if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
|
|
805
|
+
return false;
|
|
806
|
+
const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
|
|
807
|
+
if (typeof operationArgs.command !== 'string' || !operationArgs.command)
|
|
808
|
+
return false;
|
|
809
|
+
const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args: operationArgs };
|
|
810
|
+
try {
|
|
811
|
+
const scope = (await this.options.policy.scope?.(call)) ?? { allowed: true };
|
|
812
|
+
if (scope.automatic || (!scope.allowed && !scope.requiresApproval) || (scope.humanOnly && !scope.allowGlobalApproval))
|
|
813
|
+
return false;
|
|
814
|
+
await this.options.policy.authorize?.(call);
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
643
820
|
}
|
|
644
821
|
async subagentsCall(live, call, params) {
|
|
645
822
|
if (call === 'terminal.subagents.monitor')
|
|
646
823
|
return { children: live.controller.subagentSnapshot() };
|
|
647
824
|
const service = this.options.subagents;
|
|
648
|
-
if (!service || live.input.subagents === null) {
|
|
825
|
+
if ((!service || live.input.subagents === null) && call !== 'terminal.subagents.register') {
|
|
649
826
|
if (call === 'terminal.subagents.tools')
|
|
650
827
|
return { tools: [] };
|
|
651
828
|
throw invalid('Subagents are disabled for this terminal.');
|
|
652
829
|
}
|
|
653
|
-
if (call === 'terminal.subagents.tools')
|
|
654
|
-
|
|
830
|
+
if (call === 'terminal.subagents.tools') {
|
|
831
|
+
const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
|
|
832
|
+
return {
|
|
833
|
+
tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })),
|
|
834
|
+
profiles: {
|
|
835
|
+
normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
|
|
836
|
+
plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true).body
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
655
840
|
const run = live.controller.activeRun;
|
|
656
841
|
if (!run)
|
|
657
842
|
throw new HarnessError('run_not_active', 'This terminal has no active parent run.');
|
|
843
|
+
if (call === 'terminal.subagents.planSafe') {
|
|
844
|
+
const reason = service.planSpawnRefusal(run.runId, params.input);
|
|
845
|
+
return { allowed: reason === null, ...(reason ? { reason } : {}) };
|
|
846
|
+
}
|
|
658
847
|
if (call === 'terminal.subagents.register') {
|
|
659
848
|
if (!Array.isArray(params.tools) || params.tools.some((name) => typeof name !== 'string'))
|
|
660
849
|
throw invalid('Native tool names are required.');
|
|
850
|
+
if (params.plan !== undefined && typeof params.plan !== 'boolean')
|
|
851
|
+
throw invalid('plan must be a boolean.');
|
|
661
852
|
const readOnly = live.input.readOnly === true;
|
|
662
853
|
const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, CHECKLIST_WRITE, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
|
|
663
|
-
const tools = params.tools.filter(
|
|
664
|
-
const
|
|
665
|
-
|
|
854
|
+
const tools = [...new Set(params.tools)].filter(name => (!readOnly || readTools.has(name)) && (params.plan === true || (name !== 'ask_user' && name !== 'ask_user_question')));
|
|
855
|
+
const caller = { ...live.caller, runId: run.runId };
|
|
856
|
+
const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
|
|
857
|
+
freezeRun(this.options.env.root, run.runId, caller, capabilities);
|
|
858
|
+
if (!service || live.input.subagents === null)
|
|
859
|
+
return { runId: run.runId, connectors: capabilities.connectors };
|
|
666
860
|
service.register({
|
|
667
861
|
run: { id: run.runId, sessionId: live.input.sessionId, mode: live.input.run?.mode ?? 'code', projectId: live.input.projectId, worktreeId: live.input.run?.worktreeId },
|
|
668
862
|
cwd: live.input.cwd, sessionDir: this.historyDir(live.input), text: '', caller: live.caller,
|
|
@@ -670,11 +864,12 @@ export class Terminals {
|
|
|
670
864
|
effort: typeof params.effort === 'string' ? params.effort : null,
|
|
671
865
|
launch: live.input.launch, tools: { allow: capabilities.tools, gated: capabilities.tools.filter(name => !readTools.has(name)) },
|
|
672
866
|
connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
|
|
673
|
-
|
|
867
|
+
plan: params.plan === true,
|
|
868
|
+
variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
|
|
674
869
|
capabilities
|
|
675
870
|
}, live.account.id, capabilities);
|
|
676
871
|
service.restore(run.runId, params.history ?? { owners: [], receipts: [] });
|
|
677
|
-
return { runId: run.runId };
|
|
872
|
+
return { runId: run.runId, connectors: capabilities.connectors };
|
|
678
873
|
}
|
|
679
874
|
if (call === 'terminal.subagents.verify') {
|
|
680
875
|
await service.verify(run.runId, params.history);
|
|
@@ -834,16 +1029,20 @@ export class Terminals {
|
|
|
834
1029
|
if (variant)
|
|
835
1030
|
this.variantDirs.set(sessionId, variant.dir);
|
|
836
1031
|
const worldTools = await this.worldToolNames(input, preparedAccount);
|
|
837
|
-
const
|
|
1032
|
+
const ownTools = await this.ownToolNames(input, preparedAccount);
|
|
1033
|
+
const launchTools = [...new Set([...(input.launch?.tools ?? []), CHECKLIST_WRITE, SESSION_NAME, ...ownTools, ...(broker ? apps.toolNames : []), ...worldTools])];
|
|
838
1034
|
// The profiles replace the variant's own subagent list, which they include.
|
|
839
1035
|
const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
|
|
840
1036
|
const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
|
|
841
|
-
|
|
1037
|
+
// A terminal is a person's own session: the same standard sections a host's session has.
|
|
1038
|
+
const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
|
|
1039
|
+
const pack = compile(env, caller, { ...input.launch, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
|
|
842
1040
|
record(root, pack);
|
|
843
1041
|
mkdirSync(this.historyDir(input), { recursive: true });
|
|
844
1042
|
saveForSession(input.sessionFolder, pack);
|
|
845
1043
|
const live = {
|
|
846
1044
|
input,
|
|
1045
|
+
variant,
|
|
847
1046
|
account: preparedAccount,
|
|
848
1047
|
broker,
|
|
849
1048
|
caller,
|
|
@@ -853,6 +1052,12 @@ export class Terminals {
|
|
|
853
1052
|
nativeSessionId: input.nativeSessionId ?? null,
|
|
854
1053
|
begin: ({ reservedRunId, text }) => this.begin(live, reservedRunId, text),
|
|
855
1054
|
policy: (run, tool, args) => this.decide(live, run, tool, args),
|
|
1055
|
+
workspaceRoot: run => {
|
|
1056
|
+
const record = loadRun(this.options.env.root, run.runId);
|
|
1057
|
+
return record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
|
|
1058
|
+
},
|
|
1059
|
+
workspaceEscape: (run, tool, args, reason) => this.workspaceEscape(live, run, tool, args, reason),
|
|
1060
|
+
workspaceBoundaryBypass: (run, tool, args) => this.workspaceBoundaryBypass(live, run, tool, args),
|
|
856
1061
|
allowToolGlobally: target => { this.options.approvals.allowTool('pi', target); },
|
|
857
1062
|
native: (nativeSessionId) => bus.emit({ type: 'terminal.native', sessionId, nativeSessionId }),
|
|
858
1063
|
telemetry: (event) => bus.emit({ type: 'terminal.telemetry', sessionId, event }),
|
|
@@ -980,11 +1185,15 @@ export class Terminals {
|
|
|
980
1185
|
: this.options.approvals.revokeTool('pi', params.tool);
|
|
981
1186
|
return { globalTools: policy.globalTools.pi ?? [] };
|
|
982
1187
|
}
|
|
1188
|
+
if (call.startsWith('terminal.usage.'))
|
|
1189
|
+
return this.usageCall(live, call, params);
|
|
983
1190
|
if (call !== 'terminal.resources')
|
|
984
1191
|
return live.controller.control(call, params);
|
|
985
1192
|
const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
|
|
986
|
-
|
|
1193
|
+
const currentVariant = variant ? materialise(root, { dir: variant.dir, scope: variant.scope }, { trust: this.options.trust ?? null }) : null;
|
|
1194
|
+
if (resourceIdentity(current) !== resourceIdentity(loading) || (variant && (currentVariant?.fingerprint !== variant.fingerprint || JSON.stringify(currentVariant.extensionPaths) !== JSON.stringify(variant.extensionPaths)))) {
|
|
987
1195
|
throw new HarnessError('terminal_resources_changed', 'Trusted resources changed since this terminal was prepared. Review /plugins and reopen the terminal to load the new selection.');
|
|
1196
|
+
}
|
|
988
1197
|
return {};
|
|
989
1198
|
} }
|
|
990
1199
|
});
|
|
@@ -993,16 +1202,17 @@ export class Terminals {
|
|
|
993
1202
|
const args = [...command.argsPrefix, '--append-system-prompt', render(env, pack)];
|
|
994
1203
|
if (input.model)
|
|
995
1204
|
args.push('--model', input.model);
|
|
996
|
-
|
|
1205
|
+
{
|
|
997
1206
|
// Pi's own discovery is off: the terminal loads what the harness found and nothing else.
|
|
998
1207
|
args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes');
|
|
999
|
-
|
|
1208
|
+
// The pi command's own extensions are not loaded: a host's session cannot have them (PROP-0039).
|
|
1209
|
+
for (const path of [...(loading?.extensionPaths ?? []), ...(variant?.extensionPaths ?? [])])
|
|
1000
1210
|
args.push('--extension', path);
|
|
1001
|
-
for (const path of [...(variant?.skillPaths ?? []), ...loading
|
|
1211
|
+
for (const path of [...(variant?.skillPaths ?? []), ...(loading?.skillPaths ?? [])])
|
|
1002
1212
|
args.push('--skill', path);
|
|
1003
|
-
for (const path of loading
|
|
1213
|
+
for (const path of loading?.promptPaths ?? [])
|
|
1004
1214
|
args.push('--prompt-template', path);
|
|
1005
|
-
for (const path of loading
|
|
1215
|
+
for (const path of loading?.themePaths ?? [])
|
|
1006
1216
|
args.push('--theme', path);
|
|
1007
1217
|
}
|
|
1008
1218
|
// Last, so its gate sees a tool call as every other extension left it.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { AnnotateStart } from "./pages/annotatePage.js";
|
|
2
|
-
/** `/
|
|
2
|
+
/** `/annotate:annotate` accepts a page address, DevTools target, or page-bridge target. */
|
|
3
3
|
export declare function parseAnnotate(argument: string): AnnotateStart | string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** `/
|
|
1
|
+
/** `/annotate:annotate` accepts a page address, DevTools target, or page-bridge target. */
|
|
2
2
|
export function parseAnnotate(argument) {
|
|
3
3
|
const words = argument.trim().split(/\s+/).filter(Boolean);
|
|
4
4
|
if (!words.length)
|
|
@@ -6,7 +6,7 @@ export function parseAnnotate(argument) {
|
|
|
6
6
|
if (words[0] === '--app') {
|
|
7
7
|
const endpoint = words[1];
|
|
8
8
|
if (!endpoint)
|
|
9
|
-
return 'Give the app\'s DevTools address: /
|
|
9
|
+
return 'Give the app\'s DevTools address: /annotate:annotate --app http://127.0.0.1:9229';
|
|
10
10
|
const address = /^\d+$/.test(endpoint) ? `http://127.0.0.1:${endpoint}` : endpoint;
|
|
11
11
|
const name = words.slice(2).join(' ');
|
|
12
12
|
return { kind: 'cdp', endpoint: address, ...(name ? { name } : {}) };
|
|
@@ -16,7 +16,7 @@ export function parseAnnotate(argument) {
|
|
|
16
16
|
return { kind: 'bridge', ...(name ? { name } : {}) };
|
|
17
17
|
}
|
|
18
18
|
if (words.length > 1 || words[0].startsWith('--'))
|
|
19
|
-
return 'Use /
|
|
19
|
+
return 'Use /annotate:annotate [url], /annotate:annotate --app <DevTools address>, or /annotate:annotate --bridge.';
|
|
20
20
|
const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(words[0]) ? words[0] : /^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(words[0]) ? `http://${words[0]}` : `https://${words[0]}`;
|
|
21
21
|
return { kind: 'web', url };
|
|
22
22
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { TerminalPage } from "./page.js";
|
|
2
2
|
import type { TerminalCommandHost } from "./commands.js";
|
|
3
3
|
import { type AnnotateStart } from "./pages/annotatePage.js";
|
|
4
|
-
export type CommandPageId = 'doctor' | 'accounts' | 'permissions' | 'plugins' | 'hub' | 'components' | 'settings' | 'trajectory' | 'log' | 'harness' | 'skills' | 'connections' | 'tools' | 'worktree' | 'notes' | 'annotate' | 'subagents' | 'agents';
|
|
4
|
+
export type CommandPageId = 'doctor' | 'accounts' | 'permissions' | 'plugins' | 'hub' | 'components' | 'settings' | 'trajectory' | 'log' | 'harness' | 'skills' | 'connections' | 'tools' | 'worktree' | 'notes' | 'annotate' | 'subagents' | 'agents' | 'usage';
|
|
5
5
|
/** Construct one independent full-screen page for a slash command. */
|
|
6
6
|
export declare function createCommandPage(host: TerminalCommandHost, id: CommandPageId, options?: {
|
|
7
7
|
runId?: string;
|
|
8
8
|
url?: string;
|
|
9
9
|
annotate?: AnnotateStart;
|
|
10
|
+
usageTab?: 'analysis' | 'limit';
|
|
11
|
+
usagePeriod?: 'today' | '7d' | '30d' | 'all';
|
|
10
12
|
close?(): void;
|
|
11
13
|
}): TerminalPage;
|
|
@@ -10,12 +10,15 @@ import { NotesPage } from "./pages/notesPage.js";
|
|
|
10
10
|
import { AnnotatePage } from "./pages/annotatePage.js";
|
|
11
11
|
import { SubagentsPage } from "./pages/subagentsPage.js";
|
|
12
12
|
import { AgentsPage } from "./pages/agentsPage.js";
|
|
13
|
+
import { UsagePage } from "./pages/usagePage.js";
|
|
13
14
|
import { createSettingsModel } from "./settingsModel.js";
|
|
14
15
|
/** Construct one independent full-screen page for a slash command. */
|
|
15
16
|
export function createCommandPage(host, id, options = {}) {
|
|
16
17
|
const theme = () => host.ui.theme;
|
|
17
18
|
const notify = (message, type) => host.ui.notify(message, type === 'error' || type === 'warning' ? type : 'info');
|
|
18
19
|
switch (id) {
|
|
20
|
+
case 'usage':
|
|
21
|
+
return new UsagePage({ theme, call: host.control, changed: host.changed, initialTab: options.usageTab === 'limit' ? 'limits' : 'analysis', ...(options.usagePeriod ? { initialPeriod: options.usagePeriod } : {}) });
|
|
19
22
|
case 'doctor':
|
|
20
23
|
return new DoctorPage({ theme, call: host.control, confirm: host.ui.confirm.bind(host.ui), select: host.ui.select.bind(host.ui), changed: host.changed,
|
|
21
24
|
selection: () => {
|