@ikarmic/forge-core 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/agent/agent.js +132 -70
  2. package/dist/agent/cache-hints.d.ts +2 -0
  3. package/dist/agent/cache-hints.js +18 -3
  4. package/dist/agent/history-adapt.d.ts +23 -0
  5. package/dist/agent/history-adapt.js +114 -0
  6. package/dist/agent/prompts.d.ts +4 -2
  7. package/dist/agent/prompts.js +10 -2
  8. package/dist/agent/tool-repair.d.ts +8 -0
  9. package/dist/agent/tool-repair.js +16 -0
  10. package/dist/catalog/models-dev.d.ts +10 -0
  11. package/dist/catalog/models-dev.js +12 -0
  12. package/dist/checkpoints/store.js +3 -1
  13. package/dist/context/compaction.d.ts +21 -6
  14. package/dist/context/compaction.js +86 -26
  15. package/dist/context/mask.d.ts +9 -9
  16. package/dist/context/mask.js +30 -1
  17. package/dist/format/auto-format.d.ts +22 -0
  18. package/dist/format/auto-format.js +39 -0
  19. package/dist/github/github-tools.d.ts +1 -0
  20. package/dist/github/github-tools.js +9 -9
  21. package/dist/github/pr.d.ts +34 -0
  22. package/dist/github/pr.js +70 -0
  23. package/dist/human-text.d.ts +6 -0
  24. package/dist/human-text.js +37 -0
  25. package/dist/index.d.ts +13 -8
  26. package/dist/index.js +10 -6
  27. package/dist/journey/answers-block.d.ts +2 -4
  28. package/dist/lsp/diagnostics.d.ts +4 -0
  29. package/dist/lsp/diagnostics.js +8 -2
  30. package/dist/lsp/index.d.ts +1 -1
  31. package/dist/lsp/index.js +1 -1
  32. package/dist/mcp/index.d.ts +1 -1
  33. package/dist/mcp/index.js +1 -1
  34. package/dist/mcp/json-schema-to-zod.js +2 -4
  35. package/dist/mcp/mcp-client.d.ts +14 -0
  36. package/dist/mcp/mcp-client.js +102 -18
  37. package/dist/orchestrator/specialists-registry.js +14 -1
  38. package/dist/orchestrator/specialists.js +3 -3
  39. package/dist/semantic/embedder.d.ts +11 -0
  40. package/dist/semantic/embedder.js +16 -0
  41. package/dist/semantic/semantic-index.d.ts +39 -1
  42. package/dist/semantic/semantic-index.js +43 -13
  43. package/dist/semantic/vector-store.d.ts +2 -0
  44. package/dist/semantic/vector-store.js +13 -1
  45. package/dist/sessions/index.d.ts +2 -2
  46. package/dist/sessions/index.js +1 -1
  47. package/dist/sessions/search.js +2 -2
  48. package/dist/sessions/session-export.d.ts +1 -1
  49. package/dist/sessions/session-export.js +41 -33
  50. package/dist/sessions/session-store.d.ts +20 -1
  51. package/dist/sessions/session-store.js +7 -0
  52. package/dist/sessions/watch.js +4 -4
  53. package/dist/skills/activator.d.ts +6 -0
  54. package/dist/skills/activator.js +17 -0
  55. package/dist/skills/skill-tool.d.ts +12 -0
  56. package/dist/skills/skill-tool.js +28 -0
  57. package/dist/tools/background-process.d.ts +63 -0
  58. package/dist/tools/background-process.js +150 -0
  59. package/dist/tools/edit-file.js +2 -27
  60. package/dist/tools/edit-match.d.ts +16 -0
  61. package/dist/tools/edit-match.js +115 -0
  62. package/dist/tools/fetch-url.d.ts +15 -1
  63. package/dist/tools/fetch-url.js +7 -47
  64. package/dist/tools/get-diagnostics.d.ts +14 -0
  65. package/dist/tools/get-diagnostics.js +44 -0
  66. package/dist/tools/glob-filter.d.ts +7 -0
  67. package/dist/tools/glob-filter.js +12 -0
  68. package/dist/tools/html-to-markdown.d.ts +13 -0
  69. package/dist/tools/html-to-markdown.js +507 -0
  70. package/dist/tools/index.d.ts +5 -1
  71. package/dist/tools/index.js +11 -1
  72. package/dist/tools/list-files.d.ts +1 -0
  73. package/dist/tools/list-files.js +17 -5
  74. package/dist/tools/list-symbols.js +2 -2
  75. package/dist/tools/network-policy.d.ts +5 -0
  76. package/dist/tools/network-policy.js +77 -0
  77. package/dist/tools/office-docs.d.ts +6 -0
  78. package/dist/tools/office-docs.js +393 -0
  79. package/dist/tools/permission-rules.js +55 -5
  80. package/dist/tools/read-file.d.ts +2 -0
  81. package/dist/tools/read-file.js +47 -10
  82. package/dist/tools/read-image.js +2 -2
  83. package/dist/tools/read-only.d.ts +1 -1
  84. package/dist/tools/read-only.js +4 -0
  85. package/dist/tools/repo-clone.d.ts +40 -0
  86. package/dist/tools/repo-clone.js +218 -0
  87. package/dist/tools/result-cap.d.ts +8 -0
  88. package/dist/tools/result-cap.js +44 -0
  89. package/dist/tools/run-command.d.ts +18 -1
  90. package/dist/tools/run-command.js +41 -16
  91. package/dist/tools/search-files.js +5 -5
  92. package/dist/tools/search-worker.js +11 -2
  93. package/dist/tools/shared.d.ts +14 -1
  94. package/dist/tools/shared.js +76 -4
  95. package/dist/tools/shell.d.ts +27 -0
  96. package/dist/tools/shell.js +80 -0
  97. package/dist/tools/sub-agent.js +3 -1
  98. package/dist/tools/suggest-plan.d.ts +18 -0
  99. package/dist/tools/suggest-plan.js +28 -0
  100. package/dist/types.d.ts +74 -0
  101. package/dist/wakeup/wakeup-tools.d.ts +22 -0
  102. package/dist/wakeup/wakeup-tools.js +34 -0
  103. package/dist/wakeup/wakeup.d.ts +75 -0
  104. package/dist/wakeup/wakeup.js +151 -0
  105. package/dist/worktree/health.d.ts +51 -0
  106. package/dist/worktree/health.js +186 -0
  107. package/dist/worktree/index.d.ts +4 -0
  108. package/dist/worktree/index.js +2 -0
  109. package/dist/worktree/setup.d.ts +51 -0
  110. package/dist/worktree/setup.js +173 -0
  111. package/dist/worktree/worktree.d.ts +7 -0
  112. package/dist/worktree/worktree.js +9 -4
  113. package/package.json +1 -1
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pull-request status and creation for a branch (F8), on the REST layer the `github_pr_*` tools use. The host supplies the
3
+ * token (an environment variable or VS Code's GitHub sign-in); it goes in the Authorization header and nowhere else, so
4
+ * no error text built here may contain it.
5
+ */
6
+ import { type GitHubConfig, type RepoRef } from './github-tools.js';
7
+ export interface PullRequestSummary {
8
+ number: number;
9
+ url: string;
10
+ state: 'open' | 'merged' | 'closed';
11
+ draft: boolean;
12
+ /** All checks passed, one failed, some still run, or there are none. */
13
+ checks: 'passing' | 'failing' | 'pending' | 'none';
14
+ /** The latest review of each reviewer decides: any "changes requested" wins over an approval. */
15
+ review: 'approved' | 'changes requested' | 'none';
16
+ }
17
+ /** The newest pull request from `branch` (any state), summarized, or null when there is none. */
18
+ export declare function findPullRequest(cfg: GitHubConfig, repo: RepoRef, branch: string, signal?: AbortSignal): Promise<PullRequestSummary | null>;
19
+ /** The repository's default branch, the usual base for a pull request. */
20
+ export declare function defaultBranch(cfg: GitHubConfig, repo: RepoRef, signal?: AbortSignal): Promise<string>;
21
+ export interface NewPullRequest {
22
+ title: string;
23
+ body: string;
24
+ /** The branch with the changes; it must already be pushed. */
25
+ head: string;
26
+ base: string;
27
+ /** Default true: nobody is asked to review until the author says so. */
28
+ draft?: boolean;
29
+ }
30
+ export declare function openPullRequest(cfg: GitHubConfig, repo: RepoRef, input: NewPullRequest, signal?: AbortSignal): Promise<{
31
+ number: number;
32
+ url: string;
33
+ draft: boolean;
34
+ }>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pull-request status and creation for a branch (F8), on the REST layer the `github_pr_*` tools use. The host supplies the
3
+ * token (an environment variable or VS Code's GitHub sign-in); it goes in the Authorization header and nowhere else, so
4
+ * no error text built here may contain it.
5
+ */
6
+ import { githubApi } from './github-tools.js';
7
+ const FAILED = new Set(['failure', 'timed_out', 'cancelled', 'action_required', 'startup_failure']);
8
+ function checksOf(runs) {
9
+ if (runs.length === 0)
10
+ return 'none';
11
+ if (runs.some((r) => r.status !== 'completed'))
12
+ return runs.some((r) => FAILED.has(String(r.conclusion))) ? 'failing' : 'pending';
13
+ return runs.some((r) => FAILED.has(String(r.conclusion))) ? 'failing' : 'passing';
14
+ }
15
+ function reviewOf(reviews) {
16
+ const latest = new Map();
17
+ for (const r of reviews) {
18
+ const state = String(r.state);
19
+ // A comment does not replace an earlier verdict.
20
+ if (state !== 'APPROVED' && state !== 'CHANGES_REQUESTED')
21
+ continue;
22
+ latest.set(String(r.user?.login ?? 'unknown'), state);
23
+ }
24
+ const states = [...latest.values()];
25
+ if (states.includes('CHANGES_REQUESTED'))
26
+ return 'changes requested';
27
+ return states.includes('APPROVED') ? 'approved' : 'none';
28
+ }
29
+ /** The newest pull request from `branch` (any state), summarized, or null when there is none. */
30
+ export async function findPullRequest(cfg, repo, branch, signal) {
31
+ const base = `/repos/${repo.owner}/${repo.repo}`;
32
+ const found = (await githubApi(cfg, repo, 'GET', `${base}/pulls?state=all&head=${encodeURIComponent(`${repo.owner}:${branch}`)}&per_page=1`, undefined, signal));
33
+ const pr = Array.isArray(found) ? found[0] : undefined;
34
+ if (!pr)
35
+ return null;
36
+ const number = pr.number;
37
+ const sha = pr.head?.sha;
38
+ const [runs, reviews] = await Promise.all([
39
+ sha ? githubApi(cfg, repo, 'GET', `${base}/commits/${sha}/check-runs?per_page=50`, undefined, signal).catch(() => ({ check_runs: [] })) : { check_runs: [] },
40
+ githubApi(cfg, repo, 'GET', `${base}/pulls/${number}/reviews?per_page=50`, undefined, signal).catch(() => []),
41
+ ]);
42
+ return {
43
+ number,
44
+ url: String(pr.html_url ?? ''),
45
+ state: pr.merged_at ? 'merged' : pr.state === 'closed' ? 'closed' : 'open',
46
+ draft: pr.draft === true,
47
+ checks: checksOf(runs.check_runs ?? []),
48
+ review: reviewOf(Array.isArray(reviews) ? reviews : []),
49
+ };
50
+ }
51
+ /** The repository's default branch, the usual base for a pull request. */
52
+ export async function defaultBranch(cfg, repo, signal) {
53
+ const data = (await githubApi(cfg, repo, 'GET', `/repos/${repo.owner}/${repo.repo}`, undefined, signal));
54
+ return String(data.default_branch ?? 'main');
55
+ }
56
+ export async function openPullRequest(cfg, repo, input, signal) {
57
+ if (input.head === input.base)
58
+ throw new Error(`the head branch is the base branch (${input.head}); work on a separate branch`);
59
+ try {
60
+ const pr = (await githubApi(cfg, repo, 'POST', `/repos/${repo.owner}/${repo.repo}/pulls`, { title: input.title, body: input.body, head: input.head, base: input.base, draft: input.draft ?? true }, signal));
61
+ return { number: pr.number, url: String(pr.html_url), draft: pr.draft === true };
62
+ }
63
+ catch (err) {
64
+ const msg = err.message;
65
+ if (/HTTP 422/.test(msg) && /head|not.*found|No commits|invalid/i.test(msg)) {
66
+ throw new Error(`${msg}. Is ${input.head} pushed and ahead of ${input.base}? Push it with: git push -u origin ${input.head}`);
67
+ }
68
+ throw err;
69
+ }
70
+ }
@@ -0,0 +1,6 @@
1
+ /** `max_iterations`, `perChat` and `on-network` become "Max iterations", "Per chat" and "On network". */
2
+ export declare function humanize(id: string): string;
3
+ /** The name of a tool as a person would say it. */
4
+ export declare function toolLabel(name: string): string;
5
+ /** Why a run ended, for a line a person reads. */
6
+ export declare function runReasonLabel(reason: string): string;
@@ -0,0 +1,37 @@
1
+ const ACRONYMS = { mcp: 'MCP', api: 'API', url: 'URL', lsp: 'LSP', ai: 'AI', pr: 'PR', os: 'OS' };
2
+ /** `max_iterations`, `perChat` and `on-network` become "Max iterations", "Per chat" and "On network". */
3
+ export function humanize(id) {
4
+ const words = id
5
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
6
+ .split(/[\s_-]+/)
7
+ .filter(Boolean)
8
+ .map((w) => w.toLowerCase());
9
+ if (words.length === 0)
10
+ return id;
11
+ const first = words[0];
12
+ words[0] = ACRONYMS[first] ?? first.charAt(0).toUpperCase() + first.slice(1);
13
+ for (let i = 1; i < words.length; i++)
14
+ words[i] = ACRONYMS[words[i]] ?? words[i];
15
+ return words.join(' ');
16
+ }
17
+ /** The name of a tool as a person would say it. */
18
+ export function toolLabel(name) {
19
+ return humanize(name);
20
+ }
21
+ // A Record over the reason type: a new way for a run to end is a compile error until it has words here.
22
+ const RUN_REASONS = {
23
+ completed: 'Completed',
24
+ max_iterations: 'Step limit reached',
25
+ max_duration: 'Time limit reached',
26
+ budget_exceeded: 'Cost limit reached',
27
+ loop_detected: 'Stopped: repeating the same step',
28
+ aborted: 'Stopped',
29
+ error: 'Failed',
30
+ no_provider_key: 'No API key',
31
+ plan: 'Plan ready',
32
+ spec: 'Spec ready',
33
+ };
34
+ /** Why a run ended, for a line a person reads. */
35
+ export function runReasonLabel(reason) {
36
+ return RUN_REASONS[reason] ?? humanize(reason);
37
+ }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@ export { enhancePrompt } from './agent/enhance-prompt.js';
4
4
  export { generateShellCommand, shellKindOf, commandRisk } from './agent/shell-command.js';
5
5
  export type { ShellKind } from './agent/shell-command.js';
6
6
  export { stripThinkTags } from './agent/think-tags.js';
7
- export { createGitHubCreateTool, createGitHubReadTool, createGitHubTools, githubFromEnv, parseRemote, type GitHubConfig, type PullRequestReport, type RepoRef, } from './github/github-tools.js';
7
+ export { createGitHubCreateTool, createGitHubReadTool, createGitHubTools, githubFromEnv, parseRemote, type GitHubConfig, type PullRequestReport, repoFor, type RepoRef, } from './github/github-tools.js';
8
+ export { defaultBranch, findPullRequest, openPullRequest, type NewPullRequest, type PullRequestSummary } from './github/pr.js';
8
9
  export { createWebSearchTool, searchWeb, WEB_SEARCH_PROVIDERS, webSearchFromEnv, webSearchProblem, type WebSearchConfig, type WebSearchProvider, type WebSearchResult, } from './tools/web-search.js';
9
10
  export { attachmentKind, attachmentBytes, attachmentParts, attachmentProblem, describeAttachment, detectMediaType, loadAttachment, makeAttachment, mediaPartTokens, MAX_ATTACHMENT_BYTES, MAX_IMAGE_ATTACHMENT_BYTES, type Attachment, type AttachmentKind, } from './attachments.js';
10
11
  export type { CredentialStore } from './credentials/types.js';
@@ -35,17 +36,18 @@ export type { StaleOptions } from './memory/stale.js';
35
36
  export type { LoadedMemory, InjectOptions, MemoryInjection, MemorySection, MemorySource, MemoryType, SectionMeta, } from './memory/memory-store.js';
36
37
  export { MAX_BODY_CHARS } from './tools/memory.js';
37
38
  export type { MemoryForgetResult, MemoryReadResult, MemoryWriteResult } from './tools/memory.js';
38
- export { buildLlmSummarizer, compactMessages, estimateMessageTokens, estimateTokens, getModelWindowTokens, shouldCompact, } from './context/compaction.js';
39
+ export { buildLlmSummarizer, COMPACTION_SUMMARY_PREFIX, compactMessages, estimateMessageTokens, isCompactionSummary, estimateTokens, getModelWindowTokens, windowIsGuess, shouldCompact, } from './context/compaction.js';
39
40
  export type { CompactionResult, Summarizer } from './context/compaction.js';
40
41
  export { defaultModelFor, detectProvider, envVarForProvider, fastDefaultFor, hasProviderKey, readModelConfigFromEnv, resolveApiKey, SUPPORTED_PROVIDERS, } from './config.js';
41
- export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpResultToValue, mcpInstructionsBlock, createMcpResourceTools, jsonSchemaToZod, } from './mcp/index.js';
42
+ export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpResultToValue, mcpInstructionsBlock, mcpToolName, mcpToolPrefix, createMcpResourceTools, jsonSchemaToZod, } from './mcp/index.js';
42
43
  export type { McpConfigFile, McpServerSpec, ConnectedMcpServer, } from './mcp/index.js';
