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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ packages/*/dist/
*.tsbuildinfo
.npm-cache
.agent-relay
# workforce deploy bundle output (staged per-persona by `workforce deploy`)
**/.workforce/
373 changes: 373 additions & 0 deletions docs/plans/deploy-v1-codex-spec.md

Large diffs are not rendered by default.

470 changes: 470 additions & 0 deletions docs/plans/deploy-v1-workflow-spec.md

Large diffs are not rendered by default.

622 changes: 622 additions & 0 deletions docs/plans/deploy-v1.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion examples/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"noEmit": true
"noEmit": true,
"baseUrl": ".",
"paths": {
"@agentworkforce/runtime": ["../packages/runtime/src/index.ts"],
"@agentworkforce/runtime/runner": ["../packages/runtime/src/runner.ts"],
"@agentworkforce/runtime/clients": ["../packages/runtime/src/clients/index.ts"],
"@agentworkforce/runtime/raw": ["../packages/runtime/src/raw.ts"],
"@agentworkforce/persona-kit": ["../packages/persona-kit/src/index.ts"]
}
},
"include": ["./**/*.ts"]
}
55 changes: 55 additions & 0 deletions examples/weekly-digest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Example: `weekly-digest`

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`.

## 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_..."
```

## Deploy

```sh
# Validate the persona without side effects.
workforce deploy ./examples/weekly-digest/persona.json --dry-run

# Stage the bundle to a directory and inspect it (no launch).
workforce deploy ./examples/weekly-digest/persona.json \
--bundle-out /tmp/wf-weekly-digest

# Run locally as a long-lived process; pipe an envelope on stdin to fire
# the handler immediately. The runner exits when stdin closes.
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:

```sh
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
```

The handler will:

1. Resolve topics + repo + tokens 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.
5. Save a memory note tagged `weekly-digest` + `week:<W>`.
231 changes: 231 additions & 0 deletions examples/weekly-digest/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import {
createGithubClient,
handler,
WorkforceIntegrationError,
type GithubClient,
type WorkforceCtx,
type WorkforceEvent
} from '@agentworkforce/runtime';
Comment thread
coderabbitai[bot] marked this conversation as resolved.

interface DigestItem {
title: string;
url: string;
description: string;
host: string;
}

interface DigestCluster {
host: string;
items: DigestItem[];
}

export default handler(async (ctx, event) => {
if (event.source !== 'cron' || event.name !== 'weekly') {
ctx.log('warn', 'weekly-digest.ignored', { source: event.source });
return;
}

const config = readConfig();
const github = resolveGithubClient(ctx);

const topics = parseTopics(config.topics);
const fetchedAt = new Date(event.occurredAt);
const isoWeek = isoWeekString(fetchedAt);
const title = `Weekly digest — ${isoWeek}`;

ctx.log('info', 'weekly-digest.search.start', { topics, week: isoWeek });

const items: DigestItem[] = [];
for (const topic of topics) {
try {
const found = await searchBrave(topic, config.braveApiKey);
items.push(...found);
} catch (err) {
ctx.log('error', 'weekly-digest.search.failed', {
topic,
error: err instanceof Error ? err.message : String(err)
});
}
}

const deduped = dedupeByUrl(items);
const clusters = clusterByHost(deduped);

if (clusters.length === 0) {
ctx.log('info', 'weekly-digest.no-results', { week: isoWeek });
return;
}

const body = renderDigest({ week: isoWeek, fetchedAt, topics, clusters });
const repoSegments = config.repo.split('/');
if (repoSegments.length !== 2 || !repoSegments[0].trim() || !repoSegments[1].trim()) {
throw new Error(
`weekly-digest: WEEKLY_DIGEST_REPO must be exactly "owner/repo"; got "${config.repo}"`
);
}
const [owner, repo] = repoSegments as [string, string];

const result = await github.upsertIssue({
owner,
repo,
title,
body,
matchTitle: title,
labels: ['weekly-digest']
});

ctx.log('info', 'weekly-digest.issue.upserted', {
week: isoWeek,
number: result.number,
url: result.url,
created: result.created,
clusterCount: clusters.length,
itemCount: deduped.length
});

await ctx.memory.save(`Weekly digest ${isoWeek} published: ${result.url}`, {
tags: ['weekly-digest', `week:${isoWeek}`],
scope: 'workspace'
});
});

function readConfig(): { topics: string; repo: string; braveApiKey: string; githubToken: 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)');
}
if (!repo || !repo.trim()) {
throw new Error('WEEKLY_DIGEST_REPO is required (format: "owner/repo")');
}
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 };
}

function resolveGithubClient(ctx: WorkforceCtx): GithubClient {
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 });
}

function parseTopics(raw: string): string[] {
return raw
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
}

async function searchBrave(query: string, apiKey: string): Promise<DigestItem[]> {
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=10&freshness=pw`;
const response = await fetch(url, {
headers: {
accept: 'application/json',
'X-Subscription-Token': apiKey,
'user-agent': 'workforce-weekly-digest'
}
});
if (!response.ok) {
throw new WorkforceIntegrationError({
provider: 'brave',
operation: 'search',
message: `${response.status} ${response.statusText}`,
status: response.status,
retryable: response.status >= 500 || response.status === 429
});
}
const payload = (await response.json()) as {
web?: { results?: Array<{ title: string; url: string; description: string }> };
};
const results = payload.web?.results ?? [];
return results.map((r) => ({
title: r.title,
url: r.url,
description: r.description,
host: safeHost(r.url)
}));
}

