@agimon-ai/doompi-workflow 0.0.1-alpha.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/dispatcher.cjs +2 -0
- package/dist/dispatcher.cjs.map +1 -0
- package/dist/dispatcher.d.cts +6 -0
- package/dist/dispatcher.d.cts.map +1 -0
- package/dist/dispatcher.d.mts +6 -0
- package/dist/dispatcher.d.mts.map +1 -0
- package/dist/dispatcher.mjs +2 -0
- package/dist/dispatcher.mjs.map +1 -0
- package/dist/extension.cjs +2 -0
- package/dist/extension.cjs.map +1 -0
- package/dist/extension.d.cts +6 -0
- package/dist/extension.d.cts.map +1 -0
- package/dist/extension.d.mts +6 -0
- package/dist/extension.d.mts.map +1 -0
- package/dist/extension.mjs +2 -0
- package/dist/extension.mjs.map +1 -0
- package/dist/extensions/dispatcher.cjs +2 -0
- package/dist/extensions/dispatcher.cjs.map +1 -0
- package/dist/extensions/dispatcher.d.cts +2 -0
- package/dist/extensions/dispatcher.d.mts +2 -0
- package/dist/extensions/dispatcher.mjs +2 -0
- package/dist/extensions/dispatcher.mjs.map +1 -0
- package/dist/extensions/pi.cjs +2 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +2 -0
- package/dist/extensions/pi.d.mts +2 -0
- package/dist/extensions/pi.mjs +2 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1 -0
- package/dist/leader.cjs +2 -0
- package/dist/leader.cjs.map +1 -0
- package/dist/leader.mjs +2 -0
- package/dist/leader.mjs.map +1 -0
- package/dist/types.cjs +2 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.mjs +2 -0
- package/dist/types.mjs.map +1 -0
- package/dist/workflow/backgroundWork.cjs +2 -0
- package/dist/workflow/backgroundWork.cjs.map +1 -0
- package/dist/workflow/backgroundWork.mjs +2 -0
- package/dist/workflow/backgroundWork.mjs.map +1 -0
- package/dist/workflow/doomOverlay.cjs +2 -0
- package/dist/workflow/doomOverlay.cjs.map +1 -0
- package/dist/workflow/doomOverlay.mjs +2 -0
- package/dist/workflow/doomOverlay.mjs.map +1 -0
- package/dist/workflow/overlayText.cjs +2 -0
- package/dist/workflow/overlayText.cjs.map +1 -0
- package/dist/workflow/overlayText.mjs +2 -0
- package/dist/workflow/overlayText.mjs.map +1 -0
- package/dist/workflow/piExtension.cjs +10 -0
- package/dist/workflow/piExtension.cjs.map +1 -0
- package/dist/workflow/piExtension.mjs +10 -0
- package/dist/workflow/piExtension.mjs.map +1 -0
- package/dist/workflow/piToolBridge.cjs +11 -0
- package/dist/workflow/piToolBridge.cjs.map +1 -0
- package/dist/workflow/piToolBridge.mjs +11 -0
- package/dist/workflow/piToolBridge.mjs.map +1 -0
- package/dist/workflow/piTools.cjs +4 -0
- package/dist/workflow/piTools.cjs.map +1 -0
- package/dist/workflow/piTools.mjs +4 -0
- package/dist/workflow/piTools.mjs.map +1 -0
- package/dist/workflow/rendering.cjs +2 -0
- package/dist/workflow/rendering.cjs.map +1 -0
- package/dist/workflow/rendering.mjs +2 -0
- package/dist/workflow/rendering.mjs.map +1 -0
- package/dist/workflow/workflowFooterStatus.cjs +2 -0
- package/dist/workflow/workflowFooterStatus.cjs.map +1 -0
- package/dist/workflow/workflowFooterStatus.mjs +2 -0
- package/dist/workflow/workflowFooterStatus.mjs.map +1 -0
- package/dist/workflow/workflowInspector.cjs +2 -0
- package/dist/workflow/workflowInspector.cjs.map +1 -0
- package/dist/workflow/workflowInspector.mjs +2 -0
- package/dist/workflow/workflowInspector.mjs.map +1 -0
- package/dist/workflow/workflowLauncher.cjs +2 -0
- package/dist/workflow/workflowLauncher.cjs.map +1 -0
- package/dist/workflow/workflowLauncher.mjs +2 -0
- package/dist/workflow/workflowLauncher.mjs.map +1 -0
- package/dist/workflow/workflowOverlay.cjs +2 -0
- package/dist/workflow/workflowOverlay.cjs.map +1 -0
- package/dist/workflow/workflowOverlay.mjs +2 -0
- package/dist/workflow/workflowOverlay.mjs.map +1 -0
- package/dist/workflow/workflowPicker.cjs +2 -0
- package/dist/workflow/workflowPicker.cjs.map +1 -0
- package/dist/workflow/workflowPicker.mjs +2 -0
- package/dist/workflow/workflowPicker.mjs.map +1 -0
- package/dist/workflow/workflowPrompt.cjs +2 -0
- package/dist/workflow/workflowPrompt.cjs.map +1 -0
- package/dist/workflow/workflowPrompt.mjs +2 -0
- package/dist/workflow/workflowPrompt.mjs.map +1 -0
- package/dist/workflow/workflowRunPanel.cjs +2 -0
- package/dist/workflow/workflowRunPanel.cjs.map +1 -0
- package/dist/workflow/workflowRunPanel.mjs +2 -0
- package/dist/workflow/workflowRunPanel.mjs.map +1 -0
- package/dist/workflow/workflowStatusRow.cjs +2 -0
- package/dist/workflow/workflowStatusRow.cjs.map +1 -0
- package/dist/workflow/workflowStatusRow.mjs +2 -0
- package/dist/workflow/workflowStatusRow.mjs.map +1 -0
- package/dist/workflow/workflowStepMessage.cjs +2 -0
- package/dist/workflow/workflowStepMessage.cjs.map +1 -0
- package/dist/workflow/workflowStepMessage.mjs +2 -0
- package/dist/workflow/workflowStepMessage.mjs.map +1 -0
- package/package.json +82 -0
- package/skills/workflow-recovery/SKILL.md +73 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piExtension.mjs","names":[],"sources":["../../src/workflow/piExtension.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { basename, dirname, extname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { DoomExtensionContext } from '@agimon-ai/doompi-extension-contracts/config';\nimport { registerDoomFooterContribution } from '@agimon-ai/doompi-extension-contracts/footer';\nimport { registerDoomLeaderActionHandlers } from '@agimon-ai/doompi-extension-contracts/leader';\nimport { registerDoomModeContribution } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { registerDoomSkillSource } from '@agimon-ai/doompi-extension-contracts/skills';\nimport type { ExtensionAPI, ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { type OverlayHandle, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { createProtocolRuntime, type ProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport { WorkflowModeChanged } from '@agimon-ai/doompi-extension-contracts/workflow';\nimport {\n createEmbeddedWorkflowFeature,\n type EmbeddedWorkflowFeature,\n readWorkflowProgress,\n summarizeWorkflowProgress,\n type Workflow,\n type WorkflowProgressEvent,\n type WorkflowProgressJob,\n type WorkflowRunControl,\n type WorkflowRunJobEvent,\n type WorkflowRunRecord,\n} from '@agimon-ai/workflow-mcp';\nimport { type RunProviderHandle, registerRunProvider } from './backgroundWork';\nimport {\n finishedRunSummary,\n isSessionRun,\n launchNotice,\n PI_SESSION_ENV,\n runsForSession,\n toolResultText,\n withOptions,\n} from './piToolBridge';\nimport {\n createWorkflowLaunchExecutor,\n registerWorkflowPiTools,\n WORKFLOW_PI_TOOL_NAMES,\n type WorkflowLaunchExecutor,\n} from './piTools';\nimport { openWorkflowLaunchWizard, type WorkflowLauncherUi } from './workflowLauncher';\nimport { openWorkflowPickerOverlay } from './workflowPicker';\nimport { openWorkflowChoice, openWorkflowInput } from './workflowPrompt';\nimport { runPanelUiOptions, WorkflowRunPanelComponent } from './workflowRunPanel';\nimport { WorkflowInspectorComponent, type WorkflowInspectorSelection } from './workflowInspector';\nimport { TerminalInputBatcher } from './workflowOverlay';\nimport { stepReportLines } from './workflowStatusRow';\nimport {\n workflowFooterStatus,\n WORKFLOW_PULSE_FRAMES,\n type WorkflowDotState,\n type WorkflowFooterInput,\n type WorkflowFooterRun,\n} from './workflowFooterStatus';\nimport {\n LEADER_DISABLE_ACTION,\n LEADER_ENABLE_ACTION,\n LEADER_LAUNCH_ACTION,\n LEADER_MANAGE_ACTION,\n LEADER_RECOVER_ACTION,\n PACKAGE_SOURCE,\n} from '../types.ts';\nimport {\n formatWorkflowStepMessage,\n isWorkflowStepMessageDetails,\n renderWorkflowStepMessage,\n workflowStepMessageDetails,\n} from './workflowStepMessage';\n\nconst DOOM_FULLSCREEN_UI_OPTIONS = {\n overlay: true,\n overlayOptions: {\n anchor: 'top-left',\n width: '100%',\n maxHeight: '100%',\n margin: 0,\n },\n} as const;\n\n/** A run panel currently on screen, and how to take it down. */\ninterface ActiveOverlay {\n handle?: OverlayHandle;\n /**\n * Whether keys typed at this panel can reach the run.\n *\n * False for a natively hosted run, which has no multiplexer to address. Such\n * a panel is opened without focus and must never be given it: typing into it\n * would go nowhere at all.\n */\n interactive: boolean;\n runKey: string;\n teardown: () => void;\n}\n\n/**\n * Footer key, and it is load-bearing beyond identity.\n *\n * Pi joins every extension status into ONE line, sorted alphabetically by key,\n * then truncates it to the terminal width. A key of `workflow-mcp` sorted last\n * and got cut off entirely behind chattier extensions, which made an enabled\n * mode look like it had not taken effect. The `agiflow-` prefix puts the two\n * modes a user actually toggles at the front, next to each other.\n */\nconst STATUS_KEY = 'agiflow-workflow';\nconst WORKFLOW_FINISHED_MESSAGE = 'workflow-run-finished';\nconst WORKFLOW_STEP_MESSAGE = 'workflow-step';\nconst FOLLOW_WIDGET_KEY = 'workflow-mcp-follow';\n/**\n * This mode's slot on the shared mode line doom-pi-ui renders.\n *\n * Replaces the below-editor widget this used to paint on a row of its own:\n * with plan mode and the loop doing the same, three enabled modes cost three\n * rows. Contributing a label instead puts them all on one wrapped line.\n */\nconst MODE_CONTRIBUTION_ID = 'workflow';\n/** Just the name. The line's premise is that everything on it is enabled. */\nconst MODE_LABEL = 'WORKFLOW';\n/** Not plan mode's warning colour: two labels in the same colour read as one. */\nconst MODE_LABEL_STYLE = 'accent';\n/** Ahead of plan mode and the loop, matching the leader order. */\nconst MODE_CONTRIBUTION_ORDER = 20;\nconst LEADER_MODE_BREADCRUMB = 'SPC w e';\nconst LAUNCH_BREADCRUMB = 'SPC › w / workflows › w / launch';\nconst MANAGE_BREADCRUMB = 'SPC › w / workflows › l / manage';\nconst RECOVER_BREADCRUMB = 'SPC › w / workflows › r / recover';\nconst RUN_PANEL_BREADCRUMB = 'SPC › w / workflows › run';\nconst FOOTER_STATUS_ID = 'workflow-runs';\n/** Between the doom loop and doom-team's agents, matching the leader order. */\nconst FOOTER_STATUS_ORDER = 50;\n/** Pulse cadence. Fast enough to read as motion, slow enough not to churn the footer. */\nconst PULSE_INTERVAL_MS = 250;\nconst PAUSED_EXECUTION_STATE = 'paused';\n\nconst UNFOLLOW_SHORTCUT = 'ctrl+alt+w';\n/**\n * Dismisses the run panel.\n *\n * Separate from the focus toggle because a focused panel hands every key to the\n * run, Escape included, so a plain \"Escape closes an overlay\" is unavailable:\n * interrupting the agent inside a step is worth more than a close key. Folding\n * closing into the focus toggle instead would leave an unfocused panel with no\n * way back to typing into the run.\n *\n * Not the only way out. This chord relies on a modifier some terminals do not\n * transmit, so `DoubleEscapeDetector` backs it with a plain double-Escape that\n * every terminal can send.\n */\nconst CLOSE_OVERLAY_SHORTCUT = 'ctrl+alt+q';\n\n/** Modifier glyphs macOS users read on their own keyboards and in every menu. */\nconst MAC_MODIFIER_GLYPHS: Record<string, string> = {\n alt: '⌥',\n cmd: '⌘',\n ctrl: '⌃',\n meta: '⌘',\n shift: '⇧',\n};\n\n/**\n * Render a binding the way the user's own platform writes it.\n *\n * The binding string Pi registers stays canonical (`ctrl+alt+w`) on every\n * platform; only the label changes. A macOS user reading \"alt\" has to translate\n * it to the Option key they actually press, and the modifier is not even\n * labelled \"alt\" on most Apple keyboards.\n *\n * Pi branches on `process.platform` for its own keybindings, so this follows a\n * convention already in the host rather than inventing one.\n */\nexport function shortcutLabel(binding: string, platform: NodeJS.Platform = process.platform): string {\n const parts = binding.split('+');\n if (platform === 'darwin') {\n // Glyphs join without separators, as they do in a macOS menu: ⌃⌥W.\n return parts.map((part) => MAC_MODIFIER_GLYPHS[part] ?? part.toUpperCase()).join('');\n }\n return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('+');\n}\nconst DEFAULT_PAGE_SIZE = 100;\n/**\n * The reconcile sweep, which is a fallback rather than the mechanism.\n *\n * The observer reports a change as it happens, so this only bounds how long a\n * change nothing notified us about stays invisible. It was 5s when it was the\n * only way anything was noticed.\n */\nconst MONITOR_INTERVAL_MS = 30_000;\n/**\n * Coalescing window for event-driven status refreshes.\n *\n * A run emits several transitions at once, and each one would otherwise page\n * the registry for the record set the footer needs. One page per burst.\n */\nconst STATUS_REFRESH_DEBOUNCE_MS = 120;\nconst PI_EXEC_TIMEOUT_MS = 10_000;\nconst RUNNING_STATUS = 'running';\nconst COMPLETED_STATUS = 'completed';\nconst FAILED_STATUS = 'failed';\nconst ERROR_STAGE = 'error';\nconst ERROR_COLOUR = 'error';\n/**\n * How long shutdown waits for an interrupted inline run to finalize its record.\n *\n * Long enough for a step that honours SIGTERM to unwind and for the registry\n * write that follows, short enough that a step ignoring it does not strand the\n * user in a session that will not close.\n */\nconst SHUTDOWN_FINALIZE_TIMEOUT_MS = 10_000;\nconst FOLLOW_INTERVAL_MS = 2_000;\nconst TAIL_LINES = 24;\n/**\n * Overlay refresh cadence and size.\n *\n * Faster than the background widget's 2s: a foregrounded terminal that lags\n * behind your own typing reads as broken, and only the one run being watched\n * pays for it.\n */\nconst OVERLAY_REFRESH_MS = 200;\n/**\n * Slowest part of a refresh, given its own cadence.\n *\n * Reading a run's screen means forking `tmux capture-pane` or `cmux read-screen`,\n * measured at ~75ms. Repainting at 200ms is right for a panel; forking at 200ms\n * is not, so the two are decoupled and only the render keeps the fast tick. A\n * terminal tail is a rolling window of the last few seconds either way, so the\n * slower read costs the reader nothing they can perceive.\n */\nconst SCREEN_REFRESH_MS = 750;\n/**\n * How many recent run announcements the launch handshake can look back through.\n *\n * Only ever searched for a run that registered within the acknowledgement\n * window, so this needs to cover concurrent launches, not session history. The\n * per-session concurrency ceiling is five.\n */\nconst ANNOUNCED_RUN_HISTORY = 32;\n/**\n * Telemetry name for a launch being asked for.\n *\n * Named once because both launch paths, the wizard and the tool, report it and\n * a query that spans them only works while the two agree.\n */\nconst LAUNCH_REQUESTED_EVENT = 'doom_workflow.launch_requested';\n/**\n * Recovery skill shipped by this package, offered to Pi at resource discovery.\n *\n * Recovery is the one path here complex enough to deserve a real skill rather\n * than a few tool guidelines, and this package owns that domain: it cannot\n * assume the host repository provides one. Discovery is cheap, since Pi puts\n * only the name, description, and location in the system prompt and reads the\n * body on demand.\n *\n * Resolved from the module directory, not the module file: `resolve` treats a\n * trailing filename as a path segment. Both `src/extensions` and\n * `dist/extensions` sit two levels below the package root.\n */\nconst SKILL_RELATIVE_PATH = '../../skills';\nconst WORKFLOW_SKILL_SOURCE = '@agimon-ai/doompi-workflow';\n\n/**\n * Glyphs for the footer, widget, and notices.\n *\n * Deliberately restricted to characters that render in a default terminal font.\n * A box-drawing or emoji glyph that falls back to tofu is worse than no glyph.\n */\nconst GLYPH = {\n running: '▸',\n repair: '⚠',\n completed: '✔',\n failed: '✖',\n separator: '·',\n} as const;\n\n/**\n * The panel's footer: how to leave, in the order a stuck user needs it.\n *\n * Escape leads on an interactive panel because it is the key every terminal\n * transmits, and the one someone reaches for first when a view will not let go.\n * A view-only panel says so instead: a user who does not know the run is\n * unreachable reads the silence as the panel being broken.\n */\nexport function panelHint(interactive: boolean, platform: NodeJS.Platform = process.platform): string {\n const close = shortcutLabel(CLOSE_OVERLAY_SHORTCUT, platform);\n if (!interactive) return `view only ${GLYPH.separator} ${close} closes this view`;\n return [\n 'Esc Esc closes this view',\n `${shortcutLabel(UNFOLLOW_SHORTCUT, platform)} switches typing`,\n `${close} closes`,\n ].join(` ${GLYPH.separator} `);\n}\n\n/** The same hints as key caps, for the doom footer legend. */\nexport function panelHints(\n interactive: boolean,\n platform: NodeJS.Platform = process.platform,\n): readonly (readonly [string, string])[] {\n const close: readonly [string, string] = [shortcutLabel(CLOSE_OVERLAY_SHORTCUT, platform), 'close'];\n if (!interactive) return [close];\n // Leads the legend, and spelled as the copy says it: this cap is the escape\n // hatch, and the chords beside it ride on a modifier some terminals never send.\n return [['Esc Esc', 'close'], [shortcutLabel(UNFOLLOW_SHORTCUT, platform), 'switch typing'], close];\n}\n\n/**\n * Header and output lines for the follow widget, fitted to the terminal.\n *\n * Every line a component renders must fit the given width, so the header sheds\n * detail right to left as the terminal narrows: the dismissal hint goes first,\n * then the workspace/stage context, and the run key survives longest because it\n * is the identifier the user needs to name the run.\n *\n * Colour is applied after fitting. Measuring plain text and styling afterwards\n * keeps the arithmetic honest, since escape codes occupy no visible columns.\n */\nfunction renderFollowLines(record: WorkflowRunRecord, output: string[], theme: Theme, width: number): string[] {\n const key = `${GLYPH.running} ${record.runKey}`;\n const meta = [record.workspace, stageLabel(record), record.runner].filter(Boolean).join(` ${GLYPH.separator} `);\n const hint = `${shortcutLabel(UNFOLLOW_SHORTCUT)} to background`;\n const gap = ' ';\n\n let header = theme.fg('accent', truncateToWidth(key, width));\n if (visibleWidth(key) + gap.length + visibleWidth(meta) <= width) {\n header += gap + theme.fg('muted', meta);\n const used = visibleWidth(key) + gap.length + visibleWidth(meta);\n if (used + gap.length + visibleWidth(hint) <= width) header += gap + theme.fg('dim', hint);\n }\n\n return [header, ...output.map((line) => truncateToWidth(line, width))];\n}\n\n/**\n * Job tree for the follow widget, in the shape a CI run reads best.\n *\n * Finished jobs collapse to a single line so the eye lands on the one still\n * running, whose steps are listed. That is the whole reason this exists: a\n * scraped terminal shows whatever the runner last printed, which is not the\n * same as knowing which job of how many is in flight.\n */\nfunction renderProgressLines(jobs: WorkflowProgressJob[], theme: Theme, width: number): string[] {\n const lines: string[] = [];\n for (const job of jobs) {\n const glyph =\n job.status === COMPLETED_STATUS ? GLYPH.completed : job.status === FAILED_STATUS ? GLYPH.failed : GLYPH.running;\n const colour = job.status === COMPLETED_STATUS ? 'success' : job.status === FAILED_STATUS ? ERROR_COLOUR : 'accent';\n const position = job.total ? ` ${GLYPH.separator} ${(job.index ?? 0) + 1}/${job.total}` : '';\n lines.push(truncateToWidth(theme.fg(colour, `${glyph} ${job.name}`) + theme.fg('dim', position), width));\n\n // Only the job in flight expands. A finished job's steps are noise.\n if (job.status !== RUNNING_STATUS) continue;\n for (const step of job.steps) {\n const stepGlyph =\n step.status === COMPLETED_STATUS\n ? GLYPH.completed\n : step.status === FAILED_STATUS\n ? GLYPH.failed\n : GLYPH.running;\n const stepColour =\n step.status === RUNNING_STATUS ? 'muted' : step.status === FAILED_STATUS ? ERROR_COLOUR : 'dim';\n lines.push(truncateToWidth(theme.fg(stepColour, ` ${stepGlyph} ${step.name}`), width));\n }\n }\n return lines;\n}\n\nfunction progressEventsForRun(record: WorkflowRunRecord, events: WorkflowProgressEvent[]): WorkflowProgressEvent[] {\n const startedAt = Date.parse(record.startedAt);\n if (!Number.isFinite(startedAt)) return events;\n return events.filter((event) => {\n const eventAt = Date.parse(event.at);\n return !Number.isFinite(eventAt) || eventAt >= startedAt;\n });\n}\n\n/** Human-readable stage, rather than leaking the raw registry enum. */\nfunction stageLabel(record: WorkflowRunRecord): string {\n if (record.stale) return 'stale';\n if (record.stage === ERROR_STAGE) return FAILED_STATUS;\n return record.stage;\n}\n\ninterface ParsedArguments {\n invalidLauncher?: string;\n launcher?: MultiplexerLauncher;\n name?: string;\n positionals: string[];\n runner?: string;\n workspace?: string;\n}\n\n/** Spawn a launcher and forget it, which is all a delegated run needs from us. */\nexport type DetachedSpawn = (\n command: string,\n args: readonly string[],\n options: { cwd: string; env: NodeJS.ProcessEnv },\n) => void;\n\ninterface WorkflowPiExtensionOptions {\n followIntervalMs?: number;\n monitorIntervalMs?: number;\n /** Told about every mode change, so the leader menu can offer the way out rather than the way in. */\n onModeChange?: (enabled: boolean) => void;\n featureFactory?: () => EmbeddedWorkflowFeature;\n /** Injected so a test can read the delegated command without running it. */\n spawnDetached?: DetachedSpawn;\n launchAckPollMs?: number;\n launchAckTimeoutMs?: number;\n}\n\n/**\n * Multiplexer identity of THIS process, which a launcher spawn must not carry.\n *\n * A child that inherits it records Pi's own workspace as the run's launcher, so\n * every later `open` or `tail` would foreground or scrape this session's screen\n * instead of the run's. The status file is per-step and equally non-inheritable.\n */\nconst LAUNCHER_ENVIRONMENT_STRIPPED = [\n 'CMUX_SURFACE_ID',\n 'CMUX_WORKSPACE_ID',\n 'WORKFLOW_LAUNCHER_TYPE',\n 'WORKFLOW_TMUX_SESSION_NAME',\n 'WORKFLOW_STATUS_FILE',\n] as const;\n\n/**\n * Slack between this process's clock and the run record's timestamp.\n *\n * The record is written by another process, and a launch acknowledged a\n * fraction of a second \"before\" it began is a clock difference, not somebody\n * else's run.\n */\nconst LAUNCH_ACK_CLOCK_SKEW_MS = 2_000;\n\nconst LAUNCHER_ENV = 'WORKFLOW_LAUNCHER';\nconst RECOVER_COMMAND = 'recover-workflow';\nconst LAUNCH_PROCESS_COMMAND = 'launch-process';\n\n/**\n * How a run is hosted, which is also the name of the binary that addresses it.\n *\n * One token per launcher rather than two, because they are the same token: a\n * record's `launcher.type` is `tmux` precisely because tmux is what hosts it and\n * `tmux` is what reads it back. `native` has no binary, which is exactly why\n * every surface has to branch on it.\n */\nconst LAUNCHER_TMUX = 'tmux';\nconst LAUNCHER_CMUX = 'cmux';\nconst LAUNCHER_NATIVE = 'native';\ntype MultiplexerLauncher = typeof LAUNCHER_TMUX | typeof LAUNCHER_CMUX;\n\n/**\n * A run with no recorded launcher, which reading and foregrounding both hit.\n *\n * Older records predate launcher identity being persisted, so this is a real\n * state rather than a corrupt one, and both surfaces owe the user the same\n * explanation for it.\n */\nconst NO_LAUNCHER_RECORDED = 'No durable launcher identity is recorded for this run.';\n\n/**\n * The identifier a run record carries for its definition.\n *\n * Derived the same way `RunWorkflowService` derives it, so the two agree: the\n * file name without its final extension, which keeps the `.workflow` part.\n */\nfunction workflowIdFor(workflowPath: string): string {\n return basename(workflowPath, extname(workflowPath));\n}\n\nfunction launcherEnvironment(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n for (const key of LAUNCHER_ENVIRONMENT_STRIPPED) delete env[key];\n return env;\n}\n\n/** The launcher the workflow templates default to, when it is a known one. */\nfunction preferredLauncher(): MultiplexerLauncher | undefined {\n const value = process.env[LAUNCHER_ENV];\n return value === LAUNCHER_TMUX || value === LAUNCHER_CMUX ? value : undefined;\n}\n\n/**\n * One shell word per argument.\n *\n * `launch-process` hands `--command` to the launcher as a shell string, so a\n * run key or a path with a space in it has to survive one more round of shell\n * parsing than the argv we spawn with.\n */\nfunction shellCommand(argv: readonly string[]): string {\n return argv.map((word) => `'${word.replaceAll(\"'\", `'\\\\''`)}'`).join(' ');\n}\n\n/**\n * The workflow-mcp CLI this extension is built against.\n *\n * Resolved from the dependency rather than PATH: `pnpm exec workflow-mcp` finds\n * whatever the workspace root happens to install, which is not necessarily this\n * version, and it pays for a package-manager resolution on the launch path.\n */\nfunction resolveWorkflowCli(): string | undefined {\n try {\n return createRequire(import.meta.url).resolve('@agimon-ai/workflow-mcp/cli');\n } catch {\n return undefined;\n }\n}\n\nconst detachedSpawn: DetachedSpawn = (command, args, options) => {\n // Detached with no stdio: the run owns a launcher terminal of its own, and\n // anything inherited from here would be written onto Pi's screen.\n const child = spawn(command, [...args], { ...options, detached: true, stdio: 'ignore' });\n child.unref();\n};\n\nfunction tokenize(value: string): string[] {\n return (\n value.match(/\"[^\"]*\"|'[^']*'|\\S+/g)?.map((token) => {\n if ((token.startsWith('\"') && token.endsWith('\"')) || (token.startsWith(\"'\") && token.endsWith(\"'\"))) {\n return token.slice(1, -1);\n }\n return token;\n }) ?? []\n );\n}\n\nexport function parseWorkflowCommandArguments(value: string): ParsedArguments {\n const tokens = tokenize(value);\n const parsed: ParsedArguments = { positionals: [] };\n for (let index = 0; index < tokens.length; index += 1) {\n const token = tokens[index];\n const next = tokens[index + 1];\n if (token === '--launcher' && next) {\n if (next === LAUNCHER_CMUX || next === LAUNCHER_TMUX) parsed.launcher = next;\n else parsed.invalidLauncher = next;\n index += 1;\n } else if (token === '--runner' && next) {\n parsed.runner = next;\n index += 1;\n } else if (token === '--workspace' && next) {\n parsed.workspace = next;\n index += 1;\n } else if (token === '--name' && next) {\n parsed.name = next;\n index += 1;\n } else {\n parsed.positionals.push(token);\n }\n }\n return parsed;\n}\n\nfunction collectRunnerMaps(value: unknown, maps: string[][]): void {\n if (Array.isArray(value)) {\n for (const item of value) collectRunnerMaps(item, maps);\n return;\n }\n if (!value || typeof value !== 'object') return;\n for (const [key, nested] of Object.entries(value)) {\n if ((key === 'interactiveRun' || key === 'run') && nested && typeof nested === 'object' && !Array.isArray(nested)) {\n maps.push(Object.keys(nested));\n } else {\n collectRunnerMaps(nested, maps);\n }\n }\n}\n\nexport function compatibleRunners(workflow: Workflow): string[] | undefined {\n const maps: string[][] = [];\n collectRunnerMaps(workflow, maps);\n if (maps.length === 0) return undefined;\n return maps[0].filter((runner) => maps.every((available) => available.includes(runner))).sort();\n}\n\nasync function listAllRuns(\n registry: EmbeddedWorkflowFeature['registry'],\n workspace?: string,\n): Promise<WorkflowRunRecord[]> {\n const records: WorkflowRunRecord[] = [];\n let page = 1;\n let hasNextPage = true;\n while (hasNextPage) {\n const result = await registry.listRunsPage({ page, pageSize: DEFAULT_PAGE_SIZE, workspace });\n records.push(...result.items);\n hasNextPage = result.hasNextPage;\n page += 1;\n }\n return records;\n}\n\n/**\n * One run, as a person reads it.\n *\n * Middle dots rather than pipes: pipes read as table borders in a terminal and\n * collide visually with the launcher output the widget sits above.\n */\nfunction runLabel(record: WorkflowRunRecord): string {\n return [record.runKey, record.workspace, stageLabel(record), record.runner]\n .filter(Boolean)\n .join(` ${GLYPH.separator} `);\n}\n\nexport function createWorkflowPiExtension(options: WorkflowPiExtensionOptions = {}) {\n return (pi: ExtensionAPI): void => {\n const feature = options.featureFactory?.() ?? createEmbeddedWorkflowFeature();\n const { parser, recoverService, registry, runService, runTool } = feature;\n const spawnDetached = options.spawnDetached ?? detachedSpawn;\n const monitorIntervalMs = options.monitorIntervalMs ?? MONITOR_INTERVAL_MS;\n const followIntervalMs = options.followIntervalMs ?? FOLLOW_INTERVAL_MS;\n let monitorTimer: NodeJS.Timeout | undefined;\n let monitorRefreshActive = false;\n let followTimer: NodeJS.Timeout | undefined;\n let followRefreshActive = false;\n let followedRun: WorkflowRunRecord | undefined;\n let previousStages = new Map<string, string>();\n const pendingTerminalRuns = new Map<string, WorkflowRunRecord>();\n const deliveredTerminalRuns = new Set<string>();\n let sessionStartedAt = 0;\n let runProvider: RunProviderHandle | undefined;\n let protocolRuntime: ProtocolRuntime | undefined;\n let disposeModeSubscription: (() => void) | undefined;\n let sessionRunCount = 0;\n let workflowMode = false;\n /** What the line currently shows, so an unchanged mode never republishes. */\n let modeWidgetVisible = false;\n const modeContribution = registerDoomModeContribution(pi, {\n source: PACKAGE_SOURCE,\n id: MODE_CONTRIBUTION_ID,\n order: MODE_CONTRIBUTION_ORDER,\n });\n /** Last footer input, so the pulse ticker repaints without re-reading the registry. */\n let footerInput: WorkflowFooterInput = { runs: [], failed: 0 };\n let footerFingerprint: string | undefined;\n let footerFrame = 0;\n let pulseTimer: NodeJS.Timeout | undefined;\n const footerContribution = registerDoomFooterContribution(pi, {\n source: PACKAGE_SOURCE,\n id: FOOTER_STATUS_ID,\n order: FOOTER_STATUS_ORDER,\n });\n /** Kept so callers outside a handler can still resolve the session id. */\n let sessionCtx: ExtensionContext | undefined;\n let activeOverlay: ActiveOverlay | undefined;\n let inspectorHandle: OverlayHandle | undefined;\n let inspectorOpen = false;\n /** The runs the inspector last listed, keyed the way its items are. */\n let inspectorRecords = new Map<string, WorkflowRunRecord>();\n /**\n * The pushed view of runs this session owns.\n *\n * A delegated run executes in a launcher's process, so nothing in here ever\n * observes it directly. What it observes is the registry the run writes to,\n * which is why this replaces polling rather than instrumenting anything.\n */\n let runControl: WorkflowRunControl | undefined;\n const disposeRunControl: Array<() => void> = [];\n /**\n * Job statuses per run, folded from `job` events.\n *\n * Only used to tell a run that has started working from one still claiming\n * its first job. Previously this meant re-reading and re-folding the whole\n * progress log on every tick to ask one boolean question.\n */\n const runJobStatus = new Map<string, Map<string, string>>();\n /**\n * How far each run has got, as a fraction of its job list.\n *\n * The engine stamps the job's position and the job count on every job-start,\n * so the footer gets progress out of the event it already handles rather\n * than by folding a progress log per run per tick.\n */\n const runProgress = new Map<string, number>();\n /**\n * Step transitions per run, accumulated from pushed events.\n *\n * Kept as a list rather than folded on arrival because a finished step's\n * duration comes from pairing it with the `running` event that opened it,\n * which one event in isolation cannot supply. Released when the run ends.\n */\n const runEvents = new Map<string, WorkflowProgressEvent[]>();\n /**\n * Runs the observer has announced, newest first, for the launch handshake.\n *\n * A launch needs the run key of the run it just started, and the launcher\n * process never learns it. Bounded, because a session that launches all day\n * must not accumulate a record per launch for the sake of a lookup that\n * only ever cares about the last few seconds.\n */\n const announcedRuns: WorkflowRunRecord[] = [];\n /**\n * Transitions already put in the session, per run.\n *\n * Inner keys carry the event's own timestamp, so re-reading the append-only\n * log on every tick reports each transition exactly once however often we\n * poll. Grouped by run rather than held in one flat set so the memory can be\n * given back: a long session that launches many runs would otherwise\n * accumulate every step of every run it ever watched, and the keys of a\n * finished run can never match anything again.\n */\n const reportedSteps = new Map<string, Set<string>>();\n\n /**\n * The session that owns this extension's runs.\n *\n * Taken from whatever context Pi hands us, and deliberately the SAME value\n * `launch_workflow` stamps onto a run: reading the stamp from one place and\n * the filter from another is how a run goes invisible to the session that\n * just started it. `session_start` fills this in for a session that has\n * launched nothing yet; before either, it is undefined and every ownership\n * check fails closed, which is correct because such a session owns no runs.\n */\n let knownSessionId: string | undefined;\n let telemetry: DoomTelemetry | undefined;\n const getTelemetry = (ctx?: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: 'doom-workflow',\n packageName: '@agimon-ai/doompi-workflow',\n cwd: ctx?.cwd,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n return telemetry;\n };\n const observeSession = (sessionId: string | undefined): void => {\n if (sessionId) knownSessionId = resolveRootSessionId(sessionId);\n };\n const currentSessionId = (): string | undefined => {\n const sessionId = knownSessionId ?? sessionCtx?.sessionManager.getSessionId();\n return sessionId ? resolveRootSessionId(sessionId) : undefined;\n };\n const isRootProcess = (ctx: ExtensionContext, rootSessionId: string): boolean =>\n ctx.sessionManager.getSessionId().trim() === rootSessionId;\n\n /**\n * The registry is one directory under $HOME shared by every repository and\n * every Pi session on the machine, so an unscoped tool would let any session\n * list, foreground, stop, or recover any other session's runs. Scoping is a\n * host-level filter on the tool instance, not a caller input: the agent\n * cannot opt out of it, and the CLI (which is how orphaned runs are managed)\n * keeps the global view.\n */\n const recoverTool = feature.createRecoverTool({\n // Ownership follows the recovering session. Left alone, the replay\n // would inherit the failed record's stamp and the session that just\n // recovered the run could not see the run it had recovered.\n envOverrides: () => {\n const sessionId = currentSessionId();\n return sessionId ? { [PI_SESSION_ENV]: sessionId } : undefined;\n },\n });\n const controlTool = feature.createControlTool();\n\n /**\n * Inline executions still in flight, so shutdown can finalize them.\n *\n * A workflow declaring `launch-command` hands off to tmux or cmux and its\n * promise settles almost immediately; one without runs its whole engine in\n * this process. Only the latter lingers here, so the set needs no launcher\n * check of its own.\n */\n const pendingInlineRuns = new Set<Promise<unknown>>();\n const trackPendingRun = <T>(run: Promise<T>): Promise<T> => {\n const startedAt = Date.now();\n pendingInlineRuns.add(run);\n void getTelemetry().recordEvent('doom_workflow.run_started', { outcome: 'started' });\n // Bookkeeping and telemetry share one handler pair on purpose. A separate\n // `finally` derives a promise that rejects with nobody watching, and an\n // unhandled rejection ends the process, TUI included. Since a launch is\n // answered from the registry, nothing else is necessarily awaiting this\n // run by the time it fails, so this pair is the only thing standing\n // between a failed launcher and a dead session.\n void run.then(\n () => {\n pendingInlineRuns.delete(run);\n return getTelemetry().recordEvent('doom_workflow.run_finished', {\n duration_ms: Date.now() - startedAt,\n outcome: 'completed',\n });\n },\n (error: unknown) => {\n pendingInlineRuns.delete(run);\n return getTelemetry().recordError('doom_workflow.run_failed', error, {\n duration_ms: Date.now() - startedAt,\n });\n },\n );\n return run;\n };\n\n const rejectRunner = (workflowPath: string, runner: string): string | undefined => {\n const compatible = compatibleRunners(parser.parseWorkflowFile(workflowPath));\n if (!compatible || compatible.includes(runner)) return undefined;\n return `Runner ${JSON.stringify(runner)} is not available across this workflow. Compatible runners: ${compatible.join(', ') || 'none'}.`;\n };\n\n const launchExecutor: WorkflowLaunchExecutor = createWorkflowLaunchExecutor({\n activeRunCount: async () => sessionRunCount,\n observeSession,\n rejectRunner,\n runTool,\n trackPendingRun,\n ...(options.launchAckPollMs === undefined ? {} : { launchAckPollMs: options.launchAckPollMs }),\n ...(options.launchAckTimeoutMs === undefined ? {} : { launchAckTimeoutMs: options.launchAckTimeoutMs }),\n // A run registering is the earliest honest answer to \"did it start?\": the\n // launcher's own process chain can stay open long after the run is\n // working, and a launch that waits for it holds the caller's turn open\n // for as long as that takes.\n //\n // Answered from what the observer has announced, with the poll driving a\n // sweep rather than waiting for one. Reading `announcedRuns` alone would\n // make the handshake hostage to a filesystem notification, and a dropped\n // one would cost the caller the whole acknowledgement budget for a run\n // that had already registered. A sweep costs the registry read this used\n // to do on its own, and every other surface gets its events from it.\n findLaunchedRun: async ({ sessionId, since, workflowPath }) => {\n const expectedId = workflowIdFor(workflowPath);\n const matches = (record: WorkflowRunRecord): boolean => {\n if (!isSessionRun(record, sessionId) || record.stale) return false;\n // Matched on identity as well as time so two launches in flight from\n // one session cannot report each other's run key.\n if (record.workflowId && record.workflowId !== expectedId) return false;\n const startedAt = Date.parse(record.startedAt);\n return Number.isFinite(startedAt) && startedAt >= since - LAUNCH_ACK_CLOCK_SKEW_MS;\n };\n // No observer here: the dispatcher bridge runs the launch tool without\n // a session of its own, so it still has to look for itself.\n if (!runControl) return (await listAllRuns(registry)).find(matches);\n await runControl.refresh();\n return announcedRuns.find(matches);\n },\n // A launch answered early still owes the user its failure.\n onLateFailure: (error, ctx) => {\n const detail = error instanceof Error ? error.message : String(error);\n ctx.ui.notify(launchNotice(`Workflow launch failed after it was reported started.\\n${detail}`), 'warning');\n },\n onLaunch: async (ctx) => {\n await getTelemetry(ctx).recordEvent(LAUNCH_REQUESTED_EVENT, { outcome: 'requested' });\n return refreshStatus(ctx, false);\n },\n });\n\n /**\n * Put each step transition in the session without giving the agent a turn.\n *\n * `triggerTurn: false` appends, persists and displays without starting a\n * model turn while idle. While the agent is streaming, a custom message\n * still defaults to steering, so these only go out when the agent is idle.\n * A transition that arrives mid-turn waits: the run's events are kept, so\n * the next idle moment reports them in order rather than losing them.\n */\n const reportStepTransitions = (ctx: ExtensionContext, record: WorkflowRunRecord): void => {\n if (!ctx.isIdle()) return;\n const identity = runIdentity(record);\n let reported = reportedSteps.get(identity);\n if (!reported) {\n reported = new Set<string>();\n reportedSteps.set(identity, reported);\n }\n // Re-derived from the run's whole event list rather than from the one\n // event that just arrived, because a finished step's duration is only\n // knowable next to the `running` event that opened it.\n for (const report of stepReportLines(record.runKey, runEvents.get(identity) ?? [])) {\n if (reported.has(report.key)) continue;\n reported.add(report.key);\n const details = workflowStepMessageDetails(record.displayName, report);\n pi.sendMessage(\n {\n customType: WORKFLOW_STEP_MESSAGE,\n content: formatWorkflowStepMessage(details),\n details,\n display: true,\n },\n { triggerTurn: false },\n );\n }\n };\n\n /**\n * The running set, as footer dots.\n *\n * Pure, and deliberately so. This used to read and re-fold every reportable\n * run's whole progress log on every tick to answer one question per run:\n * has it started working, or is it still claiming its first job. The job\n * statuses folded from pushed events answer it without touching the disk.\n */\n const footerRunsFor = (records: WorkflowRunRecord[]): WorkflowFooterRun[] =>\n records\n .filter((record) => record.stage === RUNNING_STATUS && !record.stale)\n .map((record) => {\n const identity = runIdentity(record);\n const jobs = runJobStatus.get(identity);\n // A run with no job under way yet is starting, not working: a pulsing\n // dot for a run still in `pre` claims progress that has not begun.\n const working = jobs ? [...jobs.values()].includes(RUNNING_STATUS) : false;\n const state: WorkflowDotState =\n record.executionState === PAUSED_EXECUTION_STATE ? 'paused' : working ? 'running' : 'starting';\n const progress = runProgress.get(identity);\n return { key: identity, state, ...(progress === undefined ? {} : { progress }) };\n });\n\n /**\n * Fold one job status into a run's tally.\n *\n * `authoritative` separates a real job transition from the one a step\n * implies. A step running says its job is running, but it must not overwrite\n * a job the engine has already reported completed or failed.\n */\n const noteJobStatus = (identity: string, job: string, status: string, authoritative: boolean): void => {\n const jobs = runJobStatus.get(identity) ?? new Map<string, string>();\n if (authoritative || !jobs.has(job)) jobs.set(job, status);\n runJobStatus.set(identity, jobs);\n };\n\n /**\n * Fold a job-start into the run's position on its job list.\n *\n * Only a start carries the numbers, so nothing is derived from finishes.\n * `pre` and `post` are phase blocks the engine records as pseudo-jobs and\n * are outside the ordered list the index counts against, so they are\n * skipped rather than allowed to claim a position they do not hold.\n */\n const noteJobProgress = (identity: string, event: WorkflowRunJobEvent): void => {\n if (event.phase !== 'job' || event.status !== RUNNING_STATUS) return;\n if (event.index === undefined || !event.total) return;\n runProgress.set(identity, Math.min(1, Math.max(0, event.index) / event.total));\n };\n\n /**\n * Ask for a status refresh, collapsing a burst of events into one.\n *\n * The transcript and the job tally update from the event itself, but the\n * footer also needs the session's record set, and that still costs a\n * registry page. A run emitting six transitions at once must not buy six.\n */\n let statusRefreshTimer: NodeJS.Timeout | undefined;\n const scheduleStatusRefresh = (ctx: ExtensionContext): void => {\n if (statusRefreshTimer) return;\n statusRefreshTimer = setTimeout(() => {\n statusRefreshTimer = undefined;\n void refreshStatus(ctx, true);\n }, STATUS_REFRESH_DEBOUNCE_MS);\n statusRefreshTimer.unref?.();\n };\n\n /** Everything remembered about a run, released once it can produce no more. */\n const forgetRun = (identity: string): void => {\n runEvents.delete(identity);\n runJobStatus.delete(identity);\n runProgress.delete(identity);\n reportedSteps.delete(identity);\n };\n\n /**\n * Subscribe to the registry instead of asking it what changed.\n *\n * A run this session launched executes in a launcher's own process, so the\n * only thing connecting it to this one is the session stamp on its record.\n * The filter is that stamp, and it is applied inside the observer so a\n * shared registry never leaks another session's runs into this footer.\n */\n const startRunControl = async (ctx: ExtensionContext): Promise<void> => {\n const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n const control = feature.createRunControl({\n recordFilter: (candidate) => isSessionRun(candidate, sessionId),\n });\n runControl = control;\n\n disposeRunControl.push(\n control.on('step', (event) => {\n const identity = runIdentity(event.record);\n const events = runEvents.get(identity) ?? [];\n events.push({ at: event.at, job: event.job, status: event.status, step: event.step, type: 'step' });\n runEvents.set(identity, events);\n // A step running is a job running. The fold this replaced inferred\n // the same thing, so a workflow whose engine records steps without a\n // surrounding job transition still reads as working, not starting.\n noteJobStatus(identity, event.job, RUNNING_STATUS, false);\n reportStepTransitions(ctx, event.record);\n scheduleStatusRefresh(ctx);\n }),\n control.on('job', (event) => {\n const identity = runIdentity(event.record);\n noteJobStatus(identity, event.job, event.status, true);\n noteJobProgress(identity, event);\n scheduleStatusRefresh(ctx);\n }),\n control.on('runStarted', (event) => {\n announcedRuns.unshift(event.record);\n if (announcedRuns.length > ANNOUNCED_RUN_HISTORY) announcedRuns.length = ANNOUNCED_RUN_HISTORY;\n scheduleStatusRefresh(ctx);\n }),\n control.on('runUpdated', () => void refreshStatus(ctx, true)),\n control.on('runFinished', () => void refreshStatus(ctx, true)),\n );\n\n await control.start();\n };\n\n const stopRunControl = (): void => {\n if (statusRefreshTimer) clearTimeout(statusRefreshTimer);\n statusRefreshTimer = undefined;\n for (const dispose of disposeRunControl.splice(0)) dispose();\n runControl?.dispose();\n runControl = undefined;\n runEvents.clear();\n runJobStatus.clear();\n runProgress.clear();\n reportedSteps.clear();\n };\n\n /** Dismiss the run panel entirely, leaving the run itself alone. */\n const closeOverlay = (): void => {\n activeOverlay?.teardown();\n activeOverlay?.handle?.hide();\n activeOverlay = undefined;\n };\n\n const runIdentity = (record: WorkflowRunRecord): string => record.runId ?? `${record.workspace}/${record.runKey}`;\n\n /**\n * Paint one animation frame of the footer group.\n *\n * Published twice on purpose: the doom footer renders the segments, and Pi's\n * own status line is the fallback for a session running without the doom UI\n * extension. Both carry the same text, so the two never disagree.\n */\n const publishFooter = (ctx: ExtensionContext, force = false): void => {\n const status = workflowFooterStatus(footerInput, footerFrame);\n if (!force && status?.fingerprint === footerFingerprint) return;\n footerFingerprint = status?.fingerprint;\n footerContribution.update(status?.footer);\n ctx.ui.setStatus(STATUS_KEY, status?.text);\n syncPulseTimer(ctx, status?.pulsing ?? false);\n };\n\n /**\n * Put this mode's label on the shared mode line, or take it off.\n *\n * The footer group says what is running; this says the mode is on, which is\n * the state a session with no runs cannot otherwise see. doom-pi-ui joins\n * every contributed label into one line, so nothing here has to know which\n * other modes are enabled or how wide the terminal is.\n *\n * Only published when the state actually changes: the monitor tick would\n * otherwise republish an identical label several times a minute.\n */\n const publishModeWidget = (): void => {\n if (workflowMode === modeWidgetVisible) return;\n modeWidgetVisible = workflowMode;\n modeContribution.update(workflowMode ? { label: MODE_LABEL, color: MODE_LABEL_STYLE } : undefined);\n };\n\n /** The ticker only exists while something is animating; an idle session runs no timer. */\n const syncPulseTimer = (ctx: ExtensionContext, pulsing: boolean): void => {\n if (!pulsing) {\n if (pulseTimer) clearInterval(pulseTimer);\n pulseTimer = undefined;\n return;\n }\n if (pulseTimer) return;\n pulseTimer = setInterval(() => {\n footerFrame = (footerFrame + 1) % WORKFLOW_PULSE_FRAMES.length;\n publishFooter(ctx);\n }, PULSE_INTERVAL_MS);\n pulseTimer.unref?.();\n };\n\n const refreshStatus = async (ctx: ExtensionContext, notifyChanges: boolean): Promise<void> => {\n // Resolved before the guard is latched. Anything that can throw between\n // setting the flag and entering the try would strand it set, and a\n // stranded flag turns every later refresh into the early return above:\n // the run snapshot freezes, so the loop's launch budget never recovers.\n const activeTelemetry = getTelemetry(ctx);\n if (monitorRefreshActive) {\n void activeTelemetry.recordEvent('doom_workflow.monitor_skipped', { outcome: 'already_running' });\n return;\n }\n const startedAt = Date.now();\n monitorRefreshActive = true;\n try {\n const records = await listAllRuns(registry);\n const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n const rootProcess = isRootProcess(ctx, sessionId);\n const sessionRecords = records.filter((record) => isSessionRun(record, sessionId));\n const running = sessionRecords.filter((record) => record.stage === RUNNING_STATUS && !record.stale);\n const failed = sessionRecords.filter((record) => record.stage === ERROR_STAGE);\n void activeTelemetry.recordEvent('doom_workflow.monitor_counts', {\n 'workflow.run_count': sessionRecords.length,\n 'workflow.running_count': running.length,\n 'workflow.failed_count': failed.length,\n mode: workflowMode ? 'on' : 'off',\n });\n\n // Runs this Pi session launched, identified by the session stamp the\n // launch tool puts in the run environment. Publishing them lets other\n // extensions size their own work without reaching into this one.\n const mine = runsForSession(records, sessionId).filter((record) => !record.stale);\n sessionRunCount = mine.length;\n runProvider?.update(mine.map((record) => ({ id: record.runId ?? record.runKey, sessionId })));\n\n // Folded from pushed job events rather than read back off disk. The\n // dots distinguish a run that is working from one still claiming its\n // first job, which used to cost a full progress-log read per run per\n // tick to decide.\n footerInput = {\n runs: footerRunsFor(sessionRecords),\n failed: failed.length,\n };\n publishFooter(ctx, true);\n\n // A run that has left the session's set can never produce output again,\n // so its cached screen is dead weight.\n forgetLauncherScreens(new Set(sessionRecords.map((record) => runIdentity(record))));\n\n // Accumulated transitions belong to runs still going or still owed a\n // summary. Anything else is finished business: the delivery path below\n // releases what it reports, and this catches a run that will never be\n // reported, which is every run in a process that does not deliver.\n const liveIdentities = new Set(\n sessionRecords\n .filter((record) => record.stage === RUNNING_STATUS && !record.stale)\n .map((record) => runIdentity(record)),\n );\n for (const identity of [...runEvents.keys(), ...runJobStatus.keys()]) {\n if (!liveIdentities.has(identity) && !pendingTerminalRuns.has(identity)) forgetRun(identity);\n }\n\n const nextStages = new Map(sessionRecords.map((record) => [runIdentity(record), record.stage]));\n if (notifyChanges && rootProcess) {\n for (const record of sessionRecords) {\n if (record.stage === RUNNING_STATUS) continue;\n const identity = runIdentity(record);\n const previous = previousStages.get(identity);\n const startedAt = Date.parse(record.startedAt);\n const startedAfterBaseline =\n previous === undefined &&\n (sessionStartedAt === 0 || !Number.isFinite(startedAt) || startedAt >= sessionStartedAt);\n if (!deliveredTerminalRuns.has(identity) && (previous === RUNNING_STATUS || startedAfterBaseline)) {\n pendingTerminalRuns.set(identity, record);\n }\n }\n }\n\n if (notifyChanges) {\n for (const record of sessionRecords) {\n const previous = previousStages.get(runIdentity(record));\n if (previous !== RUNNING_STATUS || record.stage === RUNNING_STATUS) continue;\n const completed = record.stage === COMPLETED_STATUS;\n const detail = record.failedJob ? ` at ${record.failedJob}` : '';\n ctx.ui.notify(\n completed\n ? `${GLYPH.completed} ${record.runKey} completed`\n : `${GLYPH.failed} ${record.runKey} ${stageLabel(record)}${detail}`,\n completed ? 'info' : 'warning',\n );\n void activeTelemetry.recordEvent('doom_workflow.run_transition', {\n outcome: completed ? 'completed' : stageLabel(record),\n });\n }\n }\n\n if (rootProcess && pendingTerminalRuns.size > 0) {\n try {\n const terminalRecords = [...pendingTerminalRuns.values()];\n const summaries = await Promise.all(\n terminalRecords.map(async (record) => {\n if (record.stage === COMPLETED_STATUS) return finishedRunSummary(record, []);\n // A failure needs the job tree to say which job died. The\n // transitions pushed while the run was going are already in\n // hand; only a run that finished before this session began\n // watching has nothing accumulated and has to be read.\n const identity = runIdentity(record);\n const events =\n runEvents.get(identity) ??\n progressEventsForRun(record, await readWorkflowProgress(registry.runDirectoryFor(record)));\n return finishedRunSummary(record, summarizeWorkflowProgress(events));\n }),\n );\n pi.sendMessage(\n {\n customType: WORKFLOW_FINISHED_MESSAGE,\n content: summaries.join('\\n\\n'),\n display: true,\n details: { runIds: terminalRecords.map((record) => record.runId ?? runIdentity(record)) },\n },\n { triggerTurn: true, deliverAs: 'steer' },\n );\n for (const record of terminalRecords) {\n const identity = runIdentity(record);\n deliveredTerminalRuns.add(identity);\n pendingTerminalRuns.delete(identity);\n // Its summary has been delivered, so the accumulated transitions\n // can never be needed again.\n forgetRun(identity);\n }\n } catch (error) {\n await activeTelemetry.recordError('doom_workflow.terminal_delivery_failed', error, {\n 'workflow.pending_terminal_count': pendingTerminalRuns.size,\n });\n }\n }\n\n previousStages = nextStages;\n void activeTelemetry.recordEvent('doom_workflow.monitor_finished', {\n duration_ms: Date.now() - startedAt,\n outcome: 'completed',\n });\n } catch (error) {\n await activeTelemetry.recordError('doom_workflow.monitor_failed', error, {\n duration_ms: Date.now() - startedAt,\n });\n ctx.ui.setStatus(STATUS_KEY, undefined);\n ctx.ui.notify(\n `Workflow status refresh failed: ${error instanceof Error ? error.message : String(error)}`,\n 'warning',\n );\n } finally {\n monitorRefreshActive = false;\n }\n };\n\n const clearFollow = (ctx: ExtensionContext): void => {\n if (followTimer) clearInterval(followTimer);\n followTimer = undefined;\n followedRun = undefined;\n ctx.ui.setWidget(FOLLOW_WIDGET_KEY, undefined);\n };\n\n const readLauncherScreen = async (record: WorkflowRunRecord): Promise<string[]> => {\n const launcher = record.launcher;\n if (!launcher) return [NO_LAUNCHER_RECORDED];\n if (launcher.type === LAUNCHER_TMUX) {\n const target = launcher.paneId ?? launcher.sessionId ?? launcher.sessionName;\n const result = await pi.exec(LAUNCHER_TMUX, ['capture-pane', '-p', '-t', target, '-S', `-${TAIL_LINES}`], {\n timeout: PI_EXEC_TIMEOUT_MS,\n });\n if (result.code !== 0) {\n return [`Unable to read ${record.runKey}: ${result.stderr.trim() || `tmux exited ${result.code}`}`];\n }\n return result.stdout.trimEnd().split('\\n').slice(-TAIL_LINES);\n }\n\n if (launcher.type === LAUNCHER_NATIVE) {\n // Hosted on a PTY inside a process, with no durable session to read\n // back. Reading it belongs to whichever process owns that terminal.\n return ['This run is hosted natively and has no launcher screen to read.'];\n }\n\n const result = await pi.exec(\n LAUNCHER_CMUX,\n ['read-screen', '--workspace', launcher.workspaceId, '--scrollback', '--lines', String(TAIL_LINES)],\n { timeout: PI_EXEC_TIMEOUT_MS },\n );\n if (result.code !== 0) {\n return [`Unable to read ${record.runKey}: ${result.stderr.trim() || `cmux exited ${result.code}`}`];\n }\n return result.stdout.trimEnd().split('\\n').slice(-TAIL_LINES);\n };\n\n /**\n * The last screen read for each run, and any read still in flight.\n *\n * Every surface that shows live output scrapes the multiplexer by forking a\n * CLI, measured at ~75ms a call. The run panel repaints five times a second,\n * which is the right cadence for a panel and far too fast for a fork: it\n * spends a third of its life inside a child process, and the moment one read\n * runs long the next tick starts another on top of it, unbounded.\n *\n * Two runs are never scraped concurrently for the same key, and no key is\n * scraped faster than the interval, however many surfaces are watching.\n */\n const launcherScreens = new Map<string, { at: number; inFlight?: Promise<string[]>; lines: string[] }>();\n\n /**\n * A run's recent output, reusing a recent read rather than forking again.\n *\n * Rendering stays as fast as it ever was: the caller gets the cached lines\n * immediately and the next repaint picks up whatever the refresh found.\n */\n const cachedLauncherScreen = async (record: WorkflowRunRecord): Promise<string[]> => {\n const identity = runIdentity(record);\n const entry = launcherScreens.get(identity) ?? { at: 0, lines: [] };\n launcherScreens.set(identity, entry);\n if (entry.inFlight) return entry.lines;\n if (Date.now() - entry.at < SCREEN_REFRESH_MS) return entry.lines;\n\n entry.inFlight = readLauncherScreen(record);\n try {\n entry.lines = await entry.inFlight;\n } finally {\n // Stamped after the read, not before: the interval is a gap between\n // reads, so a slow scrape must not immediately earn another one.\n entry.at = Date.now();\n entry.inFlight = undefined;\n }\n return entry.lines;\n };\n\n /** Runs whose screens can never change again cannot earn their memory back. */\n const forgetLauncherScreens = (live: Set<string>): void => {\n for (const identity of launcherScreens.keys()) {\n if (!live.has(identity)) launcherScreens.delete(identity);\n }\n };\n\n /**\n * Bring a run's launcher to the foreground.\n *\n * Returns a status line rather than notifying, because its only caller is now\n * the `workflow_run` tool's `open` action and the agent needs to read the outcome.\n */\n const openLauncher = async (record: WorkflowRunRecord): Promise<string> => {\n const launcher = record.launcher;\n if (!launcher) return NO_LAUNCHER_RECORDED;\n if (launcher.type === LAUNCHER_NATIVE) {\n return 'This run is hosted natively rather than in a multiplexer, so there is no separate window to bring forward.';\n }\n if (launcher.type === LAUNCHER_CMUX) {\n await pi.exec(LAUNCHER_CMUX, ['select-workspace', '--workspace', launcher.workspaceId], {\n timeout: PI_EXEC_TIMEOUT_MS,\n });\n return `Opened ${record.runKey} in cmux.`;\n }\n if (!process.env.TMUX) {\n return 'Pi is not attached to tmux. Follow the output instead, or attach to the recorded tmux session manually.';\n }\n const target = launcher.sessionId ?? launcher.sessionName;\n const result = await pi.exec(LAUNCHER_TMUX, ['switch-client', '-t', target], { timeout: PI_EXEC_TIMEOUT_MS });\n if (result.code !== 0) return result.stderr.trim() || `Unable to open tmux session ${target}.`;\n if (launcher.paneId) {\n await pi.exec(LAUNCHER_TMUX, ['select-pane', '-t', launcher.paneId], { timeout: PI_EXEC_TIMEOUT_MS });\n }\n return `Opened ${record.runKey} in tmux.`;\n };\n\n const renderFollow = async (record: WorkflowRunRecord, ctx: ExtensionContext): Promise<void> => {\n if (followRefreshActive) return;\n followRefreshActive = true;\n try {\n // Prefer the run's own progress log: it says which job and step are in\n // flight, which a scraped screen cannot. Runs started before progress\n // recording existed, and any run whose log is not readable, still get\n // the terminal tail.\n const jobs = summarizeWorkflowProgress(\n progressEventsForRun(record, await readWorkflowProgress(registry.runDirectoryFor(record))),\n );\n const lines = jobs.length > 0 ? undefined : await cachedLauncherScreen(record);\n // Component factory rather than a string array, for two reasons the\n // string form cannot give us: render() receives the terminal width, so\n // launcher output can be fitted instead of overflowing, and the theme is\n // applied fresh on every render, so a theme switch repaints correctly\n // without any cached colours to invalidate.\n ctx.ui.setWidget(\n FOLLOW_WIDGET_KEY,\n (_tui, theme) => ({\n render: (width: number) =>\n renderFollowLines(record, lines ?? renderProgressLines(jobs, theme, width), theme, width),\n invalidate: () => {\n // Nothing cached: render() rebuilds from the current theme.\n },\n }),\n { placement: 'aboveEditor' },\n );\n } finally {\n followRefreshActive = false;\n }\n };\n\n const followRun = async (record: WorkflowRunRecord, ctx: ExtensionContext): Promise<void> => {\n clearFollow(ctx);\n followedRun = record;\n await renderFollow(record, ctx);\n followTimer = setInterval(() => {\n if (followedRun) void renderFollow(followedRun, ctx);\n }, followIntervalMs);\n followTimer.unref?.();\n };\n\n /**\n * Forward literal text to a run's terminal.\n *\n * `tmux send-keys -l` and `cmux send` both take text verbatim, which keeps\n * escape sequences intact: an arrow key must arrive as one sequence, not as\n * a bracket and a letter. A natively hosted run has no multiplexer to\n * address, so it reports rather than silently dropping the keystrokes.\n */\n const sendToRun = async (record: WorkflowRunRecord, text: string): Promise<void> => {\n const launcher = record.launcher;\n if (!launcher || launcher.type === LAUNCHER_NATIVE) return;\n if (launcher.type === LAUNCHER_TMUX) {\n const target = launcher.paneId ?? launcher.sessionId ?? launcher.sessionName;\n await pi.exec(LAUNCHER_TMUX, ['send-keys', '-t', target, '-l', text], { timeout: PI_EXEC_TIMEOUT_MS });\n return;\n }\n await pi.exec(LAUNCHER_CMUX, ['send', '--workspace', launcher.workspaceId, text], {\n timeout: PI_EXEC_TIMEOUT_MS,\n });\n };\n\n /**\n * Show a run over Pi: job tree, live output, and a keyboard that reaches it.\n *\n * Blocks until the user closes it, which is what makes it feel like the run\n * rather than a report about the run. The refresh runs faster than the\n * background widget because a foregrounded terminal that lags behind your\n * own typing reads as broken.\n */\n /**\n * Show a run over Pi as a persistent panel, not a modal.\n *\n * Returns as soon as the panel is up rather than awaiting its close. An\n * awaited overlay would hold the tool call open, which freezes the agent\n * mid-turn and makes \"switch back to the main agent\" impossible: there is\n * nothing to switch back to while it is still waiting for a tool result.\n *\n * Focus, not lifetime, is what the user toggles. The panel stays on screen\n * while typing goes back to Pi, so Escape stays free for the run's own\n * agent, which uses it to interrupt.\n */\n const openOverlay = async (record: WorkflowRunRecord, ctx: ExtensionContext): Promise<string> => {\n closeOverlay();\n // Exactly the launchers `sendToRun` can address. A natively hosted run has\n // no multiplexer, so its panel is a view rather than a terminal, and\n // saying so up front beats letting the user type into nothing.\n const interactive = record.launcher?.type === LAUNCHER_TMUX || record.launcher?.type === LAUNCHER_CMUX;\n const input = new TerminalInputBatcher((text) => sendToRun(record, text));\n let jobs: WorkflowProgressJob[] = [];\n let output: string[] = [];\n let refresh: NodeJS.Timeout | undefined;\n\n let polling = false;\n const poll = async (): Promise<void> => {\n jobs = summarizeWorkflowProgress(\n progressEventsForRun(record, await readWorkflowProgress(registry.runDirectoryFor(record))),\n );\n output = await cachedLauncherScreen(record);\n };\n await poll();\n\n const teardown = (): void => {\n if (refresh) clearInterval(refresh);\n refresh = undefined;\n input.dispose();\n };\n\n const entry: ActiveOverlay = { interactive, runKey: record.runKey, teardown };\n activeOverlay = entry;\n\n /** Shared by the panel footer, the close chord, and the double-Escape failsafe. */\n const closeWithNotice = (): void => {\n closeOverlay();\n ctx.ui.notify(`Closed the ${record.runKey} view. The run keeps going.`, 'info');\n };\n\n // Deliberately not awaited: the panel outlives this tool call.\n void ctx.ui.custom<void>(\n (tui, theme, _keybindings, done) => {\n // Guarded, because a tick that outlives its interval would otherwise\n // start another read on top of the last one and keep doing so. The\n // progress log is a local file and the screen read is coalesced, so a\n // skipped tick costs nothing: the next one repaints from the same state.\n refresh = setInterval(() => {\n if (polling) return;\n polling = true;\n void poll().finally(() => {\n polling = false;\n tui.requestRender();\n });\n }, OVERLAY_REFRESH_MS);\n refresh.unref?.();\n\n return new WorkflowRunPanelComponent(tui, theme, {\n runKey: record.runKey,\n label: `${GLYPH.running} ${runLabel(record)}`,\n breadcrumb: RUN_PANEL_BREADCRUMB,\n interactive,\n footer: panelHint(interactive),\n hints: panelHints(interactive),\n snapshot: (width) => ({ progress: renderProgressLines(jobs, theme, width), output }),\n onUnfollow: () => {\n entry.handle?.unfocus();\n ctx.ui.notify('Typing goes to this session. The run keeps going.', 'info');\n },\n onClose: closeWithNotice,\n sendInput: (data) => input.write(data),\n // `flush` takes the pending bytes synchronously and sends them in the\n // background, so the panel still closes on the same keystroke.\n // Without it the run would be interrupted or not depending on whether\n // the user out-typed a 16ms timer, which is not a behaviour anyone can\n // predict or rely on.\n flushInput: () => void input.flush(),\n unfollowShortcut: UNFOLLOW_SHORTCUT,\n closeShortcut: CLOSE_OVERLAY_SHORTCUT,\n onDispose: () => {\n teardown();\n if (activeOverlay === entry) activeOverlay = undefined;\n done();\n },\n });\n },\n {\n ...runPanelUiOptions(interactive),\n onHandle: (handle) => {\n entry.handle = handle;\n },\n },\n );\n\n // Told to the user directly, not only through the tool result. The result\n // is addressed to the agent, and the panel's own footer is one dim line\n // that a user watching a run scroll past can easily miss. A view you\n // cannot work out how to leave is the thing to avoid here.\n ctx.ui.notify(\n interactive\n ? `${record.runKey}: typing goes to the run. Press Esc twice to close, ${shortcutLabel(UNFOLLOW_SHORTCUT)} to type here instead.`\n : `${record.runKey}: view only, because this run is hosted natively. ${shortcutLabel(CLOSE_OVERLAY_SHORTCUT)} closes it.`,\n 'info',\n );\n\n if (!interactive) {\n return [\n `Showing ${record.runKey} over Pi, as a view only:`,\n 'the run is hosted natively rather than in a multiplexer, so there is no terminal to type into.',\n `Typing stays with this session, and ${shortcutLabel(CLOSE_OVERLAY_SHORTCUT)} closes the view.`,\n ].join(' ');\n }\n\n return [\n `Showing ${record.runKey} over Pi. Typing goes to the run.`,\n 'Pressing Escape twice quickly closes the view, and a single Escape still reaches the run.',\n `${shortcutLabel(UNFOLLOW_SHORTCUT)} moves typing between the run and this session,`,\n `and ${shortcutLabel(CLOSE_OVERLAY_SHORTCUT)} closes the view. The run keeps going either way.`,\n ].join(' ');\n };\n\n const openInspector = async (ctx: ExtensionContext, manage = false): Promise<void> => {\n if (inspectorOpen || activeOverlay || followedRun) return;\n inspectorOpen = true;\n try {\n const selection = await ctx.ui.custom<WorkflowInspectorSelection | undefined>(\n (tui, theme, _keybindings, done) =>\n new WorkflowInspectorComponent(\n tui,\n theme,\n {\n list: async () => {\n const sessionId = currentSessionId();\n const records = (await listAllRuns(registry)).filter(\n (record) => record.stage === RUNNING_STATUS && !record.stale && isSessionRun(record, sessionId),\n );\n // Kept so `output` can address the run under the cursor without\n // paging the whole registry a second time on every refresh.\n inspectorRecords = new Map(records.map((record) => [runIdentity(record), record]));\n return Promise.all(\n records.map(async (record) => ({\n displayName: record.displayName,\n executionState: record.executionState,\n jobs: summarizeWorkflowProgress(\n progressEventsForRun(record, await readWorkflowProgress(registry.runDirectoryFor(record))),\n ),\n key: runIdentity(record),\n output: [],\n runKey: record.runKey,\n startedAt: record.startedAt,\n workspace: record.workspace,\n })),\n );\n },\n output: async (item) => {\n const record = inspectorRecords.get(item.key);\n return record ? cachedLauncherScreen(record) : ['Workflow is no longer active.'];\n },\n },\n done,\n ),\n {\n ...DOOM_FULLSCREEN_UI_OPTIONS,\n onHandle: (handle) => {\n inspectorHandle = handle;\n },\n },\n );\n if (selection) {\n const record = await requireRun(selection);\n if (manage) await manageWorkflow(record, ctx);\n else await openOverlay(record, ctx);\n }\n } finally {\n inspectorHandle = undefined;\n inspectorOpen = false;\n inspectorRecords.clear();\n }\n };\n\n /**\n * Resolve a run this session owns, erroring rather than picking blindly.\n *\n * A run another session launched is reported exactly like one that does not\n * exist. Distinguishing them would leak the shared registry's contents into\n * a session that has no way to act on them anyway, and would invite the\n * agent to keep trying keys it can never use.\n */\n const requireRun = async (\n input: { runKey: string; workspace?: string },\n sessionId: string | undefined = currentSessionId(),\n ): Promise<WorkflowRunRecord> => {\n observeSession(sessionId);\n const records = await listAllRuns(registry, input.workspace);\n const record = records.find((candidate) => candidate.runKey === input.runKey);\n if (!record || !isSessionRun(record, sessionId)) {\n throw new Error(\n withOptions(`No workflow run matches ${JSON.stringify(input.runKey)} in this session.`, [\n 'workflow_run with action status: use a run key from this session before retrying.',\n 'Tell the user this session did not launch that run rather than guessing a similar key. A run launched by another Pi session, or from the command line, is managed there or with the workflow-mcp CLI.',\n ]),\n );\n }\n return record;\n };\n\n /**\n * Recover a run in a launcher of its own instead of replaying it here.\n *\n * `RecoverWorkflowService` replays with the launch step skipped, so an\n * in-process recovery runs every job inside this process. A workflow whose\n * steps are `interactiveRun` then has its agent spawned with inherited\n * stdio, because that agent needs a real TTY to draw. Inside Pi that TTY is\n * the TUI: the agent draws over it, and the reset sequence written when the\n * step ends is one Pi never issued and cannot repaint from. Delegating\n * costs a process and keeps the screen.\n *\n * Undefined leaves the caller on the in-process path: a workflow that\n * declares no `launch-command` has no launcher to delegate to, and a\n * definition that cannot be read is better diagnosed by the replay itself.\n */\n const delegateRecovery = async (\n input: { runKey: string; workspace?: string; runner?: string },\n ctx: ExtensionContext,\n ): Promise<string | undefined> => {\n const cli = resolveWorkflowCli();\n if (!cli) return undefined;\n const record = await requireRun(input, resolveRootSessionId(ctx.sessionManager.getSessionId()));\n let workflow: Workflow;\n try {\n workflow = parser.parseWorkflowFile(record.workflowPath);\n } catch {\n return undefined;\n }\n if (!workflow['launch-command']) return undefined;\n\n // The recovered run inherits the failed record's env, and that env\n // carries the session stamp `requireRun` just proved is ours, so the\n // delegated run stays owned by this session without stamping anything.\n const cwd = record.originalRepoPath ?? ctx.cwd;\n const recoverCommand = shellCommand([\n process.execPath,\n cli,\n RECOVER_COMMAND,\n record.runKey,\n '--workspace',\n record.workspace,\n ...(input.runner ? ['--runner', input.runner] : []),\n ]);\n const launcher = preferredLauncher();\n spawnDetached(\n process.execPath,\n [\n cli,\n LAUNCH_PROCESS_COMMAND,\n ...(launcher ? ['--launcher', launcher] : []),\n '--name',\n `${record.displayName} recover`,\n '--cwd',\n cwd,\n '--command',\n recoverCommand,\n ],\n { cwd, env: launcherEnvironment() },\n );\n await getTelemetry(ctx).recordEvent('doom_workflow.recover_delegated', {\n launcher: launcher ?? 'auto',\n outcome: 'delegated',\n });\n return `Recovering ${record.runKey} in a launcher of its own, so this session keeps its terminal. It reports back here when it ends.`;\n };\n\n const manageWorkflow = async (selected: WorkflowRunRecord, ctx: ExtensionContext): Promise<void> => {\n let record = selected;\n while (true) {\n const actions = ['Open output'];\n const state = record.executionState ?? RUNNING_STATUS;\n if (state === RUNNING_STATUS) actions.push('Pause');\n if (state === PAUSED_EXECUTION_STATE) actions.push('Resume');\n if (record.stage === RUNNING_STATUS) actions.push('Stop');\n actions.push('Back');\n const action = await openWorkflowChoice(ctx, {\n title: `Manage ${record.runKey}`,\n breadcrumb: MANAGE_BREADCRUMB,\n choices: actions,\n });\n if (!action || action === 'Back') return;\n\n record = await requireRun(record);\n if (record.stage !== RUNNING_STATUS) {\n ctx.ui.notify(`${record.runKey} is no longer running.`, 'warning');\n return;\n }\n if (action === 'Open output') {\n await openOverlay(record, ctx);\n return;\n }\n if (action === 'Stop') {\n const confirmed = await ctx.ui.confirm(\n `Stop ${record.runKey}?`,\n 'The workflow will be interrupted and will not be restarted automatically.',\n );\n if (!confirmed) continue;\n record = await requireRun(record);\n if (record.stage !== RUNNING_STATUS) {\n ctx.ui.notify(`${record.runKey} is no longer running.`, 'warning');\n return;\n }\n }\n if (action !== 'Pause' && action !== 'Resume' && action !== 'Stop') continue;\n const result = await controlTool.execute({\n action: action.toLowerCase() as 'pause' | 'resume' | 'stop',\n ...(action === 'Stop' ? { reason: 'Stopped from the workflow manager.' } : {}),\n runKey: record.runKey,\n workspace: record.workspace,\n });\n if (result.isError) throw new Error(toolResultText(result) || `Unable to ${action.toLowerCase()} workflow.`);\n ctx.ui.notify(toolResultText(result) || `${action} requested for ${record.runKey}.`, 'info');\n await refreshStatus(ctx, false);\n record = await requireRun(record);\n }\n };\n\n /**\n * The wizard's prompts, backed by doom overlays instead of Pi's native\n * widgets, so a launch reads as one framed sequence rather than three styles.\n *\n * `editor` stays with Pi: the workflow prompt is multi-line authoring, and\n * reimplementing a text editor to match a frame would be a poor trade.\n */\n const doomLauncherUi = (ctx: ExtensionContext, breadcrumb: string): WorkflowLauncherUi => ({\n editor: (title, prefill) => ctx.ui.editor(title, prefill),\n input: (title, placeholder) =>\n openWorkflowInput(ctx, { title, breadcrumb, ...(placeholder ? { fallback: placeholder } : {}) }),\n notify: (message, type) => ctx.ui.notify(message, type),\n select: (title, options) => openWorkflowChoice(ctx, { title, breadcrumb, choices: options }),\n });\n\n const openWorkflowLaunch = async (ctx: ExtensionContext): Promise<void> => {\n if (!ctx.hasUI) {\n ctx.ui.notify('Workflow launch needs an interactive Pi session.', 'warning');\n return;\n }\n const result = await openWorkflowLaunchWizard(\n {\n compatibleRunners,\n cwd: ctx.cwd,\n launch: (input, launchContext) => launchExecutor.execute(input, launchContext),\n listWorkflowsTool: feature.listWorkflowsTool,\n parseWorkflow: (workflowPath) => parser.parseWorkflowFile(workflowPath),\n pickWorkflow: (entries) =>\n openWorkflowPickerOverlay(\n ctx,\n {\n title: 'WORKFLOW LAUNCHER',\n breadcrumb: LAUNCH_BREADCRUMB,\n unit: 'workflows',\n action: 'launch',\n filterPlaceholder: 'type to filter by name, path or tag',\n emptyMessage: 'No workflow definitions were found in this repository.',\n },\n entries.map((entry) => ({\n key: entry.path,\n name: entry.name,\n detail: entry.relativePath,\n search: [entry.description, ...entry.tags].join(' '),\n value: entry,\n })),\n ),\n ui: doomLauncherUi(ctx, LAUNCH_BREADCRUMB),\n },\n ctx,\n );\n // Trimmed, because a launch that ran in this process answers with the\n // engine's whole console log, and notifying that verbatim paints tens of\n // lines of banner and job tree over the transcript.\n if (result) ctx.ui.notify(launchNotice(toolResultText(result)), 'info');\n };\n\n const openRecoveryHandoff = async (ctx: ExtensionContext): Promise<void> => {\n const sessionId = currentSessionId();\n const records = (await listAllRuns(registry)).filter(\n (record) => record.stage === ERROR_STAGE && isSessionRun(record, sessionId),\n );\n if (records.length === 0) {\n ctx.ui.notify('No failed workflows belong to this session.', 'info');\n return;\n }\n const chosen = await openWorkflowPickerOverlay(\n ctx,\n {\n title: 'WORKFLOW RECOVERY',\n breadcrumb: RECOVER_BREADCRUMB,\n unit: 'failed runs',\n action: 'recover',\n filterPlaceholder: 'type to filter by name, run key or failed job',\n emptyMessage: 'No failed workflows belong to this session.',\n },\n records.map((record) => ({\n key: `${record.workspace}/${record.runKey}`,\n name: record.displayName,\n detail: `${record.workspace}/${record.runKey}${record.failedJob ? ` · ${record.failedJob}` : ''}`,\n ...(record.failedJob ? { search: record.failedJob } : {}),\n value: record,\n })),\n );\n if (!chosen) return;\n const record = await requireRun(chosen);\n if (record.stage !== ERROR_STAGE) throw new Error(`${record.runKey} is no longer failed.`);\n const skillFile = resolve(\n dirname(fileURLToPath(import.meta.url)),\n SKILL_RELATIVE_PATH,\n 'workflow-recovery',\n 'SKILL.md',\n );\n if (!existsSync(skillFile)) throw new Error(`Workflow recovery skill is unavailable at ${skillFile}.`);\n setWorkflowMode(true);\n ctx.ui.notify(\n 'Workflow tools enabled. The agent will investigate this failure and choose the safe next step.',\n 'info',\n );\n pi.sendUserMessage(\n [\n 'Investigate recovery for this failed workflow run. This selection is permission to investigate, not permission to recover blindly.',\n `Run identity: ${JSON.stringify({ runKey: record.runKey, workspace: record.workspace })}`,\n `Read the package-owned recovery skill with the read tool at ${JSON.stringify(skillFile)} before acting.`,\n 'Inspect status, every relevant log source, process state, the staged run.json, and only the repair referenced by activeRepairId.',\n 'Choose exactly one outcome: recover, launch fresh, or defer. Never edit issue.md or repair.json, never scan historical repairs, and never pass recovery dryRun or a job override.',\n 'Do not recover terminal, approval-pending, blocked, or otherwise unsafe work. If evidence is incomplete, defer and explain what is missing.',\n 'If recovery is justified, preserve the recorded worktree and repair metadata, then verify real process and registry progress before reporting success.',\n ].join('\\n'),\n { deliverAs: 'followUp' },\n );\n };\n\n /**\n * Turn workflow mode on or off.\n *\n * `setActiveTools` is a whole-list setter, so this is always a\n * read-modify-write against `getActiveTools()`. Passing a bare literal would\n * silently deactivate every other extension's tools.\n *\n * Activating also injects each tool's `promptSnippet` and `promptGuidelines`\n * into the system prompt, and deactivating strips them: in Pi, tool gating\n * and instruction injection are the same lever.\n */\n const setWorkflowMode = (enabled: boolean): void => {\n const active = new Set(pi.getActiveTools());\n for (const name of WORKFLOW_PI_TOOL_NAMES) {\n if (enabled) active.add(name);\n else active.delete(name);\n }\n pi.setActiveTools([...active]);\n workflowMode = enabled;\n // Published here rather than in refreshStatus so the label lands with the\n // toggle: the status refresh is an async registry read, and a label that\n // waits for it looks like the command did nothing.\n publishModeWidget();\n options.onModeChange?.(enabled);\n };\n\n /**\n * What the user sees on every mode change.\n *\n * Leads with the state, then the one thing they can do next. Turning the\n * mode on is useless without knowing that you now just ask in plain\n * language, so that sentence earns its place; the tool count does not, and\n * is left to the README.\n */\n const modeSummary = (): string =>\n workflowMode\n ? [\n `${GLYPH.completed} Workflow mode on`,\n 'Ask for workflows in plain language: the agent can list, launch, inspect, pause, resume, stop, and recover runs.',\n `${shortcutLabel(UNFOLLOW_SHORTCUT)} stops following a run without stopping it.`,\n ].join('\\n')\n : [\n 'Workflow mode off',\n `Press ${LEADER_MODE_BREADCRUMB} to give the agent its workflow tools. Runs already going are unaffected.`,\n ].join('\\n');\n\n /**\n * Toggle the mode from the leader menu.\n *\n * Replaces the `/workflow [on|off|status]` command: the mode line already\n * answers `status` at a glance, and the other two arguments were a keyboard\n * path to a switch the leader menu spells out. Plan mode is reached the same\n * way, so the two modes are no longer toggled through different surfaces.\n */\n const applyWorkflowMode = async (enabled: boolean, ctx: ExtensionContext): Promise<void> => {\n setWorkflowMode(enabled);\n if (!workflowMode) clearFollow(ctx);\n ctx.ui.notify(modeSummary(), 'info');\n // Both directions: turning off has to clear the indicator, not just leave\n // the last one on screen until the monitor next fires.\n await refreshStatus(ctx, false);\n };\n\n // Zero palette cost, unlike a command. Guarded on an active follow so it\n // does not swallow the key when there is nothing to dismiss.\n pi.registerShortcut(UNFOLLOW_SHORTCUT, {\n description: 'Open workflows or move typing between a workflow run and this session',\n handler: async (ctx) => {\n // With a panel up this is a focus toggle, not a dismissal: the run stays\n // visible either way, and the user is only choosing who receives keys.\n const handle = activeOverlay?.handle;\n if (handle) {\n // pi-tui's `focus()` does not honour `nonCapturing`, so a view-only\n // panel would take the keyboard if this were left to fall through,\n // and every key after it would vanish into a run that cannot hear it.\n if (!activeOverlay?.interactive) {\n ctx.ui.notify('This run is hosted natively, so the view takes no typing. Your keys stay here.', 'info');\n return;\n }\n if (handle.isFocused()) {\n handle.unfocus();\n ctx.ui.notify('Typing goes to this session. The run keeps going.', 'info');\n } else {\n handle.focus();\n ctx.ui.notify(`Typing goes to ${activeOverlay?.runKey}.`, 'info');\n }\n return;\n }\n if (followedRun) {\n clearFollow(ctx);\n ctx.ui.notify('Workflow continues in the background.', 'info');\n return;\n }\n await openInspector(ctx);\n },\n });\n\n // Dismissing is its own key because the panel cannot use Escape: a focused\n // panel forwards every key to the run so its agent stays interruptible.\n pi.registerShortcut(CLOSE_OVERLAY_SHORTCUT, {\n description: 'Close the workflow run view',\n handler: (ctx) => {\n // Guarded so the key falls through to Pi when no panel is up.\n if (!activeOverlay) return;\n const runKey = activeOverlay.runKey;\n closeOverlay();\n ctx.ui.notify(`Closed the ${runKey} view. The run keeps going.`, 'info');\n },\n });\n\n // Guarded because the skill ships as a separate `files` entry from dist: if\n // packaging ever drops it, hand Pi nothing rather than a path that does not\n // resolve, which would otherwise fail quietly at discovery.\n pi.on('resources_discover', () => {\n const skillPath = resolve(dirname(fileURLToPath(import.meta.url)), SKILL_RELATIVE_PATH);\n return { skillPaths: existsSync(skillPath) ? [skillPath] : [] };\n });\n\n // Pi keeps no record of which extension contributed a skill path, so the\n // same directory is declared again here for the surfaces that attribute a\n // skill to the package that ships it.\n const declaredSkillPath = resolve(dirname(fileURLToPath(import.meta.url)), SKILL_RELATIVE_PATH);\n if (existsSync(declaredSkillPath)) {\n registerDoomSkillSource(pi, { source: WORKFLOW_SKILL_SOURCE, directories: [declaredSkillPath] });\n }\n\n pi.registerMessageRenderer(WORKFLOW_STEP_MESSAGE, (message, { outputPad }, theme) => {\n const details = isWorkflowStepMessageDetails(message.details) ? message.details : undefined;\n const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);\n return renderWorkflowStepMessage(details, content, outputPad, theme);\n });\n\n registerWorkflowPiTools(pi, {\n runTool,\n recoverTool,\n launchExecutor,\n controlTool,\n requireSessionRun: requireRun,\n delegateRecovery,\n trackPendingRun,\n observeSession,\n activeRunCount: async () => sessionRunCount,\n onLaunch: async (ctx) => {\n await getTelemetry(ctx).recordEvent(LAUNCH_REQUESTED_EVENT, { outcome: 'requested' });\n return refreshStatus(ctx, false);\n },\n followRun: async (input, ctx) => {\n const record = await requireRun(input, ctx.sessionManager.getSessionId());\n await getTelemetry(ctx).recordEvent('doom_workflow.follow_started', { outcome: 'started' });\n await followRun(record, ctx);\n return `Following ${runLabel(record)}. Press ${shortcutLabel(UNFOLLOW_SHORTCUT)} to stop following; the run keeps going.`;\n },\n tailRun: async (input, ctx) => {\n const record = await requireRun(input, ctx.sessionManager.getSessionId());\n await getTelemetry(ctx).recordEvent('doom_workflow.tail_requested', { outcome: 'requested' });\n return [`Workflow: ${runLabel(record)}`, ...(await readLauncherScreen(record))].join('\\n');\n },\n openRun: async (input, ctx) => {\n const record = await requireRun(input, ctx.sessionManager.getSessionId());\n await getTelemetry(ctx).recordEvent('doom_workflow.open_requested', { outcome: 'requested' });\n // Without a UI there is nothing to foreground, so fall back to handing\n // the user's own terminal to the launcher.\n if (!ctx.hasUI) return openLauncher(record);\n return openOverlay(record, ctx);\n },\n rejectRunner,\n });\n\n const piContext = (ctx: DoomExtensionContext): ExtensionContext => ctx as ExtensionContext;\n registerDoomLeaderActionHandlers(pi, {\n source: '@agimon-ai/doompi-workflow',\n handlers: {\n [LEADER_ENABLE_ACTION]: (ctx) => applyWorkflowMode(true, piContext(ctx)),\n [LEADER_DISABLE_ACTION]: (ctx) => applyWorkflowMode(false, piContext(ctx)),\n [LEADER_MANAGE_ACTION]: (ctx) => openInspector(piContext(ctx), true),\n [LEADER_LAUNCH_ACTION]: (ctx) => openWorkflowLaunch(piContext(ctx)),\n [LEADER_RECOVER_ACTION]: (ctx) => openRecoveryHandoff(piContext(ctx)),\n },\n onError: (error, _actionName, ctx) => {\n piContext(ctx).ui.notify(error instanceof Error ? error.message : String(error), 'error');\n },\n });\n\n pi.on('session_start', async (_event, ctx) => {\n const activeTelemetry = getTelemetry(ctx);\n await activeTelemetry.recordEvent('doom_workflow.session_started', {\n mode: process.env.WORKFLOW_MCP_MODE === 'on' ? 'on' : 'off',\n outcome: 'started',\n });\n // Tools registered during extension init are auto-activated by Pi, so the\n // default-off state has to be applied here rather than by not registering.\n //\n // WORKFLOW_MCP_MODE=on exists for non-interactive harnesses that steer the\n // agent at these tools without a human able to type /workflow. The doom-loop\n // dispatch prompt is exactly that case: it instructs the agent to launch with\n // the launch_workflow tool and relies on the tool's concurrency ceiling, which\n // the CLI path does not enforce.\n sessionCtx = ctx;\n const rootSessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n observeSession(rootSessionId);\n sessionStartedAt = Date.now();\n previousStages = new Map();\n pendingTerminalRuns.clear();\n deliveredTerminalRuns.clear();\n protocolRuntime = createProtocolRuntime({\n bus: pi.events,\n source: '@agimon-ai/doompi-workflow',\n sessionId: rootSessionId,\n });\n // `session_start` re-fires for the same session on reload, so every\n // subscription and registration below has to replace its predecessor\n // rather than stack on top of it. A leaked run provider keeps answering\n // background-work discovery with a snapshot nothing updates any more, and\n // the registry drops updates from any generation it did not register, so\n // the loop would read a frozen active count forever.\n disposeModeSubscription?.();\n disposeModeSubscription = protocolRuntime.onNotification(WorkflowModeChanged, ({ mode }) => {\n const enabled = mode === 'on';\n if (enabled === workflowMode) return;\n setWorkflowMode(enabled);\n void activeTelemetry.recordEvent('doom_workflow.mode_changed', { mode });\n void refreshStatus(ctx, false);\n });\n setWorkflowMode(process.env.WORKFLOW_MCP_MODE === 'on');\n runProvider?.dispose();\n runProvider = registerRunProvider(protocolRuntime);\n // Subscribed before the first status read, so a transition landing during\n // startup is delivered rather than raced past.\n await startRunControl(ctx);\n await refreshStatus(ctx, false);\n // A fallback now, not the mechanism: the observer reports a change within\n // its own debounce, and this only bounds how long a change nothing\n // notified us about stays invisible.\n if (monitorTimer) clearInterval(monitorTimer);\n monitorTimer = setInterval(() => void refreshStatus(ctx, true), monitorIntervalMs);\n monitorTimer.unref?.();\n });\n\n pi.on('session_shutdown', async (_event, ctx) => {\n // A workflow without a `launch-command` runs its engine inside this\n // process, so Pi exiting kills the orchestrator mid-step and leaves\n // run.json saying `running` behind a pid that is gone. Interrupting takes\n // the same path a Ctrl-C would: the run finalizes as interrupted before we\n // go, instead of waiting for some later reconcile to notice the corpse.\n //\n // Bounded, because a step ignoring SIGTERM must not hold Pi's exit\n // hostage. Losing that race just leaves today's stale record, which the\n // CLI still reconciles.\n if (pendingInlineRuns.size > 0) {\n for (const service of [runService, recoverService]) {\n service.interrupt('SIGTERM', { phase: 'workflow', source: 'pi-session-shutdown' });\n }\n await Promise.race([\n Promise.allSettled(pendingInlineRuns),\n new Promise((settle) => setTimeout(settle, SHUTDOWN_FINALIZE_TIMEOUT_MS).unref?.()),\n ]);\n }\n stopRunControl();\n if (monitorTimer) clearInterval(monitorTimer);\n monitorTimer = undefined;\n if (pulseTimer) clearInterval(pulseTimer);\n pulseTimer = undefined;\n footerInput = { runs: [], failed: 0 };\n footerFingerprint = undefined;\n footerContribution.update(undefined);\n runProvider?.dispose();\n runProvider = undefined;\n disposeModeSubscription?.();\n disposeModeSubscription = undefined;\n protocolRuntime = undefined;\n inspectorHandle?.hide();\n inspectorHandle = undefined;\n inspectorOpen = false;\n inspectorRecords.clear();\n launcherScreens.clear();\n closeOverlay();\n sessionRunCount = 0;\n clearFollow(ctx);\n ctx.ui.setStatus(STATUS_KEY, undefined);\n // The label must not outlive the session that turned the mode on.\n if (modeWidgetVisible) {\n modeWidgetVisible = false;\n modeContribution.update(undefined);\n }\n await telemetry?.recordEvent('doom_workflow.session_finished', {\n 'workflow.pending_count': pendingInlineRuns.size,\n outcome: pendingInlineRuns.size === 0 ? 'completed' : 'interrupted',\n });\n await telemetry?.shutdown();\n telemetry = undefined;\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function e(e=process.env){let t=Number(e.WORKFLOW_MCP_MAX_CONCURRENT);return Number.isInteger(t)&&t>0?t:5}function t(e,t){return t!==void 0&&e.env?.PI_SESSION_ID===t}function n(e,n){return e.filter(e=>t(e,n)&&e.stage===`running`)}function r(e){return[`Started ${e.displayName} in workspace ${e.workspace}.`,`Run key: ${e.runKey}`,`The run is registered and going. This call returned at that point rather than waiting for the launcher process to exit.`].join(`
|
|
2
|
+
`)}function i(e){return[`Launch handed off for ${e}, but no run has registered yet.`,`Nothing was cancelled: the launcher is still starting, and a run that registers later still reports back here when it finishes.`,`Do not launch it again. Ask the user to check the workflow footer if they need to see it sooner.`].join(`
|
|
3
|
+
`)}const a=new RegExp([`\\u001B\\[[0-9;?]*[ -/]*[@-~]`,`\\u001B\\][^\\u0007\\u001B]*(?:\\u0007|\\u001B\\\\)`,`\\u001B[@-Z\\\\-_]`].join(`|`),`g`),o=/^[\s─-╿]*$/u;function s(e,t=3){let n=e.replace(a,``).split(`
|
|
4
|
+
`).map(e=>e.trimEnd()).filter(e=>e.trim().length>0&&!o.test(e)).map(e=>e.length>100?`${e.slice(0,99)}…`:e);return n.length===0?`Workflow started.`:n.length<=t?n.join(`
|
|
5
|
+
`):[...n.slice(0,t),`… ${n.length-t} more lines, in the run's own output.`].join(`
|
|
6
|
+
`)}function c(e){return e.content.map(e=>e.type===`text`?e.text:``).filter(Boolean).join(`
|
|
7
|
+
`).trim()}function l(e,t,n=[]){let r=c(t)||`No output.`;if(t.isError)throw Error(u(r,n));return{content:[{type:`text`,text:r}],details:{tool:e}}}function u(e,t){return t.length===0?e:[e,``,`Options, put these to the user rather than picking one yourself:`,...t.map(d)].join(`
|
|
8
|
+
`)}function d(e){return`- ${e}`}function f(e,t){let n=t.find(e=>e.status===`failed`),r=n?.steps.find(e=>e.status===`failed`);return{job:n?.name??e.failedJob,step:r?.name,reason:r?.reason??n?.reason}}function p(e,t=[]){let n=e.stage===`completed`?`completed`:`ended in ${e.stage}`,r=f(e,t),i=[`Workflow run ${e.runKey} in workspace ${e.workspace} ${n}.`,e.workflowId?`Workflow: ${e.workflowId}`:void 0,r.job?`Failed job: ${r.job}${r.step?` (step: ${r.step})`:``}`:void 0,e.errorMessage?`Error: ${e.errorMessage}`:void 0,r.reason&&r.reason!==e.errorMessage?`Step reported: ${r.reason}`:void 0],a=e.env?.AGIFLOW_JOB_ID;a&&i.push(`Agiflow job: ${e.env?.AGIFLOW_JOB_KIND??`unknown`} ${a}`);let o=i.filter(e=>!!e).join(`
|
|
9
|
+
`);if(e.stage===`completed`)return o;if(e.outcome===`interrupted`)return u(o,[`workflow_run {"action":"recover","runKey":${JSON.stringify(e.runKey)}}: pick it up where it stopped.`,`Leave it stopped and move on.`]);let s=[e.errorMessage,r.reason].filter(Boolean).join(`
|
|
10
|
+
`);return s.includes(`JOB_ALREADY_CLAIMED`)?u(o,[`Report contention: another worker owns this job. Do not unlock, release, or retry the claim automatically.`]):s.includes(`WORKFLOW_NOT_OWNED`)||/release failed/i.test(s)?u(o,[`Inspect the current workflow ownership and ask the user before any release or unlock. Never force another worker’s lock.`]):/running workflow not found/i.test(s)?u(o,[`workflow_run {"action":"status","runKey":${JSON.stringify(e.runKey)}}: re-check current state before retrying.`,`Report the recorded terminal state when no running owner remains.`]):u(o,[`workflow_run {"action":"tail","runKey":${JSON.stringify(e.runKey)}}: read the run's own output first, when the error above does not explain the failure.`,`workflow_run {"action":"recover","runKey":${JSON.stringify(e.runKey)}}: resume from the failed job, keeping the work already done. Fits a transient or external cause.`,`Report the failure and stop, when it needs a code change or a decision you cannot make.`])}exports.PI_SESSION_ENV=`PI_SESSION_ID`,exports.finishedRunSummary=p,exports.isSessionRun=t,exports.launchHandoffSummary=i,exports.launchNotice=s,exports.launchedRunSummary=r,exports.resolveMaxConcurrent=e,exports.runsForSession=n,exports.toAgentToolResult=l,exports.toolResultText=c,exports.withOptions=u;
|
|
11
|
+
//# sourceMappingURL=piToolBridge.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piToolBridge.cjs","names":[],"sources":["../../src/workflow/piToolBridge.ts"],"sourcesContent":["/**\n * Pure bridging logic between MCP tool results and the Pi tool surface.\n *\n * DESIGN PATTERNS:\n * - Adapter pattern: MCP `CallToolResult` is converted to Pi's result shape\n * - Pure module: no service or tool-class imports, so this is unit-testable\n * without constructing the workflow execution stack\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported functions\n *\n * AVOID:\n * - Importing tool classes or services here (that belongs in `piTools.ts`)\n * - Returning an error flag Pi does not read; Pi tools signal failure by throwing\n */\n\nimport type { AgentToolResult } from '@earendil-works/pi-coding-agent';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { WorkflowProgressJob, WorkflowRunRecord } from '@agimon-ai/workflow-mcp';\n\n/**\n * Environment variable stamped onto every run launched from a Pi session.\n *\n * `RunWorkflowService` forwards `env` to the inner CLI invocation across the\n * `launch-command` delegation boundary and persists it on the run record, so\n * this survives backgrounding and identifies the launching session afterwards.\n */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\n/** Operator override for the per-session launch ceiling. */\nconst MAX_CONCURRENT_ENV = 'WORKFLOW_MCP_MAX_CONCURRENT';\nconst DEFAULT_MAX_CONCURRENT = 5;\n\nexport interface WorkflowToolDetails {\n tool: string;\n}\n\n/** Resolve the per-session launch ceiling from the environment. */\nexport function resolveMaxConcurrent(env: NodeJS.ProcessEnv = process.env): number {\n const parsed = Number(env[MAX_CONCURRENT_ENV]);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT;\n}\n\n/**\n * True when this session launched the run.\n *\n * Fails closed on a missing session id: an unstamped record (a CLI launch) and\n * a call arriving before the session id is known both match nothing, so no run\n * ever leaks across sessions through an undefined-equals-undefined comparison.\n */\nexport function isSessionRun(record: WorkflowRunRecord, sessionId: string | undefined): boolean {\n return sessionId !== undefined && record.env?.[PI_SESSION_ENV] === sessionId;\n}\n\n/** Runs launched by one Pi session that are still running. */\nexport function runsForSession(records: WorkflowRunRecord[], sessionId: string): WorkflowRunRecord[] {\n return records.filter((record) => isSessionRun(record, sessionId) && record.stage === 'running');\n}\n\n/**\n * What the caller is told when the registry, not the launcher, acknowledged a\n * launch.\n *\n * Leads with the run key because it is the handle every other workflow action\n * takes, and it is no longer available any other way: the launcher's own output\n * is not waited for, so nothing else in this result carries it.\n */\nexport function launchedRunSummary(record: WorkflowRunRecord): string {\n return [\n `Started ${record.displayName} in workspace ${record.workspace}.`,\n `Run key: ${record.runKey}`,\n 'The run is registered and going. This call returned at that point rather than waiting for the launcher process to exit.',\n ].join('\\n');\n}\n\n/**\n * What the caller is told when the launch was handed off but no run has\n * registered inside the acknowledgement budget.\n *\n * Deliberately not an error: the launcher is slow rather than broken often\n * enough that failing here would strand runs that go on to start normally.\n * There is no run key to offer yet, so it points at the surfaces that do not\n * need one.\n */\nexport function launchHandoffSummary(workflowPath: string): string {\n return [\n `Launch handed off for ${workflowPath}, but no run has registered yet.`,\n 'Nothing was cancelled: the launcher is still starting, and a run that registers later still reports back here when it finishes.',\n 'Do not launch it again. Ask the user to check the workflow footer if they need to see it sooner.',\n ].join('\\n');\n}\n\n/**\n * Escape sequences a terminal consumes rather than prints.\n *\n * Built from a string with the rule silenced, the way `doom-runner`'s own\n * scrubber does it, because matching the escape byte is the entire job. Not\n * imported from there: that package's published build is a stale artifact\n * missing the export, and a launch notice should not need another package's\n * build to be current.\n */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the point\nconst ESCAPE_SEQUENCE = new RegExp(\n ['\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]', '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)', '\\\\u001B[@-Z\\\\\\\\-_]'].join(\n '|',\n ),\n 'g',\n);\n\n/** Rows made only of box-drawing: separators that carry nothing once trimmed. */\nconst RULE_ROW = /^[\\s─-╿]*$/u;\nconst NOTICE_LINE_LIMIT = 3;\n/**\n * Widest a notice row may be before it is clipped.\n *\n * A three-line cap does not bound a toast on its own: the engine announces its\n * delegation by echoing the whole shell command, temp script path included, and\n * one line of that wraps across three rows of a narrow terminal and shoves the\n * transcript around. Clipped at a width every terminal can hold instead.\n */\nconst NOTICE_WIDTH_LIMIT = 100;\n\n/**\n * Cut a tool result down to something a toast can hold.\n *\n * The launch result used to reach `ui.notify` whole, and for a workflow that\n * executes in this process that result is the engine's entire console log:\n * banner, run directory, job tree, rules, colour. Pi rendered every line of it\n * over the transcript, which reads as a broken screen rather than as output.\n * Escape sequences go first: the log carries colour the notification would\n * render as literal text, and a stray clear or cursor-move would do to the\n * screen exactly what this function exists to prevent.\n */\nexport function launchNotice(text: string, limit: number = NOTICE_LINE_LIMIT): string {\n const lines = text\n .replace(ESCAPE_SEQUENCE, '')\n .split('\\n')\n .map((line) => line.trimEnd())\n .filter((line) => line.trim().length > 0 && !RULE_ROW.test(line))\n .map((line) => (line.length > NOTICE_WIDTH_LIMIT ? `${line.slice(0, NOTICE_WIDTH_LIMIT - 1)}…` : line));\n if (lines.length === 0) return 'Workflow started.';\n if (lines.length <= limit) return lines.join('\\n');\n return [...lines.slice(0, limit), `… ${lines.length - limit} more lines, in the run's own output.`].join('\\n');\n}\n\n/** Flatten MCP tool content to text for Pi. */\nexport function toolResultText(result: CallToolResult): string {\n return result.content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .filter(Boolean)\n .join('\\n')\n .trim();\n}\n\n/**\n * Convert an MCP tool result into a Pi tool result.\n *\n * Pi signals tool failure by throwing: the agent loop catches it and builds the\n * error result. So an `isError` MCP result becomes a thrown error rather than a\n * successful result carrying a flag Pi would ignore.\n */\nexport function toAgentToolResult(\n tool: string,\n result: CallToolResult,\n errorOptions: string[] = [],\n): AgentToolResult<WorkflowToolDetails> {\n const text = toolResultText(result) || 'No output.';\n if (result.isError) throw new Error(withOptions(text, errorOptions));\n return { content: [{ type: 'text', text }], details: { tool } };\n}\n\n/**\n * Attach a short menu of next steps to a problem.\n *\n * An error with no route forward is where an agent invents one: retrying a\n * capacity limit, relaunching a run that should be recovered, or silently\n * giving up on work the user is waiting for. Naming two or three real options\n * turns an error into a decision, and the decision belongs to the user.\n */\nexport function withOptions(problem: string, options: string[]): string {\n if (options.length === 0) return problem;\n return [problem, '', 'Options, put these to the user rather than picking one yourself:', ...options.map(bullet)].join(\n '\\n',\n );\n}\n\nfunction bullet(option: string): string {\n return `- ${option}`;\n}\n\n/** The job and step a run died on, preferring the progress log to the record. */\nfunction failurePosition(\n record: WorkflowRunRecord,\n jobs: WorkflowProgressJob[],\n): { job?: string; step?: string; reason?: string } {\n // The progress log knows which step was in flight; the record only names the\n // job. Falling back keeps this working for runs that predate the log.\n const failedJob = jobs.find((job) => job.status === 'failed');\n const failedStep = failedJob?.steps.find((step) => step.status === 'failed');\n return {\n job: failedJob?.name ?? record.failedJob,\n step: failedStep?.name,\n reason: failedStep?.reason ?? failedJob?.reason,\n };\n}\n\n/**\n * Describe a finished run for the agent that launched it.\n *\n * Carries the job identifiers back so a dispatching agent can match the run to\n * the work item it launched it for without another lookup. When the run failed,\n * carries the failing step and a set of options too: the agent is being told\n * about this out of band, with no user question in flight, so without them it\n * has to guess whether to recover, relaunch, or report.\n */\nexport function finishedRunSummary(record: WorkflowRunRecord, jobs: WorkflowProgressJob[] = []): string {\n const outcome = record.stage === 'completed' ? 'completed' : `ended in ${record.stage}`;\n const failure = failurePosition(record, jobs);\n const lines: Array<string | undefined> = [\n `Workflow run ${record.runKey} in workspace ${record.workspace} ${outcome}.`,\n record.workflowId ? `Workflow: ${record.workflowId}` : undefined,\n failure.job ? `Failed job: ${failure.job}${failure.step ? ` (step: ${failure.step})` : ''}` : undefined,\n record.errorMessage ? `Error: ${record.errorMessage}` : undefined,\n failure.reason && failure.reason !== record.errorMessage ? `Step reported: ${failure.reason}` : undefined,\n ];\n const jobId = record.env?.AGIFLOW_JOB_ID;\n if (jobId) lines.push(`Agiflow job: ${record.env?.AGIFLOW_JOB_KIND ?? 'unknown'} ${jobId}`);\n\n const summary = lines.filter((line): line is string => Boolean(line)).join('\\n');\n if (record.stage === 'completed') return summary;\n\n // A run the user stopped is not a defect, so it gets no diagnosis step: the\n // question is only whether to pick it back up.\n const stopped = record.outcome === 'interrupted';\n if (stopped) {\n return withOptions(summary, [\n `workflow_run {\"action\":\"recover\",\"runKey\":${JSON.stringify(record.runKey)}}: pick it up where it stopped.`,\n 'Leave it stopped and move on.',\n ]);\n }\n\n const diagnosticText = [record.errorMessage, failure.reason].filter(Boolean).join('\\n');\n if (diagnosticText.includes('JOB_ALREADY_CLAIMED')) {\n return withOptions(summary, [\n 'Report contention: another worker owns this job. Do not unlock, release, or retry the claim automatically.',\n ]);\n }\n if (diagnosticText.includes('WORKFLOW_NOT_OWNED') || /release failed/i.test(diagnosticText)) {\n return withOptions(summary, [\n 'Inspect the current workflow ownership and ask the user before any release or unlock. Never force another worker’s lock.',\n ]);\n }\n if (/running workflow not found/i.test(diagnosticText)) {\n return withOptions(summary, [\n `workflow_run {\"action\":\"status\",\"runKey\":${JSON.stringify(record.runKey)}}: re-check current state before retrying.`,\n 'Report the recorded terminal state when no running owner remains.',\n ]);\n }\n\n return withOptions(summary, [\n `workflow_run {\"action\":\"tail\",\"runKey\":${JSON.stringify(record.runKey)}}: read the run's own output first, when the error above does not explain the failure.`,\n `workflow_run {\"action\":\"recover\",\"runKey\":${JSON.stringify(record.runKey)}}: resume from the failed job, keeping the work already done. Fits a transient or external cause.`,\n 'Report the failure and stop, when it needs a code change or a decision you cannot make.',\n ]);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const e=`PI_SESSION_ID`;function t(e=process.env){let t=Number(e.WORKFLOW_MCP_MAX_CONCURRENT);return Number.isInteger(t)&&t>0?t:5}function n(e,t){return t!==void 0&&e.env?.PI_SESSION_ID===t}function r(e,t){return e.filter(e=>n(e,t)&&e.stage===`running`)}function i(e){return[`Started ${e.displayName} in workspace ${e.workspace}.`,`Run key: ${e.runKey}`,`The run is registered and going. This call returned at that point rather than waiting for the launcher process to exit.`].join(`
|
|
2
|
+
`)}function a(e){return[`Launch handed off for ${e}, but no run has registered yet.`,`Nothing was cancelled: the launcher is still starting, and a run that registers later still reports back here when it finishes.`,`Do not launch it again. Ask the user to check the workflow footer if they need to see it sooner.`].join(`
|
|
3
|
+
`)}const o=new RegExp([`\\u001B\\[[0-9;?]*[ -/]*[@-~]`,`\\u001B\\][^\\u0007\\u001B]*(?:\\u0007|\\u001B\\\\)`,`\\u001B[@-Z\\\\-_]`].join(`|`),`g`),s=/^[\s─-╿]*$/u;function c(e,t=3){let n=e.replace(o,``).split(`
|
|
4
|
+
`).map(e=>e.trimEnd()).filter(e=>e.trim().length>0&&!s.test(e)).map(e=>e.length>100?`${e.slice(0,99)}…`:e);return n.length===0?`Workflow started.`:n.length<=t?n.join(`
|
|
5
|
+
`):[...n.slice(0,t),`… ${n.length-t} more lines, in the run's own output.`].join(`
|
|
6
|
+
`)}function l(e){return e.content.map(e=>e.type===`text`?e.text:``).filter(Boolean).join(`
|
|
7
|
+
`).trim()}function u(e,t,n=[]){let r=l(t)||`No output.`;if(t.isError)throw Error(d(r,n));return{content:[{type:`text`,text:r}],details:{tool:e}}}function d(e,t){return t.length===0?e:[e,``,`Options, put these to the user rather than picking one yourself:`,...t.map(f)].join(`
|
|
8
|
+
`)}function f(e){return`- ${e}`}function p(e,t){let n=t.find(e=>e.status===`failed`),r=n?.steps.find(e=>e.status===`failed`);return{job:n?.name??e.failedJob,step:r?.name,reason:r?.reason??n?.reason}}function m(e,t=[]){let n=e.stage===`completed`?`completed`:`ended in ${e.stage}`,r=p(e,t),i=[`Workflow run ${e.runKey} in workspace ${e.workspace} ${n}.`,e.workflowId?`Workflow: ${e.workflowId}`:void 0,r.job?`Failed job: ${r.job}${r.step?` (step: ${r.step})`:``}`:void 0,e.errorMessage?`Error: ${e.errorMessage}`:void 0,r.reason&&r.reason!==e.errorMessage?`Step reported: ${r.reason}`:void 0],a=e.env?.AGIFLOW_JOB_ID;a&&i.push(`Agiflow job: ${e.env?.AGIFLOW_JOB_KIND??`unknown`} ${a}`);let o=i.filter(e=>!!e).join(`
|
|
9
|
+
`);if(e.stage===`completed`)return o;if(e.outcome===`interrupted`)return d(o,[`workflow_run {"action":"recover","runKey":${JSON.stringify(e.runKey)}}: pick it up where it stopped.`,`Leave it stopped and move on.`]);let s=[e.errorMessage,r.reason].filter(Boolean).join(`
|
|
10
|
+
`);return s.includes(`JOB_ALREADY_CLAIMED`)?d(o,[`Report contention: another worker owns this job. Do not unlock, release, or retry the claim automatically.`]):s.includes(`WORKFLOW_NOT_OWNED`)||/release failed/i.test(s)?d(o,[`Inspect the current workflow ownership and ask the user before any release or unlock. Never force another worker’s lock.`]):/running workflow not found/i.test(s)?d(o,[`workflow_run {"action":"status","runKey":${JSON.stringify(e.runKey)}}: re-check current state before retrying.`,`Report the recorded terminal state when no running owner remains.`]):d(o,[`workflow_run {"action":"tail","runKey":${JSON.stringify(e.runKey)}}: read the run's own output first, when the error above does not explain the failure.`,`workflow_run {"action":"recover","runKey":${JSON.stringify(e.runKey)}}: resume from the failed job, keeping the work already done. Fits a transient or external cause.`,`Report the failure and stop, when it needs a code change or a decision you cannot make.`])}export{e as PI_SESSION_ENV,m as finishedRunSummary,n as isSessionRun,a as launchHandoffSummary,c as launchNotice,i as launchedRunSummary,t as resolveMaxConcurrent,r as runsForSession,u as toAgentToolResult,l as toolResultText,d as withOptions};
|
|
11
|
+
//# sourceMappingURL=piToolBridge.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piToolBridge.mjs","names":[],"sources":["../../src/workflow/piToolBridge.ts"],"sourcesContent":["/**\n * Pure bridging logic between MCP tool results and the Pi tool surface.\n *\n * DESIGN PATTERNS:\n * - Adapter pattern: MCP `CallToolResult` is converted to Pi's result shape\n * - Pure module: no service or tool-class imports, so this is unit-testable\n * without constructing the workflow execution stack\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported functions\n *\n * AVOID:\n * - Importing tool classes or services here (that belongs in `piTools.ts`)\n * - Returning an error flag Pi does not read; Pi tools signal failure by throwing\n */\n\nimport type { AgentToolResult } from '@earendil-works/pi-coding-agent';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { WorkflowProgressJob, WorkflowRunRecord } from '@agimon-ai/workflow-mcp';\n\n/**\n * Environment variable stamped onto every run launched from a Pi session.\n *\n * `RunWorkflowService` forwards `env` to the inner CLI invocation across the\n * `launch-command` delegation boundary and persists it on the run record, so\n * this survives backgrounding and identifies the launching session afterwards.\n */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\n/** Operator override for the per-session launch ceiling. */\nconst MAX_CONCURRENT_ENV = 'WORKFLOW_MCP_MAX_CONCURRENT';\nconst DEFAULT_MAX_CONCURRENT = 5;\n\nexport interface WorkflowToolDetails {\n tool: string;\n}\n\n/** Resolve the per-session launch ceiling from the environment. */\nexport function resolveMaxConcurrent(env: NodeJS.ProcessEnv = process.env): number {\n const parsed = Number(env[MAX_CONCURRENT_ENV]);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT;\n}\n\n/**\n * True when this session launched the run.\n *\n * Fails closed on a missing session id: an unstamped record (a CLI launch) and\n * a call arriving before the session id is known both match nothing, so no run\n * ever leaks across sessions through an undefined-equals-undefined comparison.\n */\nexport function isSessionRun(record: WorkflowRunRecord, sessionId: string | undefined): boolean {\n return sessionId !== undefined && record.env?.[PI_SESSION_ENV] === sessionId;\n}\n\n/** Runs launched by one Pi session that are still running. */\nexport function runsForSession(records: WorkflowRunRecord[], sessionId: string): WorkflowRunRecord[] {\n return records.filter((record) => isSessionRun(record, sessionId) && record.stage === 'running');\n}\n\n/**\n * What the caller is told when the registry, not the launcher, acknowledged a\n * launch.\n *\n * Leads with the run key because it is the handle every other workflow action\n * takes, and it is no longer available any other way: the launcher's own output\n * is not waited for, so nothing else in this result carries it.\n */\nexport function launchedRunSummary(record: WorkflowRunRecord): string {\n return [\n `Started ${record.displayName} in workspace ${record.workspace}.`,\n `Run key: ${record.runKey}`,\n 'The run is registered and going. This call returned at that point rather than waiting for the launcher process to exit.',\n ].join('\\n');\n}\n\n/**\n * What the caller is told when the launch was handed off but no run has\n * registered inside the acknowledgement budget.\n *\n * Deliberately not an error: the launcher is slow rather than broken often\n * enough that failing here would strand runs that go on to start normally.\n * There is no run key to offer yet, so it points at the surfaces that do not\n * need one.\n */\nexport function launchHandoffSummary(workflowPath: string): string {\n return [\n `Launch handed off for ${workflowPath}, but no run has registered yet.`,\n 'Nothing was cancelled: the launcher is still starting, and a run that registers later still reports back here when it finishes.',\n 'Do not launch it again. Ask the user to check the workflow footer if they need to see it sooner.',\n ].join('\\n');\n}\n\n/**\n * Escape sequences a terminal consumes rather than prints.\n *\n * Built from a string with the rule silenced, the way `doom-runner`'s own\n * scrubber does it, because matching the escape byte is the entire job. Not\n * imported from there: that package's published build is a stale artifact\n * missing the export, and a launch notice should not need another package's\n * build to be current.\n */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the point\nconst ESCAPE_SEQUENCE = new RegExp(\n ['\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]', '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)', '\\\\u001B[@-Z\\\\\\\\-_]'].join(\n '|',\n ),\n 'g',\n);\n\n/** Rows made only of box-drawing: separators that carry nothing once trimmed. */\nconst RULE_ROW = /^[\\s─-╿]*$/u;\nconst NOTICE_LINE_LIMIT = 3;\n/**\n * Widest a notice row may be before it is clipped.\n *\n * A three-line cap does not bound a toast on its own: the engine announces its\n * delegation by echoing the whole shell command, temp script path included, and\n * one line of that wraps across three rows of a narrow terminal and shoves the\n * transcript around. Clipped at a width every terminal can hold instead.\n */\nconst NOTICE_WIDTH_LIMIT = 100;\n\n/**\n * Cut a tool result down to something a toast can hold.\n *\n * The launch result used to reach `ui.notify` whole, and for a workflow that\n * executes in this process that result is the engine's entire console log:\n * banner, run directory, job tree, rules, colour. Pi rendered every line of it\n * over the transcript, which reads as a broken screen rather than as output.\n * Escape sequences go first: the log carries colour the notification would\n * render as literal text, and a stray clear or cursor-move would do to the\n * screen exactly what this function exists to prevent.\n */\nexport function launchNotice(text: string, limit: number = NOTICE_LINE_LIMIT): string {\n const lines = text\n .replace(ESCAPE_SEQUENCE, '')\n .split('\\n')\n .map((line) => line.trimEnd())\n .filter((line) => line.trim().length > 0 && !RULE_ROW.test(line))\n .map((line) => (line.length > NOTICE_WIDTH_LIMIT ? `${line.slice(0, NOTICE_WIDTH_LIMIT - 1)}…` : line));\n if (lines.length === 0) return 'Workflow started.';\n if (lines.length <= limit) return lines.join('\\n');\n return [...lines.slice(0, limit), `… ${lines.length - limit} more lines, in the run's own output.`].join('\\n');\n}\n\n/** Flatten MCP tool content to text for Pi. */\nexport function toolResultText(result: CallToolResult): string {\n return result.content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .filter(Boolean)\n .join('\\n')\n .trim();\n}\n\n/**\n * Convert an MCP tool result into a Pi tool result.\n *\n * Pi signals tool failure by throwing: the agent loop catches it and builds the\n * error result. So an `isError` MCP result becomes a thrown error rather than a\n * successful result carrying a flag Pi would ignore.\n */\nexport function toAgentToolResult(\n tool: string,\n result: CallToolResult,\n errorOptions: string[] = [],\n): AgentToolResult<WorkflowToolDetails> {\n const text = toolResultText(result) || 'No output.';\n if (result.isError) throw new Error(withOptions(text, errorOptions));\n return { content: [{ type: 'text', text }], details: { tool } };\n}\n\n/**\n * Attach a short menu of next steps to a problem.\n *\n * An error with no route forward is where an agent invents one: retrying a\n * capacity limit, relaunching a run that should be recovered, or silently\n * giving up on work the user is waiting for. Naming two or three real options\n * turns an error into a decision, and the decision belongs to the user.\n */\nexport function withOptions(problem: string, options: string[]): string {\n if (options.length === 0) return problem;\n return [problem, '', 'Options, put these to the user rather than picking one yourself:', ...options.map(bullet)].join(\n '\\n',\n );\n}\n\nfunction bullet(option: string): string {\n return `- ${option}`;\n}\n\n/** The job and step a run died on, preferring the progress log to the record. */\nfunction failurePosition(\n record: WorkflowRunRecord,\n jobs: WorkflowProgressJob[],\n): { job?: string; step?: string; reason?: string } {\n // The progress log knows which step was in flight; the record only names the\n // job. Falling back keeps this working for runs that predate the log.\n const failedJob = jobs.find((job) => job.status === 'failed');\n const failedStep = failedJob?.steps.find((step) => step.status === 'failed');\n return {\n job: failedJob?.name ?? record.failedJob,\n step: failedStep?.name,\n reason: failedStep?.reason ?? failedJob?.reason,\n };\n}\n\n/**\n * Describe a finished run for the agent that launched it.\n *\n * Carries the job identifiers back so a dispatching agent can match the run to\n * the work item it launched it for without another lookup. When the run failed,\n * carries the failing step and a set of options too: the agent is being told\n * about this out of band, with no user question in flight, so without them it\n * has to guess whether to recover, relaunch, or report.\n */\nexport function finishedRunSummary(record: WorkflowRunRecord, jobs: WorkflowProgressJob[] = []): string {\n const outcome = record.stage === 'completed' ? 'completed' : `ended in ${record.stage}`;\n const failure = failurePosition(record, jobs);\n const lines: Array<string | undefined> = [\n `Workflow run ${record.runKey} in workspace ${record.workspace} ${outcome}.`,\n record.workflowId ? `Workflow: ${record.workflowId}` : undefined,\n failure.job ? `Failed job: ${failure.job}${failure.step ? ` (step: ${failure.step})` : ''}` : undefined,\n record.errorMessage ? `Error: ${record.errorMessage}` : undefined,\n failure.reason && failure.reason !== record.errorMessage ? `Step reported: ${failure.reason}` : undefined,\n ];\n const jobId = record.env?.AGIFLOW_JOB_ID;\n if (jobId) lines.push(`Agiflow job: ${record.env?.AGIFLOW_JOB_KIND ?? 'unknown'} ${jobId}`);\n\n const summary = lines.filter((line): line is string => Boolean(line)).join('\\n');\n if (record.stage === 'completed') return summary;\n\n // A run the user stopped is not a defect, so it gets no diagnosis step: the\n // question is only whether to pick it back up.\n const stopped = record.outcome === 'interrupted';\n if (stopped) {\n return withOptions(summary, [\n `workflow_run {\"action\":\"recover\",\"runKey\":${JSON.stringify(record.runKey)}}: pick it up where it stopped.`,\n 'Leave it stopped and move on.',\n ]);\n }\n\n const diagnosticText = [record.errorMessage, failure.reason].filter(Boolean).join('\\n');\n if (diagnosticText.includes('JOB_ALREADY_CLAIMED')) {\n return withOptions(summary, [\n 'Report contention: another worker owns this job. Do not unlock, release, or retry the claim automatically.',\n ]);\n }\n if (diagnosticText.includes('WORKFLOW_NOT_OWNED') || /release failed/i.test(diagnosticText)) {\n return withOptions(summary, [\n 'Inspect the current workflow ownership and ask the user before any release or unlock. Never force another worker’s lock.',\n ]);\n }\n if (/running workflow not found/i.test(diagnosticText)) {\n return withOptions(summary, [\n `workflow_run {\"action\":\"status\",\"runKey\":${JSON.stringify(record.runKey)}}: re-check current state before retrying.`,\n 'Report the recorded terminal state when no running owner remains.',\n ]);\n }\n\n return withOptions(summary, [\n `workflow_run {\"action\":\"tail\",\"runKey\":${JSON.stringify(record.runKey)}}: read the run's own output first, when the error above does not explain the failure.`,\n `workflow_run {\"action\":\"recover\",\"runKey\":${JSON.stringify(record.runKey)}}: resume from the failed job, keeping the work already done. Fits a transient or external cause.`,\n 'Report the failure and stop, when it needs a code change or a decision you cannot make.',\n ]);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const e=require("./piToolBridge.cjs");let t=require("@agimon-ai/doompi-extension-contracts/child-process"),n=require("@agimon-ai/workflow-mcp"),r=require("zod");const i={workflowPath:!0,workspace:!0,runner:!0,name:!0,env:!0,prompt:!0,job:!0,inputs:!0},a=[`list_workflows`,`launch_workflow`,`workflow_run`],[o,s,c]=a,l={runKey:r.z.string().min(1).describe(`Run key of the workflow run.`),workspace:r.z.string().optional().describe(`Workspace the run belongs to.`)},u=r.z.discriminatedUnion(`action`,[r.z.object({action:r.z.literal(`status`),...l}).strict(),r.z.object({action:r.z.literal(`tail`),...l}).strict(),r.z.object({action:r.z.literal(`follow`),...l}).strict(),r.z.object({action:r.z.literal(`open`),...l}).strict(),r.z.object({action:r.z.literal(`pause`),...l,reason:r.z.string().optional()}).strict(),r.z.object({action:r.z.literal(`resume`),...l,reason:r.z.string().optional()}).strict(),r.z.object({action:r.z.literal(`stop`),...l,reason:r.z.string().optional()}).strict(),r.z.object({action:r.z.literal(`recover`),...l,dryRun:r.z.boolean().optional(),job:r.z.string().optional(),runner:r.z.string().optional()}).strict()]);function d(e,t,n){e?.({content:[{type:`text`,text:n}],details:{tool:t}})}function f(e,t){return{...e,content:[...e.content,{type:`text`,text:t}]}}function p(e){return`workflow_run {"action":"status","runKey":${JSON.stringify(e)}}`}function m(e,t){let n=p(t);return e===`stop`?[`Next: call ${n} to confirm the run reached a terminal stage.`]:e===`recover`?[`Report the recovery failure and do not retry it blindly: call ${n} first.`,`Ask the user before launching fresh work when the recorded recovery is no longer valid.`]:[`Next: call ${n} to verify the recorded execution state.`]}const h=[`Report the failure to the user with the error above. A launch failure is usually a bad workflow path, a missing launcher, or a workflow whose own pre-conditions refused.`,`list_workflows: confirm the workflow path exists and its description matches the work, if the path may be wrong.`,`workflow_run with action status: check whether an earlier attempt is already running before launching again.`];function g(e){return e instanceof Error?e:Error(String(e),{cause:e})}function _(e){return new Promise(t=>{setTimeout(()=>t(void 0),e).unref?.()})}async function v(e,t,n){let r=t.findLaunchedRun;if(!r)return{kind:`settled`,outcome:await e};let i=t.launchAckPollMs??500,a=Date.now()+(t.launchAckTimeoutMs??15e3);for(;;){let t=await Promise.race([e,_(i)]);if(t)return{kind:`settled`,outcome:t};let o=await r(n).catch(()=>void 0);if(o)return{kind:`record`,record:o};if(Date.now()>=a)return{kind:`handoff`}}}function y(t,n,r){t.then(t=>{if(t.kind===`error`){n.onLateFailure?.(g(t.error),r);return}t.result.isError&&n.onLateFailure?.(Error(e.toolResultText(t.result)||`Workflow launch failed.`),r)})}function b(n){return{async execute(r,i,a){let o=(0,t.resolveRootSessionId)(i.sessionManager.getSessionId());n.observeSession?.(o);let c=r.env?.AGIFLOW_JOB_KIND?.trim(),l=r.env?.AGIFLOW_JOB_ID?.trim();if(!!c!=!!l)throw Error(`Agiflow workflow launches require AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together.`);if(c&&!r.prompt?.trim())throw Error(`Agiflow workflow launches require a non-empty prompt so user_prompt runs do not wait for terminal input.`);d(a,s,`Checking workflow capacity...`);let u=e.resolveMaxConcurrent(),f=await n.activeRunCount?.()??0;if(f>=u)throw Error(e.withOptions(`This session is at capacity: ${f}/${u} workflows running.`,[`Wait for a running workflow to finish, then launch again. The extension reports each one as it ends.`,`workflow_run with action status: show what is running, so the user can judge whether something is stuck.`,`workflow_run with action stop: free a slot by stopping a run the user no longer wants.`]));if(r.runner){let e=n.rejectRunner?.(r.workflowPath,r.runner);if(e)throw Error(e)}d(a,s,`Launching workflow ${r.workflowPath}...`);let p={...r.env};if(process.env.AGIFLOW_DISPATCH_CONTEXT_FILE)for(let e of[`AGIFLOW_ORGANIZATION_ID`,`AGIFLOW_PROJECT_ID`,`AGIFLOW_DEVICE_ID`,`BACKEND_AGIFLOW_API_ENDPOINT`,`AGIFLOW_DISPATCH_SECRET_FILE`]){let t=process.env[e];t&&(p[e]=t)}let m=Date.now(),_=n.trackPendingRun(n.runTool.execute({...r,env:{...p,[e.PI_SESSION_ENV]:o}})).then(e=>({kind:`value`,result:e}),e=>({kind:`error`,error:e})),b=await v(_,n,{sessionId:o,since:m,workflowPath:r.workflowPath});if(b.kind===`settled`){let t=b.outcome;if(t.kind===`error`)throw g(t.error);let r=t.result;if(r.isError)throw Error(e.withOptions(e.toolResultText(r)||`Workflow launch failed.`,h));return await n.onLaunch?.(i),r}return y(_,n,i),await n.onLaunch?.(i),{content:[{type:`text`,text:b.kind===`record`?e.launchedRunSummary(b.record):e.launchHandoffSummary(r.workflowPath)}]}}}}function x(a,l={}){let p=l.feature??(0,n.createEmbeddedWorkflowFeature)(),g=l.runTool??p.runTool,_=l.listWorkflowsTool??p.listWorkflowsTool,v=l.recoverTool??p.createRecoverTool(),y=l.controlTool??p.createControlTool(),x=l.trackPendingRun??(e=>e),S=l.launchExecutor??b({activeRunCount:l.activeRunCount,onLaunch:l.onLaunch,observeSession:l.observeSession,rejectRunner:l.rejectRunner,runTool:g,trackPendingRun:x});a.registerTool({name:o,label:`List Workflows`,description:`List the workflow definitions available in this repository.`,promptSnippet:`List available workflow definitions`,parameters:r.z.toJSONSchema(_.getInputSchema()),async execute(t,n){return e.toAgentToolResult(o,await _.execute(n))}}),a.registerTool({name:s,label:`Launch Workflow`,description:`Start a workflow run. Returns once the run has started, not once it finishes; use workflow_run with action status or wait for the completion notice.`,promptSnippet:`Start a workflow run and return its identifier`,promptGuidelines:[`Before launching, call list_workflows and pick the workflow whose own description matches the work. A workflow description is the source of truth; never infer one from its filename. If nothing matches, say so and stop rather than launching an approximate fit.`,`launch_workflow returns when the run has STARTED, not when it has finished. Never report success from its result. Poll workflow_run with action status, or wait for the completion notice the extension delivers on its own.`,`When dispatching Agiflow work, pass AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together through env plus a non-empty prompt. The id alone is not a key, and omitting prompt leaves user_prompt workflows waiting for terminal input.`,`A workflow claims its own job in its pre step. Never claim a job yourself. JOB_ALREADY_CLAIMED or HTTP 409 means another worker won the race, so report contention and do not unlock, release, or retry automatically.`,`If a result says WORKFLOW_NOT_OWNED or a release failed, inspect current ownership and ask the user before any release or unlock. Never force another worker’s lock.`,`If a running workflow is not found, call workflow_run with action status to re-check current state before retrying.`,`The per-session concurrency ceiling is a real limit. If launch_workflow refuses, wait for a run to finish; do not launch through the shell to get around it.`],parameters:r.z.toJSONSchema(g.getInputSchema().pick(i)),async execute(t,n,r,i,a){let o=n,c=await S.execute(o,a,i);return f(e.toAgentToolResult(s,c,h),`Next: call workflow_run with action status and verify the recorded stage before reporting the workflow outcome.`)}}),a.registerTool({name:c,label:`Workflow Run`,description:`Inspect or control one workflow run. Use action status, tail, follow, open, pause, resume, stop, or recover. Every action requires the run key returned by launch_workflow.`,promptSnippet:`Inspect or control a workflow run`,promptGuidelines:[`Use action status as the authority for stage, outcome, execution state, and pause cursor. A run that started is not a run that succeeded.`,`Use action pause or resume for cooperative lifecycle control. Pause is not stop: the run keeps its stage running, capacity, services, and heartbeat while paused.`,`Use action stop only when the user requests stopping a run. A user-requested stop is not a failure and needs no failure diagnosis.`,`Use action tail to read output, action follow to pin live progress, and action open only when a human wants the recorded launcher foregrounded.`,"Use action recover only after loading the `workflow-recovery` skill and gathering its required evidence. Never edit issue.md or repair.json, and verify real process and registry progress before recovery.",`Never unlock or force-release an Agiflow claim from this tool.`],parameters:r.z.toJSONSchema(u),async execute(n,r,i,a,o){let s=u.parse(r),p=(0,t.resolveRootSessionId)(o.sessionManager.getSessionId());l.observeSession?.(p);let h={runKey:s.runKey,workspace:s.workspace},g=async()=>{if(!l.requireSessionRun)throw Error(`Workflow run ownership is not available in this session.`);return l.requireSessionRun(h,p)};if(s.action===`status`){let e=await g();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}],details:{tool:c}}}if(s.action===`tail`){if(!l.tailRun)throw Error(`Reading workflow output is not available in this session.`);return await g(),d(a,c,`Reading output for workflow ${s.runKey}...`),{content:[{type:`text`,text:await l.tailRun(h,o)}],details:{tool:c}}}if(s.action===`follow`){if(!l.followRun)throw Error(`Following workflow output is not available in this session.`);return await g(),d(a,c,`Following workflow ${s.runKey}...`),{content:[{type:`text`,text:await l.followRun(h,o)}],details:{tool:c}}}if(s.action===`open`){if(!l.openRun)throw Error(`Opening a workflow launcher is not available in this session.`);return await g(),d(a,c,`Opening workflow ${s.runKey}...`),{content:[{type:`text`,text:await l.openRun(h,o)}],details:{tool:c}}}if(await g(),s.action===`recover`){if(d(a,c,`Recovering workflow ${s.runKey}...`),!s.dryRun){let e=await l.delegateRecovery?.({runKey:s.runKey,workspace:s.workspace,runner:s.runner},o);if(e){let t=m(s.action,s.runKey);return await l.onLaunch?.(o),{content:[{type:`text`,text:[e,t[0]].join(`
|
|
2
|
+
|
|
3
|
+
`)}],details:{tool:c}}}}let t={dryRun:s.dryRun,job:s.job,runKey:s.runKey,runner:s.runner,workspace:s.workspace},n=await x(v.execute(t)),r=m(s.action,s.runKey),i=e.toAgentToolResult(c,n,r);return await l.onLaunch?.(o),f(i,r[0])}d(a,c,`Requesting ${s.action} for workflow ${s.runKey}...`);let _=await y.execute({action:s.action,reason:s.reason,runKey:s.runKey,workspace:s.workspace}),b=m(s.action,s.runKey);return f(e.toAgentToolResult(c,_,b),b[0])}})}exports.WORKFLOW_PI_TOOL_NAMES=a,exports.createWorkflowLaunchExecutor=b,exports.registerWorkflowPiTools=x;
|
|
4
|
+
//# sourceMappingURL=piTools.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piTools.cjs","names":["z","toolResultText","resolveRootSessionId","resolveMaxConcurrent","withOptions","PI_SESSION_ENV","launchedRunSummary","launchHandoffSummary","createEmbeddedWorkflowFeature","toAgentToolResult"],"sources":["../../src/workflow/piTools.ts"],"sourcesContent":["import type {\n AgentToolResult,\n AgentToolUpdateCallback,\n ExtensionAPI,\n ExtensionContext,\n} from '@earendil-works/pi-coding-agent';\nimport {\n createEmbeddedWorkflowFeature,\n type EmbeddedWorkflowFeature,\n type WorkflowRunRecord,\n} from '@agimon-ai/workflow-mcp';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\nimport {\n launchedRunSummary,\n launchHandoffSummary,\n PI_SESSION_ENV,\n resolveMaxConcurrent,\n toAgentToolResult,\n toolResultText,\n withOptions,\n} from './piToolBridge';\n\nconst AGIFLOW_JOB_ID_ENV = 'AGIFLOW_JOB_ID';\nconst AGIFLOW_JOB_KIND_ENV = 'AGIFLOW_JOB_KIND';\n\nconst PI_LAUNCH_FIELDS = {\n workflowPath: true,\n workspace: true,\n runner: true,\n name: true,\n env: true,\n prompt: true,\n job: true,\n inputs: true,\n} as const;\n\nexport const WORKFLOW_PI_TOOL_NAMES = ['list_workflows', 'launch_workflow', 'workflow_run'] as const;\nconst [LIST_WORKFLOWS_TOOL_NAME, LAUNCH_WORKFLOW_TOOL_NAME, WORKFLOW_RUN_TOOL_NAME] = WORKFLOW_PI_TOOL_NAMES;\n\nconst RUN_SELECTOR_FIELDS = {\n runKey: z.string().min(1).describe('Run key of the workflow run.'),\n workspace: z.string().optional().describe('Workspace the run belongs to.'),\n} as const;\n\nconst workflowRunInputSchema = z.discriminatedUnion('action', [\n z.object({ action: z.literal('status'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('tail'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('follow'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('open'), ...RUN_SELECTOR_FIELDS }).strict(),\n z.object({ action: z.literal('pause'), ...RUN_SELECTOR_FIELDS, reason: z.string().optional() }).strict(),\n z.object({ action: z.literal('resume'), ...RUN_SELECTOR_FIELDS, reason: z.string().optional() }).strict(),\n z.object({ action: z.literal('stop'), ...RUN_SELECTOR_FIELDS, reason: z.string().optional() }).strict(),\n z\n .object({\n action: z.literal('recover'),\n ...RUN_SELECTOR_FIELDS,\n dryRun: z.boolean().optional(),\n job: z.string().optional(),\n runner: z.string().optional(),\n })\n .strict(),\n]);\ntype WorkflowRunInput = z.infer<typeof workflowRunInputSchema>;\ntype WorkflowRunSelector = Pick<WorkflowRunInput, 'runKey' | 'workspace'>;\n\nexport type WorkflowLaunchInput = z.infer<ReturnType<EmbeddedWorkflowFeature['runTool']['getInputSchema']>>;\n\n/**\n * Where a launch was answered from, which decides what the caller is told.\n *\n * Tagged rather than inferred from an absent field: the failure carries an\n * `unknown`, and `unknown` includes `undefined`, so only a tag can tell a\n * rejection apart from a result.\n */\ntype LaunchOutcome = { kind: 'value'; result: CallToolResult } | { kind: 'error'; error: unknown };\ntype LaunchAck =\n | { kind: 'settled'; outcome: LaunchOutcome }\n | { kind: 'record'; record: WorkflowRunRecord }\n | { kind: 'handoff' };\n\n/** How often the registry is asked whether the launch has produced a run. */\nconst LAUNCH_ACK_POLL_MS = 500;\n/**\n * How long to wait for that before answering without a run key.\n *\n * Long enough to cover a launcher that is merely slow, short enough that a\n * caller is never left holding a turn open for a launcher that will not answer.\n */\nconst LAUNCH_ACK_TIMEOUT_MS = 15_000;\n\nexport interface LaunchedRunQuery {\n sessionId: string | undefined;\n /** Epoch milliseconds the launch began, so an earlier run cannot match. */\n since: number;\n workflowPath: string;\n}\n\nexport interface WorkflowLaunchExecutorDependencies {\n activeRunCount?: () => Promise<number>;\n onLaunch?: (ctx: ExtensionContext) => Promise<void> | void;\n observeSession?: (sessionId: string | undefined) => void;\n rejectRunner?: (workflowPath: string, runner: string) => string | undefined;\n runTool: EmbeddedWorkflowFeature['runTool'];\n trackPendingRun: <T>(run: Promise<T>) => Promise<T>;\n /**\n * The run this launch registered, or undefined while none has appeared.\n *\n * Its presence is what lets a launch answer on the registry instead of on the\n * launcher process exiting. Left out, the launch waits for the process, which\n * is the behaviour every caller had before.\n */\n findLaunchedRun?: (query: LaunchedRunQuery) => Promise<WorkflowRunRecord | undefined>;\n /** Report a launch that fails after this call already answered \"started\". */\n onLateFailure?: (error: unknown, ctx: ExtensionContext) => void;\n launchAckPollMs?: number;\n launchAckTimeoutMs?: number;\n}\n\nexport interface WorkflowLaunchExecutor {\n execute(\n input: WorkflowLaunchInput,\n ctx: ExtensionContext,\n onUpdate?: AgentToolUpdateCallback<{ tool: string }>,\n ): Promise<CallToolResult>;\n}\n\nexport interface WorkflowPiToolDependencies {\n launchExecutor?: WorkflowLaunchExecutor;\n onLaunch?: (ctx: ExtensionContext) => Promise<void> | void;\n activeRunCount?: () => Promise<number>;\n followRun?: (input: WorkflowRunSelector, ctx: ExtensionContext) => Promise<string>;\n tailRun?: (input: WorkflowRunSelector, ctx: ExtensionContext) => Promise<string>;\n openRun?: (input: WorkflowRunSelector, ctx: ExtensionContext) => Promise<string>;\n rejectRunner?: (workflowPath: string, runner: string) => string | undefined;\n requireSessionRun?: (input: WorkflowRunSelector, sessionId: string | undefined) => Promise<unknown>;\n /**\n * Hand a recovery to a terminal launcher, returning what to tell the caller.\n *\n * Undefined means this recovery cannot be delegated, and the in-process\n * replay below is used instead.\n */\n delegateRecovery?: (\n input: { runKey: string; workspace?: string; runner?: string },\n ctx: ExtensionContext,\n ) => Promise<string | undefined>;\n observeSession?: (sessionId: string | undefined) => void;\n trackPendingRun?: <T>(run: Promise<T>) => Promise<T>;\n feature?: EmbeddedWorkflowFeature;\n recoverTool?: ReturnType<EmbeddedWorkflowFeature['createRecoverTool']>;\n controlTool?: ReturnType<EmbeddedWorkflowFeature['createControlTool']>;\n runTool?: EmbeddedWorkflowFeature['runTool'];\n listWorkflowsTool?: EmbeddedWorkflowFeature['listWorkflowsTool'];\n}\n\nfunction reportProgress(\n onUpdate: AgentToolUpdateCallback<{ tool: string }> | undefined,\n tool: string,\n message: string,\n): void {\n onUpdate?.({ content: [{ type: 'text', text: message }], details: { tool } });\n}\n\nfunction appendGuidance(\n result: AgentToolResult<{ tool: string }>,\n guidance: string,\n): AgentToolResult<{ tool: string }> {\n return { ...result, content: [...result.content, { type: 'text', text: guidance }] };\n}\n\nfunction statusAction(runKey: string): string {\n return `workflow_run {\"action\":\"status\",\"runKey\":${JSON.stringify(runKey)}}`;\n}\n\nfunction actionGuidance(action: WorkflowRunInput['action'], runKey: string): string[] {\n const status = statusAction(runKey);\n if (action === 'stop') return [`Next: call ${status} to confirm the run reached a terminal stage.`];\n if (action === 'recover') {\n return [\n `Report the recovery failure and do not retry it blindly: call ${status} first.`,\n 'Ask the user before launching fresh work when the recorded recovery is no longer valid.',\n ];\n }\n return [`Next: call ${status} to verify the recorded execution state.`];\n}\n\nconst launchFailureOptions = [\n 'Report the failure to the user with the error above. A launch failure is usually a bad workflow path, a missing launcher, or a workflow whose own pre-conditions refused.',\n 'list_workflows: confirm the workflow path exists and its description matches the work, if the path may be wrong.',\n 'workflow_run with action status: check whether an earlier attempt is already running before launching again.',\n];\n\n/**\n * An Error, whatever the launch rejected with.\n *\n * A rejection reaches here as `unknown` because it crossed a promise boundary,\n * and rethrowing that as-is hands Pi's tool layer something it cannot read a\n * message or a stack off. The original is kept as the cause rather than\n * flattened into a string.\n */\nfunction asError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value), { cause: value });\n}\n\n/** Resolves to undefined, so a race against it reads as \"nothing settled yet\". */\nfunction sleep(ms: number): Promise<undefined> {\n return new Promise((settle) => {\n const timer = setTimeout(() => settle(undefined), ms);\n timer.unref?.();\n });\n}\n\n/**\n * Wait for whichever answer arrives first: the launch settling, the run\n * appearing in the registry, or the budget running out.\n *\n * A launch that delegates to a terminal launcher settles when that launcher's\n * whole process chain closes, which on a loaded machine has been measured\n * minutes after the run itself was registered and working. The registry is the\n * earlier and more truthful signal: a run recorded there is a run that started.\n */\nasync function awaitLaunchAck(\n settled: Promise<LaunchOutcome>,\n dependencies: WorkflowLaunchExecutorDependencies,\n query: LaunchedRunQuery,\n): Promise<LaunchAck> {\n const findLaunchedRun = dependencies.findLaunchedRun;\n if (!findLaunchedRun) return { kind: 'settled', outcome: await settled };\n\n const pollMs = dependencies.launchAckPollMs ?? LAUNCH_ACK_POLL_MS;\n const deadline = Date.now() + (dependencies.launchAckTimeoutMs ?? LAUNCH_ACK_TIMEOUT_MS);\n for (;;) {\n const outcome = await Promise.race([settled, sleep(pollMs)]);\n if (outcome) return { kind: 'settled', outcome };\n // A registry read that throws must not fail the launch: the run may be\n // perfectly healthy, and the settled promise is still the fallback.\n const record = await findLaunchedRun(query).catch(() => undefined);\n if (record) return { kind: 'record', record };\n if (Date.now() >= deadline) return { kind: 'handoff' };\n }\n}\n\n/**\n * Carry a failure that lands after the caller was told the run started.\n *\n * Answering early trades the launcher's exit code for timeliness. Dropping that\n * code entirely is not part of the trade: a launch that fails a minute later\n * would otherwise leave a user waiting on a run that never existed.\n */\nfunction reportLateFailure(\n settled: Promise<LaunchOutcome>,\n dependencies: WorkflowLaunchExecutorDependencies,\n ctx: ExtensionContext,\n): void {\n void settled.then((outcome) => {\n if (outcome.kind === 'error') {\n dependencies.onLateFailure?.(asError(outcome.error), ctx);\n return;\n }\n if (outcome.result.isError) {\n dependencies.onLateFailure?.(new Error(toolResultText(outcome.result) || 'Workflow launch failed.'), ctx);\n }\n });\n}\n\nexport function createWorkflowLaunchExecutor(dependencies: WorkflowLaunchExecutorDependencies): WorkflowLaunchExecutor {\n return {\n async execute(input, ctx, onUpdate) {\n const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n dependencies.observeSession?.(sessionId);\n const agiflowJobKind = input.env?.[AGIFLOW_JOB_KIND_ENV]?.trim();\n const agiflowJobId = input.env?.[AGIFLOW_JOB_ID_ENV]?.trim();\n if (Boolean(agiflowJobKind) !== Boolean(agiflowJobId)) {\n throw new Error('Agiflow workflow launches require AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together.');\n }\n if (agiflowJobKind && !input.prompt?.trim()) {\n throw new Error(\n 'Agiflow workflow launches require a non-empty prompt so user_prompt runs do not wait for terminal input.',\n );\n }\n reportProgress(onUpdate, LAUNCH_WORKFLOW_TOOL_NAME, 'Checking workflow capacity...');\n const maxConcurrent = resolveMaxConcurrent();\n const active = (await dependencies.activeRunCount?.()) ?? 0;\n if (active >= maxConcurrent) {\n throw new Error(\n withOptions(`This session is at capacity: ${active}/${maxConcurrent} workflows running.`, [\n 'Wait for a running workflow to finish, then launch again. The extension reports each one as it ends.',\n 'workflow_run with action status: show what is running, so the user can judge whether something is stuck.',\n 'workflow_run with action stop: free a slot by stopping a run the user no longer wants.',\n ]),\n );\n }\n\n if (input.runner) {\n const rejection = dependencies.rejectRunner?.(input.workflowPath, input.runner);\n if (rejection) throw new Error(rejection);\n }\n reportProgress(onUpdate, LAUNCH_WORKFLOW_TOOL_NAME, `Launching workflow ${input.workflowPath}...`);\n const workflowEnv = { ...input.env };\n if (process.env.AGIFLOW_DISPATCH_CONTEXT_FILE) {\n for (const key of [\n 'AGIFLOW_ORGANIZATION_ID',\n 'AGIFLOW_PROJECT_ID',\n 'AGIFLOW_DEVICE_ID',\n 'BACKEND_AGIFLOW_API_ENDPOINT',\n 'AGIFLOW_DISPATCH_SECRET_FILE',\n ] as const) {\n const hostValue = process.env[key];\n if (hostValue) workflowEnv[key] = hostValue;\n }\n }\n const since = Date.now();\n const launch = dependencies.trackPendingRun(\n dependencies.runTool.execute({\n ...input,\n env: { ...workflowEnv, [PI_SESSION_ENV]: sessionId },\n }),\n );\n // Folded into a value before anything races it. A promise this function\n // may stop awaiting must never be able to reject unobserved, which in\n // Node ends the whole process, Pi's TUI included.\n const settled: Promise<LaunchOutcome> = launch.then(\n (value): LaunchOutcome => ({ kind: 'value', result: value }),\n (error: unknown): LaunchOutcome => ({ kind: 'error', error }),\n );\n\n const ack = await awaitLaunchAck(settled, dependencies, {\n sessionId,\n since,\n workflowPath: input.workflowPath,\n });\n\n if (ack.kind === 'settled') {\n const outcome = ack.outcome;\n if (outcome.kind === 'error') throw asError(outcome.error);\n const result = outcome.result;\n if (result.isError)\n throw new Error(withOptions(toolResultText(result) || 'Workflow launch failed.', launchFailureOptions));\n await dependencies.onLaunch?.(ctx);\n return result;\n }\n\n reportLateFailure(settled, dependencies, ctx);\n await dependencies.onLaunch?.(ctx);\n return {\n content: [\n {\n type: 'text',\n text: ack.kind === 'record' ? launchedRunSummary(ack.record) : launchHandoffSummary(input.workflowPath),\n },\n ],\n };\n },\n };\n}\n\nexport function registerWorkflowPiTools(pi: ExtensionAPI, dependencies: WorkflowPiToolDependencies = {}): void {\n const feature = dependencies.feature ?? createEmbeddedWorkflowFeature();\n const runTool = dependencies.runTool ?? feature.runTool;\n const listWorkflowsTool = dependencies.listWorkflowsTool ?? feature.listWorkflowsTool;\n const recoverTool = dependencies.recoverTool ?? feature.createRecoverTool();\n const controlTool = dependencies.controlTool ?? feature.createControlTool();\n const trackPendingRun = dependencies.trackPendingRun ?? (<T>(run: Promise<T>): Promise<T> => run);\n const launchExecutor =\n dependencies.launchExecutor ??\n createWorkflowLaunchExecutor({\n activeRunCount: dependencies.activeRunCount,\n onLaunch: dependencies.onLaunch,\n observeSession: dependencies.observeSession,\n rejectRunner: dependencies.rejectRunner,\n runTool,\n trackPendingRun,\n });\n\n pi.registerTool({\n name: LIST_WORKFLOWS_TOOL_NAME,\n label: 'List Workflows',\n description: 'List the workflow definitions available in this repository.',\n promptSnippet: 'List available workflow definitions',\n parameters: z.toJSONSchema(listWorkflowsTool.getInputSchema()),\n async execute(_toolCallId, params) {\n return toAgentToolResult(\n LIST_WORKFLOWS_TOOL_NAME,\n await listWorkflowsTool.execute(\n params as Parameters<EmbeddedWorkflowFeature['listWorkflowsTool']['execute']>[0],\n ),\n );\n },\n });\n\n pi.registerTool({\n name: LAUNCH_WORKFLOW_TOOL_NAME,\n label: 'Launch Workflow',\n description:\n 'Start a workflow run. Returns once the run has started, not once it finishes; use workflow_run with action status or wait for the completion notice.',\n promptSnippet: 'Start a workflow run and return its identifier',\n promptGuidelines: [\n 'Before launching, call list_workflows and pick the workflow whose own description matches the work. A workflow description is the source of truth; never infer one from its filename. If nothing matches, say so and stop rather than launching an approximate fit.',\n 'launch_workflow returns when the run has STARTED, not when it has finished. Never report success from its result. Poll workflow_run with action status, or wait for the completion notice the extension delivers on its own.',\n 'When dispatching Agiflow work, pass AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together through env plus a non-empty prompt. The id alone is not a key, and omitting prompt leaves user_prompt workflows waiting for terminal input.',\n 'A workflow claims its own job in its pre step. Never claim a job yourself. JOB_ALREADY_CLAIMED or HTTP 409 means another worker won the race, so report contention and do not unlock, release, or retry automatically.',\n 'If a result says WORKFLOW_NOT_OWNED or a release failed, inspect current ownership and ask the user before any release or unlock. Never force another worker’s lock.',\n 'If a running workflow is not found, call workflow_run with action status to re-check current state before retrying.',\n 'The per-session concurrency ceiling is a real limit. If launch_workflow refuses, wait for a run to finish; do not launch through the shell to get around it.',\n ],\n parameters: z.toJSONSchema(runTool.getInputSchema().pick(PI_LAUNCH_FIELDS)),\n async execute(_toolCallId, params, _signal, onUpdate, ctx) {\n const input = params as WorkflowLaunchInput;\n const result = await launchExecutor.execute(input, ctx, onUpdate);\n const adapted = toAgentToolResult(LAUNCH_WORKFLOW_TOOL_NAME, result, launchFailureOptions);\n return appendGuidance(\n adapted,\n 'Next: call workflow_run with action status and verify the recorded stage before reporting the workflow outcome.',\n );\n },\n });\n\n pi.registerTool({\n name: WORKFLOW_RUN_TOOL_NAME,\n label: 'Workflow Run',\n description:\n 'Inspect or control one workflow run. Use action status, tail, follow, open, pause, resume, stop, or recover. Every action requires the run key returned by launch_workflow.',\n promptSnippet: 'Inspect or control a workflow run',\n promptGuidelines: [\n 'Use action status as the authority for stage, outcome, execution state, and pause cursor. A run that started is not a run that succeeded.',\n 'Use action pause or resume for cooperative lifecycle control. Pause is not stop: the run keeps its stage running, capacity, services, and heartbeat while paused.',\n 'Use action stop only when the user requests stopping a run. A user-requested stop is not a failure and needs no failure diagnosis.',\n 'Use action tail to read output, action follow to pin live progress, and action open only when a human wants the recorded launcher foregrounded.',\n 'Use action recover only after loading the `workflow-recovery` skill and gathering its required evidence. Never edit issue.md or repair.json, and verify real process and registry progress before recovery.',\n 'Never unlock or force-release an Agiflow claim from this tool.',\n ],\n parameters: z.toJSONSchema(workflowRunInputSchema),\n async execute(_toolCallId, rawParams, _signal, onUpdate, ctx) {\n const input = workflowRunInputSchema.parse(rawParams);\n const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId());\n dependencies.observeSession?.(sessionId);\n const selector: WorkflowRunSelector = { runKey: input.runKey, workspace: input.workspace };\n const requireOwnedRun = async (): Promise<unknown> => {\n if (!dependencies.requireSessionRun) {\n throw new Error('Workflow run ownership is not available in this session.');\n }\n return dependencies.requireSessionRun(selector, sessionId);\n };\n\n if (input.action === 'status') {\n const record = await requireOwnedRun();\n return {\n content: [{ type: 'text', text: JSON.stringify(record, null, 2) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n if (input.action === 'tail') {\n if (!dependencies.tailRun) throw new Error('Reading workflow output is not available in this session.');\n await requireOwnedRun();\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Reading output for workflow ${input.runKey}...`);\n return {\n content: [{ type: 'text', text: await dependencies.tailRun(selector, ctx) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n if (input.action === 'follow') {\n if (!dependencies.followRun) throw new Error('Following workflow output is not available in this session.');\n await requireOwnedRun();\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Following workflow ${input.runKey}...`);\n return {\n content: [{ type: 'text', text: await dependencies.followRun(selector, ctx) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n if (input.action === 'open') {\n if (!dependencies.openRun) throw new Error('Opening a workflow launcher is not available in this session.');\n await requireOwnedRun();\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Opening workflow ${input.runKey}...`);\n return {\n content: [{ type: 'text', text: await dependencies.openRun(selector, ctx) }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n\n await requireOwnedRun();\n if (input.action === 'recover') {\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Recovering workflow ${input.runKey}...`);\n // A recovery replays the workflow, and the replay skips the launch\n // step, so every job runs in THIS process. For a workflow whose steps\n // are interactive that means an agent TUI spawned onto this process's\n // own terminal. Hand it to a launcher instead, where the workflow says\n // how. A dry run prints rather than executes, so it stays here.\n if (!input.dryRun) {\n const delegated = await dependencies.delegateRecovery?.(\n { runKey: input.runKey, workspace: input.workspace, runner: input.runner },\n ctx,\n );\n if (delegated) {\n const guidance = actionGuidance(input.action, input.runKey);\n await dependencies.onLaunch?.(ctx);\n return {\n content: [{ type: 'text', text: [delegated, guidance[0]].join('\\n\\n') }],\n details: { tool: WORKFLOW_RUN_TOOL_NAME },\n };\n }\n }\n const recoverInput = {\n dryRun: input.dryRun,\n job: input.job,\n runKey: input.runKey,\n runner: input.runner,\n workspace: input.workspace,\n };\n const result = await trackPendingRun(recoverTool.execute(recoverInput));\n const guidance = actionGuidance(input.action, input.runKey);\n const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME, result, guidance);\n await dependencies.onLaunch?.(ctx);\n return appendGuidance(adapted, guidance[0]!);\n }\n\n reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Requesting ${input.action} for workflow ${input.runKey}...`);\n const result = await controlTool.execute({\n action: input.action,\n reason: input.reason,\n runKey: input.runKey,\n workspace: input.workspace,\n });\n const guidance = actionGuidance(input.action, input.runKey);\n const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME, result, guidance);\n return appendGuidance(adapted, guidance[0]!);\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{PI_SESSION_ENV as e,launchHandoffSummary as t,launchedRunSummary as n,resolveMaxConcurrent as r,toAgentToolResult as i,toolResultText as a,withOptions as o}from"./piToolBridge.mjs";import{resolveRootSessionId as s}from"@agimon-ai/doompi-extension-contracts/child-process";import{createEmbeddedWorkflowFeature as c}from"@agimon-ai/workflow-mcp";import{z as l}from"zod";const u={workflowPath:!0,workspace:!0,runner:!0,name:!0,env:!0,prompt:!0,job:!0,inputs:!0},d=[`list_workflows`,`launch_workflow`,`workflow_run`],[f,p,m]=d,h={runKey:l.string().min(1).describe(`Run key of the workflow run.`),workspace:l.string().optional().describe(`Workspace the run belongs to.`)},g=l.discriminatedUnion(`action`,[l.object({action:l.literal(`status`),...h}).strict(),l.object({action:l.literal(`tail`),...h}).strict(),l.object({action:l.literal(`follow`),...h}).strict(),l.object({action:l.literal(`open`),...h}).strict(),l.object({action:l.literal(`pause`),...h,reason:l.string().optional()}).strict(),l.object({action:l.literal(`resume`),...h,reason:l.string().optional()}).strict(),l.object({action:l.literal(`stop`),...h,reason:l.string().optional()}).strict(),l.object({action:l.literal(`recover`),...h,dryRun:l.boolean().optional(),job:l.string().optional(),runner:l.string().optional()}).strict()]);function _(e,t,n){e?.({content:[{type:`text`,text:n}],details:{tool:t}})}function v(e,t){return{...e,content:[...e.content,{type:`text`,text:t}]}}function y(e){return`workflow_run {"action":"status","runKey":${JSON.stringify(e)}}`}function b(e,t){let n=y(t);return e===`stop`?[`Next: call ${n} to confirm the run reached a terminal stage.`]:e===`recover`?[`Report the recovery failure and do not retry it blindly: call ${n} first.`,`Ask the user before launching fresh work when the recorded recovery is no longer valid.`]:[`Next: call ${n} to verify the recorded execution state.`]}const x=[`Report the failure to the user with the error above. A launch failure is usually a bad workflow path, a missing launcher, or a workflow whose own pre-conditions refused.`,`list_workflows: confirm the workflow path exists and its description matches the work, if the path may be wrong.`,`workflow_run with action status: check whether an earlier attempt is already running before launching again.`];function S(e){return e instanceof Error?e:Error(String(e),{cause:e})}function C(e){return new Promise(t=>{setTimeout(()=>t(void 0),e).unref?.()})}async function w(e,t,n){let r=t.findLaunchedRun;if(!r)return{kind:`settled`,outcome:await e};let i=t.launchAckPollMs??500,a=Date.now()+(t.launchAckTimeoutMs??15e3);for(;;){let t=await Promise.race([e,C(i)]);if(t)return{kind:`settled`,outcome:t};let o=await r(n).catch(()=>void 0);if(o)return{kind:`record`,record:o};if(Date.now()>=a)return{kind:`handoff`}}}function T(e,t,n){e.then(e=>{if(e.kind===`error`){t.onLateFailure?.(S(e.error),n);return}e.result.isError&&t.onLateFailure?.(Error(a(e.result)||`Workflow launch failed.`),n)})}function E(i){return{async execute(c,l,u){let d=s(l.sessionManager.getSessionId());i.observeSession?.(d);let f=c.env?.AGIFLOW_JOB_KIND?.trim(),m=c.env?.AGIFLOW_JOB_ID?.trim();if(!!f!=!!m)throw Error(`Agiflow workflow launches require AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together.`);if(f&&!c.prompt?.trim())throw Error(`Agiflow workflow launches require a non-empty prompt so user_prompt runs do not wait for terminal input.`);_(u,p,`Checking workflow capacity...`);let h=r(),g=await i.activeRunCount?.()??0;if(g>=h)throw Error(o(`This session is at capacity: ${g}/${h} workflows running.`,[`Wait for a running workflow to finish, then launch again. The extension reports each one as it ends.`,`workflow_run with action status: show what is running, so the user can judge whether something is stuck.`,`workflow_run with action stop: free a slot by stopping a run the user no longer wants.`]));if(c.runner){let e=i.rejectRunner?.(c.workflowPath,c.runner);if(e)throw Error(e)}_(u,p,`Launching workflow ${c.workflowPath}...`);let v={...c.env};if(process.env.AGIFLOW_DISPATCH_CONTEXT_FILE)for(let e of[`AGIFLOW_ORGANIZATION_ID`,`AGIFLOW_PROJECT_ID`,`AGIFLOW_DEVICE_ID`,`BACKEND_AGIFLOW_API_ENDPOINT`,`AGIFLOW_DISPATCH_SECRET_FILE`]){let t=process.env[e];t&&(v[e]=t)}let y=Date.now(),b=i.trackPendingRun(i.runTool.execute({...c,env:{...v,[e]:d}})).then(e=>({kind:`value`,result:e}),e=>({kind:`error`,error:e})),C=await w(b,i,{sessionId:d,since:y,workflowPath:c.workflowPath});if(C.kind===`settled`){let e=C.outcome;if(e.kind===`error`)throw S(e.error);let t=e.result;if(t.isError)throw Error(o(a(t)||`Workflow launch failed.`,x));return await i.onLaunch?.(l),t}return T(b,i,l),await i.onLaunch?.(l),{content:[{type:`text`,text:C.kind===`record`?n(C.record):t(c.workflowPath)}]}}}}function D(e,t={}){let n=t.feature??c(),r=t.runTool??n.runTool,a=t.listWorkflowsTool??n.listWorkflowsTool,o=t.recoverTool??n.createRecoverTool(),d=t.controlTool??n.createControlTool(),h=t.trackPendingRun??(e=>e),y=t.launchExecutor??E({activeRunCount:t.activeRunCount,onLaunch:t.onLaunch,observeSession:t.observeSession,rejectRunner:t.rejectRunner,runTool:r,trackPendingRun:h});e.registerTool({name:f,label:`List Workflows`,description:`List the workflow definitions available in this repository.`,promptSnippet:`List available workflow definitions`,parameters:l.toJSONSchema(a.getInputSchema()),async execute(e,t){return i(f,await a.execute(t))}}),e.registerTool({name:p,label:`Launch Workflow`,description:`Start a workflow run. Returns once the run has started, not once it finishes; use workflow_run with action status or wait for the completion notice.`,promptSnippet:`Start a workflow run and return its identifier`,promptGuidelines:[`Before launching, call list_workflows and pick the workflow whose own description matches the work. A workflow description is the source of truth; never infer one from its filename. If nothing matches, say so and stop rather than launching an approximate fit.`,`launch_workflow returns when the run has STARTED, not when it has finished. Never report success from its result. Poll workflow_run with action status, or wait for the completion notice the extension delivers on its own.`,`When dispatching Agiflow work, pass AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together through env plus a non-empty prompt. The id alone is not a key, and omitting prompt leaves user_prompt workflows waiting for terminal input.`,`A workflow claims its own job in its pre step. Never claim a job yourself. JOB_ALREADY_CLAIMED or HTTP 409 means another worker won the race, so report contention and do not unlock, release, or retry automatically.`,`If a result says WORKFLOW_NOT_OWNED or a release failed, inspect current ownership and ask the user before any release or unlock. Never force another worker’s lock.`,`If a running workflow is not found, call workflow_run with action status to re-check current state before retrying.`,`The per-session concurrency ceiling is a real limit. If launch_workflow refuses, wait for a run to finish; do not launch through the shell to get around it.`],parameters:l.toJSONSchema(r.getInputSchema().pick(u)),async execute(e,t,n,r,a){let o=t,s=await y.execute(o,a,r);return v(i(p,s,x),`Next: call workflow_run with action status and verify the recorded stage before reporting the workflow outcome.`)}}),e.registerTool({name:m,label:`Workflow Run`,description:`Inspect or control one workflow run. Use action status, tail, follow, open, pause, resume, stop, or recover. Every action requires the run key returned by launch_workflow.`,promptSnippet:`Inspect or control a workflow run`,promptGuidelines:[`Use action status as the authority for stage, outcome, execution state, and pause cursor. A run that started is not a run that succeeded.`,`Use action pause or resume for cooperative lifecycle control. Pause is not stop: the run keeps its stage running, capacity, services, and heartbeat while paused.`,`Use action stop only when the user requests stopping a run. A user-requested stop is not a failure and needs no failure diagnosis.`,`Use action tail to read output, action follow to pin live progress, and action open only when a human wants the recorded launcher foregrounded.`,"Use action recover only after loading the `workflow-recovery` skill and gathering its required evidence. Never edit issue.md or repair.json, and verify real process and registry progress before recovery.",`Never unlock or force-release an Agiflow claim from this tool.`],parameters:l.toJSONSchema(g),async execute(e,n,r,a,c){let l=g.parse(n),u=s(c.sessionManager.getSessionId());t.observeSession?.(u);let f={runKey:l.runKey,workspace:l.workspace},p=async()=>{if(!t.requireSessionRun)throw Error(`Workflow run ownership is not available in this session.`);return t.requireSessionRun(f,u)};if(l.action===`status`){let e=await p();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}],details:{tool:m}}}if(l.action===`tail`){if(!t.tailRun)throw Error(`Reading workflow output is not available in this session.`);return await p(),_(a,m,`Reading output for workflow ${l.runKey}...`),{content:[{type:`text`,text:await t.tailRun(f,c)}],details:{tool:m}}}if(l.action===`follow`){if(!t.followRun)throw Error(`Following workflow output is not available in this session.`);return await p(),_(a,m,`Following workflow ${l.runKey}...`),{content:[{type:`text`,text:await t.followRun(f,c)}],details:{tool:m}}}if(l.action===`open`){if(!t.openRun)throw Error(`Opening a workflow launcher is not available in this session.`);return await p(),_(a,m,`Opening workflow ${l.runKey}...`),{content:[{type:`text`,text:await t.openRun(f,c)}],details:{tool:m}}}if(await p(),l.action===`recover`){if(_(a,m,`Recovering workflow ${l.runKey}...`),!l.dryRun){let e=await t.delegateRecovery?.({runKey:l.runKey,workspace:l.workspace,runner:l.runner},c);if(e){let n=b(l.action,l.runKey);return await t.onLaunch?.(c),{content:[{type:`text`,text:[e,n[0]].join(`
|
|
2
|
+
|
|
3
|
+
`)}],details:{tool:m}}}}let e={dryRun:l.dryRun,job:l.job,runKey:l.runKey,runner:l.runner,workspace:l.workspace},n=await h(o.execute(e)),r=b(l.action,l.runKey),s=i(m,n,r);return await t.onLaunch?.(c),v(s,r[0])}_(a,m,`Requesting ${l.action} for workflow ${l.runKey}...`);let y=await d.execute({action:l.action,reason:l.reason,runKey:l.runKey,workspace:l.workspace}),x=b(l.action,l.runKey);return v(i(m,y,x),x[0])}})}export{d as WORKFLOW_PI_TOOL_NAMES,E as createWorkflowLaunchExecutor,D as registerWorkflowPiTools};
|
|
4
|
+
//# sourceMappingURL=piTools.mjs.map
|