@a-t-h-i/bot-lobby 0.6.7 → 0.6.8

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.
Files changed (49) hide show
  1. package/README.md +132 -15
  2. package/package.json +1 -1
  3. package/prompts/master.md +15 -0
  4. package/prompts/panel.md +4 -0
  5. package/prompts/planner.md +5 -0
  6. package/prompts/pr-review.md +62 -0
  7. package/prompts/splitter.md +58 -0
  8. package/src/ask/dialog.ts +19 -5
  9. package/src/ask/state.ts +8 -2
  10. package/src/ask/tool.ts +3 -1
  11. package/src/ask/view.ts +6 -3
  12. package/src/classifier/instance.ts +6 -0
  13. package/src/classifier/knowledge.ts +158 -0
  14. package/src/classifier/review.ts +146 -0
  15. package/src/execution/workspace.ts +185 -0
  16. package/src/knowledge/edit.ts +109 -0
  17. package/src/knowledge/notes.ts +143 -0
  18. package/src/knowledge/selector.ts +1 -1
  19. package/src/knowledge/store.ts +16 -2
  20. package/src/lobby/ask.ts +42 -21
  21. package/src/lobby/issues.ts +1 -1
  22. package/src/lobby/knowledge.ts +179 -0
  23. package/src/lobby/layout.ts +95 -18
  24. package/src/lobby/planner.ts +199 -7
  25. package/src/lobby/pr-review.ts +360 -0
  26. package/src/lobby/pulls.ts +250 -0
  27. package/src/lobby/runtime.ts +115 -13
  28. package/src/lobby/split.ts +230 -0
  29. package/src/lobby/tabs/git.ts +162 -0
  30. package/src/lobby/tabs/knowledge.ts +135 -0
  31. package/src/lobby/tabs/plan.ts +3 -1
  32. package/src/lobby/tabs/tasks.ts +11 -2
  33. package/src/lobby/view.ts +432 -32
  34. package/src/master/master.ts +39 -13
  35. package/src/pi/commands.ts +12 -7
  36. package/src/pi/model-support.ts +9 -0
  37. package/src/pi/plan-checklist.ts +42 -0
  38. package/src/pi/route.ts +2 -1
  39. package/src/pi/settings-ui.ts +41 -1
  40. package/src/pi/start-flags.ts +14 -0
  41. package/src/pi/start-task.ts +56 -5
  42. package/src/pi/tools.ts +4 -2
  43. package/src/schemas/configuration.ts +32 -3
  44. package/src/schemas/task.ts +15 -0
  45. package/src/state/backlog.ts +27 -4
  46. package/src/state/metrics.ts +2 -2
  47. package/src/state/persistence.ts +5 -5
  48. package/src/text.ts +38 -0
  49. package/src/workflow/workflow.ts +11 -0
@@ -55,6 +55,18 @@ export type LobbyAgentKind = "quickfix" | "planner";
55
55
  /** Settings kinds a workflow run (domain + role) can draw from. */
56
56
  export type WorkflowProfileKind = Domain | "scout" | "researcher";
57
57
 
