@ferris1225/pi-subagents 4.2.13 → 4.3.1

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.
@@ -11,7 +11,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import { existsSync } from "node:fs";
12
12
  import { rm } from "node:fs/promises";
13
13
  import { realpath } from "node:fs/promises";
14
- import { join, resolve, dirname } from "node:path";
14
+ import { join, resolve } from "node:path";
15
15
  import {
16
16
  discoverAgents,
17
17
  isWriteCapableAgent,
@@ -20,14 +20,13 @@ import {
20
20
  } from "./agents.ts";
21
21
  import { type CompletionMessageItem } from "./completion.ts";
22
22
  import {
23
- DEFAULT_THINKING_LEVEL,
24
23
  loadConfig,
24
+ roleThinkingLevel,
25
25
  type SubagentsConfig,
26
26
  type ThinkingLevel,
27
27
  } from "./config.ts";
28
28
  import {
29
29
  isCurrentBoot,
30
- migrateLegacyThreadsManifest,
31
30
  readThreadRecords,
32
31
  referencedDurablePaths,
33
32
  removeThreadRecord,
@@ -35,7 +34,7 @@ import {
35
34
  pruneThreadRecords,
36
35
  restoredResultFromSummary,
37
36
  threadRecordFromThread,
38
- ThreadRecord,
37
+ type ThreadRecord,
39
38
  upsertThreadRecord,
40
39
  } from "./durable.ts";
41
40
  import {
@@ -54,13 +53,15 @@ import {
54
53
  resolveThinkingLevel,
55
54
  } from "./models.ts";
56
55
  import { monitor } from "./monitor.ts";
56
+ import { findDuplicateActiveDispatch } from "./prompt.ts";
57
57
  import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
58
+ import { emptyUsage } from "./rpc-run.ts";
58
59
  import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
59
60
  import { forkRetainedSession } from "./session-fork.ts";
60
61
  import {
61
62
  buildResumePrompt,
62
63
  getProjectRoot,
63
- PROJECT_ROOTS_DIR_NAME,
64
+ getSubagentsRoot,
64
65
  RpcRunControl,
65
66
  isFailedResult,
66
67
  isModelLevelFailure,
@@ -265,17 +266,16 @@ export interface ResumeReservation {
265
266
  }
266
267
 
267
268
  /** The dispatcher's full internal entry point; the public tool surface only
268
- * uses the first four parameters plus the per-call `thinking` request. */
269
+ * uses the first four parameters. */
269
270
  export interface StartBackgroundOptions {
270
- /** Reasoning strength this dispatch asked for; the user's manual
271
- * `/subagents-setup` choice still outranks it (see resolveDispatchModelRoute). */
272
- thinking?: ThinkingLevel;
273
271
  /** Resume path only: the thread whose retained context continues. */
274
272
  existingThread?: SubagentThread;
275
273
  appendedObjectiveOnResume?: boolean;
276
274
  environment?: DispatchEnvironment;
277
275
  seed?: SessionSeed;
278
276
  resumeReservation?: ResumeReservation;
277
+ /** Chosen by the tool call before the queue can start a fast child. */
278
+ deliveryRoute?: "background" | "await";
279
279
  }
280
280
 
281
281
  export type StartBackgroundInternal = (
@@ -307,7 +307,6 @@ export function resolveDispatchModelRoute(
307
307
  agent: AgentConfig,
308
308
  config: SubagentsConfig,
309
309
  ctx: ExtensionContext,
310
- requestedThinking?: ThinkingLevel,
311
310
  ): DispatchModelRoute {
312
311
  const availableModels = availableModelsInScope(ctx);
313
312
  const mainRef = currentModelRef(ctx);
@@ -316,11 +315,10 @@ export function resolveDispatchModelRoute(
316
315
  mainRef,
317
316
  availableRefs: availableModels.map(modelRef),
318
317
  });
319
- // agentThinkingLevels only holds a level the user picked by hand in
320
- // /subagents-setup (missing = Auto), so that deliberate setting outranks the
321
- // dispatching model's per-call guess, which in turn outranks frontmatter.
318
+ // A `/subagents-setup` override wins; otherwise the role default. No
319
+ // per-call or frontmatter thinking.
322
320
  const preferred =
323
- config.agentThinkingLevels[agent.name] ?? requestedThinking ?? agent.thinking ?? DEFAULT_THINKING_LEVEL;
321
+ config.agentThinkingLevels[agent.name] ?? roleThinkingLevel(agent.name);
324
322
  const thinkingLevelForModel = (ref?: string): ThinkingLevel => {
325
323
  const model = ref === mainRef && ctx.model
326
324
  ? ctx.model
@@ -372,12 +370,12 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
372
370
  startOptions: StartBackgroundOptions = {},
373
371
  ): Promise<SingleResult> => {
374
372
  const {
375
- thinking,
376
373
  existingThread,
377
374
  appendedObjectiveOnResume = false,
378
375
  environment,
379
376
  seed,
380
377
  resumeReservation,
378
+ deliveryRoute = "background",
381
379
  } = startOptions;
382
380
  if (!runtime.sessionActive) {
383
381
  return failedStartResult(agentName, task, "Parent session shut down before this subagent generation could start.");
@@ -396,43 +394,40 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
396
394
  const agent = resolveLiveAgentTools(discoveredAgent);
397
395
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
398
396
  return {
399
- ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as executor.`),
397
+ ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as artisan.`),
400
398
  isolation,
401
399
  };
402
400
  }
403
401
 
404
402
  const originalCwd = resolve(cwd ?? runCtx.cwd);
403
+ if (!existingThread) {
404
+ const duplicate = findDuplicateActiveDispatch(runtime.threads.values(), task, originalCwd);
405
+ if (duplicate) {
406
+ return failedStartResult(
407
+ agentName,
408
+ task,
409
+ `Duplicate active dispatch matches run #${duplicate.id} (${duplicate.agentName}). Use that logical thread instead; resume #${duplicate.id} when it is eligible.`,
410
+ );
411
+ }
412
+ }
405
413
  const projectRoot = getProjectRoot(runtime.configPath, originalCwd);
406
414
  const sessionsRoot = join(projectRoot, "sessions");
407
415
  const worktreesRoot = join(projectRoot, "worktrees");
408
416
  const scratchRoot = join(projectRoot, "tmp");
409
417
  const previousWorktree = existingThread?.worktree;
410
418
  let worktree = seed?.worktree ?? previousWorktree;
411
- if (isolation === "worktree") {
412
- if (worktree && worktree.state !== "active") {
413
- return {
414
- ...failedStartResult(agentName, task, `Run #${existingThread?.id ?? "?"} has no active continuation worktree.`),
415
- isolation,
416
- integrationStatus: worktree.state === "finalizing" ? "pending" : worktree.state,
417
- };
418
- }
419
- if (!worktree) {
420
- try {
421
- worktree = await createWorktreeIsolation(originalCwd, { tempBaseDir: worktreesRoot });
422
- } catch (error) {
423
- return {
424
- ...failedStartResult(agentName, task, error instanceof Error ? error.message : String(error)),
425
- isolation,
426
- };
427
- }
428
- }
419
+ if (isolation === "worktree" && worktree && worktree.state !== "active") {
420
+ return {
421
+ ...failedStartResult(agentName, task, `Run #${existingThread?.id ?? "?"} has no active continuation worktree.`),
422
+ isolation,
423
+ integrationStatus: worktree.state === "finalizing" ? "pending" : worktree.state,
424
+ };
429
425
  }
430
- const executionCwd = worktree?.cwd ?? originalCwd;
431
- const worktreeGroup = worktree ? worktreeGroupId(worktree) : undefined;
426
+ let executionCwd = worktree?.cwd ?? originalCwd;
427
+ let worktreeGroup = worktree ? worktreeGroupId(worktree) : undefined;
432
428
  // A resume re-runs at the strength its dispatch asked for, so the retained
433
429
  // request survives generations (and, via the durable record, restarts).
434
- const requestedThinking = thinking ?? existingThread?.requestedThinkingLevel;
435
- const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx, requestedThinking);
430
+ const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx);
436
431
  // Isolation is a persistent system-level invariant, not a one-shot task
437
432
  // prefix: resumes and main-model
438
433
  // handoffs all keep the same worktree boundary.
@@ -447,6 +442,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
447
442
  isolation,
448
443
  ...(worktreeGroup ? { worktreeId: worktreeGroup } : {}),
449
444
  });
445
+ runtime.claimRunDelivery(runId, deliveryRoute);
450
446
  const generation = (existingThread?.generation ?? 0) + 1;
451
447
  const pending: SingleResult = {
452
448
  ...queuedResult(route.agent, task, thinkingLevel),
@@ -493,7 +489,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
493
489
  thread.cwd = originalCwd;
494
490
  thread.executionCwd = executionCwd;
495
491
  thread.thinkingLevel = thinkingLevel;
496
- thread.requestedThinkingLevel = requestedThinking;
497
492
  thread.isolation = isolation;
498
493
  thread.worktree = worktree;
499
494
  thread.state = "queued";
@@ -517,7 +512,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
517
512
  cwd: originalCwd,
518
513
  executionCwd,
519
514
  thinkingLevel,
520
- requestedThinkingLevel: requestedThinking,
521
515
  isolation,
522
516
  worktree,
523
517
  state: "queued",
@@ -532,11 +526,12 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
532
526
  };
533
527
  runtime.threads.set(runId, thread);
534
528
  }
535
- installThreadLifecycle(thread, {
529
+ const installCurrentLifecycle = (): void => installThreadLifecycle(thread, {
536
530
  runtime,
537
531
  runCtx,
538
532
  startBackground: (...args) => startBackground(...args),
539
533
  });
534
+ if (isolation === "shared" || worktree) installCurrentLifecycle();
540
535
 
541
536
  const onLive = makeLiveHandler(runId, generation);
542
537
  // Shared write-capable runs serialize on the repository lane so their
@@ -545,6 +540,20 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
545
540
  const reserveManagedLane = isolation === "shared" && isWriteCapableAgent(agent);
546
541
  const runGeneration = async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
547
542
  if (runtime.threads.get(runId)?.generation !== generation) return;
543
+ if (isolation === "worktree" && !worktree) {
544
+ const prepared = await createWorktreeIsolation(originalCwd, { tempBaseDir: worktreesRoot });
545
+ if (runtime.threads.get(runId)?.generation !== generation || backgroundSignal.aborted) {
546
+ await prepared.discard().catch(() => undefined);
547
+ return;
548
+ }
549
+ worktree = prepared;
550
+ executionCwd = prepared.cwd;
551
+ worktreeGroup = worktreeGroupId(prepared);
552
+ thread.worktree = prepared;
553
+ thread.executionCwd = executionCwd;
554
+ monitor.setIsolation(runId, "worktree", "pending", worktreeGroup);
555
+ installCurrentLifecycle();
556
+ }
548
557
  // The generation body owns a process slot from here (a lane wait, if
549
558
  // any, was granted above). Recording the transition synchronously keeps
550
559
  // every "queued" surface truthful: only runs still pending in the pool
@@ -557,7 +566,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
557
566
  let activeIdleTimeoutMs = runConfig.idleTimeoutSec * 1000;
558
567
  try {
559
568
  const startConfig = await loadConfig(runtime.configPath);
560
- const resolvedStart = resolveDispatchModelRoute(agent, startConfig, runCtx, requestedThinking);
569
+ const resolvedStart = resolveDispatchModelRoute(agent, startConfig, runCtx);
561
570
  activeRoute = isolation === "worktree"
562
571
  ? { ...resolvedStart, agent: withWorktreeSystemPrompt(resolvedStart.agent) }
563
572
  : resolvedStart;
@@ -658,10 +667,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
658
667
  thread.lifecycleOperation === "settle" &&
659
668
  !thread.retired;
660
669
  try {
661
- // For isolated writers the apply runs only after the child settled,
662
- // so this lifecycle owner integrates the complete settled state
663
- // exactly once under the repository lane.
664
- await thread.finalizeIsolation(generation, result);
670
+ // The child process has exited. Isolated Git finalization remains
671
+ // lifecycle-owned and awaited, but no longer consumes a process slot.
672
+ if (isolation === "worktree") runtime.backgroundQueue.suspend(controller);
673
+ await thread.finalizeIsolation(generation, result);
665
674
  if (!ownsSettlement()) return;
666
675
 
667
676
  const failed = isFailedResult(result);
@@ -692,41 +701,33 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
692
701
  block: modelLevel
693
702
  ? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
694
703
  : formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) }),
695
- triggerTurn: true,
696
704
  usage: result.usage,
697
705
  };
698
- if (modelLevel) {
699
- const detail = result.errorMessage?.trim() || "model unavailable or broken";
700
- runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${detail} — task handed to the main window`, "error");
701
- } else if (dispatchFailed) {
702
- runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${result.errorMessage ?? "dispatch crashed"}`, "error");
703
- }
704
- if (failed) {
705
- runtime.sendCompletionGroup([completion]);
706
- runtime.completionBatcher.flush();
707
- } else {
708
- runtime.completionBatcher.push(completion);
709
- }
706
+ if (modelLevel) {
707
+ const detail = result.errorMessage?.trim() || "model unavailable or broken";
708
+ runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${detail} — task handed to the main window`, "error");
709
+ } else if (dispatchFailed) {
710
+ runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${result.errorMessage ?? "dispatch crashed"}`, "error");
711
+ }
712
+ runtime.publishRunCompletion(runId, completion, failed);
710
713
  } finally {
711
714
  if (ownsSettlement()) thread.lifecycleOperation = undefined;
712
715
  }
