# Investigation: Workflow Error Visibility (#260, #263, #264)

**Issues**: #260, #263, #264
**Type**: ENHANCEMENT
**Investigated**: 2026-02-17T12:00:00Z

### Assessment

| Metric     | Value  | Reasoning                                                                                                            |
| ---------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| Priority   | MEDIUM | Users can't debug workflow failures; workaround is reading server logs, which most won't do                          |
| Complexity | MEDIUM | 4 files to update, touches loader return types + command handler + router + types, but each change is self-contained |
| Confidence | HIGH   | Clear root cause in code, existing `DiscoverWorkflowsResult` type already defined but unused                         |

---

## Problem Statement

The workflow system silently swallows errors at two key points: (1) the loader filters out workflows that fail to parse/read without telling users, and (2) the router silently falls back when AI outputs an invalid workflow name. Additionally, minor code cleanup is needed in executor.ts and types.ts. Together these issues mean users have zero visibility into workflow loading failures or routing problems.

---

## Analysis

### Root Cause / Change Rationale

**#260 - Loader errors invisible:**
The `loadWorkflowsFromDir` function catches errors at the directory level but not per-file. `parseWorkflow` returns `null` for any validation failure with only `getLog().warn()` logging. The `discoverWorkflows` function returns `WorkflowDefinition[]` with no way to communicate partial failures.

**#263 - Router silent fallback:**
`parseWorkflowInvocation` returns `{ workflowName: null }` when workflow name doesn't match. The `tryWorkflowRouting` in orchestrator returns `false`, causing the raw AI response (with the malformed `/invoke-workflow` command) to be sent to the user with no explanation.

**#264 - Code cleanup:**
Duplicate JSDoc on `executeStepInternal`, unused `StepDefinition` alias, missing `@returns` on `loadCommandPrompt`, misleading `clearContext` comment.

### Evidence Chain

WHY: Users don't know which workflows failed to load
- BECAUSE: `discoverWorkflows()` returns `WorkflowDefinition[]` - no error channel
  Evidence: `loader.ts:294` - `Promise<WorkflowDefinition[]>`
- BECAUSE: `parseWorkflow()` returns `null` on failure, caller filters silently
  Evidence: `loader.ts:239-242` - `if (workflow) { workflows.set(entry, workflow); }`
- BECAUSE: `loadWorkflowsFromDir` has no per-file try-catch, so one failure aborts the loop
  Evidence: `loader.ts:225-243` - `readFile` and `stat` calls not individually wrapped

WHY: Invalid workflow names produce no user feedback
- BECAUSE: `parseWorkflowInvocation` returns null workflowName with no error info
  Evidence: `router.ts:180-183` - `return { workflowName: null, remainingMessage: message }`
- BECAUSE: `tryWorkflowRouting` just returns `false` on null
  Evidence: `orchestrator.ts:463-464` - `if (!workflowName) { return false; }`

### Affected Files

| File                                                    | Lines   | Action | Description                                                              |
| ------------------------------------------------------- | ------- | ------ | ------------------------------------------------------------------------ |
| `packages/core/src/workflows/types.ts`                  | 24-27   | UPDATE | Add `WorkflowLoadError` type, fix `StepDefinition`, fix `clearContext`   |
| `packages/core/src/workflows/loader.ts`                 | 94-400  | UPDATE | Return `WorkflowLoadResult` with errors, per-file try-catch              |
| `packages/core/src/workflows/router.ts`                 | 143-184 | UPDATE | Add error info to `WorkflowInvocation`, case-insensitive matching        |
| `packages/core/src/handlers/command-handler.ts`         | 1382-04 | UPDATE | Show workflow load errors in `/workflow list`                            |
| `packages/core/src/orchestrator/orchestrator.ts`        | 458-464 | UPDATE | Handle router error, send user message for invalid workflow              |
| `packages/core/src/workflows/executor.ts`               | 334-345 | UPDATE | Add `@returns` JSDoc, remove duplicate JSDoc                             |
| `packages/core/src/workflows/executor.ts`               | 568-574 | UPDATE | Remove orphaned JSDoc comment                                            |
| `packages/core/src/workflows/loader.test.ts`            | -       | UPDATE | Add tests for error accumulation                                         |
| `packages/core/src/workflows/router.test.ts`            | -       | UPDATE | Add tests for error info and case-insensitive matching                   |

