Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"scripts": {
"cli": "bun src/cli.ts",
"test": "bun test src/commands/version.test.ts src/commands/setup.test.ts src/commands/skill.test.ts && bun test src/commands/workflow.test.ts && bun test src/commands/isolation.test.ts && bun test src/commands/chat.test.ts && bun test src/commands/serve.test.ts",
"test": "bun test src/commands/version.test.ts src/commands/setup.test.ts src/commands/skill.test.ts src/commands/validate.test.ts && bun test src/commands/workflow.test.ts && bun test src/commands/isolation.test.ts && bun test src/commands/chat.test.ts && bun test src/commands/serve.test.ts",
"type-check": "bun x tsc --noEmit"
},
"dependencies": {
Expand Down
87 changes: 87 additions & 0 deletions packages/cli/src/commands/validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { clearConfigCache } from '@archon/core';
import { validateWorkflowsCommand } from './validate';

const tempDirs: string[] = [];
let consoleLogSpy: ReturnType<typeof spyOn>;
let originalArchonHome: string | undefined;
let originalDefaultAssistant: string | undefined;

async function makeTempDir(prefix: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}

beforeEach(async () => {
originalArchonHome = process.env.ARCHON_HOME;
originalDefaultAssistant = process.env.DEFAULT_AI_ASSISTANT;
process.env.ARCHON_HOME = await makeTempDir('archon-cli-home-');
delete process.env.DEFAULT_AI_ASSISTANT;
clearConfigCache();
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(async () => {
consoleLogSpy.mockRestore();
if (originalArchonHome === undefined) {
delete process.env.ARCHON_HOME;
} else {
process.env.ARCHON_HOME = originalArchonHome;
}
if (originalDefaultAssistant === undefined) {
delete process.env.DEFAULT_AI_ASSISTANT;
} else {
process.env.DEFAULT_AI_ASSISTANT = originalDefaultAssistant;
}
clearConfigCache();
for (const dir of tempDirs.splice(0)) {
await rm(dir, { recursive: true, force: true });
}
});

describe('validateWorkflowsCommand', () => {
test('uses configured Codex skillRoots for workflows that rely on the default provider', async () => {
const repo = await makeTempDir('archon-validate-skills-');
const skillRoot = join(repo, 'custom-skills');
await mkdir(join(skillRoot, 'alpha'), { recursive: true });
await writeFile(join(skillRoot, 'alpha', 'SKILL.md'), '# Alpha\n');
await mkdir(join(repo, '.archon', 'workflows'), { recursive: true });
await writeFile(
join(repo, '.archon', 'workflows', 'codex-skills.yaml'),
`
name: codex-skills
description: test
nodes:
- id: review
prompt: Review
skills: [alpha]
`
);
await writeFile(
join(repo, '.archon', 'config.yaml'),
`
assistant: codex
assistants:
codex:
skillRoots:
- ${skillRoot}
defaults:
loadDefaultCommands: false
loadDefaultWorkflows: false
`
);

const exitCode = await validateWorkflowsCommand(repo, undefined, true);

expect(exitCode).toBe(0);
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
const output = JSON.parse(consoleLogSpy.mock.calls[0]?.[0] as string) as {
summary: { errors: number };
};
expect(output.summary.errors).toBe(0);
});
});
19 changes: 19 additions & 0 deletions packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ async function buildValidationConfig(cwd: string): Promise<ValidationConfig> {
}
}

function buildSkillRootsByProvider(
assistants: Record<string, Record<string, unknown>>
): Record<string, string[]> {
const result: Record<string, string[]> = {};

for (const [providerId, defaults] of Object.entries(assistants)) {
const roots = defaults.skillRoots;
if (!Array.isArray(roots)) continue;

const stringRoots = roots.filter((root): root is string => typeof root === 'string');
if (stringRoots.length > 0) {
result[providerId] = stringRoots;
}
}

return result;
}

// =============================================================================
// Output formatting
// =============================================================================
Expand Down Expand Up @@ -86,6 +104,7 @@ export async function validateWorkflowsCommand(
): Promise<number> {
const config = await buildValidationConfig(cwd);
const mergedConfig = await loadConfig(cwd);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle loadConfig errors before workflow validation

validateWorkflowsCommand now awaits loadConfig(cwd) without a fallback, so a malformed or unreadable global/repo config causes the command to throw before discovery/validation runs. This regresses prior behavior where config-load failures were tolerated (via discoverWorkflowsWithConfig’s internal try/catch) and users could still validate workflows with defaults.

Useful? React with 👍 / 👎.

config.skillRootsByProvider = buildSkillRootsByProvider(mergedConfig.assistants);
const defaultProvider = mergedConfig.assistant;
const { workflows: workflowEntries, errors: loadErrors } = await discoverWorkflowsWithConfig(
cwd,
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/config/config-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ streaming:
let globalConfigRead = false;
mockReadConfigFile.mockImplementation(async (path: string) => {
if (pathMatches(path, '/repo/.archon/config.yaml')) {
return `assistants:\n codex:\n webSearchMode: live\n additionalDirectories:\n - /repo\n`;
return `assistants:\n codex:\n webSearchMode: live\n additionalDirectories:\n - /repo\n skillRoots:\n - /repo/.agents/skills\n`;
}
if (pathMatches(path, '.archon/config.yaml') && !globalConfigRead) {
globalConfigRead = true;
Expand All @@ -327,6 +327,7 @@ streaming:
expect(config.assistants.codex.modelReasoningEffort).toBe('medium');
expect(config.assistants.codex.webSearchMode).toBe('live');
expect(config.assistants.codex.additionalDirectories).toEqual(['/repo']);
expect(config.assistants.codex.skillRoots).toEqual(['/repo/.agents/skills']);
});

test('propagates baseBranch from repo worktree config', async () => {
Expand Down Expand Up @@ -613,16 +614,19 @@ assistants:
expect(safe).not.toHaveProperty('commands');
});

test('strips additionalDirectories from assistants.codex', async () => {
test('strips local path fields from assistants.codex', async () => {
mockReadConfigFile.mockResolvedValue(`
assistants:
codex:
additionalDirectories:
- /sensitive/path
skillRoots:
- /sensitive/skills
`);
const config = await loadConfig();
const safe = toSafeConfig(config);
expect(safe.assistants.codex).not.toHaveProperty('additionalDirectories');
expect(safe.assistants.codex).not.toHaveProperty('skillRoots');
});

test('preserves non-sensitive fields', async () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ const DEFAULT_CONFIG_CONTENT = `# Archon Global Configuration
# webSearchMode: disabled
# additionalDirectories:
# - /absolute/path/to/other/repo
# skillRoots:
# - /absolute/path/to/skills
# Streaming mode per platform (stream or batch)
# streaming:
Expand Down
2 changes: 1 addition & 1 deletion packages/docs-web/src/content/docs/book/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ All nodes share these base fields:
| `retry` | No | object | Retry configuration for transient failures (see Retry Options). **Hard error on loop nodes** |
| `hooks` | No | object | SDK hook callbacks (Claude only; see Hook Schema) |
| `mcp` | No | string | Path to MCP server config JSON file (Claude only) |
| `skills` | No | string[] | Skill names to preload into this node's context (Claude only) |
| `skills` | No | string[] | Skill names to preload into this node's context (Claude, Codex, and Pi) |
| `agents` | No | object | Inline sub-agent definitions keyed by kebab-case ID. Claude only |

**Script-specific fields** (required when `script:` is set):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ nodes:
model: haiku # Per-node model override
# hooks: # Optional: per-node SDK hook callbacks (Claude only) — see hooks guide
# mcp: .archon/mcp/servers.json # Optional: per-node MCP servers (Claude only)
# skills: [remotion-best-practices] # Optional: per-node skills (Claude only) — see skills guide
# skills: [remotion-best-practices] # Optional: per-node skills — see skills guide
```

### Node Fields
Expand Down Expand Up @@ -206,7 +206,7 @@ nodes:
| `denied_tools` | string[] | — | Tools to remove. Applied after `allowed_tools`. Claude only |
| `hooks` | object | — | Per-node SDK hook callbacks. Claude only. See [Hooks](/guides/hooks/) |
| `mcp` | string | — | Path to MCP server config JSON file. Claude only. See [MCP Servers](/guides/mcp-servers/) |
| `skills` | string[] | — | Skills to preload. Claude only. See [Skills](/guides/skills/) |
| `skills` | string[] | — | Skills to preload. Supported by Claude, Codex, and Pi with provider-specific mechanics. See [Skills](/guides/skills/) |
| `agents` | object | — | Inline sub-agent definitions keyed by kebab-case ID. Claude only. See [Inline sub-agents](#inline-sub-agents) |
| `effort` | `'low'`\|`'medium'`\|`'high'`\|`'max'` | — | Reasoning depth. Claude only. Also settable at workflow level |
| `thinking` | string \| object | — | Thinking mode: `'adaptive'`, `'disabled'`, or `{type:'enabled', budgetTokens:N}`. Claude only. Also settable at workflow level |
Expand All @@ -218,7 +218,7 @@ nodes:

### Claude SDK Advanced Options

These fields map directly to Claude Agent SDK options. All are Claude-only — Codex nodes emit a warning and ignore them. They can be set **per-node** or at the **workflow level** as defaults (per-node takes precedence). `maxBudgetUsd` and `systemPrompt` are per-node only.
Except for `skills`, these fields map directly to Claude Agent SDK options. Claude-only fields emit a warning and are ignored by providers that do not support them. They can be set **per-node** or at the **workflow level** as defaults (per-node takes precedence). `maxBudgetUsd` and `systemPrompt` are per-node only.

**effort** — reasoning depth:

Expand Down Expand Up @@ -1174,7 +1174,7 @@ Before deploying a workflow:
9. **`retry:`** — auto-retries transient errors (default: 2 retries / 3 total attempts, 3 s backoff); customize per node
10. **`hooks`** — attach SDK hook callbacks to Claude nodes for tool control and context injection
11. **`mcp:`** — attach per-node MCP servers via JSON config (Claude only)
12. **`skills:`** — preload skills into Claude nodes for domain expertise
12. **`skills:`** — preload selected skills into supported provider nodes for domain expertise
13. **`agents:`** — inline Claude sub-agent definitions invokable via the `Task` tool
14. **`effort` / `thinking`** — control reasoning depth and thinking mode per node or workflow (Claude only)
15. **`maxBudgetUsd`** — set a USD cost cap per node; fails with error if exceeded (Claude only)
Expand Down
7 changes: 5 additions & 2 deletions packages/docs-web/src/content/docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ How-to guides for building and running AI coding workflows with Archon.
- [Approval Nodes](/guides/approval-nodes/) — Human review gates with optional AI rework on rejection
- [Script Nodes](/guides/script-nodes/) — TypeScript/JavaScript (bun) or Python (uv) as a deterministic DAG node, without AI

## Node Features (Claude only)
## Node Features

- [Per-Node Skills](/guides/skills/) — Preload specialized knowledge into node agents

## Claude-Only Node Features

- [Per-Node Hooks](/guides/hooks/) — Attach Claude SDK hooks for tool control, context injection, and input modification
- [Per-Node MCP Servers](/guides/mcp-servers/) — Connect external tools (GitHub, Postgres, etc.) to individual nodes
- [Per-Node Skills](/guides/skills/) — Preload specialized knowledge into node agents

## Bundled Workflows

Expand Down
83 changes: 59 additions & 24 deletions packages/docs-web/src/content/docs/guides/skills.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Per-Node Skills
description: Preload specialized knowledge into individual workflow nodes using the Claude Agent SDK skills system.
description: Preload specialized knowledge into individual workflow nodes.
category: guides
area: workflows
audience: [user]
Expand All @@ -13,8 +13,6 @@ DAG workflow nodes support a `skills` field that preloads named skills into the
node's agent context. Each node gets specialized procedural knowledge — code review
patterns, Remotion best practices, testing conventions — without polluting other nodes.

**Claude only** — Codex nodes will warn and ignore the `skills` field.

## Quick Start

1. Install a skill (e.g., the official Remotion skill):
Expand All @@ -24,6 +22,7 @@ npx skills add remotion-dev/skills
```

This places SKILL.md files in `.claude/skills/remotion-best-practices/`.
Codex-style skills can also live in `.agents/skills/`.

2. Reference it in your workflow:

Expand All @@ -43,8 +42,14 @@ gotchas) without the user having to paste instructions into the prompt.

## How It Works

When a node has `skills: [name, ...]`, the executor wraps it in an
[AgentDefinition](https://platform.claude.com/docs/en/agent-sdk/subagents) — the
When a node has `skills: [name, ...]`, Archon resolves each selected skill before
the node runs. Missing or unreadable skills fail validation with the searched
paths.

### Claude

For Claude nodes, the executor wraps the node in an
[AgentDefinition](https://platform.claude.com/docs/en/agent-sdk/subagents), the
Claude Agent SDK mechanism for scoping skills to subagents.

```
Expand All @@ -66,6 +71,22 @@ Agent executes with full skill knowledge available
The `Skill` tool is automatically added to `allowedTools` so the agent can invoke
skills. You don't need to add it manually.

### Codex

For Codex nodes, Archon resolves the selected `SKILL.md` files and prepends them
to the Codex turn as explicit workflow-selected skill context. This makes
`skills:` deterministic even when the skill lives outside Codex's native
auto-discovery roots.

Codex can also use its native `$skill-name` invocation for skills installed under
`.agents/skills/` or user/admin/system Codex roots. The workflow `skills:` field
is for selecting exactly which skills a node should receive.

### Pi

For Pi nodes, Archon resolves skill names to skill directories and passes them to
Pi as additional skill paths.

## Installing Skills

Skills must be installed on the filesystem before they can be referenced.
Expand Down Expand Up @@ -101,10 +122,11 @@ npx skills add git@github.com:org/private-skills.git

### Manual

Create a directory in `.claude/skills/` with a `SKILL.md` file:
Create a directory in `.claude/skills/` or `.agents/skills/` with a `SKILL.md`
file:

```
.claude/skills/my-skill/
.agents/skills/my-skill/
└── SKILL.md
```
Comment on lines 128 to 131
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the directory-tree fence.

The anonymous fence at Line 128 will keep tripping MD040 in docs CI. Mark it as text/plaintext so the lint warning goes away.

♻️ Proposed fix
-```
+```text
 .agents/skills/my-skill/
 └── SKILL.md
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 128-128: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs-web/src/content/docs/guides/skills.md` around lines 128 - 131,
Replace the anonymous fenced code block containing the directory tree snippet
(the triple-backticks block showing ".agents/skills/my-skill/ └── SKILL.md")
with a language-labeled fence by adding "text" (or "plaintext") after the
opening backticks so the fence becomes ```text, which will satisfy MD040 in CI.


Expand All @@ -123,16 +145,35 @@ Step-by-step content here. The agent loads this when the skill activates.

## Skill Discovery

Skills are discovered from these locations (via `settingSources: ['project']`
set in ClaudeProvider):
Skills are discovered from these default locations:

| Location | Scope |
|----------|-------|
| `.agents/skills/` (in cwd) | Project-level, Codex/Pi convention |
| `.codex/skills/` (in cwd) | Project-level, Codex convention |
| `.claude/skills/` (in cwd) | Project-level |
| `~/.agents/skills/` | User-level Codex/Pi convention |
| `~/.codex/skills/` | User-level Codex convention |
| `~/.claude/skills/` | User-level (all projects) |
| `/etc/codex/skills/` | Admin/system Codex convention |

Skills installed via `npx skills add` land in `.claude/skills/` by default.
Use `-g` for global installation to `~/.claude/skills/`.
Codex nodes can add extra roots in config:

```yaml
assistants:
codex:
skillRoots:
- /absolute/path/to/team-skills
```

Skill entries can also be explicit paths to a skill directory or a `SKILL.md`
file:

```yaml
skills:
- /absolute/path/to/team-skills/release-checklist
- ./.agents/skills/local-skill/SKILL.md
```

## Scoping: Installed vs Active

Expand Down Expand Up @@ -205,32 +246,26 @@ produce better results than either alone.

## Codex Compatibility

Codex nodes with `skills` log a warning and continue without the skills:

```
Warning: Node 'review' has skills set but uses Codex — per-node skills
are not supported for Codex.
```

To use skills, ensure the node uses Claude (the default provider, or set
`provider: claude` explicitly).
Codex supports `skills:` on workflow prompt and command nodes. Archon injects the
resolved skill content into the Codex turn, so missing skills fail before the
model runs instead of being silently ignored.

## Limitations

- **Pre-installation required** — skills must exist on disk before the workflow runs.
There is no on-demand fetching (yet).
- **Claude only** — the SDK's `AgentDefinition.skills` field is Claude-specific.
- **Full injection** — skill content is fully injected at startup, not progressively
disclosed. Keep skills concise.
- **No validation** — if a named skill doesn't exist, the SDK may fail silently.
Verify skills are installed with `npx skills list`.
- **Provider mechanics differ** — Claude uses SDK `AgentDefinition.skills`, Codex
receives explicit skill context in the turn prompt, and Pi receives additional
skill paths.

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| Skill not found | Not installed | Run `npx skills add <source>` |
| Skill ignored | Node uses Codex provider | Set `provider: claude` on the node |
| Skill not found in custom location | Root not configured | Add `assistants.codex.skillRoots` or reference the skill by explicit path |
| Too many skills | Context budget exceeded | Reduce to 2-3 most relevant skills per node |
| Skill has no effect | Description too vague | Rewrite SKILL.md with specific, actionable instructions |

Expand Down
Loading