@ikarmic/forge-core 5.1.1 → 5.3.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 (81) hide show
  1. package/dist/agent/agent.js +208 -57
  2. package/dist/agent/enhance-prompt.js +2 -1
  3. package/dist/agent/goal.d.ts +5 -8
  4. package/dist/agent/prompts.d.ts +15 -0
  5. package/dist/agent/prompts.js +34 -0
  6. package/dist/agent/race-abort.d.ts +8 -0
  7. package/dist/agent/race-abort.js +24 -0
  8. package/dist/agent/retry.d.ts +50 -0
  9. package/dist/agent/retry.js +192 -0
  10. package/dist/agent/shell-command.d.ts +23 -0
  11. package/dist/agent/shell-command.js +93 -0
  12. package/dist/agent/stack-facts.d.ts +23 -0
  13. package/dist/agent/stack-facts.js +150 -0
  14. package/dist/catalog/models-dev-snapshot.d.ts +1 -0
  15. package/dist/catalog/models-dev-snapshot.js +3 -0
  16. package/dist/catalog/models-dev.d.ts +35 -0
  17. package/dist/catalog/models-dev.js +143 -0
  18. package/dist/code-index/parser.js +48 -3
  19. package/dist/context/compaction.js +8 -2
  20. package/dist/git/auto-commit.d.ts +4 -2
  21. package/dist/git/auto-commit.js +16 -5
  22. package/dist/github/github-tools.d.ts +2 -18
  23. package/dist/index.d.ts +11 -3
  24. package/dist/index.js +9 -2
  25. package/dist/journey/answers-block.js +14 -1
  26. package/dist/journey/offer.js +2 -1
  27. package/dist/mcp/json-schema-to-zod.js +12 -1
  28. package/dist/memory/session-digest.js +2 -1
  29. package/dist/orchestrator/index.d.ts +1 -1
  30. package/dist/orchestrator/index.js +1 -1
  31. package/dist/orchestrator/orchestrator.d.ts +45 -6
  32. package/dist/orchestrator/orchestrator.js +62 -12
  33. package/dist/orchestrator/router.d.ts +1 -1
  34. package/dist/orchestrator/router.js +4 -1
  35. package/dist/orchestrator/specialists.d.ts +6 -1
  36. package/dist/orchestrator/specialists.js +165 -68
  37. package/dist/orchestrator/types.d.ts +1 -1
  38. package/dist/policy/policy.d.ts +6 -59
  39. package/dist/pricing.d.ts +2 -0
  40. package/dist/pricing.js +47 -16
  41. package/dist/structured/schema.js +9 -12
  42. package/dist/tools/apply-patch.d.ts +1 -5
  43. package/dist/tools/ask-user.d.ts +2 -10
  44. package/dist/tools/ask-user.js +12 -4
  45. package/dist/tools/browser-tools.d.ts +15 -31
  46. package/dist/tools/codebase-search.d.ts +1 -7
  47. package/dist/tools/command-sandbox.d.ts +24 -0
  48. package/dist/tools/command-sandbox.js +73 -0
  49. package/dist/tools/dependency-audit.d.ts +30 -0
  50. package/dist/tools/dependency-audit.js +197 -0
  51. package/dist/tools/edit-file.d.ts +9 -15
  52. package/dist/tools/edit-file.js +64 -16
  53. package/dist/tools/fetch-url.d.ts +1 -18
  54. package/dist/tools/find-references.d.ts +1 -5
  55. package/dist/tools/find-symbol.d.ts +10 -8
  56. package/dist/tools/git-diff.d.ts +13 -0
  57. package/dist/tools/git-diff.js +85 -0
  58. package/dist/tools/index.d.ts +3 -1
  59. package/dist/tools/index.js +5 -1
  60. package/dist/tools/list-files.d.ts +1 -19
  61. package/dist/tools/list-symbols.d.ts +1 -5
  62. package/dist/tools/memory.d.ts +23 -43
  63. package/dist/tools/notebook.d.ts +11 -17
  64. package/dist/tools/read-file.d.ts +1 -11
  65. package/dist/tools/read-image.d.ts +1 -12
  66. package/dist/tools/read-only.d.ts +1 -1
  67. package/dist/tools/read-only.js +2 -0
  68. package/dist/tools/run-command.d.ts +1 -17
  69. package/dist/tools/run-command.js +12 -2
  70. package/dist/tools/search-files.d.ts +1 -17
  71. package/dist/tools/search-files.js +8 -49
  72. package/dist/tools/search-worker.d.ts +23 -0
  73. package/dist/tools/search-worker.js +77 -0
  74. package/dist/tools/sub-agent-wait.d.ts +2 -8
  75. package/dist/tools/sub-agent.d.ts +2 -70
  76. package/dist/tools/sub-agent.js +1 -0
  77. package/dist/tools/todo.d.ts +8 -20
  78. package/dist/tools/web-search.d.ts +1 -7
  79. package/dist/tools/write-file.d.ts +1 -7
  80. package/dist/types.d.ts +60 -1
  81. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { Agent, applySandbox } from './agent/agent.js';
