@ikarmic/forge-core 5.3.0 → 5.4.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.
Files changed (113) hide show
  1. package/dist/agent/agent.js +132 -70
  2. package/dist/agent/cache-hints.d.ts +2 -0
  3. package/dist/agent/cache-hints.js +18 -3
  4. package/dist/agent/history-adapt.d.ts +23 -0
  5. package/dist/agent/history-adapt.js +114 -0
  6. package/dist/agent/prompts.d.ts +4 -2
  7. package/dist/agent/prompts.js +10 -2
  8. package/dist/agent/tool-repair.d.ts +8 -0
  9. package/dist/agent/tool-repair.js +16 -0
  10. package/dist/catalog/models-dev.d.ts +10 -0
  11. package/dist/catalog/models-dev.js +12 -0
  12. package/dist/checkpoints/store.js +3 -1
  13. package/dist/context/compaction.d.ts +21 -6
  14. package/dist/context/compaction.js +86 -26
  15. package/dist/context/mask.d.ts +9 -9
  16. package/dist/context/mask.js +30 -1
  17. package/dist/format/auto-format.d.ts +22 -0
  18. package/dist/format/auto-format.js +39 -0
  19. package/dist/github/github-tools.d.ts +1 -0
  20. package/dist/github/github-tools.js +9 -9
  21. package/dist/github/pr.d.ts +34 -0
  22. package/dist/github/pr.js +70 -0
  23. package/dist/human-text.d.ts +6 -0
  24. package/dist/human-text.js +37 -0
  25. package/dist/index.d.ts +13 -8
  26. package/dist/index.js +10 -6
  27. package/dist/journey/answers-block.d.ts +2 -4
  28. package/dist/lsp/diagnostics.d.ts +4 -0
  29. package/dist/lsp/diagnostics.js +8 -2
  30. package/dist/lsp/index.d.ts +1 -1
  31. package/dist/lsp/index.js +1 -1
  32. package/dist/mcp/index.d.ts +1 -1
  33. package/dist/mcp/index.js +1 -1
  34. package/dist/mcp/json-schema-to-zod.js +2 -4
  35. package/dist/mcp/mcp-client.d.ts +14 -0
  36. package/dist/mcp/mcp-client.js +102 -18
  37. package/dist/orchestrator/specialists-registry.js +14 -1
  38. package/dist/orchestrator/specialists.js +3 -3
  39. package/dist/semantic/embedder.d.ts +11 -0
  40. package/dist/semantic/embedder.js +16 -0
  41. package/dist/semantic/semantic-index.d.ts +39 -1
  42. package/dist/semantic/semantic-index.js +43 -13
  43. package/dist/semantic/vector-store.d.ts +2 -0
  44. package/dist/semantic/vector-store.js +13 -1
  45. package/dist/sessions/index.d.ts +2 -2
  46. package/dist/sessions/index.js +1 -1
  47. package/dist/sessions/search.js +2 -2
  48. package/dist/sessions/session-export.d.ts +1 -1
  49. package/dist/sessions/session-export.js +41 -33
  50. package/dist/sessions/session-store.d.ts +20 -1
  51. package/dist/sessions/session-store.js +7 -0
  52. package/dist/sessions/watch.js +4 -4
  53. package/dist/skills/activator.d.ts +6 -0
  54. package/dist/skills/activator.js +17 -0
  55. package/dist/skills/skill-tool.d.ts +12 -0
  56. package/dist/skills/skill-tool.js +28 -0
  57. package/dist/tools/background-process.d.ts +63 -0
  58. package/dist/tools/background-process.js +150 -0
  59. package/dist/tools/edit-file.js +2 -27
  60. package/dist/tools/edit-match.d.ts +16 -0
  61. package/dist/tools/edit-match.js +115 -0
  62. package/dist/tools/fetch-url.d.ts +15 -1
  63. package/dist/tools/fetch-url.js +7 -47
  64. package/dist/tools/get-diagnostics.d.ts +14 -0
  65. package/dist/tools/get-diagnostics.js +44 -0
  66. package/dist/tools/glob-filter.d.ts +7 -0
  67. package/dist/tools/glob-filter.js +12 -0
  68. package/dist/tools/html-to-markdown.d.ts +13 -0
  69. package/dist/tools/html-to-markdown.js +507 -0
  70. package/dist/tools/index.d.ts +5 -1
  71. package/dist/tools/index.js +11 -1
  72. package/dist/tools/list-files.d.ts +1 -0
  73. package/dist/tools/list-files.js +17 -5
  74. package/dist/tools/list-symbols.js +2 -2
  75. package/dist/tools/network-policy.d.ts +5 -0
  76. package/dist/tools/network-policy.js +77 -0
  77. package/dist/tools/office-docs.d.ts +6 -0
  78. package/dist/tools/office-docs.js +393 -0
  79. package/dist/tools/permission-rules.js +55 -5
  80. package/dist/tools/read-file.d.ts +2 -0
  81. package/dist/tools/read-file.js +47 -10
  82. package/dist/tools/read-image.js +2 -2
  83. package/dist/tools/read-only.d.ts +1 -1
  84. package/dist/tools/read-only.js +4 -0
  85. package/dist/tools/repo-clone.d.ts +40 -0
  86. package/dist/tools/repo-clone.js +218 -0
  87. package/dist/tools/result-cap.d.ts +8 -0
  88. package/dist/tools/result-cap.js +44 -0
  89. package/dist/tools/run-command.d.ts +18 -1
  90. package/dist/tools/run-command.js +41 -16
  91. package/dist/tools/search-files.js +5 -5
  92. package/dist/tools/search-worker.js +11 -2
  93. package/dist/tools/shared.d.ts +14 -1
  94. package/dist/tools/shared.js +76 -4
  95. package/dist/tools/shell.d.ts +27 -0
  96. package/dist/tools/shell.js +80 -0
  97. package/dist/tools/sub-agent.js +3 -1
  98. package/dist/tools/suggest-plan.d.ts +18 -0
  99. package/dist/tools/suggest-plan.js +28 -0
  100. package/dist/types.d.ts +74 -0
  101. package/dist/wakeup/wakeup-tools.d.ts +22 -0
  102. package/dist/wakeup/wakeup-tools.js +34 -0
  103. package/dist/wakeup/wakeup.d.ts +75 -0
  104. package/dist/wakeup/wakeup.js +151 -0
  105. package/dist/worktree/health.d.ts +51 -0
  106. package/dist/worktree/health.js +186 -0
  107. package/dist/worktree/index.d.ts +4 -0
  108. package/dist/worktree/index.js +2 -0
  109. package/dist/worktree/setup.d.ts +51 -0
  110. package/dist/worktree/setup.js +173 -0
  111. package/dist/worktree/worktree.d.ts +7 -0
  112. package/dist/worktree/worktree.js +9 -4
  113. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import { realpathSync } from 'node:fs';
