Skip to content
Draft
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
89 changes: 73 additions & 16 deletions web/components/docs/DocsNav.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client';

import type { ComponentType } from 'react';
import { useEffect, useRef } from 'react';
import type { ComponentType, ReactElement } from 'react';
import { useEffect, useRef, useState } from 'react';
import { usePathname } from 'next/navigation';
import {
Activity,
Bot,
ChevronRight,
Cloud,
Clock3,
Compass,
Expand All @@ -30,7 +31,7 @@ import { PiBroadcastFill, PiLockKeyDuotone } from 'react-icons/pi';
import { RiLayout5Line } from 'react-icons/ri';
import { SiClaude, SiPython, SiTypescript } from 'react-icons/si';

import { docsNav } from '../../lib/docs-nav';
import { docsNav, type NavItem } from '../../lib/docs-nav';
import styles from './docs.module.css';

type NavIcon = ComponentType<{ className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>;
Expand Down Expand Up @@ -125,22 +126,78 @@ export function DocsNav({ variant = 'sidebar' }: { variant?: 'sidebar' | 'mobile
<div key={group.title} className={styles.navGroup}>
<h4 className={styles.navGroupTitle}>{group.title}</h4>
<ul className={styles.navList}>
{group.items.map((item) => {
const href = `/docs/${item.slug}`;
const isActive = pathname === href || (item.slug === 'introduction' && pathname === '/docs');
const Icon = navIcons[item.slug];
return (
<li key={item.slug}>
<a href={href} className={`${styles.navLink} ${isActive ? styles.navLinkActive : ''}`}>
{Icon && <Icon className={styles.navIcon} aria-hidden="true" />}
<span className={styles.navLabel}>{item.title}</span>
</a>
</li>
);
})}
{group.items.map((item) => (
<NavItemRow key={item.slug} item={item} pathname={pathname} />
))}
</ul>
</div>
))}
</nav>
);
}

function isLinkActive(slug: string, pathname: string): boolean {
return pathname === `/docs/${slug}` || (slug === 'introduction' && pathname === '/docs');
}

function containsActive(item: NavItem, pathname: string): boolean {
if (isLinkActive(item.slug, pathname)) return true;
return item.children?.some((child) => containsActive(child, pathname)) ?? false;
}

function NavItemRow({ item, pathname }: { item: NavItem; pathname: string }): ReactElement {
const href = `/docs/${item.slug}`;
const isActive = isLinkActive(item.slug, pathname);
const Icon = navIcons[item.slug];
const hasChildren = Boolean(item.children && item.children.length > 0);

// Collapsed by default; auto-expanded if the current page is in this
// item's subtree so users don't lose their bearings when navigating.
const activeInSubtree = hasChildren && containsActive(item, pathname);
const [open, setOpen] = useState(activeInSubtree);

// Re-sync open state when the pathname changes (e.g. nav click).
useEffect(() => {
if (activeInSubtree) setOpen(true);
}, [activeInSubtree]);

if (!hasChildren) {
return (
<li>
<a href={href} className={`${styles.navLink} ${isActive ? styles.navLinkActive : ''}`}>
{Icon && <Icon className={styles.navIcon} aria-hidden="true" />}
<span className={styles.navLabel}>{item.title}</span>
</a>
</li>
);
}

const childListId = `nav-children-${item.slug}`;
return (
<li>
<div className={styles.navLinkRow}>
<a href={href} className={`${styles.navLink} ${isActive ? styles.navLinkActive : ''}`}>
{Icon && <Icon className={styles.navIcon} aria-hidden="true" />}
<span className={styles.navLabel}>{item.title}</span>
</a>
<button
type="button"
className={`${styles.navToggle} ${open ? styles.navToggleOpen : ''}`}
aria-expanded={open}
aria-controls={childListId}
aria-label={open ? `Collapse ${item.title}` : `Expand ${item.title}`}
onClick={() => setOpen((prev) => !prev)}
>
<ChevronRight className={styles.navToggleIcon} aria-hidden="true" />
</button>
</div>
{open && (
<ul id={childListId} className={`${styles.navList} ${styles.navChildren}`}>
{item.children!.map((child) => (
<NavItemRow key={child.slug} item={child} pathname={pathname} />
))}
</ul>
)}
</li>
);
}
43 changes: 43 additions & 0 deletions web/components/docs/docs.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,49 @@
}

.navList { list-style: none; padding: 0; margin: 0; }
.navChildren { padding-left: 0.9rem; margin-top: 0.1rem; border-left: 1px solid rgba(255, 255, 255, 0.08); }

.navLinkRow {
display: flex;
align-items: center;
gap: 0.15rem;
}

.navLinkRow .navLink {
flex: 1;
min-width: 0;
}

.navToggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
padding: 0;
background: transparent;
border: 0;
border-radius: 0.3rem;
color: var(--fg-muted);
cursor: pointer;
opacity: 0.65;
transition: transform 0.15s ease, opacity 0.15s, background 0.15s;
}

.navToggle:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.06);
}

.navToggleOpen {
transform: rotate(90deg);
opacity: 1;
}

.navToggleIcon {
width: 0.85rem;
height: 0.85rem;
}

.navLink {
display: flex;
Expand Down
53 changes: 53 additions & 0 deletions web/content/docs/cli-cloud-commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,59 @@ agent-relay cloud sync <run-id>
- `logs` streams workflow or per-agent output.
- `sync` downloads the generated patch and applies it locally.

## `--sync-code`: uploading your local repo

`--sync-code` tarballs your current working copy and ships it to the cloud sandbox as the starting point for the run. Without it, the sandbox starts with **no code on disk** — it doesn't clone from `origin`, it just has an empty `$HOME`. Any workflow that reads your source needs `--sync-code`.

**In almost every case, you want `--sync-code`.** Running a workflow against an empty sandbox is rarely what you mean; you almost always want your local worktree state there.

### What gets uploaded

The tarball is built from `git ls-files` — the **tracked** paths — and `tar` reads their current working-tree contents. No `git clone` anywhere.

| State | Synced? |
|---|---|
| Committed, unmodified | ✅ Working-tree version |
| Committed, then modified | ✅ Working-tree version (your edits go too) |
| Committed, then modified + `git add`ed | ✅ Working-tree version |
| New file + `git add`ed (not committed) | ✅ — once added, the file is tracked |
| New file, never added | ❌ Untracked → excluded |
| `.gitignore`d path | ❌ Excluded |

**Rule of thumb:** `git add` whatever the run needs. Commit is NOT required — staging is enough because `git ls-files` returns indexed paths. You don't need to push either.

If you're not in a git repo at all, there's a fallback: the packer walks the filesystem and uses `.gitignore` as the exclude list.

### When NOT to use `--sync-code`

Rare but real:

- You're running a cloud-managed workflow that doesn't touch local code (a fully `config`-typed workflow).
- You've set up a workflow whose `setup-branch` step explicitly `git clone`s something and doesn't care about local state.

### Typical flow

```bash
# Edit your workflow locally
vim workflows/fix-bug.ts

# Stage everything the run needs — `git add` is enough; commit is optional.
# Untracked files would be silently excluded otherwise.
git add workflows/fix-bug.ts

# Ship your working tree to the cloud sandbox and run
agent-relay cloud run workflows/fix-bug.ts --sync-code
# note the run ID printed...

# Stream logs as it runs
agent-relay cloud logs <run-id> --follow

# When complete, pull the produced diff back into your local worktree
agent-relay cloud sync <run-id>
```

See [Workflows → Common mistakes](/docs/workflows-common-mistakes#output-and-exit-codes) for the "untracked files silently excluded" pitfall.

## Inspect a patch before applying it

```bash
Expand Down
95 changes: 95 additions & 0 deletions web/content/docs/github-primitive.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: 'GitHub primitive'
description: 'Workflow-specific primitive: a typed GitHub integration step. Runs through the local `gh` CLI or a cloud proxy.'
---

The GitHub primitive is a **workflow-specific primitive** — an integration step shaped for `workflow()`. It gives a workflow a typed GitHub surface (create issues, open PRs, read files, merge branches) with one call site that works the same locally via `gh` or in cloud via Nango / relay-cloud.

It's **bundled with the SDK** — no separate install. If you've run `npm install @agent-relay/sdk`, you already have it.

## Usage inside a workflow

Import `createGitHubStep` from the SDK's `/github` subpath and drop it in anywhere you'd use a regular `.step()`:

```ts
import { workflow } from '@agent-relay/sdk/workflows';
import { createGitHubStep } from '@agent-relay/sdk/github';

await workflow('ship-readme')
.agent('writer', { cli: 'claude' })

.step('read-readme', createGitHubStep({
action: 'readFile',
repo: 'AgentWorkforce/relay',
params: { path: 'README.md' },
output: { mode: 'data', format: 'text' },
}))

.step('edit', {
agent: 'writer',
dependsOn: ['read-readme'],
task: `Current README:\n{{steps.read-readme.output}}\n\nClean up the intro.`,
})

.step('open-pr', createGitHubStep({
action: 'createPR',
repo: 'AgentWorkforce/relay',
params: {
head: 'docs/readme-cleanup',
base: 'main',
title: 'docs: clean up README intro',
body: 'Lightly edited for clarity.',
},
}))

.run({ cwd: process.cwd() });
```

Under the hood each `createGitHubStep(...)` call produces a `type: 'integration'` step — the runner schedules it, applies verification, and captures output the same way as any other step.

## Supported actions

- **Repositories** — `listRepos`, `getRepo`
- **Issues** — `listIssues`, `createIssue`, `updateIssue`, `closeIssue`
- **Pull requests** — `listPRs`, `getPR`, `createPR`, `updatePR`, `mergePR`
- **Files** — `listFiles`, `readFile`, `createFile`, `updateFile`, `deleteFile`
- **Branches + commits** — `listBranches`, `createBranch`, `listCommits`, `createCommit`
- **Identity** — `getUser`, `listOrganizations`

All actions work through the `action` + `params` shape. Outputs are typed — downstream steps can use `{{steps.<name>.output}}` to chain values through the workflow.

## Runtime selection

The primitive auto-picks the right backend for the environment it's running in:

| Mode | Triggered when |
|---|---|
| `local` (via `gh` CLI) | `gh auth status` succeeds and no cloud creds are set |
| `cloud` (via Nango) | `NANGO_SECRET_KEY` + `NANGO_GITHUB_CONNECTION_ID` + `NANGO_GITHUB_PROVIDER_CONFIG_KEY` are present |
| `cloud` (via relay-cloud) | `RELAY_CLOUD_API_URL` + `RELAY_CLOUD_API_TOKEN` + `WORKSPACE_ID` are present (fallback when Nango absent) |

Pick the default (`runtime: 'auto'`) unless you need to pin one for testing. You can also set `runtime` per step via the `config` field — useful when the same workflow creates PRs across multiple tenants with different GitHub App installs.

## Multi-tenant cloud routing

Every cloud workspace can have its own GitHub App install — one Nango connection per tenant. `createGitHubStep` accepts a per-step `config` field so a single workflow can route different actions through different connections:

```ts
createGitHubStep({
action: 'createPR',
repo: 'AgentWorkforce/cloud',
params: { title, head, base, body },
config: await githubConfigForRepo({
repo: 'AgentWorkforce/cloud',
workspaceId: process.env.RELAY_WORKSPACE_ID,
}),
});
```

The primitive itself stays tenant-unaware — it takes a `GitHubRuntimeConfig` and does what it's told. Tenant lookup lives in your app (typically a `connection-resolver` helper). Adding a new GitHub App install is a config row, not a code change.

## See also

- [Workflows introduction](/docs/workflows-introduction) — where this primitive shines.
- [Patterns](/docs/workflows-patterns) — canonical workflow shapes that commonly use GitHub steps (PR review loops, multi-repo shipping, etc.).
- [Authentication](/docs/authentication) — credentials model for the cloud runtime modes.
39 changes: 37 additions & 2 deletions web/content/docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,22 @@ asyncio.run(main())

## CLI

If you want to run Agent Relay directly from the terminal instead of embedding the SDK in an app, install the CLI globally and start a local relay session:
If you want to run Agent Relay directly from the terminal instead of embedding the SDK in an app, install the CLI globally and start a local relay session.

Install `agent-relay`. The install script is the preferred option — it pulls the native broker binary for your platform along with the CLI.

<CodeGroup>
```bash install script (recommended)
curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash
```
```bash npm
npm install -g agent-relay
```
</CodeGroup>

Start a local broker and spawn agents:

```bash
npm i -g agent-relay
agent-relay up
agent-relay spawn planner claude "Break the work into steps"
agent-relay spawn coder codex "Implement the approved plan"
Expand All @@ -137,3 +149,26 @@ agent-relay who
```

See [CLI Overview](/docs/cli-overview) for the full command surface and [Broker Lifecycle](/docs/cli-broker-lifecycle) for running the local broker.

### Let another agent orchestrate the team

If you want a host agent (Claude, Codex, or any CLI) to autonomously spawn and coordinate sub-agents from inside its own session, install the `running-headless-orchestrator` skill. It wires the `agent-relay spawn` / `agent-relay dm` commands into the host's tool surface so the host can run the team without leaving its session.

<CodeGroup>
```bash prpm (recommended)
# Install for a specific host
npx prpm install @agent-relay/running-headless-orchestrator --as claude

# Or install for multiple hosts at once
npx prpm install @agent-relay/running-headless-orchestrator --as claude,codex
```
```bash skills
npx skills add https://github.com/agentworkforce/skills --skill running-headless-orchestrator
```
</CodeGroup>

Once installed, you can prompt the host like:

> "Use the running-headless-orchestrator skill and spawn a claude agent called `reviewer`, DM it instructions, and wait for its verdict before you proceed."

The host uses the skill's tools to run `agent-relay spawn reviewer claude ...`, send DMs via `agent-relay dm`, and read replies — all without you touching the shell.
Loading
Loading