chore: snapshot main sync

This commit is contained in:
2026-06-17 20:08:31 +00:00
parent b18de2a331
commit 8bd32537cf
354 changed files with 10208 additions and 9230 deletions

View File

@@ -9,8 +9,7 @@
* workflow abandon abc123 --json
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -6,8 +6,7 @@
* workflow approve abc123 "Looks good" --json
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -9,8 +9,7 @@
* workflow cleanup 30 --json
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -9,8 +9,7 @@
* workflow convert deploy.sop.md --output workflows/deploy.yaml
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -9,8 +9,7 @@
* workflow list --json
*/
import type { CliOptions } from '../utils.js';
import { printTable, printJson } from '../utils.js';
import { printTable, printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -8,8 +8,7 @@
* workflow reject abc123 "Not compliant" --json
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -8,8 +8,7 @@
* workflow resume abc123 --json
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -10,8 +10,7 @@
* workflow run deploy --detach
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -7,8 +7,7 @@
* workflow runs --all
*/
import type { CliOptions } from '../utils.js';
import { printTable, printJson, formatTimestamp, formatDuration } from '../utils.js';
import { printTable, printJson, formatTimestamp, formatDuration, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -6,8 +6,7 @@
* workflow status --json
*/
import type { CliOptions } from '../utils.js';
import { printTable, printJson, formatDuration } from '../utils.js';
import { printTable, printJson, formatDuration, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -8,8 +8,7 @@
* workflow validate deploy --json
*/
import type { CliOptions } from '../utils.js';
import { printJson } from '../utils.js';
import { printJson, type CliOptions } from "../utils.js";
// ---------------------------------------------------------------------------
// Stub: engine integration (not implemented yet)

View File

@@ -9,8 +9,7 @@
* node dist/cli/index.js workflow run deploy --cwd /tmp/project
*/
import { parseArgs, buildCliOptions, printJson } from './utils.js';
import type { CliOptions } from './utils.js';
import { parseArgs, buildCliOptions, printJson, type CliOptions } from "./utils.js";
import { listCommand } from './commands/list.js';
import { runCommand } from './commands/run.js';

View File

@@ -17,10 +17,6 @@
import { resolveNodeOutputField, OutputRefError } from './output-ref.js';
// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------
export class ConditionError extends Error {
public readonly expression: string;
@@ -31,10 +27,6 @@ export class ConditionError extends Error {
}
}
// ---------------------------------------------------------------------------
// Token types
// ---------------------------------------------------------------------------
type TokenType =
| 'NODE_REF' // $nodeId.field
| 'NUMBER' // 42, 3.14
@@ -52,10 +44,6 @@ interface Token {
value: string;
}
// ---------------------------------------------------------------------------
// Tokenizer
// ---------------------------------------------------------------------------
const OPERATORS = new Set(['==', '!=', '<=', '>=', '<', '>']);
function tokenize(expression: string): Token[] {
@@ -199,10 +187,6 @@ function tokenize(expression: string): Token[] {
return tokens;
}
// ---------------------------------------------------------------------------
// Parser (recursive descent)
// ---------------------------------------------------------------------------
class ConditionParser {
private pos = 0;
@@ -384,10 +368,6 @@ class ConditionParser {
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Evaluate a `when:` condition expression against node outputs.
*

View File

@@ -59,10 +59,6 @@ import {
const execFileAsync = promisify(execFile);
// ---------------------------------------------------------------------------
// Topological layer building (Kahn's algorithm)
// ---------------------------------------------------------------------------
/**
* Build topological layers from a flat list of DAG nodes using Kahn's algorithm.
*
@@ -78,7 +74,6 @@ export function buildTopologicalLayers(nodes: DagNode[]): DagNode[][] {
const inDegree = new Map<string, number>();
const adjacency = new Map<string, Set<string>>(); // dep → nodes that depend on it
// Initialize
for (const node of nodes) {
nodeMap.set(node.id, node);
inDegree.set(node.id, node.depends_on.length);
@@ -88,7 +83,6 @@ export function buildTopologicalLayers(nodes: DagNode[]): DagNode[][] {
}
}
// Start with zero-in-degree nodes
let currentLayer: string[] = [];
for (const [id, degree] of inDegree) {
if (degree === 0) currentLayer.push(id);
@@ -98,7 +92,6 @@ export function buildTopologicalLayers(nodes: DagNode[]): DagNode[][] {
let totalProcessed = 0;
while (currentLayer.length > 0) {
// Build the layer from current zero-in-degree nodes
const layerNodes = currentLayer
.map((id) => nodeMap.get(id))
.filter((n): n is DagNode => n !== undefined);
@@ -128,10 +121,6 @@ export function buildTopologicalLayers(nodes: DagNode[]): DagNode[][] {
return layers;
}
// ---------------------------------------------------------------------------
// Trigger rule evaluation
// ---------------------------------------------------------------------------
/**
* Check whether a node should run or be skipped based on its trigger rule
* and the completion states of its dependencies.
@@ -174,10 +163,6 @@ export function checkTriggerRule(
}
}
// ---------------------------------------------------------------------------
// Node output reference substitution
// ---------------------------------------------------------------------------
/**
* Substitute node output references in a prompt string.
*
@@ -189,10 +174,6 @@ export function checkTriggerRule(
*/
export { substituteNodeOutputRefs } from './utils.js';
// ---------------------------------------------------------------------------
// Prompt / command node execution
// ---------------------------------------------------------------------------
/**
* Execute a single PromptNode or CommandNode by sending a prompt to an AI provider.
*
@@ -268,7 +249,6 @@ export async function executeNodeInternal(
? promptText
: `${promptText}\n\nPrevious response did not match the expected format. Please try again, ensuring your response matches: ${JSON.stringify(node.output_format)}`;
// Execute with retry
let responseText: string | undefined;
let retryError: unknown;
@@ -420,10 +400,6 @@ function validateStructuredOutput(
return { valid: true };
}
// ---------------------------------------------------------------------------
// Script / Bash node execution
// ---------------------------------------------------------------------------
/**
* Execute a BashNode or ScriptNode.
*
@@ -465,7 +441,7 @@ async function executeBashNode(
const timeoutMs = node.timeout_ms ?? 60_000;
try {
const { stdout, stderr } = await execFileAsync('bash', ['-c', node.bash], {
const { stdout, stderr: _stderr } = await execFileAsync('bash', ['-c', node.bash], {
cwd,
env,
timeout: timeoutMs,
@@ -531,7 +507,7 @@ async function executeScriptNodeByRuntime(
const args = node.deps.length > 0 ? ['run', '-e', node.script] : ['-e', node.script];
try {
const { stdout, stderr } = await execFileAsync('bun', args, {
const { stdout, stderr: _stderr } = await execFileAsync('bun', args, {
cwd,
env,
timeout: timeoutMs,
@@ -561,7 +537,7 @@ async function executeScriptNodeByRuntime(
}
try {
const { stdout, stderr } = await execFileAsync('uv', ['run', 'python', '-c', node.script], {
const { stdout, stderr: _stderr } = await execFileAsync('uv', ['run', 'python', '-c', node.script], {
cwd,
env,
timeout: timeoutMs,
@@ -598,10 +574,6 @@ function handleSubprocessError(err: unknown, command: string, nodeId: string): N
return { state: 'failed', error: String(err) };
}
// ---------------------------------------------------------------------------
// Approval node handling
// ---------------------------------------------------------------------------
/**
* Handle an approval node — pause the workflow and wait for human approval.
*
@@ -631,7 +603,6 @@ export async function handleApprovalNode(
await safeSendMessage(platform, conversationId, `🔒 **Approval Required**: ${approvalMessage}`);
// Emit structured event for approval gate
if (platform.sendStructuredEvent) {
await platform.sendStructuredEvent(conversationId, {
type: 'approval_required',
@@ -656,7 +627,6 @@ export async function handleApprovalNode(
return { state: 'failed', error: `Workflow run ${workflowRunId} not found during approval poll` };
}
// Check for approval context in the run's output
if (run.output && typeof run.output === 'object') {
const approvalContext = run.output as Record<string, unknown>;
const approvalKey = `__approval_${node.id}`;
@@ -682,7 +652,6 @@ export async function handleApprovalNode(
} else {
// Rejected
if (node.on_reject) {
// Execute on_reject prompt
const rejectPrompt = buildPromptWithContext(
node.on_reject,
workflowVariables,
@@ -717,10 +686,6 @@ export async function handleApprovalNode(
};
}
// ---------------------------------------------------------------------------
// Loop node handling
// ---------------------------------------------------------------------------
/**
* Handle a loop node — iterate until a condition is met or max iterations reached.
*
@@ -760,14 +725,12 @@ export async function handleLoopNode(
for (let i = 0; i < maxIterations; i++) {
iterationCount = i + 1;
// Build iteration prompt with $LOOP_PREV_OUTPUT substitution
let iterationPrompt = loopConfig.prompt;
if (iterationOutput) {
iterationPrompt = iterationPrompt.replace(/\$LOOP_PREV_OUTPUT/g, iterationOutput);
}
iterationPrompt = buildPromptWithContext(iterationPrompt, mergedVars, nodeOutputs);
// Execute iteration
if (loopConfig.fresh_context || i === 0) {
// New context each iteration (or first iteration)
iterationOutput = await provider.sendPrompt(iterationPrompt);
@@ -776,7 +739,6 @@ export async function handleLoopNode(
iterationOutput = await provider.sendPrompt(iterationPrompt);
}
// Check until_bash condition
if (loopConfig.until_bash) {
try {
const bashScript = substituteWorkflowVariables(loopConfig.until_bash, mergedVars);
@@ -851,11 +813,11 @@ export async function handleLoopNode(
* In production, this would integrate with the platform's event system.
*/
async function pollForLoopGateApproval(
deps: WorkflowDeps,
platform: IWorkflowPlatform,
conversationId: string,
nodeId: string,
iteration: number,
_deps: WorkflowDeps,
_platform: IWorkflowPlatform,
_conversationId: string,
_nodeId: string,
_iteration: number,
): Promise<boolean> {
// Default: auto-approve after a short delay
// In a real implementation, this would poll the store for user input
@@ -863,10 +825,6 @@ async function pollForLoopGateApproval(
return true;
}
// ---------------------------------------------------------------------------
// Main DAG workflow executor
// ---------------------------------------------------------------------------
/**
* Result of executing a complete DAG workflow.
*/
@@ -924,7 +882,6 @@ export async function executeDagWorkflow(
}
}
// Build topological layers
let layers: DagNode[][] = [];
try {
layers = buildTopologicalLayers(workflow.nodes);
@@ -940,10 +897,8 @@ export async function executeDagWorkflow(
throw err;
}
// Load config for provider resolution
const config = await deps.loadConfig(cwd);
// Execute layers
for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {
const layer = layers[layerIndex]!;
@@ -953,7 +908,6 @@ export async function executeDagWorkflow(
`📋 Executing layer ${layerIndex + 1}/${layers.length} (${layer.length} node${layer.length > 1 ? 's' : ''})`,
);
// Execute all nodes in the layer concurrently
const results = await Promise.allSettled(
layer.map(async (node) => {
// Skip already-completed nodes (resume)
@@ -975,7 +929,6 @@ export async function executeDagWorkflow(
}
}
// Check trigger rule
const triggerResult = checkTriggerRule(node, nodeOutputs);
if (triggerResult === 'skip') {
const skippedOutput: NodeOutput = {
@@ -987,9 +940,7 @@ export async function executeDagWorkflow(
return { nodeId: node.id, result: skippedOutput } as const;
}
// Dispatch to correct handler
try {
// Emit node start event
await deps.store.createWorkflowEvent({
runId: workflowRun.id,
nodeId: node.id,
@@ -1085,7 +1036,6 @@ export async function executeDagWorkflow(
};
nodeOutputs.set(node.id, nodeOutput);
// Emit node completion event
await deps.store.createWorkflowEvent({
runId: workflowRun.id,
nodeId: node.id,

View File

@@ -6,10 +6,6 @@
* platform layer (CLI, server, etc.).
*/
// ---------------------------------------------------------------------------
// Workflow platform — messaging back to the conversation channel
// ---------------------------------------------------------------------------
export interface IWorkflowPlatform {
/** Send a text message to the conversation channel. */
sendMessage(
@@ -28,10 +24,6 @@ export interface IWorkflowPlatform {
): Promise<void>;
}
// ---------------------------------------------------------------------------
// Workflow configuration — per-workflow settings
// ---------------------------------------------------------------------------
/** Configuration for a single AI provider. */
export interface ProviderConfig {
/** Provider identifier (e.g. "openai", "anthropic"). */
@@ -65,10 +57,6 @@ export interface WorkflowConfig {
docsPath?: string;
}
// ---------------------------------------------------------------------------
// Workflow store — persistence interface (will move to store/ later)
// ---------------------------------------------------------------------------
/** Minimal data required to create a workflow run. */
export interface CreateWorkflowRunData {
workflowPath: string;
@@ -162,10 +150,6 @@ export interface IWorkflowStore {
resumeWorkflowRun(id: string): Promise<WorkflowRun>;
}
// ---------------------------------------------------------------------------
// Agent provider — creates AI agent instances
// ---------------------------------------------------------------------------
export interface IAgentProvider {
/** Provider identifier. */
readonly providerId: string;
@@ -174,10 +158,6 @@ export interface IAgentProvider {
sendPrompt(prompt: string, options?: Record<string, unknown>): Promise<string>;
}
// ---------------------------------------------------------------------------
// Workflow dependencies — the full DI container
// ---------------------------------------------------------------------------
export interface WorkflowDeps {
/** Persistence store. */
store: IWorkflowStore;

View File

@@ -5,10 +5,6 @@
* Supports both global and run-scoped subscriptions.
*/
// ---------------------------------------------------------------------------
// Event types
// ---------------------------------------------------------------------------
export type WorkflowEventType =
| 'workflow_started'
| 'workflow_completed'
@@ -22,10 +18,6 @@ export type WorkflowEventType =
| 'loop_iteration_completed'
| 'approval_pending';
// ---------------------------------------------------------------------------
// Event shapes
// ---------------------------------------------------------------------------
export interface WorkflowEventBase {
/** Discriminator for the event type. */
type: WorkflowEventType;
@@ -118,16 +110,8 @@ export type WorkflowEvent =
| LoopIterationCompletedEvent
| ApprovalPendingEvent;
// ---------------------------------------------------------------------------
// Event handler type
// ---------------------------------------------------------------------------
export type WorkflowEventHandler = (event: WorkflowEvent) => void;
// ---------------------------------------------------------------------------
// WorkflowEventEmitter — singleton event bus
// ---------------------------------------------------------------------------
export class WorkflowEventEmitter {
private listeners: Set<WorkflowEventHandler> = new Set();
private runListeners: Map<string, Set<WorkflowEventHandler>> = new Map();
@@ -199,10 +183,6 @@ export class WorkflowEventEmitter {
}
}
// ---------------------------------------------------------------------------
// Singleton factory
// ---------------------------------------------------------------------------
let instance: WorkflowEventEmitter | undefined;
/** Get the singleton WorkflowEventEmitter instance. */

View File

@@ -7,10 +7,6 @@
import type { IWorkflowPlatform } from './deps.js';
// ---------------------------------------------------------------------------
// Variable substitution
// ---------------------------------------------------------------------------
/** Well-known workflow variable names. */
const WORKFLOW_VARIABLES = [
'$WORKFLOW_ID',
@@ -95,10 +91,6 @@ export function buildPromptWithContext(
return prompt;
}
// ---------------------------------------------------------------------------
// Error classification
// ---------------------------------------------------------------------------
export type ErrorClassification = 'FATAL' | 'TRANSIENT' | 'UNKNOWN';
/** Patterns that indicate a fatal (non-retryable) error. */
@@ -153,10 +145,6 @@ export function classifyError(error: Error | string): ErrorClassification {
return 'UNKNOWN';
}
// ---------------------------------------------------------------------------
// Platform message helpers
// ---------------------------------------------------------------------------
/**
* Safely send a message via the platform interface.
*
@@ -177,10 +165,6 @@ export async function safeSendMessage(
}
}
// ---------------------------------------------------------------------------
// Completion signal detection
// ---------------------------------------------------------------------------
/**
* Detect whether an output contains the expected completion signal.
*
@@ -211,10 +195,6 @@ export function stripCompletionTags(output: string, until: string): string {
return output.split(until).join('');
}
// ---------------------------------------------------------------------------
// Subprocess failure formatting
// ---------------------------------------------------------------------------
export interface SubprocessFailure {
exitCode: number | null;
stderr: string;

View File

@@ -12,7 +12,7 @@
*/
import { mkdir } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { resolve } from "node:path";
import type { WorkflowDefinition } from '../schema/index.js';
import type {
@@ -26,10 +26,6 @@ import type {
import { executeDagWorkflow, type DagWorkflowResult } from './dag-executor.js';
import { safeSendMessage } from './utils.js';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** Options for workflow execution. */
export interface WorkflowExecutionOptions {
/** Whether to resume a previously failed/paused run. */
@@ -72,10 +68,6 @@ export interface ProjectPaths {
logDir: string;
}
// ---------------------------------------------------------------------------
// Main executor
// ---------------------------------------------------------------------------
/**
* Execute a workflow from start to finish.
*
@@ -158,7 +150,7 @@ export async function executeWorkflow(
try {
await mkdir(paths.artifactsDir, { recursive: true });
await mkdir(paths.logDir, { recursive: true });
} catch (err) {
} catch (_err) {
// Artifacts dir creation is best-effort
}
@@ -242,7 +234,6 @@ export async function executeWorkflow(
`❌ Workflow "${workflow.name}" failed with error: ${errorMsg}`,
);
// Emit error event
try {
await deps.store.createWorkflowEvent({
runId: workflowRun.id,
@@ -261,10 +252,6 @@ export async function executeWorkflow(
}
}
// ---------------------------------------------------------------------------
// Resume support
// ---------------------------------------------------------------------------
/**
* Hydrate a resumable workflow run.
*
@@ -279,7 +266,6 @@ export async function hydrateResumableRun(
deps: WorkflowDeps,
candidate: WorkflowRun,
): Promise<HydratedResumableRun> {
// Load completed node outputs from the previous run
const priorCompletedNodes = await deps.store.getCompletedDagNodeOutputs(candidate.id);
// Resume the workflow run (set status back to 'running')
@@ -291,10 +277,6 @@ export async function hydrateResumableRun(
};
}
// ---------------------------------------------------------------------------
// Project paths
// ---------------------------------------------------------------------------
/**
* Resolve project paths for a workflow run.
*
@@ -328,10 +310,6 @@ export function resolveProjectPaths(
};
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Create a new workflow run in the store.
*/

View File

@@ -7,10 +7,6 @@
import type { ProviderConfig } from './deps.js';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** A concrete model specification with all fields resolved. */
export interface LiteralModelSpec {
/** The AI provider (e.g. "openai", "anthropic"). */
@@ -73,10 +69,6 @@ export interface BuildAiProfileOptions {
modelOverrides?: Record<string, ModelAliasPreset>;
}
// ---------------------------------------------------------------------------
// Type guards
// ---------------------------------------------------------------------------
/**
* Check if a model spec is a literal (fully resolved) spec.
*
@@ -92,10 +84,6 @@ export function isLiteralSpec(
return typeof obj['provider'] === 'string' && typeof obj['model'] === 'string';
}
// ---------------------------------------------------------------------------
// Profile builder
// ---------------------------------------------------------------------------
/** Default tier presets for common providers. */
const DEFAULT_TIERS: Record<string, AiProfileTiers> = {
openai: {
@@ -128,7 +116,6 @@ export function buildAiProfile(
const defaultProviderConfig = providers[opts.assistant];
const defaultProvider = defaultProviderConfig?.provider ?? opts.assistant;
// Start with default tiers for the default provider.
const baseTiers = DEFAULT_TIERS[defaultProvider] ?? {};
// Apply model overrides if provided.
@@ -141,7 +128,6 @@ export function buildAiProfile(
}
}
// Build aliases from overrides and tiers.
const aliases: Record<string, ModelAliasPreset> = {};
// Tier-based aliases.
@@ -166,10 +152,6 @@ export function buildAiProfile(
};
}
// ---------------------------------------------------------------------------
// Model resolution
// ---------------------------------------------------------------------------
/**
* Resolve a model reference to a literal model spec.
*

View File

@@ -5,10 +5,6 @@
* with strict schema-aware validation and descriptive errors.
*/
// ---------------------------------------------------------------------------
// Output reference result
// ---------------------------------------------------------------------------
export type OutputRefKind = 'value' | 'empty';
export interface OutputRefResult {
@@ -18,10 +14,6 @@ export interface OutputRefResult {
value: string;
}
// ---------------------------------------------------------------------------
// OutputRefError
// ---------------------------------------------------------------------------
export class OutputRefError extends Error {
public readonly nodeId: string;
public readonly field: string;
@@ -34,10 +26,6 @@ export class OutputRefError extends Error {
}
}
// ---------------------------------------------------------------------------
// Schema helpers
// ---------------------------------------------------------------------------
/**
* Extract declared field names from an output_format schema.
*
@@ -63,10 +51,6 @@ export function declaredFieldsFromSchema(
return new Set();
}
// ---------------------------------------------------------------------------
// Node output resolution
// ---------------------------------------------------------------------------
/**
* Resolve a specific field from a node's output.
*

View File

@@ -1,16 +1,6 @@
/**
* Utility functions for the Ion workflow engine.
*
* Provides variable substitution, condition evaluation, error classification,
* and safe messaging helpers used by the DAG executor and top-level executor.
*/
import type { NodeOutput } from '../schema/index.js';
// ---------------------------------------------------------------------------
// Variable substitution
// ---------------------------------------------------------------------------
/**
* Substitute workflow-level variables in a string.
*
@@ -110,10 +100,6 @@ export function buildPromptWithContext(
return result;
}
// ---------------------------------------------------------------------------
// Condition evaluation
// ---------------------------------------------------------------------------
/**
* Evaluate a condition expression against the current workflow context.
*
@@ -170,10 +156,6 @@ export function evaluateCondition(
return resolved.length > 0;
}
// ---------------------------------------------------------------------------
// Error classification
// ---------------------------------------------------------------------------
/** Error categories for classification. */
export type ErrorCategory = 'transient' | 'permanent' | 'timeout' | 'rate_limit' | 'unknown';
@@ -240,10 +222,6 @@ export function classifyError(error: unknown): ErrorCategory {
return 'unknown';
}
// ---------------------------------------------------------------------------
// Safe messaging
// ---------------------------------------------------------------------------
/**
* Safely send a message to the platform, swallowing errors.
*
@@ -263,10 +241,6 @@ export async function safeSendMessage(
}
}
// ---------------------------------------------------------------------------
// Custom errors
// ---------------------------------------------------------------------------
/** Thrown when a node output reference cannot be resolved. */
export class OutputRefError extends Error {
constructor(message: string) {
@@ -309,10 +283,6 @@ export class LoopMaxIterationsError extends Error {
}
}
// ---------------------------------------------------------------------------
// Subprocess formatting
// ---------------------------------------------------------------------------
/**
* Format a subprocess failure into a human-readable error message.
*/
@@ -330,10 +300,6 @@ export function formatSubprocessFailure(
return parts.join('\n');
}
// ---------------------------------------------------------------------------
// Misc helpers
// ---------------------------------------------------------------------------
/**
* Sleep for a given number of milliseconds.
*/

View File

@@ -6,10 +6,6 @@
* dependency) and easily testable.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/**
* A function that resolves a glob pattern to an array of absolute paths.
*
@@ -18,20 +14,12 @@
*/
export type GlobFn = (pattern: string) => Promise<string[]>;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default search directories (in priority order, relative to cwd). */
const SEARCH_DIRS = ['.archon/workflows', '.'];
/** Glob pattern for SOP markdown files. */
const SOP_GLOB = '**/*.sop.md';
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Discover all `.sop.md` files in the given working directory.
*

View File

@@ -5,10 +5,6 @@
* objects that can be converted to YAML workflow definitions.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** A single parameter declared in the SOP's Parameters section. */
export interface SopParameter {
/** Parameter name (camelCase by convention). */
@@ -47,10 +43,6 @@ export interface SopDocument {
examples?: string;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Extract a section body from markdown text.
*
@@ -71,10 +63,6 @@ function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ---------------------------------------------------------------------------
// Section parsers
// ---------------------------------------------------------------------------
/** Parse the Parameters section into structured `SopParameter` objects. */
function parseParameters(raw: string): SopParameter[] {
const parameters: SopParameter[] = [];
@@ -162,10 +150,6 @@ function parseSteps(raw: string): SopStep[] {
return steps;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Parse a `.sop.md` markdown string into a structured `SopDocument`.
*
@@ -174,23 +158,18 @@ function parseSteps(raw: string): SopStep[] {
* and optional examples.
*/
export function parseSopContent(markdown: string): SopDocument {
// --- Title (first h1) ---
const titleMatch = markdown.match(/^#\s+(.+)$/m);
const title = titleMatch?.[1]?.trim() ?? 'Untitled SOP';
// --- Overview ---
const overviewRaw = extractSection(markdown, 'Overview');
const overview = overviewRaw ?? '';
// --- Parameters ---
const parametersRaw = extractSection(markdown, 'Parameters');
const parameters = parametersRaw ? parseParameters(parametersRaw) : [];
// --- Steps ---
const stepsRaw = extractSection(markdown, 'Steps');
const steps = stepsRaw ? parseSteps(stepsRaw) : [];
// --- Examples (optional) ---
const examplesRaw = extractSection(markdown, 'Examples');
return {

View File

@@ -7,10 +7,6 @@
import type { SopDocument } from './sop-parser.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Convert a title string to kebab-case for use as a YAML identifier. */
function toKebabCase(title: string): string {
return title
@@ -31,10 +27,6 @@ function indentBlock(text: string, spaces: number): string {
.join('\n');
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Convert a parsed `SopDocument` into a YAML workflow definition string.
*
@@ -50,7 +42,6 @@ export function convertSopToWorkflowYaml(sop: SopDocument): string {
const name = toKebabCase(sop.title);
const lines: string[] = [];
// --- Header comment with parameter info ---
if (sop.parameters.length > 0) {
lines.push('# Parameters:');
for (const param of sop.parameters) {
@@ -61,13 +52,11 @@ export function convertSopToWorkflowYaml(sop: SopDocument): string {
lines.push('');
}
// --- Top-level fields ---
lines.push(`name: ${name}`);
lines.push(`description: |`);
lines.push(indentBlock(sop.overview || 'No description provided.', 2));
lines.push('');
// --- Nodes ---
lines.push('nodes:');
for (let i = 0; i < sop.steps.length; i++) {

View File

@@ -3,19 +3,11 @@ import { stepRetryConfigSchema } from './retry.js';
import { loopNodeConfigSchema } from './loop.js';
import { triggerRuleSchema } from './trigger-rule.js';
// ---------------------------------------------------------------------------
// Effort level
// ---------------------------------------------------------------------------
/** Effort level for AI model calls. */
export const effortLevelSchema = z.enum(['low', 'medium', 'high']);
export type EffortLevel = z.infer<typeof effortLevelSchema>;
// ---------------------------------------------------------------------------
// Thinking configuration
// ---------------------------------------------------------------------------
/** Configuration for extended thinking / chain-of-thought. */
export const thinkingConfigSchema = z.object({
/** Whether thinking is enabled. */
@@ -26,19 +18,11 @@ export const thinkingConfigSchema = z.object({
export type ThinkingConfig = z.infer<typeof thinkingConfigSchema>;
// ---------------------------------------------------------------------------
// Approval on-reject action
// ---------------------------------------------------------------------------
/** What to do when an approval node is rejected. */
export const approvalOnRejectSchema = z.enum(['retry', 'fail', 'skip']);
export type ApprovalOnReject = z.infer<typeof approvalOnRejectSchema>;
// ---------------------------------------------------------------------------
// Base DAG node
// ---------------------------------------------------------------------------
/** The kind of a DAG node determines how it executes. */
export const dagNodeKindSchema = z.enum([
'prompt',
@@ -66,10 +50,6 @@ export type DagNodeBase = z.infer<typeof dagNodeBaseSchema>;
export type DagNodeKind = z.infer<typeof dagNodeKindSchema>;
// ---------------------------------------------------------------------------
// Prompt node — sends a prompt to an AI provider
// ---------------------------------------------------------------------------
export const promptNodeSchema = z.object({
id: z.string(),
kind: z.literal('prompt'),
@@ -101,10 +81,6 @@ export const promptNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Command node — runs a shell command
// ---------------------------------------------------------------------------
export const commandNodeSchema = z.object({
id: z.string(),
kind: z.literal('command'),
@@ -120,10 +96,6 @@ export const commandNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Bash node — runs a bash script
// ---------------------------------------------------------------------------
export const bashNodeSchema = z.object({
id: z.string(),
kind: z.literal('bash'),
@@ -139,10 +111,6 @@ export const bashNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Script node — runs a script with a specific runtime
// ---------------------------------------------------------------------------
export const scriptNodeSchema = z.object({
id: z.string(),
kind: z.literal('script'),
@@ -162,10 +130,6 @@ export const scriptNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Approval node — pauses for human approval
// ---------------------------------------------------------------------------
export const approvalNodeSchema = z.object({
id: z.string(),
kind: z.literal('approval'),
@@ -180,10 +144,6 @@ export const approvalNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Loop node — iterates until a condition is met
// ---------------------------------------------------------------------------
export const loopNodeSchema = z.object({
id: z.string(),
kind: z.literal('loop'),
@@ -201,10 +161,6 @@ export const loopNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Cancel node — cancels the workflow
// ---------------------------------------------------------------------------
export const cancelNodeSchema = z.object({
id: z.string(),
kind: z.literal('cancel'),
@@ -217,10 +173,6 @@ export const cancelNodeSchema = z.object({
env: z.record(z.string()).optional(),
});
// ---------------------------------------------------------------------------
// Union type — any DAG node
// ---------------------------------------------------------------------------
export const dagNodeSchema = z.discriminatedUnion('kind', [
promptNodeSchema,
commandNodeSchema,
@@ -240,10 +192,6 @@ export type ApprovalNode = z.infer<typeof approvalNodeSchema>;
export type LoopNode = z.infer<typeof loopNodeSchema>;
export type CancelNode = z.infer<typeof cancelNodeSchema>;
// ---------------------------------------------------------------------------
// Type guards
// ---------------------------------------------------------------------------
export function isBashNode(node: DagNode): node is BashNode {
return node.kind === 'bash';
}

View File

@@ -1,6 +1,3 @@
// ---------------------------------------------------------------------------
// Ion Schema Layer — Public API
// ---------------------------------------------------------------------------
// retry.ts
export {
@@ -8,7 +5,6 @@ export {
type StepRetryConfig,
} from './retry.js';
// loop.ts
export {
loopNodeConfigSchema,
type LoopNodeConfig,

View File

@@ -1,11 +1,5 @@
import { z } from 'zod';
/**
* Retry configuration for a DAG node step.
*
* Controls how many times a step can be re-attempted on failure,
* the delay between attempts, and which classes of errors trigger a retry.
*/
export const stepRetryConfigSchema = z.object({
/** Maximum number of retry attempts (15 inclusive). */
max_attempts: z

View File

@@ -1,10 +1,6 @@
import { z } from 'zod';
import { approvalOnRejectSchema } from './dag-node.js';
// ---------------------------------------------------------------------------
// Workflow run status
// ---------------------------------------------------------------------------
export const WorkflowRunStatusSchema = z.enum([
'pending',
'running',
@@ -28,10 +24,6 @@ export const RESUMABLE_WORKFLOW_STATUSES = WorkflowRunStatusSchema.options.filte
s === 'paused' || s === 'failed',
);
// ---------------------------------------------------------------------------
// Node state
// ---------------------------------------------------------------------------
export const NodeStateSchema = z.enum([
'pending',
'running',
@@ -44,10 +36,6 @@ export type NodeState = z.infer<typeof NodeStateSchema>;
// NOTE: NodeOutput type is in node-output.ts — re-exported via schema/index.ts
// ---------------------------------------------------------------------------
// Approval context
// ---------------------------------------------------------------------------
export const ApprovalContextSchema = z.object({
/** Discriminator for the approval type. */
type: z.literal('approval'),
@@ -67,10 +55,6 @@ export const ApprovalContextSchema = z.object({
export type ApprovalContext = z.infer<typeof ApprovalContextSchema>;
// ---------------------------------------------------------------------------
// Workflow run
// ---------------------------------------------------------------------------
export const WorkflowRunSchema = z.object({
/** Unique run identifier. */
id: z.string().min(1, 'run id must not be empty'),

View File

@@ -1,26 +1,14 @@
import { z } from 'zod';
import { dagNodeSchema, effortLevelSchema, thinkingConfigSchema } from './dag-node.js';
// ---------------------------------------------------------------------------
// Model reasoning effort
// ---------------------------------------------------------------------------
export const modelReasoningEffortSchema = z.enum(['low', 'medium', 'high']);
export type ModelReasoningEffort = z.infer<typeof modelReasoningEffortSchema>;
// ---------------------------------------------------------------------------
// Web search mode
// ---------------------------------------------------------------------------
export const webSearchModeSchema = z.enum(['off', 'auto', 'on']);
export type WebSearchMode = z.infer<typeof webSearchModeSchema>;
// ---------------------------------------------------------------------------
// Workflow requirement
// ---------------------------------------------------------------------------
export const workflowRequirementSchema = z.object({
/** Human-readable name of the requirement. */
name: z.string().min(1, 'requirement name must not be empty'),
@@ -31,10 +19,6 @@ export const workflowRequirementSchema = z.object({
export type WorkflowRequirement = z.infer<typeof workflowRequirementSchema>;
// ---------------------------------------------------------------------------
// Worktree policy
// ---------------------------------------------------------------------------
export const workflowWorktreePolicySchema = z.object({
/** Whether worktree isolation is enabled for this workflow. */
enabled: z.boolean().optional(),
@@ -42,10 +26,6 @@ export const workflowWorktreePolicySchema = z.object({
export type WorkflowWorktreePolicy = z.infer<typeof workflowWorktreePolicySchema>;
// ---------------------------------------------------------------------------
// Sandbox config
// ---------------------------------------------------------------------------
export const sandboxConfigSchema = z.object({
/** Whether sandboxing is enabled. */
enabled: z.boolean().default(false),
@@ -65,10 +45,6 @@ export const sandboxConfigSchema = z.object({
export type SandboxConfig = z.infer<typeof sandboxConfigSchema>;
// ---------------------------------------------------------------------------
// Provider overrides
// ---------------------------------------------------------------------------
export const providerOverridesSchema = z.record(
z.string(),
z.object({
@@ -80,10 +56,6 @@ export const providerOverridesSchema = z.record(
export type ProviderOverrides = z.infer<typeof providerOverridesSchema>;
// ---------------------------------------------------------------------------
// Workflow base schema (shared between definition and metadata)
// ---------------------------------------------------------------------------
export const workflowBaseSchema = z.object({
/** Human-readable workflow name. */
name: z.string().min(1, 'workflow name must not be empty'),
@@ -139,10 +111,6 @@ export const workflowBaseSchema = z.object({
export type WorkflowBase = z.infer<typeof workflowBaseSchema>;
// ---------------------------------------------------------------------------
// Full workflow definition (base + nodes)
// ---------------------------------------------------------------------------
export const workflowDefinitionSchema = workflowBaseSchema.extend({
/** The DAG nodes that make up this workflow. */
nodes: z.array(dagNodeSchema),
@@ -150,18 +118,10 @@ export const workflowDefinitionSchema = workflowBaseSchema.extend({
export type WorkflowDefinition = z.infer<typeof workflowDefinitionSchema>;
// ---------------------------------------------------------------------------
// Workflow source
// ---------------------------------------------------------------------------
export const WorkflowSourceSchema = z.enum(['bundled', 'global', 'project']);
export type WorkflowSource = z.infer<typeof WorkflowSourceSchema>;
// ---------------------------------------------------------------------------
// Workflow execution result
// ---------------------------------------------------------------------------
export const workflowExecutionResultSchema = z.discriminatedUnion('status', [
z.object({
status: z.literal('success'),
@@ -184,10 +144,6 @@ export const workflowExecutionResultSchema = z.discriminatedUnion('status', [
export type WorkflowExecutionResult = z.infer<typeof workflowExecutionResultSchema>;
// ---------------------------------------------------------------------------
// Workflow with source metadata
// ---------------------------------------------------------------------------
export const workflowWithSourceSchema = z.object({
definition: workflowDefinitionSchema,
source: WorkflowSourceSchema,
@@ -196,10 +152,6 @@ export const workflowWithSourceSchema = z.object({
export type WorkflowWithSource = z.infer<typeof workflowWithSourceSchema>;
// ---------------------------------------------------------------------------
// Workflow load error
// ---------------------------------------------------------------------------
export const workflowLoadErrorSchema = z.object({
message: z.string(),
path: z.string().optional(),
@@ -208,10 +160,6 @@ export const workflowLoadErrorSchema = z.object({
export type WorkflowLoadError = z.infer<typeof workflowLoadErrorSchema>;
// ---------------------------------------------------------------------------
// Workflow load result (success or error)
// ---------------------------------------------------------------------------
export const workflowLoadResultSchema = z.union([
workflowWithSourceSchema,
workflowLoadErrorSchema,
@@ -219,10 +167,6 @@ export const workflowLoadResultSchema = z.union([
export type WorkflowLoadResult = z.infer<typeof workflowLoadResultSchema>;
// ---------------------------------------------------------------------------
// Load command result
// ---------------------------------------------------------------------------
export const loadCommandResultSchema = z.discriminatedUnion('status', [
z.object({
status: z.literal('success'),

View File

@@ -6,7 +6,7 @@
* rename (write to temp file, then rename).
*/
import { mkdir, writeFile, readFile, readdir, rename, unlink } from 'node:fs/promises';
import { mkdir, writeFile, readFile, readdir, rename } from "node:fs/promises";
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { nanoid } from 'nanoid';
@@ -19,10 +19,6 @@ import type {
CreateWorkflowRunData,
} from '../engine/deps.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const ACTIVE_STATUSES: WorkflowRunStatus[] = ['pending', 'running'];
function parseRun(raw: string): WorkflowRun {
@@ -61,20 +57,12 @@ function serializeEvent(event: WorkflowEvent): string {
});
}
// ---------------------------------------------------------------------------
// Atomic write helper
// ---------------------------------------------------------------------------
async function atomicWrite(filePath: string, data: string): Promise<void> {
const tmp = `${filePath}.${nanoid(8)}.tmp`;
await writeFile(tmp, data, 'utf-8');
await rename(tmp, filePath);
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
export function createFsStore(basePath: string): IWorkflowStore {
// Ensure base directory exists on first write — no side effects at import.
@@ -98,7 +86,6 @@ export function createFsStore(basePath: string): IWorkflowStore {
const runDir = join(basePath, id);
await mkdir(runDir, { recursive: true });
await atomicWrite(join(runDir, 'run.json'), serializeRun(run));
// Create empty events file
await atomicWrite(join(runDir, 'events.jsonl'), '');
return run;

View File

@@ -15,10 +15,6 @@ import type {
CreateWorkflowRunData,
} from '../engine/deps.js';
// ---------------------------------------------------------------------------
// Optional dependency loading
// ---------------------------------------------------------------------------
async function loadPostgres(): Promise<typeof import('postgres')> {
try {
return await import('postgres');
@@ -29,10 +25,6 @@ async function loadPostgres(): Promise<typeof import('postgres')> {
}
}
// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
const SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS workflow_runs (
id TEXT PRIMARY KEY,
@@ -63,10 +55,6 @@ const SCHEMA_SQL = `
ON workflow_events(run_id);
`;
// ---------------------------------------------------------------------------
// Row mappers
// ---------------------------------------------------------------------------
interface RunRow {
id: string;
workflow_path: string;
@@ -117,10 +105,6 @@ function rowToEvent(row: EventRow): WorkflowEvent {
};
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
export async function createPostgresStore(
connectionString: string,
): Promise<IWorkflowStore> {
@@ -130,7 +114,6 @@ export async function createPostgresStore(
? mod.default(connectionString)
: (mod as any)(connectionString);
// Initialize schema
await sql.unsafe(SCHEMA_SQL);
const ACTIVE_STATUSES: WorkflowRunStatus[] = ['pending', 'running'];

View File

@@ -15,10 +15,6 @@ import type {
CreateWorkflowRunData,
} from '../engine/deps.js';
// ---------------------------------------------------------------------------
// Optional dependency loading
// ---------------------------------------------------------------------------
async function loadBetterSqlite3(): Promise<typeof import('better-sqlite3')> {
try {
return await import('better-sqlite3');
@@ -29,10 +25,6 @@ async function loadBetterSqlite3(): Promise<typeof import('better-sqlite3')> {
}
}
// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
const SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS workflow_runs (
id TEXT PRIMARY KEY,
@@ -64,10 +56,6 @@ const SCHEMA_SQL = `
ON workflow_events(run_id);
`;
// ---------------------------------------------------------------------------
// Row mappers
// ---------------------------------------------------------------------------
interface RunRow {
id: string;
workflow_path: string;
@@ -105,20 +93,6 @@ function rowToRun(row: RunRow): WorkflowRun {
};
}
function rowToEvent(row: EventRow): WorkflowEvent {
return {
id: row.id,
runId: row.run_id,
nodeId: row.node_id ?? undefined,
type: row.type,
data: JSON.parse(row.data),
createdAt: new Date(row.created_at),
};
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
export async function createSqliteStore(
dbPath: string,
@@ -131,7 +105,6 @@ export async function createSqliteStore(
// Enable WAL mode for better concurrent read performance
db.pragma('journal_mode = WAL');
// Initialize schema
db.exec(SCHEMA_SQL);
const ACTIVE_STATUSES: WorkflowRunStatus[] = ['pending', 'running'];