pi-subagents 0.40.0 → 0.41.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 +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
|
@@ -132,13 +132,13 @@ function findFamilyMatch(family: StrongWatchdogFamily, availableModels: ModelInf
|
|
|
132
132
|
function resolveStrongCandidate(ctx: ExtensionContext, family: StrongWatchdogFamily): WatchdogModelRecommendation | undefined {
|
|
133
133
|
const availableModels = modelRegistryEntries(ctx);
|
|
134
134
|
const preference = STRONG_WATCHDOG_MODELS[family];
|
|
135
|
-
const queries = [...preference.queries];
|
|
135
|
+
const queries: string[] = [...preference.queries];
|
|
136
136
|
const familyMatch = findFamilyMatch(family, availableModels);
|
|
137
137
|
if (familyMatch) queries.push(familyMatch);
|
|
138
138
|
for (const query of queries) {
|
|
139
139
|
let resolved: ResolvedWatchdogModelInput;
|
|
140
140
|
try {
|
|
141
|
-
resolved = resolveWatchdogModelInput(ctx, query);
|
|
141
|
+
resolved = resolveWatchdogModelInput(ctx, query as Parameters<typeof resolveWatchdogModelInput>[1]);
|
|
142
142
|
} catch {
|
|
143
143
|
continue;
|
|
144
144
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Agent, type AgentTool, type StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
4
|
+
import { Type, type Static } from "typebox";
|
|
5
|
+
import { appendPermissionAudit, permissionArgsPreview } from "../runs/shared/permissions.ts";
|
|
6
|
+
import { decodeChildWatchdogConfig } from "./child-status.ts";
|
|
7
|
+
import { childResolvedConfig } from "./register-child.ts";
|
|
8
|
+
import { resolveWatchdogReviewModel } from "./review.ts";
|
|
9
|
+
|
|
10
|
+
const PermissionDecisionParams = Type.Object({
|
|
11
|
+
decision: Type.String({ enum: ["approve", "deny"] }),
|
|
12
|
+
reason: Type.String({ description: "One concise reason for this exact decision." }),
|
|
13
|
+
}, { additionalProperties: false });
|
|
14
|
+
|
|
15
|
+
type PermissionDecisionParams = Static<typeof PermissionDecisionParams>;
|
|
16
|
+
|
|
17
|
+
export interface WatchdogPermissionResult {
|
|
18
|
+
approved: boolean;
|
|
19
|
+
reason: string;
|
|
20
|
+
source: "watchdog";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WatchdogPermissionRequest {
|
|
24
|
+
ctx: ExtensionContext;
|
|
25
|
+
toolName: string;
|
|
26
|
+
args: unknown;
|
|
27
|
+
rawWatchdogConfig?: string;
|
|
28
|
+
auditPath?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WatchdogPermissionArbiterOptions {
|
|
33
|
+
streamFn?: StreamFn;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function conciseReason(value: string): string {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
return trimmed ? trimmed.slice(0, 500) : "Watchdog returned an empty reason.";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbiterOptions = {}) {
|
|
42
|
+
return async (request: WatchdogPermissionRequest): Promise<WatchdogPermissionResult> => {
|
|
43
|
+
const preview = permissionArgsPreview(request.args);
|
|
44
|
+
const createdAt = Date.now();
|
|
45
|
+
const auditBase = { type: "permission.request", createdAt, toolName: request.toolName, preview, matchedRule: "ask", decisionSource: "watchdog" };
|
|
46
|
+
appendPermissionAudit(request.auditPath, auditBase);
|
|
47
|
+
const finish = (approved: boolean, reason: string, decision: string): WatchdogPermissionResult => {
|
|
48
|
+
appendPermissionAudit(request.auditPath, {
|
|
49
|
+
type: "permission.decision",
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
requestCreatedAt: createdAt,
|
|
52
|
+
toolName: request.toolName,
|
|
53
|
+
decision,
|
|
54
|
+
approved,
|
|
55
|
+
decisionSource: "watchdog",
|
|
56
|
+
reason: conciseReason(reason),
|
|
57
|
+
});
|
|
58
|
+
return { approved, reason: conciseReason(reason), source: "watchdog" };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let childConfig;
|
|
62
|
+
try {
|
|
63
|
+
childConfig = decodeChildWatchdogConfig(request.rawWatchdogConfig);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return finish(false, `Watchdog permission arbiter configuration is invalid: ${reason}`, "unavailable");
|
|
67
|
+
}
|
|
68
|
+
if (!childConfig) return finish(false, "Watchdog permission arbiter is unavailable because the child watchdog is disabled.", "unavailable");
|
|
69
|
+
if (request.signal?.aborted || request.ctx.signal?.aborted) return finish(false, "Watchdog permission decision was cancelled.", "cancelled");
|
|
70
|
+
|
|
71
|
+
let decision: PermissionDecisionParams | undefined;
|
|
72
|
+
const tool: AgentTool<typeof PermissionDecisionParams, { recorded: boolean }> = {
|
|
73
|
+
name: "watchdog_permission_decision",
|
|
74
|
+
label: "Watchdog permission decision",
|
|
75
|
+
description: "Approve or deny this exact child tool call. Call exactly once.",
|
|
76
|
+
parameters: PermissionDecisionParams,
|
|
77
|
+
executionMode: "sequential",
|
|
78
|
+
async execute(_toolCallId, params) {
|
|
79
|
+
if (!decision) decision = params;
|
|
80
|
+
return { content: [{ type: "text", text: "Permission decision recorded." }], details: { recorded: true } };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
85
|
+
let agent: Agent | undefined;
|
|
86
|
+
try {
|
|
87
|
+
const config = childResolvedConfig(childConfig);
|
|
88
|
+
const selection = await resolveWatchdogReviewModel(request.ctx, config);
|
|
89
|
+
const auth = selection.auth;
|
|
90
|
+
const registeredProvider = (request.ctx.modelRegistry as {
|
|
91
|
+
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|
|
92
|
+
}).getRegisteredProviderConfig?.(selection.model.provider);
|
|
93
|
+
const baseStreamFn = options.streamFn ?? (registeredProvider?.streamSimple && registeredProvider.api === selection.model.api
|
|
94
|
+
? registeredProvider.streamSimple
|
|
95
|
+
: streamSimple);
|
|
96
|
+
const streamFn: StreamFn = (model, context, streamOptions) => baseStreamFn(model, context, {
|
|
97
|
+
...streamOptions,
|
|
98
|
+
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
99
|
+
env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
|
|
100
|
+
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
101
|
+
});
|
|
102
|
+
agent = new Agent({
|
|
103
|
+
initialState: {
|
|
104
|
+
systemPrompt: [
|
|
105
|
+
"You are the pi-subagents watchdog permission arbiter.",
|
|
106
|
+
"Decide only whether this exact non-bash child tool call should proceed.",
|
|
107
|
+
"Call watchdog_permission_decision exactly once with approve or deny and a concise reason.",
|
|
108
|
+
"Deny when uncertain. Do not produce freeform advice or ask the parent orchestrator.",
|
|
109
|
+
].join("\n"),
|
|
110
|
+
model: selection.model,
|
|
111
|
+
thinkingLevel: selection.thinkingLevel,
|
|
112
|
+
tools: [tool],
|
|
113
|
+
},
|
|
114
|
+
convertToLlm,
|
|
115
|
+
streamFunction: streamFn,
|
|
116
|
+
getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
|
|
117
|
+
beforeToolCall: async ({ toolCall }) => toolCall.name === tool.name ? undefined : { block: true, reason: `Permission arbiter tool '${toolCall.name}' is not allowed.` },
|
|
118
|
+
toolExecution: "sequential",
|
|
119
|
+
});
|
|
120
|
+
const abort = () => agent?.abort();
|
|
121
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
122
|
+
request.ctx.signal?.addEventListener("abort", abort, { once: true });
|
|
123
|
+
try {
|
|
124
|
+
const prompt = `Tool: ${request.toolName}\nRedacted arguments: ${preview}`;
|
|
125
|
+
await Promise.race([
|
|
126
|
+
agent.prompt(prompt),
|
|
127
|
+
new Promise<never>((_, reject) => { timeout = setTimeout(() => { agent?.abort(); reject(new Error("Watchdog permission decision timed out.")); }, childConfig.agentEndTimeoutMs); }),
|
|
128
|
+
]);
|
|
129
|
+
} finally {
|
|
130
|
+
request.signal?.removeEventListener("abort", abort);
|
|
131
|
+
request.ctx.signal?.removeEventListener("abort", abort);
|
|
132
|
+
}
|
|
133
|
+
if (!decision) return finish(false, "Watchdog permission arbiter returned no decision.", "malformed");
|
|
134
|
+
const approved = decision.decision === "approve";
|
|
135
|
+
return finish(approved, decision.reason, decision.decision);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
138
|
+
return finish(false, `Watchdog permission arbiter failed closed: ${reason}`, reason.includes("timed out") ? "timeout" : "error");
|
|
139
|
+
} finally {
|
|
140
|
+
if (timeout) clearTimeout(timeout);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const requestWatchdogPermission = createWatchdogPermissionArbiter();
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./child-status.ts";
|
|
13
13
|
import type { ResolvedWatchdogConfig, WatchdogWarningDetails } from "./types.ts";
|
|
14
14
|
|
|
15
|
-
function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchdogConfig {
|
|
15
|
+
export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchdogConfig {
|
|
16
16
|
return {
|
|
17
17
|
...DEFAULT_WATCHDOG_CONFIG,
|
|
18
18
|
enabled: true,
|
|
@@ -184,7 +184,7 @@ function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): {
|
|
|
184
184
|
const resolved = resolveWatchdogModelInput(ctx as ExtensionContext, value);
|
|
185
185
|
return {
|
|
186
186
|
model: resolved.model,
|
|
187
|
-
thinking: resolved.thinking,
|
|
187
|
+
thinking: resolved.thinking ?? null,
|
|
188
188
|
description: `${resolved.model}${resolved.thinking ? `:${resolved.thinking}` : ""}`,
|
|
189
189
|
};
|
|
190
190
|
}
|
package/src/watchdog/review.ts
CHANGED
|
@@ -93,6 +93,9 @@ function resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { mode
|
|
|
93
93
|
const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
94
94
|
const preferredProvider = typeof ctx.model?.provider === "string" ? ctx.model.provider : undefined;
|
|
95
95
|
const resolved = resolveModelCandidate(rawModel, availableModels, preferredProvider);
|
|
96
|
+
if (!resolved) {
|
|
97
|
+
throw new Error(`Configured watchdog model '${rawModel}' did not match exactly one authenticated available model. Use provider/model or configure credentials for the intended provider.`);
|
|
98
|
+
}
|
|
96
99
|
const { baseModel } = splitKnownThinkingSuffix(resolved);
|
|
97
100
|
const named = splitProviderModel(baseModel);
|
|
98
101
|
if (!named) {
|
|
@@ -109,7 +112,7 @@ function resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { mode
|
|
|
109
112
|
|
|
110
113
|
async function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {
|
|
111
114
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
112
|
-
if (
|
|
115
|
+
if (auth.ok === false) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);
|
|
113
116
|
return {
|
|
114
117
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
115
118
|
...(auth.headers ? { headers: auth.headers } : {}),
|
package/src/watchdog/runtime.ts
CHANGED
|
@@ -602,8 +602,9 @@ export class MainWatchdogRuntime {
|
|
|
602
602
|
return "timeout";
|
|
603
603
|
}
|
|
604
604
|
if (!this.isCurrent(reviewEpoch, reviewId)) return "stale";
|
|
605
|
-
|
|
606
|
-
|
|
605
|
+
if (!result) return "stale";
|
|
606
|
+
for (const warning of result.warnings ?? []) this.acceptWarning(reviewEpoch, reviewId, warning);
|
|
607
|
+
if (result.stopReason && result.stopReason !== "stop") {
|
|
607
608
|
this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.`);
|
|
608
609
|
return "completed";
|
|
609
610
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { Details } from "../shared/types.ts";
|
|
5
|
+
|
|
6
|
+
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "terminal", "milestones", "live-card"] as const;
|
|
7
|
+
export type WorkflowChatProgressMode = typeof WORKFLOW_CHAT_PROGRESS_MODES[number];
|
|
8
|
+
export type ResolvedWorkflowChatProgressMode = Exclude<WorkflowChatProgressMode, "auto">;
|
|
9
|
+
|
|
10
|
+
export interface GitRepositoryIdentity {
|
|
11
|
+
root: string;
|
|
12
|
+
commonDir: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkflowChatProgressProjection {
|
|
16
|
+
mode: ResolvedWorkflowChatProgressMode;
|
|
17
|
+
repoRelation: "same" | "other";
|
|
18
|
+
repoLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ResolveWorkflowChatProgressInput {
|
|
22
|
+
requested: unknown;
|
|
23
|
+
parentCwd: string;
|
|
24
|
+
workflowCwd: string;
|
|
25
|
+
background: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function git(cwd: string, args: string[]): string | undefined {
|
|
29
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
30
|
+
if (result.status !== 0) return undefined;
|
|
31
|
+
const output = result.stdout.trim();
|
|
32
|
+
return output || undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function realPath(value: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return fs.realpathSync.native(value);
|
|
38
|
+
} catch {
|
|
39
|
+
return path.resolve(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveGitRepositoryIdentity(cwd: string): GitRepositoryIdentity | undefined {
|
|
44
|
+
if (git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return undefined;
|
|
45
|
+
const root = git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
46
|
+
const commonDir = git(cwd, ["rev-parse", "--git-common-dir"]);
|
|
47
|
+
if (!root || !commonDir) return undefined;
|
|
48
|
+
const commonDirPath = path.isAbsolute(commonDir)
|
|
49
|
+
? commonDir
|
|
50
|
+
: [path.resolve(cwd, commonDir), path.resolve(root, commonDir)].find((candidate) => fs.existsSync(candidate)) ?? path.resolve(root, commonDir);
|
|
51
|
+
return {
|
|
52
|
+
root: realPath(root),
|
|
53
|
+
commonDir: realPath(commonDirPath),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSameGitRepositoryIdentity(left: GitRepositoryIdentity | undefined, right: GitRepositoryIdentity | undefined): boolean {
|
|
58
|
+
if (!left || !right) return false;
|
|
59
|
+
return left.commonDir === right.commonDir || left.root === right.root;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isSameGitRepository(leftCwd: string, rightCwd: string): boolean {
|
|
63
|
+
return isSameGitRepositoryIdentity(resolveGitRepositoryIdentity(leftCwd), resolveGitRepositoryIdentity(rightCwd));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeRequestedMode(value: unknown): { mode?: WorkflowChatProgressMode; error?: string } {
|
|
67
|
+
if (value === undefined) return { mode: "auto" };
|
|
68
|
+
if (typeof value !== "string" || !WORKFLOW_CHAT_PROGRESS_MODES.includes(value as WorkflowChatProgressMode)) {
|
|
69
|
+
return { error: `chatProgress must be one of: ${WORKFLOW_CHAT_PROGRESS_MODES.join(", ")}.` };
|
|
70
|
+
}
|
|
71
|
+
return { mode: value as WorkflowChatProgressMode };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressInput): { projection?: WorkflowChatProgressProjection; error?: string } {
|
|
75
|
+
const requested = normalizeRequestedMode(input.requested);
|
|
76
|
+
if (requested.error) return { error: requested.error };
|
|
77
|
+
const parentIdentity = resolveGitRepositoryIdentity(input.parentCwd);
|
|
78
|
+
const workflowIdentity = resolveGitRepositoryIdentity(input.workflowCwd);
|
|
79
|
+
const sameRepo = !!(
|
|
80
|
+
parentIdentity
|
|
81
|
+
&& workflowIdentity
|
|
82
|
+
&& (parentIdentity.commonDir === workflowIdentity.commonDir || parentIdentity.root === workflowIdentity.root)
|
|
83
|
+
);
|
|
84
|
+
const repoLabel = workflowIdentity ? path.basename(workflowIdentity.root) : undefined;
|
|
85
|
+
const repoRelation = sameRepo ? "same" : "other";
|
|
86
|
+
|
|
87
|
+
const requestedMode = requested.mode ?? "auto";
|
|
88
|
+
let mode: ResolvedWorkflowChatProgressMode;
|
|
89
|
+
if (requestedMode === "auto") mode = sameRepo ? (input.background ? "milestones" : "live-card") : "terminal";
|
|
90
|
+
else mode = requestedMode;
|
|
91
|
+
|
|
92
|
+
if (mode === "live-card" && !sameRepo) return { error: "chatProgress: 'live-card' is only available for workflowScript runs in the same Git repository." };
|
|
93
|
+
if (mode === "live-card" && input.background) return { error: "chatProgress: 'live-card' requires a watched foreground workflow; pass async:false." };
|
|
94
|
+
return { projection: { mode, repoRelation, ...(repoLabel ? { repoLabel } : {}) } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface WorkflowChatProgressRow {
|
|
98
|
+
key: string;
|
|
99
|
+
state: "running" | "complete" | "failed";
|
|
100
|
+
label?: string;
|
|
101
|
+
phase?: string;
|
|
102
|
+
runId?: string;
|
|
103
|
+
durationMs?: number;
|
|
104
|
+
error?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cleanLabel(value: unknown): string | undefined {
|
|
108
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workflow"]>["trace"]): WorkflowChatProgressRow[] {
|
|
112
|
+
const rows = new Map<string, WorkflowChatProgressRow>();
|
|
113
|
+
for (const entry of trace) {
|
|
114
|
+
if (entry.operation !== "run") continue;
|
|
115
|
+
const existing = rows.get(entry.key);
|
|
116
|
+
if (entry.state === "reused") {
|
|
117
|
+
if (existing) {
|
|
118
|
+
const label = cleanLabel(entry.label);
|
|
119
|
+
const phase = cleanLabel(entry.phase);
|
|
120
|
+
if (label) existing.label = label;
|
|
121
|
+
if (phase) existing.phase = phase;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const next: WorkflowChatProgressRow = existing ?? { key: entry.key, state: "running" };
|
|
126
|
+
next.state = entry.state === "completed" ? "complete" : entry.state === "failed" ? "failed" : "running";
|
|
127
|
+
const label = cleanLabel(entry.label);
|
|
128
|
+
const phase = cleanLabel(entry.phase);
|
|
129
|
+
if (label) next.label = label;
|
|
130
|
+
if (phase) next.phase = phase;
|
|
131
|
+
if (entry.runId === undefined) delete next.runId;
|
|
132
|
+
else next.runId = entry.runId;
|
|
133
|
+
if (entry.durationMs === undefined) delete next.durationMs;
|
|
134
|
+
else next.durationMs = entry.durationMs;
|
|
135
|
+
if (entry.error === undefined) delete next.error;
|
|
136
|
+
else next.error = entry.error;
|
|
137
|
+
rows.set(entry.key, next);
|
|
138
|
+
}
|
|
139
|
+
return [...rows.values()];
|
|
140
|
+
}
|