1
+ import { realpathSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { checkPolicyViolation } from './policy.js';
4
5
  /**
@@ -15,11 +16,82 @@ export function resolveSafePath(inputPath, ctx, options = {}) {
15
16
  if (!options.allowOutside) {
16
17
  // Lexically, then for real: a symlink or junction inside the workspace can point anywhere on disk.
17
18
  if (escapes(ctx.cwd, resolved) || escapes(realPath(ctx.cwd), realPath(resolved))) {
18
- throw new Error(`Path "${inputPath}" resolves outside of the working directory (${ctx.cwd}).`);
19
+ // A read may also reach a folder this run was explicitly given (a repository it cloned), never anything else.
20
+ if (options.read && inReadRoot(resolved, ctx))
21
+ return resolved;
22
+ throw new OutsideWorkspaceError(`Path "${inputPath}" resolves outside of the working directory (${ctx.cwd}).`);
19
23
  }
20
24
  }
21
25
  return resolved;
22
26
  }
27
+ /** A path that leads outside the working directory: the one error a host may answer by asking (G16). */
28
+ export class OutsideWorkspaceError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = 'OutsideWorkspaceError';
32
+ }
33
+ }
34
+ /** Is `resolved` inside one of the run's extra read roots, judged by where it really is after links (RB-20)? */
35
+ function inReadRoot(resolved, ctx) {
36
+ const real = realPath(resolved);
37
+ return (ctx.readRoots ?? []).some((root) => !escapes(realPath(root), real));
38
+ }
39
+ const SENSITIVE_DIRS = new Set(['.ssh', '.aws', '.gnupg', '.kube', '.azure', '.docker', '.password-store', 'credentials', 'protect', 'user data']);
40
+ /** Why a directory must not be offered for reading, or undefined: too broad to be a choice, or holds credentials. */
41
+ function notOfferable(dir) {
42
+ if (path.parse(dir).root === dir)
43
+ return 'it is the top of a drive, too broad to open';
44
+ let home;
45
+ try {
46
+ home = realpathSync.native(homedir());
47
+ }
48
+ catch {
49
+ home = undefined;
50
+ }
51
+ // The home folder and everything above it would open the credential folders inside it.
52
+ if (home && !escapes(dir, home))
53
+ return 'it is your home folder or a folder above it, too broad to open';
54
+ const key = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
55
+ if (path.normalize(dir).split(path.sep).some((part) => SENSITIVE_DIRS.has(key(part))))
56
+ return 'it holds credentials';
57
+ return undefined;
58
+ }
59
+ /**
60
+ * `resolveAndCheckPath` for a READ tool, with one more way to succeed: when the path leads outside the project and the host
61
+ * can ask, the person is asked about the real directory (links resolved), once per directory per run. A yes opens that
62
+ * directory and below to read tools; a no, or a directory that is too broad or holds credentials, is a refusal.
63
+ */
64
+ export async function resolveReadPath(inputPath, ctx) {
65
+ try {
66
+ return resolveAndCheckPath(inputPath, ctx, { read: true });
67
+ }
68
+ catch (err) {
69
+ if (!(err instanceof OutsideWorkspaceError) || !ctx.askReadOutside)
70
+ throw err;
71
+ const resolved = path.isAbsolute(inputPath) ? path.normalize(inputPath) : path.resolve(ctx.cwd, inputPath);
72
+ const real = realPath(resolved);
73
+ let isDir = false;
74
+ try {
75
+ isDir = statSync(real).isDirectory();
76
+ }
77
+ catch {
78
+ isDir = false;
79
+ }
80
+ const dir = isDir ? real : path.dirname(real);
81
+ const key = process.platform === 'win32' ? dir.toLowerCase() : dir;
82
+ if (ctx.deniedReadDirs?.has(key))
83
+ throw err;
84
+ const why = notOfferable(dir);
85
+ if (why)
86
+ throw new OutsideWorkspaceError(`Path "${inputPath}" is outside the working directory, and Forge will not offer to open ${dir}: ${why}.`);
87
+ if (!(await ctx.askReadOutside(dir))) {
88
+ (ctx.deniedReadDirs ??= new Set()).add(key);
89
+ throw new OutsideWorkspaceError(`Path "${inputPath}" is outside the working directory, and reading in ${dir} was not allowed.`);
90
+ }
91
+ (ctx.readRoots ??= []).push(dir);
92
+ return resolveAndCheckPath(inputPath, ctx, { read: true });
93
+ }
94
+ }
23
95
  /**
24
96
  * For a `path.relative(root, target)` result: does it lead out of `root`? `..` itself or `../…` does; a name that merely
25
97
  * starts with two dots (`..notes.md`) does not. Use this instead of `rel.startsWith('..')`.
@@ -53,8 +125,8 @@ function realPath(p) {
53
125
  * tools call this instead of `resolveSafePath` directly so the sandbox is
54
126
  * applied uniformly.
55
127
  */
