@computesdk/pi-loops 1.0.4 → 1.0.5
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/extensions/loop.ts +48 -25
- package/package.json +1 -1
package/extensions/loop.ts
CHANGED
|
@@ -123,6 +123,8 @@ export type ToolDef<P = any> = {
|
|
|
123
123
|
outputSchema?: any;
|
|
124
124
|
/** One line for the default system prompt's "Available tools" section: without it Pi omits the tool there. */
|
|
125
125
|
promptSnippet?: string;
|
|
126
|
+
/** Rules for the Guidelines section, shown only while this tool is active. Workflow belongs here, not in the snippet. */
|
|
127
|
+
promptGuidelines?: string[];
|
|
126
128
|
/** `model-only` for the tools that ask the user or orchestrate: never callable from scripts. */
|
|
127
129
|
exposure?: "direct" | "model-only" | "codemode" | "deferred" | "hidden";
|
|
128
130
|
/** "sequential" for tools that share in-memory state, so one message's calls cannot interleave. */
|
|
@@ -133,6 +135,8 @@ export type ToolDef<P = any> = {
|
|
|
133
135
|
renderCall?: (args: any, theme: any) => any;
|
|
134
136
|
renderResult?: (result: any, options: { expanded: boolean; isPartial: boolean }, theme: any) => any;
|
|
135
137
|
};
|
|
138
|
+
/** Resolve a model spec against the registry captured while the ctx was alive. */
|
|
139
|
+
export type ModelPicker = (spec?: string | null) => Model;
|
|
136
140
|
/** A candidate for the judge: a checked-out directory at a commit. */
|
|
137
141
|
export type Candidate = { label: string; dir: string; commit: string; fresh?: boolean };
|
|
138
142
|
/** One round of a run. */
|
|
@@ -366,16 +370,16 @@ export async function stopAll() { shuttingDown = true; const all = [...active];
|
|
|
366
370
|
* *extension* adds (rather than one built into Pi) is unknown inside it and its credential cannot be resolved. Say that
|
|
367
371
|
* instead of pi's "No API key found", which points at /login and sends people the wrong way.
|
|
368
372
|
*/
|
|
369
|
-
function explainAuth(e: unknown, role: string, spec: string | null,
|
|
373
|
+
function explainAuth(e: unknown, role: string, spec: string | null, registry?: ModelRegistryLike): unknown {
|
|
370
374
|
const provider = errText(e).match(/No API key found for ([\w.-]+)/)?.[1]?.replace(/\.+$/, ""); // pi ends the sentence with a period
|
|
371
375
|
if (!provider) return e;
|
|
372
376
|
// the host can see a model on that provider (so this session is missing something the host has), not a bare missing login
|
|
373
|
-
const hostKnows = (
|
|
377
|
+
const hostKnows = (registry?.getAvailable?.() ?? []).some((m) => (m as { provider?: string }).provider === provider);
|
|
374
378
|
if (!hostKnows) return e;
|
|
375
379
|
return new Error(`the ${role} cannot run on ${spec ?? provider}: "${provider}" is a Pi extension's provider, and a one-shot ${role} session loads no extensions, so it has no credentials there — give the ${role} a provider built into Pi (attempts can still use ${provider}).`, { cause: e });
|
|
376
380
|
}
|
|
377
381
|
|
|
378
|
-
async function oneShot({ role, model, content, cwd,
|
|
382
|
+
async function oneShot({ role, model, content, cwd, pick, registry, settings, meter, signal }: { role: string; model?: string | null; content: any; cwd: string; pick: ModelPicker; registry?: ModelRegistryLike; settings: LoopSettings; meter?: Meter; signal?: AbortSignal }) {
|
|
379
383
|
const agentDir = getAgentDir();
|
|
380
384
|
const spec = model ?? settings.models[role] ?? null;
|
|
381
385
|
const agent = loadAgent(role, { cwd, agentDir });
|
|
@@ -388,15 +392,15 @@ async function oneShot({ role, model, content, cwd, ctx, settings, meter, signal
|
|
|
388
392
|
// "no extensions" would also drop the *providers* extensions add (anything not built into Pi), and an unregistered
|
|
389
393
|
// provider has no credentials. Copy the host's registrations over: config only, no extension code runs, still no tools.
|
|
390
394
|
const known = new Set(services.modelRuntime.getRegisteredProviderIds());
|
|
391
|
-
for (const id of
|
|
395
|
+
for (const id of registry?.getRegisteredProviderIds?.() ?? []) {
|
|
392
396
|
if (known.has(id)) continue;
|
|
393
|
-
const config =
|
|
397
|
+
const config = registry?.getRegisteredProviderConfig?.(id);
|
|
394
398
|
if (config) { services.modelRuntime.registerProvider(id, config as any); continue; }
|
|
395
|
-
const native =
|
|
399
|
+
const native = registry?.getRegisteredNativeProvider?.(id); // a provider built with createProvider, not config
|
|
396
400
|
if (native) services.modelRuntime.registerNativeProvider(native as any);
|
|
397
401
|
}
|
|
398
402
|
// creating the session resolves the credential too, so it belongs inside this try: that is where an unusable provider shows up
|
|
399
|
-
const { session: created } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(cwd), model:
|
|
403
|
+
const { session: created } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(cwd), model: pick(spec ?? agent.model), noTools: "all" });
|
|
400
404
|
session = track(created);
|
|
401
405
|
const { text, images } = flattenParts(content);
|
|
402
406
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -409,7 +413,7 @@ async function oneShot({ role, model, content, cwd, ctx, settings, meter, signal
|
|
|
409
413
|
if (last?.stopReason === "error") throw new Error(last.errorMessage || `${role} errored`);
|
|
410
414
|
}
|
|
411
415
|
return parseJson(session.getLastAssistantText());
|
|
412
|
-
} catch (e) { throw explainAuth(e, role, spec,
|
|
416
|
+
} catch (e) { throw explainAuth(e, role, spec, registry); }
|
|
413
417
|
finally {
|
|
414
418
|
signal?.removeEventListener("abort", onAbort);
|
|
415
419
|
if (session) {
|
|
@@ -421,14 +425,14 @@ async function oneShot({ role, model, content, cwd, ctx, settings, meter, signal
|
|
|
421
425
|
}
|
|
422
426
|
|
|
423
427
|
/* ---------- an attempt: forked session + worktree + style agent ---------- */
|
|
424
|
-
async function runAttempt({ state, style, n, dir, cwd,
|
|
428
|
+
async function runAttempt({ state, style, n, dir, cwd, pick, settings, model, meter, sessionDir, src, signal, onProgress, live }: { state: State; style: string; n: number; dir: string; cwd: string; pick: ModelPicker; settings: LoopSettings; model?: string | null; meter?: Meter; sessionDir?: string; src?: string | null; signal?: AbortSignal; onProgress: (progress: any) => void; live?: any }): Promise<Attempt> {
|
|
425
429
|
const agentDir = getAgentDir();
|
|
426
430
|
const agent = loadAgent(style, { cwd, agentDir });
|
|
427
431
|
const fork = state.best.session ?? src;
|
|
428
432
|
const sessionManager = fork && fs.existsSync(fork) ? SessionManager.forkFrom(fork, dir, sessionDir) : SessionManager.create(dir, sessionDir);
|
|
429
433
|
const loader = new DefaultResourceLoader({ cwd: dir, agentDir, noExtensions: true, noPromptTemplates: true, noThemes: true, appendSystemPrompt: [agent.body] });
|
|
430
434
|
await loader.reload();
|
|
431
|
-
const picked =
|
|
435
|
+
const picked = pick(model ?? settings.models[style] ?? agent.model);
|
|
432
436
|
const { session: created } = await createAgentSession({ cwd: dir, agentDir, model: picked, tools: agent.tools?.length ? agent.tools : undefined, resourceLoader: loader, sessionManager });
|
|
433
437
|
const session = track(created);
|
|
434
438
|
live?.attempts.set(`R${n}-${style}`, session);
|
|
@@ -832,12 +836,20 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
832
836
|
});
|
|
833
837
|
// state is module-scoped, so a session that starts after one has shut down must not inherit its latch
|
|
834
838
|
pi.on?.("session_start", async () => { shuttingDown = false; RUNS.clear(); });
|
|
835
|
-
pi.on?.("session_shutdown", async () => {
|
|
839
|
+
pi.on?.("session_shutdown", async () => {
|
|
840
|
+
shuttingDown = true;
|
|
841
|
+
const runs = [...RUNS.values()];
|
|
842
|
+
for (const r of runs) r.ctrl.abort();
|
|
843
|
+
// disposing the session invalidates the ctx, so the runs must be finished before this returns, not just aborted
|
|
844
|
+
await Promise.allSettled(runs.map((r) => r.promise));
|
|
845
|
+
await stopAll();
|
|
846
|
+
});
|
|
836
847
|
async function run(params: LoopParamsIn, signal: AbortSignal | undefined, onUpdate: ((update: any) => void) | undefined, ctx: Ctx, live: any) {
|
|
837
848
|
const cwd = ctx.cwd;
|
|
849
|
+
const hasUI = ctx.hasUI ?? false; // a getter on the ctx proxy: stale after replacement, so read it once
|
|
838
850
|
let settings = loopSettings(pi, cwd);
|
|
839
851
|
const chosen = Object.keys(settings.models).some((k) => k !== "pool");
|
|
840
|
-
if (!params.from && !chosen &&
|
|
852
|
+
if (!params.from && !chosen && hasUI && (ctx.modelRegistry?.getAvailable?.() ?? []).length > 1) {
|
|
841
853
|
const r = await pickModels(ctx, { reason: "First loop here" });
|
|
842
854
|
if (r && "error" in r) throw new Error(r.error);
|
|
843
855
|
if (r && "models" in r) settings = { ...settings, models: { ...settings.models, ...r.models } };
|
|
@@ -872,7 +884,7 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
872
884
|
if (SELF_SHOT.test(state.build ?? "")) findChrome();
|
|
873
885
|
// a visual round is only as good as the eyes on it: refuse a judge (or panel member) that cannot see the screenshots
|
|
874
886
|
if (judgedOnScreenshots(state)) {
|
|
875
|
-
const asModel = (spec: string | null | undefined) => { try { return
|
|
887
|
+
const asModel = (spec: string | null | undefined) => { try { return pick(spec); } catch { return null; } }; // an unavailable model reports itself later
|
|
876
888
|
const panelOn = settings.council !== false && settings.judgePanel !== false && (models.council?.length ?? 0) > 1;
|
|
877
889
|
const problem = visionProblem([
|
|
878
890
|
{ role: "judge", key: "judge", spec: models.judge, model: asModel(models.judge) },
|
|
@@ -888,21 +900,27 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
888
900
|
const runDir = path.join(cwd, STATE_DIR, state.id);
|
|
889
901
|
// every nested model call in this round — attempts, judge samples, council — adds to one meter, reported as the result's `usage`
|
|
890
902
|
const meter = newMeter();
|
|
891
|
-
//
|
|
892
|
-
//
|
|
903
|
+
// A TUI call returns as soon as the round starts, and `ctx` is a getter proxy that throws once the session is
|
|
904
|
+
// replaced (`runner.assertActive()`), so the background run holds only values read while it is still alive: the
|
|
905
|
+
// model registry object itself, the session paths, and the status sink. `bindUI` rebinds the new session's UI,
|
|
906
|
+
// so the old one is not touched after a switch.
|
|
893
907
|
const sessionDir = ctx.sessionManager?.getSessionDir?.();
|
|
894
908
|
const sessionFile = ctx.sessionManager?.getSessionFile?.() ?? null;
|
|
895
|
-
const
|
|
909
|
+
const pickCtx: PickCtx = { cwd, model: ctx.model, modelRegistry: ctx.modelRegistry, ui: ctx.ui };
|
|
910
|
+
const pick: ModelPicker = (spec) => pickModel(pickCtx, spec);
|
|
911
|
+
const statusSink = ctx.ui;
|
|
912
|
+
let alive = true;
|
|
913
|
+
const setStatus = (text?: string) => { if (alive) { try { statusSink.setStatus?.("loops", text); } catch {} } };
|
|
896
914
|
const details: Details = { run: state.id, kind: state.kind, round: null, judge: null, attempts: [], phase: "starting" };
|
|
897
915
|
if (live) { live.id = state.id; live.state = state; live.details = details; live.cwd = cwd; live.started?.(); }
|
|
898
916
|
const push = (phase?: string) => { details.phase = phase ?? details.phase; if (live) setStatus(`${state.id} r${details.round ?? "?"} · ${details.phase}`); onUpdate?.({ content: [{ type: "text", text: `${state.id} r${details.round ?? "?"} · ${details.phase}\n${details.attempts.map((a) => ` ${a.style.padEnd(5)} ${a.status.padEnd(8)} ${a.tool ? `[${a.tool}] ` : ""}${a.tools} tools, ${a.tokens} tok${a.text ? ` — ${a.text.split("\n").pop()?.slice(0, 80) ?? ""}` : ""}`).join("\n")}` }], details: structuredClone(details) }); };
|
|
899
|
-
const askModel = (role: string, i: number = 0) => (content: any) => oneShot({ role, model: role === "council" ? (models.council ?? [])[i % (models.council?.length || 1)] : models[role], content, cwd,
|
|
900
|
-
const askAs = (role: string, model?: string | null) => (content: any) => oneShot({ role, model, content, cwd,
|
|
917
|
+
const askModel = (role: string, i: number = 0) => (content: any) => oneShot({ role, model: role === "council" ? (models.council ?? [])[i % (models.council?.length || 1)] : models[role], content, cwd, pick, registry: pickCtx.modelRegistry, settings, meter, signal });
|
|
918
|
+
const askAs = (role: string, model?: string | null) => (content: any) => oneShot({ role, model, content, cwd, pick, registry: pickCtx.modelRegistry, settings, meter, signal });
|
|
901
919
|
// the judge panel: every council model (the attempts' models) scores every candidate, labels blind; the judge model keeps vetoes and the head-to-head
|
|
902
920
|
const panel = settings.council !== false && settings.judgePanel !== false && (models.council?.length ?? 0) > 1 ? (models.council ?? []).map((m) => ({ name: shortModel(m), ask: askAs("judge", m) })) : undefined;
|
|
903
921
|
state.models = { ...Object.fromEntries(STYLES.map((st) => [st, models[st]])), judge: models.judge, council: models.council, spread: models.spread };
|
|
904
922
|
const needsYou = async (title: string, options: string[], fallback: string): Promise<string> => {
|
|
905
|
-
if (
|
|
923
|
+
if (hasUI && !live) { const r = await ctx.ui.select(title, options); return r ?? fallback; }
|
|
906
924
|
return fallback;
|
|
907
925
|
};
|
|
908
926
|
|
|
@@ -934,7 +952,7 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
934
952
|
try { git(cwd, "worktree", "remove", "-f", dir); } catch {}
|
|
935
953
|
git(cwd, "worktree", "add", "-q", "-f", "-B", branch, dir, round.from ?? state.baseCommit);
|
|
936
954
|
const slot = details.attempts.find((a) => a.style === style)!;
|
|
937
|
-
const r = await runAttempt({ state, style, n, dir, cwd,
|
|
955
|
+
const r = await runAttempt({ state, style, n, dir, cwd, pick, settings, model: models[style], meter, sessionDir, src: sessionFile, signal, live, onProgress: (p) => { Object.assign(slot, p); push(); } });
|
|
938
956
|
Object.assign(slot, { status: r.status, tool: null, text: r.summary || r.error || "" }); push();
|
|
939
957
|
return { ...r, branch, dir: path.relative(cwd, dir) };
|
|
940
958
|
}));
|
|
@@ -1017,7 +1035,7 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
1017
1035
|
else judge.why = `${judge.why} (your pick ${live.pick} was not a passing attempt)`;
|
|
1018
1036
|
live.pick = null;
|
|
1019
1037
|
}
|
|
1020
|
-
if (
|
|
1038
|
+
if (hasUI && !live && takeable.length) {
|
|
1021
1039
|
for (;;) {
|
|
1022
1040
|
const others = takeable.filter((a) => a.label !== judge.pick);
|
|
1023
1041
|
const opts = [`Keep ${judge.pick}${judge.pick === "CURRENT" ? " (nothing beat it)" : " (judge's pick)"}`, ...others.map((a) => `Take ${a.label} instead`), ...valid.map((a) => `Show ${a.label}'s diff`), ...valid.filter((a) => a.session).map((a) => `Open ${a.label}'s session after the run`)];
|
|
@@ -1063,7 +1081,11 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
1063
1081
|
|
|
1064
1082
|
pi.registerTool?.({
|
|
1065
1083
|
name: "loop", exposure: "model-only", executionMode: "sequential",
|
|
1066
|
-
promptSnippet: "run hill-climbing rounds: three forked attempts in git worktrees, a judge keeps the best",
|
|
1084
|
+
promptSnippet: "run hill-climbing rounds: three forked attempts in git worktrees, a judge keeps the best",
|
|
1085
|
+
promptGuidelines: [
|
|
1086
|
+
"When the ask is comparative — make this faster, improve this, which of these is better — prefer `loop` over editing by hand: it forks attempts into their own worktrees and a judge keeps the best, and your checkout is untouched until `loop_apply`.",
|
|
1087
|
+
"Give a `loop` round a `bench` when a number can be measured, and `routes` when the goal is what a page looks like; without either it is judged on the diffs.",
|
|
1088
|
+
], annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1067
1089
|
label: "loop",
|
|
1068
1090
|
outputSchema: LoopResult,
|
|
1069
1091
|
description: "Run hill-climbing round(s) on the current git repo: three forked attempt sessions (safe/bold/wild agents) in their own worktrees, a judge keeps the best. Bench mode when `bench` is set (each attempt sets up and measures itself; a judge agent re-measures every candidate, reads the diffs and vetoes rule breaks); visual mode otherwise (`build` writes `shots`, a judge panel scores them). `from` continues a run from its last judged round.",
|
|
@@ -1083,12 +1105,13 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
1083
1105
|
if (!choice?.startsWith("Approve")) return notRun("discuss", "Not run. Discuss the spec with me in chat (what is unclear, what should be different), then call loop again when we agree.");
|
|
1084
1106
|
}
|
|
1085
1107
|
if (!ctx.hasUI || params.wait) return run(params, signal, onUpdate, ctx, null);
|
|
1108
|
+
const ui = ctx.ui; // the object, held by value: the ctx getter throws once the session is replaced
|
|
1086
1109
|
const live: any = { id: null, state: null, details: null, cwd: ctx.cwd, attempts: new Map(), pick: null, last: null, error: null, ctrl: new AbortController(), startedAt: Date.now() };
|
|
1087
1110
|
const started = new Promise((resolve) => { live.started = resolve; });
|
|
1088
1111
|
live.promise = run(params, live.ctrl.signal, (u) => { live.last = u; }, ctx, live).then(
|
|
1089
1112
|
(r) => { try { pi.sendUserMessage?.(r.content, { deliverAs: "followUp" }); } catch {} },
|
|
1090
1113
|
(e) => { live.error = e; if (live.id) { try { pi.sendUserMessage?.(`Loop ${live.id} failed: ${e.message}`, { deliverAs: "followUp" }); } catch {} } }
|
|
1091
|
-
).finally(() => { if (live.id) RUNS.delete(live.id); live.started?.(); try {
|
|
1114
|
+
).finally(() => { if (live.id) RUNS.delete(live.id); live.started?.(); try { ui.setStatus?.("loops", undefined); } catch {} });
|
|
1092
1115
|
await started;
|
|
1093
1116
|
if (!live.id) throw live.error ?? new Error("the loop did not start");
|
|
1094
1117
|
RUNS.set(live.id, live);
|
|
@@ -1123,13 +1146,13 @@ export default function loopsExtension(pi: Pi): void {
|
|
|
1123
1146
|
} });
|
|
1124
1147
|
pi.registerTool?.({ name: "loop_status", promptSnippet: "progress of a loop round running in the background", outputSchema: StatusResult, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, label: "loop_status", description: "Progress of a loop round running in the background: phase, each attempt's status, tools and tokens.", parameters: Type.Object({ id: idParam }),
|
|
1125
1148
|
async execute(_id, p) { const r = liveRun(p.id); return { content: r.last?.content ?? [{ type: "text", text: `${r.id} starting` }], details: r.details, structuredContent: { run: r.id, phase: String(r.details?.phase ?? "starting") } }; } });
|
|
1126
|
-
pi.registerTool?.({ name: "loop_talk", promptSnippet: "tell a running attempt something", executionMode: "sequential", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint:
|
|
1149
|
+
pi.registerTool?.({ name: "loop_talk", promptSnippet: "tell a running attempt something", executionMode: "sequential", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, label: "loop_talk", description: "Tell a running attempt something (R1-bold, or just bold): delivered into its session as a steering message it acts on next.", parameters: Type.Object({ attempt: Type.String(), message: Type.String(), id: idParam }),
|
|
1127
1150
|
async execute(_id, p) { const r = liveRun(p.id); const key = findAttempt(r, p.attempt); if (!key) throw new Error(`${p.attempt} is not running (running: ${[...r.attempts.keys()].join(", ") || "none"})`); await r.attempts.get(key).steer(`Message from the person running this loop: ${p.message}`); return said(`told ${key}: ${p.message}`); } });
|
|
1128
1151
|
pi.registerTool?.({ name: "loop_rule", promptSnippet: "add a rule a running loop must follow", executionMode: "sequential", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, label: "loop_rule", description: "Add a rule to a running loop: every running attempt is told now, the judge enforces it from this round on, and it stays for later rounds.", parameters: Type.Object({ rule: Type.String(), id: idParam }),
|
|
1129
1152
|
async execute(_id, p) { const r = liveRun(p.id); if (!r.state.rules.includes(p.rule)) { r.state.rules.push(p.rule); if (r.state.rubric) addRule(r.state.rubric, p.rule); saveState(r.cwd, r.state); } await Promise.allSettled([...r.attempts.values()].map((s) => s.steer(`New rule for this loop, from the person running it: ${p.rule}`))); return said(`rule added: ${p.rule} (told ${r.attempts.size} running attempt${r.attempts.size === 1 ? "" : "s"})`); } });
|
|
1130
1153
|
pi.registerTool?.({ name: "loop_pick", promptSnippet: "keep a specific attempt as the winner yourself", executionMode: "sequential", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, label: "loop_pick", description: "Decide the running round yourself: when the judge is done, keep this attempt (R1-wild, or just wild; or CURRENT) as the best if its check passes, whatever the judge scored.", parameters: Type.Object({ attempt: Type.String(), id: idParam }),
|
|
1131
1154
|
async execute(_id, p) { const r = liveRun(p.id); r.pick = p.attempt; return said(`when round ${r.details?.round ?? "?"} of ${r.id} is judged, ${p.attempt} is kept`); } });
|
|
1132
|
-
pi.registerTool?.({ name: "loop_apply", promptSnippet: "take a run's best into the checkout (tracked files only)", outputSchema: ApplyResult, executionMode: "sequential", annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, label: "loop_apply", description: "Take a run's best into the checkout: checks out the winner's changed tracked files from its commit and commits them (files it added or deleted are listed, not applied). Default: this session's last run.", parameters: Type.Object({ id: idParam }),
|
|
1155
|
+
pi.registerTool?.({ name: "loop_apply", promptSnippet: "take a run's best into the checkout (tracked files only)", outputSchema: ApplyResult, executionMode: "sequential", promptGuidelines: ["`loop_apply` takes the winner's changed *tracked* files as one commit; files it added or deleted are listed, not applied — copy those yourself if the round needs them."], annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, label: "loop_apply", description: "Take a run's best into the checkout: checks out the winner's changed tracked files from its commit and commits them (files it added or deleted are listed, not applied). Default: this session's last run.", parameters: Type.Object({ id: idParam }),
|
|
1133
1156
|
async execute(_id, p, _signal, _onUpdate, ctx) {
|
|
1134
1157
|
const mine = !p.id || p.id === "last" ? sessionRuns(ctx).find((id) => fs.existsSync(stateFile(ctx.cwd, id))) : null;
|
|
1135
1158
|
const state = loadState(ctx.cwd, mine ?? p.id);
|
package/package.json
CHANGED