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
35 changes: 27 additions & 8 deletions examples/weekly-digest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@ Weekly competitive-intel agent. Runs every Saturday at 09:00 UTC, queries
Brave Search for the configured topics, dedupes + clusters by source host,
and upserts a single GitHub issue per ISO week into `WEEKLY_DIGEST_REPO`.

## How GitHub writes happen

Workforce integration clients **don't make direct REST calls to GitHub**.
The handler calls `ctx.github.upsertIssue(...)`, which writes a draft
JSON file at the canonical Relayfile path
`/github/repos/<owner>/<repo>/issues/...` inside the Relayfile mount.
Relayfile's writeback worker picks up the draft, makes the real GitHub
call, and writes a receipt back to the same file. The handler reads the
receipt to populate issue numbers, URLs, etc.

This matches the rest of the workforce/cloud stack and gets writeback
durability + retry semantics for free. There's no `GITHUB_TOKEN` to
manage — Relayfile holds the GitHub App / OAuth credentials.

## Required env

```sh
export WEEKLY_DIGEST_TOPICS="agentworkforce,relayfile,proactive-agents"
export WEEKLY_DIGEST_REPO="YourOrg/weekly-digest"
export BRAVE_API_KEY="brave_..."

# GitHub credentials — either path works:
export WORKFORCE_INTEGRATION_GITHUB_TOKEN="ghp_..."
# or, for a quick demo without Relayfile:
export GITHUB_TOKEN="ghp_..."

# Workspace (only needed when actually launching, not for --dry-run):
export WORKFORCE_WORKSPACE_ID="ws_demo"
export WORKFORCE_WORKSPACE_TOKEN="ws_token_..."

# Relayfile mount root the handler writes into. The workforce runtime
# sets this automatically when it spawns the handler. Only set it
# manually when running the bundle stand-alone (smoke tests).
export RELAYFILE_MOUNT_ROOT="/path/to/your/relayfile/mount"
```

## Deploy
Expand All @@ -39,17 +53,22 @@ workforce deploy ./examples/weekly-digest/persona.json --mode dev
## Firing the handler manually

The runner reads NDJSON envelopes from stdin. To trigger the handler from
the command line, drive the bundle directly:
the command line against a Relayfile mount you've already set up, drive
the bundle directly:
Comment thread
coderabbitai[bot] marked this conversation as resolved.

```sh
RELAYFILE_MOUNT_ROOT=/path/to/mount \
echo '{"id":"manual-1","workspace":"ws_demo","type":"cron.tick","occurredAt":"2026-05-12T09:00:00Z","name":"weekly","cron":"0 9 * * 6"}' \
| node /tmp/wf-weekly-digest/runner.mjs
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

The handler will:

1. Resolve topics + repo + tokens from env.
1. Resolve topics + repo from env.
2. Query Brave Search per topic.
3. Dedupe by URL and cluster results by source host.
4. Upsert a single `Weekly digest — YYYY-WNN` issue in the target repo.
4. Write a draft (or update an existing) `Weekly digest — YYYY-WNN`
issue under `<mount>/github/repos/<owner>/<repo>/issues/...`.
Relayfile's writeback worker turns the file write into the actual
GitHub call.
5. Save a memory note tagged `weekly-digest` + `week:<W>`.
32 changes: 13 additions & 19 deletions examples/weekly-digest/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,10 @@ export default handler(async (ctx, event) => {
});
});

function readConfig(): { topics: string; repo: string; braveApiKey: string; githubToken: string } {
function readConfig(): { topics: string; repo: string; braveApiKey: string } {
const topics = process.env.WEEKLY_DIGEST_TOPICS;
const repo = process.env.WEEKLY_DIGEST_REPO;
const braveApiKey = process.env.BRAVE_API_KEY;
const githubToken =
process.env.WORKFORCE_INTEGRATION_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? '';
if (!topics || !topics.trim()) {
throw new Error('WEEKLY_DIGEST_TOPICS is required (comma-separated list)');
}
Expand All @@ -104,24 +102,21 @@ function readConfig(): { topics: string; repo: string; braveApiKey: string; gith
if (!braveApiKey) {
throw new Error('BRAVE_API_KEY is required to query Brave Search');
}
if (!githubToken) {
throw new Error(
'WORKFORCE_INTEGRATION_GITHUB_TOKEN (or GITHUB_TOKEN) is required to upsert the digest issue'
);
}
return { topics, repo, braveApiKey, githubToken };
return { topics, repo, braveApiKey };
}

function resolveGithubClient(ctx: WorkforceCtx): GithubClient {
// The runtime injects a Relayfile-VFS-backed github client whenever
// the persona declares the `github` integration. For stand-alone
// dev runs without the runtime, fall back to a client rooted at the
// configured Relayfile mount (or cwd if RELAYFILE_MOUNT_ROOT is
// unset). The fallback path is mostly useful for local smoke tests
// — production handlers always get `ctx.github`.
if (ctx.github) return ctx.github;
const token =
process.env.WORKFORCE_INTEGRATION_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? '';
if (!token) {
throw new Error(
'no GitHub client on ctx and no GITHUB_TOKEN in env — set WORKFORCE_INTEGRATION_GITHUB_TOKEN before deploy'
);
}
return createGithubClient({ token });
return createGithubClient({
...(process.env.RELAYFILE_MOUNT_ROOT ? { relayfileMountRoot: process.env.RELAYFILE_MOUNT_ROOT } : {}),
writebackTimeoutMs: 30_000
});
}

function parseTopics(raw: string): string[] {
Expand All @@ -144,8 +139,7 @@ async function searchBrave(query: string, apiKey: string): Promise<DigestItem[]>
throw new WorkforceIntegrationError({
provider: 'brave',
operation: 'search',
message: `${response.status} ${response.statusText}`,
status: response.status,
cause: new Error(`${response.status} ${response.statusText}`),
retryable: response.status >= 500 || response.status === 429
});
}
Expand Down
35 changes: 0 additions & 35 deletions packages/runtime/src/clients/errors.ts

This file was deleted.

Loading
Loading