43
44
  export { createLanguageModel, formatModelId, pickAvailableProvider, resolveModelFromEnv, resolveModelFromEnvAsync } from './providers.js';
44
- export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
45
- export type { Session, ListEntry, FormatOptions, SessionUpdate, WatchSessionOptions, SessionMatch, SearchSessionsOptions, SessionPruneOptions, SessionPruneResult } from './sessions/index.js';
45
+ export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, displayMessages, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
46
+ export type { Session, ListEntry, MessageFeedback, FormatOptions, SessionUpdate, WatchSessionOptions, SessionMatch, SearchSessionsOptions, SessionPruneOptions, SessionPruneResult } from './sessions/index.js';
46
47
  export { commandDirs, expandCustomCommand, formatCustomCommands, loadCustomCommands, parseCommandFile, type CustomCommand, } from './commands/custom-commands.js';
47
48
  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';
48
49
  export type { AgentConfig, AgentEvent, ApprovalSource, AgentRunOptions, AgentRunReason, AgentRunResult, AnyLanguageModel, ApprovalDecision, CommandRunner, ModelRouter, VerifyConfig, ApprovalRequest, LanguageModelFactory, ModelConfig, PricePerMillionTokens, ProviderName, RedactOptions, TodoItem, ToolContext, UserQuestion, ToolDefinition, ToolPolicy, ToolSandbox, } from './types.js';
