headlesscode 1.0.2
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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt construction for the headless harness.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the vendored Zoo Code prompt builder (`SYSTEM_PROMPT` in
|
|
5
|
+
* `src/vendor/zoo-code/src/core/prompts/system.ts`) with:
|
|
6
|
+
*
|
|
7
|
+
* 1. A headless `ExtensionContext` stand-in (the vendored builder only reads
|
|
8
|
+
* `globalState` for custom modes when building the MODES section).
|
|
9
|
+
* 2. Project `.roomodes` loading from the workspace root (same zod schema the
|
|
10
|
+
* vendored `CustomModesManager` uses; the manager itself can't see the
|
|
11
|
+
* project dir because the headless `vscode` shim reports no workspace
|
|
12
|
+
* folders, so we load the file directly with `customModesSettingsSchema`).
|
|
13
|
+
* 3. Tool selection: the vendored `SYSTEM_PROMPT` does NOT embed a tool
|
|
14
|
+
* catalog — tools are passed to the API separately. We filter
|
|
15
|
+
* `getNativeTools()` down to the tools the Phase 1 executor actually
|
|
16
|
+
* registers (read_file, write_to_file, execute_command, list_files,
|
|
17
|
+
* attempt_completion, ask_followup_question), intersected with the mode's
|
|
18
|
+
* allowed tool groups (via the vendored `getToolsForMode` helper).
|
|
19
|
+
*
|
|
20
|
+
* Note: `.roo/rules-<mode>/`, `.roo/rules/`, and AGENTS.md splicing happens
|
|
21
|
+
* inside the vendored `SYSTEM_PROMPT` → `addCustomInstructions()`; we only
|
|
22
|
+
* need to pass the workspace root as `cwd` plus the mode slug. Stack-specific
|
|
23
|
+
* rules (src/engine/stacks.ts) are spliced HERE, after the vendored build —
|
|
24
|
+
* the vendored prompt sections have no hook for them.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as path from "node:path"
|
|
28
|
+
import * as fsp from "node:fs/promises"
|
|
29
|
+
import * as yaml from "yaml"
|
|
30
|
+
|
|
31
|
+
import { ensureSharedInstructionsMigration, sharedInstructionsRoot } from "../project-store.js"
|
|
32
|
+
|
|
33
|
+
// Side effect: installs String.prototype.toPosix() used by the prompt sections.
|
|
34
|
+
import "../vendor/zoo-code/src/utils/path.js"
|
|
35
|
+
|
|
36
|
+
import type { ExtensionContext } from "../vendor/zoo-code/shim/vscode.js"
|
|
37
|
+
import { SYSTEM_PROMPT } from "../vendor/zoo-code/src/core/prompts/system.js"
|
|
38
|
+
import { addCustomInstructions } from "../vendor/zoo-code/src/core/prompts/sections/custom-instructions.js"
|
|
39
|
+
import { getSystemInfoSection } from "../vendor/zoo-code/src/core/prompts/sections/system-info.js"
|
|
40
|
+
import { getNativeTools } from "../vendor/zoo-code/src/core/prompts/tools/native-tools/index.js"
|
|
41
|
+
import { getGroupName, getModeBySlug, modes, getToolsForMode } from "../vendor/zoo-code/src/shared/modes.js"
|
|
42
|
+
import { TOOL_GROUPS } from "../vendor/zoo-code/src/shared/tools.js"
|
|
43
|
+
import { customModesSettingsSchema } from "../vendor/zoo-code/types/index.js"
|
|
44
|
+
import { browserActionTool } from "../tools/browser/tool.js"
|
|
45
|
+
import { describeImageTool } from "../vision/tool.js"
|
|
46
|
+
import { runTestsTool } from "../tools/run-tests.js"
|
|
47
|
+
import { setIndentationTool } from "../tools/set-indentation-tool.js"
|
|
48
|
+
import { CODE_INTEL_TOOLS, RENAME_SYMBOL_TOOL } from "../codeintel/tools.js"
|
|
49
|
+
import type { ModeConfig } from "../vendor/zoo-code/types/index.js"
|
|
50
|
+
|
|
51
|
+
import { appendStackRulesSection, loadStackRules } from "./stacks.js"
|
|
52
|
+
import type { ChatTool } from "./types.js"
|
|
53
|
+
|
|
54
|
+
/** Opt-in gate for buildLeanSystemPrompt — see its doc comment. Default OFF. */
|
|
55
|
+
export const LEAN_SYSTEM_PROMPT_ENV = "HEADLESSCODE_LEAN_SYSTEM_PROMPT"
|
|
56
|
+
|
|
57
|
+
export function isLeanSystemPromptEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
58
|
+
const v = env[LEAN_SYSTEM_PROMPT_ENV]
|
|
59
|
+
return v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BuildSystemPromptOptions {
|
|
63
|
+
workspaceRoot: string
|
|
64
|
+
mode: string
|
|
65
|
+
customModes?: ModeConfig[]
|
|
66
|
+
globalCustomInstructions?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BuiltPrompt {
|
|
70
|
+
prompt: string
|
|
71
|
+
modeConfig: ModeConfig
|
|
72
|
+
customModes: ModeConfig[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The vendored tools the executor actually implements/registers. Used as the
|
|
77
|
+
* final gate when filtering `getNativeTools()` in selectToolsForMode. Two
|
|
78
|
+
* non-vendored tool sets are NOT in here because they never appear in
|
|
79
|
+
* getNativeTools(): browser_action and the four code-intelligence tools
|
|
80
|
+
* (outline / go_to_definition / find_references / import_graph) — callers
|
|
81
|
+
* append those explicitly via appendBrowserActionTool / appendCodeIntelTools.
|
|
82
|
+
*/
|
|
83
|
+
export const EXECUTABLE_TOOL_NAMES = new Set([
|
|
84
|
+
"read_file",
|
|
85
|
+
"write_to_file",
|
|
86
|
+
"apply_diff",
|
|
87
|
+
"search_replace",
|
|
88
|
+
"edit_file",
|
|
89
|
+
"execute_command",
|
|
90
|
+
"list_files",
|
|
91
|
+
"codebase_search",
|
|
92
|
+
"attempt_completion",
|
|
93
|
+
"ask_followup_question",
|
|
94
|
+
// The structured-planning aid (see src/vendor/zoo-code/.../update_todo_list.ts).
|
|
95
|
+
// Vendored ALWAYS_AVAILABLE_TOOLS already includes it for every mode's
|
|
96
|
+
// allowedNames; this is the final gate that makes it executable/advertised.
|
|
97
|
+
"update_todo_list",
|
|
98
|
+
// Recursive task decomposition: vendored modes' ALWAYS_AVAILABLE_TOOLS
|
|
99
|
+
// already includes new_task for every mode (shared/tools.ts); this is the
|
|
100
|
+
// final gate that makes it executable/advertised now that HeadlessSession
|
|
101
|
+
// implements the handler (see src/engine/loop.ts).
|
|
102
|
+
"new_task",
|
|
103
|
+
// switch_mode (plans/switch-mode-headless.md): vendored modes'
|
|
104
|
+
// ALWAYS_AVAILABLE_TOOLS already includes switch_mode for every mode
|
|
105
|
+
// (shared/tools.ts, same as new_task); this is the final gate that makes
|
|
106
|
+
// it executable/advertised now that HeadlessSession implements the
|
|
107
|
+
// handler. NOTE: read-only executors (reviewer/QA/local explore) still
|
|
108
|
+
// stub it — their tool lists never advertise it either (see the scope
|
|
109
|
+
// boundary in plans/switch-mode-headless.md).
|
|
110
|
+
"switch_mode",
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* True when a mode's tool groups include the `edit` group — i.e. the mode is
|
|
115
|
+
* expected to modify files (code, architect, …) as opposed to read-only
|
|
116
|
+
* investigation/review roles. Uses the SAME group classification the vendored
|
|
117
|
+
* prompt builder uses for MCP (`getGroupName(group) === "mcp"` in
|
|
118
|
+
* src/vendor/zoo-code/src/core/prompts/system.ts), so custom modes from
|
|
119
|
+
* .roomodes are classified exactly like the built-ins instead of by a
|
|
120
|
+
* hand-maintained slug list. Used to scope edit-workflow guidance (commit
|
|
121
|
+
* before finishing, targeted test runs) to modes that can actually edit.
|
|
122
|
+
*/
|
|
123
|
+
export function modeHasEditGroup(mode: string, customModes: ModeConfig[] = []): boolean {
|
|
124
|
+
const modeConfig = getModeBySlug(mode, customModes) ?? modes.find((m) => m.slug === mode)
|
|
125
|
+
if (modeConfig === undefined) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
return modeConfig.groups.some((group) => getGroupName(group) === "edit")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── custom modes loading (project .roomodes + global shared modes.yaml) ─────
|
|
132
|
+
// The GLOBAL modes file lives at ~/.local/share/headlesscode/shared/modes.yaml
|
|
133
|
+
// (headlesscode-native location — src/project-store.ts) instead of the
|
|
134
|
+
// Zoo-Code-branded ~/.roo/custom_modes.yaml. The file FORMAT (YAML, schema)
|
|
135
|
+
// and the merge semantics are unchanged; only the lookup path moved.
|
|
136
|
+
|
|
137
|
+
const PROBLEMATIC_CHARS_REGEX =
|
|
138
|
+
// eslint-disable-next-line no-misleading-character-class
|
|
139
|
+
/[\u00A0\u200B\u200C\u200D\u2010\u2011\u2012\u2013\u2014\u2015\u2212\u2018\u2019\u201C\u201D]/g
|
|
140
|
+
|
|
141
|
+
function cleanInvisibleCharacters(content: string): string {
|
|
142
|
+
return content.replace(PROBLEMATIC_CHARS_REGEX, (match) => {
|
|
143
|
+
switch (match) {
|
|
144
|
+
case "\u00A0":
|
|
145
|
+
return " "
|
|
146
|
+
case "\u200B":
|
|
147
|
+
case "\u200C":
|
|
148
|
+
case "\u200D":
|
|
149
|
+
return ""
|
|
150
|
+
case "\u2018":
|
|
151
|
+
case "\u2019":
|
|
152
|
+
return "'"
|
|
153
|
+
case "\u201C":
|
|
154
|
+
case "\u201D":
|
|
155
|
+
return '"'
|
|
156
|
+
default:
|
|
157
|
+
return "-"
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse + validate a custom-modes settings file (YAML, JSON fallback), mirroring
|
|
164
|
+
* the vendored `CustomModesManager.loadModesFromFile` behaviour. Non-fatal:
|
|
165
|
+
* a missing file returns [], a malformed/unparseable file logs an error and
|
|
166
|
+
* returns [] (never throws). Used for both the project `.roomodes` and the
|
|
167
|
+
* global `~/.roo/custom_modes.yaml`.
|
|
168
|
+
*
|
|
169
|
+
* @param filePath absolute path of the modes file to read
|
|
170
|
+
* @param source "project" for `.roomodes`, "global" for `modes.yaml`
|
|
171
|
+
*/
|
|
172
|
+
async function loadModesFromFile(filePath: string, source: "project" | "global"): Promise<ModeConfig[]> {
|
|
173
|
+
let raw: string
|
|
174
|
+
try {
|
|
175
|
+
raw = await fsp.readFile(filePath, "utf-8")
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
throw error
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Strip BOM, then clean invisible/problematic chars exactly like vendored.
|
|
184
|
+
let cleaned = raw.replace(/^\uFEFF/, "")
|
|
185
|
+
cleaned = cleanInvisibleCharacters(cleaned)
|
|
186
|
+
|
|
187
|
+
let parsed: unknown
|
|
188
|
+
try {
|
|
189
|
+
parsed = yaml.parse(cleaned) ?? {}
|
|
190
|
+
} catch (yamlError) {
|
|
191
|
+
// JSON fallback for .roomodes.
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(raw) ?? {}
|
|
194
|
+
} catch {
|
|
195
|
+
console.error(`[headlesscode] Failed to parse ${path.basename(filePath)} at ${filePath}: ${String(yamlError)}`)
|
|
196
|
+
return []
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray((parsed as { customModes?: unknown }).customModes)) {
|
|
201
|
+
return []
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = customModesSettingsSchema.safeParse(parsed)
|
|
205
|
+
if (!result.success) {
|
|
206
|
+
const issues = result.error.issues
|
|
207
|
+
.map((issue) => `• ${issue.path.join(".")}: ${issue.message}`)
|
|
208
|
+
.join("\n")
|
|
209
|
+
console.error(`[headlesscode] ${path.basename(filePath)} schema validation failed at ${filePath}:\n${issues}`)
|
|
210
|
+
return []
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result.data.customModes.map((mode) => ({ ...mode, source }))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Load the merged set of custom modes for a workspace: the project's own
|
|
218
|
+
* `.roomodes` first (project wins on slug collision), then the GLOBAL shared
|
|
219
|
+
* modes file — `~/.local/share/headlesscode/shared/modes.yaml` (see
|
|
220
|
+
* src/project-store.ts; the one-time ~/.roo/ migration is triggered here on
|
|
221
|
+
* first use) — for any slug the project does NOT define. Same precedence
|
|
222
|
+
* model as the vendored `CustomModesManager.mergeCustomModes`. A missing or
|
|
223
|
+
* malformed global file degrades exactly like a missing/malformed `.roomodes`
|
|
224
|
+
* (returns [] for that source, logs, never throws).
|
|
225
|
+
*/
|
|
226
|
+
export async function loadCustomModes(workspaceRoot: string): Promise<ModeConfig[]> {
|
|
227
|
+
ensureSharedInstructionsMigration()
|
|
228
|
+
const projectModes = await loadModesFromFile(path.join(workspaceRoot, ".roomodes"), "project")
|
|
229
|
+
const globalModes = await loadModesFromFile(path.join(sharedInstructionsRoot(), "modes.yaml"), "global")
|
|
230
|
+
|
|
231
|
+
const slugs = new Set<string>()
|
|
232
|
+
const merged: ModeConfig[] = []
|
|
233
|
+
|
|
234
|
+
// Project modes first — they take precedence over same-slug global modes.
|
|
235
|
+
for (const mode of projectModes) {
|
|
236
|
+
if (!slugs.has(mode.slug)) {
|
|
237
|
+
slugs.add(mode.slug)
|
|
238
|
+
merged.push(mode)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Global modes fill in everything the project does not define.
|
|
243
|
+
for (const mode of globalModes) {
|
|
244
|
+
if (!slugs.has(mode.slug)) {
|
|
245
|
+
slugs.add(mode.slug)
|
|
246
|
+
merged.push(mode)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return merged
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Context stand-in ────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Minimal `vscode.ExtensionContext` stand-in for the vendored builder. The
|
|
257
|
+
* builder only reads `globalState.get("customModes")` (MODES section) and
|
|
258
|
+
* `vscode.env.language` (from the shim). Everything else is unused headlessly.
|
|
259
|
+
*/
|
|
260
|
+
export function createHeadlessContext(customModes: ModeConfig[]): ExtensionContext {
|
|
261
|
+
const state = new Map<string, unknown>([["customModes", customModes]])
|
|
262
|
+
return {
|
|
263
|
+
globalState: {
|
|
264
|
+
get: async <T>(key: string): Promise<T | undefined> => state.get(key) as T | undefined,
|
|
265
|
+
update: async (key: string, value: unknown): Promise<void> => {
|
|
266
|
+
state.set(key, value)
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
globalStorageUri: { fsPath: process.cwd() },
|
|
270
|
+
subscriptions: [],
|
|
271
|
+
} as unknown as ExtensionContext
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Prompt builder ──────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Build the system prompt for a mode using the vendored builder.
|
|
278
|
+
*
|
|
279
|
+
* @param options.workspaceRoot project root used as `cwd` (rules discovery)
|
|
280
|
+
* @param options.mode mode slug (built-in or from .roomodes)
|
|
281
|
+
* @param options.customModes pre-loaded custom modes (default: load from .roomodes)
|
|
282
|
+
* @param options.globalCustomInstructions optional global instructions text
|
|
283
|
+
*/
|
|
284
|
+
export async function buildSystemPrompt(options: BuildSystemPromptOptions): Promise<BuiltPrompt> {
|
|
285
|
+
const { workspaceRoot, mode, globalCustomInstructions } = options
|
|
286
|
+
const customModes = options.customModes ?? (await loadCustomModes(workspaceRoot))
|
|
287
|
+
|
|
288
|
+
const modeConfig =
|
|
289
|
+
getModeBySlug(mode, customModes) ?? modes.find((m) => m.slug === mode) ?? modes[0]
|
|
290
|
+
|
|
291
|
+
const context = createHeadlessContext(customModes)
|
|
292
|
+
|
|
293
|
+
// settings: disable the todo list prompt & subfolder rules (Phase 1 keeps it
|
|
294
|
+
// simple); keep useAgentRules on so AGENTS.md/rules files are honored.
|
|
295
|
+
const settings = {
|
|
296
|
+
todoListEnabled: false,
|
|
297
|
+
useAgentRules: true,
|
|
298
|
+
enableSubfolderRules: false,
|
|
299
|
+
newTaskRequireTodos: false,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const prompt = await SYSTEM_PROMPT(
|
|
303
|
+
context,
|
|
304
|
+
workspaceRoot,
|
|
305
|
+
false, // supportsComputerUse
|
|
306
|
+
undefined, // mcpHub
|
|
307
|
+
undefined, // diffStrategy
|
|
308
|
+
modeConfig.slug,
|
|
309
|
+
undefined, // customModePrompts
|
|
310
|
+
customModes,
|
|
311
|
+
globalCustomInstructions,
|
|
312
|
+
undefined, // experiments
|
|
313
|
+
undefined, // language (shim default "en")
|
|
314
|
+
undefined, // rooIgnoreInstructions
|
|
315
|
+
settings,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
// Stack-specific rules (src/engine/stacks.ts): appended as a clearly
|
|
319
|
+
// delimited section only when a detected stack actually has rules content
|
|
320
|
+
// — zero stacks / no rules.md keeps the prompt byte-identical.
|
|
321
|
+
const stackRules = await loadStackRules(workspaceRoot)
|
|
322
|
+
const withStackRules = stackRules.content
|
|
323
|
+
? appendStackRulesSection(prompt, stackRules.content, stackRules.stacks)
|
|
324
|
+
: prompt
|
|
325
|
+
|
|
326
|
+
// Headless harness workflow conventions, appended AFTER the vendored
|
|
327
|
+
// builder's output (which is vendored code and must not be edited in
|
|
328
|
+
// place). These are harness-level expectations that live outside any
|
|
329
|
+
// single rules file: committing real work before finishing, running the
|
|
330
|
+
// targeted test file(s) during the iterative edit-check loop, and
|
|
331
|
+
// batching same-file diffs into one apply_diff. Edit-workflow guidance
|
|
332
|
+
// only applies to modes that can actually edit files (modeHasEditGroup);
|
|
333
|
+
// read-only reviewer/QA roles never see it.
|
|
334
|
+
const conventions = buildHeadlessConventions(modeConfig.slug, customModes)
|
|
335
|
+
const fullPrompt = conventions === undefined ? withStackRules : `${withStackRules}\n${conventions}`
|
|
336
|
+
|
|
337
|
+
return { prompt: fullPrompt, modeConfig, customModes }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A minimal alternative to buildSystemPrompt for small-context local models
|
|
342
|
+
* (opt-in — see HEADLESSCODE_LEAN_SYSTEM_PROMPT in loop.ts). The vendored
|
|
343
|
+
* SYSTEM_PROMPT (~9.5K tokens measured live 2026-08-19 for headlesscode's
|
|
344
|
+
* own `code` mode) is GUI-oriented: a full modes-listing section, generic
|
|
345
|
+
* tool-use-guidelines prose, and a capabilities section describing VS
|
|
346
|
+
* Code-only features — all irrelevant to a native-tool-calling headless
|
|
347
|
+
* session and a severe cost against a ~24K-token local context budget.
|
|
348
|
+
*
|
|
349
|
+
* This does NOT edit or reimplement the vendored prompt; it composes the
|
|
350
|
+
* SAME exported section builders `buildSystemPrompt` uses, minus the
|
|
351
|
+
* GUI-oriented ones (markdownFormattingSection, getSharedToolUseSection,
|
|
352
|
+
* getToolUseGuidelinesSection, getCapabilitiesSection, getModesSection,
|
|
353
|
+
* getSkillsSection). Critically, addCustomInstructions (this project's own
|
|
354
|
+
* `.roo/rules/`, `.roo/rules-<mode>/`, AGENTS.md) is KEPT — that content is
|
|
355
|
+
* project-specific behavioral guidance, not boilerplate, and dropping it
|
|
356
|
+
* would silently change what the model is told it must/must not do.
|
|
357
|
+
*/
|
|
358
|
+
export async function buildLeanSystemPrompt(options: BuildSystemPromptOptions): Promise<BuiltPrompt> {
|
|
359
|
+
const { workspaceRoot, mode, globalCustomInstructions } = options
|
|
360
|
+
const customModes = options.customModes ?? (await loadCustomModes(workspaceRoot))
|
|
361
|
+
const modeConfig =
|
|
362
|
+
getModeBySlug(mode, customModes) ?? modes.find((m) => m.slug === mode) ?? modes[0]
|
|
363
|
+
|
|
364
|
+
// Strengthened 2026-08-19 after a live failure: Qwen2.5-Coder-14B
|
|
365
|
+
// reasoned correctly about which tool to call, then wrote
|
|
366
|
+
// `{"name": "edit_file", "arguments": {...}}` as PROSE TEXT (sometimes
|
|
367
|
+
// in a ```json fence) instead of issuing a real tool_calls entry — the
|
|
368
|
+
// harness now has a best-effort recovery for this specific shape (see
|
|
369
|
+
// extractEmbeddedToolCall in parser.ts), but recovery only fires for a
|
|
370
|
+
// name that matches a real tool and is not a substitute for calling
|
|
371
|
+
// correctly the first time: it costs a wasted turn, and anything that
|
|
372
|
+
// doesn't match a known tool name gets no recovery at all.
|
|
373
|
+
const toolUseNote =
|
|
374
|
+
"Tool calls are native — call a tool directly through the tool-calling " +
|
|
375
|
+
"mechanism, never describe an action in prose instead of calling it. " +
|
|
376
|
+
"Concretely: if your response's text content contains something that " +
|
|
377
|
+
"LOOKS like a tool call — a JSON object with \"name\" and \"arguments\" " +
|
|
378
|
+
"keys, with or without a ```json fence — that is a MISTAKE, not a way " +
|
|
379
|
+
"to invoke a tool. It will not run. Writing out what a tool call would " +
|
|
380
|
+
"look like is never itself progress; only an actual tool_calls entry " +
|
|
381
|
+
"executes anything. Never fabricate file contents or command output " +
|
|
382
|
+
"you have not actually seen."
|
|
383
|
+
// Verified live 2026-08-27: a local session correctly diagnosed a fix in
|
|
384
|
+
// its own reasoning text ("the error message says X, which means it's
|
|
385
|
+
// expecting Y instead") — genuinely correct — and then, instead of
|
|
386
|
+
// calling edit_file to apply that exact fix, just repeated the same
|
|
387
|
+
// diagnosis as plain text for 6 consecutive turns until it hit the
|
|
388
|
+
// session's mistake limit and lost the work entirely. In the same
|
|
389
|
+
// transcript, one of those repeats was a text-only tool call for a tool
|
|
390
|
+
// that was on a short cooldown from an earlier repeated call — a
|
|
391
|
+
// different, uncooled tool (edit_file) was available the whole time but
|
|
392
|
+
// never attempted. Reasoning correctly about a fix is not the same as
|
|
393
|
+
// applying it — the moment you know what change to make, make it with a
|
|
394
|
+
// real tool call (edit_file/write_to_file) in that same turn, don't
|
|
395
|
+
// restate the diagnosis first. If your last attempted action didn't run
|
|
396
|
+
// (a tool was blocked, on cooldown, or errored), the fix is to try a
|
|
397
|
+
// DIFFERENT tool that is actually available right now, not to explain
|
|
398
|
+
// the same conclusion again and wait.
|
|
399
|
+
const stalledRetryNote =
|
|
400
|
+
"When you've figured out what change to make, make it immediately with " +
|
|
401
|
+
"a real tool call (edit_file/write_to_file) in the same turn — restating " +
|
|
402
|
+
"your diagnosis in plain text first is not progress and will not retry " +
|
|
403
|
+
"itself. If a tool call didn't go through (blocked, on cooldown, or " +
|
|
404
|
+
"errored), pick a different tool that IS available right now instead of " +
|
|
405
|
+
"repeating the same reasoning or the same blocked call again."
|
|
406
|
+
const verificationNote =
|
|
407
|
+
"Only call attempt_completion after you have verified the change actually " +
|
|
408
|
+
"works (typecheck/tests as applicable) — a plausible-looking but " +
|
|
409
|
+
"unverified change is not done. Call attempt_completion ALONE, never in " +
|
|
410
|
+
"the same turn as other tool calls — a bundled attempt_completion is " +
|
|
411
|
+
"refused and your other calls are wasted."
|
|
412
|
+
// GitHub issue #137: local sessions were observed calling
|
|
413
|
+
// ask_followup_question for routine implementation decisions they
|
|
414
|
+
// should just make themselves, even when the task text explicitly
|
|
415
|
+
// said not to — a per-task instruction alone is not enough, because
|
|
416
|
+
// it competes with whatever baseline tendency this prompt otherwise
|
|
417
|
+
// leaves unaddressed. Stated once, here, so it applies regardless of
|
|
418
|
+
// what any individual task file does or doesn't say.
|
|
419
|
+
const followupQuestionNote =
|
|
420
|
+
"Prefer making a reasonable, reversible decision yourself and stating " +
|
|
421
|
+
"your reasoning over calling ask_followup_question. Reserve that tool " +
|
|
422
|
+
"for choices that are genuinely destructive or ambiguous enough that " +
|
|
423
|
+
"guessing wrong would be costly — not for routine implementation " +
|
|
424
|
+
"decisions you can just make and adjust later if wrong."
|
|
425
|
+
// Replaces the vendored getObjectiveSection() (buildSystemPrompt uses that
|
|
426
|
+
// one): it references an "environment_details" file that does not exist
|
|
427
|
+
// in this headless harness (verified live 2026-08-19 — a local session
|
|
428
|
+
// tried to read_file a literal file named "environment_details" after
|
|
429
|
+
// reading that section) and describes GUI-chat framing ("the user may
|
|
430
|
+
// provide feedback") that does not apply to a one-shot headless task.
|
|
431
|
+
// Also states the one concrete literalism trap that broke a real local
|
|
432
|
+
// session that same night: a tool description's [bracketed] example is a
|
|
433
|
+
// placeholder to replace with a real value, not text to copy verbatim.
|
|
434
|
+
const objectiveNote =
|
|
435
|
+
"Work through the task step by step: read what you need, make the " +
|
|
436
|
+
"edit, then verify it before finishing. There is no separate " +
|
|
437
|
+
"'environment_details' file — file listings and tool results appear " +
|
|
438
|
+
"directly in this conversation as they happen. When a tool's own " +
|
|
439
|
+
"description shows an example value inside [square brackets] (e.g. " +
|
|
440
|
+
"[line_number], [path]), that is a placeholder: replace the WHOLE " +
|
|
441
|
+
"bracketed expression with a real value — never include the literal " +
|
|
442
|
+
"brackets or the word inside them in an actual tool call."
|
|
443
|
+
|
|
444
|
+
// Added 2026-08-21 after a live failure pattern reproduced identically
|
|
445
|
+
// across two separate trials: the model completed the FIRST step of a
|
|
446
|
+
// two-step task (edit a file) correctly, then — instead of proceeding to
|
|
447
|
+
// the second step (write the test file) — called list_files repeatedly
|
|
448
|
+
// until the identical-call guardrail ended the session. `update_todo_list`
|
|
449
|
+
// is registered and offered every turn, but the vendored system prompt's
|
|
450
|
+
// todo-list guidance is disabled for this harness (`todoListEnabled:
|
|
451
|
+
// false` in buildSystemPrompt, kept off deliberately — Phase 1 scope, and
|
|
452
|
+
// this note is scoped to local sessions only). A smaller local model
|
|
453
|
+
// plausibly needs the EXTERNAL scaffolding of a written-out plan more
|
|
454
|
+
// than a larger cloud model does, since it has less working "attention"
|
|
455
|
+
// left over after a growing tool-result-heavy context to track what it
|
|
456
|
+
// still needs to do implicitly. Strengthened again same day: a trial
|
|
457
|
+
// wrote its FIRST update_todo_list call with all three steps already
|
|
458
|
+
// marked [x] done, before doing any of the actual work — a premature,
|
|
459
|
+
// theatrical completion-marking pattern that defeats the whole point
|
|
460
|
+
// (it also meant the identical-call nudge's "next pending step" lookup,
|
|
461
|
+
// see identicalCallNudgeMessage in loop.ts, found nothing to point at).
|
|
462
|
+
const planningNote =
|
|
463
|
+
"For any task with more than one concrete step (e.g. edit a file AND write a test for it), call " +
|
|
464
|
+
"update_todo_list ONCE near the start to write out every step as PENDING [ ] — not already checked off; " +
|
|
465
|
+
"you have not done any of it yet at that point. Only mark a step done [x] AFTER you have actually " +
|
|
466
|
+
"performed it and, where applicable, verified it. Then check the list again before deciding what to do " +
|
|
467
|
+
"next: your next action should come from the first step still marked pending, not a fresh look around " +
|
|
468
|
+
"the workspace. If a step fails (a tool error, a failing command), your next action is to read that " +
|
|
469
|
+
"error and address it specifically — not to fall back on list_files or another unrelated exploration call."
|
|
470
|
+
|
|
471
|
+
// Added 2026-08-21 after a live failure pattern reproduced across THREE
|
|
472
|
+
// separate trials: asked to write a new test file "following the
|
|
473
|
+
// existing plain-assert test style used elsewhere," the model instead
|
|
474
|
+
// defaulted to generic Jest syntax (describe/test/expect — not used
|
|
475
|
+
// anywhere in this codebase) every time, then got a tsc error pointing
|
|
476
|
+
// at exactly that mismatch. The task text already SAID to match the
|
|
477
|
+
// existing style; the model never actually looked at an existing file
|
|
478
|
+
// to see what that style was before writing its own. (The one trial
|
|
479
|
+
// that recovered did so only after an ask_followup_question forced a
|
|
480
|
+
// human to say "go read an existing file first" explicitly — this note
|
|
481
|
+
// makes that the DEFAULT behavior instead of something that only
|
|
482
|
+
// happens after a wasted round trip.)
|
|
483
|
+
// 2026-08-22 addendum, same failure family: a trial that DID avoid the
|
|
484
|
+
// Jest mistake above still skipped the read_file step entirely and wrote
|
|
485
|
+
// a NEW test file using `console.assert(...)` — plausible-looking (it
|
|
486
|
+
// even has the word "assert" in it), but `console.assert` never throws
|
|
487
|
+
// on failure, so a genuinely wrong implementation (verified live:
|
|
488
|
+
// truncateMiddle producing length 33 instead of the required 30) still
|
|
489
|
+
// printed as a clean pass. Calling out the exact trap by name, since
|
|
490
|
+
// "read an example first" alone did not stop the model from guessing.
|
|
491
|
+
// 2026-08-20 addendum, same failure family, now with the describe/it
|
|
492
|
+
// example promoted from a passing "e.g." to its own explicit trap:
|
|
493
|
+
// despite the "generic default... is not correct" line already above,
|
|
494
|
+
// separate live trials against Qwen3-14B STILL wrote `describe(...)` /
|
|
495
|
+
// `it(...)` test files every time (verified live, repeated across
|
|
496
|
+
// several trials the same day) — the assertion-library part of the
|
|
497
|
+
// note (node:assert vs console.assert) was being followed correctly,
|
|
498
|
+
// but the test-STRUCTURE convention (no test-runner globals at all;
|
|
499
|
+
// flat top-level functions) was not landing from the "e.g." mention
|
|
500
|
+
// alone. tsc's own error for `describe`/`it` suggests installing
|
|
501
|
+
// `@types/jest` or `@types/mocha` — which reads as a legitimate fix
|
|
502
|
+
// to a model that hasn't internalized this project has no test
|
|
503
|
+
// runner, producing an ask_followup_question instead of a self-
|
|
504
|
+
// correction. Naming the concrete pattern by name, same reasoning as
|
|
505
|
+
// the console.assert addendum below: a soft "e.g." was not enough to
|
|
506
|
+
// stop a strong prior toward the generic default.
|
|
507
|
+
const testStructureNote =
|
|
508
|
+
"This codebase has NO test runner (no Jest, no Mocha, no Vitest) — never write `describe(...)`, `it(...)`, " +
|
|
509
|
+
"`test(...)`, or `expect(...)`, and never suggest installing `@types/jest` or `@types/mocha` even if a tsc " +
|
|
510
|
+
"error recommends it; that error means the test file itself is wrong, not that a dependency is missing. " +
|
|
511
|
+
"The real convention is flat top-level functions (e.g. `function testSomething(): void { ... }` or " +
|
|
512
|
+
"`async function testSomething(): Promise<void> { ... }`), each containing plain assert calls, collected " +
|
|
513
|
+
"into an array of `[name, fn]` pairs and run by a `main()` that calls each one and reports pass/fail — " +
|
|
514
|
+
"read an existing `*.test.ts` file in the same directory to copy the exact shape before writing a new one."
|
|
515
|
+
|
|
516
|
+
// 2026-08-20 addendum, tool-call JSON specifically: verified live
|
|
517
|
+
// against Qwen3-14B — asked to edit_file a line of source that itself
|
|
518
|
+
// contains double-quoted string literals (e.g. a TypeScript file full
|
|
519
|
+
// of `"..."` text), the model wrapped its `old_string` JSON value in
|
|
520
|
+
// SINGLE quotes instead ('...') to dodge escaping the embedded double
|
|
521
|
+
// quotes, escaping only the occasional literal apostrophe inside
|
|
522
|
+
// (`\'`) — this produces invalid JSON (JSON strings are ALWAYS
|
|
523
|
+
// double-quoted; single quotes are never valid), the tool call fails
|
|
524
|
+
// to parse, and the same broken JSON then repeats verbatim across
|
|
525
|
+
// every retry because the model doesn't recognize single-quoting as
|
|
526
|
+
// the actual defect. Naming it explicitly since escaping quotes
|
|
527
|
+
// correctly is exactly the kind of mechanical detail a general "write
|
|
528
|
+
// valid JSON" instruction doesn't reliably cover for code containing
|
|
529
|
+
// its own quote-heavy string literals.
|
|
530
|
+
const jsonEscapingNote =
|
|
531
|
+
"Tool call arguments are JSON: every string value MUST be wrapped in double quotes, never single quotes " +
|
|
532
|
+
"— JSON has no single-quoted string syntax at all, so a single-quoted value is not a formatting choice, " +
|
|
533
|
+
"it is invalid JSON that will fail to parse. When the text you are copying into an argument (e.g. " +
|
|
534
|
+
"edit_file's old_string/new_string) itself contains double-quote characters — common when editing code " +
|
|
535
|
+
"that has string literals — keep the outer JSON string double-quoted and escape each embedded `\"` as " +
|
|
536
|
+
"`\\\"`. Do not switch the outer quote character to avoid escaping; that is the mistake, not a workaround."
|
|
537
|
+
|
|
538
|
+
// 2026-08-27 addendum, execute_command specifically: verified live
|
|
539
|
+
// against Qwen3.5-9B — asked to verify an edit it had already made
|
|
540
|
+
// correctly, the model wrote a multi-line `python3 -c "with open(...) as
|
|
541
|
+
// f: ..."` verification command whose JSON argument had the exact
|
|
542
|
+
// single/double-quote-nesting defect jsonEscapingNote describes above,
|
|
543
|
+
// which errored through the tool (the identical command ran fine when
|
|
544
|
+
// re-run by hand outside the harness, confirming the JSON encoding —
|
|
545
|
+
// not the shell command itself — was the defect). Because
|
|
546
|
+
// attempt_completion had ALREADY been deferred once by that point (a
|
|
547
|
+
// prior command had failed), the model was one mistake from the
|
|
548
|
+
// session's hard stop and used its last one narrating in prose instead
|
|
549
|
+
// of retrying — see execute_command_recovery_note below for that half
|
|
550
|
+
// of the failure.
|
|
551
|
+
const executeCommandQuotingNote =
|
|
552
|
+
"For verification commands specifically (checking a file's contents, confirming a string appears " +
|
|
553
|
+
"somewhere), prefer the simplest command that answers the question — grep, cat, wc, test — over a " +
|
|
554
|
+
"multi-line python3/node one-liner. A simple command has far less quoting for you to get right in the " +
|
|
555
|
+
"JSON argument; a one-liner that mixes single quotes, double quotes, and embedded code is exactly where " +
|
|
556
|
+
"the jsonEscapingNote mistake above tends to happen, and a failure there costs you a mistake strike for " +
|
|
557
|
+
"no benefit over the simpler command."
|
|
558
|
+
// 2026-08-27 addendum, the other half of the same live failure: after
|
|
559
|
+
// attempt_completion was deferred, the model's recovery attempts
|
|
560
|
+
// (execute_command retries, then prose) never included just re-issuing
|
|
561
|
+
// attempt_completion once it believed — correctly, per the read_file
|
|
562
|
+
// re-check it had already done — that the task was actually finished.
|
|
563
|
+
// Three non-recovering turns in a row (two failed execute_command
|
|
564
|
+
// retries, one prose-only reply) hit the session's hard stop on a task
|
|
565
|
+
// that was already done. The deferral message already names the exact
|
|
566
|
+
// fix; this states the general rule so it's not the model's first time
|
|
567
|
+
// seeing this pattern.
|
|
568
|
+
const executeCommandRecoveryNote =
|
|
569
|
+
"If attempt_completion is deferred (a system message will say so and name the reason), your NEXT reply " +
|
|
570
|
+
"must be a real tool call, not prose — either fix the specific thing the message names and re-verify with " +
|
|
571
|
+
"a command that actually succeeds, or, if you already have real evidence the work is correct, simply call " +
|
|
572
|
+
"attempt_completion again. A text-only reply explaining why you think you're done does not end the " +
|
|
573
|
+
"session and counts against your mistake budget the same as a failed command — it is never the right " +
|
|
574
|
+
"response to a deferral."
|
|
575
|
+
|
|
576
|
+
// 2026-08-27 addendum, general case (executeCommandRecoveryNote above
|
|
577
|
+
// only covers the narrower attempt_completion-deferral scenario):
|
|
578
|
+
// verified live against Qwen3.5-9B with the lean prompt active — a
|
|
579
|
+
// read_file call errored on a malformed path, and the model's next SIX
|
|
580
|
+
// replies in a row were short text-only acknowledgments ("I see the
|
|
581
|
+
// issue - I need to read from line 601 correctly. Let me continue
|
|
582
|
+
// reading the file:") with NO tool call attached, nearly identical
|
|
583
|
+
// each time, until the session hit its bounded-failure limit. This
|
|
584
|
+
// was not a fabricated/malformed tool call (extractEmbeddedToolCall's
|
|
585
|
+
// recovery does not apply here) and not task drift — the model
|
|
586
|
+
// correctly identified the fix in its own words but never issued the
|
|
587
|
+
// corrected call itself. The harness's reactive per-turn nudge
|
|
588
|
+
// ("[System: a text reply alone does not end the session...]") did
|
|
589
|
+
// not break the pattern. Independent evidence (a practitioner test
|
|
590
|
+
// against Qwen3:14b hitting the same "narrate instead of retry"
|
|
591
|
+
// pattern after a shell error) found that stating the rule as a
|
|
592
|
+
// standing directive IN THE SYSTEM PROMPT, rather than only as a
|
|
593
|
+
// reactive after-the-fact correction, was what actually fixed it —
|
|
594
|
+
// taking that model from <20% to 100% success on the same task. This
|
|
595
|
+
// note is the same intervention, generalized from execute_command to
|
|
596
|
+
// every tool.
|
|
597
|
+
const toolErrorRecoveryNote =
|
|
598
|
+
"When any tool call returns an error, your NEXT reply must be the actual corrected tool call itself — " +
|
|
599
|
+
"never a sentence describing that you will retry, are about to fix the path, or see the issue. Narrating " +
|
|
600
|
+
"an intended retry is not progress and does not get executed; only a real tool call does. If you catch " +
|
|
601
|
+
"yourself about to write something like 'let me try again' or 'I need to correct the path', stop and " +
|
|
602
|
+
"put the corrected call in that same reply instead of describing it."
|
|
603
|
+
|
|
604
|
+
const conventionNote =
|
|
605
|
+
"Before writing a NEW file of a kind that likely already has examples in this codebase (a test file, a " +
|
|
606
|
+
"config file, a module following an established pattern), actually read_file an existing example FIRST " +
|
|
607
|
+
"and match its real conventions — imports, style, framework/assertion library, naming. Do not assume a " +
|
|
608
|
+
"generic default (e.g. Jest-style describe/test/expect) is correct just because it is common elsewhere; " +
|
|
609
|
+
"this project may use something else entirely, and a task that says 'follow the existing style' means " +
|
|
610
|
+
"look at the existing style, not guess at it. This specifically includes the assertion call itself: this " +
|
|
611
|
+
"codebase's 'plain assert' style is Node's own `assert` module (`import assert from \"node:assert/strict\"`, " +
|
|
612
|
+
"then `assert.equal(...)` / `assert.match(...)`), which THROWS on failure — never `console.assert(...)`, " +
|
|
613
|
+
"which only prints a warning and lets execution continue, so a real bug would print as a clean pass. " +
|
|
614
|
+
testStructureNote +
|
|
615
|
+
" " +
|
|
616
|
+
jsonEscapingNote +
|
|
617
|
+
" " +
|
|
618
|
+
executeCommandQuotingNote
|
|
619
|
+
|
|
620
|
+
const basePrompt = [
|
|
621
|
+
modeConfig.roleDefinition,
|
|
622
|
+
toolUseNote,
|
|
623
|
+
stalledRetryNote,
|
|
624
|
+
verificationNote,
|
|
625
|
+
followupQuestionNote,
|
|
626
|
+
objectiveNote,
|
|
627
|
+
planningNote,
|
|
628
|
+
conventionNote,
|
|
629
|
+
executeCommandRecoveryNote,
|
|
630
|
+
toolErrorRecoveryNote,
|
|
631
|
+
getSystemInfoSection(workspaceRoot),
|
|
632
|
+
await addCustomInstructions(modeConfig.customInstructions ?? "", globalCustomInstructions ?? "", workspaceRoot, modeConfig.slug, {}),
|
|
633
|
+
]
|
|
634
|
+
.filter((section) => section.trim().length > 0)
|
|
635
|
+
.join("\n\n")
|
|
636
|
+
|
|
637
|
+
const stackRules = await loadStackRules(workspaceRoot)
|
|
638
|
+
const withStackRules = stackRules.content
|
|
639
|
+
? appendStackRulesSection(basePrompt, stackRules.content, stackRules.stacks)
|
|
640
|
+
: basePrompt
|
|
641
|
+
|
|
642
|
+
const conventions = buildHeadlessConventions(modeConfig.slug, customModes)
|
|
643
|
+
const fullPrompt = conventions === undefined ? withStackRules : `${withStackRules}\n${conventions}`
|
|
644
|
+
|
|
645
|
+
return { prompt: fullPrompt, modeConfig, customModes }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* The headless-specific workflow section spliced onto the end of the system
|
|
650
|
+
* prompt for edit-capable modes (see buildSystemPrompt). Keep it SHORT and
|
|
651
|
+
* scannable — it is one block of prose among the vendored sections, not a
|
|
652
|
+
* replacement for the tools' own descriptions or the rules files.
|
|
653
|
+
*/
|
|
654
|
+
export function buildHeadlessConventions(mode: string, customModes: ModeConfig[] = []): string | undefined {
|
|
655
|
+
if (!modeHasEditGroup(mode, customModes)) {
|
|
656
|
+
return undefined
|
|
657
|
+
}
|
|
658
|
+
return `# Headless harness workflow conventions
|
|
659
|
+
|
|
660
|
+
- Commit before finishing: real, working changes must be committed via \`git add\` + \`git commit\` BEFORE calling attempt_completion. Use one commit per logical change with a descriptive message, matching this repo's normal style (run \`git log --oneline\` for examples). If you genuinely have no changes to commit (e.g. a read-only investigation), finish without committing.
|
|
661
|
+
- Test selection during iterative work: after editing a file, run the SPECIFIC test file(s) for what you changed (the \`run_tests\` tool, or \`npx tsx <test-file>\` directly) instead of the full suite on every edit. The full \`npm test\` is still required once, right before attempt_completion, for real confidence.
|
|
662
|
+
- Batch same-file diffs: when several changes target the SAME file, include them as separate SEARCH/REPLACE blocks in ONE apply_diff call — and after any successful edit, re-read the file before composing further diffs, because its content has changed.
|
|
663
|
+
- Waiting on an external check (CI run, registry, live service) is legitimate verification work, but every wait call still counts against your iteration budget: prefer ONE long-running command with an explicit \`timeout\` — e.g. \`gh run watch <id> --interval 30 --exit-status\` — over repeated \`sleep N && gh run list\` polls. (Issue #119: polling burns iterations with no token spend.)`
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ─── Tool selection ──────────────────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Select the OpenAI-format tool schemas to expose to the model for a mode.
|
|
670
|
+
*
|
|
671
|
+
* Decision (Phase 1): intersect the mode's allowed tools (vendored
|
|
672
|
+
* `getToolsForMode(modeConfig.groups)` — group tools + ALWAYS_AVAILABLE_TOOLS)
|
|
673
|
+
* with the tools the executor actually registers
|
|
674
|
+
* (`EXECUTABLE_TOOL_NAMES`). Stub-only tools (apply_diff, search_files, …) are
|
|
675
|
+
* NOT advertised to the model, so it doesn't waste turns calling unimplemented
|
|
676
|
+
* tools; they remain registered in the executor purely as a safety net.
|
|
677
|
+
*
|
|
678
|
+
* NOTE (browser_action): this tool is NOT part of the vendored tool set, so
|
|
679
|
+
* it can't be selected here — callers append it explicitly via
|
|
680
|
+
* appendBrowserActionTool (see below).
|
|
681
|
+
*/
|
|
682
|
+
export function selectToolsForMode(mode: string, customModes: ModeConfig[] = []): ChatTool[] {
|
|
683
|
+
const modeConfig = getModeBySlug(mode, customModes) ?? modes.find((m) => m.slug === mode) ?? modes[0]
|
|
684
|
+
const allowedNames = new Set(getToolsForMode(modeConfig.groups))
|
|
685
|
+
|
|
686
|
+
// The vendored `getToolsForMode` only includes a group's standard `tools`,
|
|
687
|
+
// not its opt-in `customTools` (e.g. `search_replace` / `edit_file` in the
|
|
688
|
+
// `edit` group — in VS Code they're enabled via a settings checkbox).
|
|
689
|
+
// Headless has no UI to opt in, so we surface a group's customTools too —
|
|
690
|
+
// BUT only those the executor actually implements (the EXECUTABLE_TOOL_NAMES
|
|
691
|
+
// intersection below is still the final gate), so a stub like `edit` or
|
|
692
|
+
// `apply_patch` never leaks to the model.
|
|
693
|
+
for (const group of modeConfig.groups) {
|
|
694
|
+
const groupName = getGroupName(group)
|
|
695
|
+
const customTools = TOOL_GROUPS[groupName]?.customTools ?? []
|
|
696
|
+
for (const customName of customTools) {
|
|
697
|
+
allowedNames.add(customName)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const nativeTools = getNativeTools()
|
|
702
|
+
const selected: ChatTool[] = []
|
|
703
|
+
|
|
704
|
+
for (const tool of nativeTools) {
|
|
705
|
+
if (tool.type !== "function") {
|
|
706
|
+
continue
|
|
707
|
+
}
|
|
708
|
+
const name = tool.function.name
|
|
709
|
+
if (allowedNames.has(name) && EXECUTABLE_TOOL_NAMES.has(name)) {
|
|
710
|
+
selected.push(tool as unknown as ChatTool)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return selected
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Append the `browser_action` tool schema to a tool list if it isn't already
|
|
719
|
+
* present. `browser_action` is NOT a vendored Zoo Code tool (it's new work —
|
|
720
|
+
* see src/tools/browser/tool.ts), so it never appears in getNativeTools() and
|
|
721
|
+
* selectToolsForMode() can't pick it up; callers that want the model to be
|
|
722
|
+
* able to launch a browser (headless + QA sessions) append it explicitly.
|
|
723
|
+
* Appending rather than replacing keeps the mode's own tool filtering intact.
|
|
724
|
+
*/
|
|
725
|
+
export function appendBrowserActionTool(tools: ChatTool[]): ChatTool[] {
|
|
726
|
+
if (tools.some((t) => t.type === "function" && t.function.name === browserActionTool.function.name)) {
|
|
727
|
+
return tools
|
|
728
|
+
}
|
|
729
|
+
return [...tools, browserActionTool as unknown as ChatTool]
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Append the describe_image tool schema (cloud vision captioning — see
|
|
734
|
+
* src/vision/) to a tool list if it isn't already present. Like
|
|
735
|
+
* browser_action, it is a NEW tool with no vendored Zoo Code upstream, so
|
|
736
|
+
* selectToolsForMode can't pick it up from getNativeTools(); callers that
|
|
737
|
+
* want the model to inspect images append it explicitly. Only wired where a
|
|
738
|
+
* real accounting session exists (onAuxLlmUsage) — read-only reviewer/QA
|
|
739
|
+
* executors deliberately don't advertise it (their executors don't register
|
|
740
|
+
* the handler, and calls there would be untracked spend).
|
|
741
|
+
*/
|
|
742
|
+
export function appendDescribeImageTool(tools: ChatTool[]): ChatTool[] {
|
|
743
|
+
if (tools.some((t) => t.type === "function" && t.function.name === describeImageTool.function.name)) {
|
|
744
|
+
return tools
|
|
745
|
+
}
|
|
746
|
+
return [...tools, describeImageTool as unknown as ChatTool]
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Append the four code-intelligence tool schemas (outline, go_to_definition,
|
|
751
|
+
* find_references, import_graph) to a tool list if they aren't already
|
|
752
|
+
* present. Like browser_action, these are NEW tools with no vendored Zoo Code
|
|
753
|
+
* upstream (see src/codeintel/tools.ts), so selectToolsForMode can't pick
|
|
754
|
+
* them up from getNativeTools(); callers that want the model to navigate
|
|
755
|
+
* source code append them explicitly. Read-only, so they are appended for
|
|
756
|
+
* every mode including the reviewer/QA executors.
|
|
757
|
+
*/
|
|
758
|
+
export function appendCodeIntelTools(tools: ChatTool[]): ChatTool[] {
|
|
759
|
+
const present = new Set(
|
|
760
|
+
tools.filter((t) => t.type === "function").map((t) => (t.function as { name: string }).name),
|
|
761
|
+
)
|
|
762
|
+
const missing = CODE_INTEL_TOOLS.filter((t) => t.type === "function" && !present.has(t.function.name))
|
|
763
|
+
if (missing.length === 0) {
|
|
764
|
+
return tools
|
|
765
|
+
}
|
|
766
|
+
return [...tools, ...(missing as unknown as ChatTool[])]
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Append the EDIT-capable code-intelligence tool schema (`rename_symbol`) to
|
|
771
|
+
* a tool list if it isn't already present. Unlike appendCodeIntelTools (the
|
|
772
|
+
* four read-only tools, safe for every mode), rename_symbol WRITES files, so
|
|
773
|
+
* callers must gate this on the executor actually registering it (the
|
|
774
|
+
* headless executor does; the read-only reviewer/QA executors do not — see
|
|
775
|
+
* src/tools/executor.ts) — otherwise a read-only session would advertise a
|
|
776
|
+
* tool whose handler is missing.
|
|
777
|
+
*/
|
|
778
|
+
export function appendCodeIntelEditTools(tools: ChatTool[]): ChatTool[] {
|
|
779
|
+
if (tools.some((t) => t.type === "function" && t.function.name === RENAME_SYMBOL_TOOL.function.name)) {
|
|
780
|
+
return tools
|
|
781
|
+
}
|
|
782
|
+
return [...tools, RENAME_SYMBOL_TOOL as unknown as ChatTool]
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Append the `run_tests` tool schema if it isn't already present. Like
|
|
787
|
+
* rename_symbol, run_tests RUNS the test suite (side-effectful) and is only
|
|
788
|
+
* meaningful to edit-capable sessions, so callers gate this on the executor
|
|
789
|
+
* actually registering it (the headless executor does; read-only reviewer/QA
|
|
790
|
+
* executors do not — see src/tools/executor.ts).
|
|
791
|
+
*/
|
|
792
|
+
export function appendRunTestsTool(tools: ChatTool[]): ChatTool[] {
|
|
793
|
+
if (tools.some((t) => t.type === "function" && t.function.name === runTestsTool.function.name)) {
|
|
794
|
+
return tools
|
|
795
|
+
}
|
|
796
|
+
return [...tools, runTestsTool as unknown as ChatTool]
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Append the `set_indentation` tool schema if it isn't already present
|
|
801
|
+
* (issue #141). Only meaningful to edit-capable sessions — gated on the
|
|
802
|
+
* executor actually registering it, same as run_tests above (the headless
|
|
803
|
+
* executor does; read-only reviewer/QA executors do not).
|
|
804
|
+
*/
|
|
805
|
+
export function appendSetIndentationTool(tools: ChatTool[]): ChatTool[] {
|
|
806
|
+
if (tools.some((t) => t.type === "function" && t.function.name === setIndentationTool.function.name)) {
|
|
807
|
+
return tools
|
|
808
|
+
}
|
|
809
|
+
return [...tools, setIndentationTool as unknown as ChatTool]
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Patch `edit_file`'s schema for local GGUF sessions: move
|
|
814
|
+
* `expected_replacements` from optional into `required` (the model must
|
|
815
|
+
* always supply it, e.g. 1 — the executor already defaults to 1 when it's
|
|
816
|
+
* absent, so behavior is unchanged either way — see
|
|
817
|
+
* src/tools/executor.ts's `toNonNegativeInt(args.expected_replacements, 1)`).
|
|
818
|
+
*
|
|
819
|
+
* Root cause this works around: llama.cpp/llama-cpp-python constrain tool
|
|
820
|
+
* calls by converting each tool's JSON schema to a GBNF grammar. A known bug
|
|
821
|
+
* in that conversion corrupts structured decoding whenever a multi-parameter
|
|
822
|
+
* tool has ANY optional parameter — the model omits/duplicates/blanks a
|
|
823
|
+
* field instead of completing the call (ggml-org/llama.cpp#20164, confirmed
|
|
824
|
+
* against Qwen3.5/Qwen3-Coder; the reporter's own fix was moving the
|
|
825
|
+
* optional param to required). Verified live 2026-08-20 against
|
|
826
|
+
* Qwen2.5-Coder-14B: `edit_file` (3 required + 1 optional param) failed 3/3
|
|
827
|
+
* calls on an existing file in three different malformed shapes (empty
|
|
828
|
+
* old_string, identical old_string/new_string, missing file_path), while
|
|
829
|
+
* `write_to_file` (2 required params, zero optional) succeeded on the first
|
|
830
|
+
* try every time in the same session — exactly the schema-shape signature
|
|
831
|
+
* from the upstream bug report, not a reasoning failure.
|
|
832
|
+
*
|
|
833
|
+
* Cloud sessions are untouched (OpenRouter models aren't grammar-constrained
|
|
834
|
+
* this way), so this is applied only for the local backend, never to the
|
|
835
|
+
* vendored schema itself.
|
|
836
|
+
*/
|
|
837
|
+
export function patchEditFileToolForLocalModels(tools: ChatTool[]): ChatTool[] {
|
|
838
|
+
return tools.map((tool) => {
|
|
839
|
+
if (tool.type !== "function" || tool.function.name !== "edit_file") {
|
|
840
|
+
return tool
|
|
841
|
+
}
|
|
842
|
+
const params = tool.function.parameters as {
|
|
843
|
+
required?: string[]
|
|
844
|
+
[key: string]: unknown
|
|
845
|
+
}
|
|
846
|
+
if (params.required?.includes("expected_replacements")) {
|
|
847
|
+
return tool
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
...tool,
|
|
851
|
+
function: {
|
|
852
|
+
...tool.function,
|
|
853
|
+
parameters: {
|
|
854
|
+
...params,
|
|
855
|
+
required: [...(params.required ?? []), "expected_replacements"],
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
}
|
|
859
|
+
})
|
|
860
|
+
}
|