58
+ /**
59
+ * Whether a new task gets a git branch of its own (`branch`: created and
60
+ * checked out in the working folder) or a worktree of its own (`worktree`: a
61
+ * second checkout every agent of the task runs in), named after the task.
62
+ */
63
+ export const GIT_ISOLATIONS = ["off", "branch", "worktree"] as const;
64
+ export type GitIsolation = (typeof GIT_ISOLATIONS)[number];
65
+
66
+ export function isGitIsolation(value: unknown): value is GitIsolation {
67
+ return typeof value === "string" && (GIT_ISOLATIONS as readonly string[]).includes(value);
68
+ }
69
+
58
70
  export interface WorkflowConfig {
59
71
  maxReviewIterations: number;
60
72
  maxParallelScouts: number;
@@ -82,6 +94,8 @@ export interface WorkflowConfig {
82
94
  briefCheck: boolean;
83
95
  /** A new request that one agent can do alone goes to the quick-fix agent once the oracle confirms; false makes every request a task. */
84
96
  routeQuickFixes: boolean;
97
+ /** A new task gets a git branch or worktree of its own, named after it; `--branch` and `--worktree` decide for one task. */
98
+ gitIsolation: GitIsolation;
85
99
  }
86
100
 
87
101
  export interface KnowledgeConfig {
@@ -129,10 +143,15 @@ export interface LobbyConfig {
129
143
  * round skips the seats and asks nothing; later replies only revise. 0 = unlimited.
130
144
  */
131
145
  maxPlanningRounds: number;
146
+ /**
147
+ * A plan with more steps than this is offered to be split into up to five
148
+ * tasks when it is saved (the oracle proposes, the user decides); 0 = never.
149
+ */
150
+ splitPlanAbove: number;
132
151
  }
133
152
 
134
153
  /** Decisions the classifier can make, each switched on or off on its own. */
135
- export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort"] as const;
154
+ export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort", "review", "knowledge"] as const;
136
155
  export type ClassifierFeature = (typeof CLASSIFIER_FEATURES)[number];
137
156
 
138
157
  /** Hosts that serve Jev behind the same System One API; `auto` takes OpenCode's free Jev when pi holds an OpenCode key, else TypeSafe. */
@@ -151,6 +170,8 @@ export interface ClassifierThresholds {
151
170
  autoAnswerMargin: number;
152
171
  /** A file is a likely file at this relevance. */
153
172
  fileRelevantAt: number;
173
+ /** A section of a knowledge file that is over the prompt budget stays in at this relevance. */
174
+ knowledgeRelevantAt: number;
154
175
  /** A step scored simple at this confidence runs one thinking level lower. */
155
176
  simpleAt: number;
156
177
  /** A step scored trivial at this confidence runs on the cheaper model. */
@@ -224,6 +245,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
224
245
  fastTrack: true,
225
246
  briefCheck: true,
226
247
  routeQuickFixes: true,
248
+ gitIsolation: "off",
227
249
  },
228
250
  knowledge: {
229
251
  compactionThreshold: 20000,
@@ -241,6 +263,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
241
263
  mouse: true,
242
264
  miniLine: true,
243
265
  maxPlanningRounds: 5,
266
+ splitPlanAbove: 8,
244
267
  },
245
268
  classifier: {
246
269
  enabled: false,
@@ -248,13 +271,14 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
248
271
  model: "",
249
272
  baseUrl: "",
250
273
  timeoutMs: 4000,
251
- features: { seats: true, answers: true, files: true, triage: true, effort: true },
274
+ features: { seats: true, answers: true, files: true, triage: true, effort: true, review: true, knowledge: true },
252
275
  thresholds: {
253
276
  seatAt: 0.35,
254
277
  reseatReadyAt: 0.6,
255
278
  autoAnswerAt: 0.9,
256
279
  autoAnswerMargin: 0.5,
257
280
  fileRelevantAt: 0.5,
281
+ knowledgeRelevantAt: 0.4,
258
282
  simpleAt: 0.7,
259
283
  trivialAt: 0.8,
260
284
  quickFixAt: 0.7,
@@ -269,6 +293,9 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
269
293
  /** Choices the settings menu cycles through for the planning round limit; 0 = unlimited. */
270
294
  export const PLANNING_ROUND_CHOICES = [2, 3, 5, 8, 0] as const;
271
295
 
296
+ /** Choices the settings menu cycles through for the step count past which a saved plan is offered a split; 0 = never. */
297
+ export const SPLIT_PLAN_CHOICES = [6, 8, 10, 12, 0] as const;
298
+
272
299
  function positive(value: unknown): number | undefined {
273
300
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
274
301
  }
@@ -299,7 +326,7 @@ function flag(value: unknown, fallback: boolean): boolean {
299
326
  }
300
327
 
301
328
  function normalizeLobby(value: unknown): LobbyConfig {
302
- const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown; miniLine?: unknown; maxPlanningRounds?: unknown } | undefined;
329
+ const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown; miniLine?: unknown; maxPlanningRounds?: unknown; splitPlanAbove?: unknown } | undefined;
303
330
  const defaults = DEFAULT_CONFIG.lobby;
304
331
  const panel = Array.isArray(source?.planningPanel)
305
332
  ? [...new Set(source.planningPanel.filter((entry): entry is PanelMember => typeof entry === "string" && isPanelMember(entry)))]
@@ -316,6 +343,7 @@ function normalizeLobby(value: unknown): LobbyConfig {
316
343
  mouse: flag(source?.mouse, defaults.mouse),
317
344
  miniLine: flag(source?.miniLine, defaults.miniLine),
318
345
  maxPlanningRounds: roundLimit(source?.maxPlanningRounds, defaults.maxPlanningRounds),
346
+ splitPlanAbove: roundLimit(source?.splitPlanAbove, defaults.splitPlanAbove),
319
347
  };
320
348
  }
321
349
 
@@ -370,6 +398,7 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
370
398
  workflow.fastTrack = workflow.fastTrack !== false;
371
399
  workflow.briefCheck = workflow.briefCheck !== false;
372
400
  workflow.routeQuickFixes = workflow.routeQuickFixes !== false;
401
+ workflow.gitIsolation = isGitIsolation(workflow.gitIsolation) ? workflow.gitIsolation : DEFAULT_CONFIG.workflow.gitIsolation;
373
402
  workflow.taskBudgetMinutes = typeof workflow.taskBudgetMinutes === "number" && workflow.taskBudgetMinutes > 0 ? Math.min(24 * 60, Math.round(workflow.taskBudgetMinutes)) : 0;
374
403
  const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
375
404
  const srcAgents = (src.agents ?? {}) as Partial<BotLobbyConfig["agents"]>;
@@ -158,6 +158,19 @@ export interface QaWaiver {
158
158
  open: string[];
159
159
  }
160
160
 
161
+ /** The branch (or worktree and branch) a task works on, when git isolation was on as it started. */
162
+ export interface TaskGit {
163
+ mode: "branch" | "worktree";
164
+ /** Named after the task. */
165
+ branch: string;
166
+ /** What the working folder stood on when the task started: a branch name, or `detached at 1a2b3c4`. */
167
+ from?: string;
168
+ /** The commit it branched from. */
169
+ base?: string;
170
+ /** worktree: the folder every agent of the task runs in. */
171
+ path?: string;
172
+ }
173
+
161
174
  export interface Task {
162
175
  id: string;
163
176
  title: string;
@@ -197,6 +210,8 @@ export interface Task {
197
210
  extraReviewRounds?: number;
198
211
  /** Set only by the user: the work is accepted without a QA pass, so the task may complete. */
199
212
  qaWaiver?: QaWaiver;
213
+ /** The git branch (or worktree) made for this task, when isolation was on as it started. */
214
+ git?: TaskGit;
200
215
  }
201
216
 
202
217
  export function createTask(
@@ -18,6 +18,19 @@ export interface IssueRef {
18
18
  url?: string;
19
19
  }
20
20
 
21
+ /** What a task saved from a split plan knows of the others: it is one part of a plan that became several tasks. */
22
+ export interface SplitInfo {
23
+ /** Shared by every part of one plan. */
24
+ group: string;
25
+ /** This part's number (from 1) and how many parts there are. */
26
+ part: number;
27
+ of: number;
28
+ /** The title of every part, in order. */
29
+ titles: string[];
30
+ /** Parts (by number) that come before this one and should be finished first. */
31
+ after: number[];
32
+ }
33
+
21
34
  export interface PlannedTask {
22
35
  id: string;
23
36
  title: string;
@@ -29,6 +42,8 @@ export interface PlannedTask {
29
42
  issue?: IssueRef;
30
43
  /** The bot-lobby task started from this entry. */
31
44
  startedTaskId?: string;
45
+ /** Set when the plan was split into several tasks and this is one of them. */
46
+ split?: SplitInfo;
32
47
  }
33
48
 
34
49
  export function backlogDir(root: string, configDir: string): string {
@@ -48,7 +63,7 @@ function isPlannedTask(value: unknown): value is PlannedTask {
48
63
  export function savePlannedTask(
49
64
  root: string,
50
65
  configDir: string,
51
- input: { title: string; brief: string; issue?: IssueRef },
66
+ input: { title: string; brief: string; issue?: IssueRef; split?: SplitInfo },
52
67
  now = new Date(),
53
68
  ): PlannedTask {
54
69
  const title = input.title.trim() || "planned task";
@@ -58,7 +73,7 @@ export function savePlannedTask(
58
73
  let id = base;
59
74
  for (let n = 2; existsSync(entryPath(root, configDir, id)); n++) id = `${base}-${n}`;
60
75
  const at = now.toISOString();
61
- const entry: PlannedTask = { id, title, brief, createdAt: at, updatedAt: at, status: "pending", ...(input.issue ? { issue: input.issue } : {}) };
76
+ const entry: PlannedTask = { id, title, brief, createdAt: at, updatedAt: at, status: "pending", ...(input.issue ? { issue: input.issue } : {}), ...(input.split ? { split: input.split } : {}) };
62
77
  writeFileEnsured(entryPath(root, configDir, id), JSON.stringify(entry, null, 2));
63
78
  return entry;
64
79
  }
@@ -109,8 +124,16 @@ export function discardPlannedTask(root: string, configDir: string, id: string):
109
124
  rmSync(entryPath(root, configDir, id), { force: true });
110
125
  }
111
126
 
112
- /** The request a started task carries: the agreed plan, plus the issue it came from. */
127
+ /** `Part 2 of 3 of one plan…` and the other parts' titles, for a task that is one part of a split plan. */
128
+ export function splitLine(split: SplitInfo): string {
129
+ const others = split.titles.map((title, index) => `${index + 1}. ${title}${index + 1 === split.part ? " (this task)" : ""}`).join("; ");
130
+ const before = split.after.length > 0 ? ` It builds on ${split.after.map((part) => `part ${part}`).join(" and ")}, which should be done first.` : "";
131
+ return `This is part ${split.part} of ${split.of} of one plan that was split into separate tasks: ${others}.${before} Do only this part; the others are their own tasks.`;
132
+ }
133
+
134
+ /** The request a started task carries: the agreed plan, plus the issue it came from, plus where it sits in a split plan. */
113
135
  export function plannedTaskRequest(entry: PlannedTask): string {
114
136
  const source = entry.issue ? `\n\nFrom GitHub issue #${entry.issue.number}: ${entry.issue.title}${entry.issue.url ? ` (${entry.issue.url})` : ""}` : "";
115
- return `${entry.title}\n\nAgreed plan (from the planning session):\n${entry.brief}${source}`;
137
+ const part = entry.split ? `\n\n${splitLine(entry.split)}` : "";
138
+ return `${entry.title}${part}\n\nAgreed plan (from the planning session):\n${entry.brief}${source}`;
116
139
  }
@@ -33,7 +33,7 @@ export interface MetricRecord {
33
33
  cost?: number;
34
34
  taskId?: string;
35
35
  stalled?: boolean;
36
- /** Classifier calls: what the call decided (seats, answers, files, triage, effort, test). */
36
+ /** Classifier calls: what the call decided (seats, answers, files, triage, effort, review, knowledge, test). */
37
37
  purpose?: string;
38
38
  /** A run the classifier routed down: the configured profile it came from. */
39
39
  routedFrom?: string;
@@ -49,7 +49,7 @@ export interface ClassifierSummary {
49
49
  p90Ms: number;
50
50
  /** Tokens Jev read across every call. */
51
51
  input: number;
52
- /** Calls per decision (seats, answers, files, triage, effort, test). */
52
+ /** Calls per decision (seats, answers, files, triage, effort, review, knowledge, test). */
53
53
  byPurpose: Record<string, number>;
54
54
  seatRunsSkipped: number;
55
55
  questionsAnswered: number;
@@ -15,6 +15,7 @@ import {
15
15
  } from "../knowledge/paths.ts";
16
16
  import { DEFAULT_KNOWLEDGE_CONTENT, ensureFile, readFileOr, writeFileEnsured } from "../knowledge/store.ts";
17
17
  import { forgetCached, forgetCachedUnder, readJsonCached } from "./file-cache.ts";
18
+ import { taskName } from "../text.ts";
18
19
 
19
20
  /** Idempotently create the full knowledge + tasks layout with seed files. */
20
21
  export function ensureProjectStructure(root: string, configDir: string): void {
@@ -235,13 +236,12 @@ export function taskSlug(request: string, max = 40): string {
235
236
  }
236
237
 
237
238
  /**
238
- * Task id from the request (`TASK-<slug>`); requests with no usable slug fall
239
- * back to the timestamped `TASK-task-<timestamp>` form callers already suffix.
239
+ * Task id from the request: its friendly name, `Task-<Words>-<DD-MM-YYYY>`
240
+ * (`Task-Change-Table-Font-27-09-2026`); a request with no words is
241
+ * `Task-Untitled-<date>`. Callers add a `-2`, `-3`… suffix when it is taken.
240
242
  */
241
243
  export function nextTaskId(request: string, now = new Date()): string {
242
- const slug = taskSlug(request);
243
- if (slug.length > 0) return `TASK-${slug}`;
244
- return `TASK-task-${now.toISOString().replace(/[-:T]/g, "").slice(0, 14)}`;
244
+ return taskName(request, now);
245
245
  }
246
246
 
247
247
  /** Files that make up a task's temporary working state (§20). */
package/src/text.ts CHANGED
@@ -57,6 +57,44 @@ export function shortTitle(request: string, maxWords = 3): string {
57
57
  return (content.length > 0 ? content : words).slice(0, maxWords).join(" ").replace(/[:;,]+$/, "");
58
58
  }
59
59
 
60
+ /** Words a task's name leaves out: they join the words that matter without being any of them. */
61
+ const NAME_CONNECTORS = new Set(["and", "or", "but", "with", "without", "on", "in", "at", "of", "by", "from", "into", "so", "as", "is", "are", "be", "it", "its", "that", "this", "then", "also", "just", "some"]);
62
+
63
+ /** `27-09-2026`: the local date as day-month-year, as task names carry it. */
64
+ export function dateStamp(now: Date): string {
65
+ return `${String(now.getDate()).padStart(2, "0")}-${String(now.getMonth() + 1).padStart(2, "0")}-${now.getFullYear()}`;
66
+ }
67
+
68
+ /**
69
+ * The first content words of a request as one name part, each capitalised and
70
+ * joined by dashes (`Change-Table-Font`): at most `maxWords` words and
71
+ * `maxChars` characters, cut between words. Empty when the request has no
72
+ * letters or digits.
73
+ */
74
+ export function nameWords(request: string, maxWords = 5, maxChars = 36): string {
75
+ // Connectors (`and`, `on`, `with`…) say little in a name and must not fill its few slots or end it.
76
+ const words = shortTitle(request, maxWords * 3)
77
+ .split(/[^\p{L}\p{N}]+/u)
78
+ .filter((word) => word.length > 0 && !NAME_CONNECTORS.has(word.toLowerCase()))
79
+ .map((word) => word.charAt(0).toLocaleUpperCase() + word.slice(1))
80
+ .slice(0, maxWords);
81
+ const kept: string[] = [];
82
+ for (const word of words) {
83
+ if ([...kept, word].join("-").length > maxChars && kept.length > 0) break;
84
+ kept.push(word);
85
+ }
86
+ return kept.join("-").slice(0, maxChars).replace(/-+$/, "");
87
+ }
88
+
89
+ /**
90
+ * A task's friendly name: `Task-Change-Table-Font-27-09-2026`, from its
91
+ * request (or title) and the day it started. It is the task's id, the name of
92
+ * the git branch it works on and of the session that drives it.
93
+ */
94
+ export function taskName(request: string, now = new Date()): string {
95
+ return `Task-${nameWords(request) || "Untitled"}-${dateStamp(now)}`;
96
+ }
97
+
60
98
  /** Headings that only label a section (`### Objective`), not name the work. */
61
99
  const LABEL_HEADING = /^#{1,6}\s*(objective|goal|goals|summary|overview|task|request|context|background|description|plan)\s*:?\s*$/i;
62
100
 
@@ -55,12 +55,14 @@ import { pingApproval } from "../pi/notify.ts";
55
55
  import { describeRun, runLogEntry } from "../pi/run-summary.ts";
56
56
  import { nextStates } from "./transitions.ts";
57
57
  import type { FileHinter } from "../classifier/files.ts";
58
+ import type { KnowledgePicker } from "../classifier/knowledge.ts";
58
59
  import type { Classifier } from "../classifier/classifier.ts";
59
60
  import type { EffortRouter } from "../classifier/effort.ts";
60
61
  import { answerClarify } from "../classifier/triage.ts";
61
62
  import { appendMetrics, metricFromRun } from "../state/metrics.ts";
62
63
  import { markCommentsAddressed, pendingComments, readPlanComments } from "../state/comments.ts";
63
64
  import { fastNext, onFastTrack, parseRoster, qaRequired, qaStillDue, qaTookPart, rosterWords, trackSummary } from "./track.ts";
65
+ import { missingWorktree, taskCwd } from "../execution/workspace.ts";
64
66
 
65
67
  export const ORCHESTRATE_ACTIONS = [
66
68
  "clarify",
@@ -140,6 +142,8 @@ export interface WorkflowDeps {
140
142
  runProcess?: ProcessRunner;
141
143
  /** Likely files for scouts and workers, while the classifier's file hints are on. */
142
144
  hints?: FileHinter;
145
+ /** Picks the relevant sections of long knowledge files for scouts, workers and the QA gate, while the classifier's knowledge picks are on. */
146
+ knowledge?: KnowledgePicker;
143
147
  /** Answers a clarify question whose recommended option is clearly right, when it is on. */
144
148
  classifier?: Classifier;
145
149
  /** Re-reads a request after an amendment (the task's triage), when the classifier is on. */
@@ -338,6 +342,7 @@ async function handleScout(task: Task, params: OrchestrateParams, deps: Workflow
338
342
  signal: deps.signal,
339
343
  onUpdate: deps.onUpdate,
340
344
  ...(deps.hints ? { hints: deps.hints } : {}),
345
+ ...(deps.knowledge ? { knowledge: deps.knowledge } : {}),
341
346
  ...(deps.effort ? { effort: deps.effort } : {}),
342
347
  },
343
348
  deps.runProcess ?? spawnPiProcess,
@@ -677,6 +682,7 @@ function workerRequest(deps: WorkflowDeps, task: Task, domain: Domain, instructi
677
682
  signal: deps.signal,
678
683
  onUpdate: deps.onUpdate,
679
684
  ...(deps.hints ? { hints: deps.hints } : {}),
685
+ ...(deps.knowledge ? { knowledge: deps.knowledge } : {}),
680
686
  ...(deps.effort ? { effort: deps.effort } : {}),
681
687
  };
682
688
  }
@@ -1105,6 +1111,7 @@ function qaRequest(deps: WorkflowDeps, task: Task, diff: string, instruction?: s
1105
1111
  profile: deps.profile,
1106
1112
  signal: deps.signal,
1107
1113
  onUpdate: deps.onUpdate,
1114
+ ...(deps.knowledge ? { knowledge: deps.knowledge } : {}),
1108
1115
  };
1109
1116
  }
1110
1117
 
@@ -1680,9 +1687,13 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
1680
1687
  if (!handler) {
1681
1688
  return { ok: false, taskId: task.id, state: task.state, message: `Unknown action "${params.action}".` };
1682
1689
  }
1690
+ const gone = missingWorktree(task);
1691
+ if (gone && params.action !== "status") return { ok: false, taskId: task.id, state: task.state, message: `Rejected: ${gone}` };
1683
1692
  const finished = new Map<string, AgentRun>();
1684
1693
  const tracked: WorkflowDeps = {
1685
1694
  ...deps,
1695
+ // Every agent, diff and desk of a task with its own worktree works there.
1696
+ cwd: taskCwd(task, deps.cwd),
1686
1697
  onUpdate: (run) => {
1687
1698
  if (run.status !== "running") finished.set(run.runId, run);
1688
1699
  deps.onUpdate?.(run);