50
+ export { humanize, runReasonLabel, toolLabel } from './human-text.js';
49
51
  export type { CacheKeyParts, CacheStats } from './tools/index.js';
50
52
  export type { RedactionRule, RedactionResult } from './tools/index.js';
51
53
  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';
@@ -91,8 +93,8 @@ export { computeUnifiedDiff, computeDiffSummary, type DiffOptions, type DiffResu
91
93
  export { defaultRetryPrompt, formatZodError, isStructuredOutputError, loadSchemaFile, runStructuredAgent, StructuredOutputError, validateFinalText, } from './structured/index.js';
92
94
  export type { StructuredRunOptions, StructuredRunResult } from './structured/index.js';
93
95
  export type { ZodTypeAny } from './structured/index.js';
94
- export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeContexts, deleteBranch, mergeBranch, snapshotWorktreeContext, WorktreeError, WorktreeManager, } from './worktree/index.js';
95
- export type { MergeResult, WorktreeCommit, CreateOptions, RemoveOptions, RemoveResult, WorktreeContextSnapshot, WorktreeInfo, WorktreeMismatchKind, WorktreeMismatchResult, } from './worktree/index.js';
96
+ export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeContexts, copyIntoWorktree, deleteBranch, doctorWorktrees, inspectWorktrees, mergeBranch, pruneMissingWorktrees, removeStrayFolder, runSetupScript, setupWorktree, snapshotWorktreeContext, worktreeStats, WorktreeError, WorktreeManager, } from './worktree/index.js';
97
+ export type { MergeResult, WorktreeCommit, CreateOptions, RemoveOptions, RemoveResult, WorktreeContextSnapshot, WorktreeInfo, WorktreeMismatchKind, WorktreeMismatchResult, HealthFinding, ListedWorktree, WorktreeInspection, WorktreeStats, CopyOptions, CopyResult, ScriptOptions, ScriptResult, SetupOptions, SetupOutcome, } from './worktree/index.js';
96
98
  export { formatSteeringLine, isSafeSteeringName, loadSteering, overrideFilePath, parseSteering, pathMatchesGlob, readOverride, selectAtStart, selectOnToolTouch, setSteeringDisabled, stripSteering, wrapSteering, } from './steering/index.js';
