@ferris1225/pi-subagents 4.1.1 → 4.1.2

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
@@ -60,16 +60,17 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
60
60
  label: "Subagent Control",
61
61
  description: [
62
62
  "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.",
63
+ "steer queues an instruction after the current tool batch while the top-level RPC child is active.",
64
+ "retarget replaces the objective in that same active top-level child.",
65
+ "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
66
  "park aborts to a stable checkpoint, terminates the child, preserves context, and releases its concurrency slot.",
66
67
  "resume restarts a parked, completed, or failed retained thread with the same run id; objective is optional.",
67
68
  "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.",
68
69
  ].join(" "),
69
- promptSnippet: "Control a subagent thread: steer, retarget, park, resume, or fork by stable run id.",
70
+ promptSnippet: "Control a subagent thread: steer/retarget an active top-level child; park/stop a managed downstream stage; resume or fork retained context.",
70
71
  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.",
72
+ "Use subagent_control steer to refine an active top-level RPC child without restarting it; the instruction is delivered after its current tool batch.",
73
+ "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
74
  "Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later.",
74
75
  "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
76
  "Use subagent_stop only for destructive cancellation; it retires that thread's retained session without retiring independent forks.",
@@ -409,6 +410,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
409
410
  if (active) {
410
411
  const parked = active.status === "parked";
411
412
  const activeThread = runtime.threads.get(active.id);
413
+ const managedDownstream =
414
+ activeThread?.state === "running" && activeThread.control.getPhase() === "settled";
412
415
  const metadata = [
413
416
  activeThread?.isolation === "worktree" ? `worktree ${active.integrationStatus ?? activeThread.worktree?.state ?? "active"}` : undefined,
414
417
  activeThread?.forkedFromRunId !== undefined ? `forked from #${activeThread.forkedFromRunId}` : undefined,
@@ -420,7 +423,9 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
420
423
  type: "text",
421
424
  text: parked
422
425
  ? `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.`,
426
+ : managedDownstream
427
+ ? `Run #${active.id} ${active.agent} is in a managed downstream stage (${active.activity ?? statusLabel(active.status)}${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.`
428
+ : `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.`,
424
429
  },
425
430
  ],
426
431
  details: {},
@@ -617,9 +622,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
617
622
  });
618
623
  }
619
624
 
625
+ // Interrupt every claimed generation before awaiting any one of them.
626
+ // An isolated stop may need the repository lane for final integration;
627
+ // cancelling all holders first prevents stop-all from waiting behind a
628
+ // later shared workflow that this same operation has not interrupted yet.
629
+ const interruptionPromises = claimed.map(({ thread, stopMessage, controller }) => {
630
+ const stopping = thread.control.stop(stopMessage).catch(() => undefined);
631
+ runtime.backgroundQueue.cancel(controller);
632
+ return stopping;
633
+ });
634
+
620
635
  const stopped: string[] = [];
621
636
  const retainedIntegration: string[] = [];
622
- for (const claim of claimed) {
637
+ for (const [claimIndex, claim] of claimed.entries()) {
623
638
  const {
624
639
  runId,
625
640
  thread,
@@ -634,8 +649,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
634
649
  stopVersion,
635
650
  stopMessage,
636
651
  } = claim;
637
- await thread.control.stop(stopMessage).catch(() => undefined);
638
- runtime.backgroundQueue.cancel(controller);
652
+ await interruptionPromises[claimIndex];
639
653
  await completion;
640
654
  if (runtime.runControllers.get(runId) === controller) runtime.runControllers.delete(runId);
641
655
  if (thread.queueController === controller) thread.queueController = undefined;
package/src/widget.ts CHANGED
@@ -93,8 +93,8 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
93
93
  return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
94
94
  }
95
95
 
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
96
+ /** Render active runs as a tree: main-agent dispatches are roots and managed
97
+ * documenter/reviewer/fix steps nest under the stable parent row. No run ids
98
98
  * appear here — the tree and the task label say what each row is, and ids stay
99
99
  * available through subagent_status when a thread must be controlled. */
100
100
  export function formatActiveRunLines(
@@ -120,8 +120,8 @@ export function formatActiveRunLines(
120
120
  for (const root of roots) {
121
121
  const children = childrenOf.get(root.id) ?? [];
122
122
  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.
123
+ // The parent's managed-workflow placeholder is redundant while a child
124
+ // row shows live progress; keep it only between stages.
125
125
  if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
126
126
  children.forEach((child, index) => {
127
127
  const connector = index === children.length - 1 ? "└ " : "├ ";
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(