713
716
  };
714
717
  const queuedGeneration = reserveManagedLane
715
718
  ? async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
716
- // This task may spend its whole life waiting for the repository
717
- // lane (every shared write-capable generation serializes on it).
718
- // Holding a process slot while only waiting let a batch of shared
719
- // writers park the entire pool and starve independent read-only
720
- // dispatches that could have started immediately, so the slot is
721
- // released up front; the lane itself still serializes same-repo
722
- // writers, and abort/quiesce guarantees are unchanged.
719
+ // Waiting for repository serialization must not consume a process
720
+ // slot. Once the lane is granted, reacquire through the same FIFO
721
+ // scheduler before any child process can spawn.
723
722
  runtime.backgroundQueue.suspend(controller);
724
- // A lane wait is write serialization, not slot pacing: the model
725
- // must never read it as an exhausted pool.
726
723
  monitor.setWaitReason(runId, "repository-lane");
727
724
  await runInManagedRepositoryLane(
728
725
  originalCwd,
729
- () => runGeneration(backgroundSignal, controller),
726
+ async () => {
727
+ monitor.setWaitReason(runId, "process-slot");
728
+ if (!(await runtime.backgroundQueue.acquire(controller))) return;
729
+ await runGeneration(backgroundSignal, controller);
730
+ },
730
731
  backgroundSignal,
731
732
  );
