@ferris1225/pi-subagents 4.1.1 → 4.1.3

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/src/tools.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Thread controls and lookup tools around the subagent runtime:
3
- * subagent_control (steer/retarget/park/resume), subagent_wait (in-turn result
4
- * lookup), subagent_status, and destructive subagent_stop.
3
+ * subagent_control (steer/retarget/park/resume/fork), subagent_wait (in-turn
4
+ * result lookup), subagent_status, and destructive subagent_stop.
5
5
  */
6
6
 
7
7
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -12,8 +12,7 @@ import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "./config.ts";
12
12
  import { formatCompletionBlock, formatUsage, matchRunIds } from "./format.ts";
13
13
  import { emptyUsage } from "./rpc-run.ts";
14
14
  import {
15
- formatElapsed,
16
- formatUsageCompact,
15
+ formatTaskSummary,
17
16
  isRunActiveStatus,
18
17
  monitor,
19
18
  runLabel,
@@ -51,7 +50,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
51
50
  Type.String({ description: "Instruction queued by steer after the current child tool batch." }),
52
51
  ),
53
52
  objective: Type.Optional(
54
- Type.String({ description: "Replacement objective for retarget, or optional objective for resume/fork." }),
53
+ Type.String({ description: "Replacement objective for retarget; optional appended objective for resume/fork. Omit on resume/fork to continue the current retained objective." }),
55
54
  ),
56
55
  });
57
56
 
@@ -60,17 +59,18 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
60
59
  label: "Subagent Control",
61
60
  description: [
62
61
  "Control an existing sub-agent thread by stable run id.",
63
- "steer queues an instruction after the current child tool batch.",
64
- "retarget aborts the current objective to a stable checkpoint, suppresses that aborted completion, then starts the replacement objective in the same session.",
62
+ "steer queues an instruction after the current tool batch while the top-level RPC child is active.",
63
+ "retarget replaces the objective in that same active top-level child.",
64
+ "Managed downstream documenter/reviewer/fix stages are controlled by the parent queue rather than its settled RPC control: use park or stop there, then resume with an objective to redirect retained context.",
65
65
  "park aborts to a stable checkpoint, terminates the child, preserves context, and releases its concurrency slot.",
66
- "resume restarts a parked, completed, or failed retained thread with the same run id; objective is optional.",
67
- "fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; objective is optional.",
66
+ "resume restarts a parked, completed, or failed retained thread with the same run id and cumulative active time; omit objective to continue the current goal, or provide one to append it to retained context and make it the displayed current goal.",
67
+ "fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; omit objective to continue the current goal, or provide one to append a new branch goal.",
68
68
  ].join(" "),
69
- promptSnippet: "Control a subagent thread: steer, retarget, park, resume, or fork by stable run id.",
69
+ promptSnippet: "Control a subagent thread: steer/retarget an active top-level child; park/stop a managed downstream stage; resume or fork retained context.",
70
70
  promptGuidelines: [
71
- "Use subagent_control steer to refine active work without restarting it; the instruction is delivered after the child's current tool batch.",
72
- "Use subagent_control retarget when the active objective is obsolete; do not call subagent_stop and start a fresh thread because retarget preserves context and suppresses the abandoned completion.",
73
- "Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later.",
71
+ "Use subagent_control steer to refine an active top-level RPC child without restarting it; the instruction is delivered after its current tool batch.",
72
+ "Use subagent_control retarget only while that top-level child is active. During a managed downstream stage, park it and resume with a replacement objective instead.",
73
+ "Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later. Resume without objective keeps the current goal; resume with objective appends that goal to retained context.",
74
74
  "Use subagent_control fork only on a parked or settled retained thread; isolated work must settle and integrate before it can fork. Fork creates a new run id while leaving the source untouched.",
75
75
  "Use subagent_stop only for destructive cancellation; it retires that thread's retained session without retiring independent forks.",
76
76
  ],