97
99
  export type { StartSelection, SteeringConfig, SteeringDoc, SteeringInclusion, SteeringLoadResult, SteeringSkip, } from './steering/index.js';
98
100
  export { CONFIG_KEY_COVERAGE, FORGE_CONFIG_SCHEMA } from './config-schema.js';
@@ -104,10 +106,13 @@ export { shellEnv } from './tools/shell-env.js';
104
106
  export { previewPatch } from './tools/apply-patch.js';
105
107
  export { applyTextEdit } from './tools/edit-file.js';
106
108
  export { catalogEntry, loadModelCatalog, type CatalogEntry, type CatalogSource } from './catalog/models-dev.js';
109
+ export { MAX_HORIZON_MS, MAX_PER_SESSION as MAX_WAKEUPS, MIN_DELAY_MS as MIN_WAKEUP_DELAY_MS, WakeupScheduler, resolveWakeupTime, wakeupMessage } from './wakeup/wakeup.js';
110
+ export type { Wakeup, WakeupSchedulerOptions, WakeupStore } from './wakeup/wakeup.js';
107
111
  export { isContextOverflowError, retryDecision, withModelRetry } from './agent/retry.js';
108
112
  export type { PatchPreview } from './tools/apply-patch.js';
109
113
  export { applyPatchTool, parsePatch, patchedPaths } from './tools/apply-patch.js';
110
- export { buildEmbedder, type EmbedderSpec } from './semantic/embedder.js';
114
+ export { buildEmbedder, embedderDestination, type EmbedderSpec } from './semantic/embedder.js';
115
+ export { deleteSemanticIndex, SemanticConsentError, type SemanticStatus } from './semantic/semantic-index.js';
111
116
  export { parseExtensions, type SemanticOptions } from './semantic/semantic-index.js';
112
117
  export { cleanDigest, generateSessionDigest, shouldDigest, type DigestItem } from './memory/session-digest.js';
113
118
  export { buildExtraInstructionsBlock } from './steering/extra-instructions.js';
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ export { LoopGuard } from './agent/loop-guard.js';
3
3
  export { enhancePrompt } from './agent/enhance-prompt.js';
4
4
  export { generateShellCommand, shellKindOf, commandRisk } from './agent/shell-command.js';
5
5
  export { stripThinkTags } from './agent/think-tags.js';
6
- export { createGitHubCreateTool, createGitHubReadTool, createGitHubTools, githubFromEnv, parseRemote, } from './github/github-tools.js';
6
+ export { createGitHubCreateTool, createGitHubReadTool, createGitHubTools, githubFromEnv, parseRemote, repoFor, } from './github/github-tools.js';
7
+ export { defaultBranch, findPullRequest, openPullRequest } from './github/pr.js';
7
8
  export { createWebSearchTool, searchWeb, WEB_SEARCH_PROVIDERS, webSearchFromEnv, webSearchProblem, } from './tools/web-search.js';
