@xingular/harness 1.0.0 → 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 +46 -5
- package/README.md +75 -21
- package/dist/cli.js +17421 -8711
- 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.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +48 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +81 -0
- package/dist/sdk/agents/profiles.js +226 -0
- package/dist/sdk/annotations/bridge.d.ts +42 -0
- package/dist/sdk/annotations/bridge.js +264 -0
- package/dist/sdk/annotations/capture.d.ts +116 -0
- package/dist/sdk/annotations/capture.js +308 -0
- package/dist/sdk/annotations/delivery.d.ts +72 -0
- package/dist/sdk/annotations/delivery.js +130 -0
- package/dist/sdk/annotations/inspect.d.ts +10 -0
- package/dist/sdk/annotations/inspect.js +33 -0
- package/dist/sdk/annotations/isolated.d.ts +6 -0
- package/dist/sdk/annotations/isolated.js +51 -0
- package/dist/sdk/annotations/pageScript.d.ts +28 -0
- package/dist/sdk/annotations/pageScript.js +871 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +155 -0
- package/dist/sdk/annotations/queue.d.ts +4 -0
- package/dist/sdk/annotations/queue.js +40 -0
- package/dist/sdk/annotations/serverMethods.d.ts +3 -0
- package/dist/sdk/annotations/serverMethods.js +62 -0
- package/dist/sdk/annotations/store.d.ts +167 -0
- package/dist/sdk/annotations/store.js +394 -0
- 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/index.d.ts +14 -3
- package/dist/sdk/browser/index.js +11 -4
- package/dist/sdk/browser/serverMethods.d.ts +3 -0
- package/dist/sdk/browser/serverMethods.js +7 -0
- package/dist/sdk/browser/session.d.ts +1 -0
- package/dist/sdk/browser/session.js +45 -9
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +24 -15
- 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/bus.js +1 -1
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- 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/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +22 -0
- package/dist/sdk/harness.js +84 -5
- package/dist/sdk/history/recall.js +8 -8
- package/dist/sdk/history/serverMethods.d.ts +3 -0
- package/dist/sdk/history/serverMethods.js +8 -0
- package/dist/sdk/hub/cli.d.ts +1 -1
- package/dist/sdk/hub/cli.js +1 -1
- package/dist/sdk/hub/schema.d.ts +1 -1
- package/dist/sdk/hub/serverMethods.d.ts +3 -0
- package/dist/sdk/hub/serverMethods.js +11 -0
- package/dist/sdk/index.d.ts +16 -1
- package/dist/sdk/index.js +10 -0
- package/dist/sdk/mcp/connections.js +14 -7
- package/dist/sdk/mcp/serverMethods.d.ts +3 -0
- package/dist/sdk/mcp/serverMethods.js +22 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +145 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +60 -0
- package/dist/sdk/plugins/builtinRegistry.js +71 -0
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- 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 +19 -2
- package/dist/sdk/prompt/compile.js +79 -26
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/refine/cli.d.ts +1 -1
- package/dist/sdk/refine/cli.js +2 -2
- package/dist/sdk/refine/serverMethods.d.ts +3 -0
- package/dist/sdk/refine/serverMethods.js +20 -0
- package/dist/sdk/server/methods.js +27 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -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 +185 -0
- package/dist/sdk/sessions/contracts.js +101 -4
- 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 +18 -4
- package/dist/sdk/sessions/planMode.js +82 -16
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +2 -1
- package/dist/sdk/sessions/registry.js +6 -3
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +38 -1
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +163 -15
- package/dist/sdk/sessions/sessions.d.ts +40 -8
- package/dist/sdk/sessions/sessions.js +279 -27
- package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
- package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
- package/dist/sdk/sessions/sideQuestions.js +290 -0
- package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/subagentServerMethods.js +41 -0
- package/dist/sdk/sessions/subagentService.d.ts +22 -2
- package/dist/sdk/sessions/subagentService.js +93 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- 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/browser.d.ts +62 -0
- package/dist/sdk/terminal/browser.js +41 -0
- package/dist/sdk/terminal/browserControl.d.ts +29 -0
- package/dist/sdk/terminal/browserControl.js +187 -0
- package/dist/sdk/terminal/commandHelp.d.ts +17 -0
- package/dist/sdk/terminal/commandHelp.js +20 -0
- package/dist/sdk/terminal/controller.d.ts +15 -1
- package/dist/sdk/terminal/controller.js +67 -8
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- 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 +169 -84
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +9 -0
- package/dist/sdk/terminal/standalone.js +118 -19
- package/dist/sdk/terminal/subagentMonitor.d.ts +47 -0
- package/dist/sdk/terminal/subagentMonitor.js +152 -0
- package/dist/sdk/terminal/terminals.d.ts +60 -7
- package/dist/sdk/terminal/terminals.js +598 -47
- package/dist/sdk/terminal/trajectory.js +2 -2
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
- package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +7 -1
- package/dist/sdk/terminal/tui/commandPages.js +57 -6
- package/dist/sdk/terminal/tui/commands.d.ts +10 -4
- package/dist/sdk/terminal/tui/commands.js +269 -65
- package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
- package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
- package/dist/sdk/terminal/tui/dialogs.js +9 -2
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/modelPicker.js +1 -6
- package/dist/sdk/terminal/tui/page.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +27 -7
- package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
- package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
- package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
- package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
- package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
- package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
- 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/settingsPage.d.ts +8 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +24 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +152 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
- 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/pages/worktreesPage.d.ts +72 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -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/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +16 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +31 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +27 -5
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
- package/dist/sdk/terminal/tui/settingsModel.js +32 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
- package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
- package/dist/sdk/terminal/tui/ui.d.ts +2 -0
- package/dist/sdk/terminal/tui/ui.js +1 -0
- package/dist/sdk/tools/askUser.js +26 -8
- package/dist/sdk/tools/checklist.js +3 -2
- 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/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +14 -4
- 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/sdk/web/part.d.ts +10 -0
- package/dist/sdk/web/part.js +6 -0
- package/dist/sdk/web/serverMethods.d.ts +3 -0
- package/dist/sdk/web/serverMethods.js +7 -0
- package/dist/sdk/wiki/serverMethods.d.ts +3 -0
- package/dist/sdk/wiki/serverMethods.js +26 -0
- package/dist/sdk/worktrees/cli.d.ts +11 -0
- package/dist/sdk/worktrees/cli.js +146 -0
- package/dist/sdk/worktrees/copy.d.ts +9 -0
- package/dist/sdk/worktrees/copy.js +111 -0
- package/dist/sdk/worktrees/git.d.ts +16 -0
- package/dist/sdk/worktrees/git.js +81 -0
- package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
- package/dist/sdk/worktrees/serverMethods.js +30 -0
- package/dist/sdk/worktrees/service.d.ts +73 -0
- package/dist/sdk/worktrees/service.js +483 -0
- package/dist/sdk/worktrees/setup.d.ts +23 -0
- package/dist/sdk/worktrees/setup.js +65 -0
- package/dist/sdk/worktrees/types.d.ts +91 -0
- package/dist/sdk/worktrees/types.js +1 -0
- package/dist/sdk/world/cli.d.ts +2 -2
- package/dist/sdk/world/cli.js +6 -6
- package/dist/sdk/world/world.d.ts +3 -1
- package/dist/sdk/world/world.js +8 -9
- package/dist/terminal-opentui.js +16456 -5966
- package/dist/terminal-pi.js +1579 -573
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +184 -25
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +65 -7
- package/docs/architecture/06-plugins-and-capabilities.md +57 -4
- package/docs/architecture/07-tools-and-gate.md +112 -17
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -1
- package/docs/architecture/10-server-protocol.md +168 -4
- package/docs/architecture/11-terminal.md +251 -8
- package/docs/architecture/12-storage.md +45 -3
- package/docs/architecture/13-world-tools.md +49 -6
- package/docs/architecture/14-surfaces-and-parity.md +136 -0
- package/docs/architecture/README.md +17 -4
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
- package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
- package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
- package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
- package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +131 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
- package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
- package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
- package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
- package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
- package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
- package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
- package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
- package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
- package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
- package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
- package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
- 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 +39 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0009-xharness-command.md +6 -5
- package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
- package/docs/decisions/ADR-0015-resource-browser.md +28 -0
- package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
- package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
- package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
- package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
- package/docs/decisions/ADR-0020-page-annotations.md +37 -0
- package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
- package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
- package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
- package/docs/decisions/ADR-0024-xagent-command.md +40 -0
- package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
- package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
- package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
- package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
- package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
- package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
- 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/doctor.md +10 -4
- 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/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +134 -0
- package/docs/releases/1.0.0.md +2 -2
- 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 +47 -0
- package/docs/releases/README.md +37 -27
- package/docs/study/00-index.md +8 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/study/autonomous-engineering-sources.md +277 -0
- package/docs/study/autonomy-sources.md +147 -0
- package/docs/study/proactive-autonomy.md +408 -0
- package/docs/study/tui-resource-browser.md +78 -0
- package/docs/terminal-tui.md +386 -113
- package/package.json +6 -5
|
@@ -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,8 +23,19 @@ 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";
|
|
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";
|
|
33
|
+
import { createProfile, listProfiles, profileDirs, profileSection, removeProfile, resolveProfile } from "../agents/profiles.js";
|
|
24
34
|
import { SESSION_NAME } from "../tools/sessionName.js";
|
|
25
35
|
import { contributionId } from "../plugins/identity.js";
|
|
36
|
+
import { setupScript } from "../worktrees/setup.js";
|
|
37
|
+
import { inspectStep } from "../annotations/inspect.js";
|
|
38
|
+
import * as pageNotes from "../annotations/store.js";
|
|
26
39
|
import { APPS_COMMAND, WIKI_COMMAND } from "./cli.js";
|
|
27
40
|
import { SOCKET_ENV, TOKEN_ENV } from "./client.js";
|
|
28
41
|
import { TerminalController } from "./controller.js";
|
|
@@ -30,6 +43,7 @@ import { terminalCommandFiles } from "./launch.js";
|
|
|
30
43
|
import { SessionSocket, socketPath } from "./socket.js";
|
|
31
44
|
import { harnessLogInfo, readHarnessLogChunk } from "./logs.js";
|
|
32
45
|
import { listTerminalTrajectoryRuns, readTerminalTrajectory } from "./trajectory.js";
|
|
46
|
+
import { BrowserControl } from "./browserControl.js";
|
|
33
47
|
/** Identity of the exact resource decision that this terminal was prepared with. */
|
|
34
48
|
function resourceIdentity(loading) {
|
|
35
49
|
if (!loading)
|
|
@@ -63,8 +77,27 @@ export class Terminals {
|
|
|
63
77
|
worldReadOnly = new Set();
|
|
64
78
|
socket;
|
|
65
79
|
assets;
|
|
66
|
-
/** A session's `/accounts` switch, taken once its Pi process has ended. */
|
|
80
|
+
/** A session's `/accounts` or `/worktree` switch, taken once its Pi process has ended. */
|
|
67
81
|
relaunches = new Map();
|
|
82
|
+
/** The managed worktree each live session holds. */
|
|
83
|
+
worktreeLeases = new Map();
|
|
84
|
+
/** Each child's profile name and colour, looked up once per child run. */
|
|
85
|
+
childProfiles = new Map();
|
|
86
|
+
subagentProfile(record) {
|
|
87
|
+
if (!record.profile)
|
|
88
|
+
return null;
|
|
89
|
+
if (!this.childProfiles.has(record.id)) {
|
|
90
|
+
if (this.childProfiles.size > 500)
|
|
91
|
+
this.childProfiles.clear();
|
|
92
|
+
let color = 'blue';
|
|
93
|
+
try {
|
|
94
|
+
color = resolveProfile(this.options.env.root, record.workingDirectory, record.profile)?.color ?? color;
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
this.childProfiles.set(record.id, { name: record.profile, color });
|
|
98
|
+
}
|
|
99
|
+
return this.childProfiles.get(record.id) ?? null;
|
|
100
|
+
}
|
|
68
101
|
constructor(options) {
|
|
69
102
|
this.options = options;
|
|
70
103
|
this.assets = options.assets ?? defaultAssets();
|
|
@@ -83,6 +116,12 @@ export class Terminals {
|
|
|
83
116
|
const owner = this.sessions.get(parent.sessionId);
|
|
84
117
|
if (owner?.controller.activeRun?.runId === parent.id) {
|
|
85
118
|
owner.controller.observe(event, true);
|
|
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
|
+
});
|
|
86
125
|
break;
|
|
87
126
|
}
|
|
88
127
|
parentId = parent.parentRunId;
|
|
@@ -127,23 +166,48 @@ export class Terminals {
|
|
|
127
166
|
}
|
|
128
167
|
return bin;
|
|
129
168
|
}
|
|
130
|
-
/**
|
|
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
|
+
*/
|
|
131
173
|
async tools(session, listing) {
|
|
132
174
|
const brokered = this.options.apps.brokerTools(session);
|
|
133
175
|
const live = this.sessions.get(session.sessionId);
|
|
134
|
-
|
|
135
|
-
if (!world || !live)
|
|
176
|
+
if (!live)
|
|
136
177
|
return brokered;
|
|
178
|
+
const own = await this.ownTools(live, listing);
|
|
179
|
+
const { world } = this.options;
|
|
180
|
+
if (!world)
|
|
181
|
+
return [...own, ...brokered];
|
|
137
182
|
const context = { run: live.controller.activeRun, caller: live.caller, attended: true, subagent: false, sessionKey: session.sessionId, cwd: live.input.cwd };
|
|
138
183
|
if (!listing)
|
|
139
|
-
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)];
|
|
140
185
|
const on = await this.worldOn(live);
|
|
141
186
|
const tools = world.tools(context, (plugin, tool) => on(plugin, tool));
|
|
142
187
|
this.worldNames.set(session.sessionId, new Set(tools.map((tool) => tool.name)));
|
|
143
188
|
for (const tool of tools)
|
|
144
189
|
if (tool.readOnly)
|
|
145
190
|
this.worldReadOnly.add(tool.name);
|
|
146
|
-
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;
|
|
147
211
|
}
|
|
148
212
|
/** The world tools a new session will have, named in its instructions. */
|
|
149
213
|
async worldToolNames(input, account) {
|
|
@@ -155,6 +219,16 @@ export class Terminals {
|
|
|
155
219
|
const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
|
|
156
220
|
return world.plugins.flatMap((plugin) => (plugin.tools ?? []).map((tool) => tool.name).filter((name) => !on || on.has(contributionId(plugin.id, 'tool', name))));
|
|
157
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
|
+
}
|
|
158
232
|
/** Whether a world tool's plugin is switched on for a session's folder, account and project. */
|
|
159
233
|
async worldOn(live) {
|
|
160
234
|
const list = this.options.pluginControl?.list;
|
|
@@ -177,6 +251,17 @@ export class Terminals {
|
|
|
177
251
|
const { Type } = await import('typebox');
|
|
178
252
|
return { tools: (await this.tools(session, true)).map((tool) => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) };
|
|
179
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
|
+
}
|
|
180
265
|
case 'tools.call': {
|
|
181
266
|
const tool = (await this.tools(session, false)).find((candidate) => candidate.name === params.name);
|
|
182
267
|
if (!tool)
|
|
@@ -235,6 +320,14 @@ export class Terminals {
|
|
|
235
320
|
const settings = params.settings && typeof params.settings === 'object' && !Array.isArray(params.settings) ? params.settings : {};
|
|
236
321
|
return api.configure({ ...scope, pluginId: text('pluginId'), settings });
|
|
237
322
|
}
|
|
323
|
+
case 'plugins.install':
|
|
324
|
+
if (!api.install)
|
|
325
|
+
throw invalid('Package installation is unavailable in this host.');
|
|
326
|
+
return api.install({ ...scope, source: text('source') });
|
|
327
|
+
case 'plugins.remove':
|
|
328
|
+
if (!api.remove)
|
|
329
|
+
throw invalid('Package removal is unavailable in this host.');
|
|
330
|
+
return api.remove({ ...scope, pluginId: text('pluginId') });
|
|
238
331
|
default:
|
|
239
332
|
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
240
333
|
}
|
|
@@ -324,6 +417,232 @@ export class Terminals {
|
|
|
324
417
|
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
325
418
|
}
|
|
326
419
|
}
|
|
420
|
+
/** `/agents`: the agent profiles this session's folder can use (PROP-0029). */
|
|
421
|
+
agentsCall(live, call, params) {
|
|
422
|
+
const root = this.options.env.root;
|
|
423
|
+
const cwd = live.input.cwd;
|
|
424
|
+
const name = () => {
|
|
425
|
+
if (typeof params.name !== 'string' || !params.name.trim())
|
|
426
|
+
throw invalid('A profile name is required.');
|
|
427
|
+
return params.name.trim();
|
|
428
|
+
};
|
|
429
|
+
const scope = () => {
|
|
430
|
+
if (params.scope !== 'global' && params.scope !== 'project')
|
|
431
|
+
throw invalid('scope must be global or project.');
|
|
432
|
+
return params.scope;
|
|
433
|
+
};
|
|
434
|
+
switch (call.slice('terminal.agents.'.length)) {
|
|
435
|
+
case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null), dirs: profileDirs(root, cwd) };
|
|
436
|
+
case 'create': return createProfile(root, cwd, { name: name(), scope: scope(), from: typeof params.from === 'string' && params.from ? params.from : null });
|
|
437
|
+
case 'remove':
|
|
438
|
+
removeProfile(root, cwd, { name: name(), scope: scope() });
|
|
439
|
+
return {};
|
|
440
|
+
default: throw invalid('Unknown profile operation.');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** `/worktree`: the managed worktrees of the repository this session works in. */
|
|
444
|
+
async worktreesCall(live, call, params) {
|
|
445
|
+
const worktrees = this.options.worktrees;
|
|
446
|
+
if (!worktrees)
|
|
447
|
+
throw invalid('Worktrees are unavailable in this host.');
|
|
448
|
+
const sessionId = live.input.sessionId;
|
|
449
|
+
const cwd = live.input.cwd;
|
|
450
|
+
const current = this.worktreeLeases.get(sessionId) ?? null;
|
|
451
|
+
const id = () => {
|
|
452
|
+
if (typeof params.id !== 'string' || !params.id)
|
|
453
|
+
throw invalid('A worktree id is required.');
|
|
454
|
+
return params.id;
|
|
455
|
+
};
|
|
456
|
+
const action = call.slice('terminal.worktrees.'.length);
|
|
457
|
+
switch (action) {
|
|
458
|
+
case 'list': {
|
|
459
|
+
let repo;
|
|
460
|
+
try {
|
|
461
|
+
repo = await worktrees.repositoryOf(cwd);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return { repo: null, current, worktrees: [], removed: [], reason: error instanceof Error ? error.message : String(error) };
|
|
465
|
+
}
|
|
466
|
+
const statuses = await Promise.all((await worktrees.list(repo)).map((record) => worktrees.status(record.id)));
|
|
467
|
+
return { repo, current, worktrees: statuses, removed: worktrees.removed(repo), reason: null };
|
|
468
|
+
}
|
|
469
|
+
case 'status': return worktrees.status(id());
|
|
470
|
+
case 'create': return worktrees.create({
|
|
471
|
+
repo: cwd,
|
|
472
|
+
...(typeof params.name === 'string' && params.name ? { name: params.name } : {}),
|
|
473
|
+
...(typeof params.branch === 'string' && params.branch ? { branch: params.branch } : {}),
|
|
474
|
+
kind: params.kind === 'permanent' ? 'permanent' : 'managed'
|
|
475
|
+
});
|
|
476
|
+
case 'branch':
|
|
477
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
478
|
+
throw invalid('A branch name is required.');
|
|
479
|
+
return worktrees.branch(id(), params.name);
|
|
480
|
+
case 'handoff': {
|
|
481
|
+
const direction = params.direction === 'toSource' || params.direction === 'toWorktree' ? params.direction : null;
|
|
482
|
+
if (!direction)
|
|
483
|
+
throw invalid('direction is toSource or toWorktree.');
|
|
484
|
+
return worktrees.handoff(id(), direction, { move: params.move !== false });
|
|
485
|
+
}
|
|
486
|
+
case 'remove':
|
|
487
|
+
if (id() === current)
|
|
488
|
+
throw new HarnessError('worktree_in_use', 'This session works in that worktree. Switch to another folder first.');
|
|
489
|
+
return worktrees.remove(id(), { force: params.force === true });
|
|
490
|
+
case 'restore': return worktrees.restore(id());
|
|
491
|
+
case 'prune': return worktrees.prune({ repo: cwd, ...(typeof params.keep === 'number' ? { keep: params.keep } : {}) });
|
|
492
|
+
case 'trustSetup': {
|
|
493
|
+
const repo = await worktrees.repositoryOf(cwd);
|
|
494
|
+
const script = setupScript(repo);
|
|
495
|
+
if (!script)
|
|
496
|
+
throw invalid('This repository has no .xharness/worktree-setup.');
|
|
497
|
+
await worktrees.trustSetup(repo, script.hash);
|
|
498
|
+
return { hash: script.hash };
|
|
499
|
+
}
|
|
500
|
+
case 'switch': {
|
|
501
|
+
// A new conversation in the chosen folder: Pi keeps one history per working folder.
|
|
502
|
+
const target = params.main === true ? null : id();
|
|
503
|
+
const folder = target ? worktrees.pathOf(target) : await worktrees.repositoryOf(cwd);
|
|
504
|
+
if (!folder)
|
|
505
|
+
throw new HarnessError('worktree_missing', 'That worktree has no folder.');
|
|
506
|
+
this.relaunches.set(sessionId, { accountId: live.account.id, nativeSessionId: null, cwd: folder, worktreeId: target });
|
|
507
|
+
return { cwd: folder, worktreeId: target };
|
|
508
|
+
}
|
|
509
|
+
default:
|
|
510
|
+
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
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. */
|
|
552
|
+
async annotationsCall(live, call, params) {
|
|
553
|
+
const root = this.options.env.root;
|
|
554
|
+
const sessionId = live.input.sessionId;
|
|
555
|
+
const capture = this.options.noteCapture;
|
|
556
|
+
const id = () => {
|
|
557
|
+
if (typeof params.id !== 'string' || !params.id)
|
|
558
|
+
throw invalid('A note id is required.');
|
|
559
|
+
return params.id;
|
|
560
|
+
};
|
|
561
|
+
const version = () => {
|
|
562
|
+
if (typeof params.expectedVersion !== 'number')
|
|
563
|
+
throw invalid('The note version you last read is required.');
|
|
564
|
+
return params.expectedVersion;
|
|
565
|
+
};
|
|
566
|
+
const needCapture = () => {
|
|
567
|
+
if (!capture)
|
|
568
|
+
throw invalid('This host has no capture browser.');
|
|
569
|
+
return capture;
|
|
570
|
+
};
|
|
571
|
+
const needNotes = () => {
|
|
572
|
+
if (!this.options.pageNotes)
|
|
573
|
+
throw invalid('This host cannot send page notes.');
|
|
574
|
+
return this.options.pageNotes;
|
|
575
|
+
};
|
|
576
|
+
const action = call.slice('terminal.annotations.'.length);
|
|
577
|
+
switch (action) {
|
|
578
|
+
case 'list': {
|
|
579
|
+
const result = pageNotes.list(root, {
|
|
580
|
+
query: typeof params.query === 'string' ? params.query : null,
|
|
581
|
+
url: typeof params.url === 'string' ? params.url : null,
|
|
582
|
+
status: params.status === 'open' || params.status === 'resolved' ? params.status : null,
|
|
583
|
+
limit: 200
|
|
584
|
+
});
|
|
585
|
+
return { ...result, sessionId };
|
|
586
|
+
}
|
|
587
|
+
case 'read': return pageNotes.read(root, id());
|
|
588
|
+
case 'create': {
|
|
589
|
+
const note = pageNotes.create(root, { source: params.source, target: params.target, body: params.body, tags: params.tags });
|
|
590
|
+
return params.share === true ? pageNotes.share(root, note.id, { sessionId }) : note;
|
|
591
|
+
}
|
|
592
|
+
case 'update': return pageNotes.update(root, id(), version(), (params.patch ?? {}));
|
|
593
|
+
case 'delete':
|
|
594
|
+
pageNotes.remove(root, id(), version());
|
|
595
|
+
return {};
|
|
596
|
+
// A terminal shares with, and withdraws from, its own session only.
|
|
597
|
+
case 'share': return pageNotes.share(root, id(), { sessionId });
|
|
598
|
+
case 'unshare': return pageNotes.unshare(root, id(), { sessionId });
|
|
599
|
+
case 'open': return needCapture().open(sessionId, typeof params.url === 'string' && params.url ? params.url : null);
|
|
600
|
+
case 'identity': return needCapture().identity(sessionId);
|
|
601
|
+
case 'begin':
|
|
602
|
+
if (params.kind !== 'text' && params.kind !== 'element')
|
|
603
|
+
throw invalid('kind is text or element.');
|
|
604
|
+
return needCapture().begin(sessionId, params.kind);
|
|
605
|
+
case 'capture':
|
|
606
|
+
if (typeof params.captureId !== 'string')
|
|
607
|
+
throw invalid('A capture id is required.');
|
|
608
|
+
return needCapture().capture(sessionId, params.captureId);
|
|
609
|
+
case 'cancel':
|
|
610
|
+
await needCapture().cancel(sessionId, typeof params.captureId === 'string' ? params.captureId : undefined);
|
|
611
|
+
return {};
|
|
612
|
+
case 'reveal': {
|
|
613
|
+
const note = pageNotes.read(root, id());
|
|
614
|
+
return needCapture().reveal(sessionId, note.source.url, note.target);
|
|
615
|
+
}
|
|
616
|
+
// ---- The inspector and the queue (PROP-0018); a terminal queues and sends to its own session.
|
|
617
|
+
case 'target': return { ...needCapture().describe(sessionId), windows: await needCapture().windows(sessionId).catch(() => []) };
|
|
618
|
+
case 'attach': {
|
|
619
|
+
const kind = params.kind;
|
|
620
|
+
if (kind !== 'web' && kind !== 'cdp' && kind !== 'bridge')
|
|
621
|
+
throw invalid('kind is web, cdp or bridge.');
|
|
622
|
+
const spec = kind === 'cdp'
|
|
623
|
+
? { kind, endpoint: typeof params.endpoint === 'string' ? params.endpoint : '', name: typeof params.name === 'string' ? params.name : null }
|
|
624
|
+
: kind === 'bridge' ? { kind, name: typeof params.name === 'string' ? params.name : null } : { kind };
|
|
625
|
+
return needCapture().attach(sessionId, spec);
|
|
626
|
+
}
|
|
627
|
+
case 'windows': return needCapture().windows(sessionId);
|
|
628
|
+
case 'selectWindow':
|
|
629
|
+
if (typeof params.window !== 'string')
|
|
630
|
+
throw invalid('A window id is required.');
|
|
631
|
+
await needCapture().selectWindow(sessionId, params.window);
|
|
632
|
+
return {};
|
|
633
|
+
case 'inspect': return needCapture().inspect(sessionId);
|
|
634
|
+
case 'stopInspect':
|
|
635
|
+
await needCapture().stopInspect(sessionId);
|
|
636
|
+
return {};
|
|
637
|
+
case 'next': return inspectStep(needCapture(), needNotes(), sessionId, sessionId, 'terminal');
|
|
638
|
+
case 'queue': return needNotes().queue(id(), version(), sessionId);
|
|
639
|
+
case 'dequeue': return needNotes().dequeue(id(), version());
|
|
640
|
+
case 'confirm': return needNotes().confirm(id(), version());
|
|
641
|
+
case 'send': return needNotes().send(sessionId, { ids: Array.isArray(params.ids) ? params.ids.filter((entry) => typeof entry === 'string') : null });
|
|
642
|
+
default:
|
|
643
|
+
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
327
646
|
/** The account a session asked to switch to. Taken once, after its Pi process has ended and before `dispose`. */
|
|
328
647
|
takeRelaunch(sessionId) {
|
|
329
648
|
const request = this.relaunches.get(sessionId) ?? null;
|
|
@@ -356,6 +675,7 @@ export class Terminals {
|
|
|
356
675
|
sessionId: input.sessionId,
|
|
357
676
|
nativeSessionId: live.controller.nativeSessionId,
|
|
358
677
|
workingDirectory: input.cwd,
|
|
678
|
+
workspaceRoot: resolveWorkspaceRoot(input.cwd),
|
|
359
679
|
worktreeId: input.run?.worktreeId ?? null,
|
|
360
680
|
goal: text,
|
|
361
681
|
attachmentCount: 0,
|
|
@@ -364,7 +684,9 @@ export class Terminals {
|
|
|
364
684
|
status: 'queued',
|
|
365
685
|
exitCode: null,
|
|
366
686
|
handoffId: null,
|
|
367
|
-
accountId: live.account.id
|
|
687
|
+
accountId: live.account.id,
|
|
688
|
+
variantScope: live.variant?.scope ?? null,
|
|
689
|
+
variantVersion: live.variant?.fingerprint ?? null
|
|
368
690
|
};
|
|
369
691
|
}
|
|
370
692
|
/** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
|
|
@@ -390,55 +712,151 @@ export class Terminals {
|
|
|
390
712
|
const record = loadRun(this.options.env.root, run.runId);
|
|
391
713
|
if (!isActiveStatus(record.status))
|
|
392
714
|
throw invalid(`The run is no longer active (its status is ${record.status}).`);
|
|
393
|
-
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 };
|
|
394
727
|
const { policy } = this.options;
|
|
395
728
|
if (policy.scope) {
|
|
396
|
-
const scope = await policy.scope(call);
|
|
729
|
+
const scope = combineWorkspaceScope(await policy.scope(call), inspection);
|
|
397
730
|
if (!scope.allowed && (scope.automatic || !scope.requiresApproval))
|
|
398
|
-
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 };
|
|
399
732
|
try {
|
|
400
733
|
await policy.authorize?.(call);
|
|
401
734
|
}
|
|
402
735
|
catch (error) {
|
|
403
|
-
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 };
|
|
404
737
|
}
|
|
405
738
|
const requiresApproval = !!scope.requiresApproval || (scope.allowed && !!scope.humanOnly);
|
|
406
739
|
return {
|
|
407
740
|
allowed: scope.allowed,
|
|
408
741
|
requiresApproval,
|
|
409
742
|
reason: scope.reason ?? null,
|
|
743
|
+
workspaceRoot,
|
|
410
744
|
rememberable: scope.allowed && !scope.humanOnly,
|
|
411
745
|
...(scope.humanOnly ? { standing: scope.allowGlobalApproval ? 'switch' : 'none' } : {})
|
|
412
746
|
};
|
|
413
747
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (name === SESSION_NAME || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name))
|
|
417
|
-
return { allowed: true };
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
}
|
|
421
820
|
}
|
|
422
821
|
async subagentsCall(live, call, params) {
|
|
822
|
+
if (call === 'terminal.subagents.monitor')
|
|
823
|
+
return { children: live.controller.subagentSnapshot() };
|
|
423
824
|
const service = this.options.subagents;
|
|
424
|
-
if (!service || live.input.subagents === null) {
|
|
825
|
+
if ((!service || live.input.subagents === null) && call !== 'terminal.subagents.register') {
|
|
425
826
|
if (call === 'terminal.subagents.tools')
|
|
426
827
|
return { tools: [] };
|
|
427
828
|
throw invalid('Subagents are disabled for this terminal.');
|
|
428
829
|
}
|
|
429
|
-
if (call === 'terminal.subagents.tools')
|
|
430
|
-
|
|
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
|
+
}
|
|
431
840
|
const run = live.controller.activeRun;
|
|
432
841
|
if (!run)
|
|
433
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
|
+
}
|
|
434
847
|
if (call === 'terminal.subagents.register') {
|
|
435
848
|
if (!Array.isArray(params.tools) || params.tools.some((name) => typeof name !== 'string'))
|
|
436
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.');
|
|
437
852
|
const readOnly = live.input.readOnly === true;
|
|
438
|
-
const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, 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']);
|
|
439
|
-
const tools = params.tools.filter(
|
|
440
|
-
const
|
|
441
|
-
|
|
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']);
|
|
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 };
|
|
442
860
|
service.register({
|
|
443
861
|
run: { id: run.runId, sessionId: live.input.sessionId, mode: live.input.run?.mode ?? 'code', projectId: live.input.projectId, worktreeId: live.input.run?.worktreeId },
|
|
444
862
|
cwd: live.input.cwd, sessionDir: this.historyDir(live.input), text: '', caller: live.caller,
|
|
@@ -446,10 +864,12 @@ export class Terminals {
|
|
|
446
864
|
effort: typeof params.effort === 'string' ? params.effort : null,
|
|
447
865
|
launch: live.input.launch, tools: { allow: capabilities.tools, gated: capabilities.tools.filter(name => !readTools.has(name)) },
|
|
448
866
|
connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
|
|
867
|
+
plan: params.plan === true,
|
|
868
|
+
variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
|
|
449
869
|
capabilities
|
|
450
870
|
}, live.account.id, capabilities);
|
|
451
871
|
service.restore(run.runId, params.history ?? { owners: [], receipts: [] });
|
|
452
|
-
return { runId: run.runId };
|
|
872
|
+
return { runId: run.runId, connectors: capabilities.connectors };
|
|
453
873
|
}
|
|
454
874
|
if (call === 'terminal.subagents.verify') {
|
|
455
875
|
await service.verify(run.runId, params.history);
|
|
@@ -519,6 +939,43 @@ export class Terminals {
|
|
|
519
939
|
}
|
|
520
940
|
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
521
941
|
}
|
|
942
|
+
async sideQuestionCall(live, call, params) {
|
|
943
|
+
const service = this.options.sideQuestions;
|
|
944
|
+
if (!service)
|
|
945
|
+
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
946
|
+
if (call === 'terminal.sideQuestions.start') {
|
|
947
|
+
if (typeof params.question !== 'string')
|
|
948
|
+
throw invalid('A side question is required.');
|
|
949
|
+
const context = params.context && typeof params.context === 'object' && !Array.isArray(params.context)
|
|
950
|
+
? params.context
|
|
951
|
+
: undefined;
|
|
952
|
+
const latest = listTerminalTrajectoryRuns(this.options.env.root, live.input.sessionId, live.controller.nativeSessionId)
|
|
953
|
+
.find(run => findRun(this.options.env.root, run.id)?.trigger !== 'side-question');
|
|
954
|
+
return service.start({
|
|
955
|
+
sessionId: live.input.sessionId,
|
|
956
|
+
parentRunId: live.controller.activeRun?.runId ?? latest?.id ?? null,
|
|
957
|
+
parentSessionId: live.input.sessionId,
|
|
958
|
+
accountId: live.account.id,
|
|
959
|
+
model: typeof params.model === 'string' ? params.model : live.input.model ?? null,
|
|
960
|
+
effort: typeof params.effort === 'string' ? params.effort : undefined,
|
|
961
|
+
cwd: live.input.cwd,
|
|
962
|
+
caller: live.caller,
|
|
963
|
+
question: params.question,
|
|
964
|
+
context
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (typeof params.runId !== 'string')
|
|
968
|
+
throw invalid('A side-question run id is required.');
|
|
969
|
+
if (call === 'terminal.sideQuestions.read') {
|
|
970
|
+
const afterSequence = params.afterSequence === undefined ? -1 : params.afterSequence;
|
|
971
|
+
if (typeof afterSequence !== 'number')
|
|
972
|
+
throw invalid('A side-question event sequence is required.');
|
|
973
|
+
return service.read(params.runId, live.input.sessionId, afterSequence);
|
|
974
|
+
}
|
|
975
|
+
if (call === 'terminal.sideQuestions.interrupt')
|
|
976
|
+
return service.interrupt(params.runId, live.input.sessionId);
|
|
977
|
+
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
978
|
+
}
|
|
522
979
|
/** The only terminal frontend: the Bun-run OpenTUI application. */
|
|
523
980
|
frontendCommand() {
|
|
524
981
|
return {
|
|
@@ -538,32 +995,55 @@ export class Terminals {
|
|
|
538
995
|
if (!input?.sessionId || !input.cwd || !input.sessionFolder)
|
|
539
996
|
throw invalid('A terminal session needs an id, a working folder, and its own folder.');
|
|
540
997
|
await this.dispose(input.sessionId);
|
|
541
|
-
const command = this.frontendCommand();
|
|
542
|
-
// A pinned account is used as it is; it is never exchanged for another one.
|
|
543
|
-
const account = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null);
|
|
544
|
-
const launch = launchEnv(accounts.dir, account);
|
|
545
|
-
const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, account), install: true, caller: { projectId: input.projectId ?? null } })) ?? null;
|
|
546
|
-
await this.socket.listen();
|
|
547
|
-
await accounts.terminalBegin(account);
|
|
548
998
|
const sessionId = input.sessionId;
|
|
549
|
-
|
|
999
|
+
let acquiredWorktreeId = null;
|
|
1000
|
+
let account = null;
|
|
1001
|
+
let accountStarted = false;
|
|
550
1002
|
let token = null;
|
|
551
1003
|
try {
|
|
1004
|
+
const worktreeId = input.run?.worktreeId ?? null;
|
|
1005
|
+
const worktreePath = worktreeId ? this.options.worktrees?.pathOf(worktreeId) ?? null : null;
|
|
1006
|
+
if (worktreeId && worktreePath) {
|
|
1007
|
+
await this.options.worktrees.acquire(worktreeId, sessionId);
|
|
1008
|
+
acquiredWorktreeId = worktreeId;
|
|
1009
|
+
this.worktreeLeases.set(sessionId, worktreeId);
|
|
1010
|
+
input.cwd = worktreePath;
|
|
1011
|
+
}
|
|
1012
|
+
const command = this.frontendCommand();
|
|
1013
|
+
// A pinned account is used as it is; it is never exchanged for another one.
|
|
1014
|
+
const preparedAccount = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null);
|
|
1015
|
+
account = preparedAccount;
|
|
1016
|
+
const launch = launchEnv(accounts.dir, preparedAccount);
|
|
1017
|
+
const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: true, caller: { projectId: input.projectId ?? null } })) ?? null;
|
|
1018
|
+
await this.socket.listen();
|
|
1019
|
+
await accounts.terminalBegin(preparedAccount);
|
|
1020
|
+
accountStarted = true;
|
|
1021
|
+
const caller = { sessionId, projectId: input.projectId ?? null, actor: 'Code', surface: 'cli' };
|
|
552
1022
|
const broker = input.connections === false ? null : apps.grant({ sessionId, runId: null, caller, readOnly: !!input.readOnly, depth: 0 });
|
|
553
1023
|
// The session keeps these instructions; each of its runs records a copy, and a memory
|
|
554
1024
|
// block that changes later is sent along as a note.
|
|
555
1025
|
const variant = input.variant ? materialise(root, input.variant, { trust: this.options.trust ?? null }) : null;
|
|
1026
|
+
const browser = new BrowserControl({ root, cwd: input.cwd, pinned: variant,
|
|
1027
|
+
writable: Boolean(input.variant && !('dir' in input.variant)), apps, hub: this.options.hub,
|
|
1028
|
+
plugins: () => this.options.pluginControl?.list({ cwd: input.cwd, accountId: preparedAccount.id, projectId: input.projectId ?? null }) ?? Promise.resolve([]) });
|
|
556
1029
|
if (variant)
|
|
557
1030
|
this.variantDirs.set(sessionId, variant.dir);
|
|
558
|
-
const worldTools = await this.worldToolNames(input,
|
|
559
|
-
const
|
|
560
|
-
const
|
|
1031
|
+
const worldTools = await this.worldToolNames(input, preparedAccount);
|
|
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])];
|
|
1034
|
+
// The profiles replace the variant's own subagent list, which they include.
|
|
1035
|
+
const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
|
|
1036
|
+
const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
|
|
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);
|
|
561
1040
|
record(root, pack);
|
|
562
1041
|
mkdirSync(this.historyDir(input), { recursive: true });
|
|
563
1042
|
saveForSession(input.sessionFolder, pack);
|
|
564
1043
|
const live = {
|
|
565
1044
|
input,
|
|
566
|
-
|
|
1045
|
+
variant,
|
|
1046
|
+
account: preparedAccount,
|
|
567
1047
|
broker,
|
|
568
1048
|
caller,
|
|
569
1049
|
token: '',
|
|
@@ -572,6 +1052,12 @@ export class Terminals {
|
|
|
572
1052
|
nativeSessionId: input.nativeSessionId ?? null,
|
|
573
1053
|
begin: ({ reservedRunId, text }) => this.begin(live, reservedRunId, text),
|
|
574
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),
|
|
575
1061
|
allowToolGlobally: target => { this.options.approvals.allowTool('pi', target); },
|
|
576
1062
|
native: (nativeSessionId) => bus.emit({ type: 'terminal.native', sessionId, nativeSessionId }),
|
|
577
1063
|
telemetry: (event) => bus.emit({ type: 'terminal.telemetry', sessionId, event }),
|
|
@@ -586,6 +1072,46 @@ export class Terminals {
|
|
|
586
1072
|
caller: () => ({ ...caller, runId: live.controller.activeRun?.runId ?? null }),
|
|
587
1073
|
broker,
|
|
588
1074
|
terminal: { accept: (event, data) => live.controller.accept(event, data), disconnected: () => live.controller.disconnected(), control: async (call, params) => {
|
|
1075
|
+
if (call === 'terminal.browser.config') {
|
|
1076
|
+
const world = this.options.world;
|
|
1077
|
+
if (!world)
|
|
1078
|
+
throw invalid('Browser settings are unavailable in this host.');
|
|
1079
|
+
return { allowPrivate: world.browserConfig().allowPrivate };
|
|
1080
|
+
}
|
|
1081
|
+
if (call === 'terminal.browser.configure') {
|
|
1082
|
+
const world = this.options.world;
|
|
1083
|
+
if (!world)
|
|
1084
|
+
throw invalid('Browser settings are unavailable in this host.');
|
|
1085
|
+
if (typeof params.allowPrivate !== 'boolean')
|
|
1086
|
+
throw invalid('allowPrivate must be a boolean.');
|
|
1087
|
+
return { allowPrivate: world.configureBrowser({ allowPrivate: params.allowPrivate }).allowPrivate };
|
|
1088
|
+
}
|
|
1089
|
+
if (call === 'terminal.browser.snapshot')
|
|
1090
|
+
return browser.snapshot(params.refresh === true);
|
|
1091
|
+
if (call === 'terminal.browser.read') {
|
|
1092
|
+
if (typeof params.id !== 'string')
|
|
1093
|
+
throw invalid('A resource id is required.');
|
|
1094
|
+
return browser.read(params.id, params.latest === true);
|
|
1095
|
+
}
|
|
1096
|
+
if (call === 'terminal.browser.importSkill') {
|
|
1097
|
+
if (typeof params.path !== 'string' || !params.path.trim())
|
|
1098
|
+
throw invalid('A skill folder is required.');
|
|
1099
|
+
browser.importSkill(params.path);
|
|
1100
|
+
bus.emit({ type: 'plugins.changed' });
|
|
1101
|
+
return {};
|
|
1102
|
+
}
|
|
1103
|
+
if (call.startsWith('terminal.browser.connections.'))
|
|
1104
|
+
return browser.connection(call.slice('terminal.browser.connections.'.length), params);
|
|
1105
|
+
if (call === 'terminal.browser.hubPreview') {
|
|
1106
|
+
if (!this.options.hub || typeof params.id !== 'string' || typeof params.version !== 'string')
|
|
1107
|
+
throw invalid('A catalog entry and version are required.');
|
|
1108
|
+
return this.options.hub.preview(params.id, params.version, typeof params.offset === 'number' ? params.offset : 0);
|
|
1109
|
+
}
|
|
1110
|
+
if (call === 'terminal.browser.hubDocument') {
|
|
1111
|
+
if (!this.options.hub || typeof params.id !== 'string' || typeof params.version !== 'string')
|
|
1112
|
+
throw invalid('An installed document and version are required.');
|
|
1113
|
+
return this.options.hub.readDocument(params.id, params.version);
|
|
1114
|
+
}
|
|
589
1115
|
if (call.startsWith('terminal.doctor.')) {
|
|
590
1116
|
const doctor = this.options.doctor;
|
|
591
1117
|
if (!doctor)
|
|
@@ -616,8 +1142,16 @@ export class Terminals {
|
|
|
616
1142
|
return doctor.cancel(id);
|
|
617
1143
|
throw invalid('Unknown diagnostic operation.');
|
|
618
1144
|
}
|
|
1145
|
+
if (call.startsWith('terminal.agents.'))
|
|
1146
|
+
return this.agentsCall(live, call, params);
|
|
619
1147
|
if (call.startsWith('terminal.subagents.'))
|
|
620
1148
|
return this.subagentsCall(live, call, params);
|
|
1149
|
+
if (call.startsWith('terminal.sideQuestions.'))
|
|
1150
|
+
return this.sideQuestionCall(live, call, params);
|
|
1151
|
+
if (call.startsWith('terminal.worktrees.'))
|
|
1152
|
+
return this.worktreesCall(live, call, params);
|
|
1153
|
+
if (call.startsWith('terminal.annotations.'))
|
|
1154
|
+
return this.annotationsCall(live, call, params);
|
|
621
1155
|
if (call === 'terminal.logs.info')
|
|
622
1156
|
return harnessLogInfo(this.options.stateDir);
|
|
623
1157
|
if (call === 'terminal.logs.read') {
|
|
@@ -651,11 +1185,15 @@ export class Terminals {
|
|
|
651
1185
|
: this.options.approvals.revokeTool('pi', params.tool);
|
|
652
1186
|
return { globalTools: policy.globalTools.pi ?? [] };
|
|
653
1187
|
}
|
|
1188
|
+
if (call.startsWith('terminal.usage.'))
|
|
1189
|
+
return this.usageCall(live, call, params);
|
|
654
1190
|
if (call !== 'terminal.resources')
|
|
655
1191
|
return live.controller.control(call, params);
|
|
656
|
-
const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir,
|
|
657
|
-
|
|
1192
|
+
const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
|
|
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)))) {
|
|
658
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
|
+
}
|
|
659
1197
|
return {};
|
|
660
1198
|
} }
|
|
661
1199
|
});
|
|
@@ -664,16 +1202,17 @@ export class Terminals {
|
|
|
664
1202
|
const args = [...command.argsPrefix, '--append-system-prompt', render(env, pack)];
|
|
665
1203
|
if (input.model)
|
|
666
1204
|
args.push('--model', input.model);
|
|
667
|
-
|
|
1205
|
+
{
|
|
668
1206
|
// Pi's own discovery is off: the terminal loads what the harness found and nothing else.
|
|
669
1207
|
args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes');
|
|
670
|
-
|
|
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 ?? [])])
|
|
671
1210
|
args.push('--extension', path);
|
|
672
|
-
for (const path of [...(variant?.skillPaths ?? []), ...loading
|
|
1211
|
+
for (const path of [...(variant?.skillPaths ?? []), ...(loading?.skillPaths ?? [])])
|
|
673
1212
|
args.push('--skill', path);
|
|
674
|
-
for (const path of loading
|
|
1213
|
+
for (const path of loading?.promptPaths ?? [])
|
|
675
1214
|
args.push('--prompt-template', path);
|
|
676
|
-
for (const path of loading
|
|
1215
|
+
for (const path of loading?.themePaths ?? [])
|
|
677
1216
|
args.push('--theme', path);
|
|
678
1217
|
}
|
|
679
1218
|
// Last, so its gate sees a tool call as every other extension left it.
|
|
@@ -691,7 +1230,7 @@ export class Terminals {
|
|
|
691
1230
|
[SOCKET_ENV]: this.socket.path,
|
|
692
1231
|
[TOKEN_ENV]: token
|
|
693
1232
|
},
|
|
694
|
-
accountId:
|
|
1233
|
+
accountId: preparedAccount.id,
|
|
695
1234
|
nativeSessionId: input.nativeSessionId ?? null
|
|
696
1235
|
};
|
|
697
1236
|
}
|
|
@@ -699,8 +1238,15 @@ export class Terminals {
|
|
|
699
1238
|
if (token)
|
|
700
1239
|
this.socket.revoke(token);
|
|
701
1240
|
this.sessions.delete(sessionId);
|
|
1241
|
+
this.variantDirs.delete(sessionId);
|
|
1242
|
+
this.worldNames.delete(sessionId);
|
|
1243
|
+
if (this.worktreeLeases.get(sessionId) === acquiredWorktreeId)
|
|
1244
|
+
this.worktreeLeases.delete(sessionId);
|
|
702
1245
|
apps.revokeSession(sessionId);
|
|
703
|
-
|
|
1246
|
+
if (accountStarted && account)
|
|
1247
|
+
await accounts.terminalEnd(account).catch(() => undefined);
|
|
1248
|
+
if (acquiredWorktreeId)
|
|
1249
|
+
await this.options.worktrees?.release(acquiredWorktreeId, sessionId).catch(() => undefined);
|
|
704
1250
|
throw error;
|
|
705
1251
|
}
|
|
706
1252
|
}
|
|
@@ -754,6 +1300,11 @@ export class Terminals {
|
|
|
754
1300
|
}
|
|
755
1301
|
this.sessions.delete(sessionId);
|
|
756
1302
|
this.worldNames.delete(sessionId);
|
|
1303
|
+
const worktreeId = this.worktreeLeases.get(sessionId);
|
|
1304
|
+
this.worktreeLeases.delete(sessionId);
|
|
1305
|
+
if (worktreeId)
|
|
1306
|
+
await this.options.worktrees?.release(worktreeId, sessionId).catch(() => undefined);
|
|
1307
|
+
void this.options.noteCapture?.release(sessionId).catch(() => undefined);
|
|
757
1308
|
void this.options.world?.release(sessionId).catch(() => undefined);
|
|
758
1309
|
live.controller.dispose();
|
|
759
1310
|
this.socket.revoke(live.token);
|