@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,297 @@
|
|
|
1
|
+
// A reply's work slice (PROP-0041): a bounded amount of work, then a handoff checkpoint, then a
|
|
2
|
+
// continuation. The guard here refuses the call that would run past the budget, once with a
|
|
3
|
+
// request for the checkpoint, and ends the reply if the model keeps calling tools. It also names
|
|
4
|
+
// repeated work before it becomes a loop. All decisions are deterministic; no model is consulted.
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { findRun } from "./runStore.js";
|
|
7
|
+
export const WORK_SLICE_EXTENSION = 'work-slice';
|
|
8
|
+
/** The foundation defaults: enough for one meaningful step of work, short enough to report. */
|
|
9
|
+
export const DEFAULT_SLICE_POLICY = {
|
|
10
|
+
enabled: true,
|
|
11
|
+
// Roughly 40 minutes of ordinary work (about 15 s a call), so a checkpoint is a chapter of a
|
|
12
|
+
// task, not an interruption every few edits. A task still gets eight of them before the cap.
|
|
13
|
+
maxToolCalls: 160,
|
|
14
|
+
maxMinutes: 40,
|
|
15
|
+
// A generous cost ceiling, counted on new input, cache writes and output only: heavy reading at
|
|
16
|
+
// a large context can add tens of thousands of tokens a request, so a small cap stopped useful
|
|
17
|
+
// work (see the "Confirm plan model support" thread). 320k is a safety net, not the bound.
|
|
18
|
+
maxNewTokens: 320_000,
|
|
19
|
+
maxSlices: 8,
|
|
20
|
+
continue: true
|
|
21
|
+
};
|
|
22
|
+
const LIMITS = {
|
|
23
|
+
maxToolCalls: { min: 1, max: 2_000 },
|
|
24
|
+
maxMinutes: { min: 1, max: 480 },
|
|
25
|
+
maxNewTokens: { min: 1_000, max: 5_000_000 },
|
|
26
|
+
maxSlices: { min: 1, max: 100 }
|
|
27
|
+
};
|
|
28
|
+
export class WorkSliceError extends Error {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The policy a run asked for, or the defaults. `XINGULAR_WORK_SLICE=off` at launch turns the guard
|
|
32
|
+
* off for every run in this process; nothing is persisted, so restarting without it restores the
|
|
33
|
+
* defaults.
|
|
34
|
+
*/
|
|
35
|
+
export function parseWorkSlice(input) {
|
|
36
|
+
if (input === undefined || input === null) {
|
|
37
|
+
const launch = process.env.XINGULAR_WORK_SLICE?.trim().toLowerCase();
|
|
38
|
+
return { ...DEFAULT_SLICE_POLICY, ...(launch === 'off' || launch === '0' || launch === 'false' ? { enabled: false } : {}) };
|
|
39
|
+
}
|
|
40
|
+
if (typeof input !== 'object' || Array.isArray(input))
|
|
41
|
+
throw new WorkSliceError('workSlice must be an object.');
|
|
42
|
+
const record = input;
|
|
43
|
+
const policy = { ...DEFAULT_SLICE_POLICY };
|
|
44
|
+
for (const key of Object.keys(record)) {
|
|
45
|
+
if (key === 'enabled') {
|
|
46
|
+
if (typeof record.enabled !== 'boolean')
|
|
47
|
+
throw new WorkSliceError('workSlice.enabled must be true or false.');
|
|
48
|
+
policy.enabled = record.enabled;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (key === 'continue') {
|
|
52
|
+
if (typeof record.continue !== 'boolean')
|
|
53
|
+
throw new WorkSliceError('workSlice.continue must be true or false.');
|
|
54
|
+
policy.continue = record.continue;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const limits = LIMITS[key];
|
|
58
|
+
if (!limits)
|
|
59
|
+
throw new WorkSliceError(`workSlice has no field ${key}.`);
|
|
60
|
+
const value = record[key];
|
|
61
|
+
if (typeof value !== 'number' || !Number.isInteger(value))
|
|
62
|
+
throw new WorkSliceError(`workSlice.${key} must be a whole number.`);
|
|
63
|
+
// 0 switches one limit off, as the settings page says.
|
|
64
|
+
if (value === 0) {
|
|
65
|
+
policy[key] = 0;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (value < limits.min || value > limits.max)
|
|
69
|
+
throw new WorkSliceError(`workSlice.${key} must be between ${limits.min} and ${limits.max}, or 0 for no limit.`);
|
|
70
|
+
policy[key] = value;
|
|
71
|
+
}
|
|
72
|
+
return policy;
|
|
73
|
+
}
|
|
74
|
+
export function createSliceState(_policy, slice = 1, now = Date.now()) {
|
|
75
|
+
return { slice, steps: 0, tokens: 0, startedAt: now, lastTool: null, compactions: 0, checkpointWritten: false, endedBy: null, endedByLimit: null, refusedAt: null };
|
|
76
|
+
}
|
|
77
|
+
/** Counters start again for each reply; a terminal session keeps one guard across replies. */
|
|
78
|
+
export function resetSliceState(state, now = Date.now()) {
|
|
79
|
+
state.steps = 0;
|
|
80
|
+
state.tokens = 0;
|
|
81
|
+
state.startedAt = now;
|
|
82
|
+
state.lastTool = null;
|
|
83
|
+
state.compactions = 0;
|
|
84
|
+
state.checkpointWritten = false;
|
|
85
|
+
state.endedBy = null;
|
|
86
|
+
state.endedByLimit = null;
|
|
87
|
+
state.refusedAt = null;
|
|
88
|
+
}
|
|
89
|
+
/** The first limit this reply has passed, named as a person would say it. */
|
|
90
|
+
export function sliceLimitReached(policy, state, now = Date.now()) {
|
|
91
|
+
if (policy.maxToolCalls > 0 && state.steps >= policy.maxToolCalls)
|
|
92
|
+
return `${policy.maxToolCalls} tool calls`;
|
|
93
|
+
if (policy.maxMinutes > 0 && now - state.startedAt >= policy.maxMinutes * 60_000)
|
|
94
|
+
return `${policy.maxMinutes} minutes`;
|
|
95
|
+
if (policy.maxNewTokens > 0 && state.tokens >= policy.maxNewTokens)
|
|
96
|
+
return `${Math.round(policy.maxNewTokens / 1000)}k new tokens`;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/** Tools a reply may still call while it writes its handoff. A plan update waits for the next slice. */
|
|
100
|
+
const HANDOFF_TOOLS = new Set(['checkpoint_write', 'session_name']);
|
|
101
|
+
/** Tools whose repetition is how waiting works, not a loop. */
|
|
102
|
+
const POLLING_TOOLS = new Set(['wait_subagents', 'subagents.list', 'subagents.status']);
|
|
103
|
+
/** Commands that read as a check, for "no progress" and "still failing" rules. */
|
|
104
|
+
export function looksLikeCheck(command) {
|
|
105
|
+
return /\b(test|tests|typecheck|tsc|vitest|jest|cargo|check:|prove|lint|ci|build)\b/u.test(command);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A stable rendering of a value: object keys sorted, so argument order never changes the result.
|
|
109
|
+
* Depth is bounded so a cyclic or absurdly nested input cannot recurse forever.
|
|
110
|
+
*/
|
|
111
|
+
function canonical(value, depth = 0) {
|
|
112
|
+
if (depth > 8)
|
|
113
|
+
return '…';
|
|
114
|
+
if (value === null || typeof value !== 'object')
|
|
115
|
+
return JSON.stringify(value) ?? 'null';
|
|
116
|
+
if (Array.isArray(value))
|
|
117
|
+
return `[${value.map((item) => canonical(item, depth + 1)).join(',')}]`;
|
|
118
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined);
|
|
119
|
+
entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
120
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonical(item, depth + 1)}`).join(',')}}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A call's identity: its name and every argument, hashed so the key stays small even when an
|
|
124
|
+
* argument is a whole file. A changed argument is a different call; only an identical one repeats.
|
|
125
|
+
* Reading a fixed handful of fields made every `checklist_write` look identical, so a reply that
|
|
126
|
+
* reported its progress was told it was repeating work.
|
|
127
|
+
*/
|
|
128
|
+
const signature = (name, input) => `${name}\u0000${createHash('sha256').update(canonical(input ?? null)).digest('hex')}`;
|
|
129
|
+
const clip = (value, limit) => (value.length <= limit ? value : `${value.slice(0, limit - 1)}…`);
|
|
130
|
+
function handoffRequest(limit, state) {
|
|
131
|
+
return clip(`This reply reached its work-slice limit (${limit}; step ${state.steps}). Call checkpoint_write now with what is done, the single next step, anything blocking, the files you touched and the checks that matter. Then tell the user in a few lines where you stopped. Do not call other tools.`, 600);
|
|
132
|
+
}
|
|
133
|
+
function stalledNote(reason) {
|
|
134
|
+
return clip(`You are repeating work: ${reason}. Do not repeat it. Choose a different approach, read what the earlier attempt returned, or call checkpoint_write with the blocker so the next slice can decide.`, 600);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Bounds one reply and names repeated work. The first refusal asks for a checkpoint; a tool call
|
|
138
|
+
* after that refusal ends the reply. A repeated call or failing command is named once, then ends it.
|
|
139
|
+
*/
|
|
140
|
+
export function workSliceGuard(options) {
|
|
141
|
+
const { policy, state } = options;
|
|
142
|
+
const calls = new Map();
|
|
143
|
+
const edits = new Map();
|
|
144
|
+
const failures = new Map();
|
|
145
|
+
let stepsSinceProgress = 0;
|
|
146
|
+
let nudged = false;
|
|
147
|
+
/** Repetition is judged inside one reply; the next reply starts from a clean count. */
|
|
148
|
+
const resetRepetition = () => {
|
|
149
|
+
calls.clear();
|
|
150
|
+
edits.clear();
|
|
151
|
+
failures.clear();
|
|
152
|
+
stepsSinceProgress = 0;
|
|
153
|
+
nudged = false;
|
|
154
|
+
};
|
|
155
|
+
const end = (kind, limit) => {
|
|
156
|
+
state.endedBy = kind;
|
|
157
|
+
state.endedByLimit = limit;
|
|
158
|
+
options.onEnd?.(kind, limit);
|
|
159
|
+
const reason = kind === 'stuck'
|
|
160
|
+
? stalledNote(limit)
|
|
161
|
+
: kind === 'compaction-thrash'
|
|
162
|
+
? handoffRequest('three context compactions in one reply', state)
|
|
163
|
+
: handoffRequest(limit, state);
|
|
164
|
+
return { block: true, terminate: true, reason };
|
|
165
|
+
};
|
|
166
|
+
const repeated = (name, input) => {
|
|
167
|
+
if (POLLING_TOOLS.has(name))
|
|
168
|
+
return null;
|
|
169
|
+
const key = signature(name, input);
|
|
170
|
+
const count = (calls.get(key) ?? 0) + 1;
|
|
171
|
+
calls.set(key, count);
|
|
172
|
+
if (count < 3)
|
|
173
|
+
return null;
|
|
174
|
+
return name === 'bash' ? `the same Bash command ${count} times (steps ending at ${state.steps})` : `the same ${name} call ${count} times (steps ending at ${state.steps})`;
|
|
175
|
+
};
|
|
176
|
+
const failing = (name, input) => {
|
|
177
|
+
if (name !== 'bash')
|
|
178
|
+
return null;
|
|
179
|
+
const command = input && typeof input === 'object' ? String(input.command ?? '') : '';
|
|
180
|
+
if (!command || !looksLikeCheck(command))
|
|
181
|
+
return null;
|
|
182
|
+
const count = failures.get(command) ?? 0;
|
|
183
|
+
return count >= 2 ? `the check ${command} failed ${count} times` : null;
|
|
184
|
+
};
|
|
185
|
+
return (pi) => {
|
|
186
|
+
pi.on('agent_start', () => {
|
|
187
|
+
resetSliceState(state);
|
|
188
|
+
resetRepetition();
|
|
189
|
+
});
|
|
190
|
+
pi.on('message_end', (event) => {
|
|
191
|
+
const usage = event.message.usage;
|
|
192
|
+
if (event.message.role !== 'assistant' || !usage)
|
|
193
|
+
return;
|
|
194
|
+
// New tokens only: the cached prefix was sent before and is not new context or new cost.
|
|
195
|
+
state.tokens += (usage.input ?? 0) + (usage.cacheWrite ?? 0) + (usage.output ?? 0);
|
|
196
|
+
});
|
|
197
|
+
pi.on('tool_result', (event) => {
|
|
198
|
+
if (event.toolName === 'checkpoint_write' && !event.isError) {
|
|
199
|
+
state.checkpointWritten = true;
|
|
200
|
+
stepsSinceProgress = 0;
|
|
201
|
+
nudged = false;
|
|
202
|
+
}
|
|
203
|
+
if (event.toolName !== 'bash')
|
|
204
|
+
return;
|
|
205
|
+
const command = event.input && typeof event.input === 'object' ? String(event.input.command ?? '') : '';
|
|
206
|
+
if (!command || !looksLikeCheck(command))
|
|
207
|
+
return;
|
|
208
|
+
if (event.isError) {
|
|
209
|
+
failures.set(command, (failures.get(command) ?? 0) + 1);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// A passing check is progress: the repetition rules start over.
|
|
213
|
+
failures.delete(command);
|
|
214
|
+
edits.clear();
|
|
215
|
+
calls.clear();
|
|
216
|
+
stepsSinceProgress = 0;
|
|
217
|
+
nudged = false;
|
|
218
|
+
});
|
|
219
|
+
pi.on('tool_call', (event) => {
|
|
220
|
+
const name = event.toolName;
|
|
221
|
+
state.steps += 1;
|
|
222
|
+
state.lastTool = name;
|
|
223
|
+
const handoff = HANDOFF_TOOLS.has(name);
|
|
224
|
+
// A handoff is always callable; after the slice ended it is the only thing that is.
|
|
225
|
+
if (state.endedBy) {
|
|
226
|
+
return handoff ? undefined : { block: true, terminate: true, reason: 'This reply is over. Its checkpoint is recorded; nothing else runs.' };
|
|
227
|
+
}
|
|
228
|
+
if (state.compactions >= 3) {
|
|
229
|
+
if (handoff) {
|
|
230
|
+
state.endedBy = 'compaction-thrash';
|
|
231
|
+
state.endedByLimit = 'three compactions';
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return end('compaction-thrash', 'three compactions');
|
|
235
|
+
}
|
|
236
|
+
const limit = sliceLimitReached(policy, state);
|
|
237
|
+
if (limit) {
|
|
238
|
+
// The slice ends here; the model gets one wrap-up request to write the handoff. A handoff
|
|
239
|
+
// call is never refused, including when it is the very first call past the budget.
|
|
240
|
+
state.endedBy = 'budget';
|
|
241
|
+
state.endedByLimit = limit;
|
|
242
|
+
state.refusedAt ??= state.steps;
|
|
243
|
+
stepsSinceProgress = 0;
|
|
244
|
+
return handoff ? undefined : { block: true, reason: handoffRequest(limit, state) };
|
|
245
|
+
}
|
|
246
|
+
if (handoff)
|
|
247
|
+
return undefined;
|
|
248
|
+
const edit = name === 'edit' || name === 'write';
|
|
249
|
+
const path = edit && event.input && typeof event.input === 'object' ? String(event.input.path ?? '') : '';
|
|
250
|
+
if (edit && path) {
|
|
251
|
+
const count = (edits.get(path) ?? 0) + 1;
|
|
252
|
+
edits.set(path, count);
|
|
253
|
+
stepsSinceProgress = 0;
|
|
254
|
+
if (count >= 4) {
|
|
255
|
+
const reason = `${path} was edited ${count} times with no passing check between`;
|
|
256
|
+
if (nudged)
|
|
257
|
+
return end('stuck', reason);
|
|
258
|
+
nudged = true;
|
|
259
|
+
return { block: true, reason: stalledNote(reason) };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
stepsSinceProgress += 1;
|
|
264
|
+
}
|
|
265
|
+
const repeat = repeated(name, event.input);
|
|
266
|
+
const failure = failing(name, event.input);
|
|
267
|
+
const noProgress = stepsSinceProgress >= 25 ? `${stepsSinceProgress} steps with no file change and no passing check` : null;
|
|
268
|
+
const reason = repeat ?? failure ?? noProgress;
|
|
269
|
+
if (!reason)
|
|
270
|
+
return undefined;
|
|
271
|
+
if (nudged)
|
|
272
|
+
return end('stuck', reason);
|
|
273
|
+
nudged = true;
|
|
274
|
+
stepsSinceProgress = 0;
|
|
275
|
+
return { block: true, reason: stalledNote(reason) };
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Which slice of its session a run is, from 1. Counts the recorded continuation chain, so it
|
|
281
|
+
* survives a restart and needs no extra field on the run record.
|
|
282
|
+
*/
|
|
283
|
+
export function sliceOf(root, runId, limit = 100) {
|
|
284
|
+
let current = runId;
|
|
285
|
+
let count = 0;
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
while (current && count < limit && !seen.has(current)) {
|
|
288
|
+
seen.add(current);
|
|
289
|
+
count += 1;
|
|
290
|
+
current = findRun(root, current)?.resumedFromRunId ?? null;
|
|
291
|
+
}
|
|
292
|
+
return Math.max(1, count);
|
|
293
|
+
}
|
|
294
|
+
/** The reason a checkpoint the harness derives (no model handoff) records. */
|
|
295
|
+
export function derivedReason(end) {
|
|
296
|
+
return end ?? 'requested';
|
|
297
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RunRecord } from "./contracts.js";
|
|
2
|
+
import type { ScopeDecision } from "./policy.js";
|
|
3
|
+
export declare const WORKSPACE_FILE_WRITE_TOOLS: Set<string>;
|
|
4
|
+
export interface WorkspaceWriteInspection {
|
|
5
|
+
input: unknown;
|
|
6
|
+
path: string | null;
|
|
7
|
+
outside: boolean;
|
|
8
|
+
reason: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** Canonicalize native file-write arguments and identify an explicit workspace escape. */
|
|
11
|
+
export declare function inspectWorkspaceWrite(workspaceRoot: string, cwd: string, tool: string, input: unknown): WorkspaceWriteInspection;
|
|
12
|
+
/** Adds the mandatory per-operation boundary without allowing it to waive host hard denials. */
|
|
13
|
+
export declare function combineWorkspaceScope(scope: ScopeDecision, inspection: WorkspaceWriteInspection): ScopeDecision;
|
|
14
|
+
/** Resolve a root for pre-boundary run records, then inspect the write using the pinned directory. */
|
|
15
|
+
export declare function inspectRunWorkspaceWrite(run: RunRecord, tool: string, input: unknown): WorkspaceWriteInspection;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isWithinWorkspace, resolveWorkspaceRoot, resolveWritePath, verifyWorkspaceRoot } from "../core/workspace.js";
|
|
2
|
+
export const WORKSPACE_FILE_WRITE_TOOLS = new Set(['edit', 'write']);
|
|
3
|
+
/** Canonicalize native file-write arguments and identify an explicit workspace escape. */
|
|
4
|
+
export function inspectWorkspaceWrite(workspaceRoot, cwd, tool, input) {
|
|
5
|
+
if (!WORKSPACE_FILE_WRITE_TOOLS.has(tool.toLowerCase()) || !input || typeof input !== 'object' || Array.isArray(input)) {
|
|
6
|
+
return { input, path: null, outside: false, reason: null };
|
|
7
|
+
}
|
|
8
|
+
const args = input;
|
|
9
|
+
if (typeof args.path !== 'string')
|
|
10
|
+
return { input, path: null, outside: false, reason: null };
|
|
11
|
+
const root = verifyWorkspaceRoot(workspaceRoot);
|
|
12
|
+
const path = resolveWritePath(args.path, cwd);
|
|
13
|
+
const outside = !isWithinWorkspace(root, path);
|
|
14
|
+
return {
|
|
15
|
+
input: { ...args, path },
|
|
16
|
+
path,
|
|
17
|
+
outside,
|
|
18
|
+
reason: outside ? `Write target ${path} is outside the active project workspace ${root}.` : null
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Adds the mandatory per-operation boundary without allowing it to waive host hard denials. */
|
|
22
|
+
export function combineWorkspaceScope(scope, inspection) {
|
|
23
|
+
if (!inspection.outside)
|
|
24
|
+
return scope;
|
|
25
|
+
if (!scope.allowed && (scope.automatic || !scope.requiresApproval))
|
|
26
|
+
return scope;
|
|
27
|
+
return {
|
|
28
|
+
...scope,
|
|
29
|
+
allowed: false,
|
|
30
|
+
requiresApproval: true,
|
|
31
|
+
humanOnly: true,
|
|
32
|
+
allowGlobalApproval: scope.humanOnly ? scope.allowGlobalApproval : true,
|
|
33
|
+
reason: inspection.reason
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Resolve a root for pre-boundary run records, then inspect the write using the pinned directory. */
|
|
37
|
+
export function inspectRunWorkspaceWrite(run, tool, input) {
|
|
38
|
+
const cwd = run.workingDirectory;
|
|
39
|
+
const root = run.workspaceRoot ?? (cwd ? resolveWorkspaceRoot(cwd) : null);
|
|
40
|
+
if (!root || !cwd) {
|
|
41
|
+
if (WORKSPACE_FILE_WRITE_TOOLS.has(tool.toLowerCase()))
|
|
42
|
+
throw new Error('The active project workspace could not be resolved; file writes are blocked.');
|
|
43
|
+
return { input, path: null, outside: false, reason: null };
|
|
44
|
+
}
|
|
45
|
+
return inspectWorkspaceWrite(root, cwd, tool, input);
|
|
46
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { describePlugin } from "../plugins/surfaces.js";
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
5
|
import { invalid } from "../core/errors.js";
|
|
@@ -46,7 +47,7 @@ export class BrowserControl {
|
|
|
46
47
|
])
|
|
47
48
|
add({ id: `foundation:${id}`, section: 'foundation', name: name, description: description, owner: 'Foundation', origin: 'Built-in', status: 'Read-only' });
|
|
48
49
|
for (const plugin of plugins) {
|
|
49
|
-
add({ id: `plugin:${plugin.id}`, section: 'plugins', name: plugin.title, description: plugin
|
|
50
|
+
add({ id: `plugin:${plugin.id}`, section: 'plugins', name: plugin.title, description: describePlugin(plugin),
|
|
50
51
|
owner: plugin.title, origin: plugin.source, status: plugin.required ? 'Required' : plugin.trust === 'untrusted' ? 'Needs trust' : 'Saved configuration',
|
|
51
52
|
pluginId: plugin.id, path: plugin.path ?? undefined, required: plugin.required });
|
|
52
53
|
for (const entry of plugin.contributions) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type RunEvent, type RunRecord } from "../sessions/contracts.js";
|
|
2
2
|
import type { ActiveRun, ApprovalDecision, QuestionAnswer } from "../sessions/registry.js";
|
|
3
|
-
import { type TerminalSubagentActivity, type TerminalSubagentProfile } from "./subagentMonitor.js";
|
|
3
|
+
import { type TerminalSubagentActivity, type TerminalSubagentProfile, type TerminalSubagentRun } from "./subagentMonitor.js";
|
|
4
4
|
export interface PolicyReply {
|
|
5
5
|
allowed: boolean;
|
|
6
6
|
reason?: string | null;
|
|
7
|
+
workspaceRoot?: string;
|
|
7
8
|
requiresApproval?: boolean;
|
|
8
9
|
rememberable?: boolean;
|
|
9
10
|
standing?: 'full' | 'switch' | 'none';
|
|
@@ -19,6 +20,12 @@ export interface ControllerHooks {
|
|
|
19
20
|
}): ActiveRun;
|
|
20
21
|
/** Whether a native tool call may run. */
|
|
21
22
|
policy(run: ActiveRun, tool: string, args: unknown): Promise<PolicyReply>;
|
|
23
|
+
/** The workspace root pinned on the run record. */
|
|
24
|
+
workspaceRoot(run: ActiveRun): string;
|
|
25
|
+
/** One-shot approval before an explicitly broad Bash command runs. */
|
|
26
|
+
workspaceEscape(run: ActiveRun, tool: string, args: unknown, reason: string): Promise<boolean>;
|
|
27
|
+
/** Whether Full Permission may bypass the filesystem boundary for this Bash command. */
|
|
28
|
+
workspaceBoundaryBypass(run: ActiveRun, tool: string, args: unknown): Promise<boolean>;
|
|
22
29
|
/** Saves a user-selected tool grant in the shared local approval store. */
|
|
23
30
|
allowToolGlobally(target: string): void;
|
|
24
31
|
/** The native conversation identity changed. */
|
|
@@ -37,6 +44,7 @@ export declare class TerminalController {
|
|
|
37
44
|
private run;
|
|
38
45
|
private readonly pending;
|
|
39
46
|
private readonly subagents;
|
|
47
|
+
private subagentParentRunId;
|
|
40
48
|
private ready;
|
|
41
49
|
private disposed;
|
|
42
50
|
private dirty;
|
|
@@ -67,8 +75,8 @@ export declare class TerminalController {
|
|
|
67
75
|
/** Project registry-owned questions, including grouped requests, into the terminal inbox. */
|
|
68
76
|
observe(event: RunEvent, descendant?: boolean): void;
|
|
69
77
|
/** A child event that belongs to the active terminal parent run. */
|
|
70
|
-
observeSubagent(parentRunId: string, run: RunRecord, event: RunEvent, profile?: TerminalSubagentProfile | null): void;
|
|
71
|
-
/** Read-only child activity for the
|
|
78
|
+
observeSubagent(parentRunId: string, run: RunRecord, event: RunEvent, profile?: TerminalSubagentProfile | null, runtime?: Pick<TerminalSubagentRun, 'accountLabel' | 'model' | 'effort'>): void;
|
|
79
|
+
/** Read-only child activity for the latest parent run, retained until another run begins. */
|
|
72
80
|
subagentSnapshot(): TerminalSubagentActivity[];
|
|
73
81
|
/** The socket grants this interface only to the process that owns terminal input. */
|
|
74
82
|
control(call: string, data: Record<string, any>): Promise<unknown>;
|
|
@@ -14,6 +14,7 @@ export class TerminalController {
|
|
|
14
14
|
run = null;
|
|
15
15
|
pending = new Map();
|
|
16
16
|
subagents = new SubagentMonitor();
|
|
17
|
+
subagentParentRunId = null;
|
|
17
18
|
ready = false;
|
|
18
19
|
disposed = false;
|
|
19
20
|
dirty = false;
|
|
@@ -52,6 +53,7 @@ export class TerminalController {
|
|
|
52
53
|
throw new Error('The native prompt did not match the reserved delivery. Inspect the terminal before continuing.');
|
|
53
54
|
const run = this.hooks.begin({ reservedRunId: delivery?.runId ?? null, nativeTurnId, text });
|
|
54
55
|
this.run = run;
|
|
56
|
+
this.subagentParentRunId = run.runId;
|
|
55
57
|
this.subagents.begin(run.runId);
|
|
56
58
|
run.onInterrupt(() => void this.commands.push({ type: 'interrupt' }));
|
|
57
59
|
if (text)
|
|
@@ -60,10 +62,8 @@ export class TerminalController {
|
|
|
60
62
|
this.delivery = null;
|
|
61
63
|
return run;
|
|
62
64
|
}
|
|
63
|
-
finish(outcome, message) {
|
|
64
|
-
|
|
65
|
-
this.run?.finish(outcome, message ? { code: 'terminal_attention', message } : undefined);
|
|
66
|
-
this.subagents.clear(runId);
|
|
65
|
+
finish(outcome, message, reason) {
|
|
66
|
+
this.run?.finish(outcome, message ? { code: 'terminal_attention', message } : undefined, reason);
|
|
67
67
|
this.run = null;
|
|
68
68
|
this.seen.clear();
|
|
69
69
|
this.pending.clear();
|
|
@@ -133,10 +133,11 @@ export class TerminalController {
|
|
|
133
133
|
const payload = event.payload;
|
|
134
134
|
this.pending.set(String(payload.questionId), { ...payload, type: 'question', requestId: payload.questionId, runId: event.runId });
|
|
135
135
|
}
|
|
136
|
-
else if (
|
|
136
|
+
else if (event.type === 'approval.requested') {
|
|
137
137
|
const payload = event.payload;
|
|
138
|
-
if (!payload.automatic)
|
|
138
|
+
if (!payload.automatic && (descendant || payload.target === 'workspace-write')) {
|
|
139
139
|
this.pending.set(String(payload.approvalId), { ...payload, type: 'approval', requestId: payload.approvalId, runId: event.runId });
|
|
140
|
+
}
|
|
140
141
|
}
|
|
141
142
|
else if (event.type === 'approval.resolved') {
|
|
142
143
|
this.pending.delete(String(event.payload.approvalId));
|
|
@@ -146,7 +147,7 @@ export class TerminalController {
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
/** A child event that belongs to the active terminal parent run. */
|
|
149
|
-
observeSubagent(parentRunId, run, event, profile = null) {
|
|
150
|
+
observeSubagent(parentRunId, run, event, profile = null, runtime = {}) {
|
|
150
151
|
if (this.disposed || this.activeRun?.runId !== parentRunId || run.relation !== 'subagent')
|
|
151
152
|
return;
|
|
152
153
|
this.subagents.observe(parentRunId, {
|
|
@@ -158,13 +159,13 @@ export class TerminalController {
|
|
|
158
159
|
status: runStatusSchema.parse(run.status),
|
|
159
160
|
startedAt: run.startedAt,
|
|
160
161
|
endedAt: run.endedAt,
|
|
161
|
-
profile
|
|
162
|
+
profile,
|
|
163
|
+
...runtime
|
|
162
164
|
}, event);
|
|
163
165
|
}
|
|
164
|
-
/** Read-only child activity for the
|
|
166
|
+
/** Read-only child activity for the latest parent run, retained until another run begins. */
|
|
165
167
|
subagentSnapshot() {
|
|
166
|
-
|
|
167
|
-
return run ? this.subagents.snapshot(run.runId) : [];
|
|
168
|
+
return this.subagentParentRunId ? this.subagents.snapshot(this.subagentParentRunId) : [];
|
|
168
169
|
}
|
|
169
170
|
/** The socket grants this interface only to the process that owns terminal input. */
|
|
170
171
|
async control(call, data) {
|
|
@@ -185,6 +186,20 @@ export class TerminalController {
|
|
|
185
186
|
throw new HarnessError('invalid_request', 'An active run and tool name are required.');
|
|
186
187
|
return this.hooks.policy(run, data.tool, data.args ?? {});
|
|
187
188
|
}
|
|
189
|
+
if (call === 'terminal.workspaceBoundaryBypass') {
|
|
190
|
+
const run = this.activeRun;
|
|
191
|
+
if (!run || data.tool !== 'bash')
|
|
192
|
+
throw new HarnessError('invalid_request', 'An active run and Bash command are required.');
|
|
193
|
+
const args = data.args && typeof data.args === 'object' && !Array.isArray(data.args) ? data.args : {};
|
|
194
|
+
return { bypass: await this.hooks.workspaceBoundaryBypass(run, 'bash', args) };
|
|
195
|
+
}
|
|
196
|
+
if (call === 'terminal.workspaceEscape') {
|
|
197
|
+
const run = this.activeRun;
|
|
198
|
+
if (!run || data.tool !== 'bash' || typeof data.reason !== 'string' || !data.reason)
|
|
199
|
+
throw new HarnessError('invalid_request', 'An active run, Bash command, and outside-scope request are required.');
|
|
200
|
+
const args = data.args && typeof data.args === 'object' && !Array.isArray(data.args) ? data.args : {};
|
|
201
|
+
return { approved: await this.hooks.workspaceEscape(run, 'bash', args, data.reason) };
|
|
202
|
+
}
|
|
188
203
|
if (call === 'terminal.approval')
|
|
189
204
|
return this.requestNativeApproval(data);
|
|
190
205
|
if (call === 'terminal.answers' && this.hooks.answerQuestions) {
|
|
@@ -294,6 +309,10 @@ export class TerminalController {
|
|
|
294
309
|
}
|
|
295
310
|
this.ready = true;
|
|
296
311
|
if (typeof data.nativeSessionId === 'string' && data.nativeSessionId) {
|
|
312
|
+
if (this.nativeId !== data.nativeSessionId) {
|
|
313
|
+
this.subagents.clear();
|
|
314
|
+
this.subagentParentRunId = null;
|
|
315
|
+
}
|
|
297
316
|
this.nativeId = data.nativeSessionId;
|
|
298
317
|
this.hooks.native(data.nativeSessionId);
|
|
299
318
|
}
|
|
@@ -301,7 +320,7 @@ export class TerminalController {
|
|
|
301
320
|
}
|
|
302
321
|
if (event === 'begin') {
|
|
303
322
|
const run = this.begin(String(data.turnId ?? randomUUID()), String(data.text ?? ''));
|
|
304
|
-
return { runId: run.runId };
|
|
323
|
+
return { runId: run.runId, workspaceRoot: this.hooks.workspaceRoot(run) };
|
|
305
324
|
}
|
|
306
325
|
if (event === 'shutdown') {
|
|
307
326
|
this.ready = false;
|
|
@@ -309,7 +328,11 @@ export class TerminalController {
|
|
|
309
328
|
return {};
|
|
310
329
|
}
|
|
311
330
|
if (event === 'settled') {
|
|
312
|
-
|
|
331
|
+
// A reply that stopped at its work-slice boundary is interrupted, not completed: the record
|
|
332
|
+
// says `slice_complete` (or `stalled`) so a host can continue it (PROP-0041).
|
|
333
|
+
const reason = typeof data.reason === 'string' ? data.reason : undefined;
|
|
334
|
+
const outcome = data.error ? 'failed' : data.aborted || reason ? 'interrupted' : 'completed';
|
|
335
|
+
this.finish(outcome, data.error ? String(data.error) : undefined, reason);
|
|
313
336
|
return {};
|
|
314
337
|
}
|
|
315
338
|
const run = this.run;
|
|
@@ -356,6 +379,8 @@ export class TerminalController {
|
|
|
356
379
|
run.emit('usage.updated', data);
|
|
357
380
|
else if (event === 'checklist')
|
|
358
381
|
run.emit('checklist.updated', { items: data.items });
|
|
382
|
+
else if (event === 'checkpoint')
|
|
383
|
+
run.emit('checkpoint', data);
|
|
359
384
|
else if (event === 'file')
|
|
360
385
|
run.emit('file.changed', data);
|
|
361
386
|
else if (event === 'message')
|
|
@@ -379,5 +404,7 @@ export class TerminalController {
|
|
|
379
404
|
this.delivery?.reject(new Error('The terminal stopped before prompt acknowledgement.'));
|
|
380
405
|
this.delivery = null;
|
|
381
406
|
this.finish('interrupted');
|
|
407
|
+
this.subagents.clear();
|
|
408
|
+
this.subagentParentRunId = null;
|
|
382
409
|
}
|
|
383
410
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface OpenTuiTheme {
|
|
2
|
+
background: string;
|
|
3
|
+
panel: string;
|
|
4
|
+
/** The translucent layer that dims the console behind a centered page dialog. */
|
|
5
|
+
scrim: string;
|
|
6
|
+
border: string;
|
|
7
|
+
borderFocus: string;
|
|
8
|
+
text: string;
|
|
9
|
+
muted: string;
|
|
10
|
+
dim: string;
|
|
11
|
+
/** The shared selected-row field and its two text tones. */
|
|
12
|
+
selectionBg: string;
|
|
13
|
+
selectionText: string;
|
|
14
|
+
selectionMuted: string;
|
|
15
|
+
questionSelectedBg: string;
|
|
16
|
+
questionActionBg: string;
|
|
17
|
+
questionActionText: string;
|
|
18
|
+
primary: string;
|
|
19
|
+
accent: string;
|
|
20
|
+
success: string;
|
|
21
|
+
warning: string;
|
|
22
|
+
error: string;
|
|
23
|
+
/** The composer's Bash mode marker and rules; the command body keeps the normal text color. */
|
|
24
|
+
bashMode: string;
|
|
25
|
+
/** The composer's rules while plan mode is on. */
|
|
26
|
+
planMode: string;
|
|
27
|
+
/** Image and pasted-text labels in the composer and in sent messages. */
|
|
28
|
+
attachment: string;
|
|
29
|
+
purple: string;
|
|
30
|
+
cyan: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const opentuiTheme: OpenTuiTheme;
|
|
33
|
+
/** The colour of an agent profile's name (PROP-0029); an unknown name falls back to the accent. */
|
|
34
|
+
export declare function profileColor(theme: OpenTuiTheme, name: string): string;
|
|
35
|
+
export declare function blendHexColor(from: string, to: string, amount: number): string;
|
|
36
|
+
export declare function composerRuleColor(theme: OpenTuiTheme, mode: {
|
|
37
|
+
bash: boolean;
|
|
38
|
+
plan: boolean;
|
|
39
|
+
overlay: boolean;
|
|
40
|
+
}): string;
|
|
41
|
+
export declare function noticeColor(theme: OpenTuiTheme, type: string): string;
|
|
42
|
+
export declare function toolColor(theme: OpenTuiTheme, status: 'running' | 'ok' | 'error' | 'pending'): string;
|
|
43
|
+
export declare function thinkingLevelColor(theme: OpenTuiTheme, level: string): string;
|