732
733
  }
@@ -785,6 +786,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
785
786
  };
786
787
  thread.lastResult = crashed;
787
788
  runtime.retainSession(crashed);
789
+ if (isolation === "worktree" && thread.worktree) runtime.backgroundQueue.suspend(thread.queueController);
788
790
  await thread.finalizeIsolation(generation, crashed);
789
791
  if (!ownsSettlement()) return;
790
792
  thread.state = "failed";
@@ -798,15 +800,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
798
800
  if (!runtime.sessionActive || !ownsSettlement()) return;
799
801
  try {
800
802
  runCtx.ui.notify(`✗ ${crashed.agent} dispatch failed: ${crashed.errorMessage}`, "error");
801
- runtime.sendCompletionGroup([
802
- {
803
- agent: crashed.agent,
804
- block: formatCompletionBlock(crashed, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, crashed.projectCwd ?? originalCwd) }),
805
- triggerTurn: true,
806
- usage: crashed.usage,
807
- },
808
- ]);
809
- runtime.completionBatcher.flush();
803
+ runtime.publishRunCompletion(runId, {
804
+ agent: crashed.agent,
805
+ block: formatCompletionBlock(crashed, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, crashed.projectCwd ?? originalCwd) }),
806
+ usage: crashed.usage,
807
+ }, true);
810
808
  } catch {
811
809
  /* a second delivery failure must not throw through the queue */
812
810
  }
