Status: draft for review Owner: workforce Target: shipped today (Phase 1 vertical slice) Depends on: cloud proactive-runtime M1 (assumed shipping today), Daytona creds in workforce cloud env
A workforce persona is a deployable agent. Today a persona JSON describes how to launch a harness in the user's terminal. After this work, the same persona JSON — with a few new fields — also describes a cloud agent that listens for events (GitHub PR opened, Linear issue created, @mention, cron tick), runs inside a Daytona sandbox, can invoke its harness for real work, retains memory, and posts back through Slack/Relaycast/GitHub.
The user-facing command is one line:
workforce deploy ./review-agent.jsonThat command does everything: validates schema, prompts the user through integration OAuth, registers triggers, bundles the agent code, and starts a long-lived runner. The persona JSON is the single source of truth.
This unifies three product surfaces that today live as separate things:
- The local persona/harness story (
workforce agent <id>) - The sage-style addressable assistant (Slack/Relaycast inbox)
- The proactive-agents-style scheduled/event-driven worker
One file. One command. One contract.
Companion docs:
workforce/docs/plans/deploy-v1-workflow-spec.md— Ricky cross-repo execution spec (worktree layout, branch names, PR templates, blocked tracks).workforce/docs/plans/deploy-v1-codex-spec.md— parallel codex implementer tasks (do not duplicate from this plan).
- Persona JSON schema extension:
cloud,useSubscription,integrations,schedules,memory,onEvent. - New package
@agentworkforce/runtime— thin facade exposinghandler(...)that wrapsagent({...})from@agent-relay/agent(cloud proactive-runtime M1 SDK). - New package
@agentworkforce/deploy— the deploy CLI logic; the existingcli.tsgets adeploycase that dispatches to it. - Daytona sandbox launcher used in the
--sandboxrun mode. - Integration connect via
@relayfile/sdk(RelayfileSetup.connectIntegration) and provider connect via@agent-relay/cloud(connectProvider) whenuseSubscription: true. - Run modes:
--dev— long-lived local Node process connecting toagent-gateway(no sandbox).--sandbox— Daytona sandbox runs the bundle; default when Daytona creds resolve.--cloud— POST bundle to a workforce cloud deploy endpoint. Stubbed today (endpoint lands in proactive-runtime M4). The flag exists, prints "not yet available; cloud-hosted deploy lands with M4."
- Two reference examples shipped in
examples/:examples/review-agent/— GitHub PR review + autofixexamples/weekly-digest/— cron, Brave search → GitHub issue
workforce dry-runextension that validates the new fields and lints the integration trigger names.
- Declarative integration routing (
links: [{ from: "github.issue.opened", to: "slack.post" }]). Linking happens inonEventcode for v1. - Multi-tenant cloud-hosted deploy (blocked on cloud proactive-runtime M4).
- Persona schema for "personas as a service" marketplace metadata (pricing, install count, etc.).
- A web UI for managing deployed agents — CLI only for now.
- Migrating sage / sales / nightcto to this contract — they remain as-is; this is greenfield.
All new fields are optional. A persona that does not set any of them continues to behave exactly as today — workforce agent <id> works unchanged. Set cloud: true and at least one trigger to opt into the new deploy surface.
| Field | Type | Required when | Meaning |
|---|---|---|---|
cloud |
boolean |
always (default false) |
When true, this persona is deployable. workforce deploy only operates on personas where this is true. |
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. |
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.
Key choices:
- Key is the Relayfile provider slug.
github,linear,slack,notion,jira. The deploy step callsRelayfileSetup.connectIntegration({ allowedIntegrations: [key] })for any provider not yet connected to the user's workspace. triggers[]is a flat list per provider — multiple events from the same provider all fan into the sameonEvent. The handler discriminates onevent.source+event.type.matchandwhereare sugars —match: "@mention"is shorthand for "filter to events that mention the deployed agent." The deploy CLI lints them against a known set; unknown values warn but don't fail. We can always upgrade the runtime to enforce them later.scopeis optional and provider-specific. Validated by the deploy CLI against a small provider-schema map. For v1, supported keys are documented per provider in the examples.
The act of stacking integrations is just declaring multiple keys. The act of linking them ("when GitHub fires, post to Slack") is code in onEvent. We considered a declarative links: block — see §11.4 for why we deferred it.
"schedules": [
{ "name": "weekly-digest", "cron": "0 9 * * 6", "tz": "UTC" },
{ "name": "stale-prs", "cron": "0 9 * * 1-5", "tz": "America/New_York" }
]nameis required and unique within the persona; it surfaces asevent.nameto disambiguate insideonEvent.cronis a standard 5-field expression.tzdefaults toUTC.- Multiple schedules are allowed. The runtime registers each with
ctx.schedule.every(cron, { tz, payload: { name } }).
"memory": true // sensible defaults
"memory": {
"enabled": true,
"scopes": ["workspace", "user", "global"],
"ttlDays": 30,
"autoPromote": true,
"dedupMs": 300000
}- Implementation: the runtime wires
@agent-assistant/memorywith the supermemory adapter (matching sage today). API key is pulled from workforce-managed env, not declared in the persona. scopesis 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.autoPromoteflips on the sage turn-recorder pattern — agent decides if session content is worth promoting.- No
memoryMdfile. Memory is config, not prose. Personality goes in prompt/sidecar content and the persona-personality-builder flow.
packages/persona-kit/src/triggers.ts (new) ships a small registry of known trigger names per provider so the deploy CLI can lint them:
export const KNOWN_TRIGGERS = {
github: ["pull_request.opened", "pull_request.synchronize",
"issue_comment.created", "pull_request_review_comment.created",
"check_run.completed", "workflow_run.completed", "issues.opened"],
linear: ["issue.created", "issue.updated", "comment.created"],
slack: ["app_mention", "message.channels"],
// ...
} as const;Unknown trigger names log a yellow warning but don't fail deploy. The cloud runtime is the source of truth; we don't want to be a gating bottleneck.
Existing persona.inputs remain the declaration point for non-secret runtime values. workforce deploy supplies deploy-time overrides with repeatable --input KEY=value flags; the CLI rejects keys that the persona did not declare and requires each value to be a string. In --mode dev and --mode sandbox, accepted values are injected into the runner environment as WORKFORCE_INPUT_<KEY>. In --mode cloud, the same map is sent in the deployment POST body as inputs.
A new, intentionally thin package. Single export: handler(...).
// @agentworkforce/runtime
import { agent } from '@agent-relay/agent'; // PR #515 M1
type WorkforceEvent =
| { source: 'cron'; name: string; firedAt: string }
| { source: 'github'; type: GithubTrigger; ...payload }
| { source: 'linear'; type: LinearTrigger; ...payload }
| { source: 'slack'; type: SlackTrigger; ...payload };
interface WorkforceCtx {
// Inference, either workforce-billed or via the user's subscription
llm: { complete(prompt: string, opts?: LlmOpts): Promise<string> };
// Spawn the persona's declared harness inside the sandbox
harness: {
run(args: { prompt: string; cwd?: string; tier?: 'best'|'best-value'|'minimum' }): Promise<HarnessResult>;
};
// Per-integration auth-wrapped clients (only those declared in persona.integrations)
github?: GithubClient;
linear?: LinearClient;
slack?: SlackClient;
notion?: NotionClient;
jira?: JiraClient;
// 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>;
readFile(path: string): Promise<string>;
writeFile(path: string, contents: string): Promise<void>;
};
// Memory (agent-assistant memory, wired per persona.memory)
memory: {
save(content: string, opts?: { tags?: string[]; scope?: MemoryScope }): Promise<void>;
recall(query: string, opts?: { limit?: number }): Promise<MemoryItem[]>;
};
// Workflow invocation — the persona-as-orchestrator escape hatch (§6)
workflow: {
run(name: string, args: Record<string, unknown>): Promise<WorkflowRunHandle>;
};
// Schedule control — for handlers that want to schedule one-off followups
schedule: {
at(when: Date, payload: unknown): Promise<void>;
cancel(name: string): Promise<void>;
};
// Persona metadata (id, harness defaults, listeners, etc.) — read-only
persona: PersonaSpec;
}
export function handler<I extends IntegrationKeys>(
fn: (ctx: WorkforceCtx & { [k in I]: NonNullable<WorkforceCtx[k]> }, event: WorkforceEvent) => Promise<void>
): WorkforceHandler;Implementation notes:
handler(...)reads the persona JSON adjacent to the entrypoint (workforce bundles them together). At cold-start it:- Calls
agent({ workspace, schedule, watch, inbox, onEvent: shim })from@agent-relay/agent, mappingpersona.integrationstowatchandpersona.schedulestoschedule. - Builds
ctxonce per agent boot: opens Daytona handle when deploy runs in sandbox mode, wires Relayfile-derived clients, attaches memory adapter. - The
shimreshapes the raw envelope from@agent-relay/agentinto theWorkforceEventdiscriminated union and invokes the user'sfn(ctx, event).
- Calls
- The user never imports
@agent-relay/agentdirectly. 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
agentfrom@agentworkforce/runtime/rawso anyone who wants the lower-level surface can drop down. This matters for nightcto-shaped projects that outgrow the persona contract.
The runtime package has zero runtime dependencies on the CLI. It can be installed standalone in any Node project to write an agent without workforce deploy. That keeps the personas-as-code escape hatch clean.
New package. Exports deploy(persona, opts): Promise<DeployResult>. The existing packages/cli/src/cli.ts adds:
case 'deploy': await runDeploy(argv); break;
case 'login': await runLogin(argv); break; // small new helper for cloud authrunDeploy is a ~150-line orchestrator over the deploy package. Public flags:
workforce deploy <persona-path>
[--mode dev|sandbox|cloud] # default: sandbox if Daytona creds present, else dev
[--workspace <name>] # workforce workspace; defaults to active
[--no-connect] # skip integration prompts; fail if any are missing
[--detach] # background the runner
[--bundle-out <dir>] # emit bundle without launching
[--dry-run] # validate only
[--input <key>=<value>] # override declared persona input (repeatable)Flow:
- Resolve persona: load the JSON via
parsePersonaSpec(extended schema). Fail fast on schema errors with field-pointed messages. Deploy-time persona input overrides come from repeated--input KEY=valueflags. Each key must be declared bypersona.inputs; values are non-secret strings passed through to the runner asWORKFORCE_INPUT_<KEY>and included in cloud deployment requests. - Login check: if no workforce auth token in keychain, prompt
workforce login(browser OAuth via existing relayauth flow). - Workspace check: ensure user has a workspace; offer to create one (
relay workspaces create <name>semantics, called via SDK not subprocess). - Integrations: for each
persona.integrationskey, check if connected to the active workspace. If not, prompt the user before each (Connect github now? (Y/n)). On yes, callRelayfileSetup.connectIntegration({ allowedIntegrations: [key] })and open the browser. Block until callback. On no, fail with a clear message. - Subscription (if
useSubscription: true): callconnectProvider({ provider: <persona.tiers.best.harness derived> })from@agent-relay/cloud. Pick provider from the persona's primary tier harness (claude → anthropic, codex → openai, opencode → user choice). - Schedules: register each
persona.schedules[i]with the runtime — for--devand--sandbox, schedules are registered via the runtime SDK at boot; for--cloud(when M4 lands), they're part of the bundle metadata. - Bundle: stage to
.workforce/build/<persona-id>/:persona.json(the spec)agent.ts(the user'sonEventfile, possibly transpiled)runner.ts(generated; callshandler(...)and starts the runtime)package.json(with@agentworkforce/runtimeand any user-declared deps)
- Launch:
--dev:node .workforce/build/<id>/runner.tsin the foreground (or detached).--sandbox: spin up Daytona sandbox, upload the bundle,daytona.exec("node runner.ts"). Stream logs back to stdout.--cloud: print "not yet available; cloud-hosted deploy lands with proactive-runtime M4." Bundle is left in.workforce/build/for inspection.
- Print status: agent ID, workspace, integrations connected, schedules registered, runner mode, log tail command.
--bundle-out <dir> writes the bundle and exits. Useful for CI and for the future --cloud mode.
--dry-run validates schema + lints triggers + checks integration connection status, no side effects.
The user explicitly asked: "within the harness definition can we call for a workflow to be run?"
Two levels of integration:
When onEvent calls ctx.harness.run({ prompt: "Review this diff", cwd: ctx.sandbox.cwd }), the runtime spawns the persona's declared tier (claude/codex/opencode) inside the sandbox, with the sandbox cwd as the harness's working directory. The harness has:
- Filesystem access to the sandbox's mounted workspace.
- Network access per the persona's
harnessSettings(workspaceWriteNetworkAccess). - The persona's declared
skills,mcpServers, andpermissionsmaterialized as the harness expects. - Optionally, an MCP server we ship —
mcp__workforce— exposingworkflow.run,memory.save,memory.recall, and the per-integration clients. The harness can call these as tools mid-run, without re-architecting around the workforce SDK.
That's how a "review PR" handler can let Claude Code (or Codex) drive the entire review autonomously: the handler hands the harness the diff, the harness reads files, runs tests in the sandbox, drafts comments, and returns. The handler then posts the comments via ctx.github.comment.
ctx.workflow.run("name", args) is the escape hatch for the heavy machinery in cloud/workflows. Inside a handler — or from within the harness via the mcp__workforce MCP — you can kick off a multi-step workflow. The workflow runs in cloud (its native habitat) and returns a handle; the handler can await handle.completion() or fire-and-forget.
Examples:
review-agentinvokes apr-review-multi-tierworkflow that runs three independent reviewers and synthesizes.My-Senior-Dev-shaped agents invokecode-explore+propose-edits+verifyworkflows in sequence.
For v1 the workflow client is a thin HTTP wrapper around the cloud workflows endpoint. Authentication piggybacks on the workspace token already loaded for agent-gateway.
This is the bridge: personas declare the integration surface and the handler; handlers orchestrate; workflows execute. None of these need to know about the others' internals.
Direct port of the proactive-agents weekly-digest pattern.
persona.json:
{
"id": "weekly-digest",
"intent": "research",
"tags": ["analytics"],
"description": "Weekly competitive intel digest — searches the web and Reddit for mentions, dedupes, posts a single GitHub issue.",
"cloud": true,
"integrations": { "github": { "scope": { "repo": "AgentWorkforce/weekly-digest" } } },
"schedules": [{ "name": "weekly", "cron": "0 9 * * 6", "tz": "UTC" }],
"memory": { "enabled": true, "scopes": ["workspace"], "ttlDays": 90 },
"onEvent": "./agent.ts",
"tiers": { ... standard codex/opencode tiers ... }
}agent.ts: ~80 lines. Brave search → cluster → upsert GitHub issue.
persona.json:
{
"id": "review-agent",
"intent": "review",
"tags": ["review"],
"description": "Reviews opened PRs, responds to @mentions in comments, attempts autofix on red CI.",
"cloud": true,
"useSubscription": true,
"integrations": {
"github": {
"triggers": [
{ "on": "pull_request.opened" },
{ "on": "issue_comment.created", "match": "@mention" },
{ "on": "pull_request_review_comment.created" },
{ "on": "check_run.completed", "where": "conclusion=failure" }
]
},
"slack": { "triggers": [{ "on": "app_mention" }] }
},
"memory": { "enabled": true, "scopes": ["user", "workspace"] },
"onEvent": "./agent.ts",
"tiers": { ... }
}agent.ts: ~120 lines. Dispatches on event.type:
pull_request.opened→ctx.harness.run({ prompt: "Review", cwd })→ post reviewissue_comment.created+@mention→ harness with thread context → replycheck_run.completed+ failure → harness with logs → propose fix patchslack.app_mention→ conversational reply using memory
workforce/
├── packages/
│ ├── cli/ # add `deploy`, `login` cases
│ ├── persona-kit/ # extend PersonaSpec schema (§3)
│ │ └── src/
│ │ ├── 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
│ ├── workload-router/ # no changes for v1
│ ├── deploy/ # NEW — @agentworkforce/deploy
│ │ └── src/
│ │ ├── index.ts # deploy(persona, opts) entry
│ │ ├── login.ts # workspace login helper
│ │ ├── connect.ts # integration + provider connect orchestration
│ │ ├── bundle.ts # stage bundle to .workforce/build/<id>
│ │ ├── modes/
│ │ │ ├── dev.ts # local long-lived process
│ │ │ ├── sandbox.ts # Daytona launch
│ │ │ └── cloud.ts # M4-stub
│ │ └── daytona.ts # thin Daytona wrapper (or import from cloud)
│ └── runtime/ # NEW — @agentworkforce/runtime
│ └── src/
│ ├── index.ts # exports handler(), types
│ ├── ctx.ts # builds WorkforceCtx per invocation
│ ├── clients/ # per-integration auth-wrapped clients
│ │ ├── github.ts
│ │ ├── linear.ts
│ │ ├── slack.ts
│ │ └── ...
│ ├── memory.ts # wraps @agent-assistant/memory
│ ├── workflow.ts # cloud workflows HTTP client
│ ├── shim.ts # @agent-relay/agent envelope → WorkforceEvent
│ └── raw.ts # re-exports for power users
├── examples/
│ ├── weekly-digest/ # NEW
│ │ ├── persona.json
│ │ └── agent.ts
│ └── review-agent/ # NEW
│ ├── persona.json
│ └── agent.ts
└── docs/
└── plans/
└── deploy-v1.md # this file
Persona schema diffs are non-breaking. Existing personas in personas/persona-maker.json etc. continue to work unchanged.
You asked for an explicit multi-faceted split. Here's the breakdown.
The work that needs codebase fluency, schema decisions, and inline iteration:
- Schema diff in
persona-kit—types.ts,parse.ts,triggers.ts. Includes unit tests for the new field shapes. @agentworkforce/runtimeskeleton —index.ts,ctx.ts,shim.ts, types. Stubs for clients/memory/workflow that compile and return typed placeholders. Actual client implementations slotted in via §9.2 / §9.3.@agentworkforce/deployskeleton —index.ts,bundle.ts,modes/dev.ts(the simplest run mode). Login + connect orchestration with--no-connectfallback for testing.- CLI wiring —
deployandlogincases incli.ts, plus--dry-runand--bundle-outflags. examples/weekly-digest/— fully working against the--devrunner. End-to-end demo path.examples/review-agent/— persona JSON + agent.ts skeleton; full behavior depends on per-integration clients (see §9.2).- Docs — extend
README.mdwith a## Deploying agentssection pointing at the examples.
Estimate: aggressive 4–6 hours given existing surface area.
Full execution detail lives in deploy-v1-workflow-spec.md. This plan owns the what; the workflow spec owns the how.
Ready-now tracks (one PR per track, all in one Ricky workflow run):
- Track A — extract
@workforce/daytona-runner+ addPOST /api/v1/workspaces/:id/sandboxesin$CLOUD_REPO. - Track B — workforce consumes
@workforce/daytona-runner+ workforce-managed sandbox auth path. - Track C —
@agentworkforce/mcp-workforceMCP server (workflow/memory/integration tools). - Track INT — cross-repo E2E (
weekly-digest --mode dev,review-agent --mode sandbox, optionallinear-shipper).
Blocked tracks (separate workflow files when unblocked):
- Track CLOUD —
--cloudmode wiring (blocked on cloud proactive-runtime M4). - Track BILL — billing meter for workforce-managed sandboxes (post-v1).
- Track DOCS — documentation site updates (after codex Tasks 6/7/9 + human schema-diff merge).
If a track slips, §10's fallback applies: ship --dev end-to-end with weekly-digest; review-agent becomes next-milestone.
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:
- Trigger registry expansion — fill out
packages/persona-kit/src/triggers.tswith 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/. - Test fixtures — generate sample
persona.jsonfiles exercising deploy optional combinations (memory, multi-schedule, integrations, etc.) intopackages/persona-kit/src/__fixtures__/. - JSON Schema export — emit a JSON Schema from the extended
PersonaSpecfor editor autocomplete. New script:packages/persona-kit/scripts/emit-schema.mjs. Wire topnpm run buildso it ships with the package. - 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. - README polish — once the deploy command is real, codex agent rewrites the workforce README to lead with the deploy story.
Each item is a self-contained PR for codex to handle in parallel with my main thread.
| When | Milestone | Owner | Gates |
|---|---|---|---|
| T+0 | Plan reviewed + signed off | user | this doc |
| T+30m | Persona schema diff merged-to-branch | me | typechecks green, fixtures pass |
| T+1h | @agentworkforce/runtime skeleton compiles |
me | runtime imports + handler() types check |
| T+2h | @agentworkforce/deploy --dev end-to-end |
me | workforce deploy examples/weekly-digest/persona.json --mode dev runs and logs cron tick |
| T+2.5h | examples/weekly-digest posts to a real test GitHub repo |
me | demo-ready |
| T+3h | --sandbox Daytona mode lights up |
me + workflow | depends on Daytona-runner-package PR landing |
| T+4h | review-agent example end-to-end against a test PR |
me + workflow | depends on per-integration clients |
| T+5h | Codex-agent tasks merged | codex agent | parallel tracks |
| EOD | PR opened, draft for review | me | final pass, screenshots, README |
If §9.2 (Daytona-runner-package + per-integration clients) slips, we ship --dev only and demo weekly-digest end-to-end. review-agent becomes "next milestone" — but the persona contract still ships.
Plan assumes @agent-relay/agent (M1's SDK) is importable today. If M1 is in flight but not yet published, the runtime substrate falls back to a temporary shim that talks to agent-gateway over a hand-rolled WebSocket using the M1 spec's envelope shape. This is a half-day of extra work and an explicit tech-debt note. Verify before T+0.
Two paths, both ship in v1:
- BYO: if
DAYTONA_API_KEYis set in the user's env, the CLI uses it directly. Zero cloud dependency. Useful for power users / CI. - Workforce-managed: if not set, the CLI calls
POST /api/v1/workspaces/:id/sandboxesagainst the cloud API with the user's workspace token. Cloud holds the org-levelDAYTONA_API_KEY(already exists as an SST secret atcloud/infra/secrets.ts:23), callsdaytona.create(), returns{ sandboxId, jwtToken, organizationId, expiresAt }. The CLI then constructs a Daytona client with{ jwtToken, organizationId }— auth path the SDK already supports (seecloud/packages/core/src/auth/credentials.ts).
This used to be deferred to a follow-up; it isn't. The endpoint is ~30 lines on the cloud side, it reuses primitives that already exist, and it removes the need to document "you also need a Daytona account" in the v1 quickstart. Moved into Ricky's M1 (Milestone 1) so it ships alongside the Daytona runner extract.
Long-lived process lifecycle (verified): DaytonaRuntime does not auto-destroy. Cloud's executor calls destroy() explicitly at step end (cloud/packages/core/src/executor/executor.ts:1029). Workforce deploy simply never calls destroy() — the sandbox persists until the user runs workforce deployments destroy <id> (Milestone 3) or we add an idle-timeout sweeper.
Restart story (open): if the runner process inside the sandbox crashes, the OS process dies but the sandbox itself remains. A small supervisor loop inside runner.mjs (while (true) { await runAgent().catch(log) }) covers transient crashes. If the entire sandbox dies (rare; Daytona-side incident), the CLI's --detach mode is fire-and-forget — we'd need a "workforce deployments tail" command later to detect death and re-spin. Acceptable v1 gap.
Endpoint contract, audit logging, and JWT-vs-proxy fallback are pinned in deploy-v1-workflow-spec.md Track A.
The agent.ts the user authors needs to run in the Daytona sandbox. Three options for the bundler:
- esbuild — fast, minimal config. Output a single-file CJS bundle the sandbox's
node runner.jscan execute. - tsx at runtime — install tsx in the sandbox, run
tsx runner.tsdirectly. No bundle step. - No transpile — require user
agent.tsto be pre-built (pnpm tscin the persona dir).
Default to esbuild. It's a one-import dependency and gives us deterministic output.
The user's notes had:
issue opened in github, synced to linear — deterministic issue opened in github, slack message sent — deterministic OR agent reviews and summarizes
We considered:
"links": [{ "from": "github.issue.opened", "to": "linear.create", "template": "..." }]Decision: defer. Every "deterministic" route in the wild has filters, conditions, retry semantics, or templated payloads — all of which pull it back toward code. Three lines in onEvent is clearer than a config-language with its own escape hatches:
if (event.source === 'github' && event.type === 'issues.opened') {
await ctx.linear.create({ title: event.issue.title, body: event.issue.body });
await ctx.slack.post('#triage', `Issue ${event.issue.url}`);
}If we see N personas repeating the same routing skeleton, we lift it then. Premature abstraction here would lock us into a config shape we'd want to evolve.
Right now, one persona = one deployable. A user with three agents runs workforce deploy three times. That's fine for v1. We may want a workforce.config.json listing deployables later, but the implementation should treat that as sugar over the single-persona path.
When the user iterates on agent.ts, the --dev mode runs the handler in their local process, fast. The --sandbox mode pushes to Daytona each restart, slow. We need a --sandbox --watch mode that rsync-mirrors local changes into the live sandbox. Stretch goal for today; trivial follow-up if it slips.
When --cloud mode lights up post-M4, the bundle uploaded to cloud must not contain plaintext provider tokens. The Relayfile connection token model already handles this (workspace holds the connection, agents request scoped tokens at runtime). Confirm with the cloud team that M4's accept-bundle endpoint takes a persona JSON + bundled JS only — no secrets baked in.
- Polyglot handlers (
agent.py, etc.): the user's notes mentionedetc.py. Phase 2. The runtime's SDK is TS-first; a Python adapter is a substantial extra package. - GUI / dashboard for deployed agents: cloud-side surface, not workforce CLI.
- Persona marketplace metadata: pricing, install counts, ratings. Belongs in a future
marketplacepackage. - Migrating sage / sales / nightcto onto this contract: they stay as-is. The new substrate proves itself on greenfield agents first.
- Cross-persona communication (
agent.send-to(other)): relaycast already does this between agents that opt in. The persona JSON doesn't need a new field;ctx.slack.post(...)to a workspace channel works today.
A user with:
- A fresh workforce install
- A clean
examples/weekly-digest/persona.jsonandagent.ts - Their GitHub workspace connected through
relayfile
Can run:
workforce login
workforce deploy ./examples/weekly-digest/persona.json --mode devAnd within 60 seconds see:
- A "Connect github? (Y/n)" prompt if not connected
- "Workspace = my-workspace, persona = weekly-digest, sandbox = on, mode = dev"
- A long-lived process printing
[runtime] cron schedule "weekly" registered - On forcing a
cron.tick, the handler runs and posts a GitHub issue
For --mode sandbox, the same flow with logs streaming from a Daytona sandbox.
That's the shippable v1.