@pi-agents/orchid 0.1.0-beta.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 +41 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/agents/AGENTS-MANIFEST.md +42 -0
- package/agents/brain.md +42 -0
- package/agents/context-builder.md +46 -0
- package/agents/delegate.md +12 -0
- package/agents/dev-1.md +42 -0
- package/agents/oracle.md +73 -0
- package/agents/planner.md +55 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/tester.md +45 -0
- package/agents/worker.md +55 -0
- package/extensions/ralph.ts +1 -0
- package/extensions/reviewer-extension.ts +125 -0
- package/extensions/task-orchestrator.ts +28 -0
- package/package.json +63 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-context-build.md +53 -0
- package/prompts/parallel-handoff-plan.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +41 -0
- package/skills/orchid/SKILL.md +214 -0
- package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
- package/skills/orchid/orchid-converge/SKILL.md +124 -0
- package/skills/orchid/orchid-decompose/SKILL.md +201 -0
- package/skills/orchid/orchid-doctor/SKILL.md +162 -0
- package/skills/orchid/orchid-investigate/SKILL.md +102 -0
- package/skills/orchid/orchid-launch/SKILL.md +147 -0
- package/skills/ralph/SKILL.md +73 -0
- package/skills/subagents/pi-subagents/SKILL.md +813 -0
- package/src/index.ts +7 -0
- package/src/orchestrator/abort.ts +534 -0
- package/src/orchestrator/agent-bridge-extension.ts +1020 -0
- package/src/orchestrator/agent-host.ts +954 -0
- package/src/orchestrator/cleanup.ts +776 -0
- package/src/orchestrator/config-loader.ts +1412 -0
- package/src/orchestrator/config-schema.ts +690 -0
- package/src/orchestrator/config.ts +81 -0
- package/src/orchestrator/context-window.ts +66 -0
- package/src/orchestrator/diagnostic-reports.ts +475 -0
- package/src/orchestrator/diagnostics.ts +394 -0
- package/src/orchestrator/discovery.ts +1833 -0
- package/src/orchestrator/engine-worker.ts +415 -0
- package/src/orchestrator/engine.ts +5940 -0
- package/src/orchestrator/execution.ts +3104 -0
- package/src/orchestrator/extension.ts +5934 -0
- package/src/orchestrator/formatting.ts +785 -0
- package/src/orchestrator/git.ts +88 -0
- package/src/orchestrator/index.ts +28 -0
- package/src/orchestrator/lane-runner.ts +1787 -0
- package/src/orchestrator/mailbox.ts +780 -0
- package/src/orchestrator/merge.ts +3414 -0
- package/src/orchestrator/messages.ts +1062 -0
- package/src/orchestrator/migrations.ts +278 -0
- package/src/orchestrator/naming.ts +117 -0
- package/src/orchestrator/path-resolver.ts +275 -0
- package/src/orchestrator/persistence.ts +2625 -0
- package/src/orchestrator/process-registry.ts +452 -0
- package/src/orchestrator/quality-gate.ts +1085 -0
- package/src/orchestrator/resume.ts +3488 -0
- package/src/orchestrator/sessions.ts +57 -0
- package/src/orchestrator/settings-loader.ts +136 -0
- package/src/orchestrator/settings-tui.ts +2208 -0
- package/src/orchestrator/sidecar-telemetry.ts +267 -0
- package/src/orchestrator/supervisor.ts +4548 -0
- package/src/orchestrator/task-executor-core.ts +675 -0
- package/src/orchestrator/tmux-compat.ts +37 -0
- package/src/orchestrator/tool-allowlist-constants.ts +37 -0
- package/src/orchestrator/types.ts +4465 -0
- package/src/orchestrator/verification.ts +547 -0
- package/src/orchestrator/waves.ts +1564 -0
- package/src/orchestrator/workspace.ts +707 -0
- package/src/orchestrator/worktree.ts +2725 -0
- package/src/ralph/index.ts +825 -0
- package/src/subagents/agents/agent-management.ts +648 -0
- package/src/subagents/agents/agent-scope.ts +6 -0
- package/src/subagents/agents/agent-selection.ts +23 -0
- package/src/subagents/agents/agent-serializer.ts +86 -0
- package/src/subagents/agents/agents.ts +832 -0
- package/src/subagents/agents/chain-serializer.ts +137 -0
- package/src/subagents/agents/frontmatter.ts +29 -0
- package/src/subagents/agents/identity.ts +30 -0
- package/src/subagents/agents/skills.ts +632 -0
- package/src/subagents/extension/config.ts +16 -0
- package/src/subagents/extension/control-notices.ts +92 -0
- package/src/subagents/extension/doctor.ts +199 -0
- package/src/subagents/extension/fanout-child.ts +170 -0
- package/src/subagents/extension/index.ts +573 -0
- package/src/subagents/extension/schemas.ts +168 -0
- package/src/subagents/intercom/intercom-bridge.ts +379 -0
- package/src/subagents/intercom/result-intercom.ts +377 -0
- package/src/subagents/runs/background/async-execution.ts +712 -0
- package/src/subagents/runs/background/async-job-tracker.ts +310 -0
- package/src/subagents/runs/background/async-resume.ts +345 -0
- package/src/subagents/runs/background/async-status.ts +325 -0
- package/src/subagents/runs/background/completion-dedupe.ts +63 -0
- package/src/subagents/runs/background/notify.ts +108 -0
- package/src/subagents/runs/background/parallel-groups.ts +45 -0
- package/src/subagents/runs/background/result-watcher.ts +307 -0
- package/src/subagents/runs/background/run-id-resolver.ts +83 -0
- package/src/subagents/runs/background/run-status.ts +269 -0
- package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
- package/src/subagents/runs/background/subagent-runner.ts +1808 -0
- package/src/subagents/runs/background/top-level-async.ts +13 -0
- package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
- package/src/subagents/runs/foreground/chain-execution.ts +938 -0
- package/src/subagents/runs/foreground/execution.ts +918 -0
- package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
- package/src/subagents/runs/shared/completion-guard.ts +147 -0
- package/src/subagents/runs/shared/long-running-guard.ts +175 -0
- package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/subagents/runs/shared/model-fallback.ts +103 -0
- package/src/subagents/runs/shared/nested-events.ts +819 -0
- package/src/subagents/runs/shared/nested-path.ts +52 -0
- package/src/subagents/runs/shared/nested-render.ts +115 -0
- package/src/subagents/runs/shared/parallel-utils.ts +109 -0
- package/src/subagents/runs/shared/pi-args.ts +220 -0
- package/src/subagents/runs/shared/pi-spawn.ts +115 -0
- package/src/subagents/runs/shared/run-history.ts +60 -0
- package/src/subagents/runs/shared/single-output.ts +164 -0
- package/src/subagents/runs/shared/subagent-control.ts +226 -0
- package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
- package/src/subagents/runs/shared/worktree.ts +577 -0
- package/src/subagents/shared/artifacts.ts +98 -0
- package/src/subagents/shared/atomic-json.ts +16 -0
- package/src/subagents/shared/file-coalescer.ts +40 -0
- package/src/subagents/shared/fork-context.ts +76 -0
- package/src/subagents/shared/formatters.ts +133 -0
- package/src/subagents/shared/jsonl-writer.ts +81 -0
- package/src/subagents/shared/model-info.ts +78 -0
- package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
- package/src/subagents/shared/session-identity.ts +10 -0
- package/src/subagents/shared/session-tokens.ts +44 -0
- package/src/subagents/shared/settings.ts +397 -0
- package/src/subagents/shared/status-format.ts +49 -0
- package/src/subagents/shared/types.ts +822 -0
- package/src/subagents/shared/utils.ts +450 -0
- package/src/subagents/slash/prompt-template-bridge.ts +397 -0
- package/src/subagents/slash/slash-bridge.ts +174 -0
- package/src/subagents/slash/slash-commands.ts +528 -0
- package/src/subagents/slash/slash-live-state.ts +292 -0
- package/src/subagents/tui/render-helpers.ts +80 -0
- package/src/subagents/tui/render.ts +1358 -0
- package/templates/agents/local/supervisor.md +33 -0
- package/templates/agents/local/task-merger.md +27 -0
- package/templates/agents/local/task-reviewer.md +30 -0
- package/templates/agents/local/task-worker.md +34 -0
- package/templates/agents/supervisor-routing.md +92 -0
- package/templates/agents/supervisor.md +229 -0
- package/templates/agents/task-merger.md +214 -0
- package/templates/agents/task-reviewer.md +260 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +557 -0
- package/templates/tasks/CONTEXT.md +30 -0
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
- package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
const MAX_NESTED_ID_LENGTH = 128;
|
|
4
|
+
export const MAX_NESTED_PATH_ENTRIES = 4;
|
|
5
|
+
|
|
6
|
+
export type NestedPathEntry = { runId: string; stepIndex?: number; agent?: string };
|
|
7
|
+
|
|
8
|
+
export function isSafeNestedPathId(value: unknown): value is string {
|
|
9
|
+
return typeof value === "string"
|
|
10
|
+
&& value.length > 0
|
|
11
|
+
&& value.length <= MAX_NESTED_ID_LENGTH
|
|
12
|
+
&& !path.isAbsolute(value)
|
|
13
|
+
&& !value.includes("/")
|
|
14
|
+
&& !value.includes("\\")
|
|
15
|
+
&& !value.includes("..");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nonEmptyString(value: unknown, max: number): string | undefined {
|
|
23
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sanitizeNestedPath(value: unknown): NestedPathEntry[] {
|
|
27
|
+
if (!Array.isArray(value)) return [];
|
|
28
|
+
return value.map((part) => {
|
|
29
|
+
if (!part || typeof part !== "object") return undefined;
|
|
30
|
+
const record = part as Record<string, unknown>;
|
|
31
|
+
if (!isSafeNestedPathId(record.runId)) return undefined;
|
|
32
|
+
return {
|
|
33
|
+
runId: record.runId,
|
|
34
|
+
...(finiteNumber(record.stepIndex) !== undefined ? { stepIndex: finiteNumber(record.stepIndex) } : {}),
|
|
35
|
+
...(nonEmptyString(record.agent, 128) ? { agent: nonEmptyString(record.agent, 128) } : {}),
|
|
36
|
+
};
|
|
37
|
+
}).filter((part): part is NestedPathEntry => Boolean(part)).slice(0, MAX_NESTED_PATH_ENTRIES);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseNestedPathEnv(value: string | undefined): NestedPathEntry[] {
|
|
41
|
+
if (!value) return [];
|
|
42
|
+
try {
|
|
43
|
+
return sanitizeNestedPath(JSON.parse(value) as unknown);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function encodeNestedPathEnv(value: NestedPathEntry[]): string {
|
|
50
|
+
const sanitized = sanitizeNestedPath(value);
|
|
51
|
+
return sanitized.length ? JSON.stringify(sanitized) : "";
|
|
52
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
2
|
+
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
3
|
+
import type { ActivityState, NestedRunSummary, NestedStepSummary } from "../../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
export interface NestedRunCounts {
|
|
6
|
+
total: number;
|
|
7
|
+
running: number;
|
|
8
|
+
paused: number;
|
|
9
|
+
complete: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
queued: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function countNestedRuns(children: NestedRunSummary[] | undefined): NestedRunCounts {
|
|
15
|
+
const counts: NestedRunCounts = { total: 0, running: 0, paused: 0, complete: 0, failed: 0, queued: 0 };
|
|
16
|
+
for (const child of children ?? []) {
|
|
17
|
+
counts.total++;
|
|
18
|
+
counts[child.state]++;
|
|
19
|
+
const nested = countNestedRuns([...(child.children ?? []), ...(child.steps?.flatMap((step) => step.children ?? []) ?? [])]);
|
|
20
|
+
counts.total += nested.total;
|
|
21
|
+
counts.running += nested.running;
|
|
22
|
+
counts.paused += nested.paused;
|
|
23
|
+
counts.complete += nested.complete;
|
|
24
|
+
counts.failed += nested.failed;
|
|
25
|
+
counts.queued += nested.queued;
|
|
26
|
+
}
|
|
27
|
+
return counts;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatNestedAggregate(children: NestedRunSummary[] | undefined): string | undefined {
|
|
31
|
+
const counts = countNestedRuns(children);
|
|
32
|
+
if (counts.total === 0) return undefined;
|
|
33
|
+
const parts = [
|
|
34
|
+
counts.running > 0 ? `${counts.running} running` : "",
|
|
35
|
+
counts.paused > 0 ? `${counts.paused} paused` : "",
|
|
36
|
+
counts.failed > 0 ? `${counts.failed} failed` : "",
|
|
37
|
+
counts.complete > 0 ? `${counts.complete} complete` : "",
|
|
38
|
+
counts.queued > 0 ? `${counts.queued} queued` : "",
|
|
39
|
+
].filter(Boolean);
|
|
40
|
+
return `+${counts.total} nested run${counts.total === 1 ? "" : "s"}${parts.length ? ` (${parts.join(", ")})` : ""}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nestedRunLabel(run: NestedRunSummary): string {
|
|
44
|
+
if (run.agent) return run.agent;
|
|
45
|
+
if (run.agents?.length) return run.agents.length === 1 ? run.agents[0]! : `${run.agents.slice(0, 2).join(", ")}${run.agents.length > 2 ? ` +${run.agents.length - 2}` : ""}`;
|
|
46
|
+
return run.id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatNestedActivity(input: {
|
|
50
|
+
activityState?: ActivityState;
|
|
51
|
+
lastActivityAt?: number;
|
|
52
|
+
currentTool?: string;
|
|
53
|
+
currentToolStartedAt?: number;
|
|
54
|
+
currentPath?: string;
|
|
55
|
+
turnCount?: number;
|
|
56
|
+
toolCount?: number;
|
|
57
|
+
totalTokens?: NestedRunSummary["totalTokens"];
|
|
58
|
+
}): string | undefined {
|
|
59
|
+
const facts: string[] = [];
|
|
60
|
+
if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
|
|
61
|
+
else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
|
|
62
|
+
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
63
|
+
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
64
|
+
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
65
|
+
if (input.totalTokens) facts.push(`${formatTokens(input.totalTokens.total)} tok`);
|
|
66
|
+
const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
|
|
67
|
+
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatNestedRunLines(children: NestedRunSummary[] | undefined, options: { indent: string; maxDepth: number; maxLines: number; commandHints?: boolean }): string[] {
|
|
71
|
+
const lines: string[] = [];
|
|
72
|
+
const append = (items: NestedRunSummary[] | undefined, depth: number, indent: string): void => {
|
|
73
|
+
if (!items?.length || lines.length >= options.maxLines) return;
|
|
74
|
+
if (depth > options.maxDepth) {
|
|
75
|
+
const aggregate = formatNestedAggregate(items);
|
|
76
|
+
if (aggregate && lines.length < options.maxLines) lines.push(`${indent}↳ ${aggregate}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (let index = 0; index < items.length; index++) {
|
|
80
|
+
const child = items[index]!;
|
|
81
|
+
if (lines.length >= options.maxLines) {
|
|
82
|
+
const aggregate = formatNestedAggregate(items.slice(index));
|
|
83
|
+
if (aggregate) lines[lines.length - 1] = `${indent}↳ ${aggregate}`;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const activity = child.state === "running" ? formatNestedActivity(child) : undefined;
|
|
87
|
+
const error = child.error ? ` | error: ${child.error}` : "";
|
|
88
|
+
lines.push(`${indent}↳ ${nestedRunLabel(child)} [${child.id}] ${child.state}${activity ? ` | ${activity}` : ""}${error}`);
|
|
89
|
+
if (options.commandHints && lines.length < options.maxLines) lines.push(`${indent} Status: subagent({ action: "status", id: "${child.id}" })`);
|
|
90
|
+
if (depth === options.maxDepth) {
|
|
91
|
+
const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
|
|
92
|
+
if (aggregate && lines.length < options.maxLines) lines.push(`${indent} ↳ ${aggregate}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const [stepIndex, step] of (child.steps ?? []).entries()) {
|
|
96
|
+
if (lines.length >= options.maxLines) return;
|
|
97
|
+
const stepActivity = step.status === "running" ? formatNestedActivity(step) : undefined;
|
|
98
|
+
lines.push(`${indent} ${stepIndex + 1}. ${step.agent} ${step.status}${stepActivity ? ` | ${stepActivity}` : ""}${step.error ? ` | error: ${step.error}` : ""}`);
|
|
99
|
+
append(step.children, depth + 1, `${indent} `);
|
|
100
|
+
}
|
|
101
|
+
append(child.children, depth + 1, `${indent} `);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
append(children, 0, options.indent);
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatNestedRunStatusLines(children: NestedRunSummary[] | undefined, options: { indent?: string; maxDepth?: number; maxLines?: number; commandHints?: boolean } = {}): string[] {
|
|
109
|
+
return formatNestedRunLines(children, {
|
|
110
|
+
indent: options.indent ?? " ",
|
|
111
|
+
maxDepth: options.maxDepth ?? 2,
|
|
112
|
+
maxLines: options.maxLines ?? 40,
|
|
113
|
+
commandHints: options.commandHints ?? false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface RunnerSubagentStep {
|
|
2
|
+
agent: string;
|
|
3
|
+
task: string;
|
|
4
|
+
cwd?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
thinking?: string;
|
|
7
|
+
modelCandidates?: string[];
|
|
8
|
+
tools?: string[];
|
|
9
|
+
extensions?: string[];
|
|
10
|
+
mcpDirectTools?: string[];
|
|
11
|
+
completionGuard?: boolean;
|
|
12
|
+
systemPrompt?: string | null;
|
|
13
|
+
systemPromptMode?: "append" | "replace";
|
|
14
|
+
inheritProjectContext: boolean;
|
|
15
|
+
inheritSkills: boolean;
|
|
16
|
+
skills?: string[];
|
|
17
|
+
outputPath?: string;
|
|
18
|
+
outputMode?: "inline" | "file-only";
|
|
19
|
+
sessionFile?: string;
|
|
20
|
+
maxSubagentDepth?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParallelStepGroup {
|
|
24
|
+
parallel: RunnerSubagentStep[];
|
|
25
|
+
concurrency?: number;
|
|
26
|
+
failFast?: boolean;
|
|
27
|
+
worktree?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
|
|
31
|
+
|
|
32
|
+
export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
|
|
33
|
+
return "parallel" in step && Array.isArray(step.parallel);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
|
|
37
|
+
const flat: RunnerSubagentStep[] = [];
|
|
38
|
+
for (const step of steps) {
|
|
39
|
+
if (isParallelGroup(step)) {
|
|
40
|
+
for (const task of step.parallel) flat.push(task);
|
|
41
|
+
} else {
|
|
42
|
+
flat.push(step);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return flat;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function mapConcurrent<T, R>(
|
|
49
|
+
items: T[],
|
|
50
|
+
limit: number,
|
|
51
|
+
fn: (item: T, i: number) => Promise<R>,
|
|
52
|
+
): Promise<R[]> {
|
|
53
|
+
const safeLimit = Math.max(1, Math.floor(limit) || 1);
|
|
54
|
+
const results: R[] = new Array(items.length);
|
|
55
|
+
let next = 0;
|
|
56
|
+
|
|
57
|
+
async function worker(_workerIndex: number): Promise<void> {
|
|
58
|
+
while (next < items.length) {
|
|
59
|
+
const i = next++;
|
|
60
|
+
results[i] = await fn(items[i], i);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await Promise.all(
|
|
65
|
+
Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi)),
|
|
66
|
+
);
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ParallelTaskResult {
|
|
71
|
+
agent: string;
|
|
72
|
+
taskIndex?: number;
|
|
73
|
+
output: string;
|
|
74
|
+
exitCode: number | null;
|
|
75
|
+
error?: string;
|
|
76
|
+
model?: string;
|
|
77
|
+
attemptedModels?: string[];
|
|
78
|
+
outputTargetPath?: string;
|
|
79
|
+
outputTargetExists?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function aggregateParallelOutputs(
|
|
83
|
+
results: ParallelTaskResult[],
|
|
84
|
+
headerFormat: (index: number, agent: string) => string = (i, agent) =>
|
|
85
|
+
`=== Parallel Task ${i + 1} (${agent}) ===`,
|
|
86
|
+
): string {
|
|
87
|
+
return results
|
|
88
|
+
.map((r, i) => {
|
|
89
|
+
const header = headerFormat(r.taskIndex ?? i, r.agent);
|
|
90
|
+
const hasOutput = Boolean(r.output?.trim());
|
|
91
|
+
const status =
|
|
92
|
+
r.exitCode === -1
|
|
93
|
+
? "SKIPPED"
|
|
94
|
+
: r.exitCode !== 0 && r.exitCode !== null
|
|
95
|
+
? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
|
|
96
|
+
: r.error
|
|
97
|
+
? `WARNING: ${r.error}`
|
|
98
|
+
: !hasOutput && r.outputTargetPath && r.outputTargetExists === false
|
|
99
|
+
? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
|
|
100
|
+
: !hasOutput && !r.outputTargetPath
|
|
101
|
+
? "EMPTY OUTPUT (no textual response returned)"
|
|
102
|
+
: "";
|
|
103
|
+
const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
|
|
104
|
+
return `${header}\n${body}`;
|
|
105
|
+
})
|
|
106
|
+
.join("\n\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const MAX_PARALLEL_CONCURRENCY = 4;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
|
|
6
|
+
import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts";
|
|
7
|
+
|
|
8
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
9
|
+
const TASK_ARG_LIMIT = 8000;
|
|
10
|
+
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
|
|
11
|
+
const FANOUT_CHILD_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "fanout-child.ts");
|
|
12
|
+
export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
13
|
+
export const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
14
|
+
export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
|
|
15
|
+
export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
|
|
16
|
+
export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
|
|
17
|
+
export const SUBAGENT_FANOUT_CHILD_ENV = "PI_SUBAGENT_FANOUT_CHILD";
|
|
18
|
+
export const SUBAGENT_PARENT_EVENT_SINK_ENV = "PI_SUBAGENT_PARENT_EVENT_SINK";
|
|
19
|
+
export const SUBAGENT_PARENT_CONTROL_INBOX_ENV = "PI_SUBAGENT_PARENT_CONTROL_INBOX";
|
|
20
|
+
export const SUBAGENT_PARENT_ROOT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_ROOT_RUN_ID";
|
|
21
|
+
export const SUBAGENT_PARENT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_RUN_ID";
|
|
22
|
+
export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX";
|
|
23
|
+
export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
|
|
24
|
+
export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
|
|
25
|
+
export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV = "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
|
|
26
|
+
|
|
27
|
+
interface BuildPiArgsInput {
|
|
28
|
+
baseArgs: string[];
|
|
29
|
+
task: string;
|
|
30
|
+
sessionEnabled: boolean;
|
|
31
|
+
sessionDir?: string;
|
|
32
|
+
sessionFile?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
thinking?: string;
|
|
35
|
+
systemPromptMode?: "append" | "replace";
|
|
36
|
+
inheritProjectContext: boolean;
|
|
37
|
+
inheritSkills: boolean;
|
|
38
|
+
tools?: string[];
|
|
39
|
+
extensions?: string[];
|
|
40
|
+
systemPrompt?: string | null;
|
|
41
|
+
mcpDirectTools?: string[];
|
|
42
|
+
cwd?: string;
|
|
43
|
+
promptFileStem?: string;
|
|
44
|
+
intercomSessionName?: string;
|
|
45
|
+
orchestratorIntercomTarget?: string;
|
|
46
|
+
runId?: string;
|
|
47
|
+
childAgentName?: string;
|
|
48
|
+
childIndex?: number;
|
|
49
|
+
parentEventSink?: string;
|
|
50
|
+
parentControlInbox?: string;
|
|
51
|
+
parentRootRunId?: string;
|
|
52
|
+
parentRunId?: string;
|
|
53
|
+
parentChildIndex?: number;
|
|
54
|
+
parentDepth?: number;
|
|
55
|
+
parentPath?: NestedPathEntry[];
|
|
56
|
+
parentCapabilityToken?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface BuildPiArgsResult {
|
|
60
|
+
args: string[];
|
|
61
|
+
env: Record<string, string | undefined>;
|
|
62
|
+
tempDir?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
66
|
+
if (!model || !thinking || thinking === "off") return model;
|
|
67
|
+
const colonIdx = model.lastIndexOf(":");
|
|
68
|
+
if (colonIdx !== -1 && THINKING_LEVELS.includes(model.substring(colonIdx + 1))) return model;
|
|
69
|
+
return `${model}:${thinking}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
73
|
+
const args = [...input.baseArgs];
|
|
74
|
+
|
|
75
|
+
if (input.sessionFile) {
|
|
76
|
+
fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
|
|
77
|
+
args.push("--session", input.sessionFile);
|
|
78
|
+
} else {
|
|
79
|
+
if (!input.sessionEnabled) {
|
|
80
|
+
args.push("--no-session");
|
|
81
|
+
}
|
|
82
|
+
if (input.sessionDir) {
|
|
83
|
+
fs.mkdirSync(input.sessionDir, { recursive: true });
|
|
84
|
+
args.push("--session-dir", input.sessionDir);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const modelArg = applyThinkingSuffix(input.model, input.thinking);
|
|
89
|
+
if (modelArg) {
|
|
90
|
+
args.push("--model", modelArg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const declaredBuiltinTools = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
|
|
94
|
+
const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
|
|
95
|
+
const toolExtensionPaths: string[] = [];
|
|
96
|
+
if (input.tools?.length) {
|
|
97
|
+
const builtinTools = [...declaredBuiltinTools];
|
|
98
|
+
for (const tool of input.tools) {
|
|
99
|
+
if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
|
|
100
|
+
toolExtensionPaths.push(tool);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (builtinTools.length > 0) {
|
|
104
|
+
if (input.mcpDirectTools?.length) {
|
|
105
|
+
builtinTools.push(...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd));
|
|
106
|
+
}
|
|
107
|
+
args.push("--tools", builtinTools.join(","));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const runtimeExtensions = fanoutAuthorized
|
|
112
|
+
? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
|
|
113
|
+
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
114
|
+
if (input.extensions !== undefined) {
|
|
115
|
+
args.push("--no-extensions");
|
|
116
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
|
|
117
|
+
args.push("--extension", extPath);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) {
|
|
121
|
+
args.push("--extension", extPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!input.inheritSkills) {
|
|
126
|
+
args.push("--no-skills");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let tempDir: string | undefined;
|
|
130
|
+
if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
|
|
131
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
132
|
+
const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
|
|
133
|
+
const promptPath = path.join(tempDir, `${stem}.md`);
|
|
134
|
+
fs.writeFileSync(promptPath, input.systemPrompt, { mode: 0o600 });
|
|
135
|
+
args.push(input.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt", promptPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (input.task.length > TASK_ARG_LIMIT) {
|
|
139
|
+
if (!tempDir) {
|
|
140
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
141
|
+
}
|
|
142
|
+
const taskFilePath = path.join(tempDir, "task.md");
|
|
143
|
+
fs.writeFileSync(taskFilePath, `Task: ${input.task}`, { mode: 0o600 });
|
|
144
|
+
args.push(`@${taskFilePath}`);
|
|
145
|
+
} else {
|
|
146
|
+
args.push(`Task: ${input.task}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const env: Record<string, string | undefined> = {};
|
|
150
|
+
env[SUBAGENT_CHILD_ENV] = "1";
|
|
151
|
+
env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0";
|
|
152
|
+
const inheritedNestedRoute = Boolean(process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] && process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] && process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV]);
|
|
153
|
+
const parentRunId = input.parentRunId ?? input.runId ?? (inheritedNestedRoute ? process.env[SUBAGENT_RUN_ID_ENV] : undefined) ?? process.env[SUBAGENT_PARENT_RUN_ID_ENV] ?? "";
|
|
154
|
+
const parentChildIndex = input.parentChildIndex !== undefined
|
|
155
|
+
? String(input.parentChildIndex)
|
|
156
|
+
: input.childIndex !== undefined
|
|
157
|
+
? String(input.childIndex)
|
|
158
|
+
: process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV] ?? "";
|
|
159
|
+
const inheritedDepth = Number(process.env[SUBAGENT_PARENT_DEPTH_ENV]);
|
|
160
|
+
const parentDepth = input.parentDepth ?? (inheritedNestedRoute && Number.isFinite(inheritedDepth) ? inheritedDepth + 1 : 1);
|
|
161
|
+
const parentPath = input.parentPath ?? [
|
|
162
|
+
...parseNestedPathEnv(process.env[SUBAGENT_PARENT_PATH_ENV]),
|
|
163
|
+
...(parentRunId ? [{
|
|
164
|
+
runId: parentRunId,
|
|
165
|
+
...(parentChildIndex && /^\d+$/.test(parentChildIndex) ? { stepIndex: Number(parentChildIndex) } : {}),
|
|
166
|
+
...(input.childAgentName ? { agent: input.childAgentName } : {}),
|
|
167
|
+
}] : []),
|
|
168
|
+
];
|
|
169
|
+
env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized
|
|
170
|
+
? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? ""
|
|
171
|
+
: "";
|
|
172
|
+
env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized
|
|
173
|
+
? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? ""
|
|
174
|
+
: "";
|
|
175
|
+
env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized
|
|
176
|
+
? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? ""
|
|
177
|
+
: "";
|
|
178
|
+
env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : "";
|
|
179
|
+
env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : "";
|
|
180
|
+
env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : "";
|
|
181
|
+
env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
|
|
182
|
+
env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = fanoutAuthorized
|
|
183
|
+
? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? ""
|
|
184
|
+
: "";
|
|
185
|
+
env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
|
|
186
|
+
env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
|
|
187
|
+
if (input.intercomSessionName) {
|
|
188
|
+
env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
|
|
189
|
+
}
|
|
190
|
+
if (input.orchestratorIntercomTarget) {
|
|
191
|
+
env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
|
|
192
|
+
}
|
|
193
|
+
if (input.runId) {
|
|
194
|
+
env[SUBAGENT_RUN_ID_ENV] = input.runId;
|
|
195
|
+
}
|
|
196
|
+
if (input.childAgentName) {
|
|
197
|
+
env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName;
|
|
198
|
+
}
|
|
199
|
+
if (input.childIndex !== undefined) {
|
|
200
|
+
env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
|
|
201
|
+
}
|
|
202
|
+
if (input.mcpDirectTools?.length) {
|
|
203
|
+
env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
|
|
204
|
+
} else {
|
|
205
|
+
env.MCP_DIRECT_TOOLS = "__none__";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { args, env, tempDir };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const parseParentPathEnv = parseNestedPathEnv;
|
|
212
|
+
|
|
213
|
+
export function cleanupTempDir(tempDir: string | null | undefined): void {
|
|
214
|
+
if (!tempDir) return;
|
|
215
|
+
try {
|
|
216
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
217
|
+
} catch {
|
|
218
|
+
// Temp cleanup is best effort.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
export function findPiPackageRootFromEntry(entryPoint: string): string | undefined {
|
|
8
|
+
let dir = path.dirname(entryPoint);
|
|
9
|
+
while (dir !== path.dirname(dir)) {
|
|
10
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
11
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
|
|
13
|
+
if (pkg.name === PI_CODING_AGENT_PACKAGE) return dir;
|
|
14
|
+
}
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveInstalledPiPackageRoot(): string | undefined {
|
|
21
|
+
return findPiPackageRootFromEntry(fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePiPackageRoot(): string | undefined {
|
|
25
|
+
try {
|
|
26
|
+
const entry = process.argv[1];
|
|
27
|
+
return entry ? findPiPackageRootFromEntry(fs.realpathSync(entry)) : undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
// process.argv[1] probing is best-effort; callers can fall back to PATH/package resolution.
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PiSpawnDeps {
|
|
35
|
+
platform?: NodeJS.Platform;
|
|
36
|
+
execPath?: string;
|
|
37
|
+
argv1?: string;
|
|
38
|
+
existsSync?: (filePath: string) => boolean;
|
|
39
|
+
readFileSync?: (filePath: string, encoding: "utf-8") => string;
|
|
40
|
+
resolvePackageJson?: () => string;
|
|
41
|
+
resolvePackageEntry?: () => string;
|
|
42
|
+
piPackageRoot?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PiSpawnCommand {
|
|
46
|
+
command: string;
|
|
47
|
+
args: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRunnableNodeScript(filePath: string, existsSync: (filePath: string) => boolean): boolean {
|
|
51
|
+
if (!existsSync(filePath)) return false;
|
|
52
|
+
return /\.(?:mjs|cjs|js)$/i.test(filePath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizePath(filePath: string): string {
|
|
56
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveWindowsPiCliScript(deps: PiSpawnDeps = {}): string | undefined {
|
|
60
|
+
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
61
|
+
const readFileSync = deps.readFileSync ?? ((filePath, encoding) => fs.readFileSync(filePath, encoding));
|
|
62
|
+
const argv1 = deps.argv1 ?? process.argv[1];
|
|
63
|
+
|
|
64
|
+
if (argv1) {
|
|
65
|
+
const argvPath = normalizePath(argv1);
|
|
66
|
+
if (isRunnableNodeScript(argvPath, existsSync)) {
|
|
67
|
+
return argvPath;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const resolvePackageJson = deps.resolvePackageJson ?? (() => {
|
|
73
|
+
const root = deps.piPackageRoot ?? resolvePiPackageRoot();
|
|
74
|
+
if (root) return path.join(root, "package.json");
|
|
75
|
+
const packageRoot = deps.resolvePackageEntry
|
|
76
|
+
? findPiPackageRootFromEntry(deps.resolvePackageEntry())
|
|
77
|
+
: resolveInstalledPiPackageRoot();
|
|
78
|
+
if (!packageRoot) throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
|
|
79
|
+
return path.join(packageRoot, "package.json");
|
|
80
|
+
});
|
|
81
|
+
const packageJsonPath = resolvePackageJson();
|
|
82
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|
83
|
+
bin?: string | Record<string, string>;
|
|
84
|
+
};
|
|
85
|
+
const binField = packageJson.bin;
|
|
86
|
+
const binPath = typeof binField === "string"
|
|
87
|
+
? binField
|
|
88
|
+
: binField?.pi ?? Object.values(binField ?? {})[0];
|
|
89
|
+
if (!binPath) return undefined;
|
|
90
|
+
const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
|
|
91
|
+
if (isRunnableNodeScript(candidate, existsSync)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Windows CLI resolution is optional; falling back to `pi` lets PATH handle execution.
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getPiSpawnCommand(args: string[], deps: PiSpawnDeps = {}): PiSpawnCommand {
|
|
103
|
+
const platform = deps.platform ?? process.platform;
|
|
104
|
+
if (platform === "win32") {
|
|
105
|
+
const piCliPath = resolveWindowsPiCliScript(deps);
|
|
106
|
+
if (piCliPath) {
|
|
107
|
+
return {
|
|
108
|
+
command: deps.execPath ?? process.execPath,
|
|
109
|
+
args: [piCliPath, ...args],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { command: "pi", args };
|
|
115
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
4
|
+
|
|
5
|
+
export interface RunEntry {
|
|
6
|
+
agent: string;
|
|
7
|
+
task: string;
|
|
8
|
+
ts: number;
|
|
9
|
+
status: "ok" | "error";
|
|
10
|
+
duration: number;
|
|
11
|
+
exit?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ROTATE_READ_THRESHOLD = 1200;
|
|
15
|
+
const ROTATE_KEEP = 1000;
|
|
16
|
+
|
|
17
|
+
function getHistoryPath(): string {
|
|
18
|
+
return path.join(getAgentDir(), "run-history.jsonl");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
|
|
22
|
+
try {
|
|
23
|
+
const entry: RunEntry = {
|
|
24
|
+
agent,
|
|
25
|
+
task: task.slice(0, 200),
|
|
26
|
+
ts: Math.floor(Date.now() / 1000),
|
|
27
|
+
status: exitCode === 0 ? "ok" : "error",
|
|
28
|
+
duration: durationMs,
|
|
29
|
+
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
30
|
+
};
|
|
31
|
+
const historyPath = getHistoryPath();
|
|
32
|
+
fs.mkdirSync(path.dirname(historyPath), { recursive: true });
|
|
33
|
+
fs.appendFileSync(historyPath, `${JSON.stringify(entry)}\n`);
|
|
34
|
+
} catch {
|
|
35
|
+
// Best-effort — never crash the execution flow for history recording
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
40
|
+
const historyPath = getHistoryPath();
|
|
41
|
+
if (!fs.existsSync(historyPath)) return [];
|
|
42
|
+
let raw: string;
|
|
43
|
+
try {
|
|
44
|
+
raw = fs.readFileSync(historyPath, "utf-8");
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let lines = raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
50
|
+
|
|
51
|
+
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
52
|
+
lines = lines.slice(-ROTATE_KEEP);
|
|
53
|
+
try { fs.writeFileSync(historyPath, `${lines.join("\n")}\n`, "utf-8"); } catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines
|
|
57
|
+
.map((line) => { try { return JSON.parse(line) as RunEntry; } catch { return undefined; } })
|
|
58
|
+
.filter((entry): entry is RunEntry => Boolean(entry) && entry.agent === agent)
|
|
59
|
+
.reverse();
|
|
60
|
+
}
|