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
64 changes: 15 additions & 49 deletions docs/plans/deploy-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ One file. One command. One contract.

### In

- Persona JSON schema extension: `cloud`, `useSubscription`, `integrations`, `schedules`, `sandbox`, `memory`, `traits`, `onEvent`.
- Persona JSON schema extension: `cloud`, `useSubscription`, `integrations`, `schedules`, `memory`, `onEvent`.
- New package `@agentworkforce/runtime` — thin facade exposing `handler(...)` that wraps `agent({...})` from `@agent-relay/agent` (cloud proactive-runtime M1 SDK).
- New package `@agentworkforce/deploy` — the deploy CLI logic; the existing `cli.ts` gets a `deploy` case that dispatches to it.
- Daytona sandbox launcher used in the `--sandbox` run mode.
Expand Down Expand Up @@ -74,11 +74,11 @@ All new fields are optional. A persona that does not set any of them continues t
| `useSubscription` | `boolean` | optional | When `true`, inference uses the user's connected LLM subscription via `@agent-relay/cloud`'s provider link (no workforce-billed tokens). Triggers a `connectProvider` step at deploy time. |
| `integrations` | `Record<string, IntegrationConfig>` | when persona has event triggers | Declares which Relayfile providers this agent needs and what events fire its handler. See §3.2. |
| `schedules` | `Schedule[]` | when persona runs on cron | One or more cron triggers, registered with the runtime's `ctx.schedule.every(...)`. Each schedule has a `name` echoed back to the handler. See §3.3. |
| `sandbox` | `boolean \| SandboxConfig` | optional | `true` (default) means agent runs inside a Daytona sandbox. `false` means the runner process owns its own filesystem. Object form lets you tune env / timeout. See §3.4. |
| `memory` | `boolean \| MemoryConfig` | optional | Enables the agent-assistant memory subsystem. Scopes and TTL configurable. See §3.5. |
| `traits` | `Traits` | optional, **only meaningful for interactive agents** | Mirrors `@agent-assistant/traits`: voice, formality, proactivity, etc. Applied when the agent posts to a chat surface (Slack, Relaycast). Headless agents (paraglide-style "Linear issue → ship") may omit this. See §3.6. |
| `memory` | `boolean \| MemoryConfig` | optional | Enables the agent-assistant memory subsystem. Scopes and TTL configurable. See §3.4. |
| `onEvent` | `string` | when `cloud: true` and any trigger declared | Path to a TS file (relative to the persona JSON) whose default export is the event handler. Sub-file references like `./agent.ts` and `./handlers/index.ts` are supported. See §4. |

`traits` and `sandbox` were removed from the persona spec in v1. Personality belongs in the persona's prompt/sidecar and the persona-personality-builder flow. Sandbox behavior is deploy-time runtime configuration: sandbox mode is on by default for deploys, with opt-out handled by deploy flags or runtime config rather than persona JSON.

### 3.2 `integrations` shape

