129: Give dispatched agents a live preview environment instead of only a local checkout#152
Conversation
Babysitter handoff: ready for human reviewPR #152 is cleanly mergeable against current Validation:
The babysitter pass hardened the preview integration for concurrent issue ports, workspace-fenced orphan sweeping, crash-safe pending routes, Funnel rejection, remote reviewer/babysitter handoff, existing Slack threads, and teardown-before-agent-release. Documented tradeoff: Tailscale owns the issue-lifetime route; the dispatched agent owns and must keep the repository dev server running. Happy to discuss that boundary or any of the lifecycle/security choices before approval. |
…force-factory-fbac5605
Factory PR babysitter handoffPR #152 is ready for human review on head
I did not merge. Happy to discuss the deliberate v1 boundary where Tailscale owns the persistent route while the dispatched agent owns and keeps the repository dev server running through the handoff. |
…force-factory-fbac5605
…force-factory-fbac5605
…force-factory-fbac5605
|
Was this feature fully proven end to end? |
…force-factory-fbac5605
…force-factory-fbac5605 # Conflicts: # src/orchestrator/factory.ts
# Conflicts: # .agentworkforce/features/manifest.yaml # src/fleet/internal-fleet-client.test.ts
# Conflicts: # src/orchestrator/factory.test.ts
Summary
Factory's implementer/reviewer/babysitter agents work in a git worktree on a fleet node (
src/node/factory-node.ts,createFactoryNodeDefinition(),AgentSpec.clonePathinsrc/ports/fleet.ts) but there is no concept of exposing whatever the checkout runs (a dev server, a built app) as a reachable URL. A competitor (Amika, gofixpoint/amika) treats this as first-class:.amika/config.toml[services.*]blocks declare ports plus aurl_scheme, and every sandbox gets a generated preview URL (amika service list). Factory has no equivalent — confirmed zero references to port publishing or preview URLs anywhere insrc/(a repo-wide search for preview-url/port-publish/service-port patterns returns nothing). This is table stakes for letting an agent (or a human, or an eventual computer-use verification step) actually see the running result of a change instead of only reading a diff.Proposed approach
Do not build a bespoke sandboxing/port-proxy layer — evaluate and integrate an existing hosted or self-hostable preview-environment provider rather than reimplementing container orchestration and port publishing from scratch. The work here is the integration, not a new sandbox runtime.
A. Provider evaluation (spike)
Compare 2-3 concrete options against Factory's actual requirements: must run inside the same fleet-node model (local broker or hosted relay node), must be reachable long enough to survive an implementer→reviewer/babysitter handoff (a preview instance's lifetime should track the issue's in-flight lifetime, not one agent's process lifetime), and must produce a URL that can be safely posted to a Slack thread and PR comment (i.e., some access-control story, not a bare open port on the internet).
B. Node-level plumbing
Extend
createFactoryNodeDefinition()(src/node/factory-node.ts) so a node can advertise the chosen provider's availability, and extendAgentSpec(src/ports/fleet.ts) with an optional preview reference (URL + expiry/lifetime) that's set once the underlying dev command starts and torn down when the issue reaches a terminal state. Reuse the existing terminal-state plumbing (pr-human-review-terminal/pr-done-terminalinsrc/orchestrator/factory.ts) as the teardown trigger so preview environments don't leak the way.factory-worktreesdid (see issue #123 for exactly that failure mode with worktrees — the same "clean up on terminal state, sweep orphans on daemon startup" pattern applies here).C. Surfacing the URL
Once available, the URL should reach: the task template (
renderAgentTask()insrc/dispatch/templates.ts, via a new optionalpreviewUrlfield onRenderAgentTaskInput, following the same additive pattern as the existingslackDispatchThreadfield), the Slack dispatch thread, and the PR body/description, so a human reviewing the PR can click through without pulling the branch locally.D. Verification hook (depends on the feature/test map, #128/#131)
Once a feature's manifest entry (from #128) says the touched surface is UI-facing and needs a running instance to verify, the task template should instruct the agent to drive its own existing computer-use/browser tool against the preview URL before reporting readiness — Factory does not need to build browser automation itself; Claude Code and Codex already have this capability. Factory only needs to (1) have a URL to hand them and (2) know, from the manifest, when to ask for it. This should be wired in as an addition to #131's task-template changes once a
previewUrlexists on the input, rather than duplicating verify-instruction logic here.Acceptance criteria
AgentSpecand rendered into the task template, the Slack thread, and the PR.factory reap-orphansdoes for stale processes (src/orchestrator/reaper.ts) — do not reusefactory reap-orphansdirectly since it's process-PID-scoped, but mirror its "identity-checked, only touch what we own" guard.Notes for the implementer
npm test(vitest); add tests undersrc/node/andsrc/dispatch/templates.test.ts.Related: #128 (feature/test map this feeds verification decisions from), #131 (task-template consumption this hands the preview URL to)
Fixes #129
Summary by cubic
Adds issue-lifetime live previews via Tailscale Serve. A supervised dev command publishes a tailnet-only URL shown in agent tasks, Slack, and PRs; previews are created before dispatch, pinned to a capable node, and cleaned up on terminal states and by startup/periodic sweeps. Fixes Linear #129.
New Features
previewwith providertailscale-serveand enforcedaccess: tailnet; per-reposervices(port, optionalportSpan/httpsPort,startCommand), optionalregistryPathandtailscaleBinary; workspace+node config is merged and passed through the CLI to the fleet.preview:tailscale-serve; internal and relay fleets implementcreatePreview,removePreview, andreapPreviews, routing actions to the owning node with exact-match teardown; orchestrator creates previews pre-dispatch, selects a capable node, pins the team, posts the URL, rejects non-tailnet outputs, and runs startup and periodic preview sweeps; dispatch results includepreviewsfor consumers.PreviewProcessSupervisorruns and proves the repo’s foreground dev command, tracks identity, and reaps orphans;TailscalePreviewManagermanages Serve routes with a locked file-backed registry, safe HTTPS port allocation, target-port probing, published-ready checks, and preservation of unrelated Serve/Funnel routes; fail-closed.scripts/verify-tailscale-preview-e2e.mjs.Migration
preview.servicesinfactory.node.json(and on the control plane if separate). Start Factory; preview-capable nodes advertisepreview:tailscale-serve. Factory creates a tailnet-only URL per issue and repo.Written for commit 677667b. Summary will update on new commits.