@ikarmic/forge-core 5.1.1 → 5.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.
Files changed (58) hide show
  1. package/dist/agent/agent.js +14 -3
  2. package/dist/agent/goal.d.ts +5 -8
  3. package/dist/agent/race-abort.d.ts +8 -0
  4. package/dist/agent/race-abort.js +24 -0
  5. package/dist/agent/stack-facts.d.ts +23 -0
  6. package/dist/agent/stack-facts.js +150 -0
  7. package/dist/code-index/parser.js +48 -3
  8. package/dist/github/github-tools.d.ts +2 -18
  9. package/dist/index.d.ts +5 -2
  10. package/dist/index.js +5 -2
  11. package/dist/journey/answers-block.js +14 -1
  12. package/dist/journey/offer.js +2 -1
  13. package/dist/mcp/json-schema-to-zod.js +12 -1
  14. package/dist/orchestrator/index.d.ts +1 -1
  15. package/dist/orchestrator/index.js +1 -1
  16. package/dist/orchestrator/orchestrator.d.ts +45 -6
  17. package/dist/orchestrator/orchestrator.js +62 -12
  18. package/dist/orchestrator/router.d.ts +1 -1
  19. package/dist/orchestrator/router.js +4 -1
  20. package/dist/orchestrator/specialists.d.ts +6 -1
  21. package/dist/orchestrator/specialists.js +165 -68
  22. package/dist/orchestrator/types.d.ts +1 -1
  23. package/dist/policy/policy.d.ts +6 -59
  24. package/dist/pricing.d.ts +2 -0
  25. package/dist/pricing.js +35 -16
  26. package/dist/structured/schema.js +9 -12
  27. package/dist/tools/apply-patch.d.ts +1 -5
  28. package/dist/tools/ask-user.d.ts +2 -10
  29. package/dist/tools/ask-user.js +12 -4
  30. package/dist/tools/browser-tools.d.ts +15 -31
  31. package/dist/tools/codebase-search.d.ts +1 -7
  32. package/dist/tools/dependency-audit.d.ts +30 -0
  33. package/dist/tools/dependency-audit.js +197 -0
  34. package/dist/tools/edit-file.d.ts +1 -15
  35. package/dist/tools/fetch-url.d.ts +1 -18
  36. package/dist/tools/find-references.d.ts +1 -5
  37. package/dist/tools/find-symbol.d.ts +10 -8
  38. package/dist/tools/git-diff.d.ts +13 -0
  39. package/dist/tools/git-diff.js +85 -0
  40. package/dist/tools/index.d.ts +3 -1
  41. package/dist/tools/index.js +5 -1
  42. package/dist/tools/list-files.d.ts +1 -19
  43. package/dist/tools/list-symbols.d.ts +1 -5
  44. package/dist/tools/memory.d.ts +23 -43
  45. package/dist/tools/notebook.d.ts +11 -17
  46. package/dist/tools/read-file.d.ts +1 -11
  47. package/dist/tools/read-image.d.ts +1 -12
  48. package/dist/tools/read-only.d.ts +1 -1
  49. package/dist/tools/read-only.js +2 -0
  50. package/dist/tools/run-command.d.ts +1 -17
  51. package/dist/tools/search-files.d.ts +1 -17
  52. package/dist/tools/sub-agent-wait.d.ts +2 -8
  53. package/dist/tools/sub-agent.d.ts +1 -69
  54. package/dist/tools/todo.d.ts +8 -20
  55. package/dist/tools/web-search.d.ts +1 -7
  56. package/dist/tools/write-file.d.ts +1 -7
  57. package/dist/types.d.ts +2 -0
  58. package/package.json +3 -3
@@ -31,9 +31,11 @@ import { redact } from '../tools/redact.js';
31
31
  import { composeSkillsPrompt, selectSkills } from '../skills/activator.js';
32
32
  import { MAX_TOOL_INJECTIONS, selectAtStart, selectOnToolTouch, toWorkspaceRelative, wrapSteering } from '../steering/select.js';
33
33
  import { applyCacheHints } from './cache-hints.js';
34
+ import { raceAbort } from './race-abort.js';
34
35
  import { attachmentParts, formatBytes, mediaPartTokens } from '../attachments.js';
35
36
  import { renderAnswersBlock } from '../journey/answers-block.js';
36
37
  import { CONTINUE_PROMPT, DEFAULT_PLAN_MODE_SUFFIX, DEFAULT_SPEC_MODE_SUFFIX, DEFAULT_SYSTEM_PROMPT, environmentBlock } from './prompts.js';
38
+ import { detectStackFacts, stackBlock } from './stack-facts.js';
37
39
  /** Read-only tool names whose output flows through the redactor. */
38
40
  const DEFAULT_MAX_CONTINUATIONS = 3;
39
41
  export const DEFAULT_MAX_DURATION_MS = 10 * 60 * 1000;
