one-code-extension 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +32 -0
- package/README.md +202 -0
- package/agents/explore.md +24 -0
- package/agents/general-purpose.md +21 -0
- package/agents/plan.md +22 -0
- package/docs/decisions/auto-mode.md +526 -0
- package/docs/decisions/branding.md +127 -0
- package/docs/decisions/code-review-remediation.md +106 -0
- package/docs/decisions/compaction.md +140 -0
- package/docs/decisions/distribution.md +133 -0
- package/docs/decisions/hooks.md +64 -0
- package/docs/decisions/lsp.md +58 -0
- package/docs/decisions/mcp.md +104 -0
- package/docs/decisions/memory-state.md +170 -0
- package/docs/decisions/model-policy.md +126 -0
- package/docs/decisions/modes.md +178 -0
- package/docs/decisions/skills-plugins.md +56 -0
- package/docs/decisions/subagents-workflows.md +225 -0
- package/docs/decisions/system-prompt.md +98 -0
- package/docs/decisions/tools.md +407 -0
- package/docs/decisions/tui.md +202 -0
- package/docs/decisions.md +148 -0
- package/extensions/ask-user/index.ts +124 -0
- package/extensions/ask-user/questions.ts +60 -0
- package/extensions/auto-mode/actions.ts +78 -0
- package/extensions/auto-mode/classifier.ts +388 -0
- package/extensions/auto-mode/config.ts +237 -0
- package/extensions/auto-mode/decision-log.ts +60 -0
- package/extensions/auto-mode/defaults.ts +65 -0
- package/extensions/auto-mode/instructions.ts +74 -0
- package/extensions/auto-mode/model-picker.ts +225 -0
- package/extensions/auto-mode/model-select.ts +178 -0
- package/extensions/auto-mode/paths.ts +109 -0
- package/extensions/auto-mode/pause.ts +96 -0
- package/extensions/auto-mode/prompt.ts +307 -0
- package/extensions/auto-mode/rules.ts +96 -0
- package/extensions/auto-mode/safety-floor.ts +100 -0
- package/extensions/auto-mode/sensitive.ts +137 -0
- package/extensions/auto-mode/shell-analysis.ts +702 -0
- package/extensions/background/index.ts +405 -0
- package/extensions/background/registry.ts +81 -0
- package/extensions/background/wakeup.ts +41 -0
- package/extensions/bash/background.ts +136 -0
- package/extensions/bash/index.ts +149 -0
- package/extensions/branding/assistant-marker.ts +53 -0
- package/extensions/branding/index.ts +366 -0
- package/extensions/branding/prompt-editor.ts +42 -0
- package/extensions/branding/prompt-marker.ts +29 -0
- package/extensions/branding/startup.ts +152 -0
- package/extensions/claude-compat/index.ts +50 -0
- package/extensions/claude-context/index.ts +79 -0
- package/extensions/compaction/index.ts +231 -0
- package/extensions/compaction/prompt.ts +191 -0
- package/extensions/context-management/index.ts +147 -0
- package/extensions/effort/index.ts +216 -0
- package/extensions/effort/slider.ts +253 -0
- package/extensions/exit/index.ts +28 -0
- package/extensions/file-tracker/index.ts +103 -0
- package/extensions/file-tracker/tracker.ts +145 -0
- package/extensions/hooks/debug.ts +53 -0
- package/extensions/hooks/executor.ts +132 -0
- package/extensions/hooks/index.ts +310 -0
- package/extensions/hooks/matcher.ts +72 -0
- package/extensions/hooks/plugin-hooks.ts +79 -0
- package/extensions/hooks/protocol.ts +151 -0
- package/extensions/hooks/settings.ts +163 -0
- package/extensions/hooks/trust.ts +151 -0
- package/extensions/lib/branch-restore.ts +35 -0
- package/extensions/lib/claude-context.ts +148 -0
- package/extensions/lib/deferred.ts +119 -0
- package/extensions/lib/git.ts +14 -0
- package/extensions/lib/memory.ts +219 -0
- package/extensions/lib/model-policy.ts +436 -0
- package/extensions/lib/model-tier.ts +99 -0
- package/extensions/lib/notifications.ts +18 -0
- package/extensions/lib/paths.ts +26 -0
- package/extensions/lib/per-cwd.ts +21 -0
- package/extensions/lib/persisted-output.ts +70 -0
- package/extensions/lib/pi-version.ts +54 -0
- package/extensions/lib/plugins.ts +227 -0
- package/extensions/lib/reminders.ts +212 -0
- package/extensions/lib/scratchpad.ts +68 -0
- package/extensions/lib/tui-render.ts +522 -0
- package/extensions/lsp/client.ts +278 -0
- package/extensions/lsp/format.ts +46 -0
- package/extensions/lsp/index.ts +193 -0
- package/extensions/lsp/protocol.ts +64 -0
- package/extensions/lsp/servers.ts +79 -0
- package/extensions/mcp/client.ts +205 -0
- package/extensions/mcp/config.ts +199 -0
- package/extensions/mcp/index.ts +373 -0
- package/extensions/mcp/schema.ts +161 -0
- package/extensions/memory/index.ts +90 -0
- package/extensions/notebook/index.ts +77 -0
- package/extensions/notebook/notebook.ts +108 -0
- package/extensions/permissions/index.ts +876 -0
- package/extensions/permissions/matcher.ts +455 -0
- package/extensions/permissions/modes.ts +107 -0
- package/extensions/permissions/protected-paths.ts +125 -0
- package/extensions/permissions/settings.ts +98 -0
- package/extensions/plan-mode/index.ts +209 -0
- package/extensions/plan-mode/reminder.ts +33 -0
- package/extensions/plan-mode/slug.ts +28 -0
- package/extensions/plan-mode/viewer.ts +124 -0
- package/extensions/plugins/index.ts +115 -0
- package/extensions/plugins/template.ts +42 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +24 -0
- package/extensions/skill/index.ts +170 -0
- package/extensions/subagents/agents.ts +126 -0
- package/extensions/subagents/child.ts +414 -0
- package/extensions/subagents/default-model.ts +122 -0
- package/extensions/subagents/index.ts +1119 -0
- package/extensions/subagents/model-select.ts +346 -0
- package/extensions/subagents/rpc-turns.ts +123 -0
- package/extensions/subagents/runs.ts +93 -0
- package/extensions/subagents/usage.ts +53 -0
- package/extensions/subagents/worktree.ts +69 -0
- package/extensions/system-prompt/environment.ts +35 -0
- package/extensions/system-prompt/index.ts +54 -0
- package/extensions/system-prompt/template.ts +103 -0
- package/extensions/system-prompt/tiers/common.ts +63 -0
- package/extensions/system-prompt/tiers/frontier.ts +24 -0
- package/extensions/system-prompt/tiers/low.ts +71 -0
- package/extensions/system-prompt/tiers/mid.ts +56 -0
- package/extensions/system-reminder/index.ts +37 -0
- package/extensions/tasks/index.ts +140 -0
- package/extensions/tasks/store.ts +174 -0
- package/extensions/todo/index.ts +133 -0
- package/extensions/tool-search/index.ts +168 -0
- package/extensions/tool-style/index.ts +84 -0
- package/extensions/web/index.ts +43 -0
- package/extensions/web-fetch/extract.ts +139 -0
- package/extensions/web-fetch/index.ts +291 -0
- package/extensions/web-fetch/summarize.ts +78 -0
- package/extensions/workflow/agent-session.ts +368 -0
- package/extensions/workflow/globals.ts +251 -0
- package/extensions/workflow/index.ts +364 -0
- package/extensions/workflow/journal.ts +75 -0
- package/extensions/workflow/permission-gate.ts +71 -0
- package/extensions/workflow/run-manager.ts +306 -0
- package/extensions/workflow/saved-workflows.ts +45 -0
- package/extensions/workflow/script-source.ts +201 -0
- package/extensions/workflow/types.ts +99 -0
- package/extensions/workflow/vm-runtime.ts +31 -0
- package/extensions/workflow/widget.ts +56 -0
- package/extensions/worktree/index.ts +266 -0
- package/extensions/worktree/rewrite.ts +59 -0
- package/package.json +101 -0
- package/themes/one-code-light.json +76 -0
- package/themes/one-code.json +76 -0
- package/types/app.d.ts +5 -0
- package/types/community-packages.d.ts +12 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing a subagent's model (pure).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* The `model` field used to be passed as a raw string to the child's
|
|
7
|
+
* `pi --model`, whose fuzzy matcher substring-matches across **every configured
|
|
8
|
+
* provider**, preferring alias ids and then reverse-lexicographic order. With
|
|
9
|
+
* two providers authenticated, `model: "sonnet"` — which is what real
|
|
10
|
+
* `.claude/agents` files and `CLAUDE_CODE_SUBAGENT_MODEL` say — resolved to an
|
|
11
|
+
* effectively arbitrary provider. A fork child inherits the parent's entire
|
|
12
|
+
* transcript, so a silently cross-provider subagent is the same privacy bug the
|
|
13
|
+
* classifier's model selection fixed, with higher stakes.
|
|
14
|
+
*
|
|
15
|
+
* So resolution happens in the *parent*, against the real registry, and the
|
|
16
|
+
* child is spawned with a concrete `provider/id`:
|
|
17
|
+
*
|
|
18
|
+
* - Claude Code aliases (`sonnet`/`opus`/`haiku`/`fable`) resolve **within the
|
|
19
|
+
* session's provider** (and contained route/model family on gateways); where the name
|
|
20
|
+
* matches nothing, the session model serves and the parent says so.
|
|
21
|
+
* - An exact model reference resolves anywhere — the user (or an agent file the
|
|
22
|
+
* user installed) naming a model is choosing it — but crossing providers is
|
|
23
|
+
* announced, never silent.
|
|
24
|
+
* - A configured default that cannot resolve degrades to the session model with
|
|
25
|
+
* a notice naming the knob; without one, a reviewed same-provider role profile
|
|
26
|
+
* chooses an economical default before the session fallback. Only a bad
|
|
27
|
+
* *per-call* request errors, because the model can read the menu and retry.
|
|
28
|
+
*
|
|
29
|
+
* The menu keeps the main model informed without dumping a 300-model gateway
|
|
30
|
+
* catalog into every request: vendor-contained, variant- and unpriced-filtered,
|
|
31
|
+
* dated duplicates collapsed, capped — and explicitly *not* a whitelist, since
|
|
32
|
+
* resolution accepts any available model whether or not it was listed.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
36
|
+
import {
|
|
37
|
+
allowsDynamicSubagentSelection,
|
|
38
|
+
findConfigured,
|
|
39
|
+
findRoleProfileModel,
|
|
40
|
+
hasSmallModelName,
|
|
41
|
+
hintRank,
|
|
42
|
+
modelIdentity,
|
|
43
|
+
modelsContainedToSession,
|
|
44
|
+
pricedInput,
|
|
45
|
+
} from "../lib/model-policy.ts";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Cross-extension channel carrying the resolved default subagent model, so the
|
|
49
|
+
* banner can show it when it differs from the session model (jiti isolates
|
|
50
|
+
* module state, so this goes over `pi.events`).
|
|
51
|
+
*/
|
|
52
|
+
export const SUBAGENT_STATUS_CHANNEL = "one-code:subagent-status";
|
|
53
|
+
|
|
54
|
+
export interface SubagentStatus {
|
|
55
|
+
/** `provider/id` subagents will run on. Unset only when nothing resolves at all. */
|
|
56
|
+
model?: string;
|
|
57
|
+
/** Where that came from: a setting, automatic role profile, or the session. */
|
|
58
|
+
via?: "setting" | "env" | "auto" | "session";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What the banner's `subagents` slot shows: the model subagents will actually
|
|
63
|
+
* run on, always, tagged with how it was selected — a user who just ran `/subagent`
|
|
64
|
+
* must see their choice land, and after `/subagent clear` the slot going blank
|
|
65
|
+
* read as breakage rather than "back to the session model". The tag comes from
|
|
66
|
+
* the *resolution*, not the configuration: a configured default that degraded
|
|
67
|
+
* (off-family alias, unavailable model) honestly reads "session".
|
|
68
|
+
*/
|
|
69
|
+
export function subagentStatusModel(
|
|
70
|
+
configured: { source: "subagentModel setting" | "CLAUDE_CODE_SUBAGENT_MODEL" } | undefined,
|
|
71
|
+
resolution: Pick<SubagentModelResolution, "model" | "source">,
|
|
72
|
+
): SubagentStatus {
|
|
73
|
+
if (!resolution.model) return {};
|
|
74
|
+
const via =
|
|
75
|
+
resolution.source === "default" && configured
|
|
76
|
+
? configured.source === "subagentModel setting"
|
|
77
|
+
? "setting"
|
|
78
|
+
: "env"
|
|
79
|
+
: resolution.source === "automatic"
|
|
80
|
+
? "auto"
|
|
81
|
+
: "session";
|
|
82
|
+
return { model: spec(resolution.model), via };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Claude Code's Agent-tool aliases, resolved as name matches within the session's provider. */
|
|
86
|
+
const CLAUDE_CODE_ALIASES = new Set(["sonnet", "opus", "haiku", "fable"]);
|
|
87
|
+
|
|
88
|
+
export type SubagentModelSource = "call" | "agent" | "default" | "automatic" | "session";
|
|
89
|
+
|
|
90
|
+
export interface ResolveInput {
|
|
91
|
+
/** Per-call `model` param — chosen by the main model. */
|
|
92
|
+
requested?: string;
|
|
93
|
+
/** The agent definition's frontmatter `model`. */
|
|
94
|
+
agentModel?: string;
|
|
95
|
+
/** CLAUDE_CODE_SUBAGENT_MODEL / `subagentModel` setting, user or managed scope. */
|
|
96
|
+
configuredDefault?: string;
|
|
97
|
+
sessionModel?: Model<Api>;
|
|
98
|
+
available: Model<Api>[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SubagentModelResolution {
|
|
102
|
+
/** The model to spawn with. Undefined only when nothing at all is available. */
|
|
103
|
+
model?: Model<Api>;
|
|
104
|
+
source: SubagentModelSource;
|
|
105
|
+
/**
|
|
106
|
+
* Set when a *per-call* request resolves to nothing: the caller should fail
|
|
107
|
+
* the tool call with the menu, so the model that chose the string can retry.
|
|
108
|
+
*/
|
|
109
|
+
unresolved?: string;
|
|
110
|
+
/** One-line warnings the parent should surface (fallbacks, provider crossings). */
|
|
111
|
+
notices: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const spec = (model: Model<Api>): string => `${model.provider}/${model.id}`;
|
|
115
|
+
|
|
116
|
+
const isDated = (id: string): boolean => /-20\d{6}$/.test(id);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a Claude Code alias by name within the contained set, preferring
|
|
120
|
+
* undated alias ids and then the newest id. Returns undefined off-family —
|
|
121
|
+
* "sonnet" on a Groq session names nothing, and inventing a mapping would be
|
|
122
|
+
* choosing a model the user never described.
|
|
123
|
+
*/
|
|
124
|
+
function resolveAlias(alias: string, contained: Model<Api>[]): Model<Api> | undefined {
|
|
125
|
+
const matches = contained.filter((model) => model.id.toLowerCase().includes(alias));
|
|
126
|
+
if (matches.length === 0) return undefined;
|
|
127
|
+
const undated = matches.filter((model) => !isDated(model.id));
|
|
128
|
+
const pool = undated.length > 0 ? undated : matches;
|
|
129
|
+
return pool.sort((a, b) => b.id.localeCompare(a.id))[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether an explicit model leaves the session's provider/route/family boundary. */
|
|
133
|
+
export function crossesProvider(model: Model<Api>, sessionModel: Model<Api>): boolean {
|
|
134
|
+
if (model.provider !== sessionModel.provider) return true;
|
|
135
|
+
return modelIdentity(model).containment !== modelIdentity(sessionModel).containment;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolveSubagentModel(input: ResolveInput): SubagentModelResolution {
|
|
139
|
+
const { available, sessionModel } = input;
|
|
140
|
+
const notices: string[] = [];
|
|
141
|
+
|
|
142
|
+
const chain: { value: string; source: SubagentModelSource; knob: string }[] = [];
|
|
143
|
+
if (input.requested) chain.push({ value: input.requested, source: "call", knob: "the model field" });
|
|
144
|
+
if (input.agentModel) chain.push({ value: input.agentModel, source: "agent", knob: "the agent file's model" });
|
|
145
|
+
if (input.configuredDefault) {
|
|
146
|
+
chain.push({ value: input.configuredDefault, source: "default", knob: "the configured subagent default" });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const contained = sessionModel ? modelsContainedToSession(available, sessionModel) : [];
|
|
150
|
+
let suppressAutomatic = false;
|
|
151
|
+
|
|
152
|
+
for (const entry of chain) {
|
|
153
|
+
const wanted = entry.value.trim();
|
|
154
|
+
if (!wanted || wanted.toLowerCase() === "inherit") {
|
|
155
|
+
suppressAutomatic = true; // explicit "use the session model"
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const alias = wanted.toLowerCase();
|
|
160
|
+
if (CLAUDE_CODE_ALIASES.has(alias)) {
|
|
161
|
+
const resolved = resolveAlias(alias, contained);
|
|
162
|
+
if (resolved) return { model: resolved, source: entry.source, notices };
|
|
163
|
+
notices.push(
|
|
164
|
+
`No "${alias}" model exists within ${sessionModel ? spec(sessionModel) : "this session"} — the session model runs this subagent instead.`,
|
|
165
|
+
);
|
|
166
|
+
suppressAutomatic = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolved = findConfigured(available, wanted);
|
|
171
|
+
if (resolved) {
|
|
172
|
+
if (sessionModel && crossesProvider(resolved, sessionModel)) {
|
|
173
|
+
notices.push(
|
|
174
|
+
`Subagent model ${spec(resolved)} is a different provider/family than this session (${spec(sessionModel)}) — it was named via ${entry.knob}, so it is honored.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return { model: resolved, source: entry.source, notices };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (entry.source === "call") {
|
|
181
|
+
// The main model chose this string; give it the menu and let it retry.
|
|
182
|
+
return { source: "call", unresolved: wanted, notices };
|
|
183
|
+
}
|
|
184
|
+
// An explicit choice failed; automatic selection would substitute a model
|
|
185
|
+
// nobody described, so the remaining chain and the session model serve.
|
|
186
|
+
suppressAutomatic = true;
|
|
187
|
+
notices.push(
|
|
188
|
+
entry.source === "agent"
|
|
189
|
+
? `Subagent model "${wanted}" (from ${entry.knob}) is not available — falling back to the configured default or the session model.`
|
|
190
|
+
: `Subagent model "${wanted}" (from ${entry.knob}) is not available — the session model runs this subagent instead.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Automatic selection is a cost optimisation, so it needs price evidence:
|
|
195
|
+
// with either price unknown there is no demonstrable saving, and the profile
|
|
196
|
+
// order alone could silently *upgrade* a cheap session (haiku → sonnet).
|
|
197
|
+
const sessionPrice = sessionModel ? pricedInput(sessionModel) : undefined;
|
|
198
|
+
if (sessionModel && sessionPrice !== undefined && !suppressAutomatic) {
|
|
199
|
+
const isDifferentAndCheaper = (model: Model<Api>) => {
|
|
200
|
+
if (spec(model) === spec(sessionModel)) return false;
|
|
201
|
+
const candidatePrice = pricedInput(model);
|
|
202
|
+
return candidatePrice !== undefined && candidatePrice < sessionPrice;
|
|
203
|
+
};
|
|
204
|
+
const profiled = findRoleProfileModel(available, sessionModel, "subagent", isDifferentAndCheaper);
|
|
205
|
+
if (profiled) return { model: profiled, source: "automatic", notices };
|
|
206
|
+
|
|
207
|
+
// Price-ranked fallback for a stale profile. A name hint is required and
|
|
208
|
+
// ranks first: raw cheapest would pick nano/lite-tier (or wholly unknown)
|
|
209
|
+
// models as the default *coding* worker, so mini-class wins over a cheaper
|
|
210
|
+
// nano, and a model nobody labelled small never wins at all.
|
|
211
|
+
if (allowsDynamicSubagentSelection(sessionModel)) {
|
|
212
|
+
const cheaper = contained
|
|
213
|
+
.map((model) => ({ model, price: pricedInput(model), rank: hintRank(model) }))
|
|
214
|
+
.filter((entry): entry is { model: Model<Api>; price: number; rank: number } => entry.price !== undefined)
|
|
215
|
+
.filter((entry) => spec(entry.model) !== spec(sessionModel))
|
|
216
|
+
.filter((entry) => entry.price < sessionPrice && hasSmallModelName(entry.model))
|
|
217
|
+
.sort((a, b) => a.rank - b.rank || a.price - b.price)[0]?.model;
|
|
218
|
+
if (cheaper) return { model: cheaper, source: "automatic", notices };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (sessionModel) return { model: sessionModel, source: "session", notices };
|
|
223
|
+
return { model: available[0], source: "session", notices };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The per-call expensive-model gate. Only a `source: "call"` resolution is ever
|
|
228
|
+
* gated: the per-call `model` field is the one input the main model chooses for
|
|
229
|
+
* itself. User knobs (setting, env var, agent files the user installed,
|
|
230
|
+
* "inherit") are never gated — naming a model is choosing it — and the
|
|
231
|
+
* automatic default can only ever pick cheaper. With either price unknown the
|
|
232
|
+
* gate stays open: a gate that fails closed on an unpriced catalog blocks the
|
|
233
|
+
* feature outright.
|
|
234
|
+
*
|
|
235
|
+
* Returns the factual comparison; the caller appends its own override hint
|
|
236
|
+
* (the subagent tool and workflow agent() spell the field differently).
|
|
237
|
+
*/
|
|
238
|
+
export function expensiveModelGate(
|
|
239
|
+
resolution: Pick<SubagentModelResolution, "model" | "source">,
|
|
240
|
+
sessionModel: Model<Api> | undefined,
|
|
241
|
+
allowExpensive: boolean | undefined,
|
|
242
|
+
): string | undefined {
|
|
243
|
+
if (allowExpensive) return undefined;
|
|
244
|
+
if (resolution.source !== "call" || !resolution.model || !sessionModel) return undefined;
|
|
245
|
+
const sessionPrice = pricedInput(sessionModel);
|
|
246
|
+
const requestedPrice = pricedInput(resolution.model);
|
|
247
|
+
if (sessionPrice === undefined || requestedPrice === undefined || requestedPrice <= sessionPrice) return undefined;
|
|
248
|
+
return (
|
|
249
|
+
`Requested model ${spec(resolution.model)}${price(resolution.model)} costs more per input token ` +
|
|
250
|
+
`than this session's model ${spec(sessionModel)}${price(sessionModel)}.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface MenuOptions {
|
|
255
|
+
available: Model<Api>[];
|
|
256
|
+
sessionModel?: Model<Api>;
|
|
257
|
+
/** The resolved default (configured, automatic, or session fallback). */
|
|
258
|
+
defaultModel?: Model<Api>;
|
|
259
|
+
defaultSource?: SubagentModelSource;
|
|
260
|
+
/** How many cheaper-option lines to include. */
|
|
261
|
+
maxCheaper?: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const price = (model: Model<Api>): string => {
|
|
265
|
+
const input = pricedInput(model);
|
|
266
|
+
return input === undefined ? "" : ` ($${input}/M in)`;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The curated menu: never the catalog. Vendor-contained, variants and unpriced
|
|
271
|
+
* entries dropped, dated duplicates collapsed, capped — useful, not complete,
|
|
272
|
+
* which is safe because resolution accepts unlisted models too.
|
|
273
|
+
*/
|
|
274
|
+
export function subagentModelMenu({ available, sessionModel, defaultModel, defaultSource, maxCheaper = 3 }: MenuOptions): string[] {
|
|
275
|
+
const lines: string[] = [];
|
|
276
|
+
const listed = new Set<string>();
|
|
277
|
+
const add = (model: Model<Api>, label: string) => {
|
|
278
|
+
if (listed.has(spec(model))) return;
|
|
279
|
+
listed.add(spec(model));
|
|
280
|
+
lines.push(`- ${spec(model)}${price(model)} — ${label}`);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (defaultModel) {
|
|
284
|
+
const label =
|
|
285
|
+
defaultSource === "automatic"
|
|
286
|
+
? "the automatic smaller default"
|
|
287
|
+
: sessionModel && spec(defaultModel) === spec(sessionModel)
|
|
288
|
+
? "the default (this session's model)"
|
|
289
|
+
: "the configured default";
|
|
290
|
+
add(defaultModel, label);
|
|
291
|
+
}
|
|
292
|
+
if (sessionModel) add(sessionModel, "this session's model");
|
|
293
|
+
|
|
294
|
+
if (sessionModel) {
|
|
295
|
+
const contained = modelsContainedToSession(available, sessionModel).filter((model) => !listed.has(spec(model)));
|
|
296
|
+
const priced = contained
|
|
297
|
+
.map((model) => ({ model, input: pricedInput(model) }))
|
|
298
|
+
.filter((entry): entry is { model: Model<Api>; input: number } => entry.input !== undefined)
|
|
299
|
+
// Collapse dated duplicates when the undated alias is also present.
|
|
300
|
+
.filter(
|
|
301
|
+
(entry) =>
|
|
302
|
+
!isDated(entry.model.id) ||
|
|
303
|
+
!contained.some((other) => other.id !== entry.model.id && entry.model.id.startsWith(other.id)),
|
|
304
|
+
)
|
|
305
|
+
.sort((a, b) => a.input - b.input);
|
|
306
|
+
const sessionPrice = pricedInput(sessionModel);
|
|
307
|
+
for (const entry of priced) {
|
|
308
|
+
if (lines.length >= (defaultModel ? 2 : 1) + maxCheaper) break;
|
|
309
|
+
if (sessionPrice !== undefined && entry.input >= sessionPrice) break;
|
|
310
|
+
add(entry.model, "cheaper, same provider");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return lines;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* One informational line when the default subagent model costs more per input
|
|
319
|
+
* token than the session model itself. Automatic selection can never pick a
|
|
320
|
+
* pricier model, so this only ever describes a *configured* default — which can
|
|
321
|
+
* be deliberate (cheap driver, strong workers), so the line informs the main
|
|
322
|
+
* model rather than instructing it to override the user's knob.
|
|
323
|
+
*/
|
|
324
|
+
export function defaultCostsMoreWarning({ sessionModel, defaultModel }: MenuOptions): string | undefined {
|
|
325
|
+
if (!sessionModel || !defaultModel) return undefined;
|
|
326
|
+
const sessionPrice = pricedInput(sessionModel);
|
|
327
|
+
const defaultPrice = pricedInput(defaultModel);
|
|
328
|
+
if (sessionPrice === undefined || defaultPrice === undefined || defaultPrice <= sessionPrice) return undefined;
|
|
329
|
+
return (
|
|
330
|
+
`Note: the default subagent model${price(defaultModel)} costs more per input token than this session's model${price(sessionModel)}. ` +
|
|
331
|
+
"That may be deliberate, but for routine or mechanical tasks consider passing a cheaper listed model in the `model` field."
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The every-turn reminder body. Kept short: its tokens are paid on every call. */
|
|
336
|
+
export function subagentModelsReminder(options: MenuOptions): string {
|
|
337
|
+
const menu = subagentModelMenu(options);
|
|
338
|
+
const warning = defaultCostsMoreWarning(options);
|
|
339
|
+
return [
|
|
340
|
+
"Models for the subagent/workflow `model` field (omit it to use the default; set the default with /subagent):",
|
|
341
|
+
...menu,
|
|
342
|
+
...(warning ? [warning] : []),
|
|
343
|
+
'Aliases sonnet|opus|haiku|fable resolve within this session\'s provider; "inherit" means the session model. ' +
|
|
344
|
+
"Any exact provider/model-id the user asked for also works, even from another provider (that is announced to the user) or unlisted here — this is a menu, not a whitelist.",
|
|
345
|
+
].join("\n");
|
|
346
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC child protocol state (pure) — line-wise parsing for a `pi --mode rpc`
|
|
3
|
+
* child. RPC events are the same objects json mode emits (shared toJsonEvent),
|
|
4
|
+
* interleaved with `response` acks and `extension_ui_request` dialogs. This
|
|
5
|
+
* tracker accumulates per-turn output and cumulative stats, and tells the
|
|
6
|
+
* caller when it must act (answer a UI request, settle a pending turn).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ChildAction, recordAction } from "../auto-mode/actions.ts";
|
|
10
|
+
import { addUsage, emptyUsage, type UsageTotals } from "./usage.ts";
|
|
11
|
+
|
|
12
|
+
export type RpcAction =
|
|
13
|
+
| { kind: "ui_request"; id: string }
|
|
14
|
+
| { kind: "stream_start" }
|
|
15
|
+
| { kind: "turn_end" }
|
|
16
|
+
| { kind: "progress" }
|
|
17
|
+
| { kind: "message_to_main"; message: string; summary?: string };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A child's send_message {to: "main"} call needs no IPC of its own: the tool
|
|
21
|
+
* returns the message in its details, and the parent already reads every
|
|
22
|
+
* tool_execution_end in the child's event stream. This extracts it.
|
|
23
|
+
*/
|
|
24
|
+
export function toMainMessage(event: unknown): { message: string; summary?: string } | undefined {
|
|
25
|
+
const e = event as {
|
|
26
|
+
type?: string;
|
|
27
|
+
toolName?: string;
|
|
28
|
+
result?: { details?: { toMain?: boolean; message?: unknown; summary?: unknown } };
|
|
29
|
+
};
|
|
30
|
+
if (e?.type !== "tool_execution_end" || e.toolName !== "send_message") return undefined;
|
|
31
|
+
const details = e.result?.details;
|
|
32
|
+
if (!details?.toMain || typeof details.message !== "string") return undefined;
|
|
33
|
+
return { message: details.message, summary: typeof details.summary === "string" ? details.summary : undefined };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RpcTurnTracker {
|
|
37
|
+
toolCalls = 0;
|
|
38
|
+
/** What the child did this turn, for auto mode's return review. */
|
|
39
|
+
actions: ChildAction[] = [];
|
|
40
|
+
readonly usage: UsageTotals = emptyUsage();
|
|
41
|
+
/** Final assistant text of the turn in progress (or the last finished one). */
|
|
42
|
+
turnText = "";
|
|
43
|
+
/**
|
|
44
|
+
* Set when the turn's LAST assistant message ended with stopReason "error"
|
|
45
|
+
* (provider failure: auth, billing, rate limit) — the only place such a
|
|
46
|
+
* failure surfaces; stderr stays empty. A later successful assistant
|
|
47
|
+
* message (pi retried) clears it.
|
|
48
|
+
*/
|
|
49
|
+
providerError: string | undefined;
|
|
50
|
+
/** Every turn's final text, for task_output on a resident agent. */
|
|
51
|
+
transcript = "";
|
|
52
|
+
/** A prompt has been sent and its agent_end not yet seen. */
|
|
53
|
+
busy = false;
|
|
54
|
+
/**
|
|
55
|
+
* The child's agent loop is actually running (its agent_start seen). Between
|
|
56
|
+
* a prompt being sent and this flipping true, the child is still booting —
|
|
57
|
+
* a steer sent in that window would miss the turn.
|
|
58
|
+
*/
|
|
59
|
+
streaming = false;
|
|
60
|
+
|
|
61
|
+
beginTurn(): void {
|
|
62
|
+
this.busy = true;
|
|
63
|
+
this.turnText = "";
|
|
64
|
+
this.providerError = undefined;
|
|
65
|
+
// Per turn, not per session: the review that reads this judges the work of
|
|
66
|
+
// the turn that just finished.
|
|
67
|
+
this.actions = [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process(line: string): RpcAction | undefined {
|
|
71
|
+
if (!line.trim()) return undefined;
|
|
72
|
+
let event: {
|
|
73
|
+
type?: string;
|
|
74
|
+
id?: string;
|
|
75
|
+
message?: { role?: string; content?: unknown; usage?: unknown; stopReason?: string; errorMessage?: string };
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
event = JSON.parse(line);
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (event.type) {
|
|
84
|
+
case "extension_ui_request":
|
|
85
|
+
return typeof event.id === "string" ? { kind: "ui_request", id: event.id } : undefined;
|
|
86
|
+
case "agent_start":
|
|
87
|
+
this.streaming = true;
|
|
88
|
+
return { kind: "stream_start" };
|
|
89
|
+
case "tool_execution_start": {
|
|
90
|
+
this.toolCalls++;
|
|
91
|
+
const start = event as { toolName?: string; args?: unknown };
|
|
92
|
+
if (start.toolName) recordAction(this.actions, start.toolName, start.args);
|
|
93
|
+
return { kind: "progress" };
|
|
94
|
+
}
|
|
95
|
+
case "message_end": {
|
|
96
|
+
if (event.message?.role !== "assistant") return undefined;
|
|
97
|
+
addUsage(this.usage, event.message.usage);
|
|
98
|
+
this.providerError =
|
|
99
|
+
event.message.stopReason === "error" ? event.message.errorMessage || "unknown provider error" : undefined;
|
|
100
|
+
const blocks = Array.isArray(event.message.content) ? event.message.content : [];
|
|
101
|
+
const text = blocks
|
|
102
|
+
.filter((b): b is { type: string; text: string } => (b as { type?: string }).type === "text")
|
|
103
|
+
.map((b) => b.text)
|
|
104
|
+
.join("");
|
|
105
|
+
if (text.trim()) this.turnText = text;
|
|
106
|
+
return { kind: "progress" };
|
|
107
|
+
}
|
|
108
|
+
case "tool_execution_end": {
|
|
109
|
+
const toMain = toMainMessage(event);
|
|
110
|
+
return toMain ? { kind: "message_to_main", ...toMain } : undefined;
|
|
111
|
+
}
|
|
112
|
+
case "agent_end":
|
|
113
|
+
this.busy = false;
|
|
114
|
+
this.streaming = false;
|
|
115
|
+
if (this.turnText.trim()) {
|
|
116
|
+
this.transcript = this.transcript ? `${this.transcript}\n\n---\n\n${this.turnText}` : this.turnText;
|
|
117
|
+
}
|
|
118
|
+
return { kind: "turn_end" };
|
|
119
|
+
default:
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named-run bookkeeping for subagents — what send_message resolves against.
|
|
3
|
+
*
|
|
4
|
+
* Every persisted child run gets a name (explicit or `<agent>-<n>`) and a task
|
|
5
|
+
* id. Records ride in tool-result details, so the map is reconstructable from
|
|
6
|
+
* the session branch after a resume; the session files themselves live on disk
|
|
7
|
+
* under the per-run session dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export interface AgentRunRecord {
|
|
14
|
+
name: string;
|
|
15
|
+
agent: string;
|
|
16
|
+
taskId: string;
|
|
17
|
+
/** Directory the run's session file was created under (searched lazily). */
|
|
18
|
+
sessionSearchDir: string;
|
|
19
|
+
/** Resolved session file, once known. */
|
|
20
|
+
sessionFile?: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
thinking?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `<agent>-<n>` with the lowest n not already taken. */
|
|
27
|
+
export function nextRunName(existing: Iterable<string>, agent: string): string {
|
|
28
|
+
const taken = new Set(existing);
|
|
29
|
+
for (let n = 1; ; n++) {
|
|
30
|
+
const candidate = `${agent}-${n}`;
|
|
31
|
+
if (!taken.has(candidate)) return candidate;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Newest .jsonl under `dir` (recursive) — the child session pi created there. */
|
|
36
|
+
export function findSessionFile(dir: string): string | undefined {
|
|
37
|
+
let newest: { path: string; mtime: number } | undefined;
|
|
38
|
+
const walk = (current: string) => {
|
|
39
|
+
let entries: string[];
|
|
40
|
+
try {
|
|
41
|
+
entries = readdirSync(current);
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const path = join(current, entry);
|
|
47
|
+
let stats;
|
|
48
|
+
try {
|
|
49
|
+
stats = statSync(path);
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (stats.isDirectory()) walk(path);
|
|
54
|
+
else if (entry.endsWith(".jsonl") && (!newest || stats.mtimeMs > newest.mtime)) {
|
|
55
|
+
newest = { path, mtime: stats.mtimeMs };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(dir);
|
|
60
|
+
return newest?.path;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class RunRegistry {
|
|
64
|
+
private byName = new Map<string, AgentRunRecord>();
|
|
65
|
+
private byId = new Map<string, AgentRunRecord>();
|
|
66
|
+
|
|
67
|
+
/** Latest wins per name, matching Claude Code's semantics. */
|
|
68
|
+
add(record: AgentRunRecord): void {
|
|
69
|
+
this.byName.set(record.name, record);
|
|
70
|
+
this.byId.set(record.taskId, record);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
names(): string[] {
|
|
74
|
+
return [...this.byName.keys()];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Resolve by name, exact task id, or unique task-id prefix (3+ chars). */
|
|
78
|
+
resolve(ref: string): AgentRunRecord | undefined {
|
|
79
|
+
const named = this.byName.get(ref) ?? this.byId.get(ref);
|
|
80
|
+
if (named) return named;
|
|
81
|
+
// An empty or near-empty ref would prefix-match whatever run happens to
|
|
82
|
+
// exist ("".startsWith("") is true) and deliver to the wrong target.
|
|
83
|
+
if (ref.length < 3) return undefined;
|
|
84
|
+
const matches = [...this.byId.values()].filter((r) => r.taskId.startsWith(ref));
|
|
85
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Ensure the record's session file is resolved, searching its dir if needed. */
|
|
89
|
+
sessionFileFor(record: AgentRunRecord): string | undefined {
|
|
90
|
+
record.sessionFile ??= findSessionFile(record.sessionSearchDir);
|
|
91
|
+
return record.sessionFile;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token/cost accounting for child-process subagent runs.
|
|
3
|
+
*
|
|
4
|
+
* A child run in `--mode json` emits one `message_end` per assistant message,
|
|
5
|
+
* each carrying a pi-ai `Usage` for that API call; summing them gives the
|
|
6
|
+
* run's totals (the in-process equivalent is `session.getSessionStats()`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface UsageTotals {
|
|
10
|
+
input: number;
|
|
11
|
+
output: number;
|
|
12
|
+
cacheRead: number;
|
|
13
|
+
cacheWrite: number;
|
|
14
|
+
total: number;
|
|
15
|
+
cost: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function emptyUsage(): UsageTotals {
|
|
19
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Add one assistant message's `usage` (untrusted JSONL) into `totals`. */
|
|
23
|
+
export function addUsage(totals: UsageTotals, usage: unknown): void {
|
|
24
|
+
if (!usage || typeof usage !== "object") return;
|
|
25
|
+
const u = usage as Record<string, unknown>;
|
|
26
|
+
const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
27
|
+
totals.input += num(u.input);
|
|
28
|
+
totals.output += num(u.output);
|
|
29
|
+
totals.cacheRead += num(u.cacheRead);
|
|
30
|
+
totals.cacheWrite += num(u.cacheWrite);
|
|
31
|
+
totals.total += num(u.totalTokens);
|
|
32
|
+
const cost = u.cost as Record<string, unknown> | undefined;
|
|
33
|
+
totals.cost += num(cost?.total);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatTokenCount(n: number): string {
|
|
37
|
+
if (n < 1000) return String(n);
|
|
38
|
+
const k = n / 1000;
|
|
39
|
+
return `${k >= 100 ? Math.round(k) : k.toFixed(1)}k`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Compact stats suffix, e.g. "45.3k tokens · $0.0421". Empty when nothing was recorded. */
|
|
43
|
+
export function formatUsage(totals: UsageTotals): string {
|
|
44
|
+
if (totals.total <= 0) return "";
|
|
45
|
+
const parts = [`${formatTokenCount(totals.total)} tokens`];
|
|
46
|
+
if (totals.cost > 0) parts.push(`$${totals.cost.toFixed(4)}`);
|
|
47
|
+
return parts.join(" · ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A run's "N tools · <usage>" line; the usage suffix is dropped when empty. */
|
|
51
|
+
export function formatStats(toolCalls: number, totals: UsageTotals): string {
|
|
52
|
+
return [`${toolCalls} tools`, formatUsage(totals)].filter(Boolean).join(" · ");
|
|
53
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree isolation for subagent runs.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's `isolation: "worktree"` gives an agent its own checkout so
|
|
5
|
+
* parallel agents editing files cannot collide, and removes it again if the agent
|
|
6
|
+
* changed nothing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { mkdtempSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
|
|
15
|
+
const run = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
export interface Worktree {
|
|
18
|
+
path: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function git(args: string[], cwd: string): Promise<string> {
|
|
23
|
+
const { stdout } = await run("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
24
|
+
return stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function isGitRepo(cwd: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
return (await git(["rev-parse", "--is-inside-work-tree"], cwd)) === "true";
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Creates a detached worktree at the current HEAD. */
|
|
36
|
+
export async function createWorktree(cwd: string, label: string): Promise<Worktree> {
|
|
37
|
+
const safeLabel = label.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 24) || "agent";
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), `cc-wt-${safeLabel}-`));
|
|
39
|
+
const path = join(dir, "tree");
|
|
40
|
+
const branch = `cc-subagent/${safeLabel}-${Date.now().toString(36)}`;
|
|
41
|
+
await git(["worktree", "add", "-b", branch, path, "HEAD"], cwd);
|
|
42
|
+
return { path, branch };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function worktreeHasChanges(worktree: Worktree): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
const status = await git(["status", "--porcelain"], worktree.path);
|
|
48
|
+
return status.length > 0;
|
|
49
|
+
} catch {
|
|
50
|
+
// If status fails, assume there is something worth keeping.
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Removes the worktree when the agent left it untouched; otherwise keeps it so
|
|
57
|
+
* the caller can inspect or merge the work, and returns false.
|
|
58
|
+
*/
|
|
59
|
+
export async function cleanupWorktree(cwd: string, worktree: Worktree): Promise<boolean> {
|
|
60
|
+
if (await worktreeHasChanges(worktree)) return false;
|
|
61
|
+
try {
|
|
62
|
+
await git(["worktree", "remove", "--force", worktree.path], cwd);
|
|
63
|
+
await git(["branch", "-D", worktree.branch], cwd);
|
|
64
|
+
} catch {
|
|
65
|
+
// Leave it behind rather than failing the run.
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|