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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_tests` — run the SPECIFIC test file(s) relevant to a set of changed
|
|
3
|
+
* files, instead of always paying the full `npm test` cost (~70 files,
|
|
4
|
+
* measured 111s live) during the iterative edit-check-edit loop.
|
|
5
|
+
*
|
|
6
|
+
* Selection is the heuristic in src/tools/test-selection.ts (direct
|
|
7
|
+
* `src/foo/bar.ts` → `src/foo/__tests__/bar.test.ts` match, then reverse
|
|
8
|
+
* dependency via the REAL import graph — the same resolution the
|
|
9
|
+
* `import_graph` tool uses), and the matched files run through the SAME
|
|
10
|
+
* `tsx` invocation `npm test` uses per-file, sequentially, stopping at the
|
|
11
|
+
* first failure exactly like `npm test`'s `&&` chain.
|
|
12
|
+
*
|
|
13
|
+
* SECURITY: the selected test paths come from an untrusted workspace (they
|
|
14
|
+
* are file names that can contain shell metacharacters), so they are passed
|
|
15
|
+
* to `spawn` as ARGV with `shell: false` — never interpolated into a shell
|
|
16
|
+
* command string. See issue #62 (the old `shellQuote` allowed command
|
|
17
|
+
* substitution via `$()`/backticks in a hostile test file name).
|
|
18
|
+
*
|
|
19
|
+
* IMPORTANT (this is the tool's whole point, stated to the model): a
|
|
20
|
+
* passing selective run is NOT full-suite green. The FULL `npm test` is
|
|
21
|
+
* still what gates a merge/PR and is still required once before
|
|
22
|
+
* `attempt_completion`. This tool exists to make the per-edit loop cheap,
|
|
23
|
+
* not to replace the final confidence run.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync, spawn } from "node:child_process"
|
|
27
|
+
import type OpenAI from "openai"
|
|
28
|
+
|
|
29
|
+
import type { ToolContext, ToolResult } from "../engine/types.js"
|
|
30
|
+
import { getCodeIntelCache } from "../codeintel/program.js"
|
|
31
|
+
import { selectTestsForChangedFiles } from "./test-selection.js"
|
|
32
|
+
|
|
33
|
+
export const RUN_TESTS_NAME = "run_tests"
|
|
34
|
+
|
|
35
|
+
/** Default per-test-file timeout, seconds (a single tsx-invoked suite). */
|
|
36
|
+
const DEFAULT_TEST_TIMEOUT_S = 180
|
|
37
|
+
|
|
38
|
+
const RUN_TESTS_DESCRIPTION = `Run the SPECIFIC test file(s) relevant to one or more changed files, instead of always paying the full npm test cost during the iterative edit-check-edit loop. Selection is automatic: for each changed file it looks for a direct test (src/foo/bar.ts → src/foo/__tests__/bar.test.ts), then for test files that import the changed file (via the real import graph, 1-2 hops). Matched files run through the same tsx invocation npm test uses per-file, sequentially, stopping at the first failure.
|
|
39
|
+
|
|
40
|
+
Use this after editing a file to quickly verify you didn't break its tests — NOT as a replacement for the full suite. A passing selective run does NOT prove the whole project is green: run the full npm test once right before attempt_completion.
|
|
41
|
+
|
|
42
|
+
When no specific test matches (e.g. you changed package.json or a script), the tool reports "no specific tests matched — consider running the full suite" honestly rather than silently running nothing.
|
|
43
|
+
|
|
44
|
+
Parameters:
|
|
45
|
+
- paths: (optional) The changed file path(s), relative to the workspace root. Omit to infer from the session's baseline checkpoint (or the workspace's git status when no checkpoint service is active).
|
|
46
|
+
- timeout: (optional) Per-test-file timeout in seconds (default 180).
|
|
47
|
+
|
|
48
|
+
Example: { "paths": ["src/tools/executor.ts"], "timeout": 120 }`
|
|
49
|
+
|
|
50
|
+
const RT_PATHS_PARAMETER_DESCRIPTION = `Changed file path(s), relative to the workspace root. Omit to infer from the session's baseline checkpoint (or the workspace's git status).`
|
|
51
|
+
const RT_TIMEOUT_PARAMETER_DESCRIPTION = `Per-test-file timeout in seconds (default 180).`
|
|
52
|
+
|
|
53
|
+
export const runTestsTool = {
|
|
54
|
+
type: "function",
|
|
55
|
+
function: {
|
|
56
|
+
name: RUN_TESTS_NAME,
|
|
57
|
+
description: RUN_TESTS_DESCRIPTION,
|
|
58
|
+
// Note: strict mode is intentionally disabled for this tool (mirrors
|
|
59
|
+
// read_command_output.ts's precedent). With strict: true, every
|
|
60
|
+
// property must be in `required`, which forces nullable-union types
|
|
61
|
+
// (`type: ["array", "null"]`) for genuinely optional params so the
|
|
62
|
+
// model can omit them — DeepSeek's OpenRouter endpoint rejects that
|
|
63
|
+
// union-type syntax outright ("unknown variant `array`, expected one
|
|
64
|
+
// of `string`, `number`, `integer`, `boolean`, `null`"), breaking
|
|
65
|
+
// EVERY session's very first request since the tool list itself is
|
|
66
|
+
// sent up front. Plain, non-strict optional properties avoid this
|
|
67
|
+
// entirely and match how ask_followup_question's `follow_up`
|
|
68
|
+
// (required) array param is already declared elsewhere.
|
|
69
|
+
parameters: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
paths: {
|
|
73
|
+
type: "array",
|
|
74
|
+
items: { type: "string" },
|
|
75
|
+
description: RT_PATHS_PARAMETER_DESCRIPTION,
|
|
76
|
+
},
|
|
77
|
+
timeout: {
|
|
78
|
+
type: "integer",
|
|
79
|
+
description: RT_TIMEOUT_PARAMETER_DESCRIPTION,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
} satisfies OpenAI.Chat.ChatCompletionTool
|
|
86
|
+
|
|
87
|
+
/** Infer changed files from the workspace's own git status (fallback). */
|
|
88
|
+
function changedFilesFromGit(workspaceRoot: string): string[] {
|
|
89
|
+
try {
|
|
90
|
+
const out = execFileSync("git", ["status", "--short", "--untracked-files=all"], {
|
|
91
|
+
cwd: workspaceRoot,
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
timeout: 15_000,
|
|
94
|
+
})
|
|
95
|
+
const files: string[] = []
|
|
96
|
+
for (const line of out.split("\n")) {
|
|
97
|
+
const trimmed = line.trim()
|
|
98
|
+
if (trimmed === "") {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
// Format: "<XY> <path>" (2 status chars + space); renames are
|
|
102
|
+
// "R old -> new" — the current path is the rename target.
|
|
103
|
+
const m = trimmed.match(/^.{1,2}\s+(.+)$/)
|
|
104
|
+
if (m === null) {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
let p = m[1]!.trim()
|
|
108
|
+
const arrow = p.indexOf(" -> ")
|
|
109
|
+
if (arrow !== -1) {
|
|
110
|
+
p = p.slice(arrow + 4)
|
|
111
|
+
}
|
|
112
|
+
files.push(p)
|
|
113
|
+
}
|
|
114
|
+
return files
|
|
115
|
+
} catch {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ok(content: string): ToolResult {
|
|
121
|
+
return { content, isError: false }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function err(content: string): ToolResult {
|
|
125
|
+
return { content: `[Error] ${content}`, isError: true }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Cap on the combined test output fed back to the model. */
|
|
129
|
+
const MAX_TEST_OUTPUT_CHARS = 30_000
|
|
130
|
+
|
|
131
|
+
function truncateOutput(text: string): { text: string; truncated: boolean } {
|
|
132
|
+
if (text.length <= MAX_TEST_OUTPUT_CHARS) {
|
|
133
|
+
return { text, truncated: false }
|
|
134
|
+
}
|
|
135
|
+
return { text: `${text.slice(0, MAX_TEST_OUTPUT_CHARS)}\n…[output truncated]`, truncated: true }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface OneFileResult {
|
|
139
|
+
rel: string
|
|
140
|
+
exitCode: number | null
|
|
141
|
+
output: string
|
|
142
|
+
timedOut: boolean
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run one test file via `npx --no-install tsx <file>` (the npm-test
|
|
147
|
+
* invocation). The test path is treated as UNTRUSTED input (it comes from a
|
|
148
|
+
* workspace's file names): it is passed as a separate argv element with
|
|
149
|
+
* `shell: false`, so no shell metacharacter in it — `$()`, backticks, `;`,
|
|
150
|
+
* `&`, spaces, quotes — can ever be interpreted (issue #62). `npx
|
|
151
|
+
* --no-install` avoids any network fetch if the local tsx is missing.
|
|
152
|
+
*/
|
|
153
|
+
export function runOneTestFile(workspaceRoot: string, rel: string, timeoutS: number): Promise<OneFileResult> {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
let stdout = ""
|
|
156
|
+
let stderr = ""
|
|
157
|
+
let settled = false
|
|
158
|
+
let timedOut = false
|
|
159
|
+
|
|
160
|
+
const finish = (exitCode: number | null): void => {
|
|
161
|
+
if (!settled) {
|
|
162
|
+
settled = true
|
|
163
|
+
resolve({ rel, exitCode, output: `${stdout}${stderr}`, timedOut })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let child
|
|
168
|
+
try {
|
|
169
|
+
child = spawn("npx", ["--no-install", "tsx", rel], {
|
|
170
|
+
cwd: workspaceRoot,
|
|
171
|
+
// `shell: false` (default) — argv is passed verbatim, never
|
|
172
|
+
// re-parsed by a shell. This is the actual security boundary.
|
|
173
|
+
shell: false,
|
|
174
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
175
|
+
env: { ...process.env, LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8" },
|
|
176
|
+
})
|
|
177
|
+
} catch (error) {
|
|
178
|
+
resolve({ rel, exitCode: null, output: `spawn error: ${error instanceof Error ? error.message : String(error)}`, timedOut: false })
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
timedOut = true
|
|
184
|
+
child.kill("SIGKILL")
|
|
185
|
+
}, timeoutS * 1000)
|
|
186
|
+
|
|
187
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
188
|
+
stdout += chunk.toString()
|
|
189
|
+
})
|
|
190
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
191
|
+
stderr += chunk.toString()
|
|
192
|
+
})
|
|
193
|
+
child.on("error", (error) => {
|
|
194
|
+
clearTimeout(timer)
|
|
195
|
+
stdout += `spawn error: ${error.message}\n`
|
|
196
|
+
finish(null)
|
|
197
|
+
})
|
|
198
|
+
child.on("close", (code) => {
|
|
199
|
+
clearTimeout(timer)
|
|
200
|
+
finish(code)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Run the `run_tests` tool. `getSessionChangedFiles` is the session-provided
|
|
207
|
+
* inference hook (checkpoint-diff based — see HeadlessSession); when absent
|
|
208
|
+
* or returning undefined the handler falls back to the workspace git status.
|
|
209
|
+
*/
|
|
210
|
+
export async function runTestsHandler(
|
|
211
|
+
args: Record<string, unknown>,
|
|
212
|
+
ctx: ToolContext,
|
|
213
|
+
getSessionChangedFiles?: () => Promise<string[] | undefined>,
|
|
214
|
+
): Promise<ToolResult> {
|
|
215
|
+
const rawPaths = args.paths
|
|
216
|
+
const timeoutS =
|
|
217
|
+
typeof args.timeout === "number" && args.timeout > 0
|
|
218
|
+
? args.timeout
|
|
219
|
+
: typeof args.timeout === "string" && Number(args.timeout) > 0
|
|
220
|
+
? Number(args.timeout)
|
|
221
|
+
: DEFAULT_TEST_TIMEOUT_S
|
|
222
|
+
|
|
223
|
+
let changedFiles: string[] | undefined
|
|
224
|
+
if (Array.isArray(rawPaths) && rawPaths.length > 0) {
|
|
225
|
+
changedFiles = rawPaths
|
|
226
|
+
.filter((p): p is string => typeof p === "string" && p.trim() !== "")
|
|
227
|
+
.map((p) => p.trim())
|
|
228
|
+
if (changedFiles.length === 0) {
|
|
229
|
+
return err("run_tests: 'paths' must be a non-empty array of file path strings")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (changedFiles === undefined) {
|
|
234
|
+
// Session-baseline inference first (checkpoint diff), then git status.
|
|
235
|
+
try {
|
|
236
|
+
changedFiles = (await getSessionChangedFiles?.()) ?? changedFilesFromGit(ctx.workspaceRoot)
|
|
237
|
+
} catch {
|
|
238
|
+
changedFiles = changedFilesFromGit(ctx.workspaceRoot)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (changedFiles === undefined || changedFiles.length === 0) {
|
|
242
|
+
return err(
|
|
243
|
+
"run_tests: no changed files could be determined (no 'paths' given, no session baseline, and the workspace has no git status to read). Pass explicit 'paths', or run the full suite (npm test).",
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const intel = getCodeIntelCache(ctx.workspaceRoot).get()
|
|
248
|
+
const { tests, notes } = selectTestsForChangedFiles(intel, ctx.workspaceRoot, changedFiles)
|
|
249
|
+
|
|
250
|
+
if (tests.length === 0) {
|
|
251
|
+
// Fail-safe: never report a silent pass of zero tests.
|
|
252
|
+
return err(
|
|
253
|
+
`run_tests: no specific tests matched for the changed file(s). ${notes.join("; ")} — consider running the full suite (npm test) instead of treating this as a pass.`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const header = `run_tests: ${tests.length} test file(s) matched for ${changedFiles.length} changed file(s):\n${tests
|
|
258
|
+
.map((t) => ` ${t}`)
|
|
259
|
+
.join("\n")}`
|
|
260
|
+
const noteLine = notes.length > 0 ? `Selection notes:\n${notes.map((n) => ` - ${n}`).join("\n")}` : ""
|
|
261
|
+
|
|
262
|
+
// Run each file sequentially, stopping at the first failure (mirrors
|
|
263
|
+
// `npm test`'s `&&` chain). Failures and passes are both reported with
|
|
264
|
+
// their real output — the model must not mistake a selective pass for a
|
|
265
|
+
// full-suite green (the trailer says so explicitly).
|
|
266
|
+
const blocks: string[] = [header, noteLine, ""]
|
|
267
|
+
for (const rel of tests) {
|
|
268
|
+
// Defense-in-depth (the argv spawn in runOneTestFile is the primary
|
|
269
|
+
// boundary): reject paths containing newlines outright — they cannot
|
|
270
|
+
// be legitimate test files, and they would let a workspace smuggle
|
|
271
|
+
// forged "test result" content (a fake pass/fail line) into the tool
|
|
272
|
+
// result. Nothing is run for such a path.
|
|
273
|
+
if (/\r?\n/.test(rel)) {
|
|
274
|
+
blocks.push(`── ${rel} ─ (skipped: path contains a newline — not a legitimate test file)`)
|
|
275
|
+
blocks.push("[run_tests] FAILED: test path contains a newline (potential output-injection attempt)")
|
|
276
|
+
return err(blocks.join("\n"))
|
|
277
|
+
}
|
|
278
|
+
const result = await runOneTestFile(ctx.workspaceRoot, rel, timeoutS)
|
|
279
|
+
const { text: output, truncated } = truncateOutput(result.output)
|
|
280
|
+
blocks.push(`── ${rel} ─${truncated ? " (output truncated)" : ""}`)
|
|
281
|
+
blocks.push(output.trim() === "" ? "(no output)" : output)
|
|
282
|
+
if (result.timedOut) {
|
|
283
|
+
blocks.push(`[run_tests] ${rel} timed out after ${timeoutS}s (killed)`)
|
|
284
|
+
return err(blocks.join("\n"))
|
|
285
|
+
}
|
|
286
|
+
if (result.exitCode !== 0) {
|
|
287
|
+
blocks.push(`[run_tests] FAILED at ${rel} (exit ${result.exitCode ?? "spawn error"}) — remaining files not run (same as npm test's && chain)`)
|
|
288
|
+
return err(blocks.join("\n"))
|
|
289
|
+
}
|
|
290
|
+
blocks.push(`[run_tests] ${rel} exited 0`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
blocks.push(
|
|
294
|
+
"",
|
|
295
|
+
"[run_tests] all matched test file(s) passed.",
|
|
296
|
+
"NOTE: a passing selective run is NOT full-suite green — run the full `npm test` once before attempt_completion.",
|
|
297
|
+
)
|
|
298
|
+
return ok(blocks.join("\n"))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Exported for tests: the pure git-status fallback inference. */
|
|
302
|
+
export { changedFilesFromGit }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `set_indentation` schema (issue #141) — sets ONE line's leading
|
|
3
|
+
* indentation to an exact tab count. The handler lives in executor.ts
|
|
4
|
+
* (setIndentationHandler) alongside edit_file/write_to_file, since it needs
|
|
5
|
+
* the same private path-safety/protected-file helpers those tools already
|
|
6
|
+
* use; this file only holds the schema, mirroring run-tests.ts's split.
|
|
7
|
+
*
|
|
8
|
+
* All 3 parameters are required — deliberately, unlike run_tests's optional
|
|
9
|
+
* paths/timeout: a tool with any optional parameter hits a known
|
|
10
|
+
* llama.cpp/llama-cpp-python grammar-constrained-decoding bug that corrupts
|
|
11
|
+
* structured tool calls (see prompt.ts's patchEditFileToolForLocalModels
|
|
12
|
+
* doc comment). Keeping every parameter required sidesteps that bug
|
|
13
|
+
* category entirely, on top of this tool's main point — see executor.ts's
|
|
14
|
+
* setIndentationHandler doc comment for the real motivation (a local model
|
|
15
|
+
* struggling to compose two multi-line strings differing only in tab
|
|
16
|
+
* count, not a matching-strictness problem).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type OpenAI from "openai"
|
|
20
|
+
|
|
21
|
+
export const SET_INDENTATION_NAME = "set_indentation"
|
|
22
|
+
|
|
23
|
+
const SET_INDENTATION_DESCRIPTION = `Change ONE line's leading indentation to an exact number of tabs, without touching the rest of the line or any other line. Use this for a pure indentation/whitespace-only fix instead of edit_file — edit_file requires typing out the full line twice (once in old_string, once in new_string) differing only in leading whitespace, which is easy to get subtly wrong. This tool takes a plain line number and a plain tab count instead.
|
|
24
|
+
|
|
25
|
+
Only fixes indentation made of TABS. If the file uses space-based indentation, use edit_file instead.
|
|
26
|
+
|
|
27
|
+
Example: { "path": "src/foo.ts", "line": 42, "tabs": 3 } sets line 42's leading whitespace to exactly 3 tabs (\\t\\t\\t), replacing however many tabs/spaces were there before.`
|
|
28
|
+
|
|
29
|
+
const SI_PATH_PARAMETER_DESCRIPTION = `File path, relative to the workspace root.`
|
|
30
|
+
const SI_LINE_PARAMETER_DESCRIPTION = `1-indexed line number to change. Use read_file first to confirm it.`
|
|
31
|
+
const SI_TABS_PARAMETER_DESCRIPTION = `Exact number of leading tab characters the line should have after this call (0 removes all leading indentation).`
|
|
32
|
+
|
|
33
|
+
export const setIndentationTool = {
|
|
34
|
+
type: "function",
|
|
35
|
+
function: {
|
|
36
|
+
name: SET_INDENTATION_NAME,
|
|
37
|
+
description: SET_INDENTATION_DESCRIPTION,
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
path: { type: "string", description: SI_PATH_PARAMETER_DESCRIPTION },
|
|
42
|
+
line: { type: "integer", description: SI_LINE_PARAMETER_DESCRIPTION },
|
|
43
|
+
tabs: { type: "integer", description: SI_TABS_PARAMETER_DESCRIPTION },
|
|
44
|
+
},
|
|
45
|
+
required: ["path", "line", "tabs"],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} satisfies OpenAI.Chat.ChatCompletionTool
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-selection heuristic for the `run_tests` tool.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately simple — the goal is "much better than always running the
|
|
5
|
+
* full suite during the iterative edit-check-edit loop", not a perfect
|
|
6
|
+
* solver:
|
|
7
|
+
*
|
|
8
|
+
* 1. Direct match: `src/foo/bar.ts` changed → `src/foo/__tests__/bar.test.ts`
|
|
9
|
+
* if it exists (this repo's test-file convention). A changed file that
|
|
10
|
+
* IS itself a test runs itself.
|
|
11
|
+
* 2. Reverse-dependency match: reuse the REAL import graph
|
|
12
|
+
* (src/codeintel/import-graph.ts — the same resolution the `import_graph`
|
|
13
|
+
* tool uses, correct on `paths` aliases and extension resolution) to find
|
|
14
|
+
* which files import the changed file — directly (1 hop) or through one
|
|
15
|
+
* intermediate module (2 hops, the common "lib → helper → test" case).
|
|
16
|
+
* Any importer that is a test file is selected.
|
|
17
|
+
* 3. No match → the caller reports "no specific tests matched — consider
|
|
18
|
+
* running the full suite" rather than silently running nothing.
|
|
19
|
+
*
|
|
20
|
+
* A source file's own direct test wins over reverse-dependency matches for
|
|
21
|
+
* determinism; everything is deduped and sorted for stable output.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from "node:fs"
|
|
25
|
+
import * as path from "node:path"
|
|
26
|
+
|
|
27
|
+
import { getImportGraph } from "../codeintel/import-graph.js"
|
|
28
|
+
import type { CodeIntel } from "../codeintel/program.js"
|
|
29
|
+
|
|
30
|
+
/** True for `foo.test.ts` / `foo.spec.tsx` style names, or any file inside a `__tests__` dir. */
|
|
31
|
+
export function isTestFile(relPath: string): boolean {
|
|
32
|
+
const normalized = relPath.split(path.sep).join("/")
|
|
33
|
+
if (/(^|\/)__tests__(\/|$)/.test(normalized)) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
return /[^/]+\.(test|spec)\.(m|c)?[tj]sx?$/i.test(normalized)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Direct match for a changed source file: `src/foo/bar.ts` →
|
|
41
|
+
* `src/foo/__tests__/bar.test.ts`. Only applies to source extensions the
|
|
42
|
+
* project can test (TS/JS); returns undefined for everything else (scripts,
|
|
43
|
+
* config, docs — those have no per-file test to run directly).
|
|
44
|
+
*/
|
|
45
|
+
export function directTestMatch(relSource: string): string | undefined {
|
|
46
|
+
const normalized = relSource.split(path.sep).join("/")
|
|
47
|
+
if (!/\.(m|c)?[tj]sx?$/i.test(normalized)) {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
const dir = path.posix.dirname(normalized)
|
|
51
|
+
const base = path.posix.basename(normalized).replace(/\.(m|c)?[tj]sx?$/i, "")
|
|
52
|
+
// Never map a test file onto a sibling test (it already matched itself).
|
|
53
|
+
if (isTestFile(normalized)) {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
return `${dir}/__tests__/${base}.test.ts`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reverse-dependency match: every TEST file that (transitively, up to
|
|
61
|
+
* `maxDepth` hops) imports the changed file, per the real import graph.
|
|
62
|
+
* `absTarget` is the changed file's absolute path.
|
|
63
|
+
*/
|
|
64
|
+
export function reverseDependencyTests(
|
|
65
|
+
intel: CodeIntel,
|
|
66
|
+
workspaceRoot: string,
|
|
67
|
+
absTarget: string,
|
|
68
|
+
maxDepth = 2,
|
|
69
|
+
): string[] {
|
|
70
|
+
const graph = getImportGraph(intel)
|
|
71
|
+
const found: string[] = []
|
|
72
|
+
const seenFiles = new Set<string>([absTarget])
|
|
73
|
+
|
|
74
|
+
// BFS from the changed file along reverse import edges (`importedBy`).
|
|
75
|
+
let frontier = [absTarget]
|
|
76
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
77
|
+
const next: string[] = []
|
|
78
|
+
for (const file of frontier) {
|
|
79
|
+
for (const importer of graph.importedBy.get(file) ?? []) {
|
|
80
|
+
if (seenFiles.has(importer)) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
seenFiles.add(importer)
|
|
84
|
+
const rel = path.relative(workspaceRoot, importer).split(path.sep).join("/")
|
|
85
|
+
if (isTestFile(rel)) {
|
|
86
|
+
found.push(rel)
|
|
87
|
+
}
|
|
88
|
+
next.push(importer)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
frontier = next
|
|
92
|
+
}
|
|
93
|
+
return found
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TestSelectionResult {
|
|
97
|
+
/** Selected test files, workspace-relative posix paths, sorted + deduped. */
|
|
98
|
+
tests: string[]
|
|
99
|
+
/** Human-readable notes on WHY each file was selected (for the tool result). */
|
|
100
|
+
notes: string[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Select the test file(s) to run for a set of changed workspace-relative
|
|
105
|
+
* files. Returns an empty `tests` list (with a note) when nothing matches —
|
|
106
|
+
* the caller must report the fail-safe "no specific tests matched" instead
|
|
107
|
+
* of pretending zero tests ran.
|
|
108
|
+
*/
|
|
109
|
+
export function selectTestsForChangedFiles(
|
|
110
|
+
intel: CodeIntel,
|
|
111
|
+
workspaceRoot: string,
|
|
112
|
+
changedFiles: string[],
|
|
113
|
+
): TestSelectionResult {
|
|
114
|
+
const tests = new Set<string>()
|
|
115
|
+
const notes: string[] = []
|
|
116
|
+
const seenNotes = new Set<string>()
|
|
117
|
+
|
|
118
|
+
const note = (text: string): void => {
|
|
119
|
+
if (!seenNotes.has(text)) {
|
|
120
|
+
seenNotes.add(text)
|
|
121
|
+
notes.push(text)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const rel of changedFiles) {
|
|
126
|
+
const normalized = rel.split(path.sep).join("/")
|
|
127
|
+
if (isTestFile(normalized)) {
|
|
128
|
+
tests.add(normalized)
|
|
129
|
+
note(`direct: ${normalized} is itself a test file`)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
const direct = directTestMatch(normalized)
|
|
133
|
+
if (direct !== undefined && fileExists(path.join(workspaceRoot, direct))) {
|
|
134
|
+
tests.add(direct)
|
|
135
|
+
note(`direct: ${normalized} changed → ${direct}`)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const abs = path.resolve(workspaceRoot, normalized)
|
|
139
|
+
const reverse = reverseDependencyTests(intel, workspaceRoot, abs)
|
|
140
|
+
if (reverse.length > 0) {
|
|
141
|
+
for (const t of reverse) {
|
|
142
|
+
tests.add(t)
|
|
143
|
+
}
|
|
144
|
+
note(`reverse-dependency: ${normalized} is imported by ${reverse.join(", ")}`)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
note(`no test matched ${normalized} (no direct test, no test imports it)`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sorted = [...tests].sort()
|
|
151
|
+
return { tests: sorted, notes }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fileExists(p: string): boolean {
|
|
155
|
+
try {
|
|
156
|
+
return fs.statSync(p).isFile()
|
|
157
|
+
} catch {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THROWAWAY SMOKE TEST — proves the vendored prompt builder can be imported
|
|
3
|
+
* and called headlessly (no VS Code), and that the native tool schemas assemble.
|
|
4
|
+
*
|
|
5
|
+
* Run with: npm run smoke (i.e. tsx src/vendor/tests/smoke.ts)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionContext } from "../zoo-code/shim/vscode.js"
|
|
9
|
+
|
|
10
|
+
// Side effect: installs String.prototype.toPosix() used by prompt sections.
|
|
11
|
+
import "../zoo-code/src/utils/path.js"
|
|
12
|
+
|
|
13
|
+
import { SYSTEM_PROMPT } from "../zoo-code/src/core/prompts/system.js"
|
|
14
|
+
import {
|
|
15
|
+
getNativeTools,
|
|
16
|
+
convertOpenAIToolsToAnthropic,
|
|
17
|
+
} from "../zoo-code/src/core/prompts/tools/native-tools/index.js"
|
|
18
|
+
import { customModesSettingsSchema } from "../zoo-code/types/index.js"
|
|
19
|
+
|
|
20
|
+
async function main(): Promise<void> {
|
|
21
|
+
// Minimal ExtensionContext stand-in (the vendored builder only reads
|
|
22
|
+
// globalState.get("customModes"/"customModePrompts") when building MODES).
|
|
23
|
+
const context = {
|
|
24
|
+
globalState: {
|
|
25
|
+
get: async () => undefined,
|
|
26
|
+
update: async () => {},
|
|
27
|
+
},
|
|
28
|
+
globalStorageUri: { fsPath: process.cwd() },
|
|
29
|
+
subscriptions: [],
|
|
30
|
+
} as unknown as ExtensionContext
|
|
31
|
+
|
|
32
|
+
// 1. Build a system prompt for the built-in "code" mode, with custom
|
|
33
|
+
// instructions text spliced in (the acceptance-criteria shape).
|
|
34
|
+
const customInstructions = "This is throwaway smoke-test global instructions text."
|
|
35
|
+
const prompt = await SYSTEM_PROMPT(
|
|
36
|
+
context,
|
|
37
|
+
process.cwd(),
|
|
38
|
+
false,
|
|
39
|
+
undefined,
|
|
40
|
+
undefined,
|
|
41
|
+
"code",
|
|
42
|
+
undefined,
|
|
43
|
+
undefined,
|
|
44
|
+
customInstructions,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
for (const needle of [
|
|
48
|
+
"TOOL USE",
|
|
49
|
+
"OBJECTIVE",
|
|
50
|
+
"RULES",
|
|
51
|
+
"MODES",
|
|
52
|
+
"CAPABILITIES",
|
|
53
|
+
"SYSTEM INFORMATION",
|
|
54
|
+
"You are Zoo, a highly skilled software engineer",
|
|
55
|
+
"USER'S CUSTOM INSTRUCTIONS",
|
|
56
|
+
`Global Instructions:\n${customInstructions}`,
|
|
57
|
+
]) {
|
|
58
|
+
if (!prompt.includes(needle)) {
|
|
59
|
+
throw new Error(`System prompt missing expected section: "${needle}"`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Assemble the native (OpenAI-format) tool schemas.
|
|
64
|
+
const tools = getNativeTools()
|
|
65
|
+
if (tools.length < 20) {
|
|
66
|
+
throw new Error(`Expected >= 20 native tools, got ${tools.length}`)
|
|
67
|
+
}
|
|
68
|
+
const names = tools.map((t) => (t.type === "function" ? t.function.name : t.type))
|
|
69
|
+
if (!names.includes("read_file") || !names.includes("execute_command") || !names.includes("write_to_file")) {
|
|
70
|
+
throw new Error(`Native tools missing core tools: ${names.join(", ")}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Convert to Anthropic format.
|
|
74
|
+
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
|
|
75
|
+
if (anthropicTools.length !== tools.length) {
|
|
76
|
+
throw new Error("Anthropic conversion count mismatch")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. The .roomodes zod schema parses a minimal customModes YAML document.
|
|
80
|
+
const parsed = customModesSettingsSchema.safeParse({
|
|
81
|
+
customModes: [
|
|
82
|
+
{
|
|
83
|
+
slug: "smoke",
|
|
84
|
+
name: "Smoke",
|
|
85
|
+
roleDefinition: "You are a smoke test mode.",
|
|
86
|
+
groups: ["read"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
throw new Error(`customModesSettingsSchema rejected a valid config: ${JSON.stringify(parsed.error)}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(
|
|
95
|
+
`[smoke] system prompt built (${prompt.length} chars); native tools: ${tools.length}; anthropic tools: ${anthropicTools.length}; roomodes schema: ok`,
|
|
96
|
+
)
|
|
97
|
+
console.log("[smoke] OK")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch((err) => {
|
|
101
|
+
console.error("[smoke] FAILED:", err)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
})
|