8
9
  export { attachmentKind, attachmentBytes, attachmentParts, attachmentProblem, describeAttachment, detectMediaType, loadAttachment, makeAttachment, mediaPartTokens, MAX_ATTACHMENT_BYTES, MAX_IMAGE_ATTACHMENT_BYTES, } from './attachments.js';
9
10
  export { CREDENTIAL_SERVICE_NAME, credentialAccountKey } from './credentials/index.js';
@@ -21,13 +22,14 @@ export { buildMemoryBlock, composeSectionUpdate, findProjectMemoryRoot, globalMe
21
22
  export { scoreSections, tokenize } from './memory/rank.js';
22
23
  export { checkStale, staleKey } from './memory/stale.js';
23
24
  export { MAX_BODY_CHARS } from './tools/memory.js';
24
- export { buildLlmSummarizer, compactMessages, estimateMessageTokens, estimateTokens, getModelWindowTokens, shouldCompact, } from './context/compaction.js';
25
+ export { buildLlmSummarizer, COMPACTION_SUMMARY_PREFIX, compactMessages, estimateMessageTokens, isCompactionSummary, estimateTokens, getModelWindowTokens, windowIsGuess, shouldCompact, } from './context/compaction.js';
25
26
  export { defaultModelFor, detectProvider, envVarForProvider, fastDefaultFor, hasProviderKey, readModelConfigFromEnv, resolveApiKey, SUPPORTED_PROVIDERS, } from './config.js';
26
- export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpResultToValue, mcpInstructionsBlock, createMcpResourceTools, jsonSchemaToZod, } from './mcp/index.js';
27
+ export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpResultToValue, mcpInstructionsBlock, mcpToolName, mcpToolPrefix, createMcpResourceTools, jsonSchemaToZod, } from './mcp/index.js';
27
28
  export { createLanguageModel, formatModelId, pickAvailableProvider, resolveModelFromEnv, resolveModelFromEnvAsync } from './providers.js';
28
- export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
29
+ export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, displayMessages, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
29
30
  export { commandDirs, expandCustomCommand, formatCustomCommands, loadCustomCommands, parseCommandFile, } from './commands/custom-commands.js';
30
31
  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';
32
+ export { humanize, runReasonLabel, toolLabel } from './human-text.js';
31
33
  // Skills system
32
34
  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';
33
35
  export { cosine, embedderFromEnv, hashingEmbedder, openAiCompatibleEmbedder, } from './semantic/embedder.js';
@@ -71,7 +73,7 @@ export { defaultRetryPrompt, formatZodError, isStructuredOutputError, loadSchema
71
73
  // Lets child agents work in isolated git worktrees so parallel work
72
74
  // doesn't clobber the main checkout. Lifecycle: create → use → remove.
73
75
  // `pruneStale()` cleans up worktrees left behind by crashed sessions.
74
- export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeContexts, deleteBranch, mergeBranch, snapshotWorktreeContext, WorktreeError, WorktreeManager, } from './worktree/index.js';
76
+ export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeContexts, copyIntoWorktree, deleteBranch, doctorWorktrees, inspectWorktrees, mergeBranch, pruneMissingWorktrees, removeStrayFolder, runSetupScript, setupWorktree, snapshotWorktreeContext, worktreeStats, WorktreeError, WorktreeManager, } from './worktree/index.js';
75
77
  // Steering files (SP3a)
76
78
  export { formatSteeringLine, isSafeSteeringName, loadSteering, overrideFilePath, parseSteering, pathMatchesGlob, readOverride, selectAtStart, selectOnToolTouch, setSteeringDisabled, stripSteering, wrapSteering, } from './steering/index.js';
77
79
  export { CONFIG_KEY_COVERAGE, FORGE_CONFIG_SCHEMA } from './config-schema.js';
@@ -83,9 +85,11 @@ export { shellEnv } from './tools/shell-env.js';
83
85
  export { previewPatch } from './tools/apply-patch.js';
84
86
  export { applyTextEdit } from './tools/edit-file.js';
85
87
  export { catalogEntry, loadModelCatalog } from './catalog/models-dev.js';
88
+ export { MAX_HORIZON_MS, MAX_PER_SESSION as MAX_WAKEUPS, MIN_DELAY_MS as MIN_WAKEUP_DELAY_MS, WakeupScheduler, resolveWakeupTime, wakeupMessage } from './wakeup/wakeup.js';
86
89
  export { isContextOverflowError, retryDecision, withModelRetry } from './agent/retry.js';
87
90
  export { applyPatchTool, parsePatch, patchedPaths } from './tools/apply-patch.js';
88
- export { buildEmbedder } from './semantic/embedder.js';
91
+ export { buildEmbedder, embedderDestination } from './semantic/embedder.js';
92
+ export { deleteSemanticIndex, SemanticConsentError } from './semantic/semantic-index.js';
89
93
  export { parseExtensions } from './semantic/semantic-index.js';
90
94
  export { cleanDigest, generateSessionDigest, shouldDigest } from './memory/session-digest.js';
91
95
  export { buildExtraInstructionsBlock } from './steering/extra-instructions.js';
@@ -8,10 +8,8 @@
8
8
  * model scaffolds MySQL with invoicing. In the system message it cannot be
9
9
  * forgotten.
10
10
  *
11
- * Why the interview must finish before the first agent run:
12
- * `buildInitialMessages` assembles the blocks only when no history is passed
13
- * (`agent.ts:231`), so this block enters the prompt once, on a session's first
14
- * turn, and rides inside `history[0]` from then on.
11
+ * The system message is rebuilt on every turn (`buildSystemContent`, RB-57), so this block always carries the
12
+ * answers given so far.
15
13
  */
16
14
  import type { Answer } from './types.js';
17
15
  export declare function renderAnswersBlock(answers: readonly Answer[]): string;
