import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, } from '@earendil-works/pi-coding-agent'; import type { KeyId } from '@earendil-works/pi-tui'; import { registerHarnessCommands, type WorkflowCommandController, } from './commands.ts'; import { DEFAULT_STATUS_SHORTCUT, type LoadedWorkflow, type WorkflowCatalog, type WorkflowStep, } from '../../domain/index.ts'; import type { WorkflowRun } from '../../domain/index.ts'; import type { AgentDelegationClientController } from '../process/agent-client.ts'; import type { AgentDelegationResponse, AgentDelegationUpdate, WorkflowStepResult, } from '../../domain/index.ts'; import type { MainStepRuntimeController } from '../runtime/main-step-runtime.ts'; import type { SerialTaskQueueController } from '../runtime/task-queue.ts'; import { createEmptyCatalog } from './catalog.ts'; import { createWorkflowHarnessDependencies, type WorkflowHarnessDependencies, } from './dependencies.ts'; import type { ActiveDelegation, MainStepIdentity, WorkflowStartContext, } from './types.ts'; import { createStatusActions } from './status-actions.ts'; import { createStartActions } from './start-actions.ts'; import { createPauseActions } from './pause-actions.ts'; import { createResumeAction } from './resume-action.ts'; import { createLifecycleActions } from './lifecycle-actions.ts'; import { createStepExecutionActions } from './step-execution-actions.ts'; import { createDelegationResponseActions } from './delegation-response-actions.ts'; import { createDelegationControlActions } from './delegation-control-actions.ts'; import { createGateSubmissionAction } from './gate-submission-action.ts'; import { createPlannotatorResultActions } from './plannotator-result-actions.ts'; import { createCoreActions } from './core-actions.ts'; import type { SettledStepReport } from './step-reporting.ts'; import { formatShortcutLabel, type WorkflowStatusSnapshot, } from '../../ui/index.ts'; const STATUS_ACTIONS = createStatusActions(); const START_ACTIONS = createStartActions(); const PAUSE_ACTIONS = createPauseActions(); const RESUME_ACTION = createResumeAction(); const LIFECYCLE_ACTIONS = createLifecycleActions(); const STEP_EXECUTION_ACTIONS = createStepExecutionActions(); const DELEGATION_RESPONSE_ACTIONS = createDelegationResponseActions(); const DELEGATION_CONTROL_ACTIONS = createDelegationControlActions(); const GATE_SUBMISSION_ACTION = createGateSubmissionAction(); const PLANNOTATOR_RESULT_ACTIONS = createPlannotatorResultActions(); const CORE_ACTIONS = createCoreActions(); /** * Coordinates workflow commands while delegating all external effects through * injectable dependencies. */ export class WorkflowHarness implements WorkflowCommandController { private readonly pi: ExtensionAPI; private readonly dependencies: WorkflowHarnessDependencies; private readonly agents: AgentDelegationClientController; private readonly mainSteps: MainStepRuntimeController; private catalog: WorkflowCatalog = createEmptyCatalog(); private run: WorkflowRun | undefined; private latestContext: ExtensionContext | undefined; private availableSkills = new Set(); private isSessionActive = false; private sessionEpoch = 0; private activeDelegation: ActiveDelegation | undefined; private registeredWorkflowCommands = new Set(); private catalogLoadSequence = 0; private readonly mutationQueue: SerialTaskQueueController; private readonly statusShortcut: KeyId; private readonly statusShortcutLabel: string; private statusRefreshTimer: ReturnType | undefined; private isStatusOverlayOpen = false; private legacyProgressWidgetContext: ExtensionContext | undefined; private readonly workflowStatusSnapshot: () => WorkflowStatusSnapshot | undefined = STATUS_ACTIONS.workflowStatusSnapshot; private readonly updateStatus: () => void = STATUS_ACTIONS.updateStatus; private readonly refreshStatusWhileRunning: () => void = STATUS_ACTIONS.refreshStatusWhileRunning; private readonly stopStatusRefresh: () => void = STATUS_ACTIONS.stopStatusRefresh; private readonly registerWorkflowStatusShortcut: () => void = STATUS_ACTIONS.registerWorkflowStatusShortcut; private readonly showWorkflowStatus: ( context: ExtensionContext, ) => Promise = STATUS_ACTIONS.showWorkflowStatus; private readonly listWorkflows: ( context: ExtensionCommandContext, ) => Promise = START_ACTIONS.listWorkflows; private readonly doctorWorkflows: ( workflowId: string, context: ExtensionCommandContext, ) => Promise = START_ACTIONS.doctorWorkflows; private readonly startNow: ( workflowId: string, input: string, startContext: WorkflowStartContext, sessionEpoch: number, ) => Promise = START_ACTIONS.startNow; private readonly restartNow: ( input: string, startContext: WorkflowStartContext, sessionEpoch: number, ) => Promise = START_ACTIONS.restartNow; private readonly reloadNow: ( context: ExtensionCommandContext, ) => Promise = START_ACTIONS.reloadNow; private readonly pauseNow: ( reason: string, context: ExtensionCommandContext, ) => Promise = PAUSE_ACTIONS.pauseNow; private readonly abortNow: ( reason: string, context: ExtensionCommandContext, ) => Promise = PAUSE_ACTIONS.abortNow; private readonly resumeNow: ( context: ExtensionCommandContext, input?: string, ) => Promise = RESUME_ACTION.resumeNow; private readonly registerMultilineCommandInput: () => void = LIFECYCLE_ACTIONS.registerMultilineCommandInput; private readonly registerLifecycle: () => void = LIFECYCLE_ACTIONS.registerLifecycle; private readonly registerPolicy: () => void = LIFECYCLE_ACTIONS.registerPolicy; private readonly launchCurrentStep: (workflow: LoadedWorkflow) => void = STEP_EXECUTION_ACTIONS.launchCurrentStep; private readonly launchMainStep: ( workflow: LoadedWorkflow, run: WorkflowRun, step: WorkflowStep, ) => void = STEP_EXECUTION_ACTIONS.launchMainStep; private readonly queueMainStepLog: ( identity: MainStepIdentity, lines: ReadonlyArray, context: ExtensionContext, ) => Promise = STEP_EXECUTION_ACTIONS.queueMainStepLog; private readonly recordMainStepLog: ( identity: MainStepIdentity, lines: ReadonlyArray, context: ExtensionContext, ) => Promise = STEP_EXECUTION_ACTIONS.recordMainStepLog; private readonly queueMainStepResult: ( identity: MainStepIdentity, result: WorkflowStepResult | undefined, context: ExtensionContext, ) => Promise = STEP_EXECUTION_ACTIONS.queueMainStepResult; private readonly finishMainStep: ( identity: MainStepIdentity, result: WorkflowStepResult | undefined, context: ExtensionContext, ) => Promise = STEP_EXECUTION_ACTIONS.finishMainStep; private readonly handleDelegationUpdate: ( active: ActiveDelegation, update: AgentDelegationUpdate, ) => void = DELEGATION_RESPONSE_ACTIONS.handleDelegationUpdate; private readonly queueDelegationResponse: ( active: ActiveDelegation, response: AgentDelegationResponse, ) => void = DELEGATION_RESPONSE_ACTIONS.queueDelegationResponse; private readonly queueDelegationFailure: ( active: ActiveDelegation, reason: string, ) => void = DELEGATION_RESPONSE_ACTIONS.queueDelegationFailure; private readonly finishDelegation: ( active: ActiveDelegation, response: AgentDelegationResponse, ) => Promise = DELEGATION_RESPONSE_ACTIONS.finishDelegation; private readonly cancelActiveDelegation: ( reason: string, ) => Promise = DELEGATION_CONTROL_ACTIONS.cancelActiveDelegation; private readonly cleanupDelegation: ( active: ActiveDelegation, ) => Promise = DELEGATION_CONTROL_ACTIONS.cleanupDelegation; private readonly pauseForDelegationFailure: ( reason: string, failureSummary?: string, ) => void = DELEGATION_CONTROL_ACTIONS.pauseForDelegationFailure; private readonly pauseForExecutionFailure: ( label: string, reason: string, failureSummary?: string, ) => void = DELEGATION_CONTROL_ACTIONS.pauseForExecutionFailure; private readonly retainUnconfirmedDelegation: ( active: ActiveDelegation, reason: string, ) => void = DELEGATION_CONTROL_ACTIONS.retainUnconfirmedDelegation; private readonly releaseMainAfterCancellation: ( active: ActiveDelegation, ) => void = DELEGATION_CONTROL_ACTIONS.releaseMainAfterCancellation; private readonly submitGate: ( workflow: LoadedWorkflow, originalRun: WorkflowRun, outcome: string, summary: string, artifact: string, ) => Promise = GATE_SUBMISSION_ACTION.submitGate; private readonly registerPlannotatorResults: () => void = PLANNOTATOR_RESULT_ACTIONS.registerPlannotatorResults; private readonly handlePlannotatorResult: (data: unknown) => Promise = PLANNOTATOR_RESULT_ACTIONS.handlePlannotatorResult; private readonly settleAfterTransition: ( workflow: LoadedWorkflow, report: SettledStepReport, ) => void = CORE_ACTIONS.settleAfterTransition; private readonly preflight: ( workflow: LoadedWorkflow, stepId: string, ) => Array = CORE_ACTIONS.preflight; private readonly isolateMainSessionTools: () => void = CORE_ACTIONS.isolateMainSessionTools; private readonly restoreBaselineTools: () => void = CORE_ACTIONS.restoreBaselineTools; private readonly captureSkills: ( skills: ReadonlyArray<{ name: string }> | undefined, ) => void = CORE_ACTIONS.captureSkills; private readonly enqueueMutation: ( context: ExtensionContext, operation: (sessionEpoch: number) => Promise, ) => Promise = CORE_ACTIONS.enqueueMutation; private readonly persist: () => void = CORE_ACTIONS.persist; private readonly restoreFromSession: (context: ExtensionContext) => void = CORE_ACTIONS.restoreFromSession; private readonly reloadCatalog: ( context: ExtensionContext, shouldAnnounce: boolean, ) => Promise = CORE_ACTIONS.reloadCatalog; /** * Creates a workflow harness and registers its Pi integration surface. * * @param pi - Pi extension API used by the runtime adapters. * @param statusShortcut - Shortcut used to open workflow status. * @param dependencyOverrides - Optional effect implementations for tests or * alternate runtimes. */ constructor( pi: ExtensionAPI, statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT, dependencyOverrides: Partial = {}, ) { this.pi = pi; this.dependencies = createWorkflowHarnessDependencies(dependencyOverrides); this.statusShortcut = statusShortcut; this.statusShortcutLabel = formatShortcutLabel(statusShortcut); this.agents = this.dependencies.createAgentClient(pi); this.mainSteps = this.dependencies.createMainStepRuntime(pi); this.mutationQueue = this.dependencies.createMutationQueue(); registerHarnessCommands(pi, this); this.registerWorkflowStatusShortcut(); this.registerMultilineCommandInput(); this.registerLifecycle(); this.registerPolicy(); this.registerPlannotatorResults(); } /** Returns the loaded workflow identifiers in stable display order. */ workflowIds(): Array { return [...this.catalog.workflows.keys()].sort(); } /** Displays the currently loaded workflows. */ async list(context: ExtensionCommandContext): Promise { await this.listWorkflows(context); } /** Diagnoses declarative completion paths and loop risks. */ async doctor( workflowId: string, context: ExtensionCommandContext, ): Promise { await this.doctorWorkflows(workflowId, context); } /** Starts a loaded workflow with the supplied user input. */ start( workflowId: string, input: string, context: ExtensionCommandContext, ): Promise { return this.enqueueMutation(context, (sessionEpoch) => this.startNow( workflowId, input, { context, skills: () => context.getSystemPromptOptions().skills, waitForIdle: () => context.waitForIdle(), }, sessionEpoch, ), ); } /** Starts another completed iteration in its existing workflow worktree. */ restart(input: string, context: ExtensionCommandContext): Promise { return this.enqueueMutation(context, (sessionEpoch) => this.restartNow( input, { context, skills: () => context.getSystemPromptOptions().skills, waitForIdle: () => context.waitForIdle(), }, sessionEpoch, ), ); } /** Pauses the active workflow while retaining its checkpoint. */ pause(reason: string, context: ExtensionCommandContext): Promise { return this.enqueueMutation(context, () => this.pauseNow(reason, context)); } /** Resumes the active paused workflow with optional user guidance. */ resume(input: string, context: ExtensionCommandContext): Promise { return this.enqueueMutation(context, () => this.resumeNow(context, input)); } /** Aborts the active workflow and cancels any delegated execution. */ abort(reason: string, context: ExtensionCommandContext): Promise { return this.enqueueMutation(context, () => this.abortNow(reason, context)); } /** Opens the workflow status overlay, matching the configured shortcut. */ async status(context: ExtensionCommandContext): Promise { this.latestContext = context; if (this.isStatusOverlayOpen) return; if (!this.run) { context.ui.notify('No workflow checkpoint in this session', 'info'); return; } await this.showWorkflowStatus(context); } /** Reloads workflow configuration while no workflow is executing. */ reload(context: ExtensionCommandContext): Promise { return this.enqueueMutation(context, () => this.reloadNow(context)); } } /** * Creates and registers a functional workflow harness against the Pi extension API. * * @param pi - Pi extension API used for command, tool, and session registration. * @param statusShortcut - Keyboard shortcut that toggles the status overlay. * @param dependencyOverrides - Optional mock dependencies for testing. * @returns A workflow command controller. */ export function createWorkflowHarness( pi: ExtensionAPI, statusShortcut: KeyId = DEFAULT_STATUS_SHORTCUT, dependencyOverrides: Partial = {}, ): WorkflowCommandController { return new WorkflowHarness(pi, statusShortcut, dependencyOverrides); }