```jsonc
Expand Down Expand Up @@ -119,56 +119,25 @@ The act of stacking integrations is just declaring multiple keys. The act of lin
- `cron` is a standard 5-field expression. `tz` defaults to `UTC`.
- Multiple schedules are allowed. The runtime registers each with `ctx.schedule.every(cron, { tz, payload: { name } })`.

### 3.4 `sandbox` shape

```jsonc
"sandbox": true // default
"sandbox": { "enabled": true, "timeoutSeconds": 1800, "env": { "FOO": "bar" } }
"sandbox": false // run in the runner process's fs
```

- Image is **not** user-configurable in v1. Workforce picks a standard image (`node-22` baseline) for the default Daytona sandbox. We can add `image` later if a real demand surfaces; eliminating the field keeps the v1 contract small.
- `timeoutSeconds` caps a single handler invocation. Default 1800s.
- `env` adds env vars on top of the auto-injected secrets (Relayfile connection tokens, harness inference creds, etc.).
- When `sandbox: false`, the agent's `ctx.sandbox` still exists but points at the runner's own process — useful for `--dev` iteration, **not** what we recommend for production.

### 3.5 `memory` shape
### 3.4 `memory` shape

```jsonc
"memory": true // sensible defaults
"memory": {
"enabled": true,
"scopes": ["session", "user", "workspace"],
"scopes": ["workspace", "user", "global"],
"ttlDays": 30,
"autoPromote": true,
"dedupMs": 300000
}
```

- Implementation: the runtime wires `@agent-assistant/memory` with the supermemory adapter (matching sage today). API key is pulled from workforce-managed env, not declared in the persona.
- `scopes` is the only field with real semantic weight: session-only memory is wiped per handler; user-scope persists across the user's invocations of this agent; workspace persists across all users.
- `scopes` is the only field with real semantic weight: workspace memory persists across users in a workspace, user memory follows an individual user's invocations, and global memory is shared across the deployed agent.
- `autoPromote` flips on the sage turn-recorder pattern — agent decides if session content is worth promoting.
- **No `memoryMd` file.** Memory is config, not prose. Personality goes in `traits` and `description`.

### 3.6 `traits` shape

Direct mapping to `@agent-assistant/traits`:

```jsonc
"traits": {
"voice": "professional-warm",
"formality": "low",
"proactivity": "medium",
"riskPosture": "conservative",
"domain": "engineering",
"vocabulary": ["PR", "diff", "CI"],
"preferMarkdown": true
}
```

Only used when the runtime renders into a conversational surface (Slack message, Relaycast post, GitHub PR comment). Skip the field entirely for headless agents — saves the runtime a subsystem registration.
- **No `memoryMd` file.** Memory is config, not prose. Personality goes in prompt/sidecar content and the persona-personality-builder flow.

### 3.7 Trigger-name registry
### 3.5 Trigger-name registry

`packages/persona-kit/src/triggers.ts` (new) ships a small registry of known trigger names per provider so the deploy CLI can lint them:

Expand Down Expand Up @@ -217,7 +186,7 @@ interface WorkforceCtx {
notion?: NotionClient;
jira?: JiraClient;

// Daytona sandbox (or process fs if sandbox:false)
// Daytona sandbox or runtime-provided process fs
sandbox: {
cwd: string; // absolute path inside the sandbox
exec(cmd: string, opts?: { cwd?: string; env?: Record<string,string> }): Promise<ExecResult>;
Expand All @@ -242,7 +211,7 @@ interface WorkforceCtx {
cancel(name: string): Promise<void>;
};

// Persona metadata (id, traits, harness tier defaults, etc.) — read-only
// Persona metadata (id, harness defaults, listeners, etc.) — read-only
persona: PersonaSpec;
}

Expand All @@ -254,7 +223,7 @@ export function handler<I extends IntegrationKeys>(
Implementation notes:
- `handler(...)` reads the persona JSON adjacent to the entrypoint (workforce bundles them together). At cold-start it:
1. Calls `agent({ workspace, schedule, watch, inbox, onEvent: shim })` from `@agent-relay/agent`, mapping `persona.integrations` to `watch` and `persona.schedules` to `schedule`.
2. Builds `ctx` once per agent boot: opens Daytona handle (if `sandbox: true`), wires Relayfile-derived clients, attaches memory adapter.
2. Builds `ctx` once per agent boot: opens Daytona handle when deploy runs in sandbox mode, wires Relayfile-derived clients, attaches memory adapter.
3. The `shim` reshapes the raw envelope from `@agent-relay/agent` into the `WorkforceEvent` discriminated union and invokes the user's `fn(ctx, event)`.
- The user never imports `@agent-relay/agent` directly. Workforce owns the ergonomics. If the underlying SDK churns, we absorb the diff here.
- The SDK doors stay open for power users: we re-export `agent` from `@agentworkforce/runtime/raw` so anyone who wants the lower-level surface can drop down. This matters for nightcto-shaped projects that outgrow the persona contract.
Expand Down Expand Up @@ -354,7 +323,6 @@ Direct port of the proactive-agents weekly-digest pattern.
"cloud": true,
"integrations": { "github": { "scope": { "repo": "AgentWorkforce/weekly-digest" } } },
"schedules": [{ "name": "weekly", "cron": "0 9 * * 6", "tz": "UTC" }],
"sandbox": true,
"memory": { "enabled": true, "scopes": ["workspace"], "ttlDays": 90 },
"onEvent": "./agent.ts",
"tiers": { ... standard codex/opencode tiers ... }
Expand Down Expand Up @@ -385,9 +353,7 @@ Direct port of the proactive-agents weekly-digest pattern.
},
"slack": { "triggers": [{ "on": "app_mention" }] }
},
"sandbox": true,
"memory": { "enabled": true, "scopes": ["session", "workspace"] },
"traits": { "voice": "professional-warm", "formality": "low", "preferMarkdown": true },
"memory": { "enabled": true, "scopes": ["user", "workspace"] },
"onEvent": "./agent.ts",
"tiers": { ... }
}
Expand All @@ -409,7 +375,7 @@ workforce/
│ ├── cli/ # add `deploy`, `login` cases
│ ├── persona-kit/ # extend PersonaSpec schema (§3)
│ │ └── src/
│ │ ├── types.ts # +CloudFields, +IntegrationConfig, +Schedule, +Sandbox, +Memory, +Traits
│ │ ├── types.ts # +CloudFields, +IntegrationConfig, +Schedule, +Memory
│ │ ├── parse.ts # extend parsePersonaSpec to read new fields
│ │ └── triggers.ts # NEW — known triggers registry (§3.7)
│ ├── harness-kit/ # no changes for v1
Expand Down Expand Up @@ -494,7 +460,7 @@ If a track slips, §10's fallback applies: ship `--dev` end-to-end with `weekly-
Tasks that are mechanical, well-specified, and don't gate on my decisions — perfect for a codex agent spawned via `workforce agent code-implementer` or a similar persona:

1. **Trigger registry expansion** — fill out `packages/persona-kit/src/triggers.ts` with the full set of known trigger names per Tier-1 provider (Linear, GitHub, Slack, Notion, Jira) by reading the Relayfile provider docs in `/Users/khaliqgant/Projects/AgentWorkforce/relayfile/docs/`.
2. **Test fixtures** — generate sample `persona.json` files exercising every optional combination (with/without traits, sandbox false, multi-schedule, etc.) into `packages/persona-kit/src/__fixtures__/`.
2. **Test fixtures** — generate sample `persona.json` files exercising deploy optional combinations (memory, multi-schedule, integrations, etc.) into `packages/persona-kit/src/__fixtures__/`.
3. **JSON Schema export** — emit a JSON Schema from the extended `PersonaSpec` for editor autocomplete. New script: `packages/persona-kit/scripts/emit-schema.mjs`. Wire to `pnpm run build` so it ships with the package.
4. **Example expansion** — write a third example, `examples/linear-shipper/` (the paraglide pattern: Linear issue created → drive to PR), purely against the runtime substrate I land in §9.1.
5. **README polish** — once the deploy command is real, codex agent rewrites the workforce README to lead with the deploy story.
Expand Down
17 changes: 17 additions & 0 deletions examples/linear-shipper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Linear Shipper

This deployable persona follows the paraglide pattern: a Linear issue triggers a sandboxed implementation run, then the agent links the result back to Linear.

## Setup

Connect Linear and GitHub before deploying.

```bash
workforce deploy ./examples/linear-shipper/persona.json --mode dev
```

Set the target repository through the persona inputs: `GITHUB_OWNER`, `GITHUB_REPO`, and `REPO_URL`.

## Current GitHub Handoff

The v1 client contract exposes `createIssue`, not `createPr`, so the example creates a draft handoff issue and includes a `TODO(human)` where `createPr` should be used once the runtime exposes it.
69 changes: 69 additions & 0 deletions examples/linear-shipper/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { handler } from '@agentworkforce/runtime';

type LinearIssueEvent = {
issue?: { id?: string; identifier?: string; title?: string; url?: string };
};

function inputDefault(ctx: Parameters<Parameters<typeof handler>[0]>[0], name: string): string {
// Mirror `resolvePersonaInputs` precedence (packages/persona-kit/src/inputs.ts):
// explicit env var (spec.env ?? input name) wins over the static JSON default.
const spec = ctx.persona.inputs?.[name];
const envName = spec?.env ?? name;
const fromEnv = process.env[envName];
const value = (fromEnv !== undefined && fromEnv !== '' ? fromEnv : undefined) ?? spec?.default;
if (!value) throw new Error(`${name} input is required`);
return value;
}
Comment on lines +7 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 inputDefault reads static persona spec defaults, ignoring env-var overrides

The inputDefault helper at examples/linear-shipper/agent.ts:8 reads ctx.persona.inputs?.[name]?.default, which returns the literal default string from the persona JSON definition. This completely bypasses the persona-kit input resolution chain (resolvePersonaInputs at packages/persona-kit/src/inputs.ts:28-54) that checks explicit values → environment variables → defaults. When a user sets REPO_URL, GITHUB_OWNER, or GITHUB_REPO via environment variables (as the README at examples/linear-shipper/README.md:12 instructs), those overrides are silently ignored and the handler always uses the hardcoded JSON defaults (AgentWorkforce, workforce, etc.).

How the input resolution chain is supposed to work

The PersonaInputSpec type declares an env field that names the env var to read. When env is unset, the key name itself is the env var (e.g. REPO_URL maps to process.env.REPO_URL). resolvePersonaInputs implements this precedence. But inputDefault skips all of that and goes straight to .default.

Prompt for agents
The inputDefault function in examples/linear-shipper/agent.ts reads ctx.persona.inputs?.[name]?.default which only returns the static JSON default, ignoring environment variable overrides. The WorkforceCtx interface (packages/runtime/src/types.ts) does not currently expose resolved input values — it only carries the raw PersonaSpec. There are two possible fixes:

1. (Preferred) Add a resolvedInputs or inputValues field to WorkforceCtx (in packages/runtime/src/types.ts) and have the runner (packages/runtime/src/runner.ts) call resolvePersonaInputs at boot and attach the result. Then the handler reads ctx.inputValues[name] instead.

2. (Quick fix) Change inputDefault to use the same resolution logic as resolvePersonaInputs — read process.env using the input's env field (or the key name as fallback), then fall back to .default. Something like: const spec = ctx.persona.inputs?.[name]; const envName = spec?.env ?? name; const value = process.env[envName] ?? spec?.default;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}

function safeRepoDirName(value: string): string {
if (!/^[A-Za-z0-9._-]+$/.test(value)) {
throw new Error('GITHUB_REPO must be a repository name, not a path or shell fragment');
}
return value;
}

export default handler(async (ctx, event) => {
if (event.source !== 'linear' || event.type !== 'issue.created') return;
if (!ctx.linear) throw new Error('linear-shipper requires the linear integration');
if (!ctx.github) throw new Error('linear-shipper requires the github integration');

const payload =
typeof event.payload === 'object' && event.payload !== null
? (event.payload as LinearIssueEvent)
: {};
const issueRef = payload.issue;
const issueId = issueRef?.id ?? issueRef?.identifier;
if (!issueId) throw new Error('Linear event is missing an issue id');

const issue = await ctx.linear.getIssue(issueId);
const repoUrl = inputDefault(ctx, 'REPO_URL');
const owner = inputDefault(ctx, 'GITHUB_OWNER');
const repo = safeRepoDirName(inputDefault(ctx, 'GITHUB_REPO'));
const repoDir = `${ctx.sandbox.cwd}/${repo}`;

await ctx.sandbox.exec(`git clone ${shellQuote(repoUrl)} ${shellQuote(repoDir)}`);
const result = await ctx.harness.run({
prompt: `Implement this Linear issue. Create the smallest reviewable change and include verification notes.\n\nTitle: ${issue.title}\n\n${issue.description ?? ''}`,
cwd: repoDir
});

// TODO(human): createPr is not in the published GithubClient contract yet.
const created = await ctx.github.createIssue({
owner,
repo,
title: `Draft PR needed: ${issue.title}`,
body: [
`Linear issue: ${issue.url ?? issueId}`,
'',
'The harness produced an implementation attempt, but GithubClient.createPr is not exposed yet.',
'',
result.output
].join('\n')
});

await ctx.linear.comment(issueId, `Implementation attempt captured in GitHub issue: ${created.url}`);
});
41 changes: 41 additions & 0 deletions examples/linear-shipper/persona.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"id": "linear-shipper",
"intent": "implement-frontend",
"tags": ["implementation"],
"description": "Turns a new Linear issue into an implementation attempt and links the resulting GitHub work back to Linear.",
"cloud": true,
"integrations": {
"linear": {
"triggers": [{ "on": "issue.created" }]
},
"github": {
"scope": {
"repo": "AgentWorkforce/workforce"
}
}
},
"inputs": {
"GITHUB_OWNER": {
"description": "GitHub owner containing the target repository.",
"default": "AgentWorkforce"
},
"GITHUB_REPO": {
"description": "Target repository name.",
"default": "workforce"
},
"REPO_URL": {
"description": "Clone URL for the target repository.",
"default": "https://github.com/AgentWorkforce/workforce.git"
}
},
"onEvent": "./agent.ts",
"harness": "codex",
"model": "gpt-5.4",
"systemPrompt": "Implement Linear issues with small, reviewable changes and clear handoff notes.",
"harnessSettings": {
"reasoning": "medium",
"timeoutSeconds": 1200,
"sandboxMode": "workspace-write",
"workspaceWriteNetworkAccess": true
}
}
23 changes: 23 additions & 0 deletions examples/review-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Review Agent

This deployable persona listens for GitHub pull request events and Slack mentions, delegates code reasoning to the configured harness, and posts the result back through the connected integration.

## Setup

Connect GitHub and Slack before deploying. Because `useSubscription` is enabled, deployment also connects the model provider derived from the persona's `model` field.

⚠️ **Memory is not wired.** `ctx.memory` is a stub in v1; see `docs/plans/deploy-v1-schema-cascade-spec.md` § Loud hole. Memory wiring lands in a follow-up workflow (not yet specced).

```bash
workforce deploy ./examples/review-agent/persona.json --mode dev
```

## Events

The persona handles opened pull requests, issue comment mentions, pull request review comments, failed check runs, and Slack app mentions.

## Run

```bash
workforce deploy ./examples/review-agent/persona.json --mode sandbox
```
Loading