@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.
- package/dist/agent/agent.js +208 -57
- package/dist/agent/enhance-prompt.js +2 -1
- package/dist/agent/goal.d.ts +5 -8
- package/dist/agent/prompts.d.ts +15 -0
- package/dist/agent/prompts.js +34 -0
- package/dist/agent/race-abort.d.ts +8 -0
- package/dist/agent/race-abort.js +24 -0
- package/dist/agent/retry.d.ts +50 -0
- package/dist/agent/retry.js +192 -0
- package/dist/agent/shell-command.d.ts +23 -0
- package/dist/agent/shell-command.js +93 -0
- package/dist/agent/stack-facts.d.ts +23 -0
- package/dist/agent/stack-facts.js +150 -0
- package/dist/catalog/models-dev-snapshot.d.ts +1 -0
- package/dist/catalog/models-dev-snapshot.js +3 -0
- package/dist/catalog/models-dev.d.ts +35 -0
- package/dist/catalog/models-dev.js +143 -0
- package/dist/code-index/parser.js +48 -3
- package/dist/context/compaction.js +8 -2
- package/dist/git/auto-commit.d.ts +4 -2
- package/dist/git/auto-commit.js +16 -5
- package/dist/github/github-tools.d.ts +2 -18
- package/dist/index.d.ts +11 -3
- package/dist/index.js +9 -2
- package/dist/journey/answers-block.js +14 -1
- package/dist/journey/offer.js +2 -1
- package/dist/mcp/json-schema-to-zod.js +12 -1
- package/dist/memory/session-digest.js +2 -1
- package/dist/orchestrator/index.d.ts +1 -1
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/orchestrator.d.ts +45 -6
- package/dist/orchestrator/orchestrator.js +62 -12
- package/dist/orchestrator/router.d.ts +1 -1
- package/dist/orchestrator/router.js +4 -1
- package/dist/orchestrator/specialists.d.ts +6 -1
- package/dist/orchestrator/specialists.js +165 -68
- package/dist/orchestrator/types.d.ts +1 -1
- package/dist/policy/policy.d.ts +6 -59
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +47 -16
- package/dist/structured/schema.js +9 -12
- package/dist/tools/apply-patch.d.ts +1 -5
- package/dist/tools/ask-user.d.ts +2 -10
- package/dist/tools/ask-user.js +12 -4
- package/dist/tools/browser-tools.d.ts +15 -31
- package/dist/tools/codebase-search.d.ts +1 -7
- package/dist/tools/command-sandbox.d.ts +24 -0
- package/dist/tools/command-sandbox.js +73 -0
- package/dist/tools/dependency-audit.d.ts +30 -0
- package/dist/tools/dependency-audit.js +197 -0
- package/dist/tools/edit-file.d.ts +9 -15
- package/dist/tools/edit-file.js +64 -16
- package/dist/tools/fetch-url.d.ts +1 -18
- package/dist/tools/find-references.d.ts +1 -5
- package/dist/tools/find-symbol.d.ts +10 -8
- package/dist/tools/git-diff.d.ts +13 -0
- package/dist/tools/git-diff.js +85 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/list-files.d.ts +1 -19
- package/dist/tools/list-symbols.d.ts +1 -5
- package/dist/tools/memory.d.ts +23 -43
- package/dist/tools/notebook.d.ts +11 -17
- package/dist/tools/read-file.d.ts +1 -11
- package/dist/tools/read-image.d.ts +1 -12
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +1 -17
- package/dist/tools/run-command.js +12 -2
- package/dist/tools/search-files.d.ts +1 -17
- package/dist/tools/search-files.js +8 -49
- package/dist/tools/search-worker.d.ts +23 -0
- package/dist/tools/search-worker.js +77 -0
- package/dist/tools/sub-agent-wait.d.ts +2 -8
- package/dist/tools/sub-agent.d.ts +2 -70
- package/dist/tools/sub-agent.js +1 -0
- package/dist/tools/todo.d.ts +8 -20
- package/dist/tools/web-search.d.ts +1 -7
- package/dist/tools/write-file.d.ts +1 -7
- package/dist/types.d.ts +60 -1
- 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
|
|
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:',
|
package/dist/journey/offer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
/**
|
|
478
|
-
|
|
479
|
-
|
|
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) =>
|
|
482
|
-
...(args.board ? createBoardTools(args.board, args.spec.id).filter((t) =>
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
/**
|
|
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
|