### Integration Points

- `command-handler.ts:1385` calls `discoverWorkflows` for `/workflow list`
- `command-handler.ts:1503` calls `discoverWorkflows` for `/workflow run`
- `orchestrator.ts:914` calls `discoverWorkflows` during message handling
- `orchestrator.ts:458` calls `parseWorkflowInvocation` during auto-routing
- `cli/commands/workflow.ts:59,85` calls `discoverWorkflows` for CLI commands

### Git History

- **Logging added**: `c7cd67e` - Add Pino structured logging infrastructure (Phase 1) (#388)
- **Router improved**: `d9c318d` - fix: Prioritize user intent over context in workflow router (#365)
- **Error tracking added**: `680c86c` - fix: Track consecutive unknown errors in workflow executor (#259)

---

## Implementation Plan

### Step 1: Add `WorkflowLoadError` type and update `DiscoverWorkflowsResult`

**File**: `packages/core/src/workflows/types.ts`
**Lines**: 24-27, 142-148
**Action**: UPDATE

**Current code (lines 24-27):**
```typescript
/**
 * Step definition from YAML workflow file (alias for backward compatibility)
 */
export type StepDefinition = SingleStep;
```

**Required change:**
```typescript
/**
 * @deprecated Use SingleStep directly. Alias kept for external consumers.
 */
export type StepDefinition = SingleStep;
```

**Current code (lines 19-22):**
```typescript
export interface SingleStep {
  command: string;
  clearContext?: boolean; // For sequential: controls session. For parallel: always fresh (ignored)
}
```

**Required change:**
```typescript
export interface SingleStep {
  command: string;
  /** Controls session continuity between steps. When true, creates a fresh session.
   *  Only applies to sequential execution; parallel blocks always use fresh sessions. */
  clearContext?: boolean;
}
```

**Current code (lines 142-148):**
```typescript
/**
 * Result of workflow discovery - distinguishes "no workflows" from "error loading"
 */
export type DiscoverWorkflowsResult =
  | { success: true; workflows: WorkflowDefinition[] }
  | { success: false; error: string };
```

**Required change:**
```typescript
/**
 * Error encountered while loading a workflow file
 */
export interface WorkflowLoadError {
  filename: string;
  error: string;
  errorType: 'read_error' | 'parse_error' | 'validation_error';
}

/**
 * Result of workflow discovery - includes both successful loads and errors
 */
export interface WorkflowLoadResult {
  workflows: WorkflowDefinition[];
  errors: WorkflowLoadError[];
}
```

**Why**: The existing `DiscoverWorkflowsResult` type is unused and uses a success/failure discriminated union that can't express partial success. The new `WorkflowLoadResult` returns both workflows AND errors, allowing callers to surface error info.

---

### Step 2: Update `parseWorkflow` to return structured errors

**File**: `packages/core/src/workflows/loader.ts`
**Lines**: 94-213
**Action**: UPDATE

**Current code:**
```typescript
function parseWorkflow(content: string, filename: string): WorkflowDefinition | null {
```

**Required change:**

Add a new type for the return value and update `parseWorkflow`:

```typescript
type ParseResult =
  | { workflow: WorkflowDefinition; error: null }
  | { workflow: null; error: WorkflowLoadError };

function parseWorkflow(content: string, filename: string): ParseResult {
  try {
    const raw = parseYaml(content) as Record<string, unknown>;

    if (!raw.name || typeof raw.name !== 'string') {
      getLog().warn({ filename }, 'workflow_missing_name');
      return {
        workflow: null,
        error: { filename, error: "Missing required field 'name'", errorType: 'validation_error' },
      };
    }
    if (!raw.description || typeof raw.description !== 'string') {
      getLog().warn({ filename }, 'workflow_missing_description');
      return {
        workflow: null,
        error: {
          filename,
          error: "Missing required field 'description'",
          errorType: 'validation_error',
        },
      };
    }

    // ... (same validation logic for steps/loop/prompt, but return error objects instead of null)

    if (hasSteps && hasLoop) {
      getLog().warn({ filename }, 'workflow_steps_and_loop_conflict');
      return {
        workflow: null,
        error: {
          filename,
          error: "Cannot have both 'steps' and 'loop' (mutually exclusive)",
          errorType: 'validation_error',
        },
      };
    }

    if (hasLoop && !hasPrompt) {
      getLog().warn({ filename }, 'workflow_loop_missing_prompt');
      return {
        workflow: null,
        error: {
          filename,
          error: "Loop workflows require a 'prompt' field",
          errorType: 'validation_error',
        },
      };
    }

    if (!hasSteps && !hasLoop) {
      getLog().warn({ filename }, 'workflow_missing_steps_or_loop');
      return {
        workflow: null,
        error: {
          filename,
          error: "Missing 'steps' or 'loop' configuration",
          errorType: 'validation_error',
        },
      };
    }

    // ... loop config validation returns error objects ...

    // Step validation errors
    if (steps.length !== (raw.steps as unknown[]).length) {
      getLog().warn({ filename, validationErrors }, 'workflow_step_validation_failed');
      return {
        workflow: null,
        error: {
          filename,
          error: `Step validation failed: ${validationErrors.join('; ')}`,
          errorType: 'validation_error',
        },
      };
    }

    // ... success path returns { workflow: <definition>, error: null }
  } catch (error) {
    const err = error as Error;
    const linePattern = /line (\d+)/i;
    const lineMatch = linePattern.exec(err.message);
    const lineInfo = lineMatch ? ` (near line ${lineMatch[1]})` : '';
    getLog().error({ err, filename, lineInfo: lineInfo || undefined }, 'workflow_parse_failed');
    return {
      workflow: null,
      error: {
        filename,
        error: `YAML parse error${lineInfo}: ${err.message}`,
        errorType: 'parse_error',
      },
    };
  }
}
```

**Why**: Structured error returns allow callers to accumulate and surface errors without losing information.

---

### Step 3: Update `loadWorkflowsFromDir` with per-file try-catch and error accumulation

**File**: `packages/core/src/workflows/loader.ts`
**Lines**: 219-255
**Action**: UPDATE

**Current code:**
```typescript
async function loadWorkflowsFromDir(dirPath: string): Promise<Map<string, WorkflowDefinition>> {
  const workflows = new Map<string, WorkflowDefinition>();

  try {
    const entries = await readdir(dirPath);

    for (const entry of entries) {
      const entryPath = join(dirPath, entry);
      const entryStat = await stat(entryPath);
      // ... no per-file error handling
    }
  } catch (error) {
    // ... catches everything at directory level
  }

  return workflows;
}
```

**Required change:**

```typescript
interface DirLoadResult {
  workflows: Map<string, WorkflowDefinition>;
  errors: WorkflowLoadError[];
}

async function loadWorkflowsFromDir(dirPath: string): Promise<DirLoadResult> {
  const workflows = new Map<string, WorkflowDefinition>();
  const errors: WorkflowLoadError[] = [];

  try {
    const entries = await readdir(dirPath);

    for (const entry of entries) {
      const entryPath = join(dirPath, entry);

      try {
        const entryStat = await stat(entryPath);

        if (entryStat.isDirectory()) {
          const subResult = await loadWorkflowsFromDir(entryPath);
          for (const [filename, workflow] of subResult.workflows) {
            workflows.set(filename, workflow);
          }
          errors.push(...subResult.errors);
        } else if (entry.endsWith('.yaml') || entry.endsWith('.yml')) {
          const content = await readFile(entryPath, 'utf-8');
          const result = parseWorkflow(content, entry);

          if (result.workflow) {
            workflows.set(entry, result.workflow);
            getLog().debug({ workflowName: result.workflow.name, dirPath }, 'workflow_loaded');
          } else {
            errors.push(result.error);
          }
        }
      } catch (error) {
        const err = error as NodeJS.ErrnoException;
        getLog().warn({ err, entryPath }, 'workflow_file_read_error');
        errors.push({
          filename: entry,
          error: `File read error: ${err.message} (${err.code ?? 'unknown'})`,
          errorType: 'read_error',
        });
        // Continue processing remaining files
      }
    }
  } catch (error) {
    const err = error as NodeJS.ErrnoException;
    if (err.code === 'ENOENT') {
      getLog().debug({ dirPath }, 'workflow_directory_not_found');
    } else {
      getLog().warn({ err, dirPath }, 'workflow_directory_read_error');
    }
  }

  return { workflows, errors };
}
```

**Why**: Per-file try-catch ensures one broken file doesn't abort loading the rest. Error accumulation provides user-visible diagnostics.

---

### Step 4: Update `discoverWorkflows` to return `WorkflowLoadResult`

**File**: `packages/core/src/workflows/loader.ts`
**Lines**: 294-400
**Action**: UPDATE

**Current signature:**
```typescript
export async function discoverWorkflows(cwd: string): Promise<WorkflowDefinition[]> {
```

**Required change:**
```typescript
import type { WorkflowLoadResult, WorkflowLoadError } from './types';

export async function discoverWorkflows(cwd: string): Promise<WorkflowLoadResult> {
  const workflowsByFile = new Map<string, WorkflowDefinition>();
  const allErrors: WorkflowLoadError[] = [];

  // ... existing logic but collect errors from loadWorkflowsFromDir ...

  // Where currently:
  //   const appWorkflows = await loadWorkflowsFromDir(appDefaultsPath);
  // Change to:
  //   const appResult = await loadWorkflowsFromDir(appDefaultsPath);
  //   for (const [filename, workflow] of appResult.workflows) { ... }
  //   allErrors.push(...appResult.errors);

  // Same for repoWorkflows

  const workflows = Array.from(workflowsByFile.values());
  getLog().info(
    { count: workflows.length, errorCount: allErrors.length },
    'workflows_discovery_complete'
  );
  return { workflows, errors: allErrors };
}
```

**Why**: Propagates errors up the call chain so commands and orchestrator can surface them.

---

### Step 5: Update all callers of `discoverWorkflows`

**File**: `packages/core/src/handlers/command-handler.ts`
**Lines**: 1382-1412, 1500-1533
**Action**: UPDATE

**Current `/workflow list` (lines 1382-1404):**
```typescript
case 'list':
case 'ls': {
  const workflows = await discoverWorkflows(codebase.default_cwd);
  if (workflows.length === 0) { ... }
  // ... list workflows
}
```

**Required change:**
```typescript
case 'list':
case 'ls': {
  const { workflows, errors } = await discoverWorkflows(codebase.default_cwd);

  if (workflows.length === 0 && errors.length === 0) {
    return {
      success: true,
      message: 'No workflows found.\n\nCreate workflows in `.archon/workflows/` as YAML files.',
    };
  }

  let msg = '';

  if (workflows.length > 0) {
    msg += 'Available Workflows:\n\n';
    for (const w of workflows) {
      const stepsOrLoop = w.loop
        ? `Loop: until \`${w.loop.until}\` (max ${String(w.loop.max_iterations)} iterations)`
        : `Steps: ${w.steps?.map(s => (isSingleStep(s) ? `\`${s.command}\`` : `[${String(s.parallel.length)} parallel]`)).join(' -> ') ?? 'none'}`;
      msg += `**\`${w.name}\`**\n  ${w.description}\n  ${stepsOrLoop}\n\n`;
    }
  }

  if (errors.length > 0) {
    msg += `\n---\n\n**${String(errors.length)} workflow(s) failed to load:**\n\n`;
    for (const e of errors) {
      msg += `- \`${e.filename}\`: ${e.error}\n`;
    }
  }

  return { success: true, message: msg };
}
```

**Also update `/workflow run` (lines 1500-1533):**
```typescript
// Change:
//   workflows = await discoverWorkflows(codebase.default_cwd);
// To:
const { workflows: discoveredWorkflows } = await discoverWorkflows(codebase.default_cwd);
workflows = discoveredWorkflows;
```

**Also update `/workflow reload` (lines 1406-1413):**
```typescript
case 'reload': {
  const { workflows, errors } = await discoverWorkflows(codebase.default_cwd);
  let msg = `Discovered ${String(workflows.length)} workflow(s).`;
  if (errors.length > 0) {
    msg += `\n\n**${String(errors.length)} failed to load:**\n`;
    for (const e of errors) {
      msg += `- \`${e.filename}\`: ${e.error}\n`;
    }
  }
  return { success: true, message: msg };
}
```

**File**: `packages/core/src/orchestrator/orchestrator.ts`
**Lines**: 914-929
**Action**: UPDATE

```typescript
// Change:
//   availableWorkflows = await discoverWorkflows(workflowCwd);
// To:
const { workflows: discovered, errors: loadErrors } = await discoverWorkflows(workflowCwd);
availableWorkflows = discovered;
if (loadErrors.length > 0) {
  getLog().warn(
    { errorCount: loadErrors.length, errors: loadErrors },
    'workflow_load_errors'
  );
}
```

**File**: `packages/cli/src/commands/workflow.ts`
**Action**: UPDATE - Same pattern, destructure `{ workflows, errors }`.

**Why**: Every caller needs to handle the new return type. Commands surface errors to users; orchestrator logs them.

---

### Step 6: Add error info to `WorkflowInvocation` and case-insensitive matching

**File**: `packages/core/src/workflows/router.ts`
**Lines**: 143-184
**Action**: UPDATE

**Current code:**
```typescript
export interface WorkflowInvocation {
  workflowName: string | null;
  remainingMessage: string;
}
```

**Required change:**
```typescript
export interface WorkflowInvocation {
  workflowName: string | null;
  remainingMessage: string;
  /** Error message when workflow name was detected but didn't match */
  error?: string;
}
```

**Current `parseWorkflowInvocation` (lines 151-184):**
```typescript
if (workflow) { ... return valid; }
getLog().warn({ workflowName }, 'unknown_workflow');
return { workflowName: null, remainingMessage: message };
```

**Required change:**
```typescript
export function parseWorkflowInvocation(
  message: string,
  workflows: WorkflowDefinition[]
): WorkflowInvocation {
  const trimmed = message.trim();

  const match = /^\/invoke-workflow\s+(\S+)/im.exec(trimmed);

  if (match) {
    const workflowName = match[1];

    // Exact match
    const workflow = workflows.find(w => w.name === workflowName);
    if (workflow) {
      const remainingMessage = trimmed.slice(match.index + match[0].length).trim();
      return { workflowName, remainingMessage };
    }

    // Case-insensitive match
    const caseMatch = workflows.find(w => w.name.toLowerCase() === workflowName.toLowerCase());
    if (caseMatch) {
      getLog().info(
        { requested: workflowName, matched: caseMatch.name },
        'workflow_case_insensitive_match'
      );
      const remainingMessage = trimmed.slice(match.index + match[0].length).trim();
      return { workflowName: caseMatch.name, remainingMessage };
    }

    // No match - build helpful error
    const available = workflows.map(w => w.name);
    getLog().warn({ workflowName, available }, 'unknown_workflow');

    return {
      workflowName: null,
      remainingMessage: message,
      error: `Unknown workflow: \`${workflowName}\`. Available: ${available.map(n => `\`${n}\``).join(', ')}`,
    };
  }

  return { workflowName: null, remainingMessage: message };
}
```

**Why**: Case-insensitive matching handles AI case errors. Error message gives user actionable feedback with available workflow names.

---

### Step 7: Surface router errors in orchestrator

**File**: `packages/core/src/orchestrator/orchestrator.ts`
**Lines**: 458-464
**Action**: UPDATE

**Current code:**
```typescript
const { workflowName, remainingMessage } = parseWorkflowInvocation(
  aiResponse,
  ctx.availableWorkflows
);

if (!workflowName) {
  return false;
}
```

**Required change:**
```typescript
const { workflowName, remainingMessage, error } = parseWorkflowInvocation(
  aiResponse,
  ctx.availableWorkflows
);

if (!workflowName) {
  if (error) {
    getLog().warn({ error }, 'workflow_routing_failed');
    await ctx.platform.sendMessage(ctx.conversationId, error);
  }
  return false;
}
```

**Why**: When the AI tried to invoke a workflow but the name was wrong, the user gets a clear error message with available workflow names instead of seeing the raw `/invoke-workflow bad-name` output.

---

### Step 8: Code cleanup in executor.ts (#264)

**File**: `packages/core/src/workflows/executor.ts`
**Lines**: 334-345, 568-574
**Action**: UPDATE

**8a: Remove duplicate JSDoc (lines 568-574):**

**Current code:**
```typescript
/**
 * Execute a single workflow step
 */
/**
 * Internal function that executes a single step
 * (extracted to allow parallel execution)
 */
async function executeStepInternal(
```

**Required change:**
```typescript
/**
 * Internal function that executes a single step
 * (extracted to allow parallel execution)
 */
async function executeStepInternal(
```

**8b: Add `@returns` documentation (lines 334-345):**

**Current code:**
```typescript
/**
 * Load command prompt from file
 *
 * @param cwd - Working directory (repo root)
 * @param commandName - Name of the command (without .md extension)
 * @param configuredFolder - Optional additional folder from config to search
 */
async function loadCommandPrompt(
```

**Required change:**
```typescript
/**
 * Load command prompt from file
 *
 * @param cwd - Working directory (repo root)
 * @param commandName - Name of the command (without .md extension)
 * @param configuredFolder - Optional additional folder from config to search
 * @returns On success: `{ success: true, content }`. On failure: `{ success: false, reason, message }`.
 */
async function loadCommandPrompt(
```

**Why**: Removes misleading orphaned JSDoc and documents the discriminated union return type.

---

### Step 9: Remove unused `DiscoverWorkflowsResult` type

**File**: `packages/core/src/workflows/types.ts`
**Lines**: 142-148
**Action**: UPDATE (replaced in Step 1 with `WorkflowLoadError` + `WorkflowLoadResult`)

**File**: `packages/core/src/index.ts`
**Line**: 84
**Action**: UPDATE

**Current:**
```typescript
type DiscoverWorkflowsResult,
```

**Required change:**
```typescript
type WorkflowLoadError,
type WorkflowLoadResult,
```

**Why**: `DiscoverWorkflowsResult` was never used. The new types replace it.

---

### Step 10: Update and add tests

**File**: `packages/core/src/workflows/loader.test.ts`
**Action**: UPDATE

**Update existing tests** to expect `{ workflows, errors }` return shape from `discoverWorkflows`.

**Add new tests:**

```typescript
describe('error accumulation', () => {
  it('should return errors for YAML missing name', async () => {
    const workflowDir = join(testDir, '.archon', 'workflows');
    await mkdir(workflowDir, { recursive: true });

    await writeFile(
      join(workflowDir, 'invalid.yaml'),
      'description: Missing name\nsteps:\n  - command: plan\n'
    );

    const { workflows, errors } = await discoverWorkflows(testDir);

    expect(workflows).toHaveLength(0);
    expect(errors).toHaveLength(1);
    expect(errors[0].filename).toBe('invalid.yaml');
    expect(errors[0].errorType).toBe('validation_error');
    expect(errors[0].error).toContain('name');
  });

  it('should load valid workflows and report errors for invalid ones', async () => {
    const workflowDir = join(testDir, '.archon', 'workflows');
    await mkdir(workflowDir, { recursive: true });

    await writeFile(
      join(workflowDir, 'good.yaml'),
      'name: good\ndescription: Works\nsteps:\n  - command: plan\n'
    );
    await writeFile(
      join(workflowDir, 'bad.yaml'),
      'name: 123\ndescription: Bad name type\nsteps:\n  - command: plan\n'
    );

    const { workflows, errors } = await discoverWorkflows(testDir);

    expect(workflows).toHaveLength(1);
    expect(workflows[0].name).toBe('good');
    expect(errors).toHaveLength(1);
    expect(errors[0].filename).toBe('bad.yaml');
  });

  it('should report file read errors without aborting', async () => {
    // Create a workflow dir with a valid file and a permission-denied file
    // (test approach depends on OS capabilities)
  });
});
```

**File**: `packages/core/src/workflows/router.test.ts`
**Action**: UPDATE

**Add new tests:**

```typescript
describe('error information', () => {
  it('should return error message for unknown workflow', () => {
    const response = '/invoke-workflow non-existent';
    const result = parseWorkflowInvocation(response, testWorkflows);

    expect(result.workflowName).toBeNull();
    expect(result.error).toContain('non-existent');
    expect(result.error).toContain('Available');
  });

  it('should match workflow names case-insensitively', () => {
    const response = '/invoke-workflow Assist';
    const result = parseWorkflowInvocation(response, testWorkflows);

    expect(result.workflowName).toBe('assist');
  });

  it('should not have error when no /invoke-workflow pattern found', () => {
    const response = 'Just a normal message';
    const result = parseWorkflowInvocation(response, testWorkflows);

    expect(result.workflowName).toBeNull();
    expect(result.error).toBeUndefined();
  });
});
```

---

## Patterns to Follow

**From codebase - discriminated union pattern for results:**

```typescript
// SOURCE: types.ts:127-133
// Pattern for LoadCommandResult
export type LoadCommandResult =
  | { success: true; content: string }
  | {
      success: false;
      reason: 'invalid_name' | 'empty_file' | 'not_found' | 'permission_denied' | 'read_error';
      message: string;
    };
```

**From codebase - error surfacing in command handler:**

```typescript
// SOURCE: command-handler.ts:1500-1511
// Pattern for catching discovery errors and sending user-facing messages
try {
  workflows = await discoverWorkflows(codebase.default_cwd);
} catch (error) {
  const err = error as Error;
  getLog().error({ err, cwd: codebase.default_cwd }, 'workflow_discovery_failed');
  return {
    success: false,
    message: `Failed to load workflows: ${err.message}\n\nCheck .archon/workflows/ for YAML syntax issues.`,
  };
}
```

---

## Edge Cases & Risks

| Risk/Edge Case                                        | Mitigation                                                              |
| ----------------------------------------------------- | ----------------------------------------------------------------------- |
| Bundled workflows should never have errors             | Keep existing `getLog().error` for bundled failures; don't include in user-facing errors |
| Many errors could make `/workflow list` output noisy   | Limit to first 10 errors with "and N more" message                     |
| Case-insensitive match could match wrong workflow      | Exact match takes priority; case-insensitive only as fallback          |
| Breaking change: `discoverWorkflows` return type       | All callers must be updated in the same PR                              |
| CLI `workflow.ts` also calls `discoverWorkflows`       | Must update CLI callers too                                             |

---

## Validation

### Automated Checks

```bash
bun run type-check
bun test packages/core/src/workflows/loader.test.ts
bun test packages/core/src/workflows/router.test.ts
bun run lint
```

### Manual Verification

1. Create a malformed YAML in `.archon/workflows/broken.yaml` (missing `name` field)
2. Run `/workflow list` - should show available workflows AND the error for `broken.yaml`
3. Run `/workflow reload` - should report the error count
4. Trigger AI routing with an invalid workflow name - should see user-facing error message

---

## Scope Boundaries

**IN SCOPE:**
- Structured error accumulation in loader
- Per-file try-catch in `loadWorkflowsFromDir`
- Error info in router `WorkflowInvocation`
- Case-insensitive workflow name matching
- User-facing error messages in `/workflow list`, `/workflow reload`, and router fallback
- Code cleanup: duplicate JSDoc, `StepDefinition` deprecation, `@returns` doc, `clearContext` comment
- Updating all callers of `discoverWorkflows`
- Tests for error paths

**OUT OF SCOPE (do not touch):**
- Fuzzy/Levenshtein matching for workflow names (mentioned in #263 but out of scope for this batch)
- `/workflow errors` as a separate command (Option A from #260 - surface errors inline instead)
- Workflow caching or reload mechanisms
- Changes to executor error handling (already well-handled)
- Changes to event-emitter or logger modules

---

## Metadata

- **Investigated by**: Claude
- **Timestamp**: 2026-02-17
- **Artifact**: `.claude/PRPs/issues/issue-260-263-264.md`