@@ -1020,8 +1022,7 @@ export class Agent {
1020
1022
  if (abortSignal?.aborted)
1021
1023
  return { approved: false };
1022
1024
  const answered = Promise.resolve(this.config.approve(request));
1023
- const aborted = new Promise((resolve) => abortSignal?.addEventListener('abort', () => resolve('aborted'), { once: true }));
1024
- const decision = await Promise.race([answered, aborted]);
1025
+ const decision = await raceAbort(answered, abortSignal, 'aborted');
1025
1026
  if (decision === 'aborted')
1026
1027
  return { approved: false };
1027
1028
  return typeof decision === 'boolean' ? { approved: decision } : decision;
@@ -1215,6 +1216,16 @@ async function buildInitialMessages(config, task, skills, steeringSystem = [], u
1215
1216
  // the first run: an answer that arrives on turn 3 can never enter the system
1216
1217
  // message, and a middle turn would be summarised away by compaction.
1217
1218
  const answersBlock = renderAnswersBlock(config.journeyAnswers ?? []);
1219
+ // The versions the workspace's files state, so code is written for them and not for what training saw most.
1220
+ let stackFactsBlock = '';
1221
+ if (!config.disableStackFacts) {
1222
+ try {
1223
+ stackFactsBlock = stackBlock(await detectStackFacts(cwd));
1224
+ }
1225
+ catch {
1226
+ // Skipped, never fatal.
1227
+ }
1228
+ }
1218
1229
  // Facts the model would otherwise have to guess: the platform decides
1219
1230
  // whether `cat` or `type` works, and it cannot see the date at all.
1220
1231
  const envBlock = environmentBlock({
@@ -1229,7 +1240,7 @@ async function buildInitialMessages(config, task, skills, steeringSystem = [], u
1229
1240
  // stable order so cache hits survive across runs that share state. The
1230
1241
  // environment block goes last: it is the part that varies per session.
1231
1242
  const steeringBlock = steeringSystem.map(wrapSteering).join('\n\n');
1232
- const blocks = [steeringBlock, instructionsBlock, extraInstructionsBlock, config.mcpInstructions ?? '', config.projectCommands ?? '', memoryBlock, skillsBlock, answersBlock, mapBlock, envBlock].filter((b) => b.length > 0);
1243
+ const blocks = [steeringBlock, instructionsBlock, extraInstructionsBlock, config.mcpInstructions ?? '', config.projectCommands ?? '', memoryBlock, skillsBlock, answersBlock, mapBlock, stackFactsBlock, envBlock].filter((b) => b.length > 0);
1233
1244
  const systemContent = blocks.length > 0 ? `${basePrompt}\n\n${blocks.join('\n\n')}` : basePrompt;
1234
1245
  return [
1235
1246
  { role: 'system', content: systemContent },
@@ -39,15 +39,12 @@ export declare function resumeGoal(g: GoalState): GoalState;
39
39
  /** The message that drives one goal turn. */
40
40
  export declare function goalPrompt(g: GoalState): string;
41
41
  declare const reportParams: z.ZodObject<{
42
- kind: z.ZodEnum<["complete", "blocked"]>;
42
+ kind: z.ZodEnum<{
43
+ blocked: "blocked";
44
+ complete: "complete";
45
+ }>;
43
46
  reason: z.ZodString;
44
- }, "strip", z.ZodTypeAny, {
45
- kind: "blocked" | "complete";
46
- reason: string;
47
- }, {
48
- kind: "blocked" | "complete";
49
- reason: string;
50
- }>;
47
+ }, z.core.$strip>;
51
48
  export declare function createGoalReportTool(onReport: (r: GoalReport) => void): ToolDefinition<z.infer<typeof reportParams>, {
52
49
  recorded: true;
53
50
  }>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Race `work` against `signal`, resolving to `onAbort` if the signal fires first.
3
+ *
4
+ * The listener is removed once the race settles. A run's signal lives for the
5
+ * whole run, so a listener added per question or per approval and left behind
6
+ * keeps every one of those closures alive until the run ends.
7
+ */
8
+ export declare function raceAbort<T, A>(work: Promise<T>, signal: AbortSignal | undefined, onAbort: A): Promise<T | A>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Race `work` against `signal`, resolving to `onAbort` if the signal fires first.
3
+ *
4
+ * The listener is removed once the race settles. A run's signal lives for the
5
+ * whole run, so a listener added per question or per approval and left behind
6
+ * keeps every one of those closures alive until the run ends.
7
+ */
8
+ export async function raceAbort(work, signal, onAbort) {
9
+ if (!signal)
10
+ return work;
11
+ if (signal.aborted)
12
+ return onAbort;
13
+ let listener;
14
+ const aborted = new Promise((resolve) => {
15
+ listener = () => resolve(onAbort);
16
+ signal.addEventListener('abort', listener, { once: true });
17
+ });
18
+ try {
19
+ return await Promise.race([work, aborted]);
20
+ }
21
+ finally {
22
+ signal.removeEventListener('abort', listener);
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ export declare const MAX_STACK_LIBRARIES = 25;
2
+ export interface StackFacts {
3
+ runtimes: {
4
+ name: string;
5
+ version: string;
6
+ source: string;
7
+ }[];
8
+ libraries: {
9
+ name: string;
10
+ version: string;
11
+ range: boolean;
12
+ }[];
13
+ tools: {
14
+ test?: string;
15
+ lint?: string;
16
+ format?: string;
17
+ };
18
+ }
19
+ /** JSON with comments and trailing commas (tsconfig.json): comments outside strings removed, then trailing commas. */
20
+ export declare function parseJsonc(text: string): Record<string, unknown> | undefined;
21
+ export declare function detectStackFacts(cwd: string): Promise<StackFacts>;
22
+ /** The facts as a block for the system prompt; '' when there are none. */
23
+ export declare function stackBlock(facts: StackFacts): string;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The `<stack>` block: the language, runtime and library versions the workspace's own files state, so an
3
+ * agent writes code for the versions in use instead of the ones it saw most in training. File reads only,
4
+ * no model and no tree walk (the journey survey's rule: a missing fact costs less than a wrong one). Every
5
+ * source is read on its own; one that is missing or unreadable leaves its facts out and never fails a run.
6
+ * Only the workspace root's files are read.
7
+ */
8
+ import { readFile } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ export const MAX_STACK_LIBRARIES = 25;
11
+ const TEST_TOOLS = { vitest: 'vitest', jest: 'jest', mocha: 'mocha', ava: 'ava', pytest: 'pytest' };
12
+ const LINT_TOOLS = { eslint: 'eslint', '@biomejs/biome': 'biome', ruff: 'ruff', flake8: 'flake8', pylint: 'pylint' };
13
+ const FORMAT_TOOLS = { prettier: 'prettier', '@biomejs/biome': 'biome', black: 'black', ruff: 'ruff' };
14
+ async function read(cwd, rel) {
15
+ try {
16
+ return await readFile(path.join(cwd, rel), 'utf8');
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ function json(text) {
23
+ if (text === undefined)
24
+ return undefined;
25
+ try {
26
+ const v = JSON.parse(text);
27
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ /** JSON with comments and trailing commas (tsconfig.json): comments outside strings removed, then trailing commas. */
34
+ export function parseJsonc(text) {
35
+ let out = '';
36
+ let inString = false;
37
+ for (let i = 0; i < text.length; i++) {
38
+ const c = text[i];
39
+ if (inString) {
40
+ out += c;
41
+ if (c === '\\')
42
+ out += text[++i] ?? '';
43
+ else if (c === '"')
44
+ inString = false;
45
+ }
46
+ else if (c === '"') {
47
+ inString = true;
48
+ out += c;
49
+ }
50
+ else if (c === '/' && text[i + 1] === '/') {
51
+ while (i < text.length && text[i] !== '\n')
52
+ i++;
53
+ out += '\n';
54
+ }
55
+ else if (c === '/' && text[i + 1] === '*') {
56
+ const end = text.indexOf('*/', i + 2);
57
+ i = end < 0 ? text.length : end + 1;
58
+ }
59
+ else
60
+ out += c;
61
+ }
62
+ return json(out.replace(/,(\s*[}\]])/g, '$1'));
63
+ }
64
+ const strings = (v) => Object.fromEntries(Object.entries(v && typeof v === 'object' ? v : {}).filter((e) => typeof e[1] === 'string'));
65
+ async function installedVersion(cwd, name, lock) {
66
+ const own = json(await read(cwd, path.join('node_modules', name, 'package.json')));
67
+ if (typeof own?.version === 'string')
68
+ return own.version;
69
+ const entry = lock?.packages?.[`node_modules/${name}`];
70
+ if (typeof entry?.version === 'string')
71
+ return entry.version;
72
+ const legacy = lock?.dependencies?.[name];
73
+ return typeof legacy?.version === 'string' ? legacy.version : undefined;
74
+ }
75
+ function firstMatch(text, re) {
76
+ return text?.match(re)?.[1]?.trim() || undefined;
77
+ }
78
+ export async function detectStackFacts(cwd) {
79
+ const facts = { runtimes: [], libraries: [], tools: {} };
80
+ const runtime = (name, version, source) => {
81
+ if (version)
82
+ facts.runtimes.push({ name, version, source });
83
+ };
84
+ const noteTool = (dep) => {
85
+ facts.tools.test ??= TEST_TOOLS[dep];
86
+ facts.tools.lint ??= LINT_TOOLS[dep];
87
+ facts.tools.format ??= FORMAT_TOOLS[dep];
88
+ };
89
+ // Node and npm-style libraries.
90
+ const manifest = json(await read(cwd, 'package.json'));
91
+ const engines = strings(manifest?.engines);
92
+ runtime('node', engines.node, 'package.json engines');
93
+ runtime('node', firstMatch(await read(cwd, '.nvmrc'), /^\s*(\S+)/), '.nvmrc');
94
+ runtime('node', firstMatch(await read(cwd, '.node-version'), /^\s*(\S+)/), '.node-version');
95
+ if (manifest) {
96
+ const lock = json(await read(cwd, 'package-lock.json'));
97
+ const deps = Object.entries({ ...strings(manifest.dependencies) });
98
+ const dev = Object.entries(strings(manifest.devDependencies)).filter(([n]) => !deps.some(([d]) => d === n));
99
+ for (const [name] of [...deps, ...dev])
100
+ noteTool(name);
101
+ for (const [name, range] of [...deps, ...dev].slice(0, MAX_STACK_LIBRARIES)) {
102
+ const exact = await installedVersion(cwd, name, lock);
103
+ facts.libraries.push(exact ? { name, version: exact, range: false } : { name, version: range, range: true });
104
+ }
105
+ }
106
+ const tsconfig = await read(cwd, 'tsconfig.json');
107
+ const target = tsconfig === undefined ? undefined : parseJsonc(tsconfig)?.compilerOptions?.target;
108
+ if (typeof target === 'string')
109
+ runtime('tsconfig target', target, 'tsconfig.json');
110
+ // Python.
111
+ runtime('python', firstMatch(await read(cwd, 'pyproject.toml'), /^\s*requires-python\s*=\s*["']([^"']+)["']/m), 'pyproject.toml');
112
+ runtime('python', firstMatch(await read(cwd, '.python-version'), /^\s*(\S+)/), '.python-version');
113
+ const requirements = await read(cwd, 'requirements.txt');
114
+ for (const line of requirements?.split(/\r?\n/) ?? []) {
115
+ const m = line.match(/^\s*([A-Za-z0-9._-]+)\s*==\s*([^\s;#]+)/);
116
+ if (!m)
117
+ continue;
118
+ noteTool(m[1].toLowerCase());
119
+ if (facts.libraries.length < MAX_STACK_LIBRARIES)
120
+ facts.libraries.push({ name: m[1], version: m[2], range: false });
121
+ }
122
+ // Go, Rust, Java.
123
+ runtime('go', firstMatch(await read(cwd, 'go.mod'), /^\s*go\s+(\d[\w.]*)\s*$/m), 'go.mod');
124
+ runtime('rust edition', firstMatch(await read(cwd, 'Cargo.toml'), /^\s*edition\s*=\s*["']([^"']+)["']/m), 'Cargo.toml');
125
+ runtime('rust', firstMatch(await read(cwd, 'rust-toolchain.toml'), /^\s*channel\s*=\s*["']([^"']+)["']/m), 'rust-toolchain.toml');
126
+ runtime('rust', firstMatch(await read(cwd, 'rust-toolchain'), /^\s*(\S+)/), 'rust-toolchain');
127
+ const pom = await read(cwd, 'pom.xml');
128
+ runtime('java', firstMatch(pom, /<maven\.compiler\.release>\s*([^<\s]+)\s*</) ?? firstMatch(pom, /<java\.version>\s*([^<\s]+)\s*</), 'pom.xml');
129
+ for (const gradle of ['build.gradle.kts', 'build.gradle']) {
130
+ const text = await read(cwd, gradle);
131
+ runtime('java', firstMatch(text, /JavaLanguageVersion\.of\(\s*(\d+)\s*\)/) ?? firstMatch(text, /JavaVersion\.VERSION_(\d+(?:_\d+)?)/), gradle);
132
+ }
133
+ for (const k of Object.keys(facts.tools))
134
+ if (facts.tools[k] === undefined)
135
+ delete facts.tools[k];
136
+ return facts;
137
+ }
138
+ /** The facts as a block for the system prompt; '' when there are none. */
139
+ export function stackBlock(facts) {
140
+ const lines = [
141
+ ...facts.runtimes.map((r) => `${r.name} ${r.version} (${r.source})`),
142
+ ...facts.libraries.map((l) => (l.range ? `${l.name} ${l.version} (range in the manifest; installed version unknown)` : `${l.name} ${l.version}`)),
143
+ ...(facts.tools.test ? [`test runner: ${facts.tools.test}`] : []),
144
+ ...(facts.tools.lint ? [`linter: ${facts.tools.lint}`] : []),
145
+ ...(facts.tools.format ? [`formatter: ${facts.tools.format}`] : []),
146
+ ];
147
+ if (lines.length === 0)
148
+ return '';
149
+ return ['<stack>', "Versions this workspace's files state (read from the files, not guessed):", ...lines, '</stack>'].join('\n');
150
+ }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { createRequire } from 'node:module';
2
3
  import path from 'node:path';
3
4
  import Parser from 'web-tree-sitter';
@@ -44,6 +45,47 @@ const KIND_BY_NODE_TYPE = {
44
45
  method_definition: 'method',
45
46
  method_signature: 'method',
46
47
  };
48
+ /**
49
+ * The folder of a bundle that carries its wasm files beside it, or null. The VS Code extension's build copies the
50
+ * runtime and grammar wasm into `dist/` next to `extension.js`, and its VSIX has no node_modules to resolve from.
51
+ * `typeof` keeps this safe under real ESM, where `__dirname` is not defined.
52
+ */
53
+ function bundledWasmDir() {
54
+ if (typeof __dirname !== 'string')
55
+ return null;
56
+ return existsSync(path.join(__dirname, 'tree-sitter.wasm')) ? __dirname : null;
57
+ }
58
+ /** web-tree-sitter's runtime wasm, beside the bundle or resolved from the package; throws when this build does not carry it. */
59
+ function treeSitterWasm() {
60
+ const bundled = bundledWasmDir();
61
+ if (bundled)
62
+ return path.join(bundled, 'tree-sitter.wasm');
63
+ let file;
64
+ try {
65
+ file = path.join(path.dirname(getRequire().resolve('web-tree-sitter/package.json')), 'tree-sitter.wasm');
66
+ }
67
+ catch {
68
+ throw new Error('Symbol tools are not available in this build: tree-sitter (web-tree-sitter) is not installed. Use search_files instead.');
69
+ }
70
+ if (!existsSync(file)) {
71
+ throw new Error(`Symbol tools are not available in this build: ${file} is missing. Use search_files instead.`);
72
+ }
73
+ return file;
74
+ }
75
+ /** A grammar's wasm, beside the bundle or from `tree-sitter-wasms`; throws when this build does not carry it. */
76
+ function grammarWasm(grammarFile) {
77
+ let file;
78
+ try {
79
+ file = path.join(bundledWasmDir() ?? path.join(path.dirname(getRequire().resolve('tree-sitter-wasms/package.json')), 'out'), grammarFile);
80
+ }
81
+ catch {
82
+ throw new Error('Symbol tools are not available in this build: tree-sitter grammars (tree-sitter-wasms) are not installed. Use search_files instead.');
83
+ }
84
+ if (!existsSync(file)) {
85
+ throw new Error(`Symbol tools are not available in this build: ${file} is missing. Use search_files instead.`);
86
+ }
87
+ return file;
88
+ }
47
89
  let parserInitialized = false;
48
90
  const languageCache = new Map();
49
91
  async function loadLanguage(ext) {
@@ -54,11 +96,14 @@ async function loadLanguage(ext) {
54
96
  if (cached)
55
97
  return cached;
56
98
  if (!parserInitialized) {
57
- await Parser.init();
99
+ // Point web-tree-sitter at its own wasm. Left to itself it looks next to the running script, which in the
100
+ // extension's one-file CommonJS bundle is dist/, and aborts when the file is not there: the promise never
101
+ // settles and every symbol tool call hangs (RB-40). A missing wasm is a clear, immediate error instead.
102
+ const wasm = treeSitterWasm();
103
+ await Parser.init({ locateFile: () => wasm });
58
104
  parserInitialized = true;
59
105
  }
60
- const wasmDir = path.join(path.dirname(getRequire().resolve('tree-sitter-wasms/package.json')), 'out');
61
- const lang = await Parser.Language.load(path.join(wasmDir, grammarFile));
106
+ const lang = await Parser.Language.load(grammarWasm(grammarFile));
62
107
  languageCache.set(grammarFile, lang);
63
108
  return lang;
64
109
  }
@@ -49,11 +49,7 @@ export interface PullRequestReport {
49
49
  }
50
50
  declare const readParams: z.ZodObject<{
51
51
  number: z.ZodOptional<z.ZodNumber>;
52
- }, "strip", z.ZodTypeAny, {
53
- number?: number | undefined;
54
- }, {
55
- number?: number | undefined;
56
- }>;
52
+ }, z.core.$strip>;
57
53
  export declare function createGitHubReadTool(cfg: GitHubConfig): ToolDefinition<z.infer<typeof readParams>, PullRequestReport>;
58
54
  declare const createParams: z.ZodObject<{
59
55
  title: z.ZodString;
@@ -61,19 +57,7 @@ declare const createParams: z.ZodObject<{
61
57
  base: z.ZodOptional<z.ZodString>;
62
58
  head: z.ZodOptional<z.ZodString>;
63
59
  draft: z.ZodOptional<z.ZodBoolean>;
64
- }, "strip", z.ZodTypeAny, {
65
- body: string;
66
- title: string;
67
- draft?: boolean | undefined;
68
- base?: string | undefined;
69
- head?: string | undefined;
70
- }, {
71
- body: string;
72
- title: string;
73
- draft?: boolean | undefined;
74
- base?: string | undefined;
75
- head?: string | undefined;
76
- }>;
60
+ }, z.core.$strip>;
77
61
  export declare function createGitHubCreateTool(cfg: GitHubConfig): ToolDefinition<z.infer<typeof createParams>, {
78
62
  number: number;
79
63
  url: string;
package/dist/index.d.ts CHANGED
@@ -42,7 +42,7 @@ export { createLanguageModel, formatModelId, pickAvailableProvider, resolveModel
42
42
  export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
43
43
  export type { Session, ListEntry, FormatOptions, SessionUpdate, WatchSessionOptions, SessionMatch, SearchSessionsOptions, SessionPruneOptions, SessionPruneResult } from './sessions/index.js';
44
44
  export { commandDirs, expandCustomCommand, formatCustomCommands, loadCustomCommands, parseCommandFile, type CustomCommand, } from './commands/custom-commands.js';
45
- export { BUILTIN_TOOLS, editFileTool, fetchUrlTool, findSymbolTool, listFilesTool, memoryReadTool, memoryForgetTool, memoryWriteTool, readFileTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny, redact, countRedactions, buildRules, ToolResultCache, buildCacheKey, cachedExecute, } from './tools/index.js';
45
+ export { BUILTIN_TOOLS, editFileTool, fetchUrlTool, findSymbolTool, gitDiffTool, dependencyAuditTool, listFilesTool, memoryReadTool, memoryForgetTool, memoryWriteTool, readFileTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny, redact, countRedactions, buildRules, ToolResultCache, buildCacheKey, cachedExecute, } from './tools/index.js';
46
46
  export type { AgentConfig, AgentEvent, AgentRunOptions, AgentRunReason, AgentRunResult, AnyLanguageModel, ApprovalDecision, CommandRunner, ModelRouter, VerifyConfig, ApprovalRequest, LanguageModelFactory, ModelConfig, PricePerMillionTokens, ProviderName, RedactOptions, TodoItem, ToolContext, UserQuestion, ToolDefinition, ToolPolicy, ToolSandbox, } from './types.js';
47
47
  export type { CacheKeyParts, CacheStats } from './tools/index.js';
48
48
  export type { RedactionRule, RedactionResult } from './tools/index.js';
@@ -82,7 +82,7 @@ export { CheckpointStore, type CheckpointStoreLimits } from './checkpoints/index
82
82
  export type { CaptureOptions, Checkpoint, CheckpointFile, CheckpointFileStatus, CheckpointSummary, CheckpointTrigger, PruneResult, RestoreResult, } from './checkpoints/index.js';
83
83
  export { SpecStore, isSafeSpecId, runKey, slugifyTaskForSpecId, SPEC_READONLY_TOOLS, splitRequirementsDesign, splitSpecSections, splitTasksSection, convertToCheckboxes, parseTasks, setTaskDone, changedSince, changeSize, keepDoneTasks, referencedPaths, specDrift, syncSpecTask } from './specs/index.js';
84
84
  export type { SpecDraft, SpecRunRecord, SpecRuns, SpecSections, SpecSummary, SpecTask, SpecDrift } from './specs/index.js';
85
- export { BUILTIN_SPECIALISTS, defaultConfigPath, effectiveToolsFor, BOARD_TOOL_NAMES, createBoardTools, DEFAULT_BOARD_CAP, getRegisteredSpecialist, getSpecialist, MessageBoard, listSpecialists, loadSpecialistsFromConfig, parseRouterOutput, registerSpecialist, resolveEffectiveModel, ROUTER_SYSTEM_PROMPT, runOrchestrator, runRouter, saveSpecialistsToConfig, unregisterSpecialist, validateSpecialistConfig, _resetSpecialistsForTests, _customSpecialistCount, } from './orchestrator/index.js';
85
+ export { BUILTIN_SPECIALISTS, defaultConfigPath, effectiveToolsFor, BOARD_TOOL_NAMES, createBoardTools, DEFAULT_BOARD_CAP, getRegisteredSpecialist, getSpecialist, MessageBoard, listSpecialists, loadSpecialistsFromConfig, parseRouterOutput, registerSpecialist, resolveEffectiveModel, ROUTER_SYSTEM_PROMPT, specialistAgentConfig, runOrchestrator, runRouter, saveSpecialistsToConfig, unregisterSpecialist, validateSpecialistConfig, _resetSpecialistsForTests, _customSpecialistCount, } from './orchestrator/index.js';
86
86
  export type { BoardMessage } from './orchestrator/index.js';
87
87
  export type { BuiltInSpecialistId, CustomSpecialistConfig, LoadSpecialistsResult, OrchestratorOptions, OrchestratorPlan, OrchestratorProgressEvent, OrchestratorRunResult, OrchestratorStep, OrchestratorStepResult, SpecialistConfig, SpecialistId, SpecialistsConfigFile, } from './orchestrator/index.js';
88
88
  export { computeUnifiedDiff, computeDiffSummary, type DiffOptions, type DiffResult, type DiffSummary, } from './diff/index.js';
@@ -95,6 +95,9 @@ export { formatSteeringLine, isSafeSteeringName, loadSteering, overrideFilePath,
95
95
  export type { StartSelection, SteeringConfig, SteeringDoc, SteeringInclusion, SteeringLoadResult, SteeringSkip, } from './steering/index.js';
96
96
  export { CONFIG_KEY_COVERAGE, FORGE_CONFIG_SCHEMA } from './config-schema.js';
97
97
  export { FILE_WRITE_TOOL_NAMES, filesTouchedBy } from './tools/write-tools.js';
98
+ export { isSafeGitRef, MAX_DIFF_CHARS } from './tools/git-diff.js';
99
+ export { detectStackFacts, stackBlock, MAX_STACK_LIBRARIES, type StackFacts } from './agent/stack-facts.js';
100
+ export { pickAuditors, type AuditFinding, type AuditorPlan } from './tools/dependency-audit.js';
98
101
  export { shellEnv } from './tools/shell-env.js';
99
102
  export { previewPatch } from './tools/apply-patch.js';
100
103
  export type { PatchPreview } from './tools/apply-patch.js';
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpRes
26
26
  export { createLanguageModel, formatModelId, pickAvailableProvider, resolveModelFromEnv, resolveModelFromEnvAsync } from './providers.js';
27
27
  export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
28
28
  export { commandDirs, expandCustomCommand, formatCustomCommands, loadCustomCommands, parseCommandFile, } from './commands/custom-commands.js';
29
- export { BUILTIN_TOOLS, editFileTool, fetchUrlTool, findSymbolTool, listFilesTool, memoryReadTool, memoryForgetTool, memoryWriteTool, readFileTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny, redact, countRedactions, buildRules, ToolResultCache, buildCacheKey, cachedExecute, } from './tools/index.js';
29
+ export { BUILTIN_TOOLS, editFileTool, fetchUrlTool, findSymbolTool, gitDiffTool, dependencyAuditTool, listFilesTool, memoryReadTool, memoryForgetTool, memoryWriteTool, readFileTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny, redact, countRedactions, buildRules, ToolResultCache, buildCacheKey, cachedExecute, } from './tools/index.js';
30
30
  // Skills system
31
31
  export { buildSkillsBlock, builtinSkills, composeSkillsPrompt, discoverSkills, fetchSkillsCatalog, installSkillFromCatalog, loadSkillsRegistry, loadSkillsRegistryAsync, parseSkill, parseSkillsDir, resolveSkillRoots, scoreSkill, searchSkills, selectSkills, skillsRegistryFromList, DEFAULT_SKILL_CATALOG_URL, GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR, PROJECT_SKILLS_DIR, SKILL_CATALOG_VERSION, } from './skills/index.js';
32
32
  export { cosine, embedderFromEnv, hashingEmbedder, openAiCompatibleEmbedder, } from './semantic/embedder.js';
@@ -55,7 +55,7 @@ export { CheckpointStore } from './checkpoints/index.js';
55
55
  // Specs system (spec-driven planning mode)
56
56
  export { SpecStore, isSafeSpecId, runKey, slugifyTaskForSpecId, SPEC_READONLY_TOOLS, splitRequirementsDesign, splitSpecSections, splitTasksSection, convertToCheckboxes, parseTasks, setTaskDone, changedSince, changeSize, keepDoneTasks, referencedPaths, specDrift, syncSpecTask } from './specs/index.js';
57
57
  // Orchestrator system
58
- export { BUILTIN_SPECIALISTS, defaultConfigPath, effectiveToolsFor, BOARD_TOOL_NAMES, createBoardTools, DEFAULT_BOARD_CAP, getRegisteredSpecialist, getSpecialist, MessageBoard, listSpecialists, loadSpecialistsFromConfig, parseRouterOutput, registerSpecialist, resolveEffectiveModel, ROUTER_SYSTEM_PROMPT, runOrchestrator, runRouter, saveSpecialistsToConfig, unregisterSpecialist, validateSpecialistConfig, _resetSpecialistsForTests, _customSpecialistCount, } from './orchestrator/index.js';
58
+ export { BUILTIN_SPECIALISTS, defaultConfigPath, effectiveToolsFor, BOARD_TOOL_NAMES, createBoardTools, DEFAULT_BOARD_CAP, getRegisteredSpecialist, getSpecialist, MessageBoard, listSpecialists, loadSpecialistsFromConfig, parseRouterOutput, registerSpecialist, resolveEffectiveModel, ROUTER_SYSTEM_PROMPT, specialistAgentConfig, runOrchestrator, runRouter, saveSpecialistsToConfig, unregisterSpecialist, validateSpecialistConfig, _resetSpecialistsForTests, _customSpecialistCount, } from './orchestrator/index.js';
59
59
  // Diff utilities (line-level unified diff)
60
60
  export { computeUnifiedDiff, computeDiffSummary, } from './diff/index.js';
61
61
  // Structured output (`--schema` mode)
@@ -75,6 +75,9 @@ export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeCon
75
75
  export { formatSteeringLine, isSafeSteeringName, loadSteering, overrideFilePath, parseSteering, pathMatchesGlob, readOverride, selectAtStart, selectOnToolTouch, setSteeringDisabled, stripSteering, wrapSteering, } from './steering/index.js';
76
76
  export { CONFIG_KEY_COVERAGE, FORGE_CONFIG_SCHEMA } from './config-schema.js';
77
77
  export { FILE_WRITE_TOOL_NAMES, filesTouchedBy } from './tools/write-tools.js';
78
+ export { isSafeGitRef, MAX_DIFF_CHARS } from './tools/git-diff.js';
79
+ export { detectStackFacts, stackBlock, MAX_STACK_LIBRARIES } from './agent/stack-facts.js';
80
+ export { pickAuditors } from './tools/dependency-audit.js';
78
81
  export { shellEnv } from './tools/shell-env.js';
79
82
  export { previewPatch } from './tools/apply-patch.js';
80
83
  export { applyPatchTool, parsePatch, patchedPaths } from './tools/apply-patch.js';
@@ -1,3 +1,16 @@
1
+ /**
2
+ * One pass is not enough: removing the tag from `</journey-</journey-answers>answers>`
3
+ * joins the halves into a fresh one. Repeat until nothing changes.
4
+ */
5
+ function stripClosingTag(text) {
6
+ let prev;
7
+ let out = text;
8
+ do {
9
+ prev = out;
10
+ out = out.replace(/<\/journey-answers>/gi, '');
11
+ } while (out !== prev);
12
+ return out;
13
+ }
1
14
  export function renderAnswersBlock(answers) {
2
15
  // A dismissed question comes back with an empty answer. Rendering it under
3
16
  // "these are decisions" would present an open question as settled, which
@@ -5,7 +18,7 @@ export function renderAnswersBlock(answers) {
5
18
  const answered = answers.filter((a) => a.answer.trim() !== '');
6
19
  if (answered.length === 0)
7
20
  return '';
8
- const lines = answered.map((a) => `- ${a.question}: ${a.answer.replace(/<\/journey-answers>/gi, '')}`);
21
+ const lines = answered.map((a) => `- ${a.question}: ${stripClosingTag(a.answer)}`);
9
22
  return [
10
23
  '<journey-answers>',
11
24
  'The person answered these before work started. They are decisions, not suggestions:',
@@ -27,7 +27,8 @@ function areaOf(rel) {
27
27
  export function decideOffer(facts, verdict) {
28
28
  if (!verdict.survey)
29
29
  return { offer: false, evidence: [] };
30
- const areas = [...new Set(facts.relatedSymbols.map(areaOf))];
30
+ // A root file is not an area, so it neither counts toward MIN_AREAS nor appears in the evidence.
31
+ const areas = [...new Set(facts.relatedSymbols.map(areaOf))].filter((a) => a !== ROOT_AREA);
31
32
  if (facts.relatedSymbols.length === 0) {
32
33
  const where = facts.sourceFileCount === 0
33
34
  ? 'no source files here yet'
@@ -55,7 +55,18 @@ export function jsonSchemaToZod(schema) {
55
55
  const propZod = jsonSchemaToZod(propSchema);
56
56
  shape[key] = required.has(key) ? propZod : propZod.optional();
57
57
  }
58
- inner = z.object(shape).passthrough();
58
+ const objectZod = z.object(shape).passthrough();
59
+ // Zod 4 reads a missing `__proto__` key off the prototype chain and skips the property, so a required one is checked on the raw input first.
60
+ inner =
61
+ required.has('__proto__') && Object.prototype.hasOwnProperty.call(s.properties, '__proto__')
62
+ ? z
63
+ .any()
64
+ .refine((v) => typeof v === 'object' && v !== null && Object.prototype.hasOwnProperty.call(v, '__proto__'), {
65
+ message: 'Invalid input: required property "__proto__" is missing',
66
+ path: ['__proto__'],
67
+ })
68
+ .pipe(objectZod)
69
+ : objectZod;
59
70
  }
60
71
  else if (s.additionalProperties === true) {
61
72
  inner = z.record(z.string(), z.any());
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { BUILTIN_SPECIALISTS, getSpecialist, effectiveToolsFor } from './specialists.js';
9
9
  export { parseRouterOutput, ROUTER_SYSTEM_PROMPT } from './router.js';
10
- export { runOrchestrator, runRouter, groupStepsByParallel, resolveEffectiveModel, _dispatchGroup, type OrchestratorOptions, type OrchestratorProgressEvent, type OrchestratorWorktreeMode, } from './orchestrator.js';
10
+ export { runOrchestrator, runRouter, groupStepsByParallel, resolveEffectiveModel, specialistAgentConfig, handoffBlock, MAX_HANDOFF_CHARS, _dispatchGroup, type OrchestratorOptions, type StepLimits, type OrchestratorProgressEvent, type OrchestratorWorktreeMode, } from './orchestrator.js';
11
11
  export { MessageBoard, DEFAULT_BOARD_CAP, type BoardMessage } from './board.js';
12
12
  export { createBoardTools, BOARD_TOOL_NAMES } from './board-tools.js';
13
13
  export { registerSpecialist, unregisterSpecialist, listSpecialists, getRegisteredSpecialist, loadSpecialistsFromConfig, saveSpecialistsToConfig, validateSpecialistConfig, defaultConfigPath, _resetSpecialistsForTests, _customSpecialistCount, type CustomSpecialistConfig, type SpecialistsConfigFile, type LoadSpecialistsResult, } from './specialists-registry.js';
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { BUILTIN_SPECIALISTS, getSpecialist, effectiveToolsFor } from './specialists.js';
9
9
  export { parseRouterOutput, ROUTER_SYSTEM_PROMPT } from './router.js';
10
- export { runOrchestrator, runRouter, groupStepsByParallel, resolveEffectiveModel, _dispatchGroup, } from './orchestrator.js';
10
+ export { runOrchestrator, runRouter, groupStepsByParallel, resolveEffectiveModel, specialistAgentConfig, handoffBlock, MAX_HANDOFF_CHARS, _dispatchGroup, } from './orchestrator.js';
11
11
  export { MessageBoard, DEFAULT_BOARD_CAP } from './board.js';
12
12
  export { createBoardTools, BOARD_TOOL_NAMES } from './board-tools.js';
13
13
  export { registerSpecialist, unregisterSpecialist, listSpecialists, getRegisteredSpecialist, loadSpecialistsFromConfig, saveSpecialistsToConfig, validateSpecialistConfig, defaultConfigPath, _resetSpecialistsForTests, _customSpecialistCount, } from './specialists-registry.js';
@@ -29,7 +29,8 @@
29
29
  * AND the step is part of a parallel group. Solo steps never get
30
30
  * a worktree (it's wasted overhead).
31
31
  */
32
- import type { AgentConfig, AgentRunResult, ModelConfig } from '../types.js';
32
+ import type { PermissionConfig } from '../tools/permission-rules.js';
33
+ import type { AgentConfig, AgentRunResult, ModelConfig, ToolSandbox } from '../types.js';
33
34
  import { MessageBoard, type BoardMessage } from './board.js';
34
35
  import type { OrchestratorPlan, OrchestratorRunResult, OrchestratorStep, OrchestratorStepResult, SpecialistConfig } from './types.js';
35
36
  /**
@@ -63,6 +64,12 @@ export interface OrchestratorOptions {
63
64
  maxParallelism?: number;
64
65
  /** Approval callback for the router (specialists don't ask the user: they're scoped). */
65
66
  approve?: AgentConfig['approve'];
67
+ /** Tools a step runs without asking (the person's `autoApproveTools`); default: the read-only tools. */
68
+ autoApproveTools?: readonly string[];
69
+ /** The person's permission rules, kept by every step. */
70
+ permissionRules?: PermissionConfig;
71
+ /** The person's tool sandbox; a step's tools are narrowed to its `allowTools`. */
72
+ toolSandbox?: ToolSandbox;
66
73
  /** Optional modelFactory override (for tests). */
67
74
  modelFactory?: AgentConfig['modelFactory'];
68
75
  /**
@@ -180,11 +187,7 @@ export declare function runOrchestrator(options: OrchestratorOptions): Promise<O
180
187
  * git worktree. When `always`, every step gets a worktree regardless
181
188
  * of group size. `never` disables the feature.
182
189
  */
183
- export declare function _dispatchGroup(group: OrchestratorStep[], base: {
184
- model: ModelConfig;
185
- cwd?: string;
186
- modelFactory?: AgentConfig['modelFactory'];
187
- }, dispatchOpts: {
190
+ export declare function _dispatchGroup(group: OrchestratorStep[], base: StepBase, dispatchOpts: {
188
191
  worktrees: OrchestratorWorktreeMode;
189
192
  cleanupBranches?: boolean;
190
193
  commitChanges?: boolean;
@@ -195,6 +198,8 @@ export declare function _dispatchGroup(group: OrchestratorStep[], base: {
195
198
  }) => void;
196
199
  onWorktreeEvent?: OrchestratorOptions['onWorktreeEvent'];
197
200
  board?: MessageBoard;
201
+ /** Results of the groups that ran before this one, handed to every step of this group. */
202
+ prior?: readonly OrchestratorStepResult[];
198
203
  }, maxParallelism: number): Promise<OrchestratorStepResult[]>;
199
204
  /**
200
205
  * Run the router agent. Exposed for testing + for the
@@ -233,3 +238,37 @@ export declare function runRouter(args: {
233
238
  * without going through `Agent.run()`.
234
239
  */
235
240
  export declare function resolveEffectiveModel(spec: SpecialistConfig, baseModel: ModelConfig): ModelConfig;
241
+ /** The most of one earlier step's reply handed to a later step. */
242
+ export declare const MAX_HANDOFF_CHARS = 6000;
243
+ /**
244
+ * What the earlier steps of an orchestrator run produced, for a later step. Without it each step saw only the
245
+ * line the router wrote before any step ran, so in "plan, then code" Code never saw Plan's plan.
246
+ */
247
+ export declare function handoffBlock(prior: readonly OrchestratorStepResult[]): string;
248
+ /** What a step inherits from the run: the model, where it works, and the limits the person set. */
249
+ interface StepBase {
250
+ model: ModelConfig;
251
+ cwd?: string;
252
+ modelFactory?: AgentConfig['modelFactory'];
253
+ limits?: StepLimits;
254
+ }
255
+ /** The person's limits a step must keep: a step has no one to ask, so these decide what it may do (RB-14, RB-39). */
256
+ export interface StepLimits {
257
+ autoApproveTools?: readonly string[];
258
+ permissionRules?: PermissionConfig;
259
+ toolSandbox?: ToolSandbox;
260
+ }
261
+ /**
262
+ * The Agent configuration for one orchestrator step. The step sees exactly its agent's effective tools
263
+ * (builtins off, the sandbox narrowed to that list), and runs its read-only tools without asking, as a
264
+ * `sub_agent` child does: there is no one to ask, so without the list every such call is refused.
265
+ */
266
+ export declare function specialistAgentConfig(args: {
267
+ spec: SpecialistConfig;
268
+ model: ModelConfig;
269
+ cwd?: string;
270
+ modelFactory?: AgentConfig['modelFactory'];
271
+ board?: MessageBoard;
272
+ limits: StepLimits;
273
+ }): AgentConfig;
274
+ export {};