56
- export function resolveAndCheckPath(inputPath, ctx) {
57
- const resolved = resolveSafePath(inputPath, ctx);
128
+ export function resolveAndCheckPath(inputPath, ctx, options = {}) {
129
+ const resolved = resolveSafePath(inputPath, ctx, options);
58
130
  const violation = checkPolicyViolation(ctx.policy, { cwd: ctx.cwd, resolvedPath: resolved });
59
131
  if (violation)
60
132
  throw new Error(`[sandbox] ${violation}`);
@@ -0,0 +1,27 @@
1
+ export type ShellKind = 'pwsh' | 'powershell' | 'bash' | 'cmd' | 'sh';
2
+ /** What a person can ask for. */
3
+ export type ShellPreference = 'auto' | 'pwsh' | 'powershell' | 'bash' | 'cmd';
4
+ export interface ShellChoice {
5
+ kind: ShellKind;
6
+ /** The program to start; absent for cmd.exe and sh, which `spawn(..., { shell: true })` finds itself. */
7
+ file?: string;
8
+ }
9
+ export interface ChooseShellInput {
10
+ platform: NodeJS.Platform;
11
+ env: NodeJS.ProcessEnv;
12
+ preference?: ShellPreference;
13
+ /** Tests: does this file exist? */
14
+ exists?: (file: string) => boolean;
15
+ /** Tests: where is this program on PATH? */
16
+ find?: (name: string) => string | undefined;
17
+ /** Tests: Git Bash, when known. */
18
+ gitBash?: string | undefined;
19
+ }
20
+ export declare function chooseShell(input: ChooseShellInput): ShellChoice;
21
+ /** The program and arguments that run `command` in `shell`. `file` is undefined for cmd.exe and sh: use `shell: true`. */
22
+ export declare function shellInvocation(shell: ShellChoice, command: string): {
23
+ file: string | undefined;
24
+ args: string[];
25
+ };
26
+ /** The shell for this machine, looked up once per preference. */
27
+ export declare function runnerShell(preference?: ShellPreference | undefined, platform?: NodeJS.Platform): ShellChoice;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Which shell Forge's own command runner uses (G11). On Windows `cmd.exe` has no `ls`, `cat` or `grep`, and models
3
+ * write worse commands for it, so the runner prefers `pwsh`, then Windows PowerShell, then Git Bash, then `cmd.exe`.
4
+ * Elsewhere it stays `/bin/sh`. In VS Code the command runs in the integrated terminal instead and this is not used.
5
+ *
6
+ * RB-24 still holds: a shell started in the workspace must not run a program the repository ships. cmd.exe is
7
+ * handled by `shellEnv`; PowerShell and bash look in PATH only (a script in the current folder needs `.\`).
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { resolveOnPath } from '../lsp/client.js';
12
+ /** `bash.exe` of a Git for Windows install, found from `git` on PATH. Never `System32\bash.exe`, which starts WSL. */
13
+ function findGitBash(input, find, exists) {
14
+ if (input.gitBash !== undefined)
15
+ return input.gitBash;
16
+ const git = find('git');
17
+ if (!git)
18
+ return undefined;
19
+ const root = path.dirname(path.dirname(git));
20
+ for (const rel of [['bin', 'bash.exe'], ['usr', 'bin', 'bash.exe']]) {
21
+ const candidate = path.join(root, ...rel);
22
+ if (exists(candidate) && !/system32/i.test(candidate))
23
+ return candidate;
24
+ }
25
+ return undefined;
26
+ }
27
+ export function chooseShell(input) {
28
+ if (input.platform !== 'win32')
29
+ return { kind: 'sh' };
30
+ const exists = input.exists ?? existsSync;
31
+ const find = input.find ?? ((n) => resolveOnPath(n, input.env, 'win32'));
32
+ const pwsh = find('pwsh');
33
+ const powershell = find('powershell');
34
+ const bash = findGitBash(input, find, exists);
35
+ const available = {
36
+ pwsh: pwsh ? { kind: 'pwsh', file: pwsh } : undefined,
37
+ powershell: powershell ? { kind: 'powershell', file: powershell } : undefined,
38
+ bash: bash ? { kind: 'bash', file: bash } : undefined,
39
+ cmd: { kind: 'cmd' },
40
+ };
41
+ const pref = input.preference ?? 'auto';
42
+ if (pref !== 'auto' && available[pref])
43
+ return available[pref];
44
+ // Asked for a shell that is not installed: fall through the usual order.
45
+ return available.pwsh ?? available.powershell ?? available.bash ?? available.cmd;
46
+ }
47
+ /**
48
+ * `powershell -Command` ends with exit code 1 when the last command failed, whatever the code of a native program was.
49
+ * This keeps the program's own code (node, git and npm tell apart failures by it) with the status of the LAST command,
50
+ * as sh and cmd report it: a native failure followed by a cmdlet that works is a success. The tail is on its own line
51
+ * so a trailing comment in the command cannot swallow it.
52
+ */
53
+ function withExitCode(command) {
54
+ return `$global:LASTEXITCODE = $null\n${command}\nif (-not $?) { if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE } else { exit 1 } }`;
55
+ }
56
+ /** The program and arguments that run `command` in `shell`. `file` is undefined for cmd.exe and sh: use `shell: true`. */
57
+ export function shellInvocation(shell, command) {
58
+ switch (shell.kind) {
59
+ case 'pwsh':
60
+ return { file: shell.file, args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', withExitCode(command)] };
61
+ case 'powershell':
62
+ // Windows PowerShell writes the console code page (not UTF-8) to a pipe, which mangles accents and CJK text.
63
+ return { file: shell.file, args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8\n${withExitCode(command)}`] };
64
+ case 'bash':
65
+ return { file: shell.file, args: ['-c', command] };
66
+ default:
67
+ return { file: undefined, args: [] };
68
+ }
69
+ }
70
+ const cache = new Map();
71
+ /** The shell for this machine, looked up once per preference. */
72
+ export function runnerShell(preference = 'auto', platform = process.platform) {
73
+ const key = `${platform}:${preference}`;
74
+ let found = cache.get(key);
75
+ if (!found) {
76
+ found = chooseShell({ platform, env: process.env, preference });
77
+ cache.set(key, found);
78
+ }
79
+ return found;
80
+ }
@@ -107,7 +107,9 @@ const DEFAULT_MAX_ITERATIONS = 10;
107
107
  * passing `allowSubAgent: true` via context if you really want it)
