@xingular/harness 1.2.0 → 1.3.1-unstable.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +10 -5
- package/dist/cli.js +5560 -5056
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +4 -1
- package/dist/sdk/agents/profiles.js +17 -6
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/plugins/builtin.js +3 -3
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +2 -2
- package/dist/sdk/prompt/compile.d.ts +2 -2
- package/dist/sdk/prompt/compile.js +14 -16
- package/dist/sdk/server/methods.js +4 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.js +29 -11
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +194 -54
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +28 -14
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +7 -0
- package/dist/sdk/terminal/terminals.js +86 -14
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.js +14 -4
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +8 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/terminal-opentui.js +1416 -348
- package/dist/terminal-pi.js +315 -126
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +65 -34
- package/docs/architecture/04-prompt.md +8 -6
- package/docs/architecture/06-plugins-and-capabilities.md +10 -6
- package/docs/architecture/07-tools-and-gate.md +31 -5
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +16 -9
- package/docs/architecture/11-terminal.md +51 -26
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +18 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/README.md +10 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +9 -9
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/README.md +2 -2
- package/docs/terminal-tui.md +85 -46
- package/package.json +1 -1
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
|
@@ -4,12 +4,28 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { accessSync, constants, existsSync, lstatSync, mkdtempSync, realpathSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { hostShell, shellArguments } from "./shell.js";
|
|
7
9
|
import { isWithinWorkspace, verifyWorkspaceRoot } from "./workspace.js";
|
|
8
10
|
const MARKER_PREFIX = '__XINGULAR_WORKSPACE_SANDBOX_STARTED_';
|
|
9
11
|
const bashWriteScope = new AsyncLocalStorage();
|
|
10
12
|
export function runWithWorkspaceWriteScope(scope, action) {
|
|
11
13
|
return bashWriteScope.run(scope, action);
|
|
12
14
|
}
|
|
15
|
+
/** Why a Windows command asks first: Windows has no write-boundary sandbox the harness can use. */
|
|
16
|
+
export const WINDOWS_UNSANDBOXED_REASON = 'Windows has no workspace write-boundary sandbox, so this command would run without one. It will not run until this one-call request is approved.';
|
|
17
|
+
const SANDBOX_BACKENDS = {
|
|
18
|
+
darwin: ['/usr/bin/sandbox-exec'],
|
|
19
|
+
linux: ['/usr/bin/bwrap', '/bin/bwrap']
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The operating system's write-boundary sandbox for this platform, or null when it is not
|
|
23
|
+
* installed. Bash cannot run without it, so readiness diagnostics report it before a command runs.
|
|
24
|
+
*/
|
|
25
|
+
export function workspaceSandboxBackend(platform = process.platform) {
|
|
26
|
+
const candidates = SANDBOX_BACKENDS[platform];
|
|
27
|
+
return candidates ? systemExecutable(candidates) : null;
|
|
28
|
+
}
|
|
13
29
|
function systemExecutable(candidates) {
|
|
14
30
|
for (const candidate of candidates) {
|
|
15
31
|
try {
|
|
@@ -29,6 +45,18 @@ function shellPath() {
|
|
|
29
45
|
throw new Error('The workspace write-boundary sandbox requires Bash, but no Bash executable was found.');
|
|
30
46
|
return shell;
|
|
31
47
|
}
|
|
48
|
+
function requireShell(resolve) {
|
|
49
|
+
const shell = resolve();
|
|
50
|
+
if (!shell) {
|
|
51
|
+
throw new Error(process.platform === 'win32'
|
|
52
|
+
? 'No shell was found: neither Git Bash nor PowerShell is available. Bash was not run.'
|
|
53
|
+
: 'No Bash executable was found. Bash was not run.');
|
|
54
|
+
}
|
|
55
|
+
return shell;
|
|
56
|
+
}
|
|
57
|
+
function unconfinedCommand(shell, command) {
|
|
58
|
+
return { executable: shell.executable, args: shellArguments(shell, command), marker: '' };
|
|
59
|
+
}
|
|
32
60
|
function sbplString(value) {
|
|
33
61
|
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`;
|
|
34
62
|
}
|
|
@@ -72,13 +100,13 @@ export function workspaceSandboxCommand(platform, workspaceRoot, cwd, command, a
|
|
|
72
100
|
return readOnlySandboxCommand(platform, realpathSync(readOnlyTempRoot), cwd, shell, wrappedCommand, marker);
|
|
73
101
|
const writeRoots = [...new Set([workspaceRoot, ...additionalWriteRoots].map((root) => realpathSync(root)))].sort((left, right) => left.length - right.length);
|
|
74
102
|
if (platform === 'darwin') {
|
|
75
|
-
const executable =
|
|
103
|
+
const executable = workspaceSandboxBackend(platform);
|
|
76
104
|
if (!executable)
|
|
77
105
|
throw new Error('Workspace write-boundary sandbox unavailable: sandbox-exec was not found. Bash was not run.');
|
|
78
106
|
return { executable, args: ['-p', macSandboxProfile(writeRoots), shell, '-c', wrappedCommand], marker };
|
|
79
107
|
}
|
|
80
108
|
if (platform === 'linux') {
|
|
81
|
-
const executable =
|
|
109
|
+
const executable = workspaceSandboxBackend(platform);
|
|
82
110
|
if (!executable)
|
|
83
111
|
throw new Error('Workspace write-boundary sandbox unavailable: install bubblewrap (bwrap). Bash was not run.');
|
|
84
112
|
return {
|
|
@@ -99,13 +127,13 @@ export function workspaceSandboxCommand(platform, workspaceRoot, cwd, command, a
|
|
|
99
127
|
}
|
|
100
128
|
function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand, marker) {
|
|
101
129
|
if (platform === 'darwin') {
|
|
102
|
-
const executable =
|
|
130
|
+
const executable = workspaceSandboxBackend(platform);
|
|
103
131
|
if (!executable)
|
|
104
132
|
throw new Error('Read-only Bash sandbox unavailable: sandbox-exec was not found. Bash was not run.');
|
|
105
133
|
return { executable, args: ['-p', macReadOnlyProfile(tempRoot), shell, '-c', wrappedCommand], marker };
|
|
106
134
|
}
|
|
107
135
|
if (platform === 'linux') {
|
|
108
|
-
const executable =
|
|
136
|
+
const executable = workspaceSandboxBackend(platform);
|
|
109
137
|
if (!executable)
|
|
110
138
|
throw new Error('Read-only Bash sandbox unavailable: install bubblewrap (bwrap). Bash was not run.');
|
|
111
139
|
return {
|
|
@@ -127,7 +155,12 @@ function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand,
|
|
|
127
155
|
throw new Error(`Read-only Bash sandbox unavailable on ${platform}. Bash was not run.`);
|
|
128
156
|
}
|
|
129
157
|
function killProcess(child) {
|
|
130
|
-
if (child.pid && process.platform
|
|
158
|
+
if (child.pid && process.platform === 'win32') {
|
|
159
|
+
// Git Bash and PowerShell start their own children; end the whole tree.
|
|
160
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (child.pid) {
|
|
131
164
|
try {
|
|
132
165
|
process.kill(-child.pid, 'SIGKILL');
|
|
133
166
|
return;
|
|
@@ -270,9 +303,12 @@ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
270
303
|
if (!isWithinWorkspace(root, canonicalCwd))
|
|
271
304
|
throw new Error('The Bash working directory is outside the active project workspace.');
|
|
272
305
|
const scope = options.workspaceWriteScope?.() ?? bashWriteScope.getStore() ?? 'workspace';
|
|
306
|
+
const platform = options.platform ?? process.platform;
|
|
307
|
+
const shell = options.shell ?? hostShell;
|
|
273
308
|
// Nothing widens a read-only scope: no Full Permission bypass, no outside request.
|
|
274
309
|
if (scope === 'read-only') {
|
|
275
|
-
|
|
310
|
+
if (platform === 'win32')
|
|
311
|
+
throw new Error('Plan-mode Bash needs a read-only sandbox, which Windows does not have. Bash was not run.');
|
|
276
312
|
const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'harness-read-only-bash-')));
|
|
277
313
|
try {
|
|
278
314
|
const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command, [], tempRoot);
|
|
@@ -292,11 +328,15 @@ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
292
328
|
bypass = false;
|
|
293
329
|
}
|
|
294
330
|
if (bypass) {
|
|
295
|
-
const result = await runProcess(
|
|
331
|
+
const result = await runProcess(unconfinedCommand(requireShell(shell), command), canonicalCwd, execOptions, null);
|
|
296
332
|
return { exitCode: result.exitCode };
|
|
297
333
|
}
|
|
298
|
-
|
|
299
|
-
|
|
334
|
+
// Without a sandbox every Windows command is an outside request: a person approves it once.
|
|
335
|
+
if (scope === 'outside' || platform === 'win32') {
|
|
336
|
+
const resolved = requireShell(shell);
|
|
337
|
+
const reason = scope === 'outside'
|
|
338
|
+
? 'This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved.'
|
|
339
|
+
: WINDOWS_UNSANDBOXED_REASON;
|
|
300
340
|
let approved = false;
|
|
301
341
|
try {
|
|
302
342
|
approved = await options.requestWorkspaceWriteScope?.({ command, cwd: canonicalCwd, workspaceRoot: root, reason }) ?? false;
|
|
@@ -306,12 +346,12 @@ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
306
346
|
}
|
|
307
347
|
if (!approved)
|
|
308
348
|
throw new Error(`${reason} Bash was not run.`);
|
|
309
|
-
|
|
310
|
-
|
|
349
|
+
if (scope === 'outside')
|
|
350
|
+
execOptions.onData(Buffer.from('\n[Outside-workspace writes approved for this command; running once.]\n'));
|
|
351
|
+
const result = await runProcess(unconfinedCommand(resolved, command), canonicalCwd, execOptions, null);
|
|
311
352
|
return { exitCode: result.exitCode };
|
|
312
353
|
}
|
|
313
354
|
assertNoExternalHardlinks(root);
|
|
314
|
-
const platform = options.platform ?? process.platform;
|
|
315
355
|
const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command);
|
|
316
356
|
const result = await runProcess(spec, canonicalCwd, execOptions, spec.marker);
|
|
317
357
|
return { exitCode: result.exitCode };
|
|
@@ -7,6 +7,17 @@ export interface WorkspaceWriteAuthorization {
|
|
|
7
7
|
expectedContentHash?: string;
|
|
8
8
|
signal?: AbortSignal;
|
|
9
9
|
}
|
|
10
|
+
export interface AtomicWriteLaunch {
|
|
11
|
+
executable: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* How the atomic write helper starts. macOS and Linux run it inside the write-boundary sandbox.
|
|
17
|
+
* Windows has none, so Node runs the helper directly; the target was still canonicalized and
|
|
18
|
+
* authorized, and the rename replaces the directory entry rather than writing through an alias.
|
|
19
|
+
*/
|
|
20
|
+
export declare function atomicWriteLaunch(platform: NodeJS.Platform, root: string, additionalRoots: readonly string[]): AtomicWriteLaunch;
|
|
10
21
|
declare function writeWorkspaceFile(path: string, content: string): Promise<void>;
|
|
11
22
|
export declare const workspaceWriteOperations: {
|
|
12
23
|
mkdir: (_directory: string) => Promise<void>;
|
|
@@ -52,6 +52,29 @@ try {
|
|
|
52
52
|
throw error
|
|
53
53
|
}
|
|
54
54
|
`;
|
|
55
|
+
/**
|
|
56
|
+
* How the atomic write helper starts. macOS and Linux run it inside the write-boundary sandbox.
|
|
57
|
+
* Windows has none, so Node runs the helper directly; the target was still canonicalized and
|
|
58
|
+
* authorized, and the rename replaces the directory entry rather than writing through an alias.
|
|
59
|
+
*/
|
|
60
|
+
export function atomicWriteLaunch(platform, root, additionalRoots) {
|
|
61
|
+
if (platform === 'win32') {
|
|
62
|
+
// Node needs SystemRoot to start on Windows; keep only what the helper requires.
|
|
63
|
+
const env = {};
|
|
64
|
+
for (const name of ['SystemRoot', 'windir', 'TEMP', 'TMP', 'USERPROFILE']) {
|
|
65
|
+
if (process.env[name])
|
|
66
|
+
env[name] = process.env[name];
|
|
67
|
+
}
|
|
68
|
+
return { executable: process.execPath, args: ['-e', atomicWriteScript], env };
|
|
69
|
+
}
|
|
70
|
+
const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
|
|
71
|
+
const spec = workspaceSandboxCommand(platform, root, root, command, additionalRoots);
|
|
72
|
+
return {
|
|
73
|
+
executable: spec.executable,
|
|
74
|
+
args: spec.args,
|
|
75
|
+
env: { PATH: '/usr/bin:/bin', ...(process.env.HOME ? { HOME: process.env.HOME } : {}) }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
55
78
|
function shellQuote(value) {
|
|
56
79
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
57
80
|
}
|
|
@@ -96,17 +119,12 @@ async function runAtomicWrite(authorization, path, content) {
|
|
|
96
119
|
throw new Error('The write authorization no longer matches the active workspace target.');
|
|
97
120
|
}
|
|
98
121
|
const additionalRoots = outside ? [nearestExistingDirectory(canonicalPath)] : [];
|
|
99
|
-
const
|
|
100
|
-
const spec = workspaceSandboxCommand(process.platform, root, root, command, additionalRoots);
|
|
101
|
-
const env = {
|
|
102
|
-
PATH: '/usr/bin:/bin',
|
|
103
|
-
...(process.env.HOME ? { HOME: process.env.HOME } : {})
|
|
104
|
-
};
|
|
122
|
+
const launch = atomicWriteLaunch(process.platform, root, additionalRoots);
|
|
105
123
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
106
|
-
const child = spawn(
|
|
124
|
+
const child = spawn(launch.executable, launch.args, {
|
|
107
125
|
cwd: root,
|
|
108
126
|
detached: process.platform !== 'win32',
|
|
109
|
-
env,
|
|
127
|
+
env: launch.env,
|
|
110
128
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
129
|
windowsHide: true
|
|
112
130
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { delimiter, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { basename, delimiter, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { hostShell, shellLabel } from "../core/shell.js";
|
|
5
|
+
import { createWorkspaceBashOperations, workspaceSandboxBackend } from "../core/workspaceSandbox.js";
|
|
6
|
+
import { resolveWorkspaceRoot } from "../core/workspace.js";
|
|
3
7
|
import { defaultAssets } from "../terminal/terminals.js";
|
|
4
8
|
import { loadPi, PINNED_PI_VERSION } from "../pi/runtime.js";
|
|
5
9
|
import { list as listPlugins } from "../plugins/api.js";
|
|
@@ -12,18 +16,23 @@ import { BRAVE, TAVILY, EXA } from "../web/search.js";
|
|
|
12
16
|
import { KEYLESS_SEARCH, WEB_PLUGIN } from "../web/tools.js";
|
|
13
17
|
const pass = (evidence) => ({ status: 'pass', evidence });
|
|
14
18
|
const fail = (evidence, remedy) => ({ status: 'fail', evidence, remedy });
|
|
15
|
-
function
|
|
19
|
+
function resolvedExecutable(name) {
|
|
16
20
|
const paths = isAbsolute(name) ? [name] : (process.env.PATH ?? '').split(delimiter).filter(Boolean).map(dir => join(dir, name));
|
|
17
|
-
|
|
21
|
+
for (const path of paths) {
|
|
18
22
|
try {
|
|
19
23
|
accessSync(path, constants.X_OK);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
+
if (statSync(path).isFile())
|
|
25
|
+
return path;
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
catch { /* try the next PATH entry */ }
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
26
30
|
}
|
|
31
|
+
function executable(name) {
|
|
32
|
+
return resolvedExecutable(name) !== null;
|
|
33
|
+
}
|
|
34
|
+
/** One command written by the harness, not the model: it prints a fixed word and touches nothing. */
|
|
35
|
+
const BASH_PROBE_COMMAND = `printf '%s\\n' harness-doctor`;
|
|
27
36
|
export async function builtinChecks(harness, context, secrets, assets = defaultAssets()) {
|
|
28
37
|
const check = (id, category, title, local) => ({ id, category, title, local });
|
|
29
38
|
const account = () => context.accountId ? harness.accounts.find(context.accountId) : null;
|
|
@@ -96,10 +105,60 @@ export async function builtinChecks(harness, context, secrets, assets = defaultA
|
|
|
96
105
|
}), tools: ['read', 'grep', 'find', 'ls', 'write', 'edit']
|
|
97
106
|
},
|
|
98
107
|
{
|
|
99
|
-
...check('native.bash', 'tools', 'Native shell tool', async () =>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
...check('native.bash', 'tools', 'Native shell tool', async () => {
|
|
109
|
+
if (process.platform === 'win32') {
|
|
110
|
+
const shell = hostShell();
|
|
111
|
+
if (!shell)
|
|
112
|
+
return fail('Neither Git Bash nor PowerShell was found.', 'Install Git for Windows, or restore Windows PowerShell.');
|
|
113
|
+
return { status: 'warning', evidence: `Commands run in ${shellLabel(shell)} at ${shell.executable}. Windows has no workspace write-boundary sandbox, so each command asks for approval unless Full Permission is on, and plan mode cannot run commands. No shell command was run.`, remedy: 'Approve each command when asked, or turn on Full Permission for this session.' };
|
|
114
|
+
}
|
|
115
|
+
const shell = resolvedExecutable('bash');
|
|
116
|
+
if (!shell)
|
|
117
|
+
return fail('Bash was not found on the inherited PATH.', 'Verify the shell executable used by Pi.');
|
|
118
|
+
// Bash runs inside the platform's write-boundary sandbox; without that backend every call
|
|
119
|
+
// fails before the command starts, so presence of bash alone is not readiness.
|
|
120
|
+
const backend = workspaceSandboxBackend(process.platform);
|
|
121
|
+
return backend
|
|
122
|
+
? pass(`Bash is executable at ${shell}, and the ${basename(backend)} workspace write-boundary sandbox it runs inside is present. No shell command was run.`)
|
|
123
|
+
: fail(`Bash is executable at ${shell}, but the workspace write-boundary sandbox this platform needs is missing, so every Bash call fails before it runs.`, process.platform === 'linux' ? 'Install bubblewrap (bwrap) so Bash can run inside the workspace boundary.' : 'Restore the platform sandbox executable so Bash can run inside the workspace boundary.');
|
|
124
|
+
}),
|
|
125
|
+
tools: ['bash'],
|
|
126
|
+
live: {
|
|
127
|
+
notice: 'Run one fixed command written by the harness (printf) in the working folder, inside the same workspace write-boundary sandbox as a normal Bash call. No model input, no network request, no file written; only the exit status is reported.',
|
|
128
|
+
run: async (probe) => {
|
|
129
|
+
if (process.platform === 'win32')
|
|
130
|
+
return { status: 'unverified', evidence: 'On Windows every command needs a person to approve it, so the diagnostic does not run one.' };
|
|
131
|
+
const refusal = await harness.sessions.diagnosticGate(probe.run, context.caller).check('bash', { command: BASH_PROBE_COMMAND });
|
|
132
|
+
if (refusal)
|
|
133
|
+
return { status: 'blocked', evidence: `The tool policy refused the diagnostic command: ${refusal}`, remedy: 'Review what the policy allows Bash to do in this session.' };
|
|
134
|
+
const toolCallId = randomUUID();
|
|
135
|
+
probe.run.emit('tool.started', { toolCallId, name: 'bash', summary: 'Synthetic diagnostic command' });
|
|
136
|
+
let output = '';
|
|
137
|
+
try {
|
|
138
|
+
// The same root, boundary, hard-link scan and sandbox a real Bash call takes. A
|
|
139
|
+
// diagnostic never widens that boundary, so Full Permission cannot change this result.
|
|
140
|
+
const workspaceRoot = resolveWorkspaceRoot(context.cwd);
|
|
141
|
+
const { exitCode } = await createWorkspaceBashOperations(workspaceRoot).exec(BASH_PROBE_COMMAND, workspaceRoot, {
|
|
142
|
+
onData: chunk => { if (output.length < 400)
|
|
143
|
+
output += chunk.toString('utf8'); },
|
|
144
|
+
signal: probe.signal,
|
|
145
|
+
timeout: 20
|
|
146
|
+
});
|
|
147
|
+
probe.run.emit('tool.completed', { toolCallId, name: 'bash', ok: exitCode === 0 });
|
|
148
|
+
return exitCode === 0 && output.includes('harness-doctor')
|
|
149
|
+
? pass('A real command ran in the working folder through the workspace sandbox and returned exit status 0 with the expected output.')
|
|
150
|
+
: fail(`The shell returned exit status ${exitCode ?? 'unknown'} for the diagnostic command.`, 'Review the shell executable, the workspace write-boundary sandbox and the working folder permissions.');
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
probe.run.emit('tool.completed', { toolCallId, name: 'bash', ok: false });
|
|
154
|
+
return {
|
|
155
|
+
status: 'blocked',
|
|
156
|
+
evidence: `The command could not start in this working folder, so the shell itself was not exercised: ${error instanceof Error ? error.message : String(error)}`,
|
|
157
|
+
remedy: 'Resolve what stops Bash before it runs (a hard-link alias in the workspace, a missing sandbox backend, or a redirected workspace root), or choose another working folder.'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
103
162
|
},
|
|
104
163
|
check('runtime.storage', 'runtime', 'State and data storage', async () => {
|
|
105
164
|
accessSync(harness.stateDir, constants.R_OK | constants.W_OK);
|
package/dist/sdk/hub/hub.js
CHANGED
|
@@ -316,7 +316,7 @@ export class Hub {
|
|
|
316
316
|
attachments.save(root, choicesBefore);
|
|
317
317
|
if (entry.delivery.kind === 'pi-package')
|
|
318
318
|
for (const pluginId of receipt.resourceIds)
|
|
319
|
-
await packages.
|
|
319
|
+
await packages.removeDirect(this.harness, { pluginId });
|
|
320
320
|
throw error;
|
|
321
321
|
}
|
|
322
322
|
this.harness.bus.emit({ type: 'hub.changed' });
|
|
@@ -404,8 +404,10 @@ export class Hub {
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
if (receipt.entry.delivery.kind === 'pi-package')
|
|
407
|
-
for (const pluginId of receipt.resourceIds)
|
|
408
|
-
await
|
|
407
|
+
for (const pluginId of receipt.resourceIds) {
|
|
408
|
+
if ((await pluginApi.list(this.harness, {})).some((plugin) => plugin.id === pluginId && plugin.install))
|
|
409
|
+
await packages.removeDirect(this.harness, { pluginId });
|
|
410
|
+
}
|
|
409
411
|
writeJson(join(root, 'hub', 'installed.json'), records.filter((item) => item !== receipt));
|
|
410
412
|
committed = true;
|
|
411
413
|
try {
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type { RunCapabilitySnapshot } from "./capabilities/runSnapshot.js";
|
|
|
16
16
|
export type { CallerContext } from "./core/caller.js";
|
|
17
17
|
export { dispatch as harnessCall } from "./tools/dispatch.js";
|
|
18
18
|
export { compile, render, SECTION_ORDER, CONTEXT_MESSAGE_TYPE, CONTEXT_SNAPSHOT_KEY, type ContextPack, type HostSection, type HostToolGroup, type Launch, type Reference, type Section } from "./prompt/compile.js";
|
|
19
|
-
export { runEventSchema, runRecordSchema, isActiveStatus, type
|
|
19
|
+
export { runEventSchema, runRecordSchema, isActiveStatus, type TodoItem, type RunEvent, type RunEventType, type RunRecord } from "./sessions/contracts.js";
|
|
20
20
|
export type { ApprovalFacts, ApprovalPolicy, AutomaticReason } from "./sessions/approvals.js";
|
|
21
21
|
export type { ScopeDecision, ToolCall, ToolPolicy } from "./sessions/policy.js";
|
|
22
22
|
export type { ActiveRun, ApprovalDecision, ApprovalInput, QuestionAnswer, QuestionInput } from "./sessions/registry.js";
|
|
@@ -30,7 +30,7 @@ export type { Effort } from "./sessions/runner.js";
|
|
|
30
30
|
export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE, type HarnessCall, type HarnessToolDefinition } from "./tools/definitions.js";
|
|
31
31
|
export type { ToolProvider, ToolResult, ToolSession, ToolSpec } from "./tools/types.js";
|
|
32
32
|
export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
|
|
33
|
-
export { CHECKLIST_WRITE, checklistOf } from "./tools/
|
|
33
|
+
export { TODO_WRITE, todoOf, CHECKLIST_WRITE, checklistOf } from "./tools/todo.js";
|
|
34
34
|
export { BUILTIN_PROFILES } from "./agents/builtin.js";
|
|
35
35
|
export { PROFILE_COLORS, createProfile, listProfiles, parseProfile, profileDirs, removeProfile, resolveProfile, type AgentProfile, type ProfileColor, type ProfileListing, type ProfileProblem, type ProfileScope, type ProfileSource, type ProfileVariant } from "./agents/profiles.js";
|
|
36
36
|
export { continuationOf, type Continuation, type ContinuationBlock } from "./sessions/continuation.js";
|
package/dist/sdk/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { STANDARD_CONNECTIONS } from "./prompt/standard.js";
|
|
|
25
25
|
export { SideQuestions, SIDE_QUESTION_INSTRUCTION, buildSideQuestionText } from "./sessions/sideQuestions.js";
|
|
26
26
|
export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE } from "./tools/definitions.js";
|
|
27
27
|
export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
|
|
28
|
-
export { CHECKLIST_WRITE, checklistOf } from "./tools/
|
|
28
|
+
export { TODO_WRITE, todoOf, CHECKLIST_WRITE, checklistOf } from "./tools/todo.js";
|
|
29
29
|
export { BUILTIN_PROFILES } from "./agents/builtin.js";
|
|
30
30
|
export { PROFILE_COLORS, createProfile, listProfiles, parseProfile, profileDirs, removeProfile, resolveProfile } from "./agents/profiles.js";
|
|
31
31
|
export { continuationOf } from "./sessions/continuation.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Built-in feature modules register their contributions here; this registry never loads Pi code.
|
|
2
2
|
import { CONNECTION_TOOLS, NATIVE_GROUP } from "../prompt/compile.js";
|
|
3
3
|
import { ASK_USER, askUserTool } from "../tools/askUser.js";
|
|
4
|
-
import {
|
|
4
|
+
import { TODO_WRITE, todoTool } from "../tools/todo.js";
|
|
5
5
|
import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
|
|
6
6
|
import { sliceOf } from "../sessions/workSlice.js";
|
|
7
7
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS } from "../sessions/subagents.js";
|
|
@@ -79,7 +79,7 @@ const WIKI_PLUGIN = {
|
|
|
79
79
|
};
|
|
80
80
|
const HISTORY_PLUGIN = { id: 'history', title: 'History', description: 'Search earlier conversations and inspect recorded run trajectories.', required: 'Earlier runs are how an agent picks up where work stopped.', tools: tools([...RECALL_TOOLS, ...TRAJECTORY_TOOLS]), toolGroups: ['History'] };
|
|
81
81
|
const ASK_USER_PLUGIN = { id: 'ask-user', title: 'Ask the user', description: 'Lets the agent stop and ask a question with choices.', required: 'An agent must be able to stop and ask you.', tools: [{ name: ASK_USER }] };
|
|
82
|
-
const
|
|
82
|
+
const TODO_PLUGIN = { id: 'todo', title: 'Todo', description: 'Lets the agent keep a todo list of the steps of its work, recorded with the run so progress can be shown and picked up again.', required: 'Progress of longer work is kept through it.', tools: [{ name: TODO_WRITE, description: 'Write the todo list' }] };
|
|
83
83
|
const CHECKPOINT_PLUGIN = { id: 'checkpoint', title: 'Checkpoint', description: 'Lets a bounded reply record a handoff at a work-slice boundary so the next slice starts from it.', required: 'A reply that stops at its budget leaves the state the next one resumes from.', tools: [{ name: CHECKPOINT_WRITE, description: 'Write the handoff checkpoint' }] };
|
|
84
84
|
const CONNECTED_APPS_PLUGIN = {
|
|
85
85
|
id: 'connected-apps',
|
|
@@ -130,7 +130,7 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
130
130
|
serverMethods: historyServerMethods
|
|
131
131
|
},
|
|
132
132
|
{ descriptor: ASK_USER_PLUGIN, toolOrder: 40, tools: context => context.harnessTools && context.allowedTools.includes(ASK_USER) ? [askUserTool(context.run)] : [] },
|
|
133
|
-
{ descriptor:
|
|
133
|
+
{ descriptor: TODO_PLUGIN, toolOrder: 50, tools: context => context.harnessTools ? [todoTool(context.run)] : [] },
|
|
134
134
|
{ descriptor: CHECKPOINT_PLUGIN, toolOrder: 51, tools: context => context.harnessTools && context.allowedTools.includes(CHECKPOINT_WRITE) ? [checkpointTool(context.run, () => sliceOf(context.env.root, context.runId))] : [] },
|
|
135
135
|
{
|
|
136
136
|
descriptor: CONNECTED_APPS_PLUGIN,
|
|
@@ -10,11 +10,15 @@ export declare function normalizeSource(input: string): string;
|
|
|
10
10
|
export declare function install(harness: Harness, params: PluginScope & {
|
|
11
11
|
source: string;
|
|
12
12
|
}): Promise<Plugin[]>;
|
|
13
|
-
/** Removes an installed package
|
|
13
|
+
/** Removes an installed package without consulting Hub. Hub calls this while it owns the receipt transaction. */
|
|
14
|
+
export declare function removeDirect(harness: Harness, params: PluginScope & {
|
|
15
|
+
pluginId: string;
|
|
16
|
+
}): Promise<Plugin[]>;
|
|
17
|
+
/** Removes an installed package, delegating Hub-owned packages to their receipt owner. */
|
|
14
18
|
export declare function remove(harness: Harness, params: PluginScope & {
|
|
15
19
|
pluginId: string;
|
|
16
20
|
}): Promise<Plugin[]>;
|
|
17
|
-
/** Fetches
|
|
21
|
+
/** Fetches newer versions of local packages. Hub-owned packages remain pinned to catalog versions. */
|
|
18
22
|
export declare function update(harness: Harness, params: PluginScope & {
|
|
19
23
|
pluginId?: string | null;
|
|
20
24
|
}): Promise<Plugin[]>;
|
|
@@ -75,8 +75,8 @@ function installed(plugins, pluginId) {
|
|
|
75
75
|
throw invalid(`${plugin.title} was not installed as a package, so it cannot be removed or updated here.`);
|
|
76
76
|
return plugin;
|
|
77
77
|
}
|
|
78
|
-
/** Removes an installed package
|
|
79
|
-
export async function
|
|
78
|
+
/** Removes an installed package without consulting Hub. Hub calls this while it owns the receipt transaction. */
|
|
79
|
+
export async function removeDirect(harness, params) {
|
|
80
80
|
const plugin = installed(await list(harness, params), params.pluginId);
|
|
81
81
|
if (plugin.install.kind === 'folder') {
|
|
82
82
|
const trash = join(harness.env.root, 'trash', 'plugins');
|
|
@@ -98,13 +98,37 @@ export async function remove(harness, params) {
|
|
|
98
98
|
changed(harness);
|
|
99
99
|
return list(harness, params);
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
async function hubReceiptForPackage(harness, pluginId) {
|
|
102
|
+
const receipts = (await harness.hub.list()).installed.filter((receipt) => receipt.entry.delivery.kind === 'pi-package' && receipt.resourceIds.includes(pluginId));
|
|
103
|
+
if (receipts.length > 1)
|
|
104
|
+
throw invalid('More than one Hub receipt owns this package. Review the Hub receipts before changing it.');
|
|
105
|
+
return receipts[0];
|
|
106
|
+
}
|
|
107
|
+
/** Removes an installed package, delegating Hub-owned packages to their receipt owner. */
|
|
108
|
+
export async function remove(harness, params) {
|
|
109
|
+
const receipt = await hubReceiptForPackage(harness, params.pluginId);
|
|
110
|
+
if (receipt) {
|
|
111
|
+
await harness.hub.uninstall(receipt.entry.id, receipt.entry.version);
|
|
112
|
+
return list(harness, params);
|
|
113
|
+
}
|
|
114
|
+
return removeDirect(harness, params);
|
|
115
|
+
}
|
|
116
|
+
/** Fetches newer versions of local packages. Hub-owned packages remain pinned to catalog versions. */
|
|
102
117
|
export async function update(harness, params) {
|
|
103
|
-
const
|
|
118
|
+
const plugins = await list(harness, params);
|
|
119
|
+
const plugin = params.pluginId ? installed(plugins, params.pluginId) : null;
|
|
104
120
|
if (plugin?.install.kind === 'folder')
|
|
105
121
|
throw invalid(`${plugin.title} is a folder on this machine. Change its files to update it.`);
|
|
122
|
+
const receipts = (await harness.hub.list()).installed.filter((receipt) => receipt.entry.delivery.kind === 'pi-package');
|
|
123
|
+
const hubOwned = new Set(receipts.flatMap((receipt) => receipt.resourceIds));
|
|
124
|
+
if (plugin && hubOwned.has(plugin.id))
|
|
125
|
+
throw invalid(`${plugin.title} is managed by Hub. Uninstall its receipt before installing another exact catalog version.`);
|
|
126
|
+
const sources = plugin
|
|
127
|
+
? [plugin.install.source]
|
|
128
|
+
: [...new Set(plugins.filter((candidate) => candidate.install && candidate.install.kind !== 'folder' && !hubOwned.has(candidate.id)).map((candidate) => candidate.install.source))];
|
|
106
129
|
const { packages } = await manager(harness);
|
|
107
|
-
|
|
130
|
+
for (const source of sources)
|
|
131
|
+
await withoutInstallScripts(() => packages.update(source));
|
|
108
132
|
changed(harness);
|
|
109
133
|
return list(harness, params);
|
|
110
134
|
}
|
|
@@ -6,7 +6,7 @@ import { charCount } from "../core/text.js";
|
|
|
6
6
|
import { allowed } from "../mcp/connections.js";
|
|
7
7
|
import { BUILT_IN_DYNAMIC, compile, toolGroups } from "../prompt/compile.js";
|
|
8
8
|
import { ASK_USER } from "../tools/askUser.js";
|
|
9
|
-
import {
|
|
9
|
+
import { TODO_WRITE } from "../tools/todo.js";
|
|
10
10
|
import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
|
|
11
11
|
import { VARIANT_PLUGIN } from "./builtin.js";
|
|
12
12
|
const size = (path) => {
|
|
@@ -68,7 +68,7 @@ export function inventory(input) {
|
|
|
68
68
|
}
|
|
69
69
|
const names = [...input.tools];
|
|
70
70
|
const extensionToolNames = new Set(input.plugins.filter((plugin) => ['project', 'package', 'shared', 'home'].includes(plugin.source)).flatMap((plugin) => plugin.contributions.filter((entry) => entry.kind === 'tool' && entry.state === 'on').map((entry) => entry.name)));
|
|
71
|
-
for (const name of [ASK_USER,
|
|
71
|
+
for (const name of [ASK_USER, TODO_WRITE, ...DEFAULT_TOOL_NAMES, ...(input.connections ? ['mcp_list_tools', 'mcp_call_tool'] : [])])
|
|
72
72
|
if (!names.includes(name))
|
|
73
73
|
names.push(name);
|
|
74
74
|
for (const name of names) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CallerContext } from "../core/caller.js";
|
|
2
2
|
import type { HarnessEnv } from "../core/env.js";
|
|
3
3
|
import type { Materialised } from "../variant/materialise.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { TodoItem } from "../sessions/contracts.js";
|
|
5
5
|
import { type StandardSession } from "./standard.js";
|
|
6
6
|
export declare const REFERENCES_BUDGET = 12000;
|
|
7
7
|
/** Where the harness's own sections sit; an application's sections are placed among them by `order`. */
|
|
@@ -135,7 +135,7 @@ export declare function clip(text: string, limit: number): string;
|
|
|
135
135
|
/** A bounded, request-scoped checkpoint reintroduced after context compaction. */
|
|
136
136
|
export declare function renderCompactionCheckpoint(input: {
|
|
137
137
|
goal: string;
|
|
138
|
-
|
|
138
|
+
todo: readonly TodoItem[];
|
|
139
139
|
planInstructions?: string | null;
|
|
140
140
|
}): string | null;
|
|
141
141
|
export interface OrderedGroup extends ToolGroup {
|
|
@@ -13,9 +13,9 @@ import { isDir, now, readJson, safeId, writeJson } from "../core/fs.js";
|
|
|
13
13
|
import { charCount, compareText, lines, takeChars } from "../core/text.js";
|
|
14
14
|
import * as notes from "../wiki/notes.js";
|
|
15
15
|
import { isWorldTool, WORLD_GUIDANCE } from "../world/untrusted.js";
|
|
16
|
-
import {
|
|
16
|
+
import { TODO_GUIDANCE, TODO_WRITE, describeTodo } from "../tools/todo.js";
|
|
17
17
|
import { CHECKPOINT_WRITE } from "../tools/checkpoint.js";
|
|
18
|
-
import { SESSION_NAME } from "../tools/sessionName.js";
|
|
18
|
+
import { SESSION_GUIDANCE, SESSION_NAME } from "../tools/sessionName.js";
|
|
19
19
|
import { ANNOTATIONS_GUIDANCE, STANDARD_CONNECTIONS, standardSections } from "./standard.js";
|
|
20
20
|
const IDENTITY_BUDGET = 1000;
|
|
21
21
|
const WIKI_BUDGET = 2500;
|
|
@@ -72,21 +72,21 @@ export function clip(text, limit) {
|
|
|
72
72
|
}
|
|
73
73
|
/** A bounded, request-scoped checkpoint reintroduced after context compaction. */
|
|
74
74
|
export function renderCompactionCheckpoint(input) {
|
|
75
|
-
if (!input.
|
|
75
|
+
if (!input.todo.length && !input.planInstructions)
|
|
76
76
|
return null;
|
|
77
77
|
const parts = [
|
|
78
78
|
'This is current state from the active run after context compaction, not a new user request. Continue the same request. Do not infer that omitted details or decisions were settled.',
|
|
79
79
|
input.goal.trim() ? `## Current request\n\n${clip(input.goal.trim(), 1_000)}` : '',
|
|
80
80
|
input.planInstructions ? `## Active plan instructions\n\n${clip(input.planInstructions, 4_000)}` : ''
|
|
81
81
|
].filter(Boolean);
|
|
82
|
-
if (input.
|
|
83
|
-
const active = input.
|
|
84
|
-
const completed = input.
|
|
85
|
-
const
|
|
86
|
-
active.length ? `### In progress and pending\n${clip(
|
|
87
|
-
completed.length ? `### Completed (${completed.length})\n${clip(
|
|
82
|
+
if (input.todo.length) {
|
|
83
|
+
const active = input.todo.filter((item) => item.status !== 'completed');
|
|
84
|
+
const completed = input.todo.filter((item) => item.status === 'completed');
|
|
85
|
+
const todo = [
|
|
86
|
+
active.length ? `### In progress and pending\n${clip(describeTodo(active), 2_500)}` : '',
|
|
87
|
+
completed.length ? `### Completed (${completed.length})\n${clip(describeTodo(completed), 500)}` : ''
|
|
88
88
|
].filter(Boolean).join('\n\n');
|
|
89
|
-
parts.push(`## Current
|
|
89
|
+
parts.push(`## Current todo list\n\n${todo}`);
|
|
90
90
|
}
|
|
91
91
|
const body = clip(parts.join('\n\n'), 8_000);
|
|
92
92
|
return `[Harness context update — not a user message]\n\n## Compaction checkpoint\n\n${body}`;
|
|
@@ -153,20 +153,18 @@ export function toolGroups(context, launch, dataRoot) {
|
|
|
153
153
|
{ group: 'World', tools: enabled.filter((name) => isWorldTool(name) && !(launch.toolGroups ?? []).some((group) => group.tools.includes(name))), order: GROUP_ORDER.World, guidance: `Web, browser and screen: ${WORLD_GUIDANCE}` },
|
|
154
154
|
...(launch.toolGroups ?? []).map((group) => ({ group: group.group, tools: group.tools, order: group.order, guidance: group.guidance }))
|
|
155
155
|
];
|
|
156
|
-
const progressTools = [
|
|
156
|
+
const progressTools = [TODO_WRITE, CHECKPOINT_WRITE].filter((name) => enabled.includes(name));
|
|
157
157
|
if (progressTools.length) {
|
|
158
|
-
const
|
|
159
|
-
? 'for work with several steps, write a checklist early with checklist_write and keep it current. Before your final reply, write one final update marking every fully completed step completed; leave unfinished or blocked steps accurate. The user follows your work from it, and a subagent\'s checklist shows as its progress in the user\'s list of subagents.'
|
|
160
|
-
: '';
|
|
158
|
+
const todo = enabled.includes(TODO_WRITE) ? TODO_GUIDANCE : '';
|
|
161
159
|
const checkpoint = enabled.includes(CHECKPOINT_WRITE)
|
|
162
160
|
? 'a reply has a work-slice budget; when it is reached the harness asks for a handoff, and checkpoint_write records what is done, the next step, anything blocking, the files touched and the checks that matter, so the next slice starts from it instead of re-reading everything.'
|
|
163
161
|
: '';
|
|
164
|
-
groups.push({ group: 'Progress', tools: progressTools, order: 87, guidance: `Progress: ${[
|
|
162
|
+
groups.push({ group: 'Progress', tools: progressTools, order: 87, guidance: `Progress: ${[todo, checkpoint].filter(Boolean).join(' ')}` });
|
|
165
163
|
}
|
|
166
164
|
if (enabled.includes(SESSION_NAME)) {
|
|
167
165
|
groups.push({
|
|
168
166
|
group: 'Session', tools: [SESSION_NAME], order: 88,
|
|
169
|
-
guidance:
|
|
167
|
+
guidance: `Session: ${SESSION_GUIDANCE}`
|
|
170
168
|
});
|
|
171
169
|
}
|
|
172
170
|
const annotations = ANNOTATION_TOOLS.filter((name) => enabled.includes(name) && !(launch.toolGroups ?? []).some((group) => group.tools.includes(name)));
|
|
@@ -17,7 +17,7 @@ import { listCommands } from "../commands/list.js";
|
|
|
17
17
|
import { selectHistory } from "../sessions/history.js";
|
|
18
18
|
import { runsSince } from "../sessions/runIndex.js";
|
|
19
19
|
import { findRun, listRuns, loadRun, pendingDecisions, readEvents, runFamily, snapshot } from "../sessions/runStore.js";
|
|
20
|
-
import {
|
|
20
|
+
import { todoOf } from "../tools/todo.js";
|
|
21
21
|
import { dispatch } from "../tools/dispatch.js";
|
|
22
22
|
import * as components from "../variant/components.js";
|
|
23
23
|
import { materialise } from "../variant/materialise.js";
|
|
@@ -234,7 +234,9 @@ export function methods(harness) {
|
|
|
234
234
|
'runs.since': (params) => runsSince(root(), { session: optional(params.filter ?? {}, 'session'), agent: optional(params.filter ?? {}, 'agent'), account: optional(params.filter ?? {}, 'account') }, optional(params, 'since')),
|
|
235
235
|
'runs.events': (params) => readEvents(root(), text(params, 'runId')),
|
|
236
236
|
'runs.pending': (params) => pendingDecisions(readEvents(root(), text(params, 'runId'))),
|
|
237
|
-
'runs.
|
|
237
|
+
'runs.todo': (params) => todoOf(readEvents(root(), text(params, 'runId'))),
|
|
238
|
+
// Pre-rename name; kept for one release so existing hosts keep working.
|
|
239
|
+
'runs.checklist': (params) => todoOf(readEvents(root(), text(params, 'runId'))),
|
|
238
240
|
'runs.continuation': (params) => continuationOf(root(), text(params, 'runId'), optional(params, 'sessionDir')),
|
|
239
241
|
'runs.active': () => runs.activeRunIds(),
|
|
240
242
|
'runs.interrupt': async (params) => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import { type
|
|
2
|
+
import { type TodoItem } from "./contracts.js";
|
|
3
3
|
export declare const COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
|
|
4
4
|
export interface CompactionCheckpointOptions {
|
|
5
|
-
/** A stopped run's persisted
|
|
6
|
-
|
|
5
|
+
/** A stopped run's persisted todo, when the active run is a continuation. */
|
|
6
|
+
initialTodo?: readonly TodoItem[];
|
|
7
7
|
/** The current dynamic plan instructions, if this request is in plan mode. */
|
|
8
8
|
planInstructions?: () => string | null;
|
|
9
9
|
}
|