pi-subagents 0.48.0 → 0.50.0
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/CHANGELOG.md +82 -19
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +3 -0
- package/docs/configuration.md +75 -8
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +5 -3
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +37 -12
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +13 -7
- package/src/extension/config.ts +53 -0
- package/src/extension/index.ts +62 -9
- package/src/extension/public-execution.ts +32 -4
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +10 -8
- package/src/extension/tool-description.ts +13 -9
- package/src/inspectors/herdr/actions.ts +13 -8
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +5 -4
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/store.ts +8 -3
- package/src/runs/background/active-async-capacity.ts +82 -25
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +73 -43
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +14 -6
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +21 -6
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +75 -18
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +101 -4
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +298 -33
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/prompt-audit.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +328 -43
- package/src/runs/shared/completion-guard.ts +107 -1
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +51 -5
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +128 -16
- package/src/tui/render.ts +122 -44
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +10 -2
- package/src/workflows/scripted-workflow.ts +272 -76
package/src/extension/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { createSubagentParamsSchema } from "./schemas.ts";
|
|
|
31
31
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
32
32
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
33
33
|
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
34
|
+
import { cleanupResultIndexes } from "../runs/background/result-files.ts";
|
|
34
35
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
35
36
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
|
|
36
37
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
@@ -59,6 +60,7 @@ import { resolveMissionStoreLocation } from "../missions/store.ts";
|
|
|
59
60
|
import { listRetainedChildren } from "../runs/background/retained-children.ts";
|
|
60
61
|
import {
|
|
61
62
|
type Details,
|
|
63
|
+
type MainWindowRendererConfig,
|
|
62
64
|
type SubagentState,
|
|
63
65
|
DIRS,
|
|
64
66
|
DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -81,6 +83,13 @@ import {
|
|
|
81
83
|
|
|
82
84
|
export { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
83
85
|
|
|
86
|
+
const SLOW_RELOAD_PHASE_MS = 250;
|
|
87
|
+
|
|
88
|
+
function logSlowPhase(label: string, startedAt: number): void {
|
|
89
|
+
const elapsed = Date.now() - startedAt;
|
|
90
|
+
if (elapsed >= SLOW_RELOAD_PHASE_MS) console.error(`Subagent reload phase '${label}' took ${elapsed}ms.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
84
93
|
function workflowLaneKeys(script: string): string[] {
|
|
85
94
|
const keys: string[] = [];
|
|
86
95
|
const seen = new Set<string>();
|
|
@@ -253,12 +262,14 @@ function rebuildSlashResultContainer(
|
|
|
253
262
|
result: AgentToolResult<Details>,
|
|
254
263
|
options: { expanded: boolean },
|
|
255
264
|
theme: ExtensionContext["ui"]["theme"],
|
|
265
|
+
rendererConfig?: MainWindowRendererConfig,
|
|
266
|
+
foregroundDetachShortcut?: string,
|
|
256
267
|
): void {
|
|
257
268
|
container.clear();
|
|
258
269
|
container.addChild(new Spacer(1));
|
|
259
270
|
const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
|
|
260
271
|
const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
|
|
261
|
-
box.addChild(renderSubagentResult(result, options, theme));
|
|
272
|
+
box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig, foregroundDetachShortcut));
|
|
262
273
|
container.addChild(box);
|
|
263
274
|
}
|
|
264
275
|
|
|
@@ -266,6 +277,8 @@ function createSlashResultComponent(
|
|
|
266
277
|
details: SlashMessageDetails,
|
|
267
278
|
options: { expanded: boolean },
|
|
268
279
|
theme: ExtensionContext["ui"]["theme"],
|
|
280
|
+
rendererConfig?: MainWindowRendererConfig,
|
|
281
|
+
foregroundDetachShortcut?: string,
|
|
269
282
|
): Container {
|
|
270
283
|
const container = new Container();
|
|
271
284
|
let lastVersion = -1;
|
|
@@ -273,7 +286,7 @@ function createSlashResultComponent(
|
|
|
273
286
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
274
287
|
if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
|
|
275
288
|
lastVersion = snapshot.version;
|
|
276
|
-
rebuildSlashResultContainer(container, snapshot.result, options, theme);
|
|
289
|
+
rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig, foregroundDetachShortcut);
|
|
277
290
|
}
|
|
278
291
|
return Container.prototype.render.call(container, width);
|
|
279
292
|
};
|
|
@@ -370,6 +383,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
370
383
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
371
384
|
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
372
385
|
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
386
|
+
const resultIndexCleanupTimer = setTimeout(() => {
|
|
387
|
+
try {
|
|
388
|
+
cleanupResultIndexes(DIRS.results);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error("Failed to clean stale subagent result indexes:", error);
|
|
391
|
+
}
|
|
392
|
+
}, 30_000);
|
|
393
|
+
resultIndexCleanupTimer.unref?.();
|
|
373
394
|
|
|
374
395
|
const state: SubagentState = {
|
|
375
396
|
baseCwd: "",
|
|
@@ -378,6 +399,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
378
399
|
...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
|
|
379
400
|
...(config.missions ? { missionStoreConfig: config.missions } : {}),
|
|
380
401
|
parentSessionFile: null,
|
|
402
|
+
trustedSessionRoots: [],
|
|
381
403
|
subagentInProgress: false,
|
|
382
404
|
subagentSpawns: {
|
|
383
405
|
sessionId: null,
|
|
@@ -451,6 +473,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
451
473
|
);
|
|
452
474
|
|
|
453
475
|
const runtimeCleanup = () => {
|
|
476
|
+
clearTimeout(resultIndexCleanupTimer);
|
|
454
477
|
stopResultWatcher();
|
|
455
478
|
state.currentSessionId = null;
|
|
456
479
|
completionNotifier.dispose();
|
|
@@ -481,7 +504,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
481
504
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
482
505
|
const details = resolveSlashMessageDetails(message.details);
|
|
483
506
|
if (!details) return undefined;
|
|
484
|
-
return createSlashResultComponent(details, options, theme);
|
|
507
|
+
return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, config.foregroundDetachShortcut);
|
|
485
508
|
});
|
|
486
509
|
|
|
487
510
|
pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
|
|
@@ -581,22 +604,24 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
581
604
|
},
|
|
582
605
|
|
|
583
606
|
renderCall(args, theme) {
|
|
607
|
+
const gap = " ".repeat(config.mainWindowRenderer?.horizontalSpacing ?? 1);
|
|
608
|
+
const title = theme.fg("toolTitle", theme.bold("subagent"));
|
|
584
609
|
if (args.action) {
|
|
585
610
|
const target = args.agent || args.chainName || "";
|
|
586
611
|
return new Text(
|
|
587
|
-
`${
|
|
612
|
+
`${title}${gap}${args.action}${target ? `${gap}${theme.fg("accent", target)}` : ""}`,
|
|
588
613
|
0, 0,
|
|
589
614
|
);
|
|
590
615
|
}
|
|
591
616
|
if (args.workflowScript)
|
|
592
617
|
return new Text(
|
|
593
|
-
`${
|
|
618
|
+
`${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
|
|
594
619
|
0,
|
|
595
620
|
0,
|
|
596
621
|
);
|
|
597
|
-
const asyncLabel = args.async === true ? theme.fg("warning", "
|
|
622
|
+
const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
|
|
598
623
|
return new Text(
|
|
599
|
-
`${
|
|
624
|
+
`${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
600
625
|
0,
|
|
601
626
|
0,
|
|
602
627
|
);
|
|
@@ -607,7 +632,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
607
632
|
const renderedResult = { ...result, isError: context.isError };
|
|
608
633
|
return summaryInlineToolDisplay
|
|
609
634
|
? renderSubagentSummary(renderedResult, options, theme)
|
|
610
|
-
: renderSubagentResult(renderedResult, options, theme);
|
|
635
|
+
: renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer, config.foregroundDetachShortcut);
|
|
611
636
|
},
|
|
612
637
|
|
|
613
638
|
};
|
|
@@ -637,7 +662,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
637
662
|
}
|
|
638
663
|
});
|
|
639
664
|
|
|
640
|
-
registerSlashCommands(pi, state, {
|
|
665
|
+
registerSlashCommands(pi, state, {
|
|
666
|
+
fleetKeybindings: config.fleetKeybindings,
|
|
667
|
+
foregroundDetachShortcut: config.foregroundDetachShortcut,
|
|
668
|
+
});
|
|
641
669
|
|
|
642
670
|
const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
|
|
643
671
|
const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
|
|
@@ -754,6 +782,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
754
782
|
goalTurnId = 0;
|
|
755
783
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
756
784
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
785
|
+
state.trustedSessionRoots = [...new Set([
|
|
786
|
+
...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
|
|
787
|
+
...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
|
|
788
|
+
])];
|
|
757
789
|
state.subagentSpawns = {
|
|
758
790
|
sessionId: state.currentSessionId,
|
|
759
791
|
count: 0,
|
|
@@ -773,18 +805,38 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
773
805
|
}
|
|
774
806
|
}
|
|
775
807
|
state.lastUiContext = ctx;
|
|
808
|
+
let phaseStartedAt = Date.now();
|
|
776
809
|
refreshActiveAsyncCapacity();
|
|
810
|
+
logSlowPhase("active-capacity", phaseStartedAt);
|
|
811
|
+
phaseStartedAt = Date.now();
|
|
777
812
|
cleanupSessionArtifacts(ctx);
|
|
813
|
+
logSlowPhase("session-artifact-cleanup", phaseStartedAt);
|
|
778
814
|
state.foregroundControls.clear();
|
|
779
815
|
state.lastForegroundControlId = null;
|
|
816
|
+
phaseStartedAt = Date.now();
|
|
780
817
|
resetJobs(ctx);
|
|
818
|
+
logSlowPhase("reset-jobs", phaseStartedAt);
|
|
819
|
+
phaseStartedAt = Date.now();
|
|
781
820
|
restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
|
|
821
|
+
logSlowPhase("foreground-history", phaseStartedAt);
|
|
822
|
+
phaseStartedAt = Date.now();
|
|
782
823
|
restoreActiveJobs(ctx);
|
|
824
|
+
logSlowPhase("active-job-restore", phaseStartedAt);
|
|
825
|
+
phaseStartedAt = Date.now();
|
|
783
826
|
scheduledRunManager.bindSession(ctx);
|
|
827
|
+
logSlowPhase("scheduled-runs", phaseStartedAt);
|
|
828
|
+
phaseStartedAt = Date.now();
|
|
784
829
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
830
|
+
logSlowPhase("slash-snapshots", phaseStartedAt);
|
|
831
|
+
phaseStartedAt = Date.now();
|
|
785
832
|
waitSubscriptionManager.restore();
|
|
833
|
+
logSlowPhase("wait-subscriptions", phaseStartedAt);
|
|
834
|
+
phaseStartedAt = Date.now();
|
|
786
835
|
startResultWatcher();
|
|
836
|
+
logSlowPhase("result-watcher-start", phaseStartedAt);
|
|
837
|
+
phaseStartedAt = Date.now();
|
|
787
838
|
primeExistingResults({ triggerTurn: !recovering });
|
|
839
|
+
logSlowPhase("result-prime", phaseStartedAt);
|
|
788
840
|
fleetStatus?.setContext(ctx);
|
|
789
841
|
};
|
|
790
842
|
|
|
@@ -827,6 +879,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
827
879
|
|
|
828
880
|
pi.on("session_shutdown", async () => {
|
|
829
881
|
state.widgetsSuspended = false;
|
|
882
|
+
clearTimeout(resultIndexCleanupTimer);
|
|
830
883
|
stopResultWatcher();
|
|
831
884
|
state.currentSessionId = null;
|
|
832
885
|
state.parentSessionFile = null;
|
|
@@ -9,6 +9,7 @@ export interface PublicSubagentExecutionParams {
|
|
|
9
9
|
concurrency?: unknown;
|
|
10
10
|
chainDir?: unknown;
|
|
11
11
|
workflowScript?: unknown;
|
|
12
|
+
output?: unknown;
|
|
12
13
|
resume?: unknown;
|
|
13
14
|
clarify?: unknown;
|
|
14
15
|
runFanoutBudget?: unknown;
|
|
@@ -47,7 +48,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
47
48
|
if (normalizedAction !== undefined) {
|
|
48
49
|
const legacyAction = normalizedAction.toLowerCase();
|
|
49
50
|
if (legacyAction === "single") {
|
|
50
|
-
return { ok: false, error: "
|
|
51
|
+
return { ok: false, error: "action='single' is not supported. Omit action and pass { agent, task } for one child.", mode: "workflow" };
|
|
51
52
|
}
|
|
52
53
|
if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
|
|
53
54
|
return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
|
|
@@ -64,13 +65,40 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
64
65
|
if (params.workflowScript !== undefined) {
|
|
65
66
|
return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
|
|
66
67
|
}
|
|
68
|
+
if (params.task !== undefined) {
|
|
69
|
+
return { ok: false, error: "Structured single-child task cannot be combined with a management/control action.", mode: "management" };
|
|
70
|
+
}
|
|
67
71
|
return { ok: true, params: { ...params, action: normalizedAction } };
|
|
68
72
|
}
|
|
69
|
-
if (params.
|
|
70
|
-
return { ok: false, error: "
|
|
73
|
+
if (params.step !== undefined) {
|
|
74
|
+
return { ok: false, error: "step is only available with action='append-step'; it is not an execution mode.", mode: "workflow" };
|
|
75
|
+
}
|
|
76
|
+
if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
|
|
77
|
+
return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
|
|
78
|
+
}
|
|
79
|
+
if (params.agent !== undefined || params.task !== undefined) {
|
|
80
|
+
if (typeof params.agent !== "string" || !params.agent.trim()) {
|
|
81
|
+
return { ok: false, error: "Structured single-child execution requires agent to be a non-empty string.", mode: "workflow" };
|
|
82
|
+
}
|
|
83
|
+
if (params.task !== undefined && typeof params.task !== "string") {
|
|
84
|
+
return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
|
|
85
|
+
}
|
|
86
|
+
const { agent: _agent, task: _task, output, ...workflowDefaults } = params;
|
|
87
|
+
const child = {
|
|
88
|
+
agent: params.agent.trim(),
|
|
89
|
+
...(params.task !== undefined ? { task: params.task } : {}),
|
|
90
|
+
output: output === undefined ? true : output,
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
params: {
|
|
95
|
+
...workflowDefaults,
|
|
96
|
+
workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
|
|
97
|
+
} as T,
|
|
98
|
+
};
|
|
71
99
|
}
|
|
72
100
|
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
|
|
73
|
-
return { ok: false, error: "Execution requires
|
|
101
|
+
return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript for orchestration.", mode: "workflow" };
|
|
74
102
|
}
|
|
75
103
|
return { ok: true, params };
|
|
76
104
|
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text
|
|
|
20
20
|
import { readStatus } from "../shared/utils.ts";
|
|
21
21
|
import { SubagentParams } from "./schemas.ts";
|
|
22
22
|
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
23
|
+
import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
|
|
23
24
|
|
|
24
25
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
25
26
|
export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
@@ -375,6 +376,7 @@ function pingData(ctx: ExtensionContext | null) {
|
|
|
375
376
|
capabilities: {
|
|
376
377
|
status: true,
|
|
377
378
|
fleetStatus: { version: 1 },
|
|
379
|
+
asyncStatusSnapshot: { kind: ASYNC_STATUS_SNAPSHOT_KIND, version: ASYNC_STATUS_SNAPSHOT_VERSION },
|
|
378
380
|
asyncSpawn: true,
|
|
379
381
|
steer: true,
|
|
380
382
|
nonRecoveringSteer: true,
|
|
@@ -544,13 +546,15 @@ async function handleRequest(
|
|
|
544
546
|
request.method,
|
|
545
547
|
{ action: "status", ...normalizeTargetParams(request.params, "status") },
|
|
546
548
|
);
|
|
549
|
+
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
547
550
|
return {
|
|
548
551
|
...status,
|
|
549
552
|
fleet: buildFleetStatus(
|
|
550
553
|
options.state,
|
|
551
554
|
fleetKeys,
|
|
552
|
-
|
|
555
|
+
sessionId,
|
|
553
556
|
),
|
|
557
|
+
asyncSnapshot: buildAsyncStatusSnapshotForState(options.state, sessionId),
|
|
554
558
|
};
|
|
555
559
|
}
|
|
556
560
|
if (request.method === "steer") {
|
package/src/extension/schemas.ts
CHANGED
|
@@ -93,7 +93,7 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
const AgentContractOverride = Type.Object({
|
|
96
|
-
version: Type.Integer({
|
|
96
|
+
version: Type.Integer({ minimum: 1, maximum: 1, description: "Enable compatibility behavior for this run/child." }),
|
|
97
97
|
}, { additionalProperties: false, description: "Compatibility behavior. Omit for the default behavior." });
|
|
98
98
|
|
|
99
99
|
const ChainGateOverride = Type.String({
|
|
@@ -255,21 +255,22 @@ const ControlOverrides = Type.Object({
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
const SubagentParamProperties = {
|
|
258
|
-
agent: Type.Optional(Type.String({ description: "Agent
|
|
258
|
+
agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
|
|
259
|
+
task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
|
|
259
260
|
resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
|
|
260
261
|
// Management action (when present, tool operates in management mode)
|
|
261
262
|
action: Type.Optional(Type.String({ minLength: 1,
|
|
262
|
-
description: "Optional management/control action. Omit this field for workflowScript execution; use it only for management/control actions."
|
|
263
|
+
description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
|
|
263
264
|
})),
|
|
264
265
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
265
266
|
id: Type.Optional(Type.String({
|
|
266
|
-
description: "Run id
|
|
267
|
+
description: "Run id/prefix for status/debug.run, interrupt, steer, append-step, approve-checkpoint, reject-checkpoint, or mission."
|
|
267
268
|
})),
|
|
268
269
|
runId: Type.Optional(Type.String({
|
|
269
|
-
description: "Target run ID for
|
|
270
|
+
description: "Target run ID for debug.run, interrupt, steer, append-step, or mission.attach-run. Prefer id."
|
|
270
271
|
})),
|
|
271
272
|
dir: Type.Optional(Type.String({
|
|
272
|
-
description: "Async run directory for
|
|
273
|
+
description: "Async run directory for status/debug.run, stop, resume, or steer."
|
|
273
274
|
})),
|
|
274
275
|
handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
|
|
275
276
|
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
|
|
@@ -326,6 +327,7 @@ const SubagentParamProperties = {
|
|
|
326
327
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
327
328
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
|
|
328
329
|
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
|
|
330
|
+
toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional hard per-tool-call timeout in milliseconds; known-fast built-in tools have a five-minute default." })),
|
|
329
331
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
330
332
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
331
333
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
@@ -359,10 +361,10 @@ const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = Subage
|
|
|
359
361
|
const trimmedSubagentParamProperties = {
|
|
360
362
|
...subagentParamPropertiesWithoutStep,
|
|
361
363
|
id: Type.Optional(Type.String({
|
|
362
|
-
description: "Run id
|
|
364
|
+
description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
|
|
363
365
|
})),
|
|
364
366
|
runId: Type.Optional(Type.String({
|
|
365
|
-
description: "Target run ID for
|
|
367
|
+
description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
|
|
366
368
|
})),
|
|
367
369
|
};
|
|
368
370
|
const SubagentParamsSchema = Type.Object(SubagentParamProperties);
|
|
@@ -8,24 +8,26 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
|
8
8
|
|
|
9
9
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
10
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
|
-
• Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
|
|
12
|
-
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
11
|
+
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
12
|
+
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
13
|
• Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
|
+
• Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
|
|
14
15
|
• Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
|
|
15
|
-
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs,
|
|
16
|
+
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
|
|
16
17
|
|
|
17
|
-
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run
|
|
18
|
+
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
|
|
18
19
|
|
|
19
20
|
EXECUTION:
|
|
20
21
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
|
-
•
|
|
22
|
+
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
|
|
23
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
22
24
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
23
25
|
• Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
24
26
|
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
25
27
|
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
26
28
|
|
|
27
29
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
28
|
-
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
30
|
+
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
29
31
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
30
32
|
• { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.
|
|
31
33
|
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
@@ -33,11 +35,12 @@ MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
|
33
35
|
|
|
34
36
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
35
37
|
|
|
36
|
-
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run
|
|
38
|
+
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
|
|
37
39
|
|
|
38
40
|
EXECUTE:
|
|
39
41
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
40
|
-
•
|
|
42
|
+
• SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
|
|
43
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
|
|
41
44
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
42
45
|
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
43
46
|
|
|
@@ -47,8 +50,9 @@ MANAGE / CONTROL:
|
|
|
47
50
|
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
48
51
|
|
|
49
52
|
ASYNC / SAFETY:
|
|
50
|
-
• Omitted async detaches background work. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
53
|
+
• Omitted async detaches background work. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
51
54
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
55
|
+
• Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
|
|
52
56
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
53
57
|
|
|
54
58
|
|
|
@@ -10,7 +10,9 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
|
10
10
|
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
11
11
|
import { readStatus } from "../../shared/utils.ts";
|
|
12
12
|
import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
|
|
13
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
13
14
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
15
|
+
import { formatShellCommand } from "./shell-command.ts";
|
|
14
16
|
|
|
15
17
|
export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
|
|
16
18
|
export type HerdrInspectorAction = typeof HERDR_INSPECTOR_ACTIONS[number];
|
|
@@ -45,6 +47,7 @@ interface InspectorDeps {
|
|
|
45
47
|
client?: HerdrClient;
|
|
46
48
|
missions?: MissionStoreConfig;
|
|
47
49
|
authorityPolicy?: AuthorityPolicyConfig;
|
|
50
|
+
sessionRoots?: string[];
|
|
48
51
|
cwd: string;
|
|
49
52
|
signal?: AbortSignal;
|
|
50
53
|
now?: () => Date;
|
|
@@ -84,16 +87,11 @@ function extractPaneId(value: unknown): string | undefined {
|
|
|
84
87
|
return undefined;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
|
|
93
|
-
const args = [process.execPath, input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
|
|
90
|
+
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
|
|
91
|
+
const args = [input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", JSON.stringify(input.sessionRoots)];
|
|
94
92
|
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
95
93
|
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
96
|
-
return
|
|
94
|
+
return formatShellCommand(resolveNodeExecutable(), args);
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
|
|
@@ -114,6 +112,12 @@ function pathWithin(base: string, candidate: string): boolean {
|
|
|
114
112
|
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
115
113
|
}
|
|
116
114
|
|
|
115
|
+
function herdrSessionRoots(target: { runId: string }, deps: InspectorDeps): string[] {
|
|
116
|
+
const roots = deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [];
|
|
117
|
+
const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
|
|
118
|
+
return [...new Set([...roots, ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
|
|
119
|
+
}
|
|
120
|
+
|
|
117
121
|
function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
|
|
118
122
|
try {
|
|
119
123
|
if (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;
|
|
@@ -204,6 +208,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
204
208
|
missionPath: mission?.path,
|
|
205
209
|
allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
|
|
206
210
|
allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
|
|
211
|
+
sessionRoots: herdrSessionRoots(target, deps),
|
|
207
212
|
});
|
|
208
213
|
const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
|
|
209
214
|
if (started.ok === false) {
|
|
@@ -17,6 +17,7 @@ export interface RunnerOptions {
|
|
|
17
17
|
refreshMs: number;
|
|
18
18
|
allowSteer?: boolean;
|
|
19
19
|
allowStop?: boolean;
|
|
20
|
+
sessionRoots: string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function readMission(filePath: string | undefined): MissionRecord | undefined {
|
|
@@ -24,7 +25,7 @@ function readMission(filePath: string | undefined): MissionRecord | undefined {
|
|
|
24
25
|
try { return parseMissionRecord(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath); } catch { return undefined; }
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean }): string {
|
|
28
|
+
export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean; sessionRoots?: string[] }): string {
|
|
28
29
|
const { status, asyncDir, mission } = input;
|
|
29
30
|
const lines = [
|
|
30
31
|
`pi-subagents inspector for ${status.runId}`,
|
|
@@ -37,7 +38,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
37
38
|
if (open.length) lines.push(`Open decisions: ${open.map((decision) => `${decision.id}: ${decision.title}`).join(" | ")}`);
|
|
38
39
|
lines.push("");
|
|
39
40
|
}
|
|
40
|
-
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60 }));
|
|
41
|
+
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
|
|
41
42
|
const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
|
|
42
43
|
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
|
|
43
44
|
return lines.join("\n");
|
|
@@ -57,6 +58,17 @@ function parseArgs(argv: string[]): RunnerOptions {
|
|
|
57
58
|
const indexRaw = values.get("--index");
|
|
58
59
|
const childIndex = indexRaw === undefined ? undefined : Number(indexRaw);
|
|
59
60
|
if (childIndex !== undefined && (!Number.isInteger(childIndex) || childIndex < 0)) throw new Error("--index must be a non-negative integer.");
|
|
61
|
+
const sessionRootsRaw = values.get("--session-roots");
|
|
62
|
+
let sessionRoots: string[] = [];
|
|
63
|
+
if (sessionRootsRaw !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(sessionRootsRaw) as unknown;
|
|
66
|
+
if (!Array.isArray(parsed) || parsed.some((root) => typeof root !== "string")) throw new Error();
|
|
67
|
+
sessionRoots = parsed;
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("--session-roots must be a JSON array of strings.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
60
72
|
const refreshRaw = values.get("--refresh-ms");
|
|
61
73
|
const refreshMs = refreshRaw === undefined ? 1_500 : Number(refreshRaw);
|
|
62
74
|
if (!Number.isInteger(refreshMs) || refreshMs < 250) throw new Error("--refresh-ms must be an integer >= 250.");
|
|
@@ -65,6 +77,7 @@ function parseArgs(argv: string[]): RunnerOptions {
|
|
|
65
77
|
runId,
|
|
66
78
|
...(childIndex !== undefined ? { index: childIndex } : {}),
|
|
67
79
|
...(values.get("--mission-path") ? { missionPath: path.resolve(values.get("--mission-path")!) } : {}),
|
|
80
|
+
sessionRoots,
|
|
68
81
|
refreshMs,
|
|
69
82
|
allowSteer: values.get("--allow-steer") !== "false",
|
|
70
83
|
allowStop: values.get("--allow-stop") !== "false",
|
|
@@ -115,7 +128,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
|
|
|
115
128
|
process.stdout.write(`\x1b[2J\x1b[Hpi-subagents inspector\n\nLifecycle status for ${options.runId} is unavailable.\n`);
|
|
116
129
|
return;
|
|
117
130
|
}
|
|
118
|
-
process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop })}${notice ? `\n\n${notice}` : ""}\n> `);
|
|
131
|
+
process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop, sessionRoots: options.sessionRoots })}${notice ? `\n\n${notice}` : ""}\n> `);
|
|
119
132
|
if (isTerminal(status) && timer) {
|
|
120
133
|
clearInterval(timer);
|
|
121
134
|
timer = undefined;
|
|
@@ -6,6 +6,7 @@ import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
|
6
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
7
|
import type { Details } from "../../shared/types.ts";
|
|
8
8
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
|
|
9
|
+
import { formatShellCommand } from "./shell-command.ts";
|
|
9
10
|
|
|
10
11
|
export const HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"] as const;
|
|
11
12
|
export type HerdrProjectPaneAction = typeof HERDR_PROJECT_PANE_ACTIONS[number];
|
|
@@ -257,11 +258,6 @@ function projectPaneRuntime(value: unknown): ProjectPaneRuntime | undefined {
|
|
|
257
258
|
};
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
function shellQuote(value: string): string {
|
|
261
|
-
if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
|
|
262
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
261
|
function resolveProjectRoot(requested: string): ProjectPaneResult<string> {
|
|
266
262
|
const resolved = path.resolve(requested);
|
|
267
263
|
try {
|
|
@@ -284,7 +280,7 @@ async function inspectPane(client: HerdrClient, paneId: string, signal?: AbortSi
|
|
|
284
280
|
function projectPaneCommand(message: string | undefined): string {
|
|
285
281
|
const args = message?.trim() ? [message.trim()] : [];
|
|
286
282
|
const command = getPiSpawnCommand(args);
|
|
287
|
-
return
|
|
283
|
+
return formatShellCommand(command.command, command.args);
|
|
288
284
|
}
|
|
289
285
|
|
|
290
286
|
function canonicalRuntimePath(value: string | undefined): string | undefined {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function shellQuote(value: string, platform: NodeJS.Platform): string {
|
|
2
|
+
if (platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
|
|
3
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isBareExecutable(value: string): boolean {
|
|
7
|
+
return /^[\w./@:-]+$/.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatShellCommand(exe: string, args: readonly string[], platform: NodeJS.Platform = process.platform): string {
|
|
11
|
+
const quotedArgs = args.map((arg) => shellQuote(arg, platform));
|
|
12
|
+
if (platform === "win32") return `& ${[shellQuote(exe, platform), ...quotedArgs].join(" ")}`;
|
|
13
|
+
// Nushell treats a leading quoted token as a string, so use a bare invoker for paths that need quoting.
|
|
14
|
+
const invocation = isBareExecutable(exe) ? exe : `sh -c 'exec "$0" "$@"' ${shellQuote(exe, platform)}`;
|
|
15
|
+
return [invocation, ...quotedArgs].join(" ");
|
|
16
|
+
}
|