108
108
  * - nothing else: the sub-agent gets the full built-in toolset.
109
109
  */
110
- const FORBIDDEN_TOOLS = new Set(['sub_agent', 'sub_agent_wait']);
110
+ // A child has no one to approve a command and no run of its own to stop a process when it ends, so it gets no
111
+ // background processes either (RB-14).
112
+ const FORBIDDEN_TOOLS = new Set(['sub_agent', 'sub_agent_wait', 'bg_start', 'bg_read', 'bg_stop', 'schedule_wakeup', 'cancel_wakeup', 'repo_clone', 'suggest_plan']);
111
113
  /**
112
114
  * Delegate a task to a child agent. The child runs synchronously and its
113
115
  * final text + run statistics are returned to the parent. Useful for
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import type { ToolDefinition } from '../types.js';
3
+ declare const params: z.ZodObject<{
4
+ reason: z.ZodString;
5
+ }, z.core.$strip>;
6
+ export interface SuggestPlanResult {
7
+ /** True when the chat will ask the person; the model should then stop. */
8
+ suggested: boolean;
9
+ reason?: string;
10
+ note: string;
11
+ }
12
+ /**
13
+ * `suggest_plan`: the model says a request is big or unclear enough to plan before changing anything. It only signals.
14
+ * The chat reads the result after the run and asks the person ("Plan first / Just do it"); "Just do it" runs the request
15
+ * again with `planDeclined` set, so the model cannot ask twice. Where no chat can ask, it says to carry on.
16
+ */
17
+ export declare const suggestPlanTool: ToolDefinition<z.infer<typeof params>, SuggestPlanResult>;
18
+ export {};
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ const params = z.object({
3
+ reason: z.string().min(1).describe('One sentence on why this needs a plan first, for the person to read.'),
4
+ });
5
+ /**
6
+ * `suggest_plan`: the model says a request is big or unclear enough to plan before changing anything. It only signals.
7
+ * The chat reads the result after the run and asks the person ("Plan first / Just do it"); "Just do it" runs the request
8
+ * again with `planDeclined` set, so the model cannot ask twice. Where no chat can ask, it says to carry on.
9
+ */
10
+ export const suggestPlanTool = {
11
+ name: 'suggest_plan',
12
+ description: 'Suggest planning first when a request asks for a whole system or feature, a redesign or rewrite, or work of more than about ten steps or five files. ' +
13
+ 'The person is asked whether to plan first. Call it as your first action, before reading or editing anything, then stop and reply with one short sentence. Not for a simple edit, a question, or when they asked for a plan or told you to just do it.',
14
+ parameters: params,
15
+ execute: async (args, ctx) => {
16
+ if (ctx.planDeclined) {
17
+ return { suggested: false, note: 'The person already chose to go ahead without a plan. Carry on with the task.' };
18
+ }
19
+ if (!ctx.suggestPlan) {
20
+ return { suggested: false, note: 'Nobody can be asked here. Carry on with the task, and state your plan in a sentence or two first.' };
21
+ }
22
+ return {
23
+ suggested: true,
24
+ reason: args.reason,
25
+ note: 'The person is being asked whether to plan first. Stop now: do not change any file or run any command, and reply with one short sentence saying you suggested planning first.',
26
+ };
27
+ },
28
+ };
package/dist/types.d.ts CHANGED
@@ -117,6 +117,35 @@ export interface ToolContext {
117
117
  shownFolderInstructions?: Set<string>;
118
118
  /** Longest output `run_command` returns per stream before cutting the middle (`AgentConfig.maxToolOutputChars`). */
119
119
  maxOutputChars?: number;
120
+ /**
121
+ * Folders outside the project that READ tools may also reach: a repository this run cloned with `repo_clone`.
122
+ * Added to by that tool only; write tools never see them (shared.ts `resolveSafePath` `read` option). Not handed to a child.
123
+ */
124
+ readRoots?: string[];
125
+ /**
126
+ * Asks whether a directory outside the project may be read (G16). Given the real directory, after links. Absent: such a
127
+ * path is refused. Not handed to a child, which has no one to ask.
128
+ */
129
+ askReadOutside?: (dir: string) => boolean | Promise<boolean>;
130
+ /** Directories the person already refused in this run, so they are not asked again. */
131
+ deniedReadDirs?: Set<string>;
132
+ /** Where `repo_clone` keeps repositories (default `~/.forge/cache/repos`). `testGitConfig` is for tests only. */
133
+ repoCache?: {
134
+ root: string;
135
+ testGitConfig?: string[];
136
+ };
137
+ /** A chat can ask the person about planning first (`AgentConfig.suggestPlan`). */
138
+ suggestPlan?: boolean;
139
+ /** The person already chose "just do it" for this request (`AgentConfig.planDeclined`). */
140
+ planDeclined?: boolean;
141
+ /** Which shell the default `run_command` runner uses (`AgentConfig.shell`). */
142
+ shell?: import('./tools/shell.js').ShellPreference;
143
+ /** The chat's wakeup scheduler (`AgentConfig.wakeups`); never handed to a child. */
144
+ wakeups?: import('./wakeup/wakeup.js').WakeupScheduler;
145
+ /** The run's background processes (`bg_start`); the agent stops them when the run ends. Never handed to a child. */
146
+ processes?: import('./tools/background-process.js').BackgroundProcesses;
147
+ /** Language servers for `get_diagnostics` (`AgentConfig.verify.lsp` when it is an object). */
148
+ lspConfig?: import('./lsp/index.js').LspConfig;
120
149
  /** Asks the person a question (`ask_user`); absent when no surface can answer. */
121
150
  askUser?: (question: UserQuestion, signal?: AbortSignal) => Promise<string>;
122
151
  /**
@@ -186,6 +215,16 @@ export interface CommandSandbox {
186
215
  network: boolean;
187
216
  /** More folders commands may write to (a package manager's cache, say). */
188
217
  extraWritable?: string[];
218
+ /**
219
+ * With the network off: hosts Forge's own network tools (fetch_url, repo_clone, the browser, GitHub, web search with `*`) may still
220
+ * reach. Exact names, `*.example.com`, or `*`. Shell commands are all or nothing and ignore it (F13).
221
+ */
222
+ allowedHosts?: string[];
223
+ /**
224
+ * The surface told the person this system cannot confine commands (Windows has no sandbox tool) and they go on anyway.
225
+ * Without it such a command is refused rather than run unconfined in silence.
226
+ */
227
+ allowUnconfined?: boolean;
189
228
  }