2
2
  export { LoopGuard } from './agent/loop-guard.js';
3
3
  export { enhancePrompt } from './agent/enhance-prompt.js';
4
+ export { generateShellCommand, shellKindOf, commandRisk } from './agent/shell-command.js';
4
5
  export { stripThinkTags } from './agent/think-tags.js';
5
6
  export { createGitHubCreateTool, createGitHubReadTool, createGitHubTools, githubFromEnv, parseRemote, } from './github/github-tools.js';
6
7
  export { createWebSearchTool, searchWeb, WEB_SEARCH_PROVIDERS, webSearchFromEnv, webSearchProblem, } from './tools/web-search.js';
@@ -26,7 +27,7 @@ export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpRes
26
27
  export { createLanguageModel, formatModelId, pickAvailableProvider, resolveModelFromEnv, resolveModelFromEnvAsync } from './providers.js';
27
28
  export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
28
29
  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';
30
+ 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
31
  // Skills system
31
32
  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
33
  export { cosine, embedderFromEnv, hashingEmbedder, openAiCompatibleEmbedder, } from './semantic/embedder.js';
@@ -55,7 +56,7 @@ export { CheckpointStore } from './checkpoints/index.js';
55
56
  // Specs system (spec-driven planning mode)
56
57
  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
58
  // 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';
59
+ 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
60
  // Diff utilities (line-level unified diff)
60
61
  export { computeUnifiedDiff, computeDiffSummary, } from './diff/index.js';
61
62
  // Structured output (`--schema` mode)
@@ -75,8 +76,14 @@ export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeCon
75
76
  export { formatSteeringLine, isSafeSteeringName, loadSteering, overrideFilePath, parseSteering, pathMatchesGlob, readOverride, selectAtStart, selectOnToolTouch, setSteeringDisabled, stripSteering, wrapSteering, } from './steering/index.js';
76
77
  export { CONFIG_KEY_COVERAGE, FORGE_CONFIG_SCHEMA } from './config-schema.js';
77
78
  export { FILE_WRITE_TOOL_NAMES, filesTouchedBy } from './tools/write-tools.js';
79
+ export { isSafeGitRef, MAX_DIFF_CHARS } from './tools/git-diff.js';
80
+ export { detectStackFacts, stackBlock, MAX_STACK_LIBRARIES } from './agent/stack-facts.js';
81
+ export { pickAuditors } from './tools/dependency-audit.js';
78
82
  export { shellEnv } from './tools/shell-env.js';
79
83
  export { previewPatch } from './tools/apply-patch.js';
84
+ export { applyTextEdit } from './tools/edit-file.js';
85
+ export { catalogEntry, loadModelCatalog } from './catalog/models-dev.js';
86
+ export { isContextOverflowError, retryDecision, withModelRetry } from './agent/retry.js';
80
87
  export { applyPatchTool, parsePatch, patchedPaths } from './tools/apply-patch.js';