function dedupeByUrl(items: DigestItem[]): DigestItem[] {
const seen = new Set<string>();
const out: DigestItem[] = [];
for (const item of items) {
if (seen.has(item.url)) continue;
seen.add(item.url);
out.push(item);
}
return out;
}

function clusterByHost(items: DigestItem[]): DigestCluster[] {
const buckets = new Map<string, DigestItem[]>();
for (const item of items) {
const existing = buckets.get(item.host);
if (existing) existing.push(item);
else buckets.set(item.host, [item]);
}
return Array.from(buckets.entries())
.map(([host, bucketItems]) => ({ host, items: bucketItems }))
.sort((a, b) => b.items.length - a.items.length);
}

function renderDigest(args: {
week: string;
fetchedAt: Date;
topics: string[];
clusters: DigestCluster[];
}): string {
const lines: string[] = [];
lines.push(`# Weekly digest — ${args.week}`);
lines.push('');
lines.push(`Fetched at ${args.fetchedAt.toISOString()}.`);
lines.push(`Topics: ${args.topics.join(', ')}`);
lines.push('');
for (const cluster of args.clusters) {
lines.push(`## ${cluster.host} (${cluster.items.length})`);
for (const item of cluster.items) {
lines.push(`- [${item.title}](${item.url}) — ${truncate(item.description, 200)}`);
}
lines.push('');
}
return lines.join('\n');
}

function safeHost(rawUrl: string): string {
try {
return new URL(rawUrl).hostname;
} catch {
return 'unknown';
}
}

function isoWeekString(date: Date): string {
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
const dayOfWeek = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayOfWeek);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNum = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNum).padStart(2, '0')}`;
}

function truncate(s: string, n: number): string {
return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
}

// Touch the imported types so build does not warn on type-only imports.
type _Touch = WorkforceEvent;
48 changes: 48 additions & 0 deletions examples/weekly-digest/persona.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"id": "weekly-digest",
"intent": "documentation",
"tags": ["documentation"],
"description": "Weekly competitive-intel digest. Searches the web for mentions of configured topics, dedupes and clusters by source domain, and upserts a single GitHub issue per ISO week.",
"cloud": true,
"integrations": {
"github": {}
},
"schedules": [
{ "name": "weekly", "cron": "0 9 * * 6", "tz": "UTC" }
],
"sandbox": true,
"memory": { "enabled": true, "scopes": ["workspace"], "ttlDays": 90 },
"onEvent": "./agent.ts",
"inputs": {
"WEEKLY_DIGEST_TOPICS": {
"description": "Comma-separated list of topics the agent searches for each week.",
"env": "WEEKLY_DIGEST_TOPICS",
"default": "agentworkforce,relayfile,proactive-agents"
},
"WEEKLY_DIGEST_REPO": {
"description": "GitHub repo to upsert the digest issue into; format \"owner/repo\".",
"env": "WEEKLY_DIGEST_REPO",
"default": "AgentWorkforce/weekly-digest"
}
},
"tiers": {
"best": {
"harness": "codex",
"model": "openai-codex/gpt-5.3-codex",
"systemPrompt": "Research the configured topics and produce a clustered weekly digest.",
"harnessSettings": { "reasoning": "high", "timeoutSeconds": 1200 }
},
"best-value": {
"harness": "opencode",
"model": "opencode/gpt-5-nano",
"systemPrompt": "Research the configured topics and produce a clustered weekly digest.",
"harnessSettings": { "reasoning": "medium", "timeoutSeconds": 900 }
},
"minimum": {
"harness": "opencode",
"model": "opencode/minimax-m2.5-free",
"systemPrompt": "Research the configured topics and produce a clustered weekly digest.",
"harnessSettings": { "reasoning": "low", "timeoutSeconds": 600 }
}
}
}
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"package.json"
],
"dependencies": {
"@agentworkforce/deploy": "workspace:*",
"@agentworkforce/persona-kit": "workspace:*",
"@agentworkforce/workload-router": "workspace:*",
"@relayburn/sdk": "^2.5.2",
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
type AutoSyncHandle
} from '@relayfile/local-mount';
import ora, { type Ora } from 'ora';
import { runDeploy, runLogin } from './deploy-command.js';
import {
startLaunchMetadataRecording,
type LaunchMetadataRun
Expand Down Expand Up @@ -3816,6 +3817,16 @@ export async function main(): Promise<void> {
await runPick(rest);
}

if (subcommand === 'deploy') {
await runDeploy(rest);
return;
}

if (subcommand === 'login') {
await runLogin(rest);
return;
}

if (subcommand !== 'agent') {
die(`Unknown subcommand "${subcommand}".`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Expand Down
Loading
Loading