Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 97 additions & 5 deletions .claude/skills/swamp-troubleshooting/references/health-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ piece in the output.
- [When to use Tier 1](#when-to-use-tier-1)
- [`swamp doctor audit`](#swamp-doctor-audit)
- [`swamp doctor extensions`](#swamp-doctor-extensions)
- [`swamp doctor workflows`](#swamp-doctor-workflows)
- [Using doctor in CI](#using-doctor-in-ci)
- [Escalating to other tiers](#escalating-to-other-tiers)

Expand All @@ -23,7 +24,9 @@ Reach for a doctor command when the symptom maps to a known integration:
| Hooks aren't firing for the configured AI tool | `swamp doctor audit` |
| Extension model/vault/driver/datastore/report missing from CLI | `swamp doctor extensions` |
| `swamp-warning:` line on stderr mentioning a load failure | `swamp doctor extensions` |
| CI preflight needs to gate on integration health | either, with `--json` |
| Workflow YAML fails to parse or construct | `swamp doctor workflows` |
| `swamp workflow get` errors on a file that search finds | `swamp doctor workflows` |
| CI preflight needs to gate on integration health | any, with `--json` |

If the symptom is generic ("command errored", "method failed"), skip Tier 1 and
go to Tier 2 (error inspection).
Expand Down Expand Up @@ -209,22 +212,111 @@ For the deeper "extension not in type search" walkthrough (stderr inspection,
source registration, `deno.json` discovery), see
[error-inspection.md](error-inspection.md).

## `swamp doctor workflows`

Checks that every workflow YAML file in the repo can be parsed and constructed
into a valid Workflow domain object. This catches YAML syntax errors and schema
construction failures that `findAll()` silently skips — meaning
`swamp workflow
validate` never sees these broken files. Exits 1 on any load
failure.

```bash
swamp doctor workflows
swamp doctor workflows --json
```

### What it checks

Walks all workflow directories (primary `workflows/`, extension workflows,
source-mounted workflows, pulled extension workflows) and for each `*.yaml`
file:

1. Reads the file content
2. Parses YAML via `@std/yaml`
3. Constructs the domain object via `Workflow.fromData()`

Scope is **load-ability only** — whether the file can be parsed and constructed.
Schema validity (DAG integrity, model references, expression validation) is the
job of `swamp workflow validate`.

### Output — log mode

```
Checking workflows...

✓ deploy-pipeline
✓ nightly-sync
✗ emergency-kernel-update
→ YAML parse error at line 42, column 15: bad indentation of a mapping entry

2 passed, 1 failed — OVERALL: FAIL
```

### Output — JSON mode

```json
{
"overallStatus": "fail",
"workflows": [
{
"file": "/path/to/repo/workflows/workflow-abc.yaml",
"name": "deploy-pipeline",
"status": "pass"
},
{
"file": "/path/to/repo/workflows/workflow-broken.yaml",
"name": "emergency-kernel-update",
"status": "fail",
"error": "YAML parse error at line 42, column 15: bad indentation"
}
],
"totalPassed": 2,
"totalFailed": 1
}
```

### Common failures

| Error fragment | Fix |
| ------------------------------------- | --------------------------------------------------------------------- |
| YAML parse error at line N | Fix the YAML syntax at the indicated line/column |
| `type "shell" is no longer supported` | Replace `type: shell` with `type: model_method` using `command/shell` |
| Invalid uuid / missing name | Add required `id` (UUID) and `name` fields to the workflow YAML |
| Invalid cron expression | Fix the `trigger.schedule` cron expression |

### Relationship with `swamp workflow validate`

These two commands are complementary:

- `doctor workflows` catches files that **fail to load** (YAML syntax, missing
fields, invalid types). These files are silently dropped by the workflow
loader, so `workflow validate` never sees them.
- `workflow validate` checks **loaded** workflows for semantic validity (DAG
cycles, undefined model references, expression errors).

Run both in CI for complete coverage.

## Using doctor in CI

Both commands exit 1 on failure, so they compose directly into CI. Use `--json`
to capture structured output for review.
All doctor commands exit 1 on failure, so they compose directly into CI. Use
`--json` to capture structured output for review.

```bash
# Gate on audit health
swamp doctor audit --json > audit-health.json

# Gate on extension health
swamp doctor extensions --json > extension-health.json

# Gate on workflow health
swamp doctor workflows --json > workflow-health.json
```

Run `doctor extensions` after every PR that touches `extensions/` or
`.swamp-sources.yaml`. Run `doctor audit` after every `init` or `upgrade` step
in CI bootstrap.
`.swamp-sources.yaml`. Run `doctor workflows` after every PR that touches
workflow YAML files. Run `doctor audit` after every `init` or `upgrade` step in
CI bootstrap.

## Escalating to other tiers

Expand Down
8 changes: 7 additions & 1 deletion src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { Command } from "@cliffy/command";
import { doctorAuditCommand } from "./doctor_audit.ts";
import { doctorExtensionsCommand } from "./doctor_extensions.ts";
import { doctorWorkflowsCommand } from "./doctor_workflows.ts";

export const doctorCommand = new Command()
.description(
Expand All @@ -37,6 +38,10 @@ export const doctorCommand = new Command()
"Check that user-defined extensions in this repo load cleanly",
"swamp doctor extensions",
)
.example(
"Check that workflow YAML files load cleanly",
"swamp doctor workflows",
)
// `--repo-dir` is accepted on the top-level command for consistency
// with subcommands and other repo-scoped commands. The top-level
// action only shows help; subcommands consume the option.
Expand All @@ -48,4 +53,5 @@ export const doctorCommand = new Command()
this.showHelp();
})
.command("audit", doctorAuditCommand)
.command("extensions", doctorExtensionsCommand);
.command("extensions", doctorExtensionsCommand)
.command("workflows", doctorWorkflowsCommand);
116 changes: 116 additions & 0 deletions src/cli/commands/doctor_workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Swamp, an Automation Framework
// Copyright (C) 2026 System Initiative, Inc.
//
// This file is part of Swamp.
//
// Swamp is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation, with the Swamp
// Extension and Definition Exception (found in the "COPYING-EXCEPTION"
// file).
//
// Swamp is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Swamp. If not, see <https://www.gnu.org/licenses/>.

import { Command } from "@cliffy/command";
import { isAbsolute, join, resolve } from "@std/path";
import {
consumeStream,
doctorWorkflows,
enumeratePulledExtensionDirs,
} from "../../libswamp/mod.ts";
import { createWorkflowDoctorRenderer } from "../../presentation/renderers/workflow_doctor.ts";
import {
createContext,
type GlobalOptions,
resolveRepoDir,
} from "../context.ts";
import { resolveDatastoreForRepo } from "../repo_context.ts";
import { resolveWorkflowsDir } from "../resolve_workflows_dir.ts";
import { resolveModelsDir } from "../resolve_models_dir.ts";
import {
collectDirsForKind,
expandSourcePaths,
readSwampSources,
resolveSourceExtensionDirs,
} from "../../infrastructure/persistence/swamp_sources_repository.ts";

// deno-lint-ignore no-explicit-any
type AnyOptions = any;

async function getSourceWorkflowDirs(repoDir: string): Promise<string[]> {
const sourcesConfig = await readSwampSources(repoDir);
if (!sourcesConfig) return [];
const expanded = await expandSourcePaths(sourcesConfig, repoDir);
const resolved = await resolveSourceExtensionDirs(expanded);
return collectDirsForKind(resolved, "workflows");
}

export const doctorWorkflowsCommand = new Command()
.description(
"Check that workflow YAML files in this repo load cleanly.",
)
.example("Check all workflows", "swamp doctor workflows")
.example("Machine-readable output for CI", "swamp doctor workflows --json")
.option(
"--repo-dir <dir:string>",
"Repository directory (env: SWAMP_REPO_DIR)",
)
.action(async function (options: AnyOptions) {
const cliCtx = createContext(options as GlobalOptions, [
"doctor",
"workflows",
]);
cliCtx.logger.debug("Executing doctor workflows command");

const repoDir = resolveRepoDir(options.repoDir);
const { marker } = await resolveDatastoreForRepo(repoDir);

const yamlWorkflowsDir = join(repoDir, "workflows");

const workflowsDirRel = resolveWorkflowsDir(marker);
const workflowsDir = isAbsolute(workflowsDirRel)
? workflowsDirRel
: resolve(repoDir, workflowsDirRel);

const sourceWorkflowDirs = await getSourceWorkflowDirs(repoDir);

const modelsDirRel = resolveModelsDir(marker);
const modelsDir = isAbsolute(modelsDirRel)
? modelsDirRel
: resolve(repoDir, modelsDirRel);
const pulledWorkflowDirs = await enumeratePulledExtensionDirs(
join(modelsDir, "upstream_extensions.json"),
repoDir,
"workflows",
);

const workflowDirs = [
yamlWorkflowsDir,
workflowsDir,
...sourceWorkflowDirs,
...pulledWorkflowDirs,
];

const controller = new AbortController();
const renderer = createWorkflowDoctorRenderer(cliCtx.outputMode);

await consumeStream(
doctorWorkflows({
workflowDirs,
abortSignal: controller.signal,
}),
renderer.handlers(),
);

cliCtx.logger.debug("doctor workflows command completed");

if (renderer.overallStatus === "fail") {
Deno.exit(1);
}
});
9 changes: 9 additions & 0 deletions src/libswamp/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,15 @@ export {
type WorkflowValidateInput,
} from "./workflows/validate.ts";

// Workflow doctor operations
export {
type DoctorWorkflowResult,
doctorWorkflows,
type DoctorWorkflowsDeps,
type DoctorWorkflowsEvent,
type DoctorWorkflowsReport,
} from "./workflows/doctor.ts";

// Workflow evaluate operations
export {
createWorkflowEvaluateDeps,
Expand Down
Loading
Loading