@@ -109,13 +109,15 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
109
109
  thread.task = objective;
110
110
  thread.control.retargetPending(objective);
111
111
  monitor.setTask(thread.id, objective);
112
- return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the new objective; no child was spawned by this control action.` }], details: {} };
112
+ monitor.setContinuationKind(thread.id, "retarget");
113
+ return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the replacement objective; no child was spawned by this control action.` }], details: {} };
113
114
  }
114
115
  if (!(["starting", "running", "steering", "interrupting", "retrying"] as const).includes(phase as any)) {
115
116
  return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.state}; use resume with objective to restart retained context.` }], details: {} };
116
117
  }
117
118
  thread.task = objective;
118
119
  monitor.setTask(thread.id, objective);
120
+ monitor.setContinuationKind(thread.id, "retarget");
119
121
  await thread.control.retarget(objective);
120
122
  return { content: [{ type: "text", text: `Retargeted run #${thread.id} in the same session; the aborted objective will not be delivered as a completion.` }], details: {} };
121
123
  }
@@ -145,11 +147,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
145
147
  if (params.objective !== undefined && !objective) {
146
148
  return { content: [{ type: "text", text: "resume objective must be non-blank when provided." }], details: {} };
147
149
  }
150
+ const hadRetainedSession = Boolean(thread.sessionId && thread.sessionDir);
148
151
  const pending = await thread.resume(objective, ctx);
149
152
  if (pending.exitCode !== -1) {
150
153
  return { content: [{ type: "text", text: getResultOutput(pending) }], details: {} };
151
154
  }
152
- return { content: [{ type: "text", text: `Resumed run #${thread.id}${objective ? " with a new objective" : " from retained context"}; completion will arrive automatically.` }], details: {}, terminate: true };
155
+ const currentObjective = formatTaskSummary(objective ?? thread.task, 80, false);
156
+ const mode = objective
157
+ ? `appended objective: ${currentObjective}`
158
+ : `continuing current objective: ${currentObjective}`;
159
+ const context = hadRetainedSession
160
+ ? "the same retained session and prior context are preserved"
161
+ : "no prior child session existed, so only the logical run and objective are continued";
162
+ return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. Completion will arrive automatically.` }], details: {}, terminate: true };
153
163
  }
154
164
  case "fork": {
155
165
  const objective = params.objective === undefined ? undefined : nonBlank(params.objective);
@@ -163,7 +173,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
163
173
  return {
164
174
  content: [{
165
175
  type: "text",
166
- text: `Forked run #${thread.id} into new run #${pending.runId}${objective ? " with a new objective" : " from retained context"}; the source is unchanged and child completion will arrive automatically.`,
176
+ text: `Forked run #${thread.id} into new run #${pending.runId}; ${objective ? `appended branch objective: ${formatTaskSummary(objective, 80, false)}` : `continuing current objective: ${formatTaskSummary(thread.task, 80, false)}`}. Retained context is copied, the source is unchanged, and child completion will arrive automatically.`,
167
177
  }],
168
178
  details: { sourceRunId: thread.id, childRunId: pending.runId, result: pending },
169
179
  terminate: true,
@@ -351,10 +361,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
351
361
  });
352
362
 
353
363
  // Status overview: what is running right now and what finished this session,
