diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c68abad --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Copy to .env and fill in values. Loaded from the project root (walks up from cwd) +# and passed to spawned PM/dev/verifier subagents. +# Alternatively use `/login` → OpenRouter; credentials are stored in ~/.pi/agent/auth.json. + +# Required for OpenRouter models (see .pi/agents/*.md) +OPENROUTER_API_KEY= + +# Optional: integration test harness (tests/harness/pm-memory.mjs) +# PI_TEST_MODEL=anthropic/claude-sonnet-4-5 diff --git a/.gitignore b/.gitignore index 25d1823..a3c996f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ node_modules .DS_Store + +# Environment / secrets +.env +.env.* +!.env.example +.pi/workflows/sessions/ .pnpm-store npm-debug.log +bun.lock # Coverage coverage/ diff --git a/.pi/agents/developer.md b/.pi/agents/developer.md index c86272f..aa98880 100644 --- a/.pi/agents/developer.md +++ b/.pi/agents/developer.md @@ -1,7 +1,7 @@ --- name: developer description: Implements assigned tasks. -model: openrouter/stepfun/step-3.5-flash:free +model: openrouter/stepfun/step-3.5-flash tools: read,edit,write,bash,grep,find,ls --- diff --git a/.pi/agents/pm.md b/.pi/agents/pm.md index 9d7f743..b236ae4 100644 --- a/.pi/agents/pm.md +++ b/.pi/agents/pm.md @@ -1,7 +1,7 @@ --- name: pm description: Project manager who plans and delegates tasks in waves. -model: openrouter/stepfun/step-3.5-flash:free +model: openrouter/stepfun/step-3.5-flash tools: read,grep,find,ls --- diff --git a/.pi/agents/verifier.md b/.pi/agents/verifier.md index ae22b30..839ac61 100644 --- a/.pi/agents/verifier.md +++ b/.pi/agents/verifier.md @@ -1,7 +1,7 @@ --- name: verifier description: Reviews developer work and validates requirements. -model: openrouter/stepfun/step-3.5-flash:free +model: openrouter/stepfun/step-3.5-flash tools: read,grep,find,ls,bash --- diff --git a/.pi/extensions/workflow-orchestrator/engine.ts b/.pi/extensions/workflow-orchestrator/engine.ts index a494620..be415eb 100644 --- a/.pi/extensions/workflow-orchestrator/engine.ts +++ b/.pi/extensions/workflow-orchestrator/engine.ts @@ -29,8 +29,8 @@ export interface TaskFlowInput { output: TOutput | null, error?: string, reason?: "verification_failed" | "malformed_output" | "error", - ) => boolean; - applyGenericFailure: (task: TTask, error: string) => boolean; + ) => void; + applyGenericFailure: (task: TTask, error: string) => void; } function defaultGetField(obj: any, path: string): any { @@ -56,6 +56,11 @@ export async function runTaskFlow( const getNextStageId = input.getNextStageId ?? defaultGetNextStageId; let currentStageId = input.startStageId ?? input.stages[0]?.id; + function recordFailure(): boolean { + input.task.retries += 1; + return input.task.retries <= input.maxRetries; + } + while (currentStageId) { if (input.isStopped?.(input.task)) return; @@ -74,8 +79,8 @@ export async function runTaskFlow( input.onError?.(stage, input.task, error instanceof Error ? error : new Error(message)); if (stage.id === "verify") { - const retry = input.applyVerifyFailure(input.task, stage.id, null, message, "error"); - if (!retry) { + input.applyVerifyFailure(input.task, stage.id, null, message, "error"); + if (!recordFailure()) { input.markFailed(input.task, stage.id); return; } @@ -83,8 +88,8 @@ export async function runTaskFlow( continue; } - const retry = input.applyGenericFailure(input.task, message); - if (!retry) { + input.applyGenericFailure(input.task, message); + if (!recordFailure()) { input.markFailed(input.task, stage.id); return; } @@ -123,8 +128,8 @@ export async function runTaskFlow( if ((nextStageId === firstStageId || nextStageId === stage.id) && stage.id === "verify") { const reason = nextStageId === stage.id && !matchedTransition ? "malformed_output" : "verification_failed"; - const retry = input.applyVerifyFailure(input.task, stage.id, output, undefined, reason); - if (!retry) { + input.applyVerifyFailure(input.task, stage.id, output, undefined, reason); + if (!recordFailure()) { input.markFailed(input.task, stage.id); return; } diff --git a/.pi/extensions/workflow-orchestrator/env.ts b/.pi/extensions/workflow-orchestrator/env.ts new file mode 100644 index 0000000..3e90e5d --- /dev/null +++ b/.pi/extensions/workflow-orchestrator/env.ts @@ -0,0 +1,47 @@ +import { config as loadDotenv } from "dotenv"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +/** Walk upward from cwd to find the pi project root (has .pi/agents or pi package.json). */ +export function findProjectRoot(startDir: string): string | null { + let current = path.resolve(startDir); + while (true) { + if (fs.existsSync(path.join(current, ".pi", "agents"))) return current; + + const pkgPath = path.join(current, "package.json"); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { + pi?: { extensions?: unknown }; + }; + if (pkg.pi?.extensions) return current; + } catch { + /* ignore malformed package.json */ + } + } + + const parent = path.dirname(current); + if (parent === current) return null; + current = parent; + } +} + +const loadedRoots = new Set(); + +/** Load `.env` from the project root into `process.env` (does not override existing vars). */ +export function loadProjectEnv(cwd: string): void { + const root = findProjectRoot(cwd) ?? path.resolve(cwd); + if (loadedRoots.has(root)) return; + + const envPath = path.join(root, ".env"); + if (fs.existsSync(envPath)) { + loadDotenv({ path: envPath }); + } + loadedRoots.add(root); +} + +/** Environment for spawned `pi` subagents: parent env plus project `.env`. */ +export function getSubagentEnv(cwd: string): NodeJS.ProcessEnv { + loadProjectEnv(cwd); + return { ...process.env }; +} diff --git a/.pi/extensions/workflow-orchestrator/index.ts b/.pi/extensions/workflow-orchestrator/index.ts index 9b06e18..b4c1ca5 100644 --- a/.pi/extensions/workflow-orchestrator/index.ts +++ b/.pi/extensions/workflow-orchestrator/index.ts @@ -15,6 +15,7 @@ import { type WorkflowTask, type WorkflowWave, } from "./config.js"; +import { loadProjectEnv } from "./env.js"; import { runTaskFlow } from "./engine.js"; import { setPmWidgetStatus, setTaskListExpanded, updateStatus } from "./render.js"; import { RpcAgent } from "./runner.js"; @@ -41,12 +42,22 @@ let pmRunner: RpcAgent | null = null; let statusInterval: ReturnType | null = null; const taskRunners = new Map(); const taskLocks = new Set(); +const clarificationWaiters = new Map void>>(); function setState(pi: ExtensionAPI, ctx: ExtensionContext, state: WorkflowState, persist = true) { + const previousClarificationToken = currentState?.clarificationToken; const nextState = { ...state, updatedAt: Date.now() }; currentState = nextState; if (persist) appendState(pi, nextState); updateStatus(ctx, nextState); + if ( + previousClarificationToken && + (previousClarificationToken !== nextState.clarificationToken || + !nextState.waitingForClarification || + nextState.active === false) + ) { + signalClarificationResolved(previousClarificationToken); + } } function startStatusTicker(ctx: ExtensionContext) { @@ -141,6 +152,29 @@ function sendWorkflowNotice(pi: ExtensionAPI, text: string) { }); } +function createClarificationToken(): string { + return `${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +export function signalClarificationResolved(token?: string): void { + if (!token) return; + const waiters = clarificationWaiters.get(token); + if (!waiters) return; + clarificationWaiters.delete(token); + for (const resolve of waiters) resolve(); +} + +function clearClarificationWaiters(): void { + const tokens = Array.from(clarificationWaiters.keys()); + for (const token of tokens) signalClarificationResolved(token); +} + +function resetTransientWorkflowState(): void { + pmBusy = false; + taskLocks.clear(); + clearClarificationWaiters(); +} + function buildWaveSummary(state: WorkflowState): string { const lines = (state.tasks ?? []).map((task) => { const status = @@ -173,6 +207,66 @@ function buildPmChatPrompt(state: WorkflowState, message: string): string { ].join("\n\n"); } +export async function waitForClarification(signal: AbortSignal, token: string): Promise { + if (signal.aborted) return; + + await new Promise((resolve) => { + const onAbort = () => { + cleanup(); + resolve(); + }; + + const onResolve = () => { + cleanup(); + resolve(); + }; + + const cleanup = () => { + signal.removeEventListener("abort", onAbort); + const waiters = clarificationWaiters.get(token); + if (!waiters) return; + waiters.delete(onResolve); + if (waiters.size === 0) clarificationWaiters.delete(token); + }; + + let waiters = clarificationWaiters.get(token); + if (!waiters) { + waiters = new Set(); + clarificationWaiters.set(token, waiters); + } + waiters.add(onResolve); + + signal.addEventListener("abort", onAbort, { once: true }); + }); +} + +async function pauseForClarification( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + signal: AbortSignal, + waveIndex: number, + previousSummary: string, +): Promise { + if (!currentState) throw new Error("No workflow state"); + + sendWorkflowNotice(pi, "PM is waiting for your response..."); + const clarificationToken = createClarificationToken(); + setState(pi, ctx, { + ...currentState, + waveIndex, + wave: undefined, + tasks: [], + updatedAt: Date.now(), + previousSummary, + waveSummaries: currentState.waveSummaries ?? [], + active: true, + waitingForClarification: true, + clarificationToken, + }); + + await waitForClarification(signal, clarificationToken); +} + async function mapWithConcurrencyLimit( items: T[], concurrency: number, @@ -524,13 +618,12 @@ async function processTask( setState(pi, ctx, { ...currentState, tasks: [...currentState.tasks] }); }, applyVerifyFailure: (t, stageId, result, errorMessage, reason = "verification_failed") => { - if (!currentState) return false; // Guard against workflow stop during execution + if (!currentState) return; // Guard against workflow stop during execution const output = result?.output; const issues = output?.issues ?? (errorMessage ? [errorMessage] : []); if (!t.stageOutputs) t.stageOutputs = {}; t.stageOutputs[stageId] = { status: "fail", issues }; t.issues = Array.isArray(issues) ? issues.map(String) : [String(issues)]; - t.retries += 1; t.lastNote = errorMessage ? `error: ${errorMessage}` : "fail"; if (errorMessage) t.lastOutput = truncateTicker(errorMessage); @@ -557,16 +650,13 @@ async function processTask( } setState(pi, ctx, { ...currentState, tasks: [...currentState.tasks] }); - return t.retries <= (config.maxTaskRetries ?? 2); }, applyGenericFailure: (t, errorMessage) => { - if (!currentState) return false; // Guard against workflow stop during execution + if (!currentState) return; // Guard against workflow stop during execution t.issues = [errorMessage]; - t.retries += 1; t.lastNote = `error: ${errorMessage}`; t.lastOutput = truncateTicker(errorMessage); setState(pi, ctx, { ...currentState, tasks: [...currentState.tasks] }); - return t.retries <= (config.maxTaskRetries ?? 2); }, markVerified: (t, stageId) => { if (!currentState) return; // Guard against workflow stop during execution @@ -748,6 +838,65 @@ async function generateWaveFromPm( throw new Error("PM output missing wave"); } +async function resolveWaveForIndex( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + config: WorkflowConfig, + agents: ReturnType["agents"], + signal: AbortSignal, + previousSummary: string, + waveIndex: number, + currentWave?: WorkflowWave, + currentTasks?: TaskState[], +): Promise<{ + wave?: WorkflowWave; + tasks?: TaskState[]; + done?: boolean; + clarification?: boolean; +}> { + if (currentWave && currentTasks) { + return { wave: currentWave, tasks: currentTasks }; + } + + if (config.waveSource.type === "static") { + const wave = config.waveSource.staticWaves?.[waveIndex]; + if (!wave) return { done: true }; + return { wave, tasks: wave.tasks.map(buildTaskState) }; + } + + const maxPmRetries = config.maxPmRetries ?? 3; + let pmResult: Awaited> | null = null; + let pmAttempts = 0; + let lastError: string | undefined; + + while (pmAttempts < maxPmRetries) { + try { + pmResult = await generateWaveFromPm( + pi, + config, + agents, + ctx, + signal, + previousSummary, + lastError, + ); + if (pmResult.done) return { done: true }; + if (pmResult.wave) { + return { wave: pmResult.wave, tasks: pmResult.wave.tasks.map(buildTaskState) }; + } + + // PM returned clarification - surface it and let the caller wait for user input. + return { done: false, clarification: true }; + } catch (error: any) { + lastError = error.message; + pmAttempts++; + if (pmAttempts >= maxPmRetries) throw error; + } + } + + return { done: true }; +} + async function resumeWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { if (currentRun) { if (ctx.hasUI) ctx.ui.notify("Workflow already running", "warning"); @@ -777,66 +926,30 @@ async function resumeWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): P ) { if (abortController.signal.aborted) throw new Error("Workflow aborted"); - let wave: WorkflowWave | undefined; - let tasks: TaskState[] | undefined; const hasExistingWave = waveIndex === currentState!.waveIndex && currentState?.wave && currentState.tasks.length > 0; - - if (hasExistingWave) { - wave = currentState!.wave; - tasks = currentState!.tasks; - } else if (effectiveConfig.waveSource.type === "static") { - wave = effectiveConfig.waveSource.staticWaves?.[waveIndex]; - } else { - const pmResult = await generateWaveFromPm( - pi, - effectiveConfig, - agents, - ctx, - abortController.signal, - previousSummary, - ); - if (pmResult.done) { - break; - } - if (pmResult.clarification) { - // PM needs clarification - pause and wait for user response - sendWorkflowNotice(pi, "PM is waiting for your response..."); - // Set a flag that we're waiting for clarification - const waitingState: WorkflowState = { - ...currentState!, - waveIndex, - wave: undefined, - tasks: [], - updatedAt: Date.now(), - previousSummary, - waveSummaries: currentState?.waveSummaries ?? [], - active: true, - waitingForClarification: true, - }; - setState(pi, ctx, waitingState); - // Pause the workflow loop - user will respond via PM chat - await new Promise((resolve) => { - const checkInterval = setInterval(() => { - if (!currentState?.waitingForClarification || abortController.signal.aborted) { - clearInterval(checkInterval); - resolve(); - } - }, 500); - }); - // User responded, PM should have processed it - retry wave generation - continue; - } - wave = pmResult.wave; + const resolved = await resolveWaveForIndex( + pi, + ctx, + effectiveConfig, + agents, + abortController.signal, + previousSummary, + waveIndex, + hasExistingWave ? currentState!.wave : undefined, + hasExistingWave ? currentState!.tasks : undefined, + ); + if (resolved.done) break; + if (resolved.clarification) { + await pauseForClarification(pi, ctx, abortController.signal, waveIndex, previousSummary); + waveIndex -= 1; + continue; } + if (!resolved.wave || !resolved.tasks) continue; - if (!wave) break; - - if (!tasks) { - tasks = (wave.tasks ?? []).map(buildTaskState); - } + const { wave, tasks } = resolved; const updatedState: WorkflowState = { ...currentState!, @@ -875,6 +988,8 @@ async function resumeWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): P const finalState: WorkflowState = { ...currentState!, active: false, + waitingForClarification: false, + clarificationToken: undefined, updatedAt: Date.now(), }; setState(pi, ctx, finalState); @@ -885,7 +1000,13 @@ async function resumeWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): P sendWorkflowNotice(pi, `Workflow error: ${message}`); if (ctx.hasUI) ctx.ui.notify(message, "error"); if (currentState) { - setState(pi, ctx, { ...currentState, active: false, updatedAt: Date.now() }); + setState(pi, ctx, { + ...currentState, + active: false, + waitingForClarification: false, + clarificationToken: undefined, + updatedAt: Date.now(), + }); } } finally { disposePmRunner(); @@ -937,54 +1058,23 @@ async function startWorkflow( for (let waveIndex = 0; waveIndex < (effectiveConfig.maxWaves ?? 10); waveIndex++) { if (abortController.signal.aborted) throw new Error("Workflow aborted"); - - let wave: WorkflowWave | undefined; - if (effectiveConfig.waveSource.type === "static") { - wave = effectiveConfig.waveSource.staticWaves?.[waveIndex]; - if (!wave) { - break; - } - } else { - // Retry PM call if it returns invalid output - const maxPmRetries = effectiveConfig.maxPmRetries ?? 3; - let pmResult: Awaited> | null = null; - let pmAttempts = 0; - let lastError: string | undefined; - while (pmAttempts < maxPmRetries) { - try { - pmResult = await generateWaveFromPm( - pi, - effectiveConfig, - agents, - ctx, - abortController.signal, - previousSummary, - lastError, - ); - if (pmResult.done) { - break; - } - if (pmResult.wave) { - wave = pmResult.wave; - break; - } - // PM returned clarification - don't retry, just accept it - break; - } catch (error: any) { - // PM returned invalid wave - retry with error message - lastError = error.message; - pmAttempts++; - if (pmAttempts >= maxPmRetries) throw error; - } - } - if (pmResult?.done) { - break; - } - wave = pmResult?.wave ?? wave; + const resolved = await resolveWaveForIndex( + pi, + ctx, + effectiveConfig, + agents, + abortController.signal, + previousSummary, + waveIndex, + ); + if (resolved.done) break; + if (resolved.clarification) { + await pauseForClarification(pi, ctx, abortController.signal, waveIndex, previousSummary); + waveIndex -= 1; + continue; } - - if (!wave) break; - + if (!resolved.wave || !resolved.tasks) continue; + const { wave } = resolved; const updatedState: WorkflowState = { ...currentState!, waveIndex, @@ -1013,6 +1103,8 @@ async function startWorkflow( const finalState: WorkflowState = { ...currentState!, active: false, + waitingForClarification: false, + clarificationToken: undefined, updatedAt: Date.now(), }; setState(pi, ctx, finalState); @@ -1023,7 +1115,13 @@ async function startWorkflow( sendWorkflowNotice(pi, `Workflow error: ${message}`); if (ctx.hasUI) ctx.ui.notify(message, "error"); if (currentState) { - setState(pi, ctx, { ...currentState, active: false, updatedAt: Date.now() }); + setState(pi, ctx, { + ...currentState, + active: false, + waitingForClarification: false, + clarificationToken: undefined, + updatedAt: Date.now(), + }); } } finally { disposePmRunner(); @@ -1034,11 +1132,12 @@ async function startWorkflow( currentRun = { abortController, promise: runPromise }; } -function stopWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): void { +async function stopWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { if (!currentRun) { if (ctx.hasUI) ctx.ui.notify("No active workflow", "warning"); return; } + const runPromise = currentRun.promise; currentRun.abortController.abort(); currentRun = null; for (const runner of taskRunners.values()) { @@ -1049,23 +1148,34 @@ function stopWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): void { disposePmRunner(); if (currentState) { currentState.active = false; + currentState.waitingForClarification = false; + currentState.clarificationToken = undefined; setState(pi, ctx, currentState); } + resetTransientWorkflowState(); sendWorkflowNotice(pi, "Workflow stopped."); if (ctx.hasUI) ctx.ui.notify("Workflow stopped", "info"); + await runPromise.catch(() => {}); } export default function (pi: ExtensionAPI) { pi.on("session_start", (_event, ctx) => { + loadProjectEnv(ctx.cwd); currentState = restoreState(ctx); if (currentState?.active) { - setState(pi, ctx, { ...currentState, active: false }); + setState(pi, ctx, { + ...currentState, + active: false, + waitingForClarification: false, + clarificationToken: undefined, + }); } else if (currentState) { updateStatus(ctx, currentState); } setPmStatus(ctx, undefined); taskRunners.clear(); disposePmRunner(); + resetTransientWorkflowState(); startStatusTicker(ctx); }); @@ -1083,6 +1193,8 @@ export default function (pi: ExtensionAPI) { disposePmRunner(); if (currentState) { currentState.active = false; + currentState.waitingForClarification = false; + currentState.clarificationToken = undefined; currentState.tasks = (currentState.tasks ?? []).map((task) => { if (task.status === "in_progress") { return { ...task, status: "stopped", lastNote: "stopped" }; @@ -1091,6 +1203,7 @@ export default function (pi: ExtensionAPI) { }); setState(pi, ctx, currentState); } + resetTransientWorkflowState(); }); pi.on("input", async (event, ctx) => { @@ -1121,7 +1234,11 @@ export default function (pi: ExtensionAPI) { // Clear the clarification flag - user has responded if (currentState.waitingForClarification) { - setState(pi, ctx, { ...currentState, waitingForClarification: false }); + setState(pi, ctx, { + ...currentState, + waitingForClarification: false, + clarificationToken: undefined, + }); } return { action: "handled" }; @@ -1187,7 +1304,7 @@ export default function (pi: ExtensionAPI) { } if (command === "stop") { - stopWorkflow(pi, ctx); + await stopWorkflow(pi, ctx); return; } diff --git a/.pi/extensions/workflow-orchestrator/runner.ts b/.pi/extensions/workflow-orchestrator/runner.ts index c401e8a..ab6d831 100644 --- a/.pi/extensions/workflow-orchestrator/runner.ts +++ b/.pi/extensions/workflow-orchestrator/runner.ts @@ -3,6 +3,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import type { Message } from "@mariozechner/pi-ai"; +import { getSubagentEnv } from "./env.js"; export type AgentRunUpdate = | { type: "text_delta"; delta: string } @@ -114,10 +115,12 @@ export async function runAgent(input: AgentRunInput): Promise { const messages: Message[] = []; const toolCalls: ToolCallCapture[] = []; let stderr = ""; + let assistantError = ""; const exitCode = await new Promise((resolve) => { const proc = spawn("pi", args, { cwd: input.cwd, + env: getSubagentEnv(input.cwd), shell: false, stdio: ["ignore", "pipe", "pipe"], }); @@ -158,6 +161,9 @@ export async function runAgent(input: AgentRunInput): Promise { if (event.type === "message_end" && event.message) { const msg = event.message as Message; messages.push(msg); + if (event.message.stopReason === "error" && event.message.errorMessage) { + assistantError = event.message.errorMessage; + } } if (event.type === "tool_result_end" && event.message) { const msg = event.message as Message; @@ -210,7 +216,7 @@ export async function runAgent(input: AgentRunInput): Promise { /* ignore */ } - return { outputText, messages, stderr, exitCode, toolCalls }; + return { outputText, messages, stderr: assistantError || stderr, exitCode, toolCalls }; } export class RpcAgent { @@ -263,6 +269,7 @@ export class RpcAgent { this.proc = spawn("pi", args, { cwd: this.options.cwd, + env: getSubagentEnv(this.options.cwd), shell: false, stdio: ["pipe", "pipe", "pipe"], }); @@ -384,6 +391,13 @@ export class RpcAgent { const run = this.currentRun; if (!run) return; // Race condition: agent_end may have cleared currentRun + if (event.message.stopReason === "error" && event.message.errorMessage) { + this.lastToolCalls = [...run.toolCalls]; + this.currentRun = null; + run.reject(new Error(event.message.errorMessage)); + return; + } + const msg = event.message as Message; for (const part of msg.content) { if (typeof part === "string") { diff --git a/.pi/extensions/workflow-orchestrator/state.ts b/.pi/extensions/workflow-orchestrator/state.ts index 0d3bc80..d9dc3d6 100644 --- a/.pi/extensions/workflow-orchestrator/state.ts +++ b/.pi/extensions/workflow-orchestrator/state.ts @@ -36,6 +36,7 @@ export interface WorkflowState { previousSummary?: string; waveSummaries?: string[]; waitingForClarification?: boolean; + clarificationToken?: string; } export const STATE_TYPE = "workflow-state"; diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..0c3983f --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,3 @@ +[test] +root = "./tests" +coverage = false diff --git a/package-lock.json b/package-lock.json index 2dbd565..8b02d5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,22 +7,26 @@ "": { "name": "pi-workflow-orchestrator", "version": "0.1.0", + "dependencies": { + "dotenv": "^16.6.1" + }, "devDependencies": { "@mariozechner/pi-ai": "^0.57.1", "@mariozechner/pi-coding-agent": "^0.57.1", "@mariozechner/pi-tui": "^0.57.1", - "@sinclair/typebox": "^0.34.48", - "@types/node": "^25.5.0", - "@typescript-eslint/eslint-plugin": "^8.38.0", - "@typescript-eslint/parser": "^8.38.0", + "@sinclair/typebox": "^0.34.49", + "@types/bun": "^1.3.14", + "@types/node": "^25.9.1", + "@typescript-eslint/eslint-plugin": "^8.59.4", + "@typescript-eslint/parser": "^8.59.4", "@vitest/coverage-v8": "^2.1.9", - "eslint": "^9.9.0", - "eslint-config-prettier": "^9.1.0", - "globals": "^17.4.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "globals": "^17.6.0", "markdownlint-cli": "^0.42.0", - "prettier": "^3.3.3", - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "prettier": "^3.8.3", + "typescript": "^5.9.3", + "vitest": "^2.1.9" } }, "node_modules/@ampproject/remapping": { @@ -788,14 +792,15 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", - "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz", + "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.4.1", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.2", + "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" }, "engines": { @@ -1333,9 +1338,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1414,9 +1419,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2049,6 +2054,19 @@ "zod-to-json-schema": "^3.24.1" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2075,9 +2093,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "dev": true, "license": "BSD-3-Clause" }, @@ -2089,14 +2107,13 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -2107,9 +2124,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "dev": true, "license": "BSD-3-Clause" }, @@ -2128,9 +2145,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "dev": true, "license": "BSD-3-Clause" }, @@ -2492,9 +2509,9 @@ "license": "Apache-2.0" }, "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "dev": true, "license": "MIT" }, @@ -2947,9 +2964,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3226,6 +3243,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bun": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz", + "integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.14" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3248,13 +3275,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/retry": { @@ -3276,20 +3303,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3299,22 +3326,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "engines": { @@ -3326,18 +3353,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "engines": { @@ -3348,18 +3375,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3370,9 +3397,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", "dev": true, "license": "MIT", "engines": { @@ -3383,21 +3410,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3408,13 +3435,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", "dev": true, "license": "MIT", "engines": { @@ -3426,21 +3453,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3450,20 +3477,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3474,17 +3501,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3844,9 +3871,9 @@ "license": "MIT" }, "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "dev": true, "license": "MIT", "engines": { @@ -3871,9 +3898,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3900,6 +3927,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/bun-types": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4153,6 +4190,18 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4402,9 +4451,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4609,9 +4658,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -4626,9 +4675,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz", - "integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "dev": true, "funding": [ { @@ -4638,13 +4687,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "dev": true, "funding": [ { @@ -4654,8 +4704,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -4727,9 +4779,9 @@ } }, "node_modules/file-type": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.1.tgz", - "integrity": "sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "dev": true, "license": "MIT", "dependencies": { @@ -4777,9 +4829,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4975,9 +5027,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -5159,9 +5211,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -5710,9 +5762,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -5987,9 +6039,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "dev": true, "funding": [ { @@ -6061,9 +6113,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6074,9 +6126,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -6094,7 +6146,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6113,9 +6165,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -6158,25 +6210,25 @@ "license": "ISC" }, "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -6345,9 +6397,9 @@ "license": "MIT" }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -6833,9 +6885,9 @@ } }, "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "dev": true, "funding": [ { @@ -6929,9 +6981,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -7097,14 +7149,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -7170,9 +7222,9 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -7237,9 +7289,9 @@ } }, "node_modules/undici": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.23.0.tgz", - "integrity": "sha512-HVMxHKZKi+eL2mrUZDzDkKW3XvCjynhbtpSq20xQp4ePDFeSFuAfnvM0GIwZIv8fiKHjXFQ5WjxhCt15KRNj+g==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -7247,9 +7299,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, @@ -7556,9 +7608,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "license": "MIT", "engines": { @@ -7577,6 +7629,22 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7588,9 +7656,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index bb9029a..f6c184d 100644 --- a/package.json +++ b/package.json @@ -30,21 +30,25 @@ "markdownlint": "markdownlint \"**/*.md\" --ignore node_modules", "test": "vitest run" }, + "dependencies": { + "dotenv": "^16.6.1" + }, "devDependencies": { "@mariozechner/pi-ai": "^0.57.1", "@mariozechner/pi-coding-agent": "^0.57.1", "@mariozechner/pi-tui": "^0.57.1", - "@sinclair/typebox": "^0.34.48", - "@types/node": "^25.5.0", - "@typescript-eslint/eslint-plugin": "^8.38.0", - "@typescript-eslint/parser": "^8.38.0", + "@sinclair/typebox": "^0.34.49", + "@types/bun": "^1.3.14", + "@types/node": "^25.9.1", + "@typescript-eslint/eslint-plugin": "^8.59.4", + "@typescript-eslint/parser": "^8.59.4", "@vitest/coverage-v8": "^2.1.9", - "eslint": "^9.9.0", - "eslint-config-prettier": "^9.1.0", - "globals": "^17.4.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "globals": "^17.6.0", "markdownlint-cli": "^0.42.0", - "prettier": "^3.3.3", - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "prettier": "^3.8.3", + "typescript": "^5.9.3", + "vitest": "^2.1.9" } } diff --git a/tests/engine.test.ts b/tests/engine.test.ts index dcc104b..d7b2066 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -39,13 +39,9 @@ function baseOptions(task: TestTask) { const issues = result?.output?.issues ?? (error ? [error] : []); t.verifyOutput = { status: "fail", issues }; t.issues = issues; - t.retries += 1; - return t.retries <= 2; }, applyGenericFailure: (t: TestTask, error: string) => { t.issues = [error]; - t.retries += 1; - return t.retries <= 2; }, markVerified: (t: TestTask, stageId: string) => { t.status = "verified"; @@ -105,6 +101,35 @@ describe("runTaskFlow", () => { expect(task.status).toBe("failed"); }); + it("enforces maxRetries even if retry callbacks allow more", async () => { + const task: TestTask = { id: "T1", status: "pending", retries: 0 }; + let developCount = 0; + let verifyCount = 0; + + await runTaskFlow({ + ...baseOptions(task), + maxRetries: 1, + applyVerifyFailure: (t: TestTask, _stageId: string, result: any, error?: string) => { + const issues = result?.output?.issues ?? (error ? [error] : []); + t.verifyOutput = { status: "fail", issues }; + t.issues = issues; + }, + runStage: async (stage) => { + if (stage.id === "develop") { + developCount += 1; + return { output: { status: "done" }, outputText: "dev" }; + } + verifyCount += 1; + return { output: { status: "fail", issues: ["still broken"] }, outputText: "verify" }; + }, + }); + + expect(developCount).toBe(2); + expect(verifyCount).toBe(2); + expect(task.retries).toBe(2); + expect(task.status).toBe("failed"); + }); + it("returns early when stopped", async () => { const task: TestTask = { id: "T1", status: "stopped", retries: 0 }; let calls = 0; @@ -163,9 +188,10 @@ describe("runTaskFlow", () => { await runTaskFlow({ ...baseOptions(task), + maxRetries: 3, applyVerifyFailure: (t, stageId, result, error, reason) => { reasons.push(String(reason)); - return baseOptions(t).applyVerifyFailure(t, stageId, result, error); + baseOptions(t).applyVerifyFailure(t, stageId, result, error); }, runStage: async (stage) => { if (stage.id === "develop") return { output: { status: "done" }, outputText: "dev" }; diff --git a/tests/env.test.ts b/tests/env.test.ts new file mode 100644 index 0000000..8c16d90 --- /dev/null +++ b/tests/env.test.ts @@ -0,0 +1,54 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + findProjectRoot, + getSubagentEnv, + loadProjectEnv, +} from "../.pi/extensions/workflow-orchestrator/env.js"; + +describe("workflow env", () => { + const tempDirs: string[] = []; + + afterEach(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } + tempDirs.length = 0; + delete process.env.PIORCH_ENV_TEST; + }); + + it("finds project root via .pi/agents", () => { + const root = path.join(os.tmpdir(), `piorch-env-${Date.now()}`); + fs.mkdirSync(path.join(root, ".pi", "agents"), { recursive: true }); + tempDirs.push(root); + + const nested = path.join(root, "src", "deep"); + fs.mkdirSync(nested, { recursive: true }); + + expect(findProjectRoot(nested)).toBe(root); + }); + + it("loads .env from project root into subagent env", () => { + const root = path.join(os.tmpdir(), `piorch-env-${Date.now()}`); + fs.mkdirSync(path.join(root, ".pi", "agents"), { recursive: true }); + fs.writeFileSync(path.join(root, ".env"), "PIORCH_ENV_TEST=from-dotenv\n"); + tempDirs.push(root); + + delete process.env.PIORCH_ENV_TEST; + const env = getSubagentEnv(root); + expect(env.PIORCH_ENV_TEST).toBe("from-dotenv"); + }); + + it("does not override existing process.env values", () => { + const root = path.join(os.tmpdir(), `piorch-env-${Date.now()}`); + fs.mkdirSync(path.join(root, ".pi", "agents"), { recursive: true }); + fs.writeFileSync(path.join(root, ".env"), "PIORCH_ENV_TEST=from-dotenv\n"); + tempDirs.push(root); + + process.env.PIORCH_ENV_TEST = "from-shell"; + loadProjectEnv(root); + expect(process.env.PIORCH_ENV_TEST).toBe("from-shell"); + }); +}); diff --git a/tests/render.test.ts b/tests/render.test.ts index 60cc904..918a0a2 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -7,11 +7,19 @@ import { setTaskListExpanded, } from "../.pi/extensions/workflow-orchestrator/render.js"; +// Portable frozen clock: works with both vitest and bun test runners. +const FROZEN_NOW = new Date("2026-03-15T12:00:00.000Z").getTime(); +let originalDateNow: () => number; + describe("render.ts", () => { let mockCtx: ExtensionContext; let mockUi: any; beforeEach(() => { + // Freeze Date.now() without relying on vitest fake timers + originalDateNow = Date.now; + Date.now = () => FROZEN_NOW; + mockUi = { setStatus: vi.fn(), setWidget: vi.fn(), @@ -30,13 +38,10 @@ describe("render.ts", () => { appendEntry: vi.fn(), }, } as unknown as ExtensionContext; - - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-03-15T12:00:00.000Z")); }); afterEach(() => { - vi.useRealTimers(); + Date.now = originalDateNow; vi.clearAllMocks(); }); diff --git a/tests/workflow-clarification.test.ts b/tests/workflow-clarification.test.ts new file mode 100644 index 0000000..5fddf11 --- /dev/null +++ b/tests/workflow-clarification.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { + signalClarificationResolved, + waitForClarification, +} from "../.pi/extensions/workflow-orchestrator/index.js"; + +describe("waitForClarification", () => { + it("resolves when the clarification token is cleared", async () => { + const controller = new AbortController(); + const token = "clarification-token"; + const promise = waitForClarification(controller.signal, token); + let settled = false; + promise.then(() => { + settled = true; + }); + + expect(settled).toBe(false); + + signalClarificationResolved(token); + await promise; + + expect(settled).toBe(true); + }); +}); diff --git a/tests/workflow-start-clarification.test.ts b/tests/workflow-start-clarification.test.ts new file mode 100644 index 0000000..fa3d384 --- /dev/null +++ b/tests/workflow-start-clarification.test.ts @@ -0,0 +1,259 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import type { + ExtensionAPI, + ExtensionCommandContext, + ExtensionContext, +} from "@mariozechner/pi-coding-agent"; + +type RegisteredHandlers = { + commands: Record Promise | void>; + events: Record Promise | any>; +}; + +type FakeRpcOptions = { + systemPrompt?: string; +}; + +const rpcInstances: FakeRpcAgent[] = []; + +class FakeRpcAgent { + private toolCalls: Array<{ name: string; arguments: Record }> = []; + private pmWaveCalls = 0; + readonly role: "pm" | "developer" | "verifier" | "unknown"; + + constructor(readonly options: FakeRpcOptions) { + const prompt = options.systemPrompt ?? ""; + if (prompt.includes("technical PM")) this.role = "pm"; + else if (prompt.includes("You are a developer")) this.role = "developer"; + else if (prompt.includes("You are a verifier")) this.role = "verifier"; + else this.role = "unknown"; + rpcInstances.push(this); + } + + isRunning(): boolean { + return false; + } + + abort(): void {} + + dispose(): void {} + + sendSteer(): void {} + + getLastToolCalls(): Array<{ name: string; arguments: Record }> { + return this.toolCalls; + } + + async runPrompt(message: string): Promise { + this.toolCalls = []; + + if (this.role === "pm") { + if (message.includes("User message:")) { + return "Thanks, I have the clarification."; + } + + this.pmWaveCalls += 1; + if (this.pmWaveCalls === 1) { + return "I need a little clarification before I can generate the wave."; + } + + if (this.pmWaveCalls === 2) { + this.toolCalls = [ + { + name: "generate_wave", + arguments: { + done: false, + wave: { + goal: "Wave 1", + tasks: [ + { + id: "T1", + title: "Implement the feature", + description: "Build the requested feature.", + requirements: "The feature works end to end.", + assignee: "developer", + }, + ], + }, + }, + }, + ]; + return ""; + } + + this.toolCalls = [ + { + name: "generate_wave", + arguments: { done: true }, + }, + ]; + return ""; + } + + if (this.role === "developer") { + this.toolCalls = [ + { + name: "report_task_result", + arguments: { + status: "done", + summary: "Implemented the feature.", + filesChanged: ["src/feature.ts"], + notes: "Looks good.", + }, + }, + ]; + return ""; + } + + if (this.role === "verifier") { + this.toolCalls = [ + { + name: "report_task_result", + arguments: { + status: "pass", + issues: [], + }, + }, + ]; + return ""; + } + + return ""; + } +} + +vi.mock("../.pi/extensions/workflow-orchestrator/runner.js", () => { + return { RpcAgent: FakeRpcAgent }; +}); + +const { default: registerWorkflowExtension } = + await import("../.pi/extensions/workflow-orchestrator/index.js"); + +function createMockContext(branch: any[] = []): ExtensionCommandContext { + return { + cwd: process.cwd(), + hasUI: false, + sessionManager: { + getBranch: vi.fn().mockImplementation(() => branch), + }, + } as unknown as ExtensionCommandContext; +} + +function createMockPi(branch: any[] = []): ExtensionAPI & RegisteredHandlers { + const handlers: RegisteredHandlers = { + commands: {}, + events: {}, + }; + + const pi = { + cwd: process.cwd(), + hasUI: false, + sendMessage: vi.fn(), + sendUserMessage: vi.fn(), + appendEntry: vi.fn((type: string, state: any) => { + branch.push({ + type: "custom", + customType: type, + data: cloneJson(state), + }); + }), + registerCommand: vi.fn( + (name: string, config: { handler: RegisteredHandlers["commands"][string] }) => { + handlers.commands[name] = config.handler; + }, + ), + registerTool: vi.fn(), + registerMessageRenderer: vi.fn(), + on: vi.fn((event: string, handler: RegisteredHandlers["events"][string]) => { + handlers.events[event] = handler; + }), + } as unknown as ExtensionAPI & RegisteredHandlers; + + return Object.assign(pi, handlers); +} + +function cloneJson(value: T): T { + return JSON.parse(JSON.stringify(value)); +} + +async function waitFor(predicate: () => boolean, timeoutMs = 4000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (predicate()) return; + await new Promise((resolve) => setTimeout(resolve, 25)); + } + throw new Error("Timed out waiting for workflow state"); +} + +describe("workflow clarification regression", () => { + beforeEach(() => { + rpcInstances.length = 0; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("pauses on clarification during start and resumes after user input", async () => { + const pi = createMockPi(); + const ctx = createMockContext(); + + registerWorkflowExtension(pi); + pi.events.session_start?.({}, ctx); + + const workflowCommand = pi.commands.workflow; + const inputHandler = pi.events.input; + expect(workflowCommand).toBeTypeOf("function"); + expect(inputHandler).toBeTypeOf("function"); + + await workflowCommand('start default "Regression goal"', ctx); + + await waitFor(() => + (pi.appendEntry as any).mock.calls.some( + ([type, state]: [string, any]) => + type === "workflow-state" && state?.waitingForClarification === true, + ), + ); + + const waitingStates: any[] = (pi.appendEntry as any).mock.calls + .filter(([type]: [string, any]) => type === "workflow-state") + .map(([, state]: [string, any]) => cloneJson(state)); + const firstWaitingState = waitingStates.find((state: any) => state.waitingForClarification); + expect(firstWaitingState).toMatchObject({ + active: true, + waitingForClarification: true, + waveIndex: 0, + tasks: [], + }); + expect(firstWaitingState?.wave).toBeUndefined(); + + await inputHandler({ source: "user", text: "Please proceed with the planned feature." }, ctx); + + await waitFor(() => + (pi.appendEntry as any).mock.calls.some( + ([type, state]: [string, any]) => + type === "workflow-state" && + Array.isArray(state?.tasks) && + state.tasks.some((task: any) => task.status === "verified"), + ), + ); + + const states: any[] = (pi.appendEntry as any).mock.calls + .filter(([type]: [string, any]) => type === "workflow-state") + .map(([, state]: [string, any]) => cloneJson(state)); + + const clarificationIndex = states.findIndex( + (state: any) => state.waitingForClarification === true, + ); + expect(clarificationIndex).toBeGreaterThanOrEqual(0); + const resumedWaveState = states + .slice(clarificationIndex + 1) + .find( + (state: any) => + state.wave?.goal === "Wave 1" && state.tasks?.some((task: any) => task.id === "T1"), + ); + expect(resumedWaveState).toBeDefined(); + expect(resumedWaveState?.waveIndex).toBe(0); + expect(states.some((state) => state.active === false)).toBe(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index c217778..e8184e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "types": ["node"] + "types": ["node", "bun-types"] }, "include": [".pi/extensions/**/*.ts", "tests/**/*.ts"] }