v3 workflow: intent, phases, review, submission, enforcement, burn-in

The v3 stack, previously an uncommitted working tree, plus this session's two
units and the burn-in instrument. This commit is the burn-in build identity:
coordinator and worker must both report this revision before a task is created.

Workflow (earlier sessions, uncommitted until now): human decision events and
reduction, source cursors and reconcile-before-launch, turn-boundary
reconciliation, internal/agentctx as the single renderer, ace-fca phases with
sealed artifacts, the trajectory gate, bounded grilling, independent review,
task pr enforcement, and human review reflection.

Capability restrictions at the agent boundary: an authz.Agent surface at
GatedWrite may ask and may not act. It also fixes two bugs the unit exposed --
gated surfaces could not reach the two endpoints written for them, and
RequestHumanDecision would block an unowned task while rejecting a question
from the session that did own it.

Turn-boundary reconcile-failure escalation: a streak of consecutive failures
asks the session to hand off, fenced on the lease epoch, with reconcile_failure
as a real handoff reason. The worker was dropping the coordinator's verdict on
the floor; it now acts on it.

Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to
<worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md
is the runbook. deploy/build.sh stamps both binaries from one commit.

go build, go vet and go test ./... pass, 20 packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 18:31:20 +04:00
parent 97a9c65302
commit 7f12c7fc37
78 changed files with 16417 additions and 352 deletions
+371 -11
View File
@@ -17,10 +17,12 @@ import (
"orchestra/internal/domain"
"orchestra/internal/federation"
"orchestra/internal/herdr"
"orchestra/internal/human"
"orchestra/internal/operations"
"orchestra/internal/orchestrator"
"orchestra/internal/provider"
"orchestra/internal/registry"
"orchestra/internal/review"
"orchestra/internal/router"
"orchestra/internal/store"
"orchestra/internal/ui"
@@ -37,6 +39,9 @@ func id() string { return domain.NewID() }
const defaultHerdrPort = "9245"
func herdrAddress(rr registry.Registry, h registry.Herdr) string {
if h.Backend == "tmux" {
return rr.Endpoint(h)
}
if h.Address != "" {
return h.Address
}
@@ -57,9 +62,9 @@ type federatedAvailability struct {
localMachine string
}
// federatedReachability keeps the legacy TCP probe for local herdrs, while
// avoiding a coordinator-side probe of a remote worker's herdr socket. A
// remote harness is reachable precisely when its worker is registered (as
// federatedReachability keeps the legacy TCP probe for coordinator-owned
// herdrs, while avoiding a coordinator-side probe of a worker-owned backend.
// A worker-owned harness is reachable precisely when its worker is registered (as
// enforced by federatedAvailability); probing its raw herdr endpoint here
// would reintroduce the cross-machine Design A dependency.
type federatedReachability struct {
@@ -77,7 +82,7 @@ func (r federatedReachability) Reachable(address string, timeout time.Duration)
func remoteHerdrAddresses(rr registry.Registry, localMachine string) map[string]bool {
remote := map[string]bool{}
for _, h := range rr.Herdrs() {
if h.MachineID != localMachine {
if !coordinatorOwnsHerdr(h, localMachine) {
remote[herdrAddress(rr, h)] = true
}
}
@@ -89,6 +94,9 @@ func remoteHerdrAddresses(rr registry.Registry, localMachine string) map[string]
// reaching into that machine would turn a worker-owned health signal back
// into a misleading coordinator TCP result.
func coordinatorOwnsHerdr(h registry.Herdr, localMachine string) bool {
if h.Backend == "tmux" {
return false
}
return localMachine == "" || h.MachineID == localMachine
}
@@ -96,7 +104,7 @@ func (a federatedAvailability) Available(h registry.Herdr) bool {
if a.base != nil && !a.base.Available(h) {
return false
}
if a.localMachine == "" || h.MachineID == a.localMachine {
if coordinatorOwnsHerdr(h, a.localMachine) {
return true
}
return a.workers.Available(h.ID)
@@ -106,7 +114,7 @@ func (a federatedAvailability) Supports(h registry.Herdr, project string) bool {
// Locally-owned herdrs keep their static registry/project affinity. A
// remote worker must additionally prove it has a local checkout for the
// project before the router can offer it a lease.
if a.localMachine == "" || h.MachineID == a.localMachine {
if coordinatorOwnsHerdr(h, a.localMachine) {
return true
}
return a.workers.Supports(h.ID, project)
@@ -114,11 +122,18 @@ func (a federatedAvailability) Supports(h registry.Herdr, project string) bool {
func validateLocalMachine(rr registry.Registry, localMachine string) error {
machines := rr.Machines()
if len(machines) <= 1 {
requiresWorkerOwnership := false
for _, h := range rr.Herdrs() {
if h.Backend == "tmux" {
requiresWorkerOwnership = true
break
}
}
if len(machines) <= 1 && !requiresWorkerOwnership {
return nil
}
if localMachine == "" {
return fmt.Errorf("ORCHESTRA_MACHINE_ID is required for a multi-machine registry; refusing unsafe remote-herdr coordination")
return fmt.Errorf("ORCHESTRA_MACHINE_ID is required when the registry is multi-machine or has worker-owned backends")
}
if _, ok := rr.Machine(localMachine); !ok {
return fmt.Errorf("ORCHESTRA_MACHINE_ID %q is not in the registry", localMachine)
@@ -138,6 +153,15 @@ func main() {
var rr registry.Registry
var rt *router.Router
var coordinator *orchestrator.Coordinator
// submissionPublisher is nil until a forge is configured. Submission then
// returns the verified plan instead of performing it, which keeps `task
// pr` the only path without inventing a fake success.
var submissionPublisher func(operations.SubmissionPlan) operations.Publisher
// Worktree root per project, so a submission can find the checkout that
// holds the commit it is publishing.
projectRoots := map[string]string{}
// Pull-request readers by source name, for reflecting submitted work.
pullRequests := map[string]human.PullRequestSource{}
localMachine := os.Getenv("ORCHESTRA_MACHINE_ID")
workers := &federation.Registry{AdmitToken: os.Getenv("ORCHESTRA_FEDERATION_ADMIT_TOKEN"), StatePath: filepath.Join(dir, "federation-state.json")}
if err := workers.Load(); err != nil {
@@ -208,6 +232,7 @@ func main() {
for _, p := range rr.Projects() {
if p.Repo != "" && p.WorktreeRoot != "" {
projectRepos[p.ID] = orchestrator.ProjectRepo{Repo: p.Repo, WorktreeRoot: p.WorktreeRoot}
projectRoots[p.ID] = p.WorktreeRoot
}
}
worktrees := orchestrator.PerProjectGitWorktrees{
@@ -219,15 +244,15 @@ func main() {
return ok && coordinatorOwnsHerdr(h, localMachine)
}}
rt.OnLease = func(e domain.Event) error {
// In federated mode the coordinator must never inspect a remote
// In federated mode the coordinator must never inspect a worker-owned
// checkout. Its worker consumes the router-issued lease event and
// performs all Git/herdr operations on that machine (§2.1).
// performs all Git/backend operations on that machine (§2.1).
if localMachine != "" {
var p struct {
HarnessID string `json:"harness_id"`
}
if json.Unmarshal(e.Payload, &p) == nil {
if h, ok := rr.Herdr(p.HarnessID); ok && h.MachineID != localMachine {
if h, ok := rr.Herdr(p.HarnessID); ok && !coordinatorOwnsHerdr(h, localMachine) {
return nil
}
}
@@ -628,6 +653,18 @@ func main() {
mux.HandleFunc("/v1/admin/diagnostics", adminServer.Diagnostics)
mux.HandleFunc("/v1/tasks/", func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if r.Method == http.MethodGet && len(parts) == 4 && parts[3] == "intent" {
// The reduced authority for one task. Federation workers read this
// and render their own launch instruction with agentctx, so there
// is one renderer in the codebase rather than one per machine.
intent, err := s.EffectiveIntent(parts[2])
if err != nil {
http.Error(w, err.Error(), 404)
return
}
json.NewEncoder(w).Encode(intent)
return
}
if r.Method == http.MethodGet && coordinator != nil && len(parts) == 4 {
taskID, view := parts[2], parts[3]
if view == "health" {
@@ -653,6 +690,199 @@ func main() {
return
}
taskID, action := parts[2], parts[3]
if (action == "decision-request" || action == "deferred") && len(parts) == 4 {
// An agent surface may state a bounded question or a deferred
// finding. Neither moves the lifecycle: Orchestra decides what a
// question does to the task.
if err := authz.AuthorizeEvent(surface(r), "ApprovalRequested"); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
t, ok := s.Task(taskID)
if !ok {
http.Error(w, "task not found", 404)
return
}
project, _ := rr.Project(t.Project)
r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
if action == "deferred" {
var f domain.DeferredFinding
if json.NewDecoder(r.Body).Decode(&f) != nil {
http.Error(w, "invalid deferred finding", http.StatusBadRequest)
return
}
e, err := operations.RecordDeferredFinding(s, taskID, f)
if err != nil {
http.Error(w, err.Error(), 409)
return
}
json.NewEncoder(w).Encode(e)
return
}
var req domain.DecisionRequest
if json.NewDecoder(r.Body).Decode(&req) != nil {
http.Error(w, "invalid decision request", http.StatusBadRequest)
return
}
e, err := operations.RequestHumanDecision(s, project, taskID, req)
if errors.Is(err, operations.ErrDecisionBudgetSpent) {
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{"status": "operator_required", "detail": err.Error()})
return
}
if err != nil {
http.Error(w, err.Error(), 409)
return
}
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(e)
return
}
if action == "submission" && len(parts) == 4 {
// The only path from a reviewed implementation to the human. It
// verifies first and returns the plan, or performs the submission
// when the caller supplies a publisher-backed request.
if err := authz.AuthorizeEvent(surface(r), domain.EventTaskSubmitted); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
t, ok := s.Task(taskID)
if !ok {
http.Error(w, "task not found", 404)
return
}
project, ok := rr.Project(t.Project)
if !ok {
http.Error(w, "unknown project "+t.Project, 409)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var body struct {
HeadSHA string `json:"head_sha"`
Gate domain.GateResult `json:"gate"`
Notes operations.Notes `json:"notes"`
DryRun bool `json:"dry_run"`
}
if json.NewDecoder(r.Body).Decode(&body) != nil || body.HeadSHA == "" {
http.Error(w, "head_sha and gate are required", http.StatusBadRequest)
return
}
check := domain.CheckSubmission(t, body.HeadSHA, body.Gate)
check.Reasons = append(check.Reasons, t.RequirePhaseArtifacts(project.Phases())...)
check.Eligible = len(check.Reasons) == 0
if body.DryRun || !check.Eligible {
code := http.StatusOK
if !check.Eligible {
code = http.StatusConflict
}
w.WriteHeader(code)
json.NewEncoder(w).Encode(check)
return
}
plan, err := operations.PrepareSubmission(s, project, taskID, body.HeadSHA, body.Gate, body.Notes)
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if submissionPublisher == nil {
// No forge configured: hand back the verified plan so an
// operator can perform the submission themselves.
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]any{"status": "no_publisher", "plan": plan})
return
}
e, err := operations.ExecuteSubmission(r.Context(), s, plan, submissionPublisher(plan), nil)
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
json.NewEncoder(w).Encode(e)
return
}
if action == "review" && len(parts) == 4 {
// Entering review and sealing a review are both Orchestra's, not
// the reviewing session's. The session only supplies findings.
if err := authz.AuthorizeEvent(surface(r), domain.EventReviewRecorded); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
t, ok := s.Task(taskID)
if !ok {
http.Error(w, "task not found", 404)
return
}
project, ok := rr.Project(t.Project)
if !ok {
http.Error(w, "unknown project "+t.Project, 409)
return
}
r.Body = http.MaxBytesReader(w, r.Body, int64(review.MaxDiffBytes)+(1<<20))
var body struct {
Evidence *review.Evidence `json:"evidence"`
Result *review.Result `json:"result"`
}
if json.NewDecoder(r.Body).Decode(&body) != nil || (body.Evidence == nil) == (body.Result == nil) {
http.Error(w, "send either evidence (to enter review) or result (to seal one)", http.StatusBadRequest)
return
}
var e domain.Event
var err error
if body.Evidence != nil {
e, err = operations.EnterReview(s, project, taskID, *body.Evidence)
} else {
e, err = operations.RecordReview(s, project, taskID, *body.Result)
}
if errors.Is(err, operations.ErrReviewNotEligible) || errors.Is(err, domain.ErrInvalid) {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if err != nil {
http.Error(w, err.Error(), 409)
return
}
json.NewEncoder(w).Encode(e)
return
}
if action == "phase" && len(parts) == 4 {
// Orchestra owns the phase. An agent asks for a change through the
// approval surface; this endpoint is how the decision is applied.
if err := authz.AuthorizeEvent(surface(r), domain.EventWorkPhaseChanged); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
t, ok := s.Task(taskID)
if !ok {
http.Error(w, "task not found", 404)
return
}
project, ok := rr.Project(t.Project)
if !ok {
http.Error(w, "unknown project "+t.Project, 409)
return
}
// The body, when present, is the artifact the finished phase
// produced. Bounded like every other artifact upload.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
artifact, readErr := io.ReadAll(r.Body)
if readErr != nil {
http.Error(w, "artifact unreadable", http.StatusRequestEntityTooLarge)
return
}
e, err := operations.AdvanceWorkPhase(s, project, taskID, artifact)
if errors.Is(err, operations.ErrTrajectoryGate) {
// Not a failure: the task is blocked on the human, and the
// packet is on the block event the notification surfaces read.
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{"status": "trajectory_gate", "detail": err.Error()})
return
}
if err != nil {
http.Error(w, err.Error(), 409)
return
}
json.NewEncoder(w).Encode(e)
return
}
if action == "approval" {
if err := authz.AuthorizeEvent(surface(r), map[bool]string{true: "ApprovalRequested", false: "ApprovalGranted"}[len(parts) == 4]); err != nil && len(parts) == 4 {
http.Error(w, err.Error(), http.StatusForbidden)
@@ -791,6 +1021,12 @@ func main() {
// past their lease TTL). Calling ExpireLeases here too would
// just resurrect that race, so leave reclaim to the
// coordinator and only keep retrying pending assignment.
// A blocked task is invisible to the router, so answered
// blockers are returned to the queue here, immediately
// upstream of assignment.
if _, err := operations.ResumeAnsweredBlockers(s); err != nil {
log.Printf("resume answered blockers: %v", err)
}
if coordinator != nil {
if _, err := rt.AssignPending(); err != nil {
log.Printf("route expired task: %v", err)
@@ -805,6 +1041,45 @@ func main() {
}
}()
}
if len(pullRequests) > 0 {
// Submitted work is reconciled on its own loop, not behind
// Store.PreLease: an in-review task cannot be leased, so a pre-lease
// hook could never see the feedback that should make it leasable.
trust := human.Trust{
Accepted: splitList(os.Getenv("ORCHESTRA_REVIEW_ACTORS")),
Ignored: splitList(os.Getenv("ORCHESTRA_REVIEW_IGNORE_ACTORS")),
}
go func() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
for _, t := range s.Tasks() {
if t.Submission == nil || (t.State != domain.StateInReview && t.State != domain.StateQueued) {
continue
}
source, ok := pullRequests[t.Submission.PR.Provider]
if !ok {
continue
}
project, ok := rr.Project(t.Project)
if !ok {
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
state, err := source.PullRequest(ctx, t)
cancel()
if err != nil {
// Observable, and the task stays exactly where it was.
log.Printf("reflect submission %s: %v", t.ID, err)
continue
}
if _, err := operations.ReflectSubmission(s, project, t.ID, state, trust); err != nil {
log.Printf("reflect submission %s: %v", t.ID, err)
}
}
}
}()
}
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok\n")) })
mux.HandleFunc("/readyz", adminServer.Readiness)
mux.HandleFunc("/v1/providers/health", func(w http.ResponseWriter, r *http.Request) {
@@ -869,6 +1144,42 @@ func main() {
}
return wid, nil
}
mux.HandleFunc("/v1/federation/turn", func(w http.ResponseWriter, r *http.Request) {
if _, err := workerAuth(r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
if coordinator == nil {
http.Error(w, "coordinator not configured", http.StatusServiceUnavailable)
return
}
var body struct {
TaskID string `json:"task_id"`
LeaseEpoch string `json:"lease_epoch"`
Verdict string `json:"verdict"`
Delivered []string `json:"delivered_decisions"`
}
if json.NewDecoder(r.Body).Decode(&body) != nil || body.TaskID == "" || body.LeaseEpoch == "" {
http.Error(w, "task_id and lease_epoch are required", http.StatusBadRequest)
return
}
switch body.Verdict {
case orchestrator.TurnContinue, orchestrator.TurnPrepareHandoff, orchestrator.TurnRotateNow, orchestrator.TurnRefuse:
default:
http.Error(w, "unknown verdict "+body.Verdict, http.StatusBadRequest)
return
}
verdict, decisions, err := coordinator.RemoteTurn(r.Context(), body.TaskID, body.LeaseEpoch, body.Verdict, body.Delivered)
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
json.NewEncoder(w).Encode(federation.TurnDecision{Verdict: verdict, Decisions: decisions})
})
mux.HandleFunc("/v1/federation/commands", func(w http.ResponseWriter, r *http.Request) {
wid, err := workerAuth(r)
if err != nil {
@@ -1218,6 +1529,7 @@ func main() {
Token: os.Getenv("ORCHESTRA_GITEA_TOKEN"), WebhookSecret: os.Getenv("ORCHESTRA_GITEA_WEBHOOK_SECRET"),
}}
}
humanSources := map[string]human.Source{}
if len(giteaSources) > 0 {
reflectors := map[string]provider.Gitea{}
for _, c := range giteaSources {
@@ -1252,6 +1564,38 @@ func main() {
}}
sup.Start(context.Background())
providerHealth[name] = sup
humanSources[g.SourceName()] = provider.GiteaComments{Gitea: g}
if publisher := (provider.GiteaPublisher{Gitea: g, Base: os.Getenv("ORCHESTRA_PR_BASE")}); publisher.BaseURL != "" {
root := projectRoots[c.Project]
submissionPublisher = func(plan operations.SubmissionPlan) operations.Publisher {
p := publisher
p.Root = filepath.Join(root, plan.TaskID)
return p
}
pullRequests[g.SourceName()] = publisher
}
}
}
// Reconciliation runs immediately before every lease, which is where
// ownership of a task begins. A configured source that cannot be read
// refuses the lease rather than letting a successor resume from an older
// intent. Set ORCHESTRA_HUMAN_RECONCILE=off to disable it for an
// operator who needs to run while a source is down.
if len(humanSources) > 0 && !strings.EqualFold(os.Getenv("ORCHESTRA_HUMAN_RECONCILE"), "off") {
reconciler := &human.Reconciler{Store: s, Sources: humanSources, Timeout: 30 * time.Second}
s.PreLease = func(taskID string) error {
return reconciler.Reconcile(context.Background(), taskID)
}
if coordinator != nil {
// The second reconciliation point: a verified turn boundary on a
// lease that is already running. Nothing here preempts the agent.
coordinator.ReconcileHumanInput = reconciler.Reconcile
// After this many consecutive failures at that boundary, the
// session is asked to hand off rather than keep running on intent
// Orchestra can no longer refresh.
if v, parseErr := strconv.Atoi(os.Getenv("ORCHESTRA_RECONCILE_FAILURE_HANDOFF")); parseErr == nil && v > 0 {
coordinator.ReconcileFailureHandoff = v
}
}
}
if path := os.Getenv("ORCHESTRA_JSONL"); path != "" {
@@ -1321,6 +1665,11 @@ func main() {
tokens := map[authz.Surface]string{
authz.TUI: os.Getenv("ORCHESTRA_TUI_TOKEN"),
authz.MCP: os.Getenv("ORCHESTRA_MCP_TOKEN"), authz.Maven: os.Getenv("ORCHESTRA_MAVEN_TOKEN"),
// The credential an in-pane coding session presents. It buys the two
// request endpoints and read access — never a lifecycle mutation. The
// forge, Vikunja and operator tokens must never reach an agent pane;
// this one is the only Orchestra credential an agent may hold.
authz.Agent: os.Getenv("ORCHESTRA_AGENT_TOKEN"),
// S12: this is the credential callers present *to* Orchestra on the
// ntfy surface. ORCHESTRA_NTFY_TOKEN is a different secret entirely —
// it is handed out to the third-party ntfy server (see the sender
@@ -1329,3 +1678,14 @@ func main() {
}
log.Fatal(http.ListenAndServe(":"+port, authz.HTTPWithSessions(tokens, sessions, mux)))
}
// splitList reads a comma-separated env list, ignoring blanks.
func splitList(v string) []string {
var out []string
for _, item := range strings.Split(v, ",") {
if s := strings.TrimSpace(item); s != "" {
out = append(out, s)
}
}
return out
}
+20
View File
@@ -36,6 +36,7 @@ func TestFederatedReachabilityDefersRemoteHerdrToWorkerHeartbeat(t *testing.T) {
func TestCoordinatorOwnsOnlyLocalHerdrInFederationMode(t *testing.T) {
local := registry.Herdr{ID: "homesrv-opencode", MachineID: "homesrv"}
localTmux := registry.Herdr{ID: "homesrv-claude", MachineID: "homesrv", Backend: "tmux", Harness: "claude"}
remote := registry.Herdr{ID: "workpc-opencode", MachineID: "workpc"}
if !coordinatorOwnsHerdr(local, "homesrv") {
t.Fatal("coordinator does not own its local herdr")
@@ -43,6 +44,9 @@ func TestCoordinatorOwnsOnlyLocalHerdrInFederationMode(t *testing.T) {
if coordinatorOwnsHerdr(remote, "homesrv") {
t.Fatal("coordinator claimed a worker-owned remote herdr")
}
if coordinatorOwnsHerdr(localTmux, "homesrv") {
t.Fatal("coordinator claimed a local worker-owned tmux backend")
}
if !coordinatorOwnsHerdr(remote, "") {
t.Fatal("single-machine mode should retain legacy local ownership")
}
@@ -69,3 +73,19 @@ func TestMultiMachineRegistryRequiresKnownLocalMachine(t *testing.T) {
t.Fatalf("known local machine rejected: %v", err)
}
}
func TestTmuxRegistryRequiresMachineIdentityEvenOnOneMachine(t *testing.T) {
r, err := registry.New(registry.Config{
Machines: []registry.Machine{{ID: "homesrv", Address: "homesrv:9145"}},
Herdrs: []registry.Herdr{{ID: "homesrv-claude", MachineID: "homesrv", Backend: "tmux", Harness: "claude"}},
})
if err != nil {
t.Fatal(err)
}
if err := validateLocalMachine(r, ""); err == nil {
t.Fatal("worker-owned tmux backend accepted without machine identity")
}
if err := validateLocalMachine(r, "homesrv"); err != nil {
t.Fatal(err)
}
}