pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
import type { AsyncStatus,
|
|
1
|
+
import type { AgentProgress, AsyncStatus, WorkflowChildActivity, WorkflowChildSummary } from "../shared/types.ts";
|
|
2
2
|
import type { WorkflowScriptChildResult, WorkflowScriptTraceEntry } from "./scripted-workflow.ts";
|
|
3
3
|
|
|
4
4
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
5
5
|
const TERMINAL_STATES = new Set(["completed", "failed", "paused", "stopped", "rejected", "detached"]);
|
|
6
6
|
const MAX_REQUIRED_ID_BYTES = 4_096;
|
|
7
|
+
const ACTIVITY_COUNTERS = ["currentToolStartedAt", "lastActivityAt", "durationMs", "toolCount", "turnCount", "tokens", "inputTokens", "outputTokens"] as const;
|
|
8
|
+
type WorkflowChildLiveProgress = WorkflowChildActivity & Pick<AgentProgress, "agent" | "sessionName" | "model" | "thinking">;
|
|
9
|
+
|
|
10
|
+
export function workflowChildProgress(progress: AgentProgress): WorkflowChildLiveProgress {
|
|
11
|
+
return {
|
|
12
|
+
...workflowChildActivity(progress),
|
|
13
|
+
agent: bounded(progress.agent, 256) ?? "",
|
|
14
|
+
...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
|
|
15
|
+
...(bounded(progress.model, 256) ? { model: progress.model } : {}),
|
|
16
|
+
...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Fixed field count and a 256 UTF-8 byte tool name keep JSON activity below 2 KiB, including escaping. */
|
|
21
|
+
export function workflowChildActivity(progress: WorkflowChildActivity): WorkflowChildActivity {
|
|
22
|
+
const activity: WorkflowChildActivity = {};
|
|
23
|
+
if (bounded(progress.currentTool, 256)) activity.currentTool = progress.currentTool;
|
|
24
|
+
for (const field of ACTIVITY_COUNTERS) {
|
|
25
|
+
const value = progress[field];
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) activity[field] = value;
|
|
27
|
+
}
|
|
28
|
+
return activity;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseActivity(value: unknown): WorkflowChildActivity | undefined {
|
|
32
|
+
if (value === undefined) return undefined;
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren child activity is invalid.");
|
|
34
|
+
const input = value as Record<string, unknown>;
|
|
35
|
+
for (const [key, value] of Object.entries(input)) {
|
|
36
|
+
if (key === "currentTool" ? !bounded(value, 256) : !ACTIVITY_COUNTERS.includes(key as typeof ACTIVITY_COUNTERS[number]) || typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error("workflowChildren child activity is invalid.");
|
|
37
|
+
}
|
|
38
|
+
return { ...input } as WorkflowChildActivity;
|
|
39
|
+
}
|
|
7
40
|
|
|
8
41
|
function bounded(value: unknown, maxBytes: number): string | undefined {
|
|
9
42
|
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) return undefined;
|
|
@@ -19,13 +52,15 @@ function requiredId(value: string, label: string): string {
|
|
|
19
52
|
export function workflowChildSummary(input: {
|
|
20
53
|
parentToolCallId: string;
|
|
21
54
|
workflowRunId: string;
|
|
22
|
-
workflowState:
|
|
55
|
+
workflowState: WorkflowChildSummary["workflowState"];
|
|
23
56
|
inventoryComplete: boolean;
|
|
24
57
|
trace?: WorkflowScriptTraceEntry[];
|
|
25
58
|
children?: WorkflowScriptChildResult[];
|
|
26
59
|
steps?: NonNullable<AsyncStatus["steps"]>;
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
/** In-memory snapshots from synchronous child callbacks, keyed by workflow key. */
|
|
61
|
+
progress?: ReadonlyMap<string, WorkflowChildLiveProgress>;
|
|
62
|
+
}): WorkflowChildSummary {
|
|
63
|
+
const rows = new Map<string, WorkflowChildSummary["children"][number]>();
|
|
29
64
|
for (const entry of input.trace ?? []) {
|
|
30
65
|
if (entry.operation !== "run" || !KEY_PATTERN.test(entry.key)) continue;
|
|
31
66
|
const previous = rows.get(entry.key);
|
|
@@ -83,6 +118,18 @@ export function workflowChildSummary(input: {
|
|
|
83
118
|
if (!TERMINAL_STATES.has(row.state)) rows.set(key, { ...row, state: input.workflowState === "stopped" ? "stopped" : "failed" });
|
|
84
119
|
}
|
|
85
120
|
}
|
|
121
|
+
for (const [key, progress] of input.progress ?? []) {
|
|
122
|
+
const row = rows.get(key);
|
|
123
|
+
if (!row || row.state !== "running") continue;
|
|
124
|
+
rows.set(key, {
|
|
125
|
+
...row,
|
|
126
|
+
...(bounded(progress.agent, 256) ? { agent: progress.agent } : {}),
|
|
127
|
+
...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
|
|
128
|
+
...(bounded(progress.model, 256) ? { model: progress.model } : {}),
|
|
129
|
+
...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
|
|
130
|
+
activity: workflowChildActivity(progress),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
86
133
|
return {
|
|
87
134
|
version: 1,
|
|
88
135
|
parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"),
|
|
@@ -93,7 +140,7 @@ export function workflowChildSummary(input: {
|
|
|
93
140
|
};
|
|
94
141
|
}
|
|
95
142
|
|
|
96
|
-
export function parseWorkflowChildSummary(value: unknown):
|
|
143
|
+
export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummary | undefined {
|
|
97
144
|
if (value === undefined) return undefined;
|
|
98
145
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren must be an object.");
|
|
99
146
|
const input = value as Record<string, unknown>;
|
|
@@ -102,17 +149,19 @@ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV
|
|
|
102
149
|
if (input.version !== 1 || typeof input.inventoryComplete !== "boolean" || !Array.isArray(input.children)) throw new Error("workflowChildren is invalid.");
|
|
103
150
|
const workflowState = input.workflowState;
|
|
104
151
|
if (workflowState !== "queued" && workflowState !== "running" && workflowState !== "completed" && workflowState !== "failed" && workflowState !== "paused" && workflowState !== "stopped") throw new Error("workflowChildren.workflowState is invalid.");
|
|
105
|
-
const children = input.children.map((row):
|
|
152
|
+
const children = input.children.map((row): WorkflowChildSummary["children"][number] => {
|
|
106
153
|
if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("workflowChildren child row is invalid.");
|
|
107
154
|
const child = row as Record<string, unknown>;
|
|
108
|
-
if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
|
|
155
|
+
if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state", "activity"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
|
|
109
156
|
if (typeof child.childId !== "string" || !KEY_PATTERN.test(child.childId)) throw new Error("workflowChildren childId is invalid.");
|
|
110
157
|
const state = child.state;
|
|
111
158
|
if (state !== "pending" && state !== "running" && state !== "completed" && state !== "failed" && state !== "paused" && state !== "stopped" && state !== "rejected" && state !== "detached") throw new Error("workflowChildren child state is invalid.");
|
|
112
159
|
for (const [field, maxBytes] of [["runId", 256], ["agent", 256], ["sessionName", 256], ["model", 256], ["thinking", 32]] as const) {
|
|
113
160
|
if (child[field] !== undefined && bounded(child[field], maxBytes) === undefined) throw new Error(`workflowChildren child ${field} is invalid.`);
|
|
114
161
|
}
|
|
115
|
-
|
|
162
|
+
const activity = parseActivity(child.activity);
|
|
163
|
+
if (activity && state !== "running") throw new Error("workflowChildren child activity requires running state.");
|
|
164
|
+
return { childId: child.childId, state, ...(activity ? { activity } : {}), ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.sessionName ? { sessionName: child.sessionName as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
|
|
116
165
|
});
|
|
117
166
|
if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
|
|
118
167
|
if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
|
|
2
2
|
|
|
3
3
|
export const WORKFLOW_PREFLIGHT_VERSION = 1 as const;
|
|
4
4
|
export const WORKFLOW_PREFLIGHT_MAX_LANES = 64;
|
|
@@ -11,7 +11,7 @@ export const WORKFLOW_PREFLIGHT_MAX_WARNINGS = 16;
|
|
|
11
11
|
const WORKFLOW_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
12
|
const PREFLIGHT_FIELDS = new Set(["version", "coverage", "lanes"]);
|
|
13
13
|
const LANE_FIELDS = new Set(["key", "mode", "decision", "claims", "expectedOutput", "independence"]);
|
|
14
|
-
const PREFLIGHT_MODES = new Set<
|
|
14
|
+
const PREFLIGHT_MODES = new Set<WorkflowPreflightLane["mode"]>(["mutation", "review", "scout", "gate"]);
|
|
15
15
|
|
|
16
16
|
type WorkflowTraceLike = {
|
|
17
17
|
operation: string;
|
|
@@ -23,7 +23,7 @@ type WorkflowTraceLike = {
|
|
|
23
23
|
|
|
24
24
|
export interface WorkflowPreflightValidationResult {
|
|
25
25
|
ok: boolean;
|
|
26
|
-
preflight?:
|
|
26
|
+
preflight?: WorkflowPreflight;
|
|
27
27
|
error?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -60,18 +60,18 @@ function normalizeDisplayString(value: unknown, path: string): string {
|
|
|
60
60
|
return normalized;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function normalizeLane(value: unknown, index: number):
|
|
63
|
+
function normalizeLane(value: unknown, index: number): WorkflowPreflightLane {
|
|
64
64
|
const path = `preflight.lanes[${index}]`;
|
|
65
65
|
assertPlainObject(value, path);
|
|
66
66
|
assertKnownFields(value, LANE_FIELDS, path);
|
|
67
67
|
const key = normalizeDisplayString(value.key, `${path}.key`);
|
|
68
68
|
if (!WORKFLOW_KEY_PATTERN.test(key)) throw new Error(`${path}.key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
|
|
69
|
-
let mode:
|
|
69
|
+
let mode: WorkflowPreflightLane["mode"];
|
|
70
70
|
if (value.mode !== undefined) {
|
|
71
|
-
if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as
|
|
71
|
+
if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as WorkflowPreflightLane["mode"])) {
|
|
72
72
|
throw new Error(`${path}.mode must be one of: mutation, review, scout, gate.`);
|
|
73
73
|
}
|
|
74
|
-
mode = value.mode as
|
|
74
|
+
mode = value.mode as WorkflowPreflightLane["mode"];
|
|
75
75
|
}
|
|
76
76
|
let claims: string[] | undefined;
|
|
77
77
|
if (value.claims !== undefined) {
|
|
@@ -97,7 +97,7 @@ function normalizeLane(value: unknown, index: number): WorkflowPreflightLaneV1 {
|
|
|
97
97
|
* independent of workflowScript parsing: callers may describe dynamic fanout
|
|
98
98
|
* without making the metadata a second execution graph.
|
|
99
99
|
*/
|
|
100
|
-
export function normalizeWorkflowPreflight(input: unknown):
|
|
100
|
+
export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflight | undefined {
|
|
101
101
|
if (input === undefined) return undefined;
|
|
102
102
|
assertPlainObject(input, "preflight");
|
|
103
103
|
assertKnownFields(input, PREFLIGHT_FIELDS, "preflight");
|
|
@@ -115,7 +115,7 @@ export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflightV1
|
|
|
115
115
|
if (seenKeys.has(lane.key)) throw new Error(`preflight.lanes contains duplicate key '${lane.key}'.`);
|
|
116
116
|
seenKeys.add(lane.key);
|
|
117
117
|
}
|
|
118
|
-
const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies
|
|
118
|
+
const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies WorkflowPreflight;
|
|
119
119
|
const depth = 1 + (lanes.length > 0 ? 1 : 0) + (lanes.some((lane) => lane.claims !== undefined) ? 1 : 0);
|
|
120
120
|
if (depth > WORKFLOW_PREFLIGHT_MAX_DEPTH) throw new Error(`preflight exceeds the maximum nesting depth of ${WORKFLOW_PREFLIGHT_MAX_DEPTH}.`);
|
|
121
121
|
const bytes = Buffer.byteLength(JSON.stringify(normalized), "utf8");
|
|
@@ -138,7 +138,7 @@ function warningLimit(messages: string[]): string[] {
|
|
|
138
138
|
return [...messages.slice(0, WORKFLOW_PREFLIGHT_MAX_WARNINGS - 1), `Preflight advisory: ${omitted} additional mismatch warning(s) omitted.`];
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function declaredKeys(preflight:
|
|
141
|
+
function declaredKeys(preflight: WorkflowPreflight): Set<string> {
|
|
142
142
|
return new Set(preflight.lanes.map((lane) => lane.key));
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -152,15 +152,15 @@ export function workflowKeyMatchesPreflightLane(key: string, laneKey: string, ge
|
|
|
152
152
|
|
|
153
153
|
/** Select the most specific advisory lane without allowing declaration order to override an exact runtime key. */
|
|
154
154
|
export function workflowPreflightLaneForRuntimeKey(
|
|
155
|
-
preflight:
|
|
155
|
+
preflight: WorkflowPreflight | undefined,
|
|
156
156
|
key: string,
|
|
157
157
|
preferredKeys: readonly (string | undefined)[] = [],
|
|
158
|
-
):
|
|
158
|
+
): WorkflowPreflightLane | undefined {
|
|
159
159
|
const lanes = preflight?.lanes;
|
|
160
160
|
if (!lanes) return undefined;
|
|
161
161
|
const exact = lanes.find((lane) => lane.key === key);
|
|
162
162
|
if (exact) return exact;
|
|
163
|
-
let closestRoot:
|
|
163
|
+
let closestRoot: WorkflowPreflightLane | undefined;
|
|
164
164
|
for (const lane of lanes) {
|
|
165
165
|
if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
|
|
166
166
|
}
|
|
@@ -203,7 +203,7 @@ function unlaunchedWarning(key: string): string {
|
|
|
203
203
|
|
|
204
204
|
/** Return bounded advisory mismatch warnings; these never reject a child launch. */
|
|
205
205
|
export function workflowPreflightWarnings(
|
|
206
|
-
preflight:
|
|
206
|
+
preflight: WorkflowPreflight | undefined,
|
|
207
207
|
trace: readonly WorkflowTraceLike[],
|
|
208
208
|
options: { settled?: boolean } = {},
|
|
209
209
|
): string[] {
|
|
@@ -218,7 +218,7 @@ export function workflowPreflightWarnings(
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/** Attach the first undeclared-key warning to its first trace row for status/debug views. */
|
|
221
|
-
export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight:
|
|
221
|
+
export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight: WorkflowPreflight | undefined): T[] {
|
|
222
222
|
if (!preflight) return [...trace];
|
|
223
223
|
const declared = declaredKeys(preflight);
|
|
224
224
|
const warned = new Set<string>();
|
|
@@ -235,14 +235,14 @@ function laneCell(value: string | undefined): string {
|
|
|
235
235
|
|
|
236
236
|
const WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH = 96;
|
|
237
237
|
|
|
238
|
-
function planLaneLabel(lane:
|
|
238
|
+
function planLaneLabel(lane: WorkflowPreflightLane): string {
|
|
239
239
|
const value = lane.decision?.trim() || lane.key;
|
|
240
240
|
if (value.length <= WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH) return value;
|
|
241
241
|
return `${value.slice(0, WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH - 1).trimEnd()}…`;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/** Render the detailed bounded table reserved for tool output and expanded/debug views. */
|
|
245
|
-
export function formatWorkflowPreflight(preflight:
|
|
245
|
+
export function formatWorkflowPreflight(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
|
|
246
246
|
if (!preflight) return "";
|
|
247
247
|
const indent = options.indent ?? "";
|
|
248
248
|
const count = preflight.lanes.length;
|
|
@@ -263,7 +263,7 @@ export function formatWorkflowPreflight(preflight: WorkflowPreflightV1 | undefin
|
|
|
263
263
|
return lines.join("\n");
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
export function formatWorkflowPreflightSummary(preflight:
|
|
266
|
+
export function formatWorkflowPreflightSummary(preflight: WorkflowPreflight | undefined): string {
|
|
267
267
|
if (!preflight) return "";
|
|
268
268
|
const keys = preflight.lanes.slice(0, 4).map((lane) => lane.key).join(", ");
|
|
269
269
|
const remainder = preflight.lanes.length > 4 ? `, +${preflight.lanes.length - 4}` : "";
|
|
@@ -271,7 +271,7 @@ export function formatWorkflowPreflightSummary(preflight: WorkflowPreflightV1 |
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
/** Render the operator-facing one-line plan shown in routine status views. */
|
|
274
|
-
export function formatWorkflowPreflightPlanSummary(preflight:
|
|
274
|
+
export function formatWorkflowPreflightPlanSummary(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
|
|
275
275
|
if (!preflight) return "";
|
|
276
276
|
const indent = options.indent ?? "";
|
|
277
277
|
const count = preflight.lanes.length;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
4
|
-
import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction,
|
|
4
|
+
import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenance, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
|
|
5
5
|
import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
|
|
7
7
|
import { HOST_STEP_MAX_COUNT, assertUniqueHostStepIds, parseHostStepNode } from "../runs/shared/host-step-status.ts";
|
|
@@ -39,7 +39,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
39
39
|
children: WorkflowScriptChildResult[];
|
|
40
40
|
hostSteps?: WorkflowReceipt["hostSteps"];
|
|
41
41
|
workflowChildren?: WorkflowReceipt["workflowChildren"];
|
|
42
|
-
resource?:
|
|
42
|
+
resource?: WorkflowResourceProvenance;
|
|
43
43
|
terminalOutcome?: WorkflowTerminalOutcome;
|
|
44
44
|
createdAt?: number;
|
|
45
45
|
}): WorkflowReceipt {
|
|
@@ -262,7 +262,7 @@ function parseWorkflowResolution(value: unknown, source: string): WorkflowTermin
|
|
|
262
262
|
return value;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function parseWorkflowResource(value: unknown, source: string):
|
|
265
|
+
function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenance | undefined {
|
|
266
266
|
if (value === undefined) return undefined;
|
|
267
267
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': resource must be an object.`);
|
|
268
268
|
const resource = value as Record<string, unknown>;
|
|
@@ -2,10 +2,10 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { stableJsonDigest } from "../shared/launch-contract.ts";
|
|
3
3
|
import {
|
|
4
4
|
createWorkflowResourcePermit,
|
|
5
|
-
type
|
|
5
|
+
type WorkflowResourceHostAuthority,
|
|
6
6
|
type WorkflowResourcePermit,
|
|
7
7
|
} from "../shared/workflow-child-permit.ts";
|
|
8
|
-
import type {
|
|
8
|
+
import type { WorkflowResourceProvenance } from "../shared/types.ts";
|
|
9
9
|
|
|
10
10
|
const RESOURCE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
11
11
|
const MAX_ARGS_BYTES = 16 * 1024;
|
|
@@ -14,18 +14,75 @@ const MAX_STRING_BYTES = 16 * 1024;
|
|
|
14
14
|
export interface ResolvedWorkflowResource {
|
|
15
15
|
script: string;
|
|
16
16
|
permit: WorkflowResourcePermit;
|
|
17
|
-
provenance:
|
|
17
|
+
provenance: WorkflowResourceProvenance;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export type WorkflowResourceResolution =
|
|
21
21
|
| { ok: true; resource: ResolvedWorkflowResource }
|
|
22
22
|
| { ok: false; error: string };
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
export interface WorkflowResourceDefinition {
|
|
25
25
|
name: string;
|
|
26
26
|
version: number;
|
|
27
|
-
|
|
28
|
-
};
|
|
27
|
+
/** Trusted synchronous validation/expansion. The extension owns semantic command binding. */
|
|
28
|
+
resolve: (args: Readonly<Record<string, unknown>>) => { script: string; hostCommands?: readonly WorkflowResourceHostAuthority[] } | { error: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkflowResourceRegistration {
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RegisterWorkflowResourceInput {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
definition: WorkflowResourceDefinition;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface WorkflowResourceRegistry {
|
|
41
|
+
version: 1;
|
|
42
|
+
bySession: Map<string, Map<string, WorkflowResourceDefinition>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function registry(): WorkflowResourceRegistry {
|
|
46
|
+
const key = Symbol.for("pi-subagents.workflow-resources.v1");
|
|
47
|
+
const globalObject = globalThis as Record<PropertyKey, unknown>;
|
|
48
|
+
const existing = globalObject[key];
|
|
49
|
+
if (existing === undefined) {
|
|
50
|
+
const created: WorkflowResourceRegistry = { version: 1, bySession: new Map() };
|
|
51
|
+
globalObject[key] = created;
|
|
52
|
+
return created;
|
|
53
|
+
}
|
|
54
|
+
if (!isPlainRecord(existing) || existing.version !== 1 || !(existing.bySession instanceof Map)) throw new Error("Malformed or unsupported workflow resource registry.");
|
|
55
|
+
return existing as unknown as WorkflowResourceRegistry;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Session ID scopes lookup, not authentication. Dispose on session_shutdown; issued permits remain valid. */
|
|
59
|
+
export function registerWorkflowResource(input: RegisterWorkflowResourceInput): WorkflowResourceRegistration {
|
|
60
|
+
if (!isPlainRecord(input) || Object.keys(input).some((key) => key !== "sessionId" && key !== "definition")) throw new Error("Workflow registration requires only sessionId and definition.");
|
|
61
|
+
const { sessionId, definition } = input;
|
|
62
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.trim() !== sessionId || sessionId.length > 256 || sessionId.includes("\0")) throw new Error("Workflow registration requires a non-empty trimmed sessionId of at most 256 characters without NUL.");
|
|
63
|
+
if (!isPlainRecord(definition) || Object.keys(definition).some((key) => !["name", "version", "resolve"].includes(key))) throw new Error("Workflow definition requires only name, version and resolve.");
|
|
64
|
+
const { name, version, resolve } = definition;
|
|
65
|
+
if (typeof name !== "string" || !RESOURCE_NAME_PATTERN.test(name)) throw new Error("Workflow definition requires a safe resource name.");
|
|
66
|
+
if (!Number.isSafeInteger(version) || version < 1) throw new Error("Workflow definition version must be a positive safe integer.");
|
|
67
|
+
if (typeof resolve !== "function") throw new Error("Workflow definition requires a synchronous resolve function.");
|
|
68
|
+
if (findWorkflowResource(name)) throw new Error(`Workflow resource '${name}' is a protected builtin.`);
|
|
69
|
+
const current = registry();
|
|
70
|
+
const bucket = current.bySession.get(sessionId) ?? new Map<string, WorkflowResourceDefinition>();
|
|
71
|
+
if (!(bucket instanceof Map)) throw new Error("Malformed workflow resource session registry.");
|
|
72
|
+
if (bucket.has(name)) throw new Error(`Workflow resource '${name}' is already registered in this session; dispose it first.`);
|
|
73
|
+
const snapshot = Object.freeze({ name, version, resolve });
|
|
74
|
+
bucket.set(name, snapshot);
|
|
75
|
+
current.bySession.set(sessionId, bucket);
|
|
76
|
+
let disposed = false;
|
|
77
|
+
return {
|
|
78
|
+
dispose() {
|
|
79
|
+
if (disposed) return;
|
|
80
|
+
disposed = true;
|
|
81
|
+
if (bucket.get(name) === snapshot) bucket.delete(name);
|
|
82
|
+
if (bucket.size === 0 && current.bySession.get(sessionId) === bucket) current.bySession.delete(sessionId);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
29
86
|
|
|
30
87
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
31
88
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
@@ -68,19 +125,19 @@ function validatePlainJson(value: unknown, path: string, depth = 0): void {
|
|
|
68
125
|
}
|
|
69
126
|
}
|
|
70
127
|
|
|
71
|
-
function normalizeArgs(value: unknown): { args
|
|
128
|
+
function normalizeArgs(value: unknown): { args: Record<string, unknown> } | { error: string } {
|
|
72
129
|
if (value === undefined) return { args: {} };
|
|
73
130
|
if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." };
|
|
74
131
|
try {
|
|
75
132
|
validatePlainJson(value, "workflow args");
|
|
76
133
|
if (jsonByteLength(value) > MAX_ARGS_BYTES) return { error: `workflow args exceed ${MAX_ARGS_BYTES} bytes.` };
|
|
77
|
-
return { args:
|
|
134
|
+
return { args: JSON.parse(JSON.stringify(value)) };
|
|
78
135
|
} catch (error) {
|
|
79
136
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
80
137
|
}
|
|
81
138
|
}
|
|
82
139
|
|
|
83
|
-
function resolveRunCi(args: Record<string, unknown
|
|
140
|
+
function resolveRunCi(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
|
|
84
141
|
const allowed = new Set(["command", "timeoutMs"]);
|
|
85
142
|
const unsupported = Object.keys(args).filter((key) => !allowed.has(key));
|
|
86
143
|
if (unsupported.length > 0) return { error: `workflow 'run-ci' args contain unsupported fields: ${unsupported.join(", ")}.` };
|
|
@@ -91,18 +148,17 @@ function resolveRunCi(args: Record<string, unknown>): { script: string; authorit
|
|
|
91
148
|
const params = { kind: "command", command, timeoutMs, role: "ci" };
|
|
92
149
|
return {
|
|
93
150
|
script: `return await runs.host("ci", ${JSON.stringify(params)});`,
|
|
94
|
-
|
|
151
|
+
hostCommands: [{ key: "ci", command }],
|
|
95
152
|
};
|
|
96
153
|
}
|
|
97
154
|
|
|
98
|
-
function resolveReview(args: Record<string, unknown
|
|
155
|
+
function resolveReview(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
|
|
99
156
|
const unsupported = Object.keys(args).filter((key) => key !== "task");
|
|
100
157
|
if (unsupported.length > 0) return { error: `workflow 'review' args contain unsupported fields: ${unsupported.join(", ")}.` };
|
|
101
158
|
const task = args.task;
|
|
102
159
|
if (typeof task !== "string" || !task.trim()) return { error: "workflow 'review' requires a non-empty string args.task." };
|
|
103
160
|
return {
|
|
104
161
|
script: `return (await runs.run("review", { agent: "reviewer", task: ${JSON.stringify(task.trim())} })).output;`,
|
|
105
|
-
authority: {},
|
|
106
162
|
};
|
|
107
163
|
}
|
|
108
164
|
|
|
@@ -120,25 +176,44 @@ function listWorkflowResourceNames(): string[] {
|
|
|
120
176
|
}
|
|
121
177
|
|
|
122
178
|
/** Resolve only extension-owned resources so policy can distinguish them from raw scripts; caller-provided script text is never consulted. */
|
|
123
|
-
export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown): WorkflowResourceResolution {
|
|
179
|
+
export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown, sessionId?: string): WorkflowResourceResolution {
|
|
180
|
+
try {
|
|
181
|
+
const result = resolveResource(nameValue, argsValue, sessionId);
|
|
182
|
+
return result.ok ? result : { ok: false, error: result.error.slice(0, 4096) };
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return { ok: false, error: error instanceof Error ? error.message.slice(0, 4096) : "Workflow resource resolution failed." };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveResource(nameValue: unknown, argsValue?: unknown, sessionId?: string): WorkflowResourceResolution {
|
|
124
189
|
if (typeof nameValue !== "string" || !nameValue.trim()) return { ok: false, error: "workflow must be a non-empty resource name." };
|
|
125
190
|
const name = nameValue.trim();
|
|
126
191
|
if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." };
|
|
127
|
-
const resource = findWorkflowResource(name);
|
|
192
|
+
const resource = findWorkflowResource(name) ?? (sessionId ? registry().bySession.get(sessionId)?.get(name) : undefined);
|
|
128
193
|
if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` };
|
|
129
194
|
const normalizedArgs = normalizeArgs(argsValue);
|
|
130
|
-
if (normalizedArgs
|
|
131
|
-
const resolved = resource.resolve(normalizedArgs.args
|
|
132
|
-
if (
|
|
195
|
+
if ("error" in normalizedArgs) return { ok: false, error: normalizedArgs.error };
|
|
196
|
+
const resolved = resource.resolve(normalizedArgs.args);
|
|
197
|
+
if (resolved && typeof (resolved as unknown as { then?: unknown }).then === "function") {
|
|
198
|
+
void Promise.resolve(resolved).catch(() => {});
|
|
199
|
+
throw new Error("Workflow resource resolve must be synchronous.");
|
|
200
|
+
}
|
|
201
|
+
if (!isPlainRecord(resolved)) throw new Error("Workflow resource resolve must return an expansion or error object.");
|
|
202
|
+
if ("error" in resolved) {
|
|
203
|
+
if (Object.keys(resolved).some((key) => key !== "error") || typeof resolved.error !== "string" || !resolved.error.trim()) throw new Error("Workflow resource returned an invalid error.");
|
|
204
|
+
return { ok: false, error: resolved.error };
|
|
205
|
+
}
|
|
206
|
+
const { script, hostCommands } = resolved;
|
|
207
|
+
if (Object.keys(resolved).some((key) => key !== "script" && key !== "hostCommands") || typeof script !== "string" || !script.trim()) throw new Error("Workflow resource returned an invalid expansion.");
|
|
133
208
|
const resourceId = randomUUID();
|
|
134
209
|
const permit = createWorkflowResourcePermit({
|
|
135
210
|
resourceName: resource.name,
|
|
136
211
|
resourceVersion: resource.version,
|
|
137
212
|
resourceId,
|
|
138
|
-
scriptDigest: stableJsonDigest(
|
|
139
|
-
authority:
|
|
213
|
+
scriptDigest: stableJsonDigest(script),
|
|
214
|
+
authority: { host: hostCommands },
|
|
140
215
|
});
|
|
141
|
-
const provenance:
|
|
216
|
+
const provenance: WorkflowResourceProvenance = Object.freeze({
|
|
142
217
|
kind: "workflow",
|
|
143
218
|
name: resource.name,
|
|
144
219
|
version: resource.version,
|
|
@@ -146,5 +221,5 @@ export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown)
|
|
|
146
221
|
expansion: "resolved",
|
|
147
222
|
id: resourceId,
|
|
148
223
|
});
|
|
149
|
-
return { ok: true, resource: { script
|
|
224
|
+
return { ok: true, resource: { script, permit, provenance } };
|
|
150
225
|
}
|
|
@@ -209,6 +209,8 @@ export function planWorkflowSettlement(input: {
|
|
|
209
209
|
if (!resolution) resolution = classifyWorkflowSettlement(status);
|
|
210
210
|
}
|
|
211
211
|
const recovery = workflowRecoveryActions(receipt);
|
|
212
|
+
delete status.workflowReceiptPath;
|
|
213
|
+
if (receipt && input.receiptPath) status.workflowReceiptPath = input.receiptPath;
|
|
212
214
|
if (receipt) {
|
|
213
215
|
receipt = {
|
|
214
216
|
...receipt,
|
|
@@ -233,6 +235,7 @@ export function planWorkflowSettlement(input: {
|
|
|
233
235
|
timestamp: now,
|
|
234
236
|
};
|
|
235
237
|
const terminal = status.state === "complete" || status.state === "failed" || status.state === "partial" || status.state === "stopped";
|
|
238
|
+
if (!status.workflowReceiptPath) delete publicResult.workflowReceipt;
|
|
236
239
|
const completionEvent = terminal ? {
|
|
237
240
|
type: "subagent.workflow.completed",
|
|
238
241
|
state: status.state,
|
|
File without changes
|