@@ -4,6 +4,8 @@ export interface LspConfig {
4
4
  servers?: Record<string, LspServerSpec>;
5
5
  /** How long to wait for a server to report on a file. Default 10s. */
6
6
  waitMs?: number;
7
+ /** Also report warnings, not only errors (the verify loop wants errors only). */
8
+ includeWarnings?: boolean;
7
9
  }
8
10
  export interface FileDiagnostics {
9
11
  file: string;
@@ -15,6 +17,8 @@ export interface LspReport {
15
17
  /** Extensions whose server could not be started (not installed, crashed). */
16
18
  skipped: string[];
17
19
  }
20
+ /** Whether a file's type has a language server configured (built-in table or an override). */
21
+ export declare function hasServerFor(file: string, config?: LspConfig): boolean;
18
22
  /** Runs the matching language server over `files` (paths relative to `cwd` or absolute). */
19
23
  export declare function collectErrors(files: string[], cwd: string, config?: LspConfig): Promise<LspReport>;
20
24
  /** Plain-text rendering fed to the model, in `file:line:col: message` form. */
@@ -23,6 +23,11 @@ const DEFAULT_LANGUAGES = {
23
23
  const DEFAULT_WAIT_MS = 10_000;
24
24
  const START_TIMEOUT_MS = 15_000;
25
25
  const MAX_PER_FILE = 20;
26
+ /** Whether a file's type has a language server configured (built-in table or an override). */
27
+ export function hasServerFor(file, config = {}) {
28
+ const ext = path.extname(file).toLowerCase();
29
+ return Boolean(config.servers?.[ext] ?? DEFAULT_LANGUAGES[ext]);
30
+ }
26
31
  /** Runs the matching language server over `files` (paths relative to `cwd` or absolute). */
27
32
  export async function collectErrors(files, cwd, config = {}) {
28
33
  const waitMs = config.waitMs ?? DEFAULT_WAIT_MS;
@@ -54,7 +59,7 @@ export async function collectErrors(files, cwd, config = {}) {
54
59
  const text = await readFile(f.abs, 'utf8').catch(() => null);
55
60
  if (text === null)
56
61
  continue;
57
- const diagnostics = (await client.diagnose(pathToFileURL(f.abs).href, f.languageId, text, waitMs)).filter((d) => d.severity === 'error');
62
+ const diagnostics = (await client.diagnose(pathToFileURL(f.abs).href, f.languageId, text, waitMs)).filter((d) => d.severity === 'error' || (config.includeWarnings === true && d.severity === 'warning'));
58
63
  if (diagnostics.length > 0)
59
64
  report.errors.push({ file: path.relative(cwd, f.abs) || f.abs, diagnostics });
60
65
  }
@@ -71,7 +76,8 @@ export function formatErrors(errors) {
71
76
  .map(({ file, diagnostics }) => {
72
77
  const lines = diagnostics.slice(0, MAX_PER_FILE).map((d) => {
73
78
  const tag = [d.source, d.code].filter((x) => x !== undefined && x !== '').join(' ');
74
- return `${file}:${d.line}:${d.column}: ${d.message}${tag ? ` (${tag})` : ''}`;
79
+ const level = d.severity === 'error' ? '' : `${d.severity}: `;
80
+ return `${file}:${d.line}:${d.column}: ${level}${d.message}${tag ? ` (${tag})` : ''}`;
75
81
  });
76
82
  if (diagnostics.length > MAX_PER_FILE)
77
83
  lines.push(`... ${diagnostics.length - MAX_PER_FILE} more in ${file}`);
@@ -1,4 +1,4 @@
1
1
  export { LspClient } from './client.js';
2
2
  export type { LspDiagnostic, LspServerSpec, LspSeverity } from './client.js';
3
- export { collectErrors, formatErrors } from './diagnostics.js';
3
+ export { collectErrors, formatErrors, hasServerFor } from './diagnostics.js';
4
4
  export type { FileDiagnostics, LspConfig, LspReport } from './diagnostics.js';
package/dist/lsp/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { LspClient } from './client.js';
2
- export { collectErrors, formatErrors } from './diagnostics.js';
2
+ export { collectErrors, formatErrors, hasServerFor } from './diagnostics.js';
@@ -1,7 +1,7 @@
1
1
  export { loadMcpConfig, parseMcpConfig } from './mcp-config.js';
2
2
  export type { McpConfigFile, McpServerSpec } from './mcp-config.js';
3
3
  export { jsonSchemaToZod } from './json-schema-to-zod.js';
4
- export { connectAll, connectOne, closeAll, mcpInstructionsBlock, mcpResultToValue, McpAuthRequiredError } from './mcp-client.js';
4
+ export { connectAll, connectOne, closeAll, mcpInstructionsBlock, mcpResultToValue, mcpToolName, mcpToolPrefix, McpAuthRequiredError } from './mcp-client.js';
5
5
  export type { ConnectOptions } from './mcp-client.js';
6
6
  export { forgetSavedAuth, defaultMcpAuthDir } from './oauth.js';
7
7
  export type { McpOAuthOptions } from './oauth.js';
package/dist/mcp/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { loadMcpConfig, parseMcpConfig } from './mcp-config.js';
2
2
  export { jsonSchemaToZod } from './json-schema-to-zod.js';
3
- export { connectAll, connectOne, closeAll, mcpInstructionsBlock, mcpResultToValue, McpAuthRequiredError } from './mcp-client.js';
3
+ export { connectAll, connectOne, closeAll, mcpInstructionsBlock, mcpResultToValue, mcpToolName, mcpToolPrefix, McpAuthRequiredError } from './mcp-client.js';
4
4
  export { forgetSavedAuth, defaultMcpAuthDir } from './oauth.js';
5
5
  export { createMcpResourceTools } from './resource-tools.js';
6
6
  export { BUILTIN_MCP_CATALOG, loadMcpCatalog } from './catalog.js';
@@ -68,11 +68,9 @@ export function jsonSchemaToZod(schema) {
68
68
  .pipe(objectZod)
69
69
  : objectZod;
70
70
  }
71
- else if (s.additionalProperties === true) {
72
- inner = z.record(z.string(), z.any());
73
- }
74
71
  else {
75
- inner = z.record(z.string(), z.any());
72
+ // Any object. Not z.record(): its JSON Schema carries `propertyNames`, a keyword Gemini's schema input does not list.
73
+ inner = z.looseObject({});
76
74
  }
77
75
  break;
78
76
  }
@@ -26,6 +26,20 @@ export declare function connectOne(name: string, spec: McpServerSpec, options?:
26
26
  export declare function mcpInstructionsBlock(servers: readonly ConnectedMcpServer[]): string;
27
27
  /** Close all clients and their underlying transports. */
28
28
  export declare function closeAll(servers: ConnectedMcpServer[]): Promise<void>;
29
+ /** The start of every tool name of one server. Names a provider accepts only: letters, digits, `_` and `-`. */
30
+ export declare function mcpToolPrefix(serverName: string): string;
31
+ /**
32
+ * The name the model sees for a server's tool. OpenAI and Anthropic refuse a tool name with any other character or
33
+ * over 64 characters, and one refused name fails the whole request. A name that needed cleaning or cutting gets a
34
+ * short hash of the original pair so two different tools never become one; the call itself still uses the server's
35
+ * own tool name.
36
+ */
37
+ export declare function mcpToolName(serverName: string, toolName: string): string;
38
+ export declare function mcpToolToForgeTool(serverName: string, mcpTool: {
39
+ name: string;
40
+ description?: string;
41
+ inputSchema?: unknown;
42
+ }, getClient: () => Client, timeout?: number, reconnect?: () => Promise<Client>): ToolDefinition;
29
43
  /**
30
44
  * Reduce an MCP `CallToolResult` to a model-friendly value.
31
45
  *
@@ -21,6 +21,7 @@
21
21
  * - **streamable-http**: `url` + optional `headers`. The 2025-03-26 modern
22
22
  * transport. Uses the SDK's `StreamableHTTPClientTransport`.
23
23
  */
24
+ import { createHash } from 'node:crypto';
24
25
  import { readFileSync } from 'node:fs';
25
26
  import { dirname, join } from 'node:path';
26
27
  import { fileURLToPath } from 'node:url';
@@ -32,6 +33,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
32
33
  import { filterMcpServers, getOrgPolicy } from '../policy/policy.js';
33
34
  import { jsonSchemaToZod } from './json-schema-to-zod.js';
34
35
  import { ForgeOAuthProvider, McpAuthRequiredError, defaultMcpAuthDir, readSavedAuth } from './oauth.js';
36
+ import { clampOutput } from '../tools/run-command.js';
35
37
  /**
36
38
  * Self-identifying info we send to every MCP server during the `initialize`
37
39
  * handshake. Version is read from our own package.json so it stays in sync
@@ -90,6 +92,32 @@ function isUnauthorized(err) {
90
92
  }
91
93
  /** Connect to a single server and pull its tools. */
92
94
  export async function connectOne(name, spec, options = {}) {
95
+ let current = await openClient(name, spec, options);
96
+ // One restart at a time: calls that find the server gone together share one new process (G7).
97
+ let restarting;
98
+ const reconnect = () => {
99
+ restarting ??= (async () => {
100
+ await current.close().catch(() => undefined);
101
+ current = await openClient(name, spec, options);
102
+ return current;
103
+ })().finally(() => {
104
+ restarting = undefined;
105
+ });
106
+ return restarting;
107
+ };
108
+ const { tools: mcpTools } = await current.listTools(undefined, spec.timeout ? { timeout: spec.timeout } : undefined);
109
+ const tools = mcpTools.map((t) => mcpToolToForgeTool(name, t, () => current, spec.timeout, reconnect));
110
+ return {
111
+ name,
112
+ get client() {
113
+ return current;
114
+ },
115
+ tools,
116
+ instructions: current.getInstructions()?.trim() || undefined,
117
+ };
118
+ }
119
+ /** Start the transport, run the handshake (and the sign-in when the server wants one) and return the live client. */
120
+ async function openClient(name, spec, options) {
93
121
  const timeout = spec.timeout ? { timeout: spec.timeout } : undefined;
94
122
  let client = new Client(CLIENT_INFO, { capabilities: {} });
95
123
  const oauth = options.oauth;
@@ -134,9 +162,12 @@ export async function connectOne(name, spec, options = {}) {
134
162
  await client.connect(transport, timeout);
135
163
  }
136
164
  }
137
- const { tools: mcpTools } = await client.listTools(undefined, spec.timeout ? { timeout: spec.timeout } : undefined);
138
- const tools = mcpTools.map((t) => mcpToolToForgeTool(name, t, client, spec.timeout));
139
- return { name, client, tools, instructions: client.getInstructions()?.trim() || undefined };
165
+ return client;
166
+ }
167
+ /** A call that failed because the server's process or connection is gone, not because the call itself was bad. */
168
+ function isConnectionLost(err) {
169
+ const e = err;
170
+ return e?.code === -32000 || (typeof e?.message === 'string' && /connection closed|not connected/i.test(e.message));
140
171
  }
141
172
  /**
142
173
  * Pick the right SDK transport for the server spec. The transport type is
@@ -193,8 +224,36 @@ export async function closeAll(servers) {
193
224
  * namespace of the built-in tools and tells the model which server a tool
194
225
  * came from at a glance.
195
226
  */
196
- function mcpToolToForgeTool(serverName, mcpTool, client, timeout) {
197
- const fullName = `mcp__${serverName}__${mcpTool.name}`;
227
+ const MAX_TOOL_NAME = 64;
228
+ const MAX_SERVER_PART = 30;
229
+ function cleanNamePart(s) {
230
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_');
231
+ }
232
+ function shortHash(s) {
233
+ return createHash('sha256').update(s).digest('hex').slice(0, 8);
234
+ }
235
+ /** The start of every tool name of one server. Names a provider accepts only: letters, digits, `_` and `-`. */
236
+ export function mcpToolPrefix(serverName) {
237
+ return `mcp__${cleanNamePart(serverName).slice(0, MAX_SERVER_PART)}__`;
238
+ }
239
+ /**
240
+ * The name the model sees for a server's tool. OpenAI and Anthropic refuse a tool name with any other character or
241
+ * over 64 characters, and one refused name fails the whole request. A name that needed cleaning or cutting gets a
242
+ * short hash of the original pair so two different tools never become one; the call itself still uses the server's
243
+ * own tool name.
244
+ */
245
+ export function mcpToolName(serverName, toolName) {
246
+ const prefix = mcpToolPrefix(serverName);
247
+ const body = cleanNamePart(toolName);
248
+ const name = prefix + body;
249
+ const untouched = prefix === `mcp__${serverName}__` && body === toolName && name.length <= MAX_TOOL_NAME;
250
+ if (untouched)
251
+ return name;
252
+ const hash = shortHash(`${serverName}\u0000${toolName}`);
253
+ return `${name.slice(0, MAX_TOOL_NAME - hash.length - 1)}_${hash}`;
254
+ }
255
+ export function mcpToolToForgeTool(serverName, mcpTool, getClient, timeout, reconnect) {
256
+ const fullName = mcpToolName(serverName, mcpTool.name);
198
257
  const description = [
199
258
  mcpTool.description ?? '',
200
259
  `(MCP server: ${serverName})`,
@@ -210,8 +269,24 @@ function mcpToolToForgeTool(serverName, mcpTool, client, timeout) {
210
269
  // destructive so the user gets a confirmation prompt unless --auto.
211
270
  destructive: true,
212
271
  execute: async (args) => {
213
- const result = await client.callTool({ name: mcpTool.name, arguments: args }, undefined, timeout ? { timeout } : undefined);
214
- return mcpResultToValue(result);
272
+ const call = (client) => client.callTool({ name: mcpTool.name, arguments: args }, undefined, timeout ? { timeout } : undefined);
273
+ let result;
274
+ try {
275
+ result = await call(getClient());
276
+ }
277
+ catch (err) {
278
+ // A server that exited is started again for this call, once; any other failure goes to the caller as it is.
279
+ if (!reconnect || !isConnectionLost(err))
280
+ throw err;
281
+ result = await call(await reconnect());
282
+ }
283
+ const value = mcpResultToValue(result);
284
+ // An MCP server reports a failed call as a normal result with isError set: the model must see a failure.
285
+ if (result?.isError === true) {
286
+ const said = typeof value === 'string' ? value : value?.text;
287
+ throw new Error(said?.trim() || `MCP server "${serverName}" reported an error for ${mcpTool.name}.`);
288
+ }
289
+ return value;
215
290
  },
216
291
  };
217
292
  }
@@ -229,16 +304,25 @@ export function mcpResultToValue(result) {
229
304
  if (!Array.isArray(r.content)) {
230
305
  return { ...r };
231
306
  }
232
- const text = r.content
233
- .filter((c) => typeof c === 'object' && c !== null && c.type === 'text')
234
- .map((c) => c.text)
235
- .join('\n');
236
- const other = r.content.filter((c) => {
237
- const t = typeof c === 'object' && c !== null ? c.type : undefined;
238
- return t !== 'text';
239
- });
240
- if (other.length === 0) {
241
- return text;
307
+ const texts = [];
308
+ let image;
309
+ let omitted = 0;
310
+ for (const c of r.content) {
311
+ const b = (typeof c === 'object' && c !== null ? c : {});
312
+ if (b.type === 'text' && typeof b.text === 'string')
313
+ texts.push(b.text);
314
+ else if (b.type === 'resource' && typeof b.resource?.text === 'string')
315
+ texts.push(b.resource.text);
316
+ // The first image goes to the model as media (like `read_image`); more images, audio and binary resources do not.
317
+ else if (b.type === 'image' && !image && typeof b.data === 'string' && b.mimeType?.startsWith('image/'))
318
+ image = { data: b.data, mime: b.mimeType };
319
+ else
320
+ omitted++;
242
321
  }
243
- return { text, content: r.content, isError: r.isError };
322
+ if (omitted > 0)
323
+ texts.push(`[${omitted} non-text ${omitted === 1 ? 'item' : 'items'} omitted]`);
324
+ const text = clampOutput(texts.join('\n'));
325
+ if (!image)
326
+ return text;
327
+ return { text, dataUrl: `data:${image.mime};base64,${image.data}`, mime: image.mime, bytes: Math.floor((image.data.length * 3) / 4), path: 'MCP tool result' };
244
328
  }
@@ -134,6 +134,9 @@ export function loadSpecialistsFromConfig(configPath, options = {}) {
134
134
  out.warnings.push({ message: 'Skipped non-object entry' });
135
135
  continue;
136
136
  }
137
+ // A word-for-word copy would shadow the built-in and keep it at the version that was copied (RB-54).
138
+ if (isBuiltinCopy(entry))
139
+ continue;
137
140
  try {
138
141
  registerSpecialist(entry, { override: options.override ?? true, configDir });
139
142
  out.registered.push(entry.id);
@@ -166,9 +169,19 @@ export async function saveSpecialistsToConfig(entries, configPath) {
166
169
  for (const entry of entries) {
167
170
  validateSpecialistConfig(entry, configDir);
168
171
  }
169
- const file = { version: 1, specialists: entries };
172
+ // A word-for-word copy of a built-in would shadow it and freeze it at this version (RB-54). Drop it, and say
173
+ // where the call came from: nothing in Forge is meant to write one, so the stack names whoever does.
174
+ const copies = entries.filter(isBuiltinCopy);
175
+ if (copies.length > 0) {
176
+ console.warn(`forge: not saving ${copies.map((c) => c.id).join(', ')}: word-for-word copies of built-in agents.\n${new Error().stack ?? ''}`);
177
+ }
178
+ const file = { version: 1, specialists: entries.filter((e) => !isBuiltinCopy(e)) };
170
179
  await updateJsonFile(path, () => file);
171
180
  }
181
+ /** Same id and same prompt as a built-in: a copy, not the person's own agent. */
182
+ function isBuiltinCopy(entry) {
183
+ return BUILTIN_SPECIALISTS.some((b) => b.id === entry.id && b.systemPrompt === entry.systemPrompt);
184
+ }
172
185
  /**
173
186
  * Pure validator: returns a normalized {@link SpecialistConfig}.
174
187
  * Throws on any rule violation with a descriptive message. Library