81
88
  export { buildEmbedder } from './semantic/embedder.js';
82
89
  export { parseExtensions } from './semantic/semantic-index.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());
@@ -1,4 +1,5 @@
1
1
  import { generateText } from 'ai';
2
+ import { withModelRetry } from '../agent/retry.js';
2
3
  import { stripThinkTags } from '../agent/think-tags.js';
3
4
  import { createLanguageModel } from '../providers.js';
4
5
  const MAX_DIGEST_CHARS = 600;
@@ -38,7 +39,7 @@ export async function generateSessionDigest(items, model, modelFactory = createL
38
39
  if (!model || !shouldDigest(items))
39
40
  return undefined;
40
41
  try {
41
- const result = await generateText({ model: modelFactory(model), prompt: `${INSTRUCTION}\n\nConversation:\n${transcript(items)}` });
42
+ const result = await withModelRetry(() => generateText({ model: modelFactory(model), prompt: `${INSTRUCTION}\n\nConversation:\n${transcript(items)}`, maxRetries: 0 }));
42
43
  return cleanDigest(result.text);
43
44
  }
44
45
  catch {
@@ -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 {};
@@ -31,12 +31,14 @@
31
31
  */
32
32
  import { randomBytes } from 'node:crypto';
33
33
  import { Agent } from '../agent/agent.js';
34
+ import { NO_TOOLS_SENTINEL } from '../journey/stages.js';
34
35
  import { BUILTIN_TOOLS } from '../tools/index.js';
36
+ import { READ_ONLY_TOOLS } from '../tools/read-only.js';
35
37
  import { commitAllInWorktree } from '../worktree/branch-ops.js';
36
38
  import { WorktreeError, WorktreeManager } from '../worktree/index.js';
37
39
  import { MessageBoard } from './board.js';
38
40
  import { createBoardTools } from './board-tools.js';
39
- import { getSpecialist, effectiveToolsFor } from './specialists.js';
41
+ import { BUILTIN_SPECIALISTS, getSpecialist, effectiveToolsFor } from './specialists.js';
40
42
  import { parseRouterOutput, ROUTER_SYSTEM_PROMPT } from './router.js';
41
43
  /** Default cap on how many specialists can run at once within a group. */
42
44
  const DEFAULT_MAX_PARALLELISM = 5;
@@ -134,6 +136,11 @@ export async function runOrchestrator(options) {
134
136
  model: options.model,
135
137
  cwd: options.cwd,
136
138
  modelFactory: options.modelFactory,
139
+ limits: {
140
+ ...(options.autoApproveTools ? { autoApproveTools: options.autoApproveTools } : {}),
141
+ ...(options.permissionRules ? { permissionRules: options.permissionRules } : {}),
142
+ ...(options.toolSandbox ? { toolSandbox: options.toolSandbox } : {}),
143
+ },
137
144
  };
138
145
  const board = new MessageBoard();
139
146
  if (options.onBoardEvent)
@@ -158,7 +165,8 @@ export async function runOrchestrator(options) {
158
165
  };
159
166
  const groups = groupStepsByParallel(plan.steps);
160
167
  for (const group of groups) {
161
- const groupResults = await _dispatchGroup(group, base, dispatchOpts, maxParallelism);
168
+ // Steps in one group run in parallel and do not see each other; they see every group before them.
169
+ const groupResults = await _dispatchGroup(group, base, { ...dispatchOpts, prior: [...stepResults] }, maxParallelism);
162
170
  for (const result of groupResults) {
163
171
  stepResults.push(result);
164
172
  totalIn += result.inputTokens;
@@ -362,7 +370,7 @@ async function runSpecialistStepInner(step, base, dispatchOpts, isParallelGroup,
362
370
  inputTokens: 0,
363
371
  outputTokens: 0,
364
372
  costUsd: 0,
365
- error: `unknown specialist "${step.specialist}": known: plan, code, debug, ask, review`,
373
+ error: `unknown specialist "${step.specialist}": known: ${BUILTIN_SPECIALISTS.map((s) => s.id).join(', ')}`,
366
374
  };
367
375
  }
368
376
  // Worktree setup: same fallback pattern as `subAgentTool`:
@@ -408,12 +416,14 @@ async function runSpecialistStepInner(step, base, dispatchOpts, isParallelGroup,
408
416
  const effectiveCwd = worktree ? worktree.path : base.cwd;
409
417
  const effectiveModel = resolveEffectiveModel(spec, base.model);
410
418
  try {
419
+ const handoff = handoffBlock(dispatchOpts.prior ?? []);
411
420
  const result = await runSpecialist({
412
421
  spec,
413
- task: step.task,
422
+ task: handoff ? `${step.task}\n\n${handoff}` : step.task,
414
423
  model: effectiveModel,
415
424
  cwd: effectiveCwd,
416
425
  modelFactory: base.modelFactory,
426
+ limits: base.limits ?? {},
417
427
  ...(dispatchOpts.board ? { board: dispatchOpts.board } : {}),
418
428
  });
419
429
  if (dispatchOpts.commitChanges && worktree) {
@@ -474,23 +484,63 @@ async function runSpecialistStepInner(step, base, dispatchOpts, isParallelGroup,
474
484
  }
475
485
  }
476
486
  }
477
- /** Construct + run a specialist sub-agent. */
478
- async function runSpecialist(args) {
479
- const allowedSet = new Set(effectiveToolsFor(args.spec));
487
+ /** The most of one earlier step's reply handed to a later step. */
488
+ export const MAX_HANDOFF_CHARS = 6000;
489
+ /** Remove the block's closing tag until none is left, so a reply cannot end the block early (RB-35). */
490
+ function stripHandoffTag(text) {
491
+ let out = text;
492
+ for (let prev = ''; prev !== out;) {
493
+ prev = out;
494
+ out = out.replace(/<\/previous-steps>/gi, '');
495
+ }
496
+ return out;
497
+ }
498
+ /**
499
+ * What the earlier steps of an orchestrator run produced, for a later step. Without it each step saw only the
500
+ * line the router wrote before any step ran, so in "plan, then code" Code never saw Plan's plan.
501
+ */
502
+ export function handoffBlock(prior) {
503
+ if (prior.length === 0)
504
+ return '';
505
+ const parts = prior.map((r, i) => {
506
+ const reply = stripHandoffTag(r.finalText.trim());
507
+ const capped = reply.length > MAX_HANDOFF_CHARS ? `${reply.slice(0, MAX_HANDOFF_CHARS)}\n[cut: ${reply.length - MAX_HANDOFF_CHARS} characters left out]` : reply;
508
+ const status = r.error ? `failed: ${stripHandoffTag(r.error)}` : r.reason;
509
+ return `## Step ${i + 1}: ${r.step.specialist} (${status})\nTask: ${stripHandoffTag(r.step.task)}\n\n${capped || '(no reply)'}`;
510
+ });
511
+ return ['<previous-steps>', 'Earlier steps of this task, in order. Build on their results; do not redo their work.', '', ...parts, '</previous-steps>'].join('\n');
512
+ }
513
+ /**
514
+ * The Agent configuration for one orchestrator step. The step sees exactly its agent's effective tools
515
+ * (builtins off, the sandbox narrowed to that list), and runs its read-only tools without asking, as a
516
+ * `sub_agent` child does: there is no one to ask, so without the list every such call is refused.
517
+ */
518
+ export function specialistAgentConfig(args) {
519
+ const effective = effectiveToolsFor(args.spec);
520
+ const offered = new Set(effective);
480
521
  const tools = [
481
- ...BUILTIN_TOOLS.filter((t) => allowedSet.has(t.name)),
482
- ...(args.board ? createBoardTools(args.board, args.spec.id).filter((t) => allowedSet.has(t.name)) : []),
522
+ ...BUILTIN_TOOLS.filter((t) => offered.has(t.name)),
523
+ ...(args.board ? createBoardTools(args.board, args.spec.id).filter((t) => offered.has(t.name)) : []),
483
524
  ];
484
- const config = {
525
+ const parentAllow = args.limits.toolSandbox?.allowTools;
526
+ const narrowed = parentAllow ? effective.filter((t) => parentAllow.includes(t)) : effective;
527
+ return {
485
528
  model: args.model,
486
529
  systemPrompt: args.spec.systemPrompt,
487
530
  ...(args.cwd ? { cwd: args.cwd } : {}),
488
531
  auto: args.spec.auto ?? false,
489
532
  maxIterations: args.spec.maxIterations ?? 10,
490
533
  extraTools: tools,
491
- disableBuiltins: false,
534
+ disableBuiltins: true,
535
+ autoApproveTools: args.limits.autoApproveTools ? [...args.limits.autoApproveTools] : [...READ_ONLY_TOOLS],
536
+ ...(args.limits.permissionRules ? { permissionRules: args.limits.permissionRules } : {}),
537
+ // An empty allowTools means "no whitelist" to the sandbox, so "nothing left" is the sentinel (RB-29).
538
+ toolSandbox: { ...(args.limits.toolSandbox ?? {}), allowTools: narrowed.length > 0 ? narrowed : [NO_TOOLS_SENTINEL] },
492
539
  ...(args.modelFactory ? { modelFactory: args.modelFactory } : {}),
493
540
  };
494
- const agent = new Agent(config);
541
+ }
542
+ /** Construct + run a specialist sub-agent. */
543
+ async function runSpecialist(args) {
544
+ const agent = new Agent(specialistAgentConfig(args));
495
545
  return agent.run({ task: args.task });
496
546
  }
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { OrchestratorPlan } from './types.js';
15
15
  /** System prompt used by the router agent. */
16
- export declare const ROUTER_SYSTEM_PROMPT = "You are the IKarmic Forge ORCHESTRATOR. Your job is to read the user's task and decide which specialist(s) to dispatch to.\n\nAvailable specialists:\n- **plan** \u2014 read-only investigation, produces a structured plan as text\n- **code** \u2014 focused file edits, runs tests\n- **debug** \u2014 investigates failures and identifies the root cause\n- **ask** \u2014 pure Q&A, no file mutations\n- **review** \u2014 read-only code review, identifies issues + suggests fixes\n\nYour output MUST be a single JSON object (no prose around it) with this exact shape:\n\n```json\n{\n \"rationale\": \"One-sentence reason for this routing decision.\",\n \"steps\": [\n { \"specialist\": \"<id>\", \"task\": \"<focused task for the specialist>\", \"parallelGroup\": \"<optional tag>\" }\n ]\n}\n```\n\nRouting heuristics:\n- A clear bug report \u2192 1 step: debug\n- \"What does X do?\" or \"How does Y work?\" \u2192 1 step: ask\n- \"Implement X\" / \"Add feature Y\" \u2192 2 steps in order: plan, code (sequential, parallelGroup unset)\n- \"Review PR / diff / file\" \u2192 1 step: review\n- \"Refactor X\" \u2192 2 steps: plan, code\n- Multi-area research (e.g. \"compare X and Y\") \u2192 2+ ask steps with the SAME parallelGroup so they run concurrently\n- \"Fix and add tests for X\" \u2192 2 steps: debug, code\n- If you're unsure, default to: plan, then code. Never dispatch the same specialist twice unless the user asks for revisions.\n\nAbout parallelGroup:\n- Set it ONLY when you want two or more consecutive steps to run concurrently.\n- Consecutive steps with the same parallelGroup run in parallel. A different (or missing) label between them breaks the group, so a later step with the same label will start a new group.\n- Examples:\n - [{\"ask\",\"how does auth work?\",\"parallelGroup\":\"research\"}, {\"ask\",\"how does billing work?\",\"parallelGroup\":\"research\"}] \u2192 both run concurrently.\n - [{\"ask\",\"q1\",\"parallelGroup\":\"x\"}, {\"ask\",\"q2\",\"parallelGroup\":\"x\"}, {\"ask\",\"q3\"}] \u2192 q1 and q2 run together, then q3 runs alone after them.\n- Do NOT use parallelGroup for steps with side effects on the same files (e.g. two 'code' specialists editing the same module) \u2014 they would race.\n\nConstraints:\n- All specialist ids must be one of: plan, code, debug, ask, review.\n- Tasks must be focused (1-3 sentences each).\n- Don't dispatch more than 5 steps \u2014 decompose is the user's job, not yours.";
16
+ export declare const ROUTER_SYSTEM_PROMPT: string;
17
17
  /**
18
18
  * Parse the router agent's final text into an {@link OrchestratorPlan}.
19
19
  * Tolerant of:
@@ -11,6 +11,7 @@
11
11
  * from a free-form model response. We strip ```json fences, try
12
12
  * JSON.parse, and accept the first object that has a steps array.
13
13
  */
14
+ import { BUILTIN_SPECIALISTS } from './specialists.js';
14
15
  /** System prompt used by the router agent. */
15
16
  export const ROUTER_SYSTEM_PROMPT = `You are the IKarmic Forge ORCHESTRATOR. Your job is to read the user's task and decide which specialist(s) to dispatch to.
16
17
 
@@ -20,6 +21,7 @@ Available specialists:
20
21
  - **debug** — investigates failures and identifies the root cause
21
22
  - **ask** — pure Q&A, no file mutations
22
23
  - **review** — read-only code review, identifies issues + suggests fixes
24
+ - **security** — finds exploitable security problems and vulnerable dependencies; changes files only for findings the user named
23
25
 
24
26
  Your output MUST be a single JSON object (no prose around it) with this exact shape:
25
27
 
@@ -37,6 +39,7 @@ Routing heuristics:
37
39
  - "What does X do?" or "How does Y work?" → 1 step: ask
38
40
  - "Implement X" / "Add feature Y" → 2 steps in order: plan, code (sequential, parallelGroup unset)
39
41
  - "Review PR / diff / file" → 1 step: review
42
+ - "Check for security problems / vulnerabilities / audit" → 1 step: security
40
43
  - "Refactor X" → 2 steps: plan, code
41
44
  - Multi-area research (e.g. "compare X and Y") → 2+ ask steps with the SAME parallelGroup so they run concurrently
42
45
  - "Fix and add tests for X" → 2 steps: debug, code
@@ -51,7 +54,7 @@ About parallelGroup:
51
54
  - Do NOT use parallelGroup for steps with side effects on the same files (e.g. two 'code' specialists editing the same module) — they would race.
52
55
 
53
56
  Constraints:
54
- - All specialist ids must be one of: plan, code, debug, ask, review.
57
+ - All specialist ids must be one of: ${BUILTIN_SPECIALISTS.map((s) => s.id).join(', ')}.
55
58
  - Tasks must be focused (1-3 sentences each).
56
59
  - Don't dispatch more than 5 steps — decompose is the user's job, not yours.`;
57
60
  /**
@@ -16,7 +16,12 @@
16
16
  * `~/.forge/specialists.json` (loaded automatically on startup).
17
17
  */
18
18
  import type { SpecialistConfig, SpecialistId } from './types.js';
19
- /** The five built-in specialists. Order is the order shown in `forge orchestrator list`. */
19
+ /**
20
+ * The built-in specialists. Order is the order shown in `forge orchestrator list` and in the chat's mode menu.
21
+ * Each prompt works both as a chat mode and as an orchestrator step: nothing in it assumes another agent.
22
+ * Prompt rules (RB-27): a tool named in the text is a built-in, in backticks, and never one that is only
23
+ * registered when a feature is on. `core/tests/specialist-profiles.test.ts` pins tools, limits and size.
24
+ */
20
25
  export declare const BUILTIN_SPECIALISTS: readonly SpecialistConfig[];
21
26
  /**
22
27
  * Look up a specialist by id. Checks custom registrations first