190
229
  /**
191
230
  * Executes one shell command on behalf of `run_command`. Return `null` to decline
@@ -665,6 +704,7 @@ export interface AgentConfig {
665
704
  maskOldResults?: boolean | {
666
705
  keepRecentTurns?: number;
667
706
  minChars?: number;
707
+ protectTokens?: number;
668
708
  };
669
709
  /**
670
710
  * Model to use for the summarizer. Defaults to the agent's own model.
@@ -717,6 +757,10 @@ export interface AgentConfig {
717
757
  extensions?: string[];
718
758
  minScore?: number;
719
759
  defaultLimit?: number;
760
+ /** Asked before code is sent to a remote embedder (F12). Absent: no question. */
761
+ consent?: import('./semantic/semantic-index.js').SemanticOptions['consent'];
762
+ /** Progress of the index, for a status item. */
763
+ onStatus?: import('./semantic/semantic-index.js').SemanticOptions['onStatus'];
720
764
  };
721
765
  /**
722
766
  * After the model finishes a run that edited files, run these commands and
@@ -754,6 +798,31 @@ export interface AgentConfig {
754
798
  * linter commands) and append any problems to the tool result so the model fixes them.
755
799
  */
756
800
  autoLint?: import('./lint/auto-lint.js').AutoLintConfig;
