@gajae-code/coding-agent 0.8.2 → 0.9.1
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/CHANGELOG.md +53 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +22 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +30 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +8 -11
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +76 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/launch-tmux.ts +6 -0
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +175 -19
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +18 -9
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +13 -18
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +43 -4
- package/src/notifications/index.ts +105 -9
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-cli.ts +54 -7
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +390 -56
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +68 -30
- package/src/session/agent-session.ts +734 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +143 -1
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
+
import { prompt } from "@gajae-code/utils";
|
|
3
|
+
import * as z from "zod/v4";
|
|
4
|
+
import bisectDescription from "../prompts/tools/bisect.md" with { type: "text" };
|
|
5
|
+
import * as git from "../utils/git";
|
|
6
|
+
import type { ToolSession } from ".";
|
|
7
|
+
import type { OutputMeta } from "./output-meta";
|
|
8
|
+
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
9
|
+
import { toolResult } from "./tool-result";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_STEPS = 40;
|
|
12
|
+
const DEFAULT_STEP_TIMEOUT_MS = 10 * 60 * 1000;
|
|
13
|
+
/** git bisect run convention: a predicate exiting 125 means "cannot test this revision". */
|
|
14
|
+
const SKIP_EXIT_CODE = 125;
|
|
15
|
+
|
|
16
|
+
const bisectSchema = z.object({
|
|
17
|
+
good: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe("A known-good commit-ish (must be an ancestor of `bad`) where the predicate passes."),
|
|
21
|
+
bad: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.default("HEAD")
|
|
25
|
+
.describe("A known-bad commit-ish (defaults to HEAD) where the predicate fails."),
|
|
26
|
+
run: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.describe("Shell command evaluated at each revision. Exit 0 = good, 125 = skip, any other non-zero = bad."),
|
|
30
|
+
invert: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.default(false)
|
|
33
|
+
.describe("Find the commit that FIXED the behavior instead of the one that broke it (exit 0 is treated as bad)."),
|
|
34
|
+
maxSteps: z
|
|
35
|
+
.number()
|
|
36
|
+
.int()
|
|
37
|
+
.positive()
|
|
38
|
+
.max(1000)
|
|
39
|
+
.default(DEFAULT_MAX_STEPS)
|
|
40
|
+
.describe("Maximum bisection steps before giving up."),
|
|
41
|
+
stepTimeoutMs: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.positive()
|
|
45
|
+
.default(DEFAULT_STEP_TIMEOUT_MS)
|
|
46
|
+
.describe("Per-step timeout in milliseconds; a timed-out step is treated as a skip."),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
type BisectParams = z.infer<typeof bisectSchema>;
|
|
50
|
+
|
|
51
|
+
export type BisectVerdict = "good" | "bad" | "skip";
|
|
52
|
+
|
|
53
|
+
export interface BisectStep {
|
|
54
|
+
rev: string;
|
|
55
|
+
verdict: BisectVerdict;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface BisectMarkResult {
|
|
59
|
+
exitCode: number;
|
|
60
|
+
output: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BisectControllerDeps {
|
|
64
|
+
maxSteps: number;
|
|
65
|
+
/** SHA of the revision git currently has checked out. */
|
|
66
|
+
currentRev: () => Promise<string>;
|
|
67
|
+
/** Evaluate the predicate at the checked-out revision. */
|
|
68
|
+
evaluate: (rev: string) => Promise<BisectVerdict>;
|
|
69
|
+
/** Apply the verdict via `git bisect good|bad|skip` and return the raw result. */
|
|
70
|
+
mark: (verdict: BisectVerdict) => Promise<BisectMarkResult>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BisectOutcome {
|
|
74
|
+
culprit: string | null;
|
|
75
|
+
steps: BisectStep[];
|
|
76
|
+
concluded: boolean;
|
|
77
|
+
reason?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BisectToolDetails {
|
|
81
|
+
meta?: OutputMeta;
|
|
82
|
+
culprit: string | null;
|
|
83
|
+
invert: boolean;
|
|
84
|
+
concluded: boolean;
|
|
85
|
+
steps: BisectStep[];
|
|
86
|
+
author?: string;
|
|
87
|
+
date?: string;
|
|
88
|
+
subject?: string;
|
|
89
|
+
reason?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const FIRST_BAD_RE = /^([0-9a-f]{7,40}) is the first bad commit$/m;
|
|
93
|
+
const ONLY_SKIPPED_RE = /only '?skip'?ped commits left to test/i;
|
|
94
|
+
|
|
95
|
+
/** Parse the culprit SHA from `git bisect good|bad` output, or null when not yet converged. */
|
|
96
|
+
export function parseFirstBadCommit(output: string): string | null {
|
|
97
|
+
const match = FIRST_BAD_RE.exec(output);
|
|
98
|
+
return match ? match[1]! : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Map a predicate exit code to a bisect verdict. `invert` swaps the good/bad
|
|
103
|
+
* mapping (to hunt for a fixing commit) but never reinterprets a skip.
|
|
104
|
+
*/
|
|
105
|
+
export function classifyExit(exitCode: number, invert: boolean): BisectVerdict {
|
|
106
|
+
if (exitCode === SKIP_EXIT_CODE) return "skip";
|
|
107
|
+
const verdict: BisectVerdict = exitCode === 0 ? "good" : "bad";
|
|
108
|
+
if (!invert) return verdict;
|
|
109
|
+
return verdict === "good" ? "bad" : "good";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Drive the bisect loop. Pure orchestration — every git/predicate effect is
|
|
114
|
+
* injected — so it can be exercised deterministically without a real repo.
|
|
115
|
+
*/
|
|
116
|
+
export async function runBisectController(deps: BisectControllerDeps, signal?: AbortSignal): Promise<BisectOutcome> {
|
|
117
|
+
const steps: BisectStep[] = [];
|
|
118
|
+
for (let step = 0; step < deps.maxSteps; step += 1) {
|
|
119
|
+
throwIfAborted(signal);
|
|
120
|
+
const rev = await deps.currentRev();
|
|
121
|
+
const verdict = await deps.evaluate(rev);
|
|
122
|
+
steps.push({ rev, verdict });
|
|
123
|
+
const marked = await deps.mark(verdict);
|
|
124
|
+
if (marked.exitCode !== 0) {
|
|
125
|
+
return {
|
|
126
|
+
culprit: null,
|
|
127
|
+
steps,
|
|
128
|
+
concluded: false,
|
|
129
|
+
reason: marked.output.trim() || `git bisect ${verdict} failed with exit code ${marked.exitCode}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const culprit = parseFirstBadCommit(marked.output);
|
|
133
|
+
if (culprit) return { culprit, steps, concluded: true };
|
|
134
|
+
if (ONLY_SKIPPED_RE.test(marked.output)) {
|
|
135
|
+
return {
|
|
136
|
+
culprit: null,
|
|
137
|
+
steps,
|
|
138
|
+
concluded: false,
|
|
139
|
+
reason: "only skipped commits are left to test — the result is undetermined",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
culprit: null,
|
|
145
|
+
steps,
|
|
146
|
+
concluded: false,
|
|
147
|
+
reason: `reached the ${deps.maxSteps}-step limit without converging`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Run the predicate as `sh -c <run>` in `cwd` and return its exit code. A step
|
|
153
|
+
* that exceeds `timeoutMs` is killed and reported as a skip (125). Honors the
|
|
154
|
+
* outer abort signal by propagating a {@link ToolAbortError}.
|
|
155
|
+
*/
|
|
156
|
+
async function evaluatePredicate(
|
|
157
|
+
run: string,
|
|
158
|
+
cwd: string,
|
|
159
|
+
timeoutMs: number,
|
|
160
|
+
signal: AbortSignal | undefined,
|
|
161
|
+
): Promise<number> {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
let timedOut = false;
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
timedOut = true;
|
|
166
|
+
controller.abort();
|
|
167
|
+
}, timeoutMs);
|
|
168
|
+
const onExternalAbort = () => controller.abort();
|
|
169
|
+
signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
170
|
+
try {
|
|
171
|
+
const child = Bun.spawn(["sh", "-c", run], {
|
|
172
|
+
cwd,
|
|
173
|
+
stdin: "ignore",
|
|
174
|
+
stdout: "ignore",
|
|
175
|
+
stderr: "ignore",
|
|
176
|
+
signal: controller.signal,
|
|
177
|
+
});
|
|
178
|
+
const exitCode = await child.exited;
|
|
179
|
+
if (timedOut) return SKIP_EXIT_CODE;
|
|
180
|
+
if (signal?.aborted) throw new ToolAbortError();
|
|
181
|
+
return exitCode ?? 1;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (timedOut) return SKIP_EXIT_CODE;
|
|
184
|
+
if (signal?.aborted) throw new ToolAbortError();
|
|
185
|
+
throw new ToolError(`Failed to run bisect predicate: ${error instanceof Error ? error.message : String(error)}`);
|
|
186
|
+
} finally {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
signal?.removeEventListener("abort", onExternalAbort);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatSteps(steps: BisectStep[]): string {
|
|
193
|
+
if (steps.length === 0) return " (seeded range converged immediately)";
|
|
194
|
+
return steps.map((step, index) => ` ${index + 1}. ${step.rev.slice(0, 12)} → ${step.verdict}`).join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Report the post-teardown worktree state honestly. `git bisect reset` plus a
|
|
199
|
+
* `git reset --hard` restores every *tracked* file to its pre-bisect state, so
|
|
200
|
+
* the common "predicate wrote to a tracked file" case is fully cleaned. A
|
|
201
|
+
* predicate that created *untracked* files is intentionally left untouched (the
|
|
202
|
+
* tool never deletes files it did not create), so those are surfaced as a note
|
|
203
|
+
* rather than hidden behind a false "restored" claim. Any residual tracked
|
|
204
|
+
* change is reported as a warning so the tool never overstates the cleanup.
|
|
205
|
+
*/
|
|
206
|
+
async function describeRestore(cwd: string): Promise<string> {
|
|
207
|
+
const tracked = (await git.status(cwd, { porcelainV1: true, untrackedFiles: "no" }).catch(() => "")).trim();
|
|
208
|
+
if (tracked) {
|
|
209
|
+
return `WARNING: working tree not fully restored — tracked changes remain after teardown:\n${tracked}`;
|
|
210
|
+
}
|
|
211
|
+
const all = (await git.status(cwd, { porcelainV1: true, untrackedFiles: "all" }).catch(() => "")).trim();
|
|
212
|
+
if (all) {
|
|
213
|
+
return "Working tree restored: tracked files reset to their pre-bisect state (untracked files the predicate created are left in place).";
|
|
214
|
+
}
|
|
215
|
+
return "Working tree restored to its pre-bisect state.";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class BisectTool implements AgentTool<typeof bisectSchema, BisectToolDetails> {
|
|
219
|
+
readonly name = "bisect";
|
|
220
|
+
readonly label = "Bisect";
|
|
221
|
+
readonly summary = "Find the commit that introduced a regression by driving git bisect with a shell predicate";
|
|
222
|
+
readonly description: string;
|
|
223
|
+
readonly parameters = bisectSchema;
|
|
224
|
+
readonly strict = true;
|
|
225
|
+
readonly loadMode = "discoverable";
|
|
226
|
+
readonly intent = (args: Partial<BisectParams>): string =>
|
|
227
|
+
args.run ? `bisecting: ${args.run}` : "bisecting regression";
|
|
228
|
+
|
|
229
|
+
constructor(private readonly session: ToolSession) {
|
|
230
|
+
this.description = prompt.render(bisectDescription);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async execute(
|
|
234
|
+
_toolCallId: string,
|
|
235
|
+
params: BisectParams,
|
|
236
|
+
signal?: AbortSignal,
|
|
237
|
+
_onUpdate?: AgentToolUpdateCallback<BisectToolDetails>,
|
|
238
|
+
_context?: AgentToolContext,
|
|
239
|
+
): Promise<AgentToolResult<BisectToolDetails>> {
|
|
240
|
+
// Resolve the worktree top level once and run every git, predicate, and
|
|
241
|
+
// teardown operation from it — never from the raw session cwd. The session
|
|
242
|
+
// cwd may be a subdirectory that a candidate commit deletes mid-bisect
|
|
243
|
+
// (leaving later commands with a missing working directory), and `git
|
|
244
|
+
// bisect` itself must be run from the top level of the working tree.
|
|
245
|
+
const cwd = await git.repo.root(this.session.cwd, signal);
|
|
246
|
+
if (!cwd) {
|
|
247
|
+
throw new ToolError("bisect requires a git repository; the current directory is not inside a git worktree.");
|
|
248
|
+
}
|
|
249
|
+
const dirty = (await git.status(cwd, { porcelainV1: true, untrackedFiles: "no" })).trim();
|
|
250
|
+
if (dirty) {
|
|
251
|
+
throw new ToolError(
|
|
252
|
+
"Working tree has uncommitted changes. Commit or stash them before bisecting — git bisect checks out historical commits and would clobber uncommitted work.",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const goodSha = await git.ref.resolve(cwd, params.good, signal);
|
|
257
|
+
if (!goodSha) throw new ToolError(`Could not resolve the good ref: ${params.good}`);
|
|
258
|
+
const badSha = await git.ref.resolve(cwd, params.bad, signal);
|
|
259
|
+
if (!badSha) throw new ToolError(`Could not resolve the bad ref: ${params.bad}`);
|
|
260
|
+
if (goodSha === badSha) {
|
|
261
|
+
throw new ToolError("good and bad resolve to the same commit; there is nothing to bisect.");
|
|
262
|
+
}
|
|
263
|
+
if (!(await git.bisect.isAncestor(cwd, goodSha, badSha, signal))) {
|
|
264
|
+
throw new ToolError(
|
|
265
|
+
`good (${params.good}) must be an ancestor of bad (${params.bad}). Swap them or pick a correct known-good commit.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let outcome: BisectOutcome;
|
|
270
|
+
// Defensive reset in case a previous aborted bisect left state behind; a
|
|
271
|
+
// no-op when not bisecting (reset never throws).
|
|
272
|
+
await git.bisect.reset(cwd, signal);
|
|
273
|
+
try {
|
|
274
|
+
await git.bisect.start(cwd, signal);
|
|
275
|
+
const badMark = await git.bisect.bad(cwd, badSha, signal);
|
|
276
|
+
if (badMark.exitCode !== 0) {
|
|
277
|
+
throw new ToolError(`git bisect bad failed: ${badMark.stderr.trim() || badMark.stdout.trim()}`);
|
|
278
|
+
}
|
|
279
|
+
const goodMark = await git.bisect.good(cwd, goodSha, signal);
|
|
280
|
+
if (goodMark.exitCode !== 0) {
|
|
281
|
+
throw new ToolError(`git bisect good failed: ${goodMark.stderr.trim() || goodMark.stdout.trim()}`);
|
|
282
|
+
}
|
|
283
|
+
// A tiny good..bad range can converge on the seeding marks themselves.
|
|
284
|
+
const seededCulprit = parseFirstBadCommit(`${goodMark.stdout}\n${goodMark.stderr}`);
|
|
285
|
+
if (seededCulprit) {
|
|
286
|
+
outcome = { culprit: seededCulprit, steps: [], concluded: true };
|
|
287
|
+
} else {
|
|
288
|
+
outcome = await runBisectController(
|
|
289
|
+
{
|
|
290
|
+
maxSteps: params.maxSteps,
|
|
291
|
+
currentRev: async () => (await git.head.sha(cwd, signal)) ?? "HEAD",
|
|
292
|
+
evaluate: async () =>
|
|
293
|
+
classifyExit(
|
|
294
|
+
await evaluatePredicate(params.run, cwd, params.stepTimeoutMs, signal),
|
|
295
|
+
params.invert,
|
|
296
|
+
),
|
|
297
|
+
mark: async verdict => {
|
|
298
|
+
const result =
|
|
299
|
+
verdict === "good"
|
|
300
|
+
? await git.bisect.good(cwd, undefined, signal)
|
|
301
|
+
: verdict === "bad"
|
|
302
|
+
? await git.bisect.bad(cwd, undefined, signal)
|
|
303
|
+
: await git.bisect.skip(cwd, signal);
|
|
304
|
+
return { exitCode: result.exitCode, output: `${result.stdout}\n${result.stderr}` };
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
signal,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
// Always restore the working tree, even on error/abort. `git bisect
|
|
312
|
+
// reset` returns HEAD to the original branch/commit but leaves behind
|
|
313
|
+
// any tracked-file edits the `sh -c` predicate made mid-run. The tracked
|
|
314
|
+
// tree was verified clean before we started (precondition above), so
|
|
315
|
+
// discarding tracked modifications (reset --hard) only reverts the
|
|
316
|
+
// predicate's side effects, never user work; then bisect reset restores
|
|
317
|
+
// the original HEAD. Both are best-effort and must not throw here.
|
|
318
|
+
await git.reset(cwd, { hard: true }).catch(() => {});
|
|
319
|
+
await git.bisect.reset(cwd);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Never claim a clean restore we did not actually achieve.
|
|
323
|
+
const restoreLine = await describeRestore(cwd);
|
|
324
|
+
|
|
325
|
+
if (outcome.concluded && outcome.culprit) {
|
|
326
|
+
const info = await git.bisect.describe(cwd, outcome.culprit).catch(() => null);
|
|
327
|
+
const lines = [
|
|
328
|
+
params.invert ? `First fixing commit: ${outcome.culprit}` : `First bad commit: ${outcome.culprit}`,
|
|
329
|
+
];
|
|
330
|
+
if (info) {
|
|
331
|
+
lines.push(`Author: ${info.author}`, `Date: ${info.date}`, `Subject: ${info.subject}`);
|
|
332
|
+
if (info.stat) lines.push("", "Files changed:", info.stat);
|
|
333
|
+
}
|
|
334
|
+
lines.push("", `Tested ${outcome.steps.length} revision(s):`, formatSteps(outcome.steps));
|
|
335
|
+
lines.push("", restoreLine);
|
|
336
|
+
return toolResult<BisectToolDetails>({
|
|
337
|
+
culprit: outcome.culprit,
|
|
338
|
+
invert: params.invert,
|
|
339
|
+
concluded: true,
|
|
340
|
+
steps: outcome.steps,
|
|
341
|
+
author: info?.author,
|
|
342
|
+
date: info?.date,
|
|
343
|
+
subject: info?.subject,
|
|
344
|
+
})
|
|
345
|
+
.text(lines.join("\n"))
|
|
346
|
+
.done();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const lines = [
|
|
350
|
+
`Bisect did not converge: ${outcome.reason ?? "unknown reason"}.`,
|
|
351
|
+
"",
|
|
352
|
+
`Tested ${outcome.steps.length} revision(s):`,
|
|
353
|
+
formatSteps(outcome.steps),
|
|
354
|
+
"",
|
|
355
|
+
restoreLine,
|
|
356
|
+
];
|
|
357
|
+
return toolResult<BisectToolDetails>({
|
|
358
|
+
culprit: null,
|
|
359
|
+
invert: params.invert,
|
|
360
|
+
concluded: false,
|
|
361
|
+
steps: outcome.steps,
|
|
362
|
+
reason: outcome.reason,
|
|
363
|
+
})
|
|
364
|
+
.text(lines.join("\n"))
|
|
365
|
+
.done();
|
|
366
|
+
}
|
|
367
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { AstEditTool } from "./ast-edit";
|
|
|
34
34
|
import { AstGrepTool } from "./ast-grep";
|
|
35
35
|
import { BashTool } from "./bash";
|
|
36
36
|
import type { BashRestrictionProfile } from "./bash-allowed-prefixes";
|
|
37
|
+
import { BisectTool } from "./bisect";
|
|
37
38
|
import { BrowserTool } from "./browser";
|
|
38
39
|
import { CalculatorTool } from "./calculator";
|
|
39
40
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
@@ -72,6 +73,7 @@ export * from "./ask";
|
|
|
72
73
|
export * from "./ast-edit";
|
|
73
74
|
export * from "./ast-grep";
|
|
74
75
|
export * from "./bash";
|
|
76
|
+
export * from "./bisect";
|
|
75
77
|
export * from "./browser";
|
|
76
78
|
export * from "./calculator";
|
|
77
79
|
export * from "./checkpoint";
|
|
@@ -387,6 +389,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
387
389
|
render_mermaid: s => new RenderMermaidTool(s),
|
|
388
390
|
ask: AskTool.createIf,
|
|
389
391
|
debug: DebugTool.createIf,
|
|
392
|
+
bisect: s => new BisectTool(s),
|
|
390
393
|
eval: s => new EvalTool(s),
|
|
391
394
|
calc: s => new CalculatorTool(s),
|
|
392
395
|
ssh: loadSshTool,
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -554,13 +554,17 @@ function getSpillConfig(s: Settings | undefined) {
|
|
|
554
554
|
| "tools.artifactSpillThreshold"
|
|
555
555
|
| "tools.artifactTailBytes"
|
|
556
556
|
| "tools.artifactTailLines"
|
|
557
|
-
| "tools.artifactHeadBytes"
|
|
557
|
+
| "tools.artifactHeadBytes"
|
|
558
|
+
| "tools.maxInlineResultBytes"
|
|
559
|
+
| "tools.readArtifactSpillThreshold";
|
|
558
560
|
const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
|
|
559
561
|
return {
|
|
560
562
|
threshold: get("tools.artifactSpillThreshold") * 1024,
|
|
563
|
+
readThreshold: get("tools.readArtifactSpillThreshold") * 1024,
|
|
561
564
|
tailBytes: get("tools.artifactTailBytes") * 1024,
|
|
562
565
|
tailLines: get("tools.artifactTailLines"),
|
|
563
566
|
headBytes: get("tools.artifactHeadBytes") * 1024,
|
|
567
|
+
maxInlineBytes: get("tools.maxInlineResultBytes") * 1024,
|
|
564
568
|
};
|
|
565
569
|
}
|
|
566
570
|
|
|
@@ -596,8 +600,12 @@ async function spillLargeResultToArtifact(
|
|
|
596
600
|
): Promise<AgentToolResult> {
|
|
597
601
|
const sessionManager = context?.sessionManager;
|
|
598
602
|
if (!sessionManager) return result;
|
|
599
|
-
|
|
600
|
-
|
|
603
|
+
const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
604
|
+
// `read` manages its own per-range truncation, but the combined multi-range
|
|
605
|
+
// output has no cap — enforce a read-specific (higher) combined threshold
|
|
606
|
+
// instead of exempting read entirely. 0 disables read spill (backstop only).
|
|
607
|
+
const effectiveThreshold = toolName === "read" ? readThreshold : threshold;
|
|
608
|
+
if (effectiveThreshold <= 0) return result;
|
|
601
609
|
|
|
602
610
|
// Skip if tool already saved an artifact
|
|
603
611
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
@@ -614,7 +622,7 @@ async function spillLargeResultToArtifact(
|
|
|
614
622
|
|
|
615
623
|
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
616
624
|
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
617
|
-
if (totalBytes <=
|
|
625
|
+
if (totalBytes <= effectiveThreshold) return result;
|
|
618
626
|
|
|
619
627
|
// Save full output as artifact
|
|
620
628
|
const artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
@@ -691,6 +699,106 @@ async function spillLargeResultToArtifact(
|
|
|
691
699
|
return { ...result, content: newContent, details: newDetails };
|
|
692
700
|
}
|
|
693
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Absolute inline-size backstop enforced after {@link spillLargeResultToArtifact}.
|
|
704
|
+
*
|
|
705
|
+
* The threshold-based spill above has escape hatches: it early-returns for the
|
|
706
|
+
* `read` tool and for results that already carry an `artifactId` (a tool may set
|
|
707
|
+
* partial truncation meta yet still emit oversized inline text). This backstop
|
|
708
|
+
* closes those gaps: when `tools.maxInlineResultBytes` is configured (> 0), any
|
|
709
|
+
* final result whose inline text exceeds the cap is force-saved to an artifact
|
|
710
|
+
* (reusing an existing artifactId to avoid double-artifacting) and truncated to a
|
|
711
|
+
* head+tail view that fits the cap. Disabled by default (opt-in pending
|
|
712
|
+
* measurement); a 0 cap returns the result untouched.
|
|
713
|
+
*/
|
|
714
|
+
async function enforceInlineResultBackstop(
|
|
715
|
+
result: AgentToolResult,
|
|
716
|
+
toolName: string,
|
|
717
|
+
context: AgentToolContext | undefined,
|
|
718
|
+
): Promise<AgentToolResult> {
|
|
719
|
+
const { maxInlineBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
720
|
+
if (maxInlineBytes <= 0) return result;
|
|
721
|
+
|
|
722
|
+
const textParts: string[] = [];
|
|
723
|
+
for (const block of result.content) {
|
|
724
|
+
if (block.type === "text" && block.text) {
|
|
725
|
+
textParts.push(block.text);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (textParts.length === 0) return result;
|
|
729
|
+
|
|
730
|
+
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
731
|
+
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
732
|
+
if (totalBytes <= maxInlineBytes) return result;
|
|
733
|
+
|
|
734
|
+
// Reuse an existing artifact (avoid double-artifacting); otherwise save the
|
|
735
|
+
// full output so the truncated view keeps a reference to the complete text.
|
|
736
|
+
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
737
|
+
let artifactId = existingMeta?.truncation?.artifactId;
|
|
738
|
+
if (!artifactId) {
|
|
739
|
+
artifactId = (await context?.sessionManager?.saveArtifact(fullText, toolName)) ?? undefined;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Budget head+tail below the cap, reserving room for the elision marker so the
|
|
743
|
+
// composed `<head>\n<marker>\n<tail>` view never exceeds the configured cap.
|
|
744
|
+
const MARKER_RESERVE = 256;
|
|
745
|
+
const budget = maxInlineBytes - MARKER_RESERVE;
|
|
746
|
+
const useMiddle = headBytes > 0 && budget > 0;
|
|
747
|
+
let truncated = useMiddle
|
|
748
|
+
? truncateMiddle(fullText, {
|
|
749
|
+
maxBytes: budget,
|
|
750
|
+
maxLines: tailLines * 2,
|
|
751
|
+
maxHeadBytes: Math.min(headBytes, Math.floor(budget / 2)),
|
|
752
|
+
maxHeadLines: tailLines,
|
|
753
|
+
})
|
|
754
|
+
: truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
|
|
755
|
+
|
|
756
|
+
// Defensive clamp: guarantee the contract even for pathological marker sizes.
|
|
757
|
+
if (Buffer.byteLength(truncated.content, "utf-8") > maxInlineBytes) {
|
|
758
|
+
truncated = truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const newContent: (TextContent | ImageContent)[] = [];
|
|
762
|
+
for (const block of result.content) {
|
|
763
|
+
if (block.type !== "text") {
|
|
764
|
+
newContent.push(block);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
newContent.push({ type: "text", text: truncated.content });
|
|
768
|
+
|
|
769
|
+
const outputLines = truncated.outputLines ?? truncated.totalLines;
|
|
770
|
+
const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
|
|
771
|
+
const truncationMeta: TruncationMeta =
|
|
772
|
+
truncated.truncatedBy === "middle"
|
|
773
|
+
? {
|
|
774
|
+
direction: "middle",
|
|
775
|
+
truncatedBy: "middle",
|
|
776
|
+
totalLines: truncated.totalLines,
|
|
777
|
+
totalBytes: truncated.totalBytes,
|
|
778
|
+
outputLines,
|
|
779
|
+
outputBytes,
|
|
780
|
+
maxBytes: maxInlineBytes,
|
|
781
|
+
elidedLines: truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines),
|
|
782
|
+
elidedBytes: truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes),
|
|
783
|
+
artifactId,
|
|
784
|
+
}
|
|
785
|
+
: {
|
|
786
|
+
direction: "tail",
|
|
787
|
+
truncatedBy: truncated.truncatedBy ?? "bytes",
|
|
788
|
+
totalLines: truncated.totalLines,
|
|
789
|
+
totalBytes: truncated.totalBytes,
|
|
790
|
+
outputLines,
|
|
791
|
+
outputBytes,
|
|
792
|
+
maxBytes: maxInlineBytes,
|
|
793
|
+
shownRange: { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
|
|
794
|
+
artifactId,
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
|
|
798
|
+
const newDetails = { ...(result.details ?? {}), meta: newMeta };
|
|
799
|
+
return { ...result, content: newContent, details: newDetails };
|
|
800
|
+
}
|
|
801
|
+
|
|
694
802
|
// =============================================================================
|
|
695
803
|
// Tool wrapper
|
|
696
804
|
// =============================================================================
|
|
@@ -711,6 +819,10 @@ async function wrappedExecute(
|
|
|
711
819
|
// Spill large results to artifact, truncate to tail
|
|
712
820
|
result = await spillLargeResultToArtifact(result, this.name, context);
|
|
713
821
|
|
|
822
|
+
// Absolute inline-size backstop: catches oversized text the threshold spill
|
|
823
|
+
// skipped (read exemption, tools with pre-existing partial artifact meta).
|
|
824
|
+
result = await enforceInlineResultBackstop(result, this.name, context);
|
|
825
|
+
|
|
714
826
|
// Append notices from meta
|
|
715
827
|
const meta: OutputMeta | undefined = result.details?.meta;
|
|
716
828
|
if (meta) {
|
package/src/tools/search.ts
CHANGED
|
@@ -44,7 +44,10 @@ const searchSchema = z
|
|
|
44
44
|
paths: z
|
|
45
45
|
.array(z.string().describe("file, directory, glob, or internal URL to search"))
|
|
46
46
|
.min(1)
|
|
47
|
-
.
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
"files, directories, globs, or internal URLs to search (defaults to the working directory when omitted)",
|
|
50
|
+
),
|
|
48
51
|
i: z.boolean().optional().describe("case-insensitive search"),
|
|
49
52
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
50
53
|
skip: z
|
|
@@ -236,7 +239,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
236
239
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
237
240
|
_toolContext?: AgentToolContext,
|
|
238
241
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
239
|
-
const { pattern,
|
|
242
|
+
const { pattern, i, gitignore, skip } = params;
|
|
243
|
+
// Recurring real-world failure mode: models omit `paths` for repo-wide
|
|
244
|
+
// searches. Default to the working directory instead of failing the call.
|
|
245
|
+
const paths = params.paths ?? ["."];
|
|
240
246
|
|
|
241
247
|
return untilAborted(signal, async () => {
|
|
242
248
|
const normalizedPattern = pattern.trim();
|
package/src/tools/skill.ts
CHANGED
|
@@ -37,6 +37,8 @@ function normalizeSkillName(name: string | undefined): string {
|
|
|
37
37
|
return (name ?? "").trim();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const SKILL_NAME_GLOB_PATTERN = /[*?[\]{}]/;
|
|
41
|
+
|
|
40
42
|
type SkillToolInput = z.infer<typeof skillSchema>;
|
|
41
43
|
|
|
42
44
|
export interface SkillToolDetails {
|
|
@@ -87,6 +89,19 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
87
89
|
if (!requestedName) {
|
|
88
90
|
throw new ToolError("skill tool: `name` is required");
|
|
89
91
|
}
|
|
92
|
+
// Fail fast on glob/wildcard names (e.g. `*`). A model that sees the
|
|
93
|
+
// `--skills '*'` launch filter may echo the glob into the skill tool.
|
|
94
|
+
// Without this guard `*` slips through to the generic unknown-skill
|
|
95
|
+
// path only because no skill is literally named `*`, so a future
|
|
96
|
+
// odd skill name could get dispatched — and the actionable signal
|
|
97
|
+
// that names are concrete (not globs) is lost. Reject before any
|
|
98
|
+
// dispatch or handoff state mutation so the call ends immediately
|
|
99
|
+
// instead of spawning skill work that burns the whole turn budget.
|
|
100
|
+
if (SKILL_NAME_GLOB_PATTERN.test(requestedName)) {
|
|
101
|
+
throw new ToolError(
|
|
102
|
+
`skill tool: "${requestedName}" is not a valid skill name. The name must be a single concrete skill (e.g. "ralplan"), not a glob or wildcard pattern. Pass one exact skill name as shown in /skill:<name>.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
90
105
|
const activeState = this.#session.getActiveSkillState?.();
|
|
91
106
|
const activeSkill = normalizeSkillName(activeState?.skill);
|
|
92
107
|
if (activeSkill && requestedName === activeSkill) {
|