@@ -986,6 +984,9 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
986
984
  return failedStartResult(thread.agentName, thread.task, "resume objective must be non-blank when provided.");
987
985
  }
988
986
  if (thread.retired) return failedStartResult(thread.agentName, thread.task, `Run #${runId} was retired by subagent_stop.`);
987
+ if (thread.resumeUnavailableReason) {
988
+ return failedStartResult(thread.agentName, thread.task, thread.resumeUnavailableReason);
989
+ }
989
990
  if (thread.lifecycleOperation) {
990
991
  return failedStartResult(thread.agentName, thread.task, `Run #${runId} is already resuming.`);
991
992
  }
@@ -1182,9 +1183,6 @@ function createRestoredThread(
1182
1183
  cwd: record.cwd,
1183
1184
  executionCwd: record.executionCwd,
1184
1185
  ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
1185
- ...(record.requestedThinkingLevel
1186
- ? { requestedThinkingLevel: record.requestedThinkingLevel as ThinkingLevel }
1187
- : {}),
1188
1186
  isolation: record.isolation,
1189
1187
  worktree,
1190
1188
  state,
@@ -1249,16 +1247,53 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
1249
1247
  const worktree = record.worktree
1250
1248
  ? await restoreWorktreeIsolation(record.worktree).catch(() => undefined)
1251
1249
  : undefined;
1252
- // A worktree thread whose isolated filesystem is gone cannot continue its
1253
- // isolation invariant; surface it as failed instead of pretending.
1254
- const state: ThreadState = worktree
1255
- ? "parked"
1256
- : record.isolation === "worktree" && record.worktree
1257
- ? "failed"
1258
- : "parked";
1259
- const thread = createRestoredThread(runtime, record, worktree, state);
1250
+ const restorationFailed = record.isolation === "worktree" && record.worktree !== undefined && !worktree;
1251
+ const thread = createRestoredThread(runtime, record, worktree, restorationFailed ? "failed" : "parked");
1260
1252
  runtime.threads.set(record.runId, thread);
1261
1253
  runtime.sessionDirs.add(record.sessionDir!);
1254
+ if (restorationFailed) {
1255
+ const reason = `Run #${record.runId}'s recorded worktree could not be restored; isolated edits may be unavailable. The retained session and durable record were kept, but this thread cannot be resumed.`;
1256
+ thread.resumeUnavailableReason = reason;
1257
+ thread.restorationRecord = record;
1258
+ const previous = restoredResultFromSummary(record);
1259
+ const failed: SingleResult = {
1260
+ agent: record.agentName,
1261
+ task: record.task,
1262
+ exitCode: 1,
1263
+ messages: previous?.messages ?? [],
1264
+ stderr: reason,
1265
+ usage: previous?.usage ?? emptyUsage(),
1266
+ model: previous?.model,
1267
+ thinking: previous?.thinking,
1268
+ stopReason: "error",
1269
+ errorMessage: reason,
1270
+ dispatchFailed: true,
1271
+ sessionId: record.sessionId,
1272
+ sessionDir: record.sessionDir,
1273
+ projectCwd: record.cwd,
1274
+ runId: record.runId,
1275
+ isolation: "worktree",
1276
+ integrationStatus: "retained",
1277
+ };
1278
+ thread.lastResult = failed;
1279
+ runtime.registerRunResult(record.runId, failed);
1280
+ monitor.restoreRun({
1281
+ id: record.runId,
1282
+ agent: record.agentName,
1283
+ task: record.task,
1284
+ status: "failed",
1285
+ elapsedMs: record.elapsedMs,
1286
+ isolation: "worktree",
1287
+ integrationStatus: "retained",
1288
+ });
1289
+ runtime.claimRunDelivery(record.runId, "background");
1290
+ runtime.publishRunCompletion(record.runId, {
1291
+ agent: record.agentName,
1292
+ block: `### Subagent restoration failed: #${record.runId} ${record.agentName}\n\n${reason}`,
1293
+ usage: failed.usage,
1294
+ }, true);
1295
+ continue;
1296
+ }
1262
1297
  monitor.restoreRun({
1263
1298
  id: record.runId,
1264
1299
  agent: record.agentName,
@@ -1280,9 +1315,9 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
1280
1315
  return restoredIds;
1281
1316
  }
1282
1317
 
1283
- /** Load-time durable bootstrap: restore threads, age out expired records, and
1318
+ /** Session-start durable bootstrap: restore threads, age out expired records, and
1284
1319
  * sweep leaked temp/state directories. Every stage is best-effort so a broken
1285
- * manifest never blocks extension registration.
1320
+ * manifest never blocks the session.
1286
1321
  *
1287
1322
  * Restore is published on the runtime as `durableRestore` before this returns,
1288
1323
  * so callers that must see restored threads await that pass alone and never the
@@ -1291,9 +1326,6 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
1291
1326
  export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
1292
1327
  const restore = (async () => {
1293
1328
  try {
1294
- // Records from a pre-per-project manifest must land in their project
1295
- // roots before restore reads anything.
1296
- await migrateLegacyThreadsManifest(runtime.configPath);
1297
1329
  runtime.restoredRunIds = await restoreDurableThreads(runtime);
1298
1330
  } catch {
1299
1331
  /* restore is best-effort */
@@ -1307,7 +1339,7 @@ export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
1307
1339
  } catch {
1308
1340
  /* retention is best-effort */
1309
1341
  }
1310
- const projectRoots = join(dirname(runtime.configPath), PROJECT_ROOTS_DIR_NAME);
1342
+ const projectRoots = getSubagentsRoot(runtime.configPath);
1311
1343
  try {
1312
1344
  sweepProjectTempDirs(projectRoots);
1313
1345
  } catch {
package/src/tools.ts CHANGED
@@ -46,14 +46,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
46
46
  pi.registerTool({
47
47
  name: "subagent_control",
48
48
  label: "Subagent Control",
49
- description: [
50
- "Resume an existing sub-agent thread by stable run id: a parked, completed, or failed retained thread restarts with the same run id and cumulative active time.",
51
- "Omit objective to continue the current goal, or provide one to append it to retained context and make it the displayed goal. Threads parked or interrupted by a shutdown/reload are restorable; use subagent_stop for destructive cancellation.",
52
- ].join(" "),
53
- promptSnippet: "Resume a parked or settled subagent thread with its retained context.",
54
- promptGuidelines: [
55
- "Resume keeps the run id and retained context; use subagent_stop only for destructive cancellation, which retires that thread's session.",
56
- ],
49
+ description: "Resume a parked or settled child thread by run id, reusing its retained session when available. An optional objective is appended to its current goal.",
57
50
  parameters: SubagentControlParams,
58
51
 
59
52
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -91,10 +84,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
91
84
  const mode = objective
92
85
  ? `appended objective: ${currentObjective}`
93
86
  : `continuing current objective: ${currentObjective}`;
94
- const context = hadRetainedSession
95
- ? "the same retained session and prior context are preserved"
96
- : "no prior child session existed, so only the logical run and objective are continued";
97
- return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. It runs in the background — keep working; the result resumes you automatically.` }], details: {} };
87
+ const context = hadRetainedSession ? "retained context reused" : "no prior child context";
88
+ return { content: [{ type: "text", text: `Resumed run #${thread.id}: ${mode}; ${context}.` }], details: {} };
98
89
  }
99
90
  }
100
91
  } catch (error) {
@@ -125,14 +116,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
125
116
  pi.registerTool({
126
117
  name: "subagent_stop",
127
118
  label: "Subagent Stop",
128
- description: [
129
- "Destructively stop a sub-agent thread: terminate active work, deliver its aborted partial result, and retire any retained session so it cannot be resumed.",
130
- "Pass id (run id or prefix) to stop one active, parked, or completed thread; all: true stops every active run.",
131
- ].join(" "),
132
- promptSnippet: "Stop a running background subagent (id from dispatch output; or all: true).",
133
- promptGuidelines: [
134
- "Stop a run when its task is obsolete, stuck, or superseded — do not leave it burning tokens. It then reports as failed with 'aborted' plus its partial output.",
135
- ],
119
+ description: "Stop and retire one child thread by run id/prefix, or every active thread with all: true.",
136
120
  parameters: SubagentStopParams,
137
121
 
138
122
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -1,54 +0,0 @@
1
- ---
2
- name: executor
3
- description: A self-contained unit that changes the repository or condenses inputs — implement, fix, refactor, test, clean up, sync docs, merge fan-out results — carried through verification to a result-only handoff.
4
- thinking: high
5
- # No `tools` field => inherits all tools (full capability).
6
- ---
7
-
8
- You are an executor agent with full capabilities in an isolated context window. You own one delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
9
-
10
- Repository instructions (AGENTS.md) and any skills available in this session apply to you as to any agent: follow their process for the domains they own (language style, tests, debugging, cleanup discipline, verification). Where a skill covers the same ground as this brief, the skill's discipline wins — except for the release boundary below, which always wins.
11
-
12
- ## Procedure
13
-
14
- 1. **Context.** Read the brief fully, plus referenced files and images, before acting. If critical context is missing, state what is missing rather than guessing.
15
- 2. **Plan.** Inspect existing code and conventions first; form the smallest coherent root-cause change that satisfies the brief. Prefer the design that deletes complexity over one that rearranges it. No unrelated refactors or standalone docs work unless the brief asks.
16
- 3. **Confirm.** A finding is not a change. Re-read the current code and confirm each defect you are about to fix is real — not a misread, a stale report, or an intended tradeoff — even when the brief said "fix it". A false positive means zero edits and a note.
17
- 4. **Implement.** Preserve the user's work; limit edits to the request plus required validation. Follow the project's error handling, naming, and style. Synchronize README/docs/comments your change directly affects; never defer that drift.
18
- 5. **Verify.** Run the project's format/build/tests when they exist. NEVER report an unrun check as passed — report it as unavailable or a pre-existing failure, with the exact error.
19
-
20
- ## Conditional playbooks
21
-
22
- Skip both unless the brief matches one.
23
-
24
- **Cleanup** (dead code, duplication, simplification): a candidate is not a deletion. Re-read the load-bearing files and repeat the decisive searches yourself — never inherit proof from another agent's report — and search the whole repository for consumers first. Keep a candidate when a real consumer exists, dynamic reachability is unresolved, or the cut removes a user capability, public API, persisted format, or compatibility path the brief did not explicitly approve. Finding no safe cut and making zero edits is valid.
25
-
26
- **Merging inputs** (result artifacts, reports, logs): read every named input fully before writing, deduplicate restatements into one attributed entry, and report surviving conflicts side by side instead of averaging them away. Stay within the named inputs; report what they cannot answer as a gap.
27
-
28
- ## Boundaries
29
-
30
- - Never commit, push, publish, tag, release, or bump a package version — the caller owns every release action, even when repository instructions normally automate release after green checks.
31
- - Children are leaf processes: you cannot dispatch sub-agents.
32
- - Never change runtime behavior to make documentation true; report the defect instead.
33
-
34
- ## Output format
35
-
36
- Return only the concrete outcome. Do not repeat the task brief, the plan, the root-cause investigation, or the tool chronology.
37
-
38
- ## Completed
39
-
40
- What was done, in a few lines.
41
-
42
- ## Files Changed
43
-
44
- - `path/to/file.ts` — what changed.
45
-
46
- ## Verification
47
-
48
- Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean). State explicitly anything you could not run and why.
49
-
50
- ## Notes (only when material)
51
-
52
- Unresolved blockers, rejected requirements, or decisions the caller must know. Omit when nothing actionable.
53
-
54
- Keep the final response comfortably below the 40-line delivery cap unless the result genuinely requires more.
@@ -1,37 +0,0 @@
1
- ---
2
- name: explorer
3
- description: Fast read-only reconnaissance for broad or multi-file search in unfamiliar areas; returns exact paths and compressed findings as retrieval leads.
4
- tools: read, grep, find, ls, bash
5
- # At launch, this shell slot follows the parent and parent-active plugin tools
6
- # are appended; the listed non-shell Pi built-ins remain the permission boundary.
7
- thinking: low
8
- ---
9
-
10
- You are an explorer agent: a fast, read-only reconnaissance specialist. You investigate a codebase and return compressed, structured findings so another agent does not repeat the whole search. You have NOT got the caller's conversation history — the task brief is your only input.
11
-
12
- ## Hard constraints
13
-
14
- - You are READ-ONLY. Never create, edit, or delete files; never run mutating commands. Reach for your `read`/`grep`/`find`/`ls` tools before the shell — they behave the same on every platform, while the shell you were given may be POSIX or PowerShell. Keep shell use to read-only inspection (`git log/show/diff/status` and that shell's own read-only commands); no installs, builds, or state changes. Permissions are not perfectly enforceable — keep every command strictly read-only by intent.
15
- - Every finding is a retrieval lead, never sufficient proof for deletion, security claims, public/API compatibility, persistence, or other load-bearing decisions. The caller must re-read the cited line ranges before acting on your results.
16
-
17
- ## Workflow
18
-
19
- 1. Orient with `grep`/`find` to locate the relevant code fast. Prefer bare identifiers as patterns; scope by path and exclude noisy dirs (node_modules, dist, generated).
20
- 2. Read KEY SECTIONS, not whole files. After 1-2 greps, read the top match instead of running more greps.
21
- 3. Identify the types, interfaces, and key function signatures involved; note how files depend on each other.
22
- 4. Record exact paths and line ranges so the caller can jump straight in.
23
- 5. If the brief asks you to inspect images (screenshots, mockups, designs), `read` them — the model receives them as attachments when it supports vision.
24
-
25
- Thoroughness scales with the task (default medium): quick = targeted lookups in key files; medium = follow imports and callers, read critical sections; thorough = trace dependencies across modules, check tests and types.
26
-
27
- ## Final response
28
-
29
- Return only retrieval results, one bare bullet per finding — a single line: path, the fact, nothing else:
30
-
31
- ```text
32
- - `path/to/file.ts:10-50` — the fact
33
- Start here: `path/to/file.ts` — entry symbol and why (only when the caller could not guess it)
34
- Gaps: unresolved uncertainty (only when real)
35
- ```
36
-
37
- No preamble or closing summary. Do not repeat the task brief, inventory every file opened, paste nonessential code, or narrate the search; every line must carry a path with a fact or name a gap — delete anything else. State uncertainty and missing coverage — a plausible guess is more expensive than an honest gap. Stay under 15 lines by default; go longer only when the brief genuinely demands a wide survey — the 40-line delivery cap truncates your tail (usually the Gaps) and the caller pays for every line.