@arhen/pi-core-subagent 1.3.52 → 1.3.54
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/README.md +3 -0
- package/package.json +1 -1
- package/src/format.ts +18 -11
- package/src/index.ts +32 -1
- package/src/manager.ts +125 -5
- package/src/schemas.ts +13 -0
- package/src/worktree.ts +27 -0
package/README.md
CHANGED
|
@@ -282,6 +282,8 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
282
282
|
|
|
283
283
|
**Steering a running child:** while a background run is active the leader stays responsive, and you can push a message into a live child's session mid-run with `steer_subagent` — e.g. `steer_subagent({ runId, taskId, message: "Ignore tests/, only audit runtime deps" })`. The message queues as a steer if the child is mid-turn and lands at its next model boundary. Omit `taskId` to steer every still-running task in the run. Combined with `notifyPerTask`, this makes a background run feel like a live team you can redirect, not a fire-and-forget blob.
|
|
284
284
|
|
|
285
|
+
**Resuming a failed child:** a child that dies mid-work (provider rate limit, timeout, network error) keeps its session JSONL and its worktree branch. `resume_subagent({ runId, taskId, model?: "openai/gpt-5", message? })` reopens that session with full context, re-attaches the branch, and prompts it to recap and continue — no respawn, no lost tokens. `model` swaps provider when the original one is exhausted. Refused for tasks that never started (no session file); those you respawn. Wait for the run to settle before resuming (the tool tells you if it hasn't).
|
|
286
|
+
|
|
285
287
|
## Tools
|
|
286
288
|
|
|
287
289
|
| Tool | Purpose |
|
|
@@ -292,6 +294,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
292
294
|
| `await_subagent` | block until a run finishes (optional `timeoutMs`) |
|
|
293
295
|
| `reply_subagent` | answer a child's `ask_parent` question |
|
|
294
296
|
| `steer_subagent` | inject a steering message into a running child's session (queues as steer if mid-turn; lands at its next model boundary) |
|
|
297
|
+
| `resume_subagent` | revive a failed/aborted task in its original session (context + branch preserved); optional `model` swap and custom `message` |
|
|
295
298
|
| `subagent_cancel` | abort a running/queued run |
|
|
296
299
|
|
|
297
300
|
### Per-task fields
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.54",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/format.ts
CHANGED
|
@@ -114,7 +114,8 @@ export function compactLines(run: RunSnapshot): string[] {
|
|
|
114
114
|
if (run.tasks.length > MAX_TASKS) lines.push(`… +${run.tasks.length - MAX_TASKS} more`);
|
|
115
115
|
return lines;
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
// header + 4 task rows; live tasks are ordered first, so the cap only ever hides finished work
|
|
118
|
+
const WIDGET_MAX_LINES = 5;
|
|
118
119
|
|
|
119
120
|
export class SubagentsWidget implements Component {
|
|
120
121
|
constructor(
|
|
@@ -139,17 +140,21 @@ export class SubagentsWidget implements Component {
|
|
|
139
140
|
),
|
|
140
141
|
];
|
|
141
142
|
const budget = WIDGET_MAX_LINES - 1;
|
|
143
|
+
// live tasks first so the budget never hides work in progress
|
|
144
|
+
const all = runs.flatMap((run) => run.tasks);
|
|
145
|
+
const ordered = [
|
|
146
|
+
...all.filter((t) => !TERMINAL.includes(t.status)),
|
|
147
|
+
...all.filter((t) => TERMINAL.includes(t.status)),
|
|
148
|
+
];
|
|
142
149
|
let shown = 0;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const activity = task.lastActivity ? `${this.theme.fg("dim", `→ ${task.lastActivity}`)} · ` : "";
|
|
150
|
+
for (const task of ordered) {
|
|
151
|
+
if (shown >= budget) break;
|
|
152
|
+
shown += 1;
|
|
153
|
+
const activity = task.lastActivity ? `${this.theme.fg("dim", `→ ${task.lastActivity}`)} · ` : "";
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
155
|
+
lines.push(
|
|
156
|
+
truncateToWidth(`${this.theme.fg("dim", "├─")} ${themedTaskLine(task, this.theme, activity)}`, width, "…"),
|
|
157
|
+
);
|
|
153
158
|
}
|
|
154
159
|
const hidden = total - shown;
|
|
155
160
|
if (hidden > 0) {
|
|
@@ -228,7 +233,9 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
228
233
|
`Goal: ${goal}${src}${swap}${tools}`,
|
|
229
234
|
isStartupFailure(task, kind)
|
|
230
235
|
? "Never started — stop and diagnose before spawning anything else: a config-level error (model, plan, auth, agent file) fails identically on every respawn."
|
|
231
|
-
:
|
|
236
|
+
: kind === "completed"
|
|
237
|
+
? `Use subagent_result(runId: "${run.id}", taskId: "${task.id}") for full output.`
|
|
238
|
+
: `Session file kept — resume_subagent(runId: "${run.id}", taskId: "${task.id}", model?: ...) revives it with full context. subagent_result for what it produced so far.`,
|
|
232
239
|
].join("\n");
|
|
233
240
|
}
|
|
234
241
|
export function makeNotice(run: RunSnapshot, kind: string): string {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
AwaitParam,
|
|
9
9
|
ReplyParam,
|
|
10
10
|
ResultParam,
|
|
11
|
+
ResumeParam,
|
|
11
12
|
RunIdParam,
|
|
12
13
|
SteerParam,
|
|
13
14
|
SubagentParams,
|
|
@@ -139,7 +140,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
139
140
|
"Define each agent inline: invented name, focused system prompt, read-only by default (write:true to edit). A matched agent file takes over (see description); matching is by description, not name — name the agent whatever fits the goal.",
|
|
140
141
|
"Right after spawning, call subagent_status(runId) ONCE before any other work — a child that died on spawn (or never started) is invisible until far later otherwise. If it shows a task failed/never started, fix or respawn immediately.",
|
|
141
142
|
"Never block with nothing to do: if you have no work left after spawning, end your turn — completion notifies you and wakes a fresh turn with the results. await_subagent/autoAwait while idle only burns time and tokens.",
|
|
143
|
+
"A failed task interrupts you immediately as a steering message — handle it in the same turn (resume, swap model, re-dispatch) instead of finishing the plan on a broken intermediate result. Completes and aborts queue as follow-ups.",
|
|
142
144
|
"autoAwait:true only when this SAME turn must consume the result immediately. await_subagent is for syncing with your own parallel work — not the default follow-up to a spawn.",
|
|
145
|
+
"A task that failed mid-work (provider error, rate limit, timeout) keeps its session file and branch: resume_subagent(runId, taskId, model?) revives it with full context — prefer that over respawning. Respawn only when it never started (no session file).",
|
|
143
146
|
],
|
|
144
147
|
parameters: SubagentParams,
|
|
145
148
|
executionMode: "parallel",
|
|
@@ -182,7 +185,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
182
185
|
content: [
|
|
183
186
|
{
|
|
184
187
|
type: "text",
|
|
185
|
-
text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nNext: call subagent_status("${details.run.id}") now to confirm the tasks actually started before doing anything else.\nAfter that, completion will notify you — if you have no other work, end your turn instead of waiting.\nOther tools: subagent_result / reply_subagent / steer_subagent / subagent_cancel.`,
|
|
188
|
+
text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nNext: call subagent_status("${details.run.id}") now to confirm the tasks actually started before doing anything else.\nAfter that, completion will notify you — if you have no other work, end your turn instead of waiting.\nOther tools: subagent_result / reply_subagent / steer_subagent / resume_subagent / subagent_cancel.`,
|
|
186
189
|
},
|
|
187
190
|
],
|
|
188
191
|
details,
|
|
@@ -373,6 +376,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
373
376
|
},
|
|
374
377
|
});
|
|
375
378
|
|
|
379
|
+
pi.registerTool<typeof ResumeParam, { run?: RunSnapshot }>({
|
|
380
|
+
name: "resume_subagent",
|
|
381
|
+
label: "Resume Subagent",
|
|
382
|
+
description:
|
|
383
|
+
"Revive a failed/aborted task in its original session (full context + worktree branch preserved). Optional `model` swaps provider (e.g. after a rate limit); optional `message` replaces the default 'recap and continue' prompt. Refuses tasks that never started — respawn those.",
|
|
384
|
+
parameters: ResumeParam,
|
|
385
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
386
|
+
const { runId, taskId, message, model } = params as {
|
|
387
|
+
runId: string;
|
|
388
|
+
taskId: string;
|
|
389
|
+
message?: string;
|
|
390
|
+
model?: string;
|
|
391
|
+
};
|
|
392
|
+
const res = manager.resumeTask(runId, taskId, ctx, { message, model });
|
|
393
|
+
if (!res.ok) return { content: [{ type: "text", text: res.reason }], isError: true, details: {} };
|
|
394
|
+
const run = manager.getRun(runId);
|
|
395
|
+
return {
|
|
396
|
+
content: [
|
|
397
|
+
{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: `Resumed ${runId}/${taskId} (${res.task.agent})${model ? ` on ${model}` : ""} from ${res.task.sessionFile}${res.task.branch ? `, branch ${res.task.branch}` : ""}.\nNext: subagent_status("${runId}") to confirm it is running; completion will notify you.`,
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
details: { run: run ? cloneRun(run) : undefined },
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
376
407
|
pi.registerTool<typeof RunIdParam, { aborted?: number }>({
|
|
377
408
|
name: "subagent_cancel",
|
|
378
409
|
label: "Subagent Cancel",
|
package/src/manager.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type UsageStats,
|
|
45
45
|
} from "./types.ts";
|
|
46
46
|
import {
|
|
47
|
+
attachWorktree,
|
|
47
48
|
branchDiff,
|
|
48
49
|
claimWorktree,
|
|
49
50
|
cleanupMerged,
|
|
@@ -226,6 +227,12 @@ export function validateThinking(model: Model<Api> | undefined, level: string |
|
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
interface ResumeInput {
|
|
231
|
+
sessionFile: string;
|
|
232
|
+
branch?: string;
|
|
233
|
+
message: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
229
236
|
interface ChildEventState {
|
|
230
237
|
pendingFailure?: ReturnType<typeof classifyFailure>;
|
|
231
238
|
failChildEnd?: (error: Error) => void;
|
|
@@ -430,7 +437,9 @@ export class SubagentManager {
|
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
private deliverMode(kind: string, task: TaskSnapshot): "steer" | "followUp" {
|
|
433
|
-
|
|
440
|
+
// failures interrupt the leader's turn: it must decide immediately (resume, swap model, respawn)
|
|
441
|
+
// instead of finding out after its own turn ended. Aborts are user-initiated, so they queue.
|
|
442
|
+
return kind === "failed" || isStartupFailure(task, kind) ? "steer" : "followUp";
|
|
434
443
|
}
|
|
435
444
|
|
|
436
445
|
private notifyTask(run: RunSnapshot, task: TaskSnapshot, kind: "completed" | "failed" | "aborted"): void {
|
|
@@ -463,7 +472,7 @@ export class SubagentManager {
|
|
|
463
472
|
? `A subagent is asking you a question (task ${extra?.taskId}): ${extra?.question ?? ""}\nReply with reply_subagent(runId: "${run.id}", taskId: "${extra?.taskId}", message: ...).`
|
|
464
473
|
: makeNotice(run, kind);
|
|
465
474
|
try {
|
|
466
|
-
this.pi.sendUserMessage(body, { deliverAs: "followUp" });
|
|
475
|
+
this.pi.sendUserMessage(body, { deliverAs: kind === "failed" ? "steer" : "followUp" });
|
|
467
476
|
} catch {}
|
|
468
477
|
this.emit("subagent:notification", { runId: run.id, kind, body });
|
|
469
478
|
}
|
|
@@ -709,6 +718,7 @@ export class SubagentManager {
|
|
|
709
718
|
ctx: ExtensionContext,
|
|
710
719
|
signal: AbortSignal | undefined,
|
|
711
720
|
onUpdate?: (partial: any) => void,
|
|
721
|
+
resume?: ResumeInput,
|
|
712
722
|
): Promise<void> {
|
|
713
723
|
if (TERMINAL.includes(task.status)) return;
|
|
714
724
|
|
|
@@ -757,7 +767,14 @@ export class SubagentManager {
|
|
|
757
767
|
|
|
758
768
|
let wt: Worktree | undefined;
|
|
759
769
|
let isolationReason: string | undefined;
|
|
760
|
-
if (canWrite) {
|
|
770
|
+
if (canWrite && resume?.branch) {
|
|
771
|
+
try {
|
|
772
|
+
wt = attachWorktree(task.cwd, resume.branch);
|
|
773
|
+
if (!wt) isolationReason = `branch ${resume.branch} no longer exists`;
|
|
774
|
+
} catch (err) {
|
|
775
|
+
isolationReason = `git worktree add failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
776
|
+
}
|
|
777
|
+
} else if (canWrite) {
|
|
761
778
|
try {
|
|
762
779
|
const upstream = (task.needs ?? [])
|
|
763
780
|
.map((id) => run.tasks.find((t) => t.id === id))
|
|
@@ -850,7 +867,9 @@ export class SubagentManager {
|
|
|
850
867
|
agentDir: getAgentDir(),
|
|
851
868
|
modelRuntime: await createChildModelRuntime(ctx),
|
|
852
869
|
resourceLoader: loader,
|
|
853
|
-
sessionManager:
|
|
870
|
+
sessionManager: resume
|
|
871
|
+
? SessionManager.open(resume.sessionFile, undefined, childCwd)
|
|
872
|
+
: SessionManager.create(childCwd, undefined, { parentSession: getParentSessionFile(ctx) }),
|
|
854
873
|
model,
|
|
855
874
|
thinkingLevel: thinking as ThinkingLevel | undefined,
|
|
856
875
|
tools,
|
|
@@ -906,7 +925,7 @@ export class SubagentManager {
|
|
|
906
925
|
});
|
|
907
926
|
|
|
908
927
|
const maxRuntimeMs = input.maxRuntimeMs ?? (this.autoLimit ? DEFAULT_RUNTIME_MS : UNLIMITED_RUNTIME_MS);
|
|
909
|
-
const promptPromise = child.prompt(task.task, { source: "extension" });
|
|
928
|
+
const promptPromise = child.prompt(resume?.message ?? task.task, { source: "extension" });
|
|
910
929
|
const races: Promise<unknown>[] = [promptPromise, childFailurePromise, childEndPromise];
|
|
911
930
|
if (maxRuntimeMs > 0) {
|
|
912
931
|
races.push(
|
|
@@ -1304,6 +1323,107 @@ export class SubagentManager {
|
|
|
1304
1323
|
return { run: cloneRun(run) };
|
|
1305
1324
|
}
|
|
1306
1325
|
|
|
1326
|
+
resumeTask(
|
|
1327
|
+
runId: string,
|
|
1328
|
+
taskId: string,
|
|
1329
|
+
ctx: ExtensionContext,
|
|
1330
|
+
opts: { message?: string; model?: string } = {},
|
|
1331
|
+
): { ok: true; task: TaskSnapshot } | { ok: false; reason: string } {
|
|
1332
|
+
const run = this.runs.get(runId);
|
|
1333
|
+
const task = run?.tasks.find((t) => t.id === taskId);
|
|
1334
|
+
if (!run || !task) return { ok: false, reason: `Unknown ${runId}/${taskId}.` };
|
|
1335
|
+
if (!TERMINAL.includes(task.status))
|
|
1336
|
+
return { ok: false, reason: `${taskId} is still ${task.status} — use steer_subagent.` };
|
|
1337
|
+
if (task.status === "completed") return { ok: false, reason: `${taskId} completed — spawn a new task instead.` };
|
|
1338
|
+
if (!task.sessionFile || !existsSync(task.sessionFile)) {
|
|
1339
|
+
return { ok: false, reason: `${taskId} has no session file to resume (never started) — respawn it.` };
|
|
1340
|
+
}
|
|
1341
|
+
if (this.liveChildren.has(`${runId}:${taskId}`)) return { ok: false, reason: `${taskId} is already live.` };
|
|
1342
|
+
// ponytail: resume only into a settled run — executeTasks' final sweep would abort a task revived mid-run. Upgrade: make the sweep skip tasks with a live child.
|
|
1343
|
+
if (!TERMINAL.includes(run.status)) {
|
|
1344
|
+
return { ok: false, reason: `Run ${runId} is still ${run.status} — wait for it to settle before resuming.` };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const tools = task.tools?.filter((t) => !(CHILD_TALK_TOOLS as readonly string[]).includes(t));
|
|
1348
|
+
const write = tools?.some((t) => WRITE_CAPABLE.includes(t)) ?? false;
|
|
1349
|
+
const input: TaskInput = {
|
|
1350
|
+
id: task.id,
|
|
1351
|
+
agent: task.agent,
|
|
1352
|
+
task: task.task,
|
|
1353
|
+
cwd: task.cwd,
|
|
1354
|
+
write,
|
|
1355
|
+
tools: tools?.length ? tools : undefined,
|
|
1356
|
+
model: opts.model ?? task.model,
|
|
1357
|
+
thinking: task.thinking as TaskInput["thinking"],
|
|
1358
|
+
needs: task.needs,
|
|
1359
|
+
};
|
|
1360
|
+
const resume: ResumeInput = {
|
|
1361
|
+
sessionFile: task.sessionFile,
|
|
1362
|
+
branch: task.branch,
|
|
1363
|
+
message:
|
|
1364
|
+
opts.message?.trim() ||
|
|
1365
|
+
`Your previous turn ended with an error (${task.error ?? "unknown"}). Resume where you left off: briefly recap what you already did and what remains, then continue and finish the original task.`,
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
this.cleared = false;
|
|
1369
|
+
this.turnActivity = true;
|
|
1370
|
+
Object.assign(task, {
|
|
1371
|
+
status: "queued" as TaskStatus,
|
|
1372
|
+
error: undefined,
|
|
1373
|
+
endedAt: undefined,
|
|
1374
|
+
finalText: undefined,
|
|
1375
|
+
notifiedParent: false,
|
|
1376
|
+
diffStat: undefined,
|
|
1377
|
+
changedFiles: undefined,
|
|
1378
|
+
worktreeError: undefined,
|
|
1379
|
+
});
|
|
1380
|
+
run.status = "running";
|
|
1381
|
+
run.endedAt = undefined;
|
|
1382
|
+
run.awaited = false;
|
|
1383
|
+
this.settlers.set(run.id, true);
|
|
1384
|
+
this.runControllers.set(run.id, new AbortController());
|
|
1385
|
+
for (const t of run.tasks) this.mailboxes.open(`${run.id}:${t.id}`);
|
|
1386
|
+
this.updateRun(run, ctx);
|
|
1387
|
+
this.emit("subagent:task-resumed", { runId: run.id, taskId: task.id });
|
|
1388
|
+
|
|
1389
|
+
void this.runChild(run, task, input, task.task, ctx, undefined, undefined, resume)
|
|
1390
|
+
.catch((err) => {
|
|
1391
|
+
if (!TERMINAL.includes(task.status)) {
|
|
1392
|
+
this.updateTask(
|
|
1393
|
+
run,
|
|
1394
|
+
task,
|
|
1395
|
+
{ status: "failed", error: err instanceof Error ? err.message : String(err), endedAt: Date.now() },
|
|
1396
|
+
ctx,
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
})
|
|
1400
|
+
.then(() => {
|
|
1401
|
+
if (run.notifyPerTask) this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
1402
|
+
this.finishRunIfSettled(run, ctx);
|
|
1403
|
+
});
|
|
1404
|
+
return { ok: true, task };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
private finishRunIfSettled(run: RunSnapshot, ctx: ExtensionContext): void {
|
|
1408
|
+
if (run.tasks.some((t) => !TERMINAL.includes(t.status))) return;
|
|
1409
|
+
const failed = run.tasks.some((t) => t.status === "failed");
|
|
1410
|
+
const aborted = run.tasks.some((t) => t.status === "aborted");
|
|
1411
|
+
run.status = aborted ? "aborted" : failed ? "failed" : "completed";
|
|
1412
|
+
run.endedAt = Date.now();
|
|
1413
|
+
this.flushWidget(run, ctx);
|
|
1414
|
+
this.emit("subagent:run-completed", {
|
|
1415
|
+
runId: run.id,
|
|
1416
|
+
status: run.status,
|
|
1417
|
+
run: cloneRun(run),
|
|
1418
|
+
aggregateUsage: run.aggregateUsage,
|
|
1419
|
+
});
|
|
1420
|
+
this.settleRun(run.id, run);
|
|
1421
|
+
this.runControllers.delete(run.id);
|
|
1422
|
+
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
1423
|
+
this.persist(ctx);
|
|
1424
|
+
this.notifyParent(run, run.status === "completed" ? "completed" : run.status === "aborted" ? "aborted" : "failed");
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1307
1427
|
steerTask(runId: string, taskId: string | undefined, message: string): boolean {
|
|
1308
1428
|
const run = this.runs.get(runId);
|
|
1309
1429
|
if (!run) return false;
|
package/src/schemas.ts
CHANGED
|
@@ -77,6 +77,19 @@ export const ReplyParam = Type.Object({
|
|
|
77
77
|
taskId: Type.String(),
|
|
78
78
|
message: Type.String({ description: "Answer for the child" }),
|
|
79
79
|
});
|
|
80
|
+
export const ResumeParam = Type.Object({
|
|
81
|
+
runId: Type.String(),
|
|
82
|
+
taskId: Type.String({ description: "Failed/aborted task to revive" }),
|
|
83
|
+
message: Type.Optional(
|
|
84
|
+
Type.String({ description: "Prompt delivered on resume (default: recap state, then continue the original task)" }),
|
|
85
|
+
),
|
|
86
|
+
model: Type.Optional(
|
|
87
|
+
Type.String({
|
|
88
|
+
description:
|
|
89
|
+
"Model override for the resumed session (provider/model-id) — use when the original provider is rate-limited",
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
});
|
|
80
93
|
export const SteerParam = Type.Object({
|
|
81
94
|
runId: Type.String(),
|
|
82
95
|
taskId: Type.Optional(Type.String({ description: "Specific task id; defaults to all still-running tasks" })),
|
package/src/worktree.ts
CHANGED
|
@@ -98,6 +98,33 @@ export function createWorktree(cwd: string, runId: string, taskId: string, baseR
|
|
|
98
98
|
return { root, path, branch, base };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export function attachWorktree(cwd: string, branch: string): Worktree | undefined {
|
|
102
|
+
if (!branch.startsWith(BRANCH_PREFIX)) return undefined;
|
|
103
|
+
const root = repoRoot(cwd);
|
|
104
|
+
if (!root) return undefined;
|
|
105
|
+
const container = subagentsDir(root);
|
|
106
|
+
if (!container) return undefined;
|
|
107
|
+
if (!gitOk(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`])) return undefined;
|
|
108
|
+
const path = join(container, branch.slice(BRANCH_PREFIX.length));
|
|
109
|
+
if (!worktreePaths(root)?.some((p) => samePath(p, path))) {
|
|
110
|
+
if (existsSync(path)) rmSync(path, { recursive: true, force: true });
|
|
111
|
+
git(root, ["worktree", "add", path, branch]);
|
|
112
|
+
}
|
|
113
|
+
let base: string;
|
|
114
|
+
try {
|
|
115
|
+
base = git(root, ["merge-base", "HEAD", branch]);
|
|
116
|
+
} catch {
|
|
117
|
+
base = git(root, ["rev-parse", "HEAD"]);
|
|
118
|
+
}
|
|
119
|
+
const nm = join(root, "node_modules");
|
|
120
|
+
if (existsSync(nm) && !existsSync(join(path, "node_modules"))) {
|
|
121
|
+
try {
|
|
122
|
+
symlinkSync(nm, join(path, "node_modules"));
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
return { root, path, branch, base };
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
export function commitWorktree(wt: Worktree, message: string): "committed" | "empty" {
|
|
102
129
|
return commitIn(wt.path, message, wt.branch);
|
|
103
130
|
}
|