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
66 changes: 66 additions & 0 deletions docs/src/content/docs/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,72 @@ When multiple repositories (or multiple client paths within the same repo) conta

This differs from [plugin skill deduplication](/docs/guides/plugins/#duplicate-skill-handling), which keeps all copies and renames them for uniqueness. Repository skills are an index of pointers, so only the best source is kept.

## Lifecycle Hooks

The optional `lifecycleHooks` section lets a workspace run scripts at defined
phases of the sync/update flow. This is an escape hatch for runtime prerequisites
that AllAgents cannot sync as agent artifacts (e.g., installing helper binaries
like `agent-tui` or `bd`).

```yaml
lifecycleHooks:
preSync:
# String shorthand (required, name derived from script)
- scripts/check-deps.sh

# Object form with explicit name
- name: install-agent-tui
script: |
if ! command -v agent-tui >/dev/null 2>&1; then
cargo install agent-tui
fi

# Optional script — failure is logged as a warning, sync continues
- name: nice-to-have
script: ./optional-setup.sh
optional: true
```

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `script` | Yes | — | Shell command to execute (run via `sh -c`) |
| `name` | No | `script` value | Display name for CLI output |
| `optional` | No | `false` | If `true`, failure is a warning instead of an abort |

### Behavior

- **Ordering**: Scripts run in the order listed, before any plugin validation or filesystem mutation.
- **Required scripts**: A failing required script (the default) aborts the sync before any changes are made to the workspace.
- **Optional scripts**: A failing optional script is logged as a warning; sync continues.
- **Dry-run**: `--dry-run` shows which scripts would run without executing them.
- **Environment**: Each script runs in the workspace root with these variables set:

| Variable | Description |
|---|---|
| `ALLAGENTS_WORKSPACE` | Absolute path to the workspace root |
| `ALLAGENTS_CONFIG_DIR` | Absolute path to `.allagents/` |

### Idempotent script pattern

Scripts can store a marker (pinned version, commit hash) under `.allagents/` to
skip redundant work on re-run:

```bash
MARKER="$ALLAGENTS_CONFIG_DIR/.my-tool-version"
CURRENT=$(cat "$MARKER" 2>/dev/null || echo "none")
if [ "$CURRENT" = "1.2.3" ] && command -v my-tool >/dev/null 2>&1; then
exit 0
fi
# ... install my-tool ...
echo "1.2.3" > "$MARKER"
```

See `examples/workspaces/lifecycle-hooks/` for a complete working example.

:::caution[Security]
Lifecycle scripts are arbitrary local commands. Review any script from a plugin or template before enabling it in your workspace. Only the workspace owner can opt in — scripts are never silently executed.
:::

## MCP Servers

The optional top-level `mcpServers` field defines MCP servers managed directly by the workspace. Servers defined here are synced to all configured project-scoped MCP clients (`claude`, `codex`, `vscode`, `copilot`).
Expand Down
64 changes: 64 additions & 0 deletions examples/workspaces/lifecycle-hooks/.allagents/workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
version: 2

# Example: Workspace with lifecycle hooks for runtime prerequisites
#
# This workspace uses lifecycleHooks.preSync scripts to ensure required
# binaries are available before plugin artifacts are synced.
#
# SECURITY: These are arbitrary local commands. Review scripts from any
# plugin or template before enabling them in your workspace.
#
# Scripts run in the workspace root directory and receive:
# ALLAGENTS_WORKSPACE - absolute path to the workspace root
# ALLAGENTS_CONFIG_DIR - absolute path to the .allagents/ directory
#
# Idempotent pattern: scripts can store a marker (e.g., a pinned version
# or commit hash) under .allagents/ to skip redundant work on re-run.

repositories: []

plugins:
# Beads project-management skills and Codex hooks.
- https://github.com/gastownhall/beads/tree/main/plugins/beads

# Agent TUI skills.
- https://github.com/pproenca/agent-tui/tree/master/skills

clients:
- codex

syncMode: copy

lifecycleHooks:
preSync:
# Install agent-tui if not already present.
# Uses .allagents/.agent-tui-version as an idempotency marker.
- name: install-agent-tui
script: |
AGENT_TUI_VERSION="0.5.0"
MARKER="$ALLAGENTS_CONFIG_DIR/.agent-tui-version"
CURRENT=$(cat "$MARKER" 2>/dev/null || echo "none")
if [ "$CURRENT" = "$AGENT_TUI_VERSION" ] && command -v agent-tui >/dev/null 2>&1; then
echo "agent-tui $AGENT_TUI_VERSION already installed"
exit 0
fi
echo "Installing agent-tui $AGENT_TUI_VERSION..."
# Replace with actual install command for your platform:
# cargo install agent-tui --version "$AGENT_TUI_VERSION"
# npm install -g agent-tui@"$AGENT_TUI_VERSION"
echo "$AGENT_TUI_VERSION" > "$MARKER"

# Install bd (Beads CLI) if not already present.
- name: install-bd
script: |
BD_VERSION="0.3.0"
MARKER="$ALLAGENTS_CONFIG_DIR/.bd-version"
CURRENT=$(cat "$MARKER" 2>/dev/null || echo "none")
if [ "$CURRENT" = "$BD_VERSION" ] && command -v bd >/dev/null 2>&1; then
echo "bd $BD_VERSION already installed"
exit 0
fi
echo "Installing bd $BD_VERSION..."
# Replace with actual install command:
# cargo install bd --version "$BD_VERSION"
echo "$BD_VERSION" > "$MARKER"
56 changes: 56 additions & 0 deletions examples/workspaces/lifecycle-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Lifecycle Hooks Example

This workspace demonstrates the `lifecycleHooks.preSync` escape hatch for
installing runtime prerequisites that AllAgents cannot sync as agent artifacts.

## What it does

Before syncing plugin artifacts, AllAgents runs each `preSync` script in order:

1. `install-agent-tui` - installs the `agent-tui` binary (if not already present)
2. `install-bd` - installs the `bd` (Beads) CLI (if not already present)

Both scripts use an idempotency marker stored under `.allagents/` to avoid
re-downloading on every sync.

## Security

Lifecycle scripts are arbitrary local commands. **Review any script from a
plugin or template before enabling it in your workspace.** Only the workspace
owner can opt into running these scripts -- they are never silently executed.

## Environment

Scripts run in the workspace root and receive:

| Variable | Value |
|---|---|
| `ALLAGENTS_WORKSPACE` | Absolute path to the workspace root |
| `ALLAGENTS_CONFIG_DIR` | Absolute path to `.allagents/` |

## Usage

```bash
# Copy this example to a new workspace
allagents workspace init my-workspace --from examples/workspaces/lifecycle-hooks

# Run sync (lifecycle hooks execute before plugin copy)
cd my-workspace && allagents update

# Dry-run (shows what scripts would run, does not execute)
allagents update --dry-run
```

## Optional scripts

Mark a script as `optional: true` to continue sync even if it fails:

```yaml
lifecycleHooks:
preSync:
- name: nice-to-have
script: ./optional-setup.sh
optional: true
```

A required (default) script failure aborts the sync before any filesystem mutation.
11 changes: 11 additions & 0 deletions src/cli/commands/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
syncWorkspace,
} from '../../core/sync.js';
import type { SyncResult } from '../../core/sync.js';
import { formatLifecycleResults } from '../../core/lifecycle-scripts.js';
import {
ensureUserWorkspace,
getUserWorkspaceConfig,
Expand Down Expand Up @@ -329,6 +330,16 @@ const syncCmd = command({
}
console.log('');

// Print lifecycle hook results
if (result.lifecycleResults) {
for (const [phase, phaseResult] of Object.entries(result.lifecycleResults)) {
for (const line of formatLifecycleResults(phase, phaseResult)) {
console.log(line);
}
console.log('');
}
}

// Print plugin results
for (const pluginResult of result.pluginResults) {
console.log(formatPluginHeader(pluginResult));
Expand Down
20 changes: 20 additions & 0 deletions src/cli/format-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,26 @@ export function buildSyncData(result: SyncResult) {
...(r.error && { error: r.error }),
})),
}),
...(result.lifecycleResults && {
lifecycleHooks: Object.fromEntries(
Object.entries(result.lifecycleResults).map(([phase, r]) => [
phase,
{
success: r.success,
error: r.error,
scripts: r.results.map((s) => ({
name: s.name,
script: s.script,
success: s.success,
exitCode: s.exitCode,
durationMs: s.durationMs,
...(s.skipped && { skipped: true }),
...(s.stderr && { stderr: s.stderr }),
})),
},
]),
),
}),
};
}

Expand Down
Loading