801
+ /**
802
+ * After a file is written, run the person's formatter for its type on it (`prettier --write`), before auto-lint, and tell
803
+ * the model when the file changed. The commands run on every edit without asking, so they come from the person only (RB-12).
804
+ */
805
+ autoFormat?: import('./format/auto-format.js').AutoFormatConfig;
806
+ /** The surface shows a "plan first?" card after a run that called `suggest_plan`. Without it the tool says to carry on. */
807
+ suggestPlan?: boolean;
808
+ /** The person already answered "just do it": `suggest_plan` says to carry on instead of asking again. */
809
+ planDeclined?: boolean;
810
+ /** Folders outside the project that read tools may reach from the start of the run: what the person allowed earlier in this chat (G16). */
811
+ readRoots?: string[];
812
+ /** Asks the person whether a directory outside the project may be read; absent = such a path is refused. */
813
+ askReadOutside?: (dir: string) => boolean | Promise<boolean>;
814
+ /** Which shell Forge's own `run_command` runner uses on Windows. Default `auto`: pwsh, Windows PowerShell, Git Bash, cmd.exe. */
815
+ shell?: import('./tools/shell.js').ShellPreference;
816
+ /**
817
+ * The shell commands really run in when a surface supplies its own runner (VS Code's terminal), so the prompt can say so
818
+ * (RB-27). Without it a custom runner is described as the platform default.
819
+ */
820
+ runnerShell?: import('./tools/shell.js').ShellKind;
821
+ /**
822
+ * The chat's scheduler for `schedule_wakeup` / `cancel_wakeup`. Lives longer than a run: the surface creates it once per
823
+ * chat, delivers a due wakeup as an automatic message, and disposes it. Absent = the tools are not offered.
824
+ */
825
+ wakeups?: import('./wakeup/wakeup.js').WakeupScheduler;
757
826
  /**
758
827
  * Stop the run (`reason: 'budget_exceeded'`) once the running USD cost reaches
759
828
  * this cap. Checked before each model call, so one in-flight turn can overshoot.
@@ -944,6 +1013,11 @@ export interface AgentRunResult {
944
1013
  * across turns (used by the REPL).
945
1014
  */
