@dungle-scrubs/tallow 0.8.7 → 0.8.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.
- package/README.md +1 -1
- package/dist/cli.js +244 -54
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/sdk.d.ts +39 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +322 -13
- package/dist/sdk.js.map +1 -1
- package/dist/session-utils.d.ts +8 -0
- package/dist/session-utils.d.ts.map +1 -1
- package/dist/session-utils.js +38 -1
- package/dist/session-utils.js.map +1 -1
- package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
- package/extensions/__integration__/worktree.test.ts +88 -0
- package/extensions/_icons/index.ts +2 -3
- package/extensions/_shared/inline-preview.ts +2 -4
- package/extensions/_shared/interop-events.ts +48 -0
- package/extensions/_shared/shell-policy.ts +2 -1
- package/extensions/_shared/tallow-paths.ts +48 -0
- package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
- package/extensions/agent-commands-tool/index.ts +38 -6
- package/extensions/background-task-tool/index.ts +2 -2
- package/extensions/bash-tool-enhanced/index.ts +3 -4
- package/extensions/cd-tool/index.ts +3 -3
- package/extensions/claude-bridge/index.ts +2 -1
- package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
- package/extensions/command-prompt/index.ts +36 -0
- package/extensions/context-files/index.ts +2 -1
- package/extensions/context-fork/index.ts +2 -1
- package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
- package/extensions/context-usage/index.ts +169 -8
- package/extensions/debug/__tests__/analysis.test.ts +35 -2
- package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
- package/extensions/debug/analysis.ts +53 -16
- package/extensions/debug/index.ts +84 -10
- package/extensions/debug/logger.ts +2 -2
- package/extensions/health/index.ts +2 -2
- package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
- package/extensions/hooks/extension.json +3 -1
- package/extensions/hooks/hooks.schema.json +17 -1
- package/extensions/hooks/index.ts +52 -7
- package/extensions/lsp/index.ts +2 -7
- package/extensions/output-styles-tool/index.ts +2 -4
- package/extensions/plan-mode-tool/extension.json +1 -0
- package/extensions/plan-mode-tool/index.ts +119 -4
- package/extensions/prompt-suggestions/index.ts +2 -3
- package/extensions/random-spinner/index.ts +2 -3
- package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
- package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
- package/extensions/read-tool-enhanced/extension.json +4 -4
- package/extensions/read-tool-enhanced/index.ts +112 -10
- package/extensions/read-tool-enhanced/notebook.ts +526 -0
- package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
- package/extensions/rewind/snapshots.ts +18 -6
- package/extensions/session-memory/index.ts +3 -2
- package/extensions/stats/stats-log.ts +2 -3
- package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
- package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
- package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
- package/extensions/subagent-tool/agents.ts +77 -16
- package/extensions/subagent-tool/index.ts +213 -48
- package/extensions/subagent-tool/model-router.ts +3 -3
- package/extensions/subagent-tool/process.ts +233 -22
- package/extensions/subagent-tool/schema.ts +11 -0
- package/extensions/subagent-tool/widget.ts +15 -2
- package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
- package/extensions/tasks/state/index.ts +2 -2
- package/extensions/theme-selector/index.ts +3 -7
- package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
- package/extensions/worktree/extension.json +31 -0
- package/extensions/worktree/index.ts +149 -0
- package/extensions/worktree/lifecycle.ts +372 -0
- package/package.json +1 -1
- package/skills/tallow-expert/SKILL.md +1 -1
|
@@ -14,13 +14,23 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
|
14
14
|
import type { Message } from "@mariozechner/pi-ai";
|
|
15
15
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
16
|
import { extractPreview, isInlineResultsEnabled } from "../_shared/inline-preview.js";
|
|
17
|
+
import {
|
|
18
|
+
emitWorktreeLifecycleEvent,
|
|
19
|
+
type WorktreeLifecycleScope,
|
|
20
|
+
} from "../_shared/interop-events.js";
|
|
17
21
|
import { expandFileReferences } from "../file-reference/index.js";
|
|
22
|
+
import { createWorktree, removeWorktree, validateGitRepo } from "../worktree/lifecycle.js";
|
|
18
23
|
import type { AgentConfig, AgentDefaults } from "./agents.js";
|
|
19
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
computeEffectiveTools,
|
|
26
|
+
resolveAgentForExecution,
|
|
27
|
+
resolveEffectiveIsolation,
|
|
28
|
+
} from "./agents.js";
|
|
20
29
|
import { getFinalOutput, type SingleResult, type SubagentDetails } from "./formatting.js";
|
|
21
30
|
import type { RoutingHints } from "./model-router.js";
|
|
22
31
|
import { routeModel } from "./model-router.js";
|
|
23
32
|
import type {
|
|
33
|
+
IsolationMode,
|
|
24
34
|
SubagentCompleteDetails,
|
|
25
35
|
SubagentStartEvent,
|
|
26
36
|
SubagentStopEvent,
|
|
@@ -266,6 +276,100 @@ export function applyBackgroundResultRetention(
|
|
|
266
276
|
subagent.historyRetainedMessageCount = compacted.retainedMessageCount;
|
|
267
277
|
}
|
|
268
278
|
|
|
279
|
+
/** Managed isolation metadata for one subagent invocation. */
|
|
280
|
+
interface SubagentIsolationContext {
|
|
281
|
+
readonly mode: IsolationMode;
|
|
282
|
+
readonly repoRoot: string;
|
|
283
|
+
readonly worktreePath: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Provision execution isolation for one subagent invocation.
|
|
288
|
+
*
|
|
289
|
+
* @param baseCwd - Base working directory before isolation
|
|
290
|
+
* @param requestedIsolation - Explicit per-call isolation, if any
|
|
291
|
+
* @param agent - Resolved agent config
|
|
292
|
+
* @param defaults - Defaults from _defaults.md
|
|
293
|
+
* @param lifecycleScope - Lifecycle scope for emitted events
|
|
294
|
+
* @param lifecycleId - Subagent/task identifier for lifecycle payloads
|
|
295
|
+
* @param events - Optional event bus for lifecycle hooks
|
|
296
|
+
* @returns Effective cwd and optional isolation context
|
|
297
|
+
*/
|
|
298
|
+
function provisionIsolation(
|
|
299
|
+
baseCwd: string,
|
|
300
|
+
requestedIsolation: IsolationMode | undefined,
|
|
301
|
+
agent: AgentConfig,
|
|
302
|
+
defaults: AgentDefaults | undefined,
|
|
303
|
+
lifecycleScope: WorktreeLifecycleScope,
|
|
304
|
+
lifecycleId: string,
|
|
305
|
+
events?: ExtensionAPI["events"]
|
|
306
|
+
): {
|
|
307
|
+
readonly isolation: SubagentIsolationContext | undefined;
|
|
308
|
+
readonly workingDirectory: string;
|
|
309
|
+
} {
|
|
310
|
+
const isolationMode = resolveEffectiveIsolation(
|
|
311
|
+
requestedIsolation,
|
|
312
|
+
agent.isolation,
|
|
313
|
+
defaults?.isolation
|
|
314
|
+
);
|
|
315
|
+
if (isolationMode !== "worktree") {
|
|
316
|
+
return { isolation: undefined, workingDirectory: baseCwd };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const repoRoot = validateGitRepo(baseCwd).repoRoot;
|
|
320
|
+
const created = createWorktree(repoRoot, {
|
|
321
|
+
agentId: lifecycleScope === "subagent" ? lifecycleId : undefined,
|
|
322
|
+
id: lifecycleId,
|
|
323
|
+
scope: "subagent",
|
|
324
|
+
});
|
|
325
|
+
const isolation: SubagentIsolationContext = {
|
|
326
|
+
mode: "worktree",
|
|
327
|
+
repoRoot,
|
|
328
|
+
worktreePath: created.worktreePath,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (events) {
|
|
332
|
+
emitWorktreeLifecycleEvent(events, "worktree_create", {
|
|
333
|
+
agentId: lifecycleScope === "subagent" ? lifecycleId : undefined,
|
|
334
|
+
repoRoot,
|
|
335
|
+
scope: lifecycleScope,
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
worktreePath: created.worktreePath,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
isolation,
|
|
343
|
+
workingDirectory: created.worktreePath,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Cleanup worktree isolation for one subagent invocation.
|
|
349
|
+
*
|
|
350
|
+
* @param lifecycleScope - Lifecycle scope for emitted events
|
|
351
|
+
* @param lifecycleId - Subagent/task identifier
|
|
352
|
+
* @param isolation - Isolation context to cleanup
|
|
353
|
+
* @param events - Optional event bus for lifecycle hooks
|
|
354
|
+
*/
|
|
355
|
+
function cleanupIsolation(
|
|
356
|
+
lifecycleScope: WorktreeLifecycleScope,
|
|
357
|
+
lifecycleId: string,
|
|
358
|
+
isolation: SubagentIsolationContext | undefined,
|
|
359
|
+
events?: ExtensionAPI["events"]
|
|
360
|
+
): void {
|
|
361
|
+
if (!isolation) return;
|
|
362
|
+
removeWorktree(isolation.worktreePath);
|
|
363
|
+
if (!events) return;
|
|
364
|
+
emitWorktreeLifecycleEvent(events, "worktree_remove", {
|
|
365
|
+
agentId: lifecycleScope === "subagent" ? lifecycleId : undefined,
|
|
366
|
+
repoRoot: isolation.repoRoot,
|
|
367
|
+
scope: lifecycleScope,
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
worktreePath: isolation.worktreePath,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
269
373
|
/**
|
|
270
374
|
* Write a subagent prompt to a temporary file for the pi subprocess.
|
|
271
375
|
* @param agentName - Agent name (sanitized for filename)
|
|
@@ -513,6 +617,7 @@ export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => vo
|
|
|
513
617
|
* @param parentModelId - Parent model ID for inheritance
|
|
514
618
|
* @param defaults - Optional agent defaults
|
|
515
619
|
* @param hints - Optional routing hints
|
|
620
|
+
* @param isolationOverride - Optional per-call isolation override
|
|
516
621
|
* @returns Background subagent ID, error string if model unresolvable, or null if agent not found
|
|
517
622
|
*/
|
|
518
623
|
export async function spawnBackgroundSubagent(
|
|
@@ -526,11 +631,14 @@ export async function spawnBackgroundSubagent(
|
|
|
526
631
|
modelOverride?: string,
|
|
527
632
|
parentModelId?: string,
|
|
528
633
|
defaults?: AgentDefaults,
|
|
529
|
-
hints?: RoutingHints
|
|
634
|
+
hints?: RoutingHints,
|
|
635
|
+
isolationOverride?: IsolationMode
|
|
530
636
|
): Promise<string | null> {
|
|
531
637
|
const resolved = resolveAgentForExecution(agentName, agents, defaults);
|
|
532
|
-
const
|
|
533
|
-
|
|
638
|
+
const id = `bg_${generateId()}`;
|
|
639
|
+
const requestedCwd = cwd ?? defaultCwd;
|
|
640
|
+
|
|
641
|
+
// Route model via fuzzy resolution + auto-routing.
|
|
534
642
|
const routing = await routeModel(
|
|
535
643
|
task,
|
|
536
644
|
modelOverride,
|
|
@@ -538,9 +646,29 @@ export async function spawnBackgroundSubagent(
|
|
|
538
646
|
parentModelId,
|
|
539
647
|
resolved.agent.description,
|
|
540
648
|
hints,
|
|
541
|
-
|
|
649
|
+
requestedCwd
|
|
542
650
|
);
|
|
543
651
|
if (!routing.ok) return routing.error;
|
|
652
|
+
|
|
653
|
+
let isolationContext: SubagentIsolationContext | undefined;
|
|
654
|
+
let effectiveCwd = requestedCwd;
|
|
655
|
+
try {
|
|
656
|
+
const provisioned = provisionIsolation(
|
|
657
|
+
requestedCwd,
|
|
658
|
+
isolationOverride,
|
|
659
|
+
resolved.agent,
|
|
660
|
+
defaults,
|
|
661
|
+
"subagent",
|
|
662
|
+
id,
|
|
663
|
+
piEvents
|
|
664
|
+
);
|
|
665
|
+
isolationContext = provisioned.isolation;
|
|
666
|
+
effectiveCwd = provisioned.workingDirectory;
|
|
667
|
+
} catch (error) {
|
|
668
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
669
|
+
return `Failed to create worktree isolation for ${agentName}: ${reason}`;
|
|
670
|
+
}
|
|
671
|
+
|
|
544
672
|
const agent = { ...resolved.agent, model: routing.model.id };
|
|
545
673
|
const agentSource = resolved.resolution === "ephemeral" ? ("ephemeral" as const) : agent.source;
|
|
546
674
|
|
|
@@ -573,7 +701,14 @@ export async function spawnBackgroundSubagent(
|
|
|
573
701
|
args.push("--append-system-prompt", tmpPromptPath);
|
|
574
702
|
}
|
|
575
703
|
|
|
576
|
-
|
|
704
|
+
let expandedTask: string;
|
|
705
|
+
try {
|
|
706
|
+
expandedTask = await expandFileReferences(task, effectiveCwd);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
cleanupIsolation("subagent", id, isolationContext, piEvents);
|
|
709
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
710
|
+
return `Failed to expand task references for ${agentName}: ${reason}`;
|
|
711
|
+
}
|
|
577
712
|
args.push(`Task: ${expandedTask}`);
|
|
578
713
|
|
|
579
714
|
const childEnv: Record<string, string> = { ...process.env, PI_IS_SUBAGENT: "1" } as Record<
|
|
@@ -587,14 +722,19 @@ export async function spawnBackgroundSubagent(
|
|
|
587
722
|
childEnv.PI_MCP_SERVERS = agent.mcpServers.join(",");
|
|
588
723
|
}
|
|
589
724
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
725
|
+
let proc: ReturnType<typeof spawn>;
|
|
726
|
+
try {
|
|
727
|
+
proc = spawn("pi", args, {
|
|
728
|
+
cwd: effectiveCwd,
|
|
729
|
+
shell: false,
|
|
730
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
731
|
+
env: childEnv,
|
|
732
|
+
});
|
|
733
|
+
} catch (error) {
|
|
734
|
+
cleanupIsolation("subagent", id, isolationContext, piEvents);
|
|
735
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
736
|
+
return `Failed to spawn background subagent ${agentName}: ${reason}`;
|
|
737
|
+
}
|
|
598
738
|
|
|
599
739
|
// Emit subagent_start event
|
|
600
740
|
piEvents?.emit("subagent_start", {
|
|
@@ -627,6 +767,7 @@ export async function spawnBackgroundSubagent(
|
|
|
627
767
|
const bgSubagent: BackgroundSubagent = {
|
|
628
768
|
id,
|
|
629
769
|
agent: agentName,
|
|
770
|
+
isolationMode: isolationContext?.mode,
|
|
630
771
|
model: agent.model,
|
|
631
772
|
task,
|
|
632
773
|
startTime: Date.now(),
|
|
@@ -635,12 +776,25 @@ export async function spawnBackgroundSubagent(
|
|
|
635
776
|
status: "running",
|
|
636
777
|
tmpPromptDir,
|
|
637
778
|
tmpPromptPath,
|
|
779
|
+
worktreePath: isolationContext?.worktreePath,
|
|
638
780
|
};
|
|
639
781
|
|
|
640
782
|
backgroundSubagents.set(id, bgSubagent);
|
|
641
783
|
publishSubagentSnapshot(piEvents);
|
|
642
784
|
startBackgroundSubagentCleanupLoop(piEvents);
|
|
643
785
|
|
|
786
|
+
let isolationCleaned = false;
|
|
787
|
+
const cleanupBackgroundIsolation = () => {
|
|
788
|
+
if (isolationCleaned) return;
|
|
789
|
+
isolationCleaned = true;
|
|
790
|
+
cleanupIsolation("subagent", id, isolationContext, piEvents);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
if (!proc.stdout || !proc.stderr) {
|
|
794
|
+
cleanupBackgroundIsolation();
|
|
795
|
+
return `Failed to spawn background subagent ${agentName}: missing stdio pipes`;
|
|
796
|
+
}
|
|
797
|
+
|
|
644
798
|
// Collect output
|
|
645
799
|
let buffer = "";
|
|
646
800
|
let bgTurnCount = 0;
|
|
@@ -714,6 +868,11 @@ export async function spawnBackgroundSubagent(
|
|
|
714
868
|
result.stderr += data.toString();
|
|
715
869
|
});
|
|
716
870
|
|
|
871
|
+
proc.on("error", (error) => {
|
|
872
|
+
result.stderr += error.message;
|
|
873
|
+
cleanupBackgroundIsolation();
|
|
874
|
+
});
|
|
875
|
+
|
|
717
876
|
proc.on("close", (code) => {
|
|
718
877
|
if (buffer.trim()) {
|
|
719
878
|
try {
|
|
@@ -757,6 +916,7 @@ export async function spawnBackgroundSubagent(
|
|
|
757
916
|
/* ignore */
|
|
758
917
|
}
|
|
759
918
|
|
|
919
|
+
cleanupBackgroundIsolation();
|
|
760
920
|
updateWidget();
|
|
761
921
|
|
|
762
922
|
// Post inline result for background subagent completion
|
|
@@ -812,6 +972,7 @@ export async function spawnBackgroundSubagent(
|
|
|
812
972
|
* @param parentModelId - Parent model ID for inheritance
|
|
813
973
|
* @param defaults - Optional agent defaults
|
|
814
974
|
* @param hints - Optional routing hints
|
|
975
|
+
* @param isolationOverride - Optional per-call isolation override
|
|
815
976
|
* @returns Result from the subagent execution
|
|
816
977
|
*/
|
|
817
978
|
export async function runSingleAgent(
|
|
@@ -829,10 +990,11 @@ export async function runSingleAgent(
|
|
|
829
990
|
modelOverride?: string,
|
|
830
991
|
parentModelId?: string,
|
|
831
992
|
defaults?: AgentDefaults,
|
|
832
|
-
hints?: RoutingHints
|
|
993
|
+
hints?: RoutingHints,
|
|
994
|
+
isolationOverride?: IsolationMode
|
|
833
995
|
): Promise<SingleResult> {
|
|
834
996
|
const resolved = resolveAgentForExecution(agentName, agents, defaults);
|
|
835
|
-
const
|
|
997
|
+
const requestedCwd = cwd ?? defaultCwd;
|
|
836
998
|
// Route model via fuzzy resolution + auto-routing
|
|
837
999
|
const routing = await routeModel(
|
|
838
1000
|
task,
|
|
@@ -841,7 +1003,7 @@ export async function runSingleAgent(
|
|
|
841
1003
|
parentModelId,
|
|
842
1004
|
resolved.agent.description,
|
|
843
1005
|
hints,
|
|
844
|
-
|
|
1006
|
+
requestedCwd
|
|
845
1007
|
);
|
|
846
1008
|
if (!routing.ok) {
|
|
847
1009
|
// Return a failed SingleResult so the caller can surface the error
|
|
@@ -870,7 +1032,53 @@ export async function runSingleAgent(
|
|
|
870
1032
|
const agentSource = resolved.resolution === "ephemeral" ? ("ephemeral" as const) : agent.source;
|
|
871
1033
|
const taskId = `fg_${generateId()}`;
|
|
872
1034
|
|
|
873
|
-
|
|
1035
|
+
let isolationContext: SubagentIsolationContext | undefined;
|
|
1036
|
+
let effectiveCwd = requestedCwd;
|
|
1037
|
+
try {
|
|
1038
|
+
const provisioned = provisionIsolation(
|
|
1039
|
+
requestedCwd,
|
|
1040
|
+
isolationOverride,
|
|
1041
|
+
agent,
|
|
1042
|
+
defaults,
|
|
1043
|
+
"subagent",
|
|
1044
|
+
taskId,
|
|
1045
|
+
piEvents
|
|
1046
|
+
);
|
|
1047
|
+
isolationContext = provisioned.isolation;
|
|
1048
|
+
effectiveCwd = provisioned.workingDirectory;
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1051
|
+
return {
|
|
1052
|
+
agent: agentName,
|
|
1053
|
+
agentSource,
|
|
1054
|
+
task,
|
|
1055
|
+
exitCode: 1,
|
|
1056
|
+
messages: [],
|
|
1057
|
+
stderr: message,
|
|
1058
|
+
usage: {
|
|
1059
|
+
input: 0,
|
|
1060
|
+
output: 0,
|
|
1061
|
+
cacheRead: 0,
|
|
1062
|
+
cacheWrite: 0,
|
|
1063
|
+
cost: 0,
|
|
1064
|
+
contextTokens: 0,
|
|
1065
|
+
turns: 0,
|
|
1066
|
+
denials: 0,
|
|
1067
|
+
},
|
|
1068
|
+
errorMessage: message,
|
|
1069
|
+
step,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
registerForegroundSubagent(
|
|
1074
|
+
taskId,
|
|
1075
|
+
agentName,
|
|
1076
|
+
task,
|
|
1077
|
+
Date.now(),
|
|
1078
|
+
piEvents,
|
|
1079
|
+
agent.model,
|
|
1080
|
+
isolationContext?.mode
|
|
1081
|
+
);
|
|
874
1082
|
|
|
875
1083
|
// Emit subagent_start event
|
|
876
1084
|
piEvents?.emit("subagent_start", {
|
|
@@ -993,7 +1201,7 @@ export async function runSingleAgent(
|
|
|
993
1201
|
let fgTurnCount = 0;
|
|
994
1202
|
const exitCode = await new Promise<number>((resolve) => {
|
|
995
1203
|
const proc = spawn("pi", args, {
|
|
996
|
-
cwd:
|
|
1204
|
+
cwd: effectiveCwd,
|
|
997
1205
|
shell: false,
|
|
998
1206
|
stdio: ["ignore", "pipe", "pipe"],
|
|
999
1207
|
env: fgChildEnv,
|
|
@@ -1210,7 +1418,7 @@ export async function runSingleAgent(
|
|
|
1210
1418
|
agents,
|
|
1211
1419
|
agentName,
|
|
1212
1420
|
task,
|
|
1213
|
-
|
|
1421
|
+
effectiveCwd,
|
|
1214
1422
|
step,
|
|
1215
1423
|
signal,
|
|
1216
1424
|
onUpdate,
|
|
@@ -1219,8 +1427,10 @@ export async function runSingleAgent(
|
|
|
1219
1427
|
session,
|
|
1220
1428
|
nextModel.id,
|
|
1221
1429
|
parentModelId,
|
|
1222
|
-
defaults
|
|
1223
|
-
|
|
1430
|
+
defaults,
|
|
1431
|
+
undefined,
|
|
1432
|
+
undefined
|
|
1433
|
+
// Clear hints and isolation override — explicit model + existing cwd are used
|
|
1224
1434
|
);
|
|
1225
1435
|
}
|
|
1226
1436
|
|
|
@@ -1228,5 +1438,6 @@ export async function runSingleAgent(
|
|
|
1228
1438
|
} finally {
|
|
1229
1439
|
completeForegroundSubagent(taskId, piEvents);
|
|
1230
1440
|
cleanupTempFiles();
|
|
1441
|
+
cleanupIsolation("subagent", taskId, isolationContext, piEvents);
|
|
1231
1442
|
}
|
|
1232
1443
|
}
|
|
@@ -7,6 +7,14 @@ import { Type } from "@sinclair/typebox";
|
|
|
7
7
|
|
|
8
8
|
// ── Parameter Schemas ────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
+
export const IsolationModeSchema = StringEnum(["worktree"] as const, {
|
|
11
|
+
description:
|
|
12
|
+
'Isolation strategy for subagent execution. "worktree" runs the subagent in a temporary detached git worktree.',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Isolation mode supported by the subagent tool. */
|
|
16
|
+
export type IsolationMode = "worktree";
|
|
17
|
+
|
|
10
18
|
export const TaskItem = Type.Object({
|
|
11
19
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
12
20
|
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
@@ -14,6 +22,7 @@ export const TaskItem = Type.Object({
|
|
|
14
22
|
model: Type.Optional(
|
|
15
23
|
Type.String({ description: "Model ID to use for this agent (overrides agent default)" })
|
|
16
24
|
),
|
|
25
|
+
isolation: Type.Optional(IsolationModeSchema),
|
|
17
26
|
});
|
|
18
27
|
|
|
19
28
|
export const CentipedeItem = Type.Object({
|
|
@@ -25,6 +34,7 @@ export const CentipedeItem = Type.Object({
|
|
|
25
34
|
model: Type.Optional(
|
|
26
35
|
Type.String({ description: "Model ID to use for this step (overrides agent default)" })
|
|
27
36
|
),
|
|
37
|
+
isolation: Type.Optional(IsolationModeSchema),
|
|
28
38
|
});
|
|
29
39
|
|
|
30
40
|
export const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
@@ -58,6 +68,7 @@ export const SubagentParams = Type.Object({
|
|
|
58
68
|
cwd: Type.Optional(
|
|
59
69
|
Type.String({ description: "Working directory for the agent process (single mode)" })
|
|
60
70
|
),
|
|
71
|
+
isolation: Type.Optional(IsolationModeSchema),
|
|
61
72
|
background: Type.Optional(
|
|
62
73
|
Type.Boolean({
|
|
63
74
|
description: "Run in background, return immediately. Use subagent_status to check.",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type InteropSubagentView,
|
|
17
17
|
} from "../_shared/interop-events.js";
|
|
18
18
|
import { getFinalOutput, type SingleResult } from "./formatting.js";
|
|
19
|
+
import type { IsolationMode } from "./schema.js";
|
|
19
20
|
|
|
20
21
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -23,6 +24,7 @@ import { getFinalOutput, type SingleResult } from "./formatting.js";
|
|
|
23
24
|
export interface RunningSubagent {
|
|
24
25
|
id: string;
|
|
25
26
|
agent: string;
|
|
27
|
+
isolationMode?: IsolationMode;
|
|
26
28
|
model?: string;
|
|
27
29
|
task: string;
|
|
28
30
|
startTime: number;
|
|
@@ -37,6 +39,7 @@ export interface BackgroundSubagent {
|
|
|
37
39
|
historyOriginalMessageCount?: number;
|
|
38
40
|
historyRetainedMessageCount?: number;
|
|
39
41
|
id: string;
|
|
42
|
+
isolationMode?: IsolationMode;
|
|
40
43
|
model?: string;
|
|
41
44
|
process: ReturnType<typeof spawn>;
|
|
42
45
|
result: SingleResult;
|
|
@@ -46,6 +49,7 @@ export interface BackgroundSubagent {
|
|
|
46
49
|
task: string;
|
|
47
50
|
tmpPromptDir?: string;
|
|
48
51
|
tmpPromptPath?: string;
|
|
52
|
+
worktreePath?: string;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
@@ -324,9 +328,18 @@ export function registerForegroundSubagent(
|
|
|
324
328
|
task: string,
|
|
325
329
|
startTime: number,
|
|
326
330
|
piEvents?: ExtensionAPI["events"],
|
|
327
|
-
model?: string
|
|
331
|
+
model?: string,
|
|
332
|
+
isolationMode?: IsolationMode
|
|
328
333
|
): void {
|
|
329
|
-
runningSubagents.set(id, {
|
|
334
|
+
runningSubagents.set(id, {
|
|
335
|
+
id,
|
|
336
|
+
agent,
|
|
337
|
+
isolationMode,
|
|
338
|
+
model,
|
|
339
|
+
task,
|
|
340
|
+
startTime,
|
|
341
|
+
status: "running",
|
|
342
|
+
});
|
|
330
343
|
publishSubagentSnapshot(piEvents);
|
|
331
344
|
startWidgetUpdates();
|
|
332
345
|
}
|
|
@@ -273,8 +273,8 @@ afterEach(() => {
|
|
|
273
273
|
});
|
|
274
274
|
|
|
275
275
|
describe("tasks widget subagent sections", () => {
|
|
276
|
-
it("
|
|
277
|
-
const fixture = await createFixture();
|
|
276
|
+
it("omits subagent sections for foreground-only snapshots", async () => {
|
|
277
|
+
const fixture = await createFixture("Keep task list visible");
|
|
278
278
|
try {
|
|
279
279
|
emitSubagentsSnapshot(fixture.harness, {
|
|
280
280
|
foreground: [
|
|
@@ -289,7 +289,8 @@ describe("tasks widget subagent sections", () => {
|
|
|
289
289
|
|
|
290
290
|
const lines = renderWidget(fixture.captured, 90);
|
|
291
291
|
const text = stripAnsi(lines.join("\n"));
|
|
292
|
-
expect(text).toContain("
|
|
292
|
+
expect(text).toContain("Tasks (0/1)");
|
|
293
|
+
expect(text).not.toContain("Foreground Subagents (blocking");
|
|
293
294
|
expect(text).not.toContain("Background Subagents (non-blocking");
|
|
294
295
|
expectLinesWithinWidth(lines, 90);
|
|
295
296
|
} finally {
|
|
@@ -297,6 +298,26 @@ describe("tasks widget subagent sections", () => {
|
|
|
297
298
|
}
|
|
298
299
|
});
|
|
299
300
|
|
|
301
|
+
it("does not mount the widget when foreground subagents are the only activity", async () => {
|
|
302
|
+
const fixture = await createFixture();
|
|
303
|
+
try {
|
|
304
|
+
emitSubagentsSnapshot(fixture.harness, {
|
|
305
|
+
foreground: [
|
|
306
|
+
createSubagent({
|
|
307
|
+
agent: "reviewer",
|
|
308
|
+
id: "fg_only",
|
|
309
|
+
status: "running",
|
|
310
|
+
task: "Review changed files",
|
|
311
|
+
}),
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(fixture.captured.render).toBeNull();
|
|
316
|
+
} finally {
|
|
317
|
+
await shutdownFixture(fixture);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
300
321
|
it("labels a background-only snapshot as non-blocking background subagents", async () => {
|
|
301
322
|
const fixture = await createFixture();
|
|
302
323
|
try {
|
|
@@ -321,7 +342,7 @@ describe("tasks widget subagent sections", () => {
|
|
|
321
342
|
}
|
|
322
343
|
});
|
|
323
344
|
|
|
324
|
-
it("renders
|
|
345
|
+
it("renders only the background heading for mixed foreground/background snapshots", async () => {
|
|
325
346
|
const fixture = await createFixture();
|
|
326
347
|
try {
|
|
327
348
|
emitSubagentsSnapshot(fixture.harness, {
|
|
@@ -345,8 +366,8 @@ describe("tasks widget subagent sections", () => {
|
|
|
345
366
|
|
|
346
367
|
const lines = renderWidget(fixture.captured, 95);
|
|
347
368
|
const text = stripAnsi(lines.join("\n"));
|
|
348
|
-
expect(text).toContain("Foreground Subagents (blocking · 1 running)");
|
|
349
369
|
expect(text).toContain("Background Subagents (non-blocking · 1 stalled)");
|
|
370
|
+
expect(text).not.toContain("Foreground Subagents (blocking");
|
|
350
371
|
expect(text).not.toContain("Subagents (1 running · 1 stalled)");
|
|
351
372
|
expectLinesWithinWidth(lines, 95);
|
|
352
373
|
} finally {
|
|
@@ -363,10 +384,10 @@ describe("tasks widget subagent sections", () => {
|
|
|
363
384
|
|
|
364
385
|
try {
|
|
365
386
|
emitSubagentsSnapshot(fixture.harness, {
|
|
366
|
-
|
|
387
|
+
background: [
|
|
367
388
|
createSubagent({
|
|
368
389
|
agent: "widthprobe",
|
|
369
|
-
id: "
|
|
390
|
+
id: "width_bg",
|
|
370
391
|
status: "running",
|
|
371
392
|
task: longTask,
|
|
372
393
|
}),
|
|
@@ -233,22 +233,19 @@ export function registerTasksExtension(
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* Render subagent lines (right column in side-by-side mode, or below tasks in
|
|
236
|
+
* Render background subagent lines (right column in side-by-side mode, or below tasks in
|
|
237
|
+
* stacked mode).
|
|
237
238
|
*/
|
|
238
239
|
function renderSubagentLines(
|
|
239
240
|
ctx: ExtensionContext,
|
|
240
241
|
spinner: string,
|
|
241
|
-
foreground: SubagentView[],
|
|
242
242
|
background: SubagentView[],
|
|
243
243
|
maxLineWidth: number
|
|
244
244
|
): string[] {
|
|
245
|
-
const activeForeground = foreground.filter(
|
|
246
|
-
(subagent) => subagent.status === "running" || subagent.status === "stalled"
|
|
247
|
-
);
|
|
248
245
|
const activeBackground = background.filter(
|
|
249
246
|
(subagent) => subagent.status === "running" || subagent.status === "stalled"
|
|
250
247
|
);
|
|
251
|
-
if (
|
|
248
|
+
if (activeBackground.length === 0) return [];
|
|
252
249
|
|
|
253
250
|
const safeLineWidth = Math.max(1, maxLineWidth);
|
|
254
251
|
|
|
@@ -355,21 +352,14 @@ export function registerTasksExtension(
|
|
|
355
352
|
}
|
|
356
353
|
|
|
357
354
|
/**
|
|
358
|
-
* Append
|
|
355
|
+
* Append the background-subagent section to the output.
|
|
359
356
|
* @param lines - Mutable output line sink
|
|
360
|
-
* @param sectionTitle - Section title label
|
|
361
|
-
* @param modeLabel - Blocking mode label
|
|
362
357
|
* @param subagents - Section subagents
|
|
363
358
|
*/
|
|
364
|
-
function appendSection(
|
|
365
|
-
lines: string[],
|
|
366
|
-
sectionTitle: string,
|
|
367
|
-
modeLabel: string,
|
|
368
|
-
subagents: SubagentView[]
|
|
369
|
-
): void {
|
|
359
|
+
function appendSection(lines: string[], subagents: SubagentView[]): void {
|
|
370
360
|
if (subagents.length === 0) return;
|
|
371
361
|
|
|
372
|
-
const sectionSummary =
|
|
362
|
+
const sectionSummary = `Background Subagents (non-blocking · ${getStatusSummary(subagents)})`;
|
|
373
363
|
lines.push(
|
|
374
364
|
clampLine(`${formatWidgetRole(ctx, "title", sectionSummary)}${getModelSummary(subagents)}`)
|
|
375
365
|
);
|
|
@@ -413,9 +403,7 @@ export function registerTasksExtension(
|
|
|
413
403
|
}
|
|
414
404
|
|
|
415
405
|
const lines: string[] = [];
|
|
416
|
-
appendSection(lines,
|
|
417
|
-
if (activeForeground.length > 0 && activeBackground.length > 0) lines.push("");
|
|
418
|
-
appendSection(lines, "Background Subagents", "non-blocking", activeBackground);
|
|
406
|
+
appendSection(lines, activeBackground);
|
|
419
407
|
return lines;
|
|
420
408
|
}
|
|
421
409
|
|
|
@@ -608,18 +596,15 @@ export function registerTasksExtension(
|
|
|
608
596
|
return;
|
|
609
597
|
}
|
|
610
598
|
|
|
611
|
-
const fgSubagents = foregroundSubagents.filter(
|
|
612
|
-
(subagent) => subagent.status === "running" || subagent.status === "stalled"
|
|
613
|
-
);
|
|
614
599
|
const bgSubagents = backgroundSubagents.filter(
|
|
615
600
|
(subagent) => subagent.status === "running" || subagent.status === "stalled"
|
|
616
601
|
);
|
|
617
602
|
const runningBgTasks = backgroundTasks.filter((task) => task.status === "running");
|
|
618
603
|
|
|
619
|
-
const
|
|
604
|
+
const hasBackgroundSubagents = bgSubagents.length > 0;
|
|
620
605
|
const hasBgTasks = runningBgTasks.length > 0;
|
|
621
606
|
const hasTeams = activeTeams.length > 0;
|
|
622
|
-
const hasRightColumn =
|
|
607
|
+
const hasRightColumn = hasBackgroundSubagents || hasBgTasks || hasTeams;
|
|
623
608
|
const hasTasks = state.tasks.length > 0;
|
|
624
609
|
|
|
625
610
|
if (!(state.visible && (hasTasks || hasRightColumn))) {
|
|
@@ -634,7 +619,6 @@ export function registerTasksExtension(
|
|
|
634
619
|
|
|
635
620
|
// Build stable key for structure changes
|
|
636
621
|
const taskStates = state.tasks.map((t) => `${t.id}:${t.status}`).join(",");
|
|
637
|
-
const fgIds = fgSubagents.map((s) => `${s.id}:${s.status}`).join(",");
|
|
638
622
|
const bgIds = bgSubagents.map((s) => `${s.id}:${s.status}`).join(",");
|
|
639
623
|
const bgTaskIds = runningBgTasks.map((t) => t.id).join(",");
|
|
640
624
|
const teamKey = activeTeams
|
|
@@ -643,7 +627,7 @@ export function registerTasksExtension(
|
|
|
643
627
|
`${t.name}:${t.tasks.map((tk) => tk.status).join("")}:${t.teammates.map((m) => m.status).join("")}`
|
|
644
628
|
)
|
|
645
629
|
.join("|");
|
|
646
|
-
const stableKey = `${taskStates}|${
|
|
630
|
+
const stableKey = `${taskStates}|${bgIds}|${bgTaskIds}|${teamKey}`;
|
|
647
631
|
|
|
648
632
|
// Re-render when structure changes, background items running (for animation),
|
|
649
633
|
// or in_progress tasks exist (spinner needs to animate every frame).
|
|
@@ -678,16 +662,14 @@ export function registerTasksExtension(
|
|
|
678
662
|
|
|
679
663
|
const taskLines = renderTaskLines(ctx, maxTitleLen);
|
|
680
664
|
|
|
681
|
-
// Build right column: teams,
|
|
665
|
+
// Build right column: teams, background subagents, then bg tasks
|
|
682
666
|
const rightLines: string[] = [];
|
|
683
667
|
if (hasTeams) {
|
|
684
668
|
rightLines.push(...renderTeamLines(ctx, spinner, activeTeams, maxTeamLen));
|
|
685
669
|
}
|
|
686
|
-
if (
|
|
670
|
+
if (hasBackgroundSubagents) {
|
|
687
671
|
if (rightLines.length > 0) rightLines.push(""); // Spacer
|
|
688
|
-
rightLines.push(
|
|
689
|
-
...renderSubagentLines(ctx, spinner, fgSubagents, bgSubagents, rightColumnWidth)
|
|
690
|
-
);
|
|
672
|
+
rightLines.push(...renderSubagentLines(ctx, spinner, bgSubagents, rightColumnWidth));
|
|
691
673
|
}
|
|
692
674
|
if (hasBgTasks) {
|
|
693
675
|
if (rightLines.length > 0) rightLines.push(""); // Spacer
|
|
@@ -713,9 +695,9 @@ export function registerTasksExtension(
|
|
|
713
695
|
lines.push(...renderTeamLines(ctx, spinner, activeTeams, maxTeamLen));
|
|
714
696
|
}
|
|
715
697
|
|
|
716
|
-
if (
|
|
698
|
+
if (hasBackgroundSubagents) {
|
|
717
699
|
if (lines.length > 0) lines.push(""); // Spacer
|
|
718
|
-
lines.push(...renderSubagentLines(ctx, spinner,
|
|
700
|
+
lines.push(...renderSubagentLines(ctx, spinner, bgSubagents, width));
|
|
719
701
|
}
|
|
720
702
|
|
|
721
703
|
if (hasBgTasks) {
|