@xingular/harness 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +28 -25
- package/dist/cli.js +6675 -2501
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +15 -1
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +2 -1
- package/dist/sdk/agents/profiles.js +9 -4
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +29 -12
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -29
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +18 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +168 -0
- package/dist/sdk/sessions/contracts.js +59 -1
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +73 -16
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +35 -0
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +202 -17
- package/dist/sdk/sessions/subagentService.d.ts +9 -0
- package/dist/sdk/sessions/subagentService.js +63 -4
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +11 -3
- package/dist/sdk/terminal/controller.js +40 -13
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +154 -54
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
- package/dist/sdk/terminal/subagentMonitor.js +32 -1
- package/dist/sdk/terminal/terminals.d.ts +19 -2
- package/dist/sdk/terminal/terminals.js +245 -35
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +205 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checklist.js +1 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/spawnSubagent.js +4 -3
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +5207 -1883
- package/dist/terminal-pi.js +1658 -271
- package/docs/architecture/02-sessions-and-runs.md +128 -26
- package/docs/architecture/04-prompt.md +50 -12
- package/docs/architecture/06-plugins-and-capabilities.md +24 -7
- package/docs/architecture/07-tools-and-gate.md +104 -24
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +31 -1
- package/docs/architecture/10-server-protocol.md +58 -3
- package/docs/architecture/11-terminal.md +105 -42
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +54 -7
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +14 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +80 -24
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +100 -59
- package/package.json +4 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// OpenCode's terminal palette, carried by the Harness's own product name.
|
|
2
|
+
import { SELECTION_COLORS } from "../tui/selection.js";
|
|
3
|
+
export const opentuiTheme = {
|
|
4
|
+
background: '#0b0b0d',
|
|
5
|
+
panel: '#111114',
|
|
6
|
+
scrim: '#0b0b0d99',
|
|
7
|
+
border: '#2a2a31',
|
|
8
|
+
borderFocus: '#3a3a44',
|
|
9
|
+
text: '#e6e6e9',
|
|
10
|
+
muted: '#8b8b95',
|
|
11
|
+
dim: '#5c5c66',
|
|
12
|
+
selectionBg: SELECTION_COLORS.dark.background,
|
|
13
|
+
selectionText: SELECTION_COLORS.dark.text,
|
|
14
|
+
selectionMuted: SELECTION_COLORS.dark.muted,
|
|
15
|
+
questionSelectedBg: '#193322',
|
|
16
|
+
questionActionBg: '#35754a',
|
|
17
|
+
questionActionText: '#f7fff8',
|
|
18
|
+
primary: '#fab283',
|
|
19
|
+
accent: '#8ab4f8',
|
|
20
|
+
success: '#a6da95',
|
|
21
|
+
warning: '#eed49f',
|
|
22
|
+
error: '#ed8796',
|
|
23
|
+
bashMode: '#00ff88',
|
|
24
|
+
planMode: '#8ab4f8',
|
|
25
|
+
attachment: '#5ea1ff',
|
|
26
|
+
purple: '#c6a0f6',
|
|
27
|
+
cyan: '#8bd5ca'
|
|
28
|
+
};
|
|
29
|
+
/** The colour of an agent profile's name (PROP-0029); an unknown name falls back to the accent. */
|
|
30
|
+
export function profileColor(theme, name) {
|
|
31
|
+
switch (name) {
|
|
32
|
+
case 'blue': return theme.accent;
|
|
33
|
+
case 'green': return theme.success;
|
|
34
|
+
case 'yellow': return theme.warning;
|
|
35
|
+
case 'red': return theme.error;
|
|
36
|
+
case 'magenta': return '#f5bde6';
|
|
37
|
+
case 'cyan': return theme.cyan;
|
|
38
|
+
case 'orange': return theme.primary;
|
|
39
|
+
case 'purple': return theme.purple;
|
|
40
|
+
default: return theme.accent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function blendHexColor(from, to, amount) {
|
|
44
|
+
const start = /^#([\da-f]{6})$/iu.exec(from)?.[1];
|
|
45
|
+
const end = /^#([\da-f]{6})$/iu.exec(to)?.[1];
|
|
46
|
+
if (!start || !end)
|
|
47
|
+
return from;
|
|
48
|
+
const weight = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 0;
|
|
49
|
+
const channels = [0, 2, 4].map(offset => {
|
|
50
|
+
const first = Number.parseInt(start.slice(offset, offset + 2), 16);
|
|
51
|
+
const second = Number.parseInt(end.slice(offset, offset + 2), 16);
|
|
52
|
+
return Math.round(first + (second - first) * weight).toString(16).padStart(2, '0');
|
|
53
|
+
});
|
|
54
|
+
return `#${channels.join('')}`;
|
|
55
|
+
}
|
|
56
|
+
export function composerRuleColor(theme, mode) {
|
|
57
|
+
if (mode.bash)
|
|
58
|
+
return theme.bashMode;
|
|
59
|
+
if (mode.plan)
|
|
60
|
+
return theme.planMode;
|
|
61
|
+
return mode.overlay ? theme.border : theme.dim;
|
|
62
|
+
}
|
|
63
|
+
export function noticeColor(theme, type) {
|
|
64
|
+
if (type === 'error')
|
|
65
|
+
return theme.error;
|
|
66
|
+
if (type === 'warning')
|
|
67
|
+
return theme.warning;
|
|
68
|
+
return theme.muted;
|
|
69
|
+
}
|
|
70
|
+
export function toolColor(theme, status) {
|
|
71
|
+
if (status === 'error')
|
|
72
|
+
return theme.error;
|
|
73
|
+
if (status === 'running')
|
|
74
|
+
return theme.warning;
|
|
75
|
+
if (status === 'pending')
|
|
76
|
+
return theme.dim;
|
|
77
|
+
return theme.success;
|
|
78
|
+
}
|
|
79
|
+
export function thinkingLevelColor(theme, level) {
|
|
80
|
+
if (level === 'off')
|
|
81
|
+
return theme.dim;
|
|
82
|
+
if (level === 'minimal' || level === 'low')
|
|
83
|
+
return theme.muted;
|
|
84
|
+
if (level === 'medium')
|
|
85
|
+
return theme.accent;
|
|
86
|
+
return theme.primary;
|
|
87
|
+
}
|
|
@@ -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, 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
12
|
import { checklistTool } from "../tools/checklist.js";
|
|
13
|
+
import { 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 { withWorkspaceWriteScope } 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;
|
|
@@ -42,6 +58,8 @@ export default async function (pi, planRun, getSession) {
|
|
|
42
58
|
let requestStartedAt = null;
|
|
43
59
|
const toolStartedAt = new Map();
|
|
44
60
|
const fileTools = new Map();
|
|
61
|
+
const writeAuthorizations = new Map();
|
|
62
|
+
let workspaceRoot = null;
|
|
45
63
|
// The harness's own tools for this session (connected apps and what the application offers).
|
|
46
64
|
let stage = 'Tool discovery';
|
|
47
65
|
try {
|
|
@@ -62,8 +80,19 @@ export default async function (pi, planRun, getSession) {
|
|
|
62
80
|
}
|
|
63
81
|
});
|
|
64
82
|
}
|
|
83
|
+
// The harness's own extensions a person switched on, as a structured session loads them inline.
|
|
84
|
+
const own = await socketCall(connection, 'firstParty.list', {}, 15_000)
|
|
85
|
+
.then((reply) => reply?.plugins ?? [], () => []);
|
|
86
|
+
for (const plugin of own) {
|
|
87
|
+
if (plugin.id === OUTPUT_TRIM)
|
|
88
|
+
outputTrim(plugin.settings)(pi);
|
|
89
|
+
if (plugin.id === BUDGET_GUARD)
|
|
90
|
+
budgetGuard(plugin.settings)(pi);
|
|
91
|
+
}
|
|
65
92
|
const native = await controlCall('terminal.subagents.tools');
|
|
66
93
|
nativeSubagents = native.tools.length > 0;
|
|
94
|
+
if (planRun && native.profiles)
|
|
95
|
+
planRun.subagentProfiles = native.profiles;
|
|
67
96
|
for (const tool of native.tools)
|
|
68
97
|
pi.registerTool({
|
|
69
98
|
name: tool.name, label: tool.label, description: tool.description, parameters: Type.Unsafe(tool.parameters),
|
|
@@ -77,6 +106,46 @@ export default async function (pi, planRun, getSession) {
|
|
|
77
106
|
await terminalCall('startupFailed', { reason: `${stage} failed: ${error instanceof Error ? error.message : 'Unavailable'}` }).catch(() => { });
|
|
78
107
|
throw error;
|
|
79
108
|
}
|
|
109
|
+
pi.registerTool({
|
|
110
|
+
...CAPABILITY_DISCOVERY,
|
|
111
|
+
parameters: Type.Unsafe(CAPABILITY_DISCOVERY.parameters),
|
|
112
|
+
async execute(_id, input) {
|
|
113
|
+
const active = pi.getActiveTools();
|
|
114
|
+
const tools = pi.getAllTools();
|
|
115
|
+
const selected = planRun?.active ? [...planRun.allowedTools] : active;
|
|
116
|
+
const name = input && typeof input === 'object' && typeof input.tool === 'string'
|
|
117
|
+
? input.tool
|
|
118
|
+
: undefined;
|
|
119
|
+
const text = JSON.stringify(describeCapabilities({
|
|
120
|
+
capabilities: { version: 1, tools: selected, connectors: planRun?.connectors ?? [], contributions: [] },
|
|
121
|
+
active,
|
|
122
|
+
registered: tools
|
|
123
|
+
}, name));
|
|
124
|
+
return { content: [{ type: 'text', text }], details: undefined };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const workspaceBash = createWorkspaceBashOperations(() => workspaceRoot, {
|
|
128
|
+
bypassWorkspaceBoundary: async (request) => {
|
|
129
|
+
const result = await controlCall('terminal.workspaceBoundaryBypass', { tool: 'bash', args: { command: request.command, cwd: request.cwd, workspaceRoot: request.workspaceRoot } });
|
|
130
|
+
return result.bypass;
|
|
131
|
+
},
|
|
132
|
+
requestWorkspaceWriteScope: async (request) => {
|
|
133
|
+
const result = await controlCall('terminal.workspaceEscape', { tool: 'bash', args: { command: request.command, workspaceWriteScope: 'outside' }, reason: request.reason });
|
|
134
|
+
return result.approved;
|
|
135
|
+
},
|
|
136
|
+
workspaceWriteScope: () => planRun?.active ? 'read-only' : undefined
|
|
137
|
+
});
|
|
138
|
+
pi.registerTool(withWorkspaceWriteScope(createBashTool(process.cwd(), { operations: workspaceBash })));
|
|
139
|
+
pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
|
|
140
|
+
pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
|
|
141
|
+
compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
|
|
142
|
+
// A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
|
|
143
|
+
// person decides whether to continue.
|
|
144
|
+
const slicePolicy = parseWorkSlice(undefined);
|
|
145
|
+
const sliceState = createSliceState(slicePolicy);
|
|
146
|
+
// `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
|
|
147
|
+
if (slicePolicy.enabled)
|
|
148
|
+
workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
|
|
80
149
|
// Only the latest screenshots stay in what the model sees (world/images.ts).
|
|
81
150
|
pi.on('context', (event) => {
|
|
82
151
|
const messages = pruneToolImages(event.messages);
|
|
@@ -120,9 +189,14 @@ export default async function (pi, planRun, getSession) {
|
|
|
120
189
|
costReported = false;
|
|
121
190
|
// A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
|
|
122
191
|
const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' });
|
|
192
|
+
workspaceRoot = typeof begun.workspaceRoot === 'string' ? begun.workspaceRoot : null;
|
|
193
|
+
if (!workspaceRoot)
|
|
194
|
+
throw new Error('The active project workspace could not be resolved; the turn was not started.');
|
|
123
195
|
if (nativeSubagents) {
|
|
124
196
|
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
125
|
-
await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), history: subagentHistory(ctx.sessionManager.getBranch()) });
|
|
197
|
+
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()) });
|
|
198
|
+
if (planRun && Array.isArray(snapshot.connectors))
|
|
199
|
+
planRun.connectors = snapshot.connectors;
|
|
126
200
|
subagentParent = true;
|
|
127
201
|
}
|
|
128
202
|
if (planRun && event.messageType === 'user') {
|
|
@@ -130,24 +204,31 @@ export default async function (pi, planRun, getSession) {
|
|
|
130
204
|
event.systemPromptOptions.sections.plan = PLAN_INSTRUCTIONS;
|
|
131
205
|
else
|
|
132
206
|
delete event.systemPromptOptions.sections.plan;
|
|
207
|
+
if (planRun.subagentProfiles)
|
|
208
|
+
event.systemPromptOptions.sections.subagents = planRun.active ? planRun.subagentProfiles.plan : planRun.subagentProfiles.normal;
|
|
133
209
|
}
|
|
134
210
|
const bang = event.messageType === 'user' ? /^\s*!\s*([\s\S]+)$/u.exec(event.prompt) : null;
|
|
135
211
|
if (!bang)
|
|
136
212
|
return;
|
|
137
|
-
const
|
|
213
|
+
const initial = parseInitialBashCommand(bang[1]);
|
|
214
|
+
const command = initial.command;
|
|
215
|
+
const args = { command, ...(initial.workspaceWriteScope === 'outside' ? { workspaceWriteScope: 'outside' } : {}) };
|
|
138
216
|
const id = `initial-bash:${begun.runId}`;
|
|
139
217
|
const started = Date.now();
|
|
140
|
-
await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify(
|
|
218
|
+
await terminalCall('tool', { phase: 'start', id, name: 'bash', summary: command.slice(0, 200), input: JSON.stringify(args) });
|
|
141
219
|
let refusal;
|
|
142
220
|
let result;
|
|
143
|
-
if (
|
|
221
|
+
if (!command) {
|
|
222
|
+
refusal = 'Enter a Bash command after !outside.';
|
|
223
|
+
}
|
|
224
|
+
else if (planRun?.active) {
|
|
144
225
|
refusal = 'Bash commands are disabled while a plan request is running.';
|
|
145
226
|
}
|
|
146
227
|
else {
|
|
147
228
|
try {
|
|
148
|
-
const policy = await terminalCall('policy', { tool: 'bash', args
|
|
229
|
+
const policy = await terminalCall('policy', { tool: 'bash', args });
|
|
149
230
|
if (policy.requiresApproval) {
|
|
150
|
-
const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args
|
|
231
|
+
const approval = await terminalCall('approval', { requestId: id, tool: 'bash', args });
|
|
151
232
|
if (approval.decision !== 'approved')
|
|
152
233
|
refusal = 'The user denied this Bash command.';
|
|
153
234
|
}
|
|
@@ -159,7 +240,7 @@ export default async function (pi, planRun, getSession) {
|
|
|
159
240
|
if (!session)
|
|
160
241
|
refusal = 'The native session is unavailable.';
|
|
161
242
|
else
|
|
162
|
-
result = await session.executeBash(command, undefined, { id });
|
|
243
|
+
result = await runWithWorkspaceWriteScope(initial.workspaceWriteScope, () => withCommandBudget(() => session.executeBash(command, undefined, { id, operations: workspaceBash }), () => session.abortBash(), DEFAULT_COMMAND_TIMEOUT_SECONDS));
|
|
163
244
|
}
|
|
164
245
|
}
|
|
165
246
|
catch (error) {
|
|
@@ -240,21 +321,61 @@ export default async function (pi, planRun, getSession) {
|
|
|
240
321
|
pi.on('turn_start', async () => {
|
|
241
322
|
requestStartedAt = Date.now();
|
|
242
323
|
});
|
|
243
|
-
pi.on('tool_call', async (event) => {
|
|
324
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
244
325
|
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.`;
|
|
326
|
+
const reason = planRefusal(event.toolName, new Set(), event.input) ?? `Plan mode is research-only: ${event.toolName} is not available in this session.`;
|
|
246
327
|
return { block: true, reason };
|
|
247
328
|
}
|
|
329
|
+
if (planRun?.active && event.toolName === SPAWN_SUBAGENT) {
|
|
330
|
+
try {
|
|
331
|
+
const eligibility = await controlCall('terminal.subagents.planSafe', { input: event.input });
|
|
332
|
+
if (!eligibility.allowed)
|
|
333
|
+
return { block: true, reason: eligibility.reason ?? 'This profile is not eligible for plan-mode research.' };
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
return { block: true, reason: `Plan-mode research eligibility could not be verified: ${error instanceof Error ? error.message : 'controller unavailable'}` };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (planRun?.active) {
|
|
340
|
+
const reason = planRefusal(event.toolName, new Set(PLAN_RESEARCH_TOOLS), event.input);
|
|
341
|
+
if (reason)
|
|
342
|
+
return { block: true, reason };
|
|
343
|
+
}
|
|
344
|
+
let authorization;
|
|
345
|
+
if ((event.toolName === 'edit' || event.toolName === 'write') && typeof event.input.path === 'string') {
|
|
346
|
+
try {
|
|
347
|
+
if (!workspaceRoot)
|
|
348
|
+
throw new Error('The active project workspace is unavailable.');
|
|
349
|
+
const inspection = inspectWorkspaceWrite(workspaceRoot, ctx.cwd, event.toolName, event.input);
|
|
350
|
+
Object.assign(event.input, inspection.input);
|
|
351
|
+
if (!inspection.path)
|
|
352
|
+
throw new Error('The native file-write target could not be verified.');
|
|
353
|
+
authorization = {
|
|
354
|
+
workspaceRoot,
|
|
355
|
+
cwd: ctx.cwd,
|
|
356
|
+
path: inspection.path,
|
|
357
|
+
outside: inspection.outside,
|
|
358
|
+
authorized: false
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
return { block: true, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.' };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
248
365
|
try {
|
|
249
366
|
const policy = await controlCall('terminal.policy', { tool: event.toolName, args: event.input });
|
|
250
367
|
if (policy.requiresApproval) {
|
|
251
368
|
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.' };
|
|
369
|
+
if (approval.decision !== 'approved')
|
|
370
|
+
return { block: true, reason: 'The user denied this action. Ask how they would like to proceed.' };
|
|
255
371
|
}
|
|
256
|
-
if (!policy.allowed)
|
|
372
|
+
else if (!policy.allowed) {
|
|
257
373
|
return { block: true, reason: policy.reason ?? 'Outside what this session may do.' };
|
|
374
|
+
}
|
|
375
|
+
if (authorization) {
|
|
376
|
+
authorization.authorized = true;
|
|
377
|
+
writeAuthorizations.set(event.toolCallId, authorization);
|
|
378
|
+
}
|
|
258
379
|
}
|
|
259
380
|
catch (error) {
|
|
260
381
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -287,7 +408,10 @@ export default async function (pi, planRun, getSession) {
|
|
|
287
408
|
}
|
|
288
409
|
}
|
|
289
410
|
subagentParent = false;
|
|
290
|
-
|
|
411
|
+
// A reply that stopped at its work-slice boundary is not a completed task: the record says so,
|
|
412
|
+
// so a host can offer the continuation instead of treating it as done (PROP-0041).
|
|
413
|
+
const reason = sliceState.endedBy ? sliceState.endedBy === 'stuck' ? 'stalled' : 'slice_complete' : undefined;
|
|
414
|
+
await terminalCall('settled', { error: failure, aborted: event.aborted, ...(reason ? { reason } : {}) });
|
|
291
415
|
});
|
|
292
416
|
pi.on('user_bash', async () => {
|
|
293
417
|
throw new Error('Use the ! command composer so this action can be checked.');
|
|
@@ -326,6 +450,19 @@ export default async function (pi, planRun, getSession) {
|
|
|
326
450
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
327
451
|
}
|
|
328
452
|
});
|
|
453
|
+
// A bounded reply records its handoff here; the terminal reply is its own slice.
|
|
454
|
+
let checkpointRecorded = Promise.resolve();
|
|
455
|
+
const checkpoint = checkpointTool({ emit: (_type, payload) => { checkpointRecorded = terminalCall('checkpoint', payload); } }, 1);
|
|
456
|
+
pi.registerTool({
|
|
457
|
+
name: checkpoint.name, label: checkpoint.label, description: checkpoint.description,
|
|
458
|
+
parameters: Type.Unsafe(checkpoint.parameters(Type)),
|
|
459
|
+
async execute(_id, input) {
|
|
460
|
+
checkpointRecorded = Promise.resolve();
|
|
461
|
+
const result = await checkpoint.execute(input);
|
|
462
|
+
await checkpointRecorded;
|
|
463
|
+
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
464
|
+
}
|
|
465
|
+
});
|
|
329
466
|
const ask = askUserTool({ requestAnswers: questions => terminalCall('questions', { questions }, 1_230_000) });
|
|
330
467
|
pi.registerTool({
|
|
331
468
|
name: ask.name, label: ask.label, description: ask.description,
|
|
@@ -335,43 +472,6 @@ export default async function (pi, planRun, getSession) {
|
|
|
335
472
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
336
473
|
}
|
|
337
474
|
});
|
|
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
|
-
});
|
|
475
|
+
// One question tool, as in every structured session: the terminal has no ask_user_question alias (PROP-0039).
|
|
376
476
|
await terminalCall('startup', { phase: 'Pi hooks registered' });
|
|
377
477
|
}
|
|
@@ -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,4 +1,5 @@
|
|
|
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;
|
|
@@ -15,9 +16,17 @@ export interface TerminalSubagentRun {
|
|
|
15
16
|
startedAt: string;
|
|
16
17
|
endedAt: string | null;
|
|
17
18
|
profile?: TerminalSubagentProfile | null;
|
|
19
|
+
/** The pinned account's display label; subagents never switch accounts. */
|
|
20
|
+
accountLabel?: string | null;
|
|
21
|
+
/** Effective launch selection, superseded by provider-reported usage when available. */
|
|
22
|
+
model?: string | null;
|
|
23
|
+
effort?: Effort | null;
|
|
18
24
|
}
|
|
25
|
+
export type TerminalSubagentUsage = Partial<Pick<UsagePayload, 'provider' | 'model' | 'inputTokens' | 'outputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens' | 'costUsd' | 'costSource' | 'requests' | 'contextTokens' | 'contextWindow'>>;
|
|
19
26
|
export interface TerminalSubagentActivity extends TerminalSubagentRun {
|
|
20
27
|
activity: string;
|
|
28
|
+
/** Cumulative run usage only; provider reasoning summaries and request details stay private. */
|
|
29
|
+
usage: TerminalSubagentUsage | null;
|
|
21
30
|
/** How far the child's own checklist is: counts only, never the items. */
|
|
22
31
|
checklist: {
|
|
23
32
|
done: number;
|
|
@@ -27,7 +36,7 @@ export interface TerminalSubagentActivity extends TerminalSubagentRun {
|
|
|
27
36
|
export declare const ACTIVE_SUBAGENT_STATUSES: Set<"queued" | "running" | "waiting_for_approval" | "waiting_for_input" | "interrupted" | "completed" | "failed">;
|
|
28
37
|
export declare function terminalSubagentDuration(activity: TerminalSubagentRun, now?: number): string;
|
|
29
38
|
export declare function terminalSubagentTone(status: RunStatus): 'success' | 'warning' | 'error' | 'muted';
|
|
30
|
-
/** Volatile, read-only activity projection for the
|
|
39
|
+
/** Volatile, read-only activity projection for the latest terminal parent run. */
|
|
31
40
|
export declare class SubagentMonitor {
|
|
32
41
|
private parentRunId;
|
|
33
42
|
private readonly activities;
|