946
1015
  messages: ModelMessage[];
1016
+ /**
1017
+ * This run's own messages: its user message and everything added after it, untouched by compaction (RB-58).
1018
+ * Append them to a display transcript; `messages` is what the next turn should send.
1019
+ */
1020
+ turnMessages: ModelMessage[];
947
1021
  }
948
1022
  /** Public alias for the AI SDK language model handle. */
949
1023
  export type AnyLanguageModel = LanguageModel;
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import type { ToolDefinition } from '../types.js';
3
+ declare const scheduleParams: z.ZodObject<{
4
+ reason: z.ZodString;
5
+ delay: z.ZodOptional<z.ZodString>;
6
+ when: z.ZodOptional<z.ZodString>;
7
+ prompt: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ export declare const scheduleWakeupTool: ToolDefinition<z.infer<typeof scheduleParams>, {
10
+ id: string;
11
+ dueAt: string;
12
+ notice?: string;
13
+ note: string;
14
+ }>;
15
+ declare const cancelParams: z.ZodObject<{
16
+ id: z.ZodString;
17
+ }, z.core.$strip>;
18
+ export declare const cancelWakeupTool: ToolDefinition<z.infer<typeof cancelParams>, {
19
+ id: string;
20
+ cancelled: boolean;
21
+ }>;
22
+ export {};
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ function scheduler(ctx) {
3
+ if (!ctx.wakeups)
4
+ throw new Error('Scheduled wakeups are not available here.');
5
+ return ctx.wakeups;
6
+ }
7
+ const scheduleParams = z.object({
8
+ reason: z.string().min(1).describe('Why you want to be woken, in one sentence. You will see it again when the time comes.'),
9
+ delay: z.string().optional().describe('How long to wait: a number of seconds, or 30s, 5m, 2h, 1d. Give this or when.'),
10
+ when: z.string().optional().describe('An exact time as ISO date and time, for example 2026-10-27T15:30:00. Give this or delay.'),
11
+ prompt: z.string().optional().describe('What you will do when woken, in your own words.'),
12
+ });
13
+ export const scheduleWakeupTool = {
14
+ name: 'schedule_wakeup',
15
+ description: 'Ask to be woken once, later, for example to check on a build or a background process. At the time you get an automatic message with your reason. ' +
16
+ 'It works only while the editor is open (a closed editor wakes you when it is next opened), one time per call, at least 10 seconds and at most 7 days ahead, and a chat holds at most 10.',
17
+ parameters: scheduleParams,
18
+ execute: async (args, ctx) => {
19
+ const w = await scheduler(ctx).schedule(args);
20
+ return {
21
+ id: w.id,
22
+ dueAt: new Date(w.dueAt).toISOString(),
23
+ ...(w.notice ? { notice: w.notice } : {}),
24
+ note: 'Scheduled. Finish what you are doing now; do not wait for it.',
25
+ };
26
+ },
27
+ };
28
+ const cancelParams = z.object({ id: z.string().describe('The id returned by schedule_wakeup.') });
29
+ export const cancelWakeupTool = {
30
+ name: 'cancel_wakeup',
31
+ description: 'Cancel a wakeup you scheduled with schedule_wakeup, before it fires.',
32
+ parameters: cancelParams,
33
+ execute: async (args, ctx) => ({ id: args.id, cancelled: await scheduler(ctx).cancel(args.id) }),
34
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Scheduled wakeups: the model asks to be woken later ("check the build in 10 minutes") and the chat delivers an
3
+ * automatic message when the time comes. The scheduler belongs to the chat, not to a run, and works only while the
4
+ * editor is open. Caps follow Kilo's wakeup/schema.ts; recurring (cron) wakeups are not offered.
5
+ */
6
+ /** A delay under this is raised to it; an absolute time is honoured as given. */
7
+ export declare const MIN_DELAY_MS = 10000;
8
+ /** A wakeup never fires further out than seven days. */
9
+ export declare const MAX_HORIZON_MS: number;
10
+ /** One chat holds at most this many pending wakeups. */
11
+ export declare const MAX_PER_SESSION = 10;
12
+ export interface Wakeup {
13
+ id: string;
14
+ /** Why the model wants to be woken; shown to the model again and to the person. */
15
+ reason: string;
16
+ /** What to do when woken, in the model's own words. */
17
+ prompt?: string;
18
+ /** Epoch milliseconds. */
19
+ dueAt: number;
20
+ created: number;
21
+ }
22
+ /** Where pending wakeups are kept so they survive a reload of the editor. */
23
+ export interface WakeupStore {
24
+ load(): Promise<Wakeup[]>;
25
+ save(wakeups: Wakeup[]): Promise<void>;
26
+ }
27
+ /** When a wakeup is due, with the clamp that applied (as a sentence for the model), or an error naming the problem. */
28
+ export declare function resolveWakeupTime(input: {
29
+ delay?: string;
30
+ when?: string;
31
+ }, now?: number): {
32
+ dueAt: number;
33
+ notice?: string;
34
+ };
35
+ export interface WakeupSchedulerOptions {
36
+ /** Deliver a due wakeup. Return false when the chat is busy: it is offered again after `retryMs`. */
37
+ onDue: (wakeup: Wakeup) => Promise<boolean>;
38
+ store?: WakeupStore;
39
+ retryMs?: number;
40
+ }
41
+ export declare class WakeupScheduler {
42
+ private readonly options;
43
+ private readonly pending;
44
+ private readonly timers;
45
+ private nextId;
46
+ private disposed;
47
+ private readonly retryMs;
48
+ constructor(options: WakeupSchedulerOptions);
49
+ list(): Wakeup[];
50
+ schedule(input: {
51
+ delay?: string;
52
+ when?: string;
53
+ reason: string;
54
+ prompt?: string;
55
+ }): Promise<Wakeup & {
56
+ notice?: string;
57
+ }>;
58
+ cancel(id: string): Promise<boolean>;
59
+ cancelAll(): Promise<void>;
60
+ /** Load what a previous window left; anything already due fires on the next tick. */
61
+ restore(): Promise<void>;
62
+ /** Stop every timer without forgetting what is stored (the editor is closing, or the chat was deleted elsewhere). */
63
+ dispose(): void;
64
+ private arm;
65
+ private disarm;
66
+ private fire;
67
+ private save;
68
+ }
69
+ /**
70
+ * The automatic message a due wakeup becomes. It is a user-role message, so it says what it is and that the person's
71
+ * own words win: the first wording of the todo nudge ("carry on now") overrode "wait for my go-ahead" in live runs (RB-44).
72
+ */
73
+ export declare function wakeupMessage(w: Pick<Wakeup, 'reason' | 'prompt' | 'created' | 'dueAt'> & {
74
+ id?: string;
75
+ }): string;