@xingular/harness 1.1.1 → 1.3.1-unstable.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 +48 -0
- package/README.md +36 -28
- package/dist/cli.js +10837 -6159
- 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 +18 -4
- package/dist/sdk/agents/planResearch.d.ts +27 -0
- package/dist/sdk/agents/planResearch.js +82 -0
- package/dist/sdk/agents/profiles.d.ts +5 -1
- package/dist/sdk/agents/profiles.js +21 -5
- 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/capabilities/runSnapshot.js +10 -4
- 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 +258 -0
- package/dist/sdk/commands/registry.js +48 -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/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -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 +20 -0
- package/dist/sdk/core/workspaceBashTool.js +50 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
- package/dist/sdk/core/workspaceSandbox.js +360 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
- package/dist/sdk/core/workspaceWriteOperations.js +224 -0
- package/dist/sdk/doctor/checks.js +71 -12
- 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/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +8 -3
- package/dist/sdk/index.js +4 -1
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +32 -15
- 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/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +6 -5
- 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 -31
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +22 -2
- 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 +7 -2
- package/dist/sdk/sessions/continuation.js +11 -6
- package/dist/sdk/sessions/contracts.d.ts +190 -4
- package/dist/sdk/sessions/contracts.js +71 -8
- 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 +74 -17
- 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 +38 -1
- 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 +225 -22
- package/dist/sdk/sessions/subagentService.d.ts +24 -0
- package/dist/sdk/sessions/subagentService.js +225 -26
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +106 -0
- package/dist/sdk/sessions/workSlice.js +329 -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 +31 -3
- package/dist/sdk/terminal/controller.js +81 -29
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +88 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +177 -63
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
- package/dist/sdk/terminal/subagentMonitor.js +61 -9
- package/dist/sdk/terminal/terminals.d.ts +26 -2
- package/dist/sdk/terminal/terminals.js +325 -43
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +17 -4
- package/dist/sdk/terminal/tui/commands.d.ts +6 -0
- package/dist/sdk/terminal/tui/commands.js +213 -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 +21 -5
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- 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 +8 -6
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
- 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/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/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +11 -8
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- 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 +9204 -4812
- package/dist/terminal-pi.js +1848 -272
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +169 -36
- package/docs/architecture/04-prompt.md +55 -15
- package/docs/architecture/06-plugins-and-capabilities.md +36 -15
- package/docs/architecture/07-tools-and-gate.md +131 -25
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -3
- package/docs/architecture/10-server-protocol.md +71 -9
- package/docs/architecture/11-terminal.md +148 -60
- 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 +58 -8
- package/docs/architecture/README.md +20 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- 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 +258 -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/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/README.md +24 -6
- 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/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/doctor.md +14 -1
- 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 +83 -27
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/1.3.0.md +21 -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 +162 -82
- package/package.json +4 -3
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -54
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentSession, type ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
export interface TerminalPlanRunState {
|
|
3
3
|
active: boolean;
|
|
4
4
|
allowedTools: ReadonlySet<string>;
|
|
5
|
+
connectors?: string[];
|
|
6
|
+
subagentProfiles?: {
|
|
7
|
+
normal: string;
|
|
8
|
+
plan: string;
|
|
9
|
+
};
|
|
5
10
|
}
|
|
6
11
|
export default function (pi: ExtensionAPI, planRun?: TerminalPlanRunState, getSession?: () => AgentSession): Promise<void>;
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
// Loaded by the `pi` command in a terminal session (`--extension`). It reports what the native
|
|
2
|
+
// session does to the harness, asks before each tool call, takes instructions the harness
|
|
3
|
+
// queues for it, and gives the session the harness's own tools. It runs in the `pi` process,
|
|
4
|
+
// so everything it knows about the harness comes through the session socket.
|
|
5
|
+
import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from '@earendil-works/pi-coding-agent';
|
|
1
6
|
import { Type } from 'typebox';
|
|
2
|
-
import { QUESTION_SKETCH_MAX_LENGTH, QUESTION_SKETCH_PATTERN, questionSketchError } from "../sessions/contracts.js";
|
|
3
7
|
import { connectionFromEnv, socketCall, terminalCall } from "./client.js";
|
|
4
8
|
import { controlCall } from "./control.js";
|
|
5
|
-
import { SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
|
|
9
|
+
import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
|
|
10
|
+
import { PLAN_RESEARCH_TOOLS } from "../agents/planResearch.js";
|
|
6
11
|
import { askUserTool } from "../tools/askUser.js";
|
|
7
|
-
import {
|
|
12
|
+
import { todoTool } from "../tools/todo.js";
|
|
13
|
+
import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
|
|
14
|
+
import { createSliceState, parseWorkSlice, workSliceGuard } from "../sessions/workSlice.js";
|
|
15
|
+
import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
|
|
8
16
|
import { sessionNameTool } from "../tools/sessionName.js";
|
|
17
|
+
import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
|
|
9
18
|
import { PLAN_INSTRUCTIONS, planRefusal } from "../sessions/planMode.js";
|
|
19
|
+
import { compactionCheckpoint } from "../sessions/compactionCheckpoint.js";
|
|
10
20
|
import { pruneToolImages } from "../world/images.js";
|
|
21
|
+
import { parseInitialBashCommand } from "../sessions/initialBash.js";
|
|
22
|
+
import { inspectWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
23
|
+
import { createWorkspaceShellTool } from "../core/workspaceBashTool.js";
|
|
24
|
+
import { createWorkspaceBashOperations, runWithWorkspaceWriteScope } from "../core/workspaceSandbox.js";
|
|
25
|
+
import { withWorkspaceWriteAuthorization, workspaceEditOperations, workspaceWriteOperations } from "../core/workspaceWriteOperations.js";
|
|
26
|
+
import { BUDGET_GUARD, OUTPUT_TRIM, budgetGuard, outputTrim } from "../plugins/firstParty.js";
|
|
11
27
|
/** How much text a tool handed back to the model. */
|
|
12
28
|
function resultSize(result) {
|
|
13
29
|
const content = result?.content;
|
|
@@ -32,6 +48,7 @@ export default async function (pi, planRun, getSession) {
|
|
|
32
48
|
let context = null;
|
|
33
49
|
let nativeSubagents = false;
|
|
34
50
|
let subagentParent = false;
|
|
51
|
+
let stoppedChildren = null;
|
|
35
52
|
let timer = null;
|
|
36
53
|
let polling = false;
|
|
37
54
|
let failure;
|
|
@@ -42,6 +59,8 @@ export default async function (pi, planRun, getSession) {
|
|
|
42
59
|
let requestStartedAt = null;
|
|
43
60
|
const toolStartedAt = new Map();
|
|
44
61
|
const fileTools = new Map();
|
|
62
|
+
const writeAuthorizations = new Map();
|
|
63
|
+
let workspaceRoot = null;
|
|
45
64
|
// The harness's own tools for this session (connected apps and what the application offers).
|
|
46
65
|
let stage = 'Tool discovery';
|
|
47
66
|
try {
|
|
@@ -62,8 +81,19 @@ export default async function (pi, planRun, getSession) {
|
|
|
62
81
|
}
|
|
63
82
|
});
|
|
64
83
|
}
|
|
84
|
+
// The harness's own extensions a person switched on, as a structured session loads them inline.
|
|
85
|
+
const own = await socketCall(connection, 'firstParty.list', {}, 15_000)
|
|
86
|
+
.then((reply) => reply?.plugins ?? [], () => []);
|
|
87
|
+
for (const plugin of own) {
|
|
88
|
+
if (plugin.id === OUTPUT_TRIM)
|
|
89
|
+
outputTrim(plugin.settings)(pi);
|
|
90
|
+
if (plugin.id === BUDGET_GUARD)
|
|
91
|
+
budgetGuard(plugin.settings)(pi);
|
|
92
|
+
}
|
|
65
93
|
const native = await controlCall('terminal.subagents.tools');
|
|
66
94
|
nativeSubagents = native.tools.length > 0;
|
|
95
|
+
if (planRun && native.profiles)
|
|
96
|
+
planRun.subagentProfiles = native.profiles;
|
|
67
97
|
for (const tool of native.tools)
|
|
68
98
|
pi.registerTool({
|
|
69
99
|
name: tool.name, label: tool.label, description: tool.description, parameters: Type.Unsafe(tool.parameters),
|
|
@@ -77,6 +107,46 @@ export default async function (pi, planRun, getSession) {
|
|
|
77
107
|
await terminalCall('startupFailed', { reason: `${stage} failed: ${error instanceof Error ? error.message : 'Unavailable'}` }).catch(() => { });
|
|
78
108
|
throw error;
|
|
79
109
|
}
|
|
110
|
+
pi.registerTool({
|
|
111
|
+
...CAPABILITY_DISCOVERY,
|
|
112
|
+
parameters: Type.Unsafe(CAPABILITY_DISCOVERY.parameters),
|
|
113
|
+
async execute(_id, input) {
|
|
114
|
+
const active = pi.getActiveTools();
|
|
115
|
+
const tools = pi.getAllTools();
|
|
116
|
+
const selected = planRun?.active ? [...planRun.allowedTools] : active;
|
|
117
|
+
const name = input && typeof input === 'object' && typeof input.tool === 'string'
|
|
118
|
+
? input.tool
|
|
119
|
+
: undefined;
|
|
120
|
+
const text = JSON.stringify(describeCapabilities({
|
|
121
|
+
capabilities: { version: 1, tools: selected, connectors: planRun?.connectors ?? [], contributions: [] },
|
|
122
|
+
active,
|
|
123
|
+
registered: tools
|
|
124
|
+
}, name));
|
|
125
|
+
return { content: [{ type: 'text', text }], details: undefined };
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const workspaceBash = createWorkspaceBashOperations(() => workspaceRoot, {
|
|
129
|
+
bypassWorkspaceBoundary: async (request) => {
|
|
130
|
+
const result = await controlCall('terminal.workspaceBoundaryBypass', { tool: 'bash', args: { command: request.command, cwd: request.cwd, workspaceRoot: request.workspaceRoot } });
|
|
131
|
+
return result.bypass;
|
|
132
|
+
},
|
|
133
|
+
requestWorkspaceWriteScope: async (request) => {
|
|
134
|
+
const result = await controlCall('terminal.workspaceEscape', { tool: 'bash', args: { command: request.command, workspaceWriteScope: 'outside' }, reason: request.reason });
|
|
135
|
+
return result.approved;
|
|
136
|
+
},
|
|
137
|
+
workspaceWriteScope: () => planRun?.active ? 'read-only' : undefined
|
|
138
|
+
});
|
|
139
|
+
pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
|
|
140
|
+
pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
|
|
141
|
+
pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
|
|
142
|
+
compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
|
|
143
|
+
// A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
|
|
144
|
+
// person decides whether to continue.
|
|
145
|
+
const slicePolicy = parseWorkSlice(undefined);
|
|
146
|
+
const sliceState = createSliceState(slicePolicy);
|
|
147
|
+
// `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
|
|
148
|
+
if (slicePolicy.enabled)
|
|
149
|
+
workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
|
|
80
150
|
// Only the latest screenshots stay in what the model sees (world/images.ts).
|
|
81
151
|
pi.on('context', (event) => {
|
|
82
152
|
const messages = pruneToolImages(event.messages);
|
|
@@ -86,7 +156,8 @@ export default async function (pi, planRun, getSession) {
|
|
|
86
156
|
context = ctx;
|
|
87
157
|
if (timer)
|
|
88
158
|
clearInterval(timer);
|
|
89
|
-
|
|
159
|
+
// The branch's parent runs let the controller show the latest one's rows again on reopen.
|
|
160
|
+
await terminalCall('ready', { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
|
|
90
161
|
timer = setInterval(() => {
|
|
91
162
|
if (polling || !context)
|
|
92
163
|
return;
|
|
@@ -119,10 +190,20 @@ export default async function (pi, planRun, getSession) {
|
|
|
119
190
|
reasoningTokens = undefined;
|
|
120
191
|
costReported = false;
|
|
121
192
|
// A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
|
|
122
|
-
const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' });
|
|
193
|
+
const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '', model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel?.() ?? null });
|
|
194
|
+
workspaceRoot = typeof begun.workspaceRoot === 'string' ? begun.workspaceRoot : null;
|
|
195
|
+
if (!workspaceRoot)
|
|
196
|
+
throw new Error('The active project workspace could not be resolved; the turn was not started.');
|
|
197
|
+
// Every turn marks its run on the branch, so a reopened conversation finds its latest one.
|
|
198
|
+
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
199
|
+
stoppedChildren = null;
|
|
123
200
|
if (nativeSubagents) {
|
|
124
|
-
pi.
|
|
125
|
-
|
|
201
|
+
const snapshot = await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), plan: planRun?.active === true, history: subagentHistory(ctx.sessionManager.getBranch()) });
|
|
202
|
+
if (planRun && Array.isArray(snapshot.connectors))
|
|
203
|
+
planRun.connectors = snapshot.connectors;
|
|
204
|
+
if (planRun && snapshot.subagentProfiles)
|
|
205
|
+
planRun.subagentProfiles = snapshot.subagentProfiles;
|
|
206
|
+
stoppedChildren = snapshot.notice ?? null;
|
|
126
207
|
subagentParent = true;
|
|
127
208
|
}
|
|
128
209
|
if (planRun && event.messageType === 'user') {
|
|
@@ -130,24 +211,31 @@ export default async function (pi, planRun, getSession) {
|
|
|
130
211
|
event.systemPromptOptions.sections.plan = PLAN_INSTRUCTIONS;
|
|
131
212
|
else
|
|
132
213
|
delete event.systemPromptOptions.sections.plan;
|
|
214
|
+
if (planRun.subagentProfiles)
|
|
215
|
+
event.systemPromptOptions.sections.subagents = planRun.active ? planRun.subagentProfiles.plan : planRun.subagentProfiles.normal;
|
|
133
216
|
}
|
|
134
217
|
const bang = event.messageType === 'user' ? /^\s*!\s*([\s\S]+)$/u.exec(event.prompt) : null;
|
|
135
218
|
if (!bang)
|
|
136
219
|
return;
|
|
137
|
-
const
|
|
220
|
+
const initial = parseInitialBashCommand(bang[1]);
|
|
221
|
+
const command = initial.command;
|
|
222
|
+
const args = { command, ...(initial.workspaceWriteScope === 'outside' ? { workspaceWriteScope: 'outside' } : {}) };
|
|
138
223
|
const id = `initial-bash:${begun.runId}`;
|
|
139
224
|
const started = Date.now();
|
|
140
|
-
await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify(
|
|
225
|
+
await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify(args) });
|
|
141
226
|
let refusal;
|
|
142
227
|
let result;
|
|
143
|
-
if (
|
|
228
|
+
if (!command) {
|
|
229
|
+
refusal = 'Enter a Bash command after !outside.';
|
|
230
|
+
}
|
|
231
|
+
else if (planRun?.active) {
|
|
144
232
|
refusal = 'Bash commands are disabled while a plan request is running.';
|
|
145
233
|
}
|
|
146
234
|
else {
|
|
147
235
|
try {
|
|
148
|
-
const policy = await terminalCall('policy', { tool: 'bash', args
|
|
236
|
+
const policy = await terminalCall('policy', { tool: 'bash', args });
|
|
149
237
|
if (policy.requiresApproval) {
|
|
150
|
-
const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args
|
|
238
|
+
const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args });
|
|
151
239
|
if (approval.decision !== 'approved')
|
|
152
240
|
refusal = 'The user denied this Bash command.';
|
|
153
241
|
}
|
|
@@ -159,7 +247,7 @@ export default async function (pi, planRun, getSession) {
|
|
|
159
247
|
if (!session)
|
|
160
248
|
refusal = 'The native session is unavailable.';
|
|
161
249
|
else
|
|
162
|
-
result = await session.executeBash(command, undefined, { id });
|
|
250
|
+
result = await runWithWorkspaceWriteScope(initial.workspaceWriteScope, () => withCommandBudget(() => session.executeBash(command, undefined, { id, operations: workspaceBash }), () => session.abortBash(), DEFAULT_COMMAND_TIMEOUT_SECONDS));
|
|
163
251
|
}
|
|
164
252
|
}
|
|
165
253
|
catch (error) {
|
|
@@ -179,6 +267,13 @@ export default async function (pi, planRun, getSession) {
|
|
|
179
267
|
: 'The user’s initial Bash command has already run. Read its native Bash result and explain what it shows. Do not run the same command again unless the user explicitly asks.';
|
|
180
268
|
return { message: { customType: 'xingular-initial-bash', content: instruction, display: false, details: {} } };
|
|
181
269
|
});
|
|
270
|
+
// A second handler, so the notice joins any initial-Bash message rather than replacing it.
|
|
271
|
+
pi.on('before_agent_start', async () => {
|
|
272
|
+
const notice = stoppedChildren;
|
|
273
|
+
stoppedChildren = null;
|
|
274
|
+
if (notice)
|
|
275
|
+
return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
|
|
276
|
+
});
|
|
182
277
|
const deliverChildren = async (event, ctx, settle) => {
|
|
183
278
|
if (!nativeSubagents || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
|
|
184
279
|
return;
|
|
@@ -240,21 +335,61 @@ export default async function (pi, planRun, getSession) {
|
|
|
240
335
|
pi.on('turn_start', async () => {
|
|
241
336
|
requestStartedAt = Date.now();
|
|
242
337
|
});
|
|
243
|
-
pi.on('tool_call', async (event) => {
|
|
338
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
244
339
|
if (planRun?.active && !planRun.allowedTools.has(event.toolName)) {
|
|
245
|
-
const reason = planRefusal(event.toolName, new Set()) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`;
|
|
340
|
+
const reason = planRefusal(event.toolName, new Set(), event.input) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`;
|
|
246
341
|
return { block: true, reason };
|
|
247
342
|
}
|
|
343
|
+
if (planRun?.active && event.toolName === SPAWN_SUBAGENT) {
|
|
344
|
+
try {
|
|
345
|
+
const eligibility = await controlCall('terminal.subagents.planSafe', { input: event.input });
|
|
346
|
+
if (!eligibility.allowed)
|
|
347
|
+
return { block: true, reason: eligibility.reason ?? 'This profile is not eligible for plan-mode research.' };
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
return { block: true, reason: `Plan-mode research eligibility could not be verified: ${error instanceof Error ? error.message : 'controller unavailable'}` };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (planRun?.active) {
|
|
354
|
+
const reason = planRefusal(event.toolName, new Set(PLAN_RESEARCH_TOOLS), event.input);
|
|
355
|
+
if (reason)
|
|
356
|
+
return { block: true, reason };
|
|
357
|
+
}
|
|
358
|
+
let authorization;
|
|
359
|
+
if ((event.toolName === 'edit' || event.toolName === 'write') && typeof event.input.path === 'string') {
|
|
360
|
+
try {
|
|
361
|
+
if (!workspaceRoot)
|
|
362
|
+
throw new Error('The active project workspace is unavailable.');
|
|
363
|
+
const inspection = inspectWorkspaceWrite(workspaceRoot, ctx.cwd, event.toolName, event.input);
|
|
364
|
+
Object.assign(event.input, inspection.input);
|
|
365
|
+
if (!inspection.path)
|
|
366
|
+
throw new Error('The native file-write target could not be verified.');
|
|
367
|
+
authorization = {
|
|
368
|
+
workspaceRoot,
|
|
369
|
+
cwd: ctx.cwd,
|
|
370
|
+
path: inspection.path,
|
|
371
|
+
outside: inspection.outside,
|
|
372
|
+
authorized: false
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
return { block: true, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.' };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
248
379
|
try {
|
|
249
380
|
const policy = await controlCall('terminal.policy', { tool: event.toolName, args: event.input });
|
|
250
381
|
if (policy.requiresApproval) {
|
|
251
382
|
const approval = await controlCall('terminal.approval', { requestId: event.toolCallId, tool: event.toolName, args: event.input });
|
|
252
|
-
if (approval.decision
|
|
253
|
-
return;
|
|
254
|
-
return { block: true, reason: 'The user denied this action. Ask how they would like to proceed.' };
|
|
383
|
+
if (approval.decision !== 'approved')
|
|
384
|
+
return { block: true, reason: 'The user denied this action. Ask how they would like to proceed.' };
|
|
255
385
|
}
|
|
256
|
-
if (!policy.allowed)
|
|
386
|
+
else if (!policy.allowed) {
|
|
257
387
|
return { block: true, reason: policy.reason ?? 'Outside what this session may do.' };
|
|
388
|
+
}
|
|
389
|
+
if (authorization) {
|
|
390
|
+
authorization.authorized = true;
|
|
391
|
+
writeAuthorizations.set(event.toolCallId, authorization);
|
|
392
|
+
}
|
|
258
393
|
}
|
|
259
394
|
catch (error) {
|
|
260
395
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -287,7 +422,10 @@ export default async function (pi, planRun, getSession) {
|
|
|
287
422
|
}
|
|
288
423
|
}
|
|
289
424
|
subagentParent = false;
|
|
290
|
-
|
|
425
|
+
// A reply that stopped at its work-slice boundary is not a completed task: the record says so,
|
|
426
|
+
// so a host can offer the continuation instead of treating it as done (PROP-0041).
|
|
427
|
+
const reason = sliceState.endedBy ? sliceState.endedBy === 'stuck' ? 'stalled' : 'slice_complete' : undefined;
|
|
428
|
+
await terminalCall('settled', { error: failure, aborted: event.aborted, ...(reason ? { reason } : {}) });
|
|
291
429
|
});
|
|
292
430
|
pi.on('user_bash', async () => {
|
|
293
431
|
throw new Error('Use the ! command composer so this action can be checked.');
|
|
@@ -313,19 +451,32 @@ export default async function (pi, planRun, getSession) {
|
|
|
313
451
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
314
452
|
}
|
|
315
453
|
});
|
|
316
|
-
// The
|
|
454
|
+
// The todo list is recorded on the harness run; the shared tool validates and the socket carries the event.
|
|
317
455
|
let recorded = Promise.resolve();
|
|
318
|
-
const
|
|
456
|
+
const todo = todoTool({ emit: (_type, payload) => { recorded = terminalCall('todo', payload); } });
|
|
319
457
|
pi.registerTool({
|
|
320
|
-
name:
|
|
321
|
-
parameters: Type.Unsafe(
|
|
458
|
+
name: todo.name, label: todo.label, description: todo.description,
|
|
459
|
+
parameters: Type.Unsafe(todo.parameters(Type)),
|
|
322
460
|
async execute(_id, input) {
|
|
323
461
|
recorded = Promise.resolve();
|
|
324
|
-
const result = await
|
|
462
|
+
const result = await todo.execute(input);
|
|
325
463
|
await recorded;
|
|
326
464
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
327
465
|
}
|
|
328
466
|
});
|
|
467
|
+
// A bounded reply records its handoff here; the terminal reply is its own slice.
|
|
468
|
+
let checkpointRecorded = Promise.resolve();
|
|
469
|
+
const checkpoint = checkpointTool({ emit: (_type, payload) => { checkpointRecorded = terminalCall('checkpoint', payload); } }, 1);
|
|
470
|
+
pi.registerTool({
|
|
471
|
+
name: checkpoint.name, label: checkpoint.label, description: checkpoint.description,
|
|
472
|
+
parameters: Type.Unsafe(checkpoint.parameters(Type)),
|
|
473
|
+
async execute(_id, input) {
|
|
474
|
+
checkpointRecorded = Promise.resolve();
|
|
475
|
+
const result = await checkpoint.execute(input);
|
|
476
|
+
await checkpointRecorded;
|
|
477
|
+
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
478
|
+
}
|
|
479
|
+
});
|
|
329
480
|
const ask = askUserTool({ requestAnswers: questions => terminalCall('questions', { questions }, 1_230_000) });
|
|
330
481
|
pi.registerTool({
|
|
331
482
|
name: ask.name, label: ask.label, description: ask.description,
|
|
@@ -335,43 +486,6 @@ export default async function (pi, planRun, getSession) {
|
|
|
335
486
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
336
487
|
}
|
|
337
488
|
});
|
|
338
|
-
|
|
339
|
-
name: 'ask_user_question',
|
|
340
|
-
label: 'Ask a question',
|
|
341
|
-
description: 'Ask the user a question and wait for their answer. Include a concise 1–2 word summary label; if omitted, the terminal derives one from the question. Every question must include at least one choice and at least one choice must be recommended. Recommendations are visual only and never preselect answers; submitted choices keep their labels. Mark at most one choice recommended for single-choice questions or several when multiSelect is true. When a UI diagram helps compare choices, set uiSketch: true and include a detailed, bounded ASCII wireframe for every choice. Use ASCII tables and panel borders to show the affected element alongside relevant neighboring UI, with readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes, not a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels.',
|
|
342
|
-
parameters: Type.Object({
|
|
343
|
-
summary: Type.Optional(Type.String({ description: 'A concise label of one or two words' })),
|
|
344
|
-
question: Type.String(),
|
|
345
|
-
uiSketch: Type.Optional(Type.Literal(true, { description: 'Enable an ASCII sketch preview; when true, every choice must include a sketch.' })),
|
|
346
|
-
options: Type.Array(Type.Object({
|
|
347
|
-
label: Type.String(),
|
|
348
|
-
description: Type.Optional(Type.String()),
|
|
349
|
-
recommended: Type.Optional(Type.Boolean()),
|
|
350
|
-
sketch: Type.Optional(Type.String({ minLength: 1, maxLength: QUESTION_SKETCH_MAX_LENGTH, pattern: QUESTION_SKETCH_PATTERN, description: 'Literal ASCII UI wireframe: 1–24 LF-separated lines, at most 80 printable ASCII characters per line. Include the affected element and neighboring UI using structured table/panel borders with fixed, matching outer-edge columns on every row. Required for every choice when uiSketch is true.' }))
|
|
351
|
-
}), {
|
|
352
|
-
minItems: 1,
|
|
353
|
-
contains: Type.Object({ recommended: Type.Literal(true) })
|
|
354
|
-
}),
|
|
355
|
-
multiSelect: Type.Optional(Type.Boolean())
|
|
356
|
-
}),
|
|
357
|
-
async execute(id, input) {
|
|
358
|
-
if (!Array.isArray(input.options) || input.options.length === 0)
|
|
359
|
-
throw new Error('Every question needs at least one choice.');
|
|
360
|
-
const recommendedCount = input.options.filter(option => option.recommended).length;
|
|
361
|
-
if (recommendedCount === 0)
|
|
362
|
-
throw new Error('Every question needs at least one recommended choice.');
|
|
363
|
-
if (!input.multiSelect && recommendedCount > 1)
|
|
364
|
-
throw new Error('A single-choice question may recommend at most one option.');
|
|
365
|
-
const sketchError = questionSketchError(input.uiSketch, input.options);
|
|
366
|
-
if (sketchError)
|
|
367
|
-
throw new Error(sketchError);
|
|
368
|
-
const summary = typeof input.summary === 'string' ? input.summary.trim() : '';
|
|
369
|
-
if (summary && (summary.length > 100 || summary.split(/\s+/u).filter(Boolean).length > 2)) {
|
|
370
|
-
throw new Error('The question summary must be a concise 1–2 word label of no more than 100 characters.');
|
|
371
|
-
}
|
|
372
|
-
const answer = await terminalCall('question', { requestId: id, ...input, ...(summary ? { summary } : {}) });
|
|
373
|
-
return { content: [{ type: 'text', text: JSON.stringify(answer) }], details: undefined };
|
|
374
|
-
}
|
|
375
|
-
});
|
|
489
|
+
// One question tool, as in every structured session: the terminal has no ask_user_question alias (PROP-0039).
|
|
376
490
|
await terminalCall('startup', { phase: 'Pi hooks registered' });
|
|
377
491
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
import type { SecretStore } from "../accounts/secrets.js";
|
|
3
|
+
import { type SecretBackendDescriptor } from "../core/interactiveProfile.js";
|
|
2
4
|
import { type Harness, type HarnessOptions } from "../harness.js";
|
|
3
5
|
export interface StandalonePaths {
|
|
4
6
|
dataRoot: string;
|
|
5
7
|
stateDir: string;
|
|
6
8
|
/** `--worktree [name]`: start in a new managed worktree (true) or the named one. */
|
|
7
9
|
worktree?: string | true | null;
|
|
10
|
+
/** `--variant <scope>`: run on that scope's head, as a host's agent of that scope does. */
|
|
11
|
+
variant?: string | null;
|
|
8
12
|
}
|
|
9
13
|
export interface StandaloneDependencies {
|
|
10
14
|
home?: string;
|
|
@@ -13,6 +17,9 @@ export interface StandaloneDependencies {
|
|
|
13
17
|
stdoutIsTTY?: boolean;
|
|
14
18
|
createSessionId?: () => string;
|
|
15
19
|
createHarness?: (options: HarnessOptions) => Harness;
|
|
20
|
+
hostSecretStore?: (descriptor: Extract<SecretBackendDescriptor, {
|
|
21
|
+
kind: 'host';
|
|
22
|
+
}>) => SecretStore | Promise<SecretStore>;
|
|
16
23
|
spawn?: typeof nodeSpawn;
|
|
17
24
|
}
|
|
18
25
|
export declare function standaloneArguments(argv: string[]): string[] | null;
|
|
@@ -6,30 +6,39 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
7
7
|
import { setLogSink } from "../core/bus.js";
|
|
8
8
|
import { DEFAULT_PRODUCT_NAME } from "../core/env.js";
|
|
9
|
+
import { interactiveProfileSecretStore, resolveInteractiveProfile } from "../core/interactiveProfile.js";
|
|
9
10
|
import { createHarness } from "../harness.js";
|
|
11
|
+
import { seedVariant } from "../variant/seed.js";
|
|
12
|
+
import * as variantStore from "../variant/store.js";
|
|
10
13
|
export function standaloneArguments(argv) {
|
|
11
14
|
const command = argv[0];
|
|
12
15
|
if (!command || command.startsWith('-'))
|
|
13
16
|
return argv;
|
|
14
17
|
return command === 'terminal' ? argv.slice(1) : null;
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const paths = {
|
|
19
|
-
dataRoot: join(root, 'data'),
|
|
20
|
-
stateDir: join(root, 'state')
|
|
21
|
-
};
|
|
19
|
+
function resolveStandaloneOptions(argv, home = homedir(), cwd = process.cwd()) {
|
|
20
|
+
const overrides = {};
|
|
22
21
|
const given = new Set();
|
|
22
|
+
let worktree;
|
|
23
|
+
let variant;
|
|
23
24
|
for (let index = 0; index < argv.length; index += 1) {
|
|
24
25
|
const argument = argv[index];
|
|
25
26
|
if (argument === '--worktree' || argument === '-w') {
|
|
26
27
|
const name = argv[index + 1];
|
|
27
28
|
if (name && !name.startsWith('-')) {
|
|
28
|
-
|
|
29
|
+
worktree = name;
|
|
29
30
|
index += 1;
|
|
30
31
|
}
|
|
31
32
|
else
|
|
32
|
-
|
|
33
|
+
worktree = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (argument === '--variant') {
|
|
37
|
+
const scope = argv[index + 1];
|
|
38
|
+
if (!scope || scope.startsWith('-'))
|
|
39
|
+
throw new Error('--variant needs a variant scope.');
|
|
40
|
+
variant = scope;
|
|
41
|
+
index += 1;
|
|
33
42
|
continue;
|
|
34
43
|
}
|
|
35
44
|
if (argument !== '--data' && argument !== '--state')
|
|
@@ -37,7 +46,10 @@ export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd
|
|
|
37
46
|
const value = argv[index + 1];
|
|
38
47
|
if (!value || value.startsWith('--'))
|
|
39
48
|
throw new Error(`${argument} needs a directory path.`);
|
|
40
|
-
|
|
49
|
+
if (argument === '--data')
|
|
50
|
+
overrides.dataRoot = resolve(cwd, value);
|
|
51
|
+
else
|
|
52
|
+
overrides.stateDir = resolve(cwd, value);
|
|
41
53
|
given.add(argument);
|
|
42
54
|
index += 1;
|
|
43
55
|
}
|
|
@@ -46,7 +58,49 @@ export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd
|
|
|
46
58
|
if (!isAbsolute(home) && !(given.has('--data') && given.has('--state'))) {
|
|
47
59
|
throw new Error(`The home folder "${home}" is not an absolute path, so the default data and state folders cannot be placed. Set HOME to your home folder, or pass --data and --state with absolute folders.`);
|
|
48
60
|
}
|
|
49
|
-
|
|
61
|
+
// `npm run dev` passes these roots explicitly. Keep its profile and credentials separate from
|
|
62
|
+
// the installed interactive profile, even when that production profile selects a host store.
|
|
63
|
+
const devRoot = isAbsolute(home) ? join(home, '.xingular-harness-dev') : null;
|
|
64
|
+
const devData = devRoot ? join(devRoot, 'data') : null;
|
|
65
|
+
const devState = devRoot ? join(devRoot, 'state') : null;
|
|
66
|
+
const development = !!devRoot && (overrides.dataRoot === devData || overrides.stateDir === devState);
|
|
67
|
+
const resolvedProfile = isAbsolute(home)
|
|
68
|
+
? resolveInteractiveProfile({
|
|
69
|
+
home,
|
|
70
|
+
...(development ? {
|
|
71
|
+
configPath: join(devRoot, 'profile.json'),
|
|
72
|
+
legacyDefaults: {
|
|
73
|
+
dataRoot: devData,
|
|
74
|
+
stateDir: devState,
|
|
75
|
+
defaultVariantScope: 'default',
|
|
76
|
+
secretBackend: { kind: 'file', path: join(devState, 'secrets.json') }
|
|
77
|
+
}
|
|
78
|
+
} : {}),
|
|
79
|
+
overrides
|
|
80
|
+
})
|
|
81
|
+
: {
|
|
82
|
+
version: 1,
|
|
83
|
+
dataRoot: overrides.dataRoot,
|
|
84
|
+
stateDir: overrides.stateDir,
|
|
85
|
+
defaultVariantScope: 'default',
|
|
86
|
+
secretBackend: { kind: 'file', path: join(overrides.stateDir, 'secrets.json') }
|
|
87
|
+
};
|
|
88
|
+
// The standalone development launch has no host adapter and must not use the App's Keychain.
|
|
89
|
+
const profile = development
|
|
90
|
+
? { ...resolvedProfile, secretBackend: { kind: 'file', path: join(resolvedProfile.stateDir, 'secrets.json') } }
|
|
91
|
+
: resolvedProfile;
|
|
92
|
+
return {
|
|
93
|
+
profile,
|
|
94
|
+
paths: {
|
|
95
|
+
dataRoot: profile.dataRoot,
|
|
96
|
+
stateDir: profile.stateDir,
|
|
97
|
+
...(worktree !== undefined ? { worktree } : {}),
|
|
98
|
+
...(variant !== undefined ? { variant } : {})
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function parseStandaloneOptions(argv, home = homedir(), cwd = process.cwd()) {
|
|
103
|
+
return resolveStandaloneOptions(argv, home, cwd).paths;
|
|
50
104
|
}
|
|
51
105
|
function processEnvironment(overrides) {
|
|
52
106
|
const environment = { ...process.env };
|
|
@@ -133,9 +187,10 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
|
|
|
133
187
|
throw new Error('The interactive Harness terminal needs a TTY. Run it from a terminal, or use `xagent serve` for a host process.');
|
|
134
188
|
}
|
|
135
189
|
let cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
136
|
-
const paths =
|
|
190
|
+
const { paths, profile } = resolveStandaloneOptions(argv, dependencies.home, cwd);
|
|
191
|
+
const secrets = await interactiveProfileSecretStore(profile, dependencies.hostSecretStore);
|
|
137
192
|
const makeHarness = dependencies.createHarness ?? createHarness;
|
|
138
|
-
const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: DEFAULT_PRODUCT_NAME });
|
|
193
|
+
const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: DEFAULT_PRODUCT_NAME, secrets });
|
|
139
194
|
const newSessionId = dependencies.createSessionId ?? randomUUID;
|
|
140
195
|
let current = { sessionId: newSessionId(), accountId: null, nativeSessionId: null };
|
|
141
196
|
// The frontend draws on this terminal, so the host's diagnostics go to a file the person can read instead.
|
|
@@ -150,6 +205,10 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
|
|
|
150
205
|
});
|
|
151
206
|
try {
|
|
152
207
|
await harness.ready;
|
|
208
|
+
const variantScope = paths.variant ?? profile.defaultVariantScope;
|
|
209
|
+
if (variantScope === profile.defaultVariantScope && !variantStore.head(paths.dataRoot, variantScope)) {
|
|
210
|
+
variantStore.seed(paths.dataRoot, variantScope, seedVariant(paths.dataRoot, {}), 'interactive profile default');
|
|
211
|
+
}
|
|
153
212
|
if (harness.accounts.overview().accounts.length === 0)
|
|
154
213
|
harness.accounts.create('Default', 'subscription');
|
|
155
214
|
let worktreeId = null;
|
|
@@ -160,7 +219,7 @@ export async function runStandaloneTerminal(argv, dependencies = {}) {
|
|
|
160
219
|
}
|
|
161
220
|
for (;;) {
|
|
162
221
|
const sessionFolder = join(paths.dataRoot, 'sessions', 'terminal', createHash('sha256').update(cwd).digest('hex'));
|
|
163
|
-
const launch = await harness.terminals.prepare({ ...current, cwd, sessionFolder, ...(worktreeId ? { run: { worktreeId } } : {}) });
|
|
222
|
+
const launch = await harness.terminals.prepare({ ...current, cwd, sessionFolder, variant: { scope: variantScope }, ...(worktreeId ? { run: { worktreeId } } : {}) });
|
|
164
223
|
const code = await spawnFrontend(launch, cwd, dependencies.spawn);
|
|
165
224
|
// `/accounts` and `/worktree` ask for a switch and end the frontend. An account switch resumes
|
|
166
225
|
// the conversation; a folder switch starts a new one there.
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RunEvent, type RunStatus, type UsagePayload } from "../sessions/contracts.js";
|
|
2
|
+
import type { Effort } from "../sessions/runner.js";
|
|
2
3
|
/** The agent profile a child started from, as the roster shows it (PROP-0029). */
|
|
3
4
|
export interface TerminalSubagentProfile {
|
|
4
5
|
name: string;
|
|
5
6
|
color: string;
|
|
6
7
|
}
|
|
8
|
+
/** The main agent's row: the parent run itself, shown by name rather than by a profile. */
|
|
9
|
+
export declare const MAIN_PROFILE: TerminalSubagentProfile;
|
|
7
10
|
export interface TerminalSubagentRun {
|
|
8
11
|
id: string;
|
|
9
12
|
parentRunId: string | null;
|
|
10
|
-
|
|
13
|
+
/** `main` is the parent run itself; every other row is a child or nested child. */
|
|
14
|
+
relation: 'main' | 'subagent';
|
|
11
15
|
/** The short name the parent gave the child (PROP-0030). */
|
|
12
16
|
title: string;
|
|
13
17
|
goal: string | null;
|
|
@@ -15,11 +19,19 @@ export interface TerminalSubagentRun {
|
|
|
15
19
|
startedAt: string;
|
|
16
20
|
endedAt: string | null;
|
|
17
21
|
profile?: TerminalSubagentProfile | null;
|
|
22
|
+
/** The pinned account's display label; subagents never switch accounts. */
|
|
23
|
+
accountLabel?: string | null;
|
|
24
|
+
/** Effective launch selection, superseded by provider-reported usage when available. */
|
|
25
|
+
model?: string | null;
|
|
26
|
+
effort?: Effort | null;
|
|
18
27
|
}
|
|
28
|
+
export type TerminalSubagentUsage = Partial<Pick<UsagePayload, 'provider' | 'model' | 'inputTokens' | 'outputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens' | 'costUsd' | 'costSource' | 'requests' | 'contextTokens' | 'contextWindow'>>;
|
|
19
29
|
export interface TerminalSubagentActivity extends TerminalSubagentRun {
|
|
20
30
|
activity: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
31
|
+
/** Cumulative run usage only; provider reasoning summaries and request details stay private. */
|
|
32
|
+
usage: TerminalSubagentUsage | null;
|
|
33
|
+
/** How far the child's own todo is: counts only, never the items. */
|
|
34
|
+
todo: {
|
|
23
35
|
done: number;
|
|
24
36
|
total: number;
|
|
25
37
|
} | null;
|
|
@@ -27,12 +39,22 @@ export interface TerminalSubagentActivity extends TerminalSubagentRun {
|
|
|
27
39
|
export declare const ACTIVE_SUBAGENT_STATUSES: Set<"queued" | "running" | "waiting_for_approval" | "waiting_for_input" | "interrupted" | "completed" | "failed">;
|
|
28
40
|
export declare function terminalSubagentDuration(activity: TerminalSubagentRun, now?: number): string;
|
|
29
41
|
export declare function terminalSubagentTone(status: RunStatus): 'success' | 'warning' | 'error' | 'muted';
|
|
30
|
-
/** Volatile, read-only activity projection for the
|
|
42
|
+
/** Volatile, read-only activity projection for the latest terminal parent run. */
|
|
31
43
|
export declare class SubagentMonitor {
|
|
32
44
|
private parentRunId;
|
|
33
45
|
private readonly activities;
|
|
34
46
|
begin(parentRunId: string): void;
|
|
35
|
-
|
|
47
|
+
/** Folds one event of the tracked parent (`main`) or one of its descendants; `null` seeds the row. */
|
|
48
|
+
observe(parentRunId: string, run: TerminalSubagentRun, event: RunEvent | null): void;
|
|
49
|
+
/**
|
|
50
|
+
* Rebuilds the rows of a parent run that is no longer live, such as the latest one of a reopened
|
|
51
|
+
* conversation, by folding each run's saved events. Rows keep their final status.
|
|
52
|
+
*/
|
|
53
|
+
restore(parentRunId: string, runs: readonly {
|
|
54
|
+
run: TerminalSubagentRun;
|
|
55
|
+
events: readonly RunEvent[];
|
|
56
|
+
}[]): void;
|
|
57
|
+
/** The main row first, then children in start order. */
|
|
36
58
|
snapshot(parentRunId: string): TerminalSubagentActivity[];
|
|
37
59
|
clear(parentRunId?: string): void;
|
|
38
60
|
}
|