354
- // with per-run details (id, agent, model, usage, elapsed, activity) so the
355
- // main agent can decide whether to wait, stop, or re-dispatch. Learned from
356
- // nicobailon/pi-subagents ({action:"status"} + status files): inspect before
357
- // you act, and report run ids when handing off.
364
+ // with per-run details (id, role, model, usage, elapsed, activity).
358
365
  const SubagentStatusParams = Type.Object({
359
366
  id: Type.Optional(
360
367
  Type.String({
@@ -367,7 +374,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
367
374
  name: "subagent_status",
368
375
  label: "Subagent Status",
369
376
  description: [
370
- "List active background sub-agent runs (id, agent, model, usage, elapsed, current activity) and recently finished results.",
377
+ "List active background sub-agent runs (id, role, model, thinking, usage, elapsed, current activity) and recently finished results.",
371
378
  "Pass id to read the full result of a finished run; pass no id for the overview.",
372
379
  "Use it to decide whether to subagent_wait, subagent_stop, or re-dispatch — never to poll: results arrive by themselves.",
373
380
  ].join(" "),
@@ -409,18 +416,32 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
409
416
  if (active) {
410
417
  const parked = active.status === "parked";
411
418
  const activeThread = runtime.threads.get(active.id);
419
+ const managedDownstream =
420
+ activeThread?.state === "running" && activeThread.control.getPhase() === "settled";
421
+ const activeChild = runs.find((run) =>
422
+ run.parentRunId === active.id && isRunActiveStatus(run.status)
423
+ );
424
+ const owner = active.managedWorkflow ? `${active.agent} workflow` : active.agent;
425
+ const retainedStage = active.managedWorkflow && activeThread?.agentName !== active.agent
426
+ ? activeThread?.agentName
427
+ : undefined;
412
428
  const metadata = [
413
429
  activeThread?.isolation === "worktree" ? `worktree ${active.integrationStatus ?? activeThread.worktree?.state ?? "active"}` : undefined,
414
430
  activeThread?.forkedFromRunId !== undefined ? `forked from #${activeThread.forkedFromRunId}` : undefined,
415
431
  (activeThread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${activeThread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
416
432
  ].filter(Boolean).join(" · ");
433
+ const stageStatus = activeChild
434
+ ? monitor.summarize(activeChild)
435
+ : active.activity ?? statusLabel(active.status);
417
436
  return {
418
437
  content: [
419
438
  {
420
439
  type: "text",
421
440
  text: parked
422
- ? `Run #${active.id} ${active.agent} is parked with retained context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
423
- : `Run #${active.id} ${active.agent} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
441
+ ? `Run #${active.id} ${owner} is parked with retained${retainedStage ? ` ${retainedStage} stage` : ""} context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
442
+ : managedDownstream
443
+ ? `Run #${active.id} ${owner} is in a managed downstream stage (${stageStatus}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait for its result, subagent_control park to checkpoint it, or subagent_stop to cancel it. Steer/retarget are unavailable until you park and resume the retained stage.`
444
+ : `Run #${active.id} ${owner} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
424
445
  },
425
446
  ],
426
447
  details: {},
@@ -429,36 +450,34 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
429
450
  return { content: [{ type: "text", text: `No subagent run matches "${requested}".` }], details: {} };
430
451
  }
431
452
 
432
- const now = Date.now();
433
453
  const activeRuns = monitor.getRuns().filter(
434
454
  (run) => isRunActiveStatus(run.status),
435
455
  );
436
456
  const activeLines = activeRuns.map((run) => {
437
457
  const thread = runtime.threads.get(run.id);
438
- const model = run.modelFallbackFrom
439
- ? `${run.model ?? "?"} (main after ${run.modelFallbackFrom} failed)`
440
- : (run.model ?? "?");
441
458
  const parts = [
442
- `#${run.id} ${run.agent}`,
459
+ `#${run.id} ${monitor.summarize(run)}`,
443
460
  run.label,
444
- model,
445
- formatUsageCompact(run.usage),
446
- formatElapsed(run, now),
447
- thread?.isolation === "worktree" ? `worktree ${run.integrationStatus ?? thread.worktree?.state ?? "active"}` : undefined,
448
461
  thread?.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
449
462
  (thread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${thread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
463
+ run.activity ?? statusLabel(run.status),
450
464
  ].filter(Boolean);
451
- return `- ${parts.join(" · ")} · ${run.activity ?? statusLabel(run.status)}`;
465
+ return `- ${parts.join(" · ")}`;
452
466
  });
453
467
  const parkedThreads = [...runtime.threads.values()].filter((thread) => thread.state === "parked");
454
468
  const parkedLines = parkedThreads.map((thread) => {
469
+ const run = monitor.findRun(thread.id);
470
+ const owner = run?.managedWorkflow ? `${run.agent} workflow` : run?.agent ?? thread.agentName;
471
+ const retainedStage = run?.managedWorkflow && thread.agentName !== run.agent
472
+ ? ` · retained stage ${thread.agentName}`
473
+ : "";
455
474
  const relations = [
456
475
  thread.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
457
476
  thread.forkChildRunIds.length > 0 ? `forks ${thread.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
458
477
  ].filter(Boolean);
459
478
  const relation = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
460
479
  const isolation = thread.isolation === "worktree" ? ` · worktree ${thread.worktree?.state ?? "active"}` : "";
461
- return `- #${thread.id} ${thread.agentName} · ${runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${isolation}${relation}`;
480
+ return `- #${thread.id} ${owner} · ${run?.label ?? runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${retainedStage}${isolation}${relation}`;
462
481
  });
463
482
  const completed = [...runtime.settledRuns.entries()].slice(-5);
464
483
  const completedLines = completed.map(([id, result]) => {
@@ -617,9 +636,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
617
636
  });
618
637
  }
619
638
 
639
+ // Interrupt every claimed generation before awaiting any one of them.
640
+ // An isolated stop may need the repository lane for final integration;
641
+ // cancelling all holders first prevents stop-all from waiting behind a
642
+ // later shared workflow that this same operation has not interrupted yet.
643
+ const interruptionPromises = claimed.map(({ thread, stopMessage, controller }) => {
644
+ const stopping = thread.control.stop(stopMessage).catch(() => undefined);
645
+ runtime.backgroundQueue.cancel(controller);
646
+ return stopping;
647
+ });
648
+
620
649
  const stopped: string[] = [];
621
650
  const retainedIntegration: string[] = [];
622
- for (const claim of claimed) {
651
+ for (const [claimIndex, claim] of claimed.entries()) {
623
652
  const {
624
653
  runId,
625
654
  thread,
@@ -634,8 +663,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
634
663
  stopVersion,
635
664
  stopMessage,
636
665
  } = claim;
637
- await thread.control.stop(stopMessage).catch(() => undefined);
638
- runtime.backgroundQueue.cancel(controller);
666
+ await interruptionPromises[claimIndex];
639
667
  await completion;
640
668
  if (runtime.runControllers.get(runId) === controller) runtime.runControllers.delete(runId);
641
669
  if (thread.queueController === controller) thread.queueController = undefined;
package/src/widget.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
5
  import {
6
+ continuationLabel,
6
7
  formatElapsed,
7
8
  formatTaskSummary,
8
9
  isRunActiveStatus,
@@ -25,10 +26,22 @@ function compactLine(left: string, right: string, width: number): string {
25
26
  return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
26
27
  }
27
28
 
29
+ /** Keep continuation semantics intact while independently compacting the task. */
30
+ function formatContinuationTask(label: string, task: string, width: number): string {
31
+ if (width <= 0) return "";
32
+ const labelWidth = visibleWidth(label);
33
+ if (labelWidth >= width) return truncateToWidth(label, width, "");
34
+ const separator = " · ";
35
+ const taskWidth = width - labelWidth - visibleWidth(separator);
36
+ if (taskWidth <= 0) return label;
37
+ const summary = formatTaskSummary(task, taskWidth);
38
+ return summary ? `${label}${separator}${summary}` : label;
39
+ }
40
+
28
41
  /** One compact primary line per genuinely active run, plus an optional indented
29
- * activity line. The primary line reserves effective model/thinking and elapsed
30
- * width before truncating the task. Settled and parked threads never appear, so
31
- * elapsed time cannot keep ticking beside a terminal status. */
42
+ * activity line. The primary line reserves stage model/thinking when present and
43
+ * elapsed width before truncating the task. Settled and parked threads never
44
+ * appear, so elapsed time cannot keep ticking beside a terminal status. */
32
45
  function runPrimaryLine(
33
46
  run: RunView,
34
47
  theme: Theme,
@@ -38,22 +51,31 @@ function runPrimaryLine(
38
51
  ): string {
39
52
  const dim = (text: string): string => theme.fg("dim", text);
40
53
  const icon = statusIcon(run.status, theme);
41
- const name = theme.fg("accent", theme.bold(run.agent));
54
+ const displayName = run.managedWorkflow ? `${run.agent} workflow` : run.agent;
55
+ const name = theme.fg("accent", theme.bold(displayName));
42
56
  const identity = `${prefix}${icon} ${name}`;
43
57
  const elapsed = formatElapsed(run, now);
44
58
  // Render only the resolved model id plus thinking level. Provider auth and
45
59
  // other configuration never enter monitor state or this line.
46
60
  const modelId = run.model?.split("/").at(-1);
47
- const modelSource = formatTaskSummary(
48
- modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
49
- 64,
50
- false,
51
- );
61
+ const modelSource = run.managedWorkflow
62
+ ? ""
63
+ : formatTaskSummary(
64
+ modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
65
+ 64,
66
+ false,
67
+ );
52
68
  // A chain child shows its role in the chain plus a task-derived label; the
53
69
  // templated fix brief itself would only repeat the parent review's content.
70
+ const continuation = run.parentRunId === undefined
71
+ ? continuationLabel(run.continuationKind, run.forkedFromRunId)
72
+ : undefined;
54
73
  const taskSource = run.parentRunId !== undefined
55
74
  ? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
56
75
  : formatTaskSummary(run.task, 64);
76
+ const taskDesiredSource = [continuation, taskSource]
77
+ .filter((part): part is string => Boolean(part))
78
+ .join(" · ");
57
79
  const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
58
80
  const contentWidth = Math.max(
59
81
  0,
@@ -64,15 +86,40 @@ function runPrimaryLine(
64
86
  );
65
87
  const modelDesired = visibleWidth(modelSource);
66
88
  const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
67
- let modelWidth = modelSource
68
- ? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
69
- : 0;
89
+ let modelWidth = 0;
90
+ if (modelSource) {
91
+ if (continuation) {
92
+ const continuationWidth = visibleWidth(continuation);
93
+ // Preserve the full semantic label and the usual eight-column task tail
94
+ // before giving the remaining space to model/thinking.
95
+ const taskFloor = Math.min(
96
+ visibleWidth(taskDesiredSource),
97
+ continuationWidth +
98
+ (taskSource ? visibleWidth(" · ") + Math.min(8, visibleWidth(taskSource)) : 0),
99
+ contentWidth,
100
+ );
101
+ const effectiveModelFloor = Math.min(
102
+ modelFloor,
103
+ Math.max(0, contentWidth - continuationWidth),
104
+ );
105
+ modelWidth = Math.min(
106
+ modelDesired,
107
+ Math.max(effectiveModelFloor, contentWidth - taskFloor),
108
+ );
109
+ } else {
110
+ modelWidth = Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8));
111
+ }
112
+ }
70
113
  let taskWidth = contentWidth - modelWidth;
71
- if (visibleWidth(taskSource) < taskWidth) {
72
- modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
114
+ if (visibleWidth(taskDesiredSource) < taskWidth) {
115
+ modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskDesiredSource));
73
116
  taskWidth = contentWidth - modelWidth;
74
117
  }
75
- const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined) : "";
118
+ const task = taskWidth <= 0
119
+ ? ""
120
+ : continuation
121
+ ? formatContinuationTask(continuation, taskSource, taskWidth)
122
+ : formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined);
76
123
  const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
77
124
  const primaryLeft = [
78
125
  identity,
@@ -93,10 +140,9 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
93
140
  return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
94
141
  }
95
142
 
96
- /** Render active runs as a tree: main-agent dispatches are roots, auto-fix chain
97
- * rounds nest under the triggering reviewer row that owns the chain. No run ids
98
- * appear here the tree and the task label say what each row is, and ids stay
99
- * available through subagent_status when a thread must be controlled. */
143
+ /** Render active runs as a tree: main-agent dispatches are roots and managed
144
+ * documenter/reviewer/fix steps nest under the stable parent row. Fork labels
145
+ * include their source id; other control ids remain available through status. */
100
146
  export function formatActiveRunLines(
101
147
  runs: readonly RunView[],
102
148
  theme: Theme,
@@ -120,8 +166,8 @@ export function formatActiveRunLines(
120
166
  for (const root of roots) {
121
167
  const children = childrenOf.get(root.id) ?? [];
122
168
  lines.push(runPrimaryLine(root, theme, width, now, ""));
123
- // The parent's "auto-fix chain running" placeholder is redundant while its
124
- // child rows show live progress; keep it only between rounds.
169
+ // The parent's managed-workflow placeholder is redundant while a child
170
+ // row shows live progress; keep it only between stages.
125
171
  if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
126
172
  children.forEach((child, index) => {
127
173
  const connector = index === children.length - 1 ? "└ " : "├ ";
@@ -134,7 +180,7 @@ export function formatActiveRunLines(
134
180
 
135
181
  function hasTickingRun(): boolean {
136
182
  return monitor.getRuns().some(
137
- (run) => isRunActiveStatus(run.status) && run.startedAt !== undefined,
183
+ (run) => isRunActiveStatus(run.status) && run.activeSince !== undefined,
138
184
  );
139
185
  }
140
186
 
package/src/worktree.ts CHANGED
@@ -298,11 +298,18 @@ export function isPathInside(root: string, candidate: string): boolean {
298
298
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
299
299
  }
300
300
 
301
- /** Resolve and validate the Git repository/worktree that contains cwd. */
302
- export async function resolveWorktreeTarget(
301
+ interface RepositoryLocation {
302
+ originalCwd: string;
303
+ originalRoot: string;
304
+ }
305
+
306
+ /** Resolve the canonical repository root without requiring a committed HEAD.
307
+ * Managed repository lanes use this for empty repositories as well as normal
308
+ * worktrees; worktree creation validates HEAD separately below. */
309
+ async function resolveRepositoryLocation(
303
310
  cwd: string,
304
- runner: CommandRunner = runCommand,
305
- ): Promise<WorktreeTarget> {
311
+ runner: CommandRunner,
312
+ ): Promise<RepositoryLocation> {
306
313
  const requested = resolve(cwd);
307
314
  try {
308
315
  if (!(await stat(requested)).isDirectory()) throw new Error("not a directory");
@@ -335,6 +342,22 @@ export async function resolveWorktreeTarget(
335
342
  `Requested cwd ${originalCwd} is not inside Git worktree root ${originalRoot}.`,
336
343
  );
337
344
  }
345
+ return { originalCwd, originalRoot };
346
+ }
347
+
348
+ export async function resolveRepositoryRoot(
349
+ cwd: string,
350
+ runner: CommandRunner = runCommand,
351
+ ): Promise<string> {
352
+ return (await resolveRepositoryLocation(cwd, runner)).originalRoot;
353
+ }
354
+
355
+ /** Resolve and validate the Git repository/worktree that contains cwd. */
356
+ export async function resolveWorktreeTarget(
357
+ cwd: string,
358
+ runner: CommandRunner = runCommand,
359
+ ): Promise<WorktreeTarget> {
360
+ const { originalCwd, originalRoot } = await resolveRepositoryLocation(cwd, runner);
338
361
  let headResult: CommandResult;
339
362
  try {
340
363
  headResult = await runGit(