From 97a9c65302998e951e181742c4425858f114ea3e Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 31 Jul 2026 00:29:42 +0400 Subject: [PATCH] Delete Design A, the harness-hook completion path, and retired deploy files Acts on the seven review comments on PR #1. Design A is gone (comment 4). clients/ deleted rather than tracked: with workers carrying cross-machine work the bridge is undeployed, which supersedes the 2026-07-27 "keep through Phase 5" decision. CLAUDE.md, AGENTS.md and AUDIT.md updated from "retained" to "deleted". The harness-hook completion path is gone (comment 10). Investigation of the live OpenCode QA run showed orchestra-worker owns completion end to end: it watches for .orchestra/done, confirms via AgentStatus that the agent is not busy, then posts through /v1/federation/* with both lease epoch and expected version. The hook scripts used a different, older convention (.orchestra-report.md) and posted to /v1/harness/complete, which had already been reduced to a 410 stub - so that path could not have completed a task. Nothing exercised it, because the live run never used it. Deleted: the three deploy/hooks scripts, the 410 route, the unmounted harnessCompletion handler, and its test. That test passed against a handler no mux routed to, which is the exact "looks wired but isn't" pattern CLAUDE.md warns about; the constant-time token compare added to it earlier today goes with it, having never been reachable. /v1/harness/turn is untouched and still live. Retired deployment files (comments 8, 12, 14): deploy/orchestra.service and deploy/redeploy.sh (which sudo-installed to /usr/local/bin and restarted that unit), plus deploy/docker-api-entrypoint.sh. The entrypoint was safe to remove once its premise was checked: env vars reach the container through `env_file: .env` in compose.yaml, not by sourcing /etc/orchestra/orchestra.env - only config.jsonc is bind-mounted there - and Dockerfile.api's line 17 already sets ORCHESTRA_DATA/ORCHESTRA_PORT. Dockerfile.api now execs /app/orchestra directly. orchestra-worker.service is a different, current unit and is kept. deploy/config.example.json deleted as a duplicate (comment 6); the annotated .jsonc is the one registry.go points at, and its header no longer tells the reader to copy the file that just went away. Documentation corrected beyond the deletions: - CLAUDE.md's deployment section claimed the container bind-mounts /etc/orchestra:ro and its entrypoint sources the env file. Both wrong. - AGENTS.md still described a systemd deployment on homesrv as of 2026-07-27. - AUDIT.md's H5 row still described a "retained compatibility handler". - deploy/DEPLOYMENT.md still named redeploy.sh as the deployment path. - deploy/orchestra.env.example still cited EnvironmentFile=. TOKEN_MINIMAL_WORKFLOW_PLAN.md (comment 2) is untouched: it and WEB_UI_PLAN.md were both missed by REVIEW.md's documentation sweep, and reconciling a 534-line forward-looking plan against AUDIT.md is its own task, not a review fixup. Verified: go build ./..., go vet ./..., go test ./... all pass after the deletions, and go list ./... has no node_modules entry. No live herdr or pane was touched; nothing was deployed. The running image still predates this commit until compose is rebuilt. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GEugbHVYfAXFpTqDYbByEB --- .gitignore | 6 +- AGENTS.md | 47 +++++++--- AUDIT.md | 24 ++++-- CLAUDE.md | 48 +++++++---- Dockerfile.api | 3 +- clients/README.md | 32 ------- clients/herdr-bridge.go | 42 --------- clients/herdr-bridge.service | 11 --- cmd/orchestra/main.go | 109 +++--------------------- cmd/orchestra/main_test.go | 76 ----------------- deploy/DEPLOYMENT.md | 8 +- deploy/config.example.json | 66 -------------- deploy/config.example.jsonc | 7 +- deploy/docker-api-entrypoint.sh | 13 --- deploy/hooks/orchestra-codex-poll.sh | 84 ------------------ deploy/hooks/orchestra-opencode-poll.sh | 79 ----------------- deploy/hooks/orchestra-stop.sh | 70 --------------- deploy/orchestra.env.example | 8 +- deploy/orchestra.service | 25 ------ deploy/redeploy.sh | 21 ----- 20 files changed, 114 insertions(+), 665 deletions(-) delete mode 100644 clients/README.md delete mode 100644 clients/herdr-bridge.go delete mode 100644 clients/herdr-bridge.service delete mode 100644 deploy/config.example.json delete mode 100755 deploy/docker-api-entrypoint.sh delete mode 100755 deploy/hooks/orchestra-codex-poll.sh delete mode 100755 deploy/hooks/orchestra-opencode-poll.sh delete mode 100755 deploy/hooks/orchestra-stop.sh delete mode 100644 deploy/orchestra.service delete mode 100755 deploy/redeploy.sh diff --git a/.gitignore b/.gitignore index 7a096e2..c6fb17c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,14 @@ .orchestra-config/ # Built binaries (never commit — a stale committed binary is a deployment- -# confusion hazard). `clients/` is deliberately NOT ignored: it holds the -# still-deployed Design A herdr bridge, and deployed code must be tracked. +# confusion hazard). /orchestra /orchestra-worker # Web UI build inputs/outputs. node_modules in particular ships vendored Go # packages (e.g. flatted/golang), so leaving it merely untracked is not -# enough — `go build ./...` and `go test ./...` walk into it. +# enough — `go build ./...` and `go test ./...` walk into it. `web/go.mod` is +# the fix: it ends the parent module's package graph at that directory. /package-lock.json node_modules/ .node_modules/ diff --git a/AGENTS.md b/AGENTS.md index 438087c..50a3ff3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,15 +57,25 @@ the live herdr instance and check. today — don't "clean up" that code without checking this note first, or it might start doing a real numeric-vs-string comparison and break. -## Deployment topology (as of 2026-07-27) +## Deployment topology (as of 2026-07-31) -- Runs as `orchestra.service` on **homesrv** (this machine's own systemd — - `journalctl -u orchestra.service` for logs; `sudo` isn't available in this - sandbox environment, but plain `journalctl` without sudo works here). -- Config: `.orchestra-config/orchestra.env` (env vars) + - `.orchestra-config/config.jsonc` (projects/machines/herdrs registry). - `ORCHESTRA_CONFIG=/etc/orchestra/config.jsonc` — the deployed copy, not the - repo's `deploy/config.example.jsonc`. +- **Runs under Docker Compose, not systemd.** `docker compose -f compose.yaml + -f compose.live.yaml` in `/home/kami/docker-apps/orchestra-web-ui`, building + both images from this repo: `orchestra-api` (bound `0.0.0.0:9145`, which is + intentional — ufw restricts the port to one other LAN machine) and + `orchestra-web-ui` (nginx proxy, `127.0.0.1:19145`). Logs are + `docker logs orchestra-api`. **Deploying a code change means rebuilding the + compose images** (`up -d --build`) — the running image can silently predate + recent commits, so compare its build time against `git log`. +- `orchestra.service` was the previous deployment; its unit file and + `redeploy.sh` were deleted from `deploy/` on 2026-07-31. A stale installed + copy must stay stopped — it binds the same port and data dir as the + container. `orchestra-worker.service` is a *different*, still-current unit. +- Config: env vars come from **`.env` in the compose directory** via + `env_file:`; only `config.jsonc` is bind-mounted into `/etc/orchestra/`. + There is no container entrypoint script — `Dockerfile.api` execs + `/app/orchestra` directly. Neither deployed file is the repo's + `deploy/config.example.jsonc`. - Two machines in the registry: `homesrv` (192.168.1.104) and `workpc` (192.168.1.105), each nominally running 3 herdrs (claude/codex/opencode). In practice **homesrv has no local herdr running** (connection refused on @@ -94,12 +104,21 @@ resolve an adapter for a session owned by a non-local herdr, returning (`git rev-parse HEAD`) against the wrong machine's checkout. Rotation/cleanup therefore no longer act on remote leases. -**Design A is retained, not dead.** `clients/herdr-bridge.go` ("drive the -remote socket": homesrv calling `worktree.create`/`agent.start` directly on -workpc's herdr over TCP as if it were local) is kept through Phase 5 per the -standing AUDIT.md decision of 2026-07-27; deletion of Design A and `clients/` -is deferred to the Phase 6 cutover. Do not delete it early, and do not add new -cross-machine call paths to it. +**Design A is gone (deleted 2026-07-31).** `clients/herdr-bridge.go` ("drive +the remote socket": homesrv calling `worktree.create`/`agent.start` directly on +workpc's herdr over TCP as if it were local) was deleted along with the whole +`clients/` directory — the operator confirmed it is undeployed now that workers +carry cross-machine work, which superseded the 2026-07-27 "keep through Phase +5" decision. There is no bridge to preserve; do not reintroduce +coordinator-side calls to a remote herdr socket. + +**Completion is worker-owned.** `orchestra-worker` watches for an +`.orchestra/done` marker, confirms via `AgentStatus` that the agent is no +longer busy, then posts through `/v1/federation/*` with the lease epoch and +expected version. The old harness-hook path — `.orchestra-report.md` plus +`POST /v1/harness/complete` — is **deleted**, endpoint, handler, and +`deploy/hooks/` scripts alike. `/v1/harness/turn` remains for turn-boundary +decisions. ## Working conventions diff --git a/AUDIT.md b/AUDIT.md index 7b040b1..1ca3ce1 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -36,10 +36,24 @@ resolve an adapter for a session owned by a non-local herdr, so rotation and cleanup can no longer validate a git anchor against the wrong machine's checkout. -Design A itself is **retained through Phase 5** per that same decision. -`clients/herdr-bridge.go` is now tracked in git (deployed code must be in -version control); deleting Design A and `clients/` is deferred to the Phase 6 -cutover. +Design A is **deleted as of 2026-07-31**, superseding the 2026-07-27 "retain +through Phase 5" decision: the operator confirmed the bridge is undeployed now +that workers carry cross-machine work, so `clients/` was removed rather than +tracked. The Phase 6 cutover is therefore already done on this axis. + +The legacy harness-hook completion path was removed in the same pass, once it +was confirmed that nothing calls it. `orchestra-worker` owns completion — it +watches for `.orchestra/done`, confirms via `AgentStatus` that the agent is not +busy, then posts through `/v1/federation/*` with the lease epoch and expected +version. Deleted: the `/v1/harness/complete` route (a `410` stub), its unmounted +`harnessCompletion` handler, that handler's test (green against unreachable +code — the pattern this audit exists to catch), and the three `deploy/hooks/` +scripts, which still used the older `.orchestra-report.md` marker and would +have failed against the `410`. `/v1/harness/turn` is unaffected and still live. + +Note for the QA matrix: the OpenCode run completed through the worker path, so +no hook script was exercised. Nothing about hook-based completion was ever +verified live, which is why deleting it costs nothing. ## Remaining release blockers @@ -87,7 +101,7 @@ cutover. | H2 | **Closed 2026-07-30.** `PrepareRelease` verifies immutable `TASK.md`, checkpoints all repository work except protocol markers, always pushes the per-task project's scratch anchor, verifies it with `ls-remote`, and only then seals the CAS handoff. | `TestScratchCommitCapturesAllGitStatesExceptProtocolMarkers` covers staged, deleted, renamed, untracked, and protocol-marker cases; release uses the configured project remote. | | H3 | **Closed 2026-07-30.** Worker state persists idempotent release transactions through `prepared → anchor_pushed → event_committed → pickup_validated → predecessor_retired`. Release/pickup endpoints bind transaction, anchor, and lease version; a predecessor remains mapped and is retired only after matching pickup validation. | `TestReleaseTransactionSurvivesReLeaseUntilMatchingPickup` covers transaction propagation and pickup epoch binding; full race suite passes. | | H4 | **Closed 2026-07-30.** Every new lease carries an opaque durable `lease_epoch`; renew/release/pickup/complete validate the exact harness owner and epoch at the store boundary and federation API. Offline heartbeats retain leases until expiry, new workers require a fresh reachable local-herdr probe, and local/worker ownership loss stops or durably quarantines the old pane before its mapping is dropped. | `TestLeaseEpochFencesStaleOwnerLifecycleWrites`, `TestAvailableRequiresFreshReachableLocalHerdrHealth`, plus the full race suite cover stale re-lease/completion and health admission. | -| H5 | **Closed 2026-07-30.** `Store.Append` validates legal state/owner/epoch transitions, fsyncs the event before applying its projection, and replays projections solely from `events.jsonl` (snapshots are disposable caches). CAS, worker/federation/coordinator state use temp-file + fsync + rename; corrupt worker state aborts startup. The live legacy `/v1/harness/complete` route is retired (410); its retained compatibility handler is fenced if invoked directly. | `TestOpenRebuildsOnlyFromLogAndIgnoresCorruptSnapshot`, `TestWorkerRefusesCorruptDurableState`, and `go test -race ./...` pass. | +| H5 | **Closed 2026-07-30.** `Store.Append` validates legal state/owner/epoch transitions, fsyncs the event before applying its projection, and replays projections solely from `events.jsonl` (snapshots are disposable caches). CAS, worker/federation/coordinator state use temp-file + fsync + rename; corrupt worker state aborts startup. The legacy `/v1/harness/complete` route and its handler were deleted outright on 2026-07-31 (previously a 410 stub plus an unmounted, separately-fenced handler). | `TestOpenRebuildsOnlyFromLogAndIgnoresCorruptSnapshot`, `TestWorkerRefusesCorruptDurableState`, and `go test -race ./...` pass. | ## P1 — autonomy and recovery diff --git a/CLAUDE.md b/CLAUDE.md index 167209f..9cb91c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,15 +66,22 @@ the live herdr instance and check. `docker logs orchestra-api`. **Deploying a code change means rebuilding the compose images** (`up -d --build`) — the running image can silently predate recent commits, so compare its build time against `git log`. -- `orchestra.service` is the **previous** deployment and is retired — - `compose.live.yaml` requires it stopped, since both bind the same port and - data dir. Do not start it. (`sudo` isn't available in this sandbox, so - `systemctl disable` needs the operator.) -- Config: `/etc/orchestra/orchestra.env` (env vars) + `/etc/orchestra/ - config.jsonc` (projects/machines/herdrs registry) — the deployed copies, not - the repo's `.orchestra-config/` or `deploy/config.example.jsonc`. The - container bind-mounts `/etc/orchestra:ro` and its entrypoint sources the env - file, so Docker never copies the secrets. +- `orchestra.service` was the **previous** deployment; the unit file was + deleted from `deploy/` on 2026-07-31 along with `redeploy.sh` (which + `sudo install`ed to `/usr/local/bin` and restarted it). If a stale copy is + still installed on a host, it must stay stopped — it binds the same port and + data dir as the container. (`sudo` isn't available in this sandbox, so + `systemctl disable` needs the operator.) `orchestra-worker.service` is a + *different*, still-current unit — don't delete it by association. +- Config: env vars come from **`.env` in the compose directory**, loaded via + `env_file:` in `compose.yaml` — that is where the Gitea/ntfy/web tokens and + the bcrypt operator hash live. The only thing bind-mounted into + `/etc/orchestra/` is a single file, `config.jsonc` + (projects/machines/herdrs registry), via `compose.override.yaml`. There is no + container entrypoint script — `Dockerfile.api` execs `/app/orchestra` + directly, and `ORCHESTRA_DATA`/`ORCHESTRA_PORT` come from the image `ENV` + plus compose. Neither the deployed `config.jsonc` nor `.env` is the repo's + `deploy/config.example.jsonc`. - The `0.0.0.0` bind on 9145 is **intentional**: ufw restricts the port to one other LAN machine. Don't report it as an exposure. - Two machines in the registry: `homesrv` (192.168.1.104) and `workpc` @@ -107,12 +114,23 @@ resolve an adapter for a session owned by a non-local herdr, returning (`git rev-parse HEAD`) against the wrong machine's checkout. Rotation/cleanup therefore no longer act on remote leases. -**Design A is retained, not dead.** `clients/herdr-bridge.go` ("drive the -remote socket": homesrv calling `worktree.create`/`agent.start` directly on -workpc's herdr over TCP as if it were local) is kept through Phase 5 per the -standing AUDIT.md decision of 2026-07-27; deletion of Design A and `clients/` -is deferred to the Phase 6 cutover. Do not delete it early, and do not add new -cross-machine call paths to it. +**Design A is gone (deleted 2026-07-31).** `clients/herdr-bridge.go` ("drive +the remote socket": homesrv calling `worktree.create`/`agent.start` directly on +workpc's herdr over TCP as if it were local) was deleted along with the whole +`clients/` directory — the operator confirmed it is undeployed now that workers +carry cross-machine work, which superseded the 2026-07-27 "keep through Phase +5" decision. There is no bridge to preserve; do not reintroduce coordinator-side +calls to a remote herdr socket. + +**Completion is worker-owned.** `orchestra-worker` watches for an +`.orchestra/done` marker in the worktree, confirms via `AgentStatus` that the +agent is no longer busy (a marker alone is intent, not proof), then finalizes +and posts through `/v1/federation/*` with both the lease epoch and the expected +version. The old harness-hook path — `.orchestra-report.md` plus +`POST /v1/harness/complete` — is **deleted**: the endpoint, its handler, and the +`deploy/hooks/` scripts are all gone, because an unaffiliated hook has no +durable worker identity or fencing epoch. `/v1/harness/turn` remains for +turn-boundary decisions. ## Working conventions diff --git a/Dockerfile.api b/Dockerfile.api index 213bc5c..65ca221 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -12,9 +12,8 @@ FROM alpine:3.21 RUN adduser -D -u 10001 orchestra WORKDIR /app COPY --from=build /out/orchestra /app/orchestra -COPY deploy/docker-api-entrypoint.sh /app/docker-api-entrypoint.sh RUN mkdir /data && chown orchestra:orchestra /data ENV ORCHESTRA_DATA=/data ORCHESTRA_PORT=9145 VOLUME ["/data"] EXPOSE 9145 -ENTRYPOINT ["/app/docker-api-entrypoint.sh"] +ENTRYPOINT ["/app/orchestra"] diff --git a/clients/README.md b/clients/README.md deleted file mode 100644 index 80ed090..0000000 --- a/clients/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# workpc herdr TCP bridges - -These files are intentionally local and gitignored. The workpc herdr exposes -one local Unix socket; Orchestra reaches it over the LAN through three bound -TCP bridges: - -| machine | listen | herdr socket | -|---|---|---| -| workpc | 192.168.1.105:9245 | ~/.config/herdr/herdr.sock | - -The bridge is a byte-preserving proxy for the newline-delimited JSON protocol. -It does not interpret or authenticate requests; restrict the ports with the -LAN firewall/WireGuard policy. - -## Not a federation worker - -`herdr-bridge` deliberately does **not** own a checkout. It cannot write -`TASK.md`, inspect Git `HEAD`/branch/dirty hashes, make a scratch commit, or -publish a canonical continuity handoff. Treat it as Design A compatibility -only, not the cross-machine protocol described by Orchestra's spec. - -`cmd/orchestra-worker` is the worker-side client. It registers and -heartbeats as the configured herdr id, consumes only router-issued -`TaskLeased` events for that id, creates/validates the local checkout, drives -the local herdr, and seals/pushes a canonical handoff locally before calling -the federation handoff endpoint. It deliberately has no task-claim operation: -homesrv's router is the only scheduler. - -Install `deploy/orchestra-worker.service` on the worker host and configure -the federation variables in `deploy/orchestra.env.example`. Set -`ORCHESTRA_MACHINE_ID` on homesrv; that prevents its coordinator from ever -opening, validating, rotating, or cleaning up a workpc checkout. diff --git a/clients/herdr-bridge.go b/clients/herdr-bridge.go deleted file mode 100644 index 5c90c23..0000000 --- a/clients/herdr-bridge.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "flag" - "io" - "log" - "net" - "os" -) - -func main() { - listen := flag.String("listen", "192.168.1.105:9245", "TCP address to expose") - socket := flag.String("socket", os.Getenv("HOME")+"/.config/herdr/herdr.sock", "herdr Unix socket") - flag.Parse() - ln, err := net.Listen("tcp", *listen) - if err != nil { - log.Fatal(err) - } - log.Printf("herdr bridge listening on %s -> unix:%s", *listen, *socket) - for { - c, err := ln.Accept() - if err != nil { - log.Printf("accept: %v", err) - continue - } - go proxy(c, *socket) - } -} - -func proxy(c net.Conn, socket string) { - defer c.Close() - u, err := net.Dial("unix", socket) - if err != nil { - log.Printf("dial herdr: %v", err) - return - } - defer u.Close() - done := make(chan struct{}, 2) - go func() { _, _ = io.Copy(u, c); _ = u.Close(); done <- struct{}{} }() - go func() { _, _ = io.Copy(c, u); _ = c.Close(); done <- struct{}{} }() - <-done -} diff --git a/clients/herdr-bridge.service b/clients/herdr-bridge.service deleted file mode 100644 index 759d9b4..0000000 --- a/clients/herdr-bridge.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Herdr LAN bridge for Orchestra -After=network-online.target herdr.service - -[Service] -ExecStart=/usr/local/bin/herdr-bridge -listen 192.168.1.105:9245 -socket %h/.config/herdr/herdr.sock -Restart=on-failure -NoNewPrivileges=true - -[Install] -WantedBy=default.target diff --git a/cmd/orchestra/main.go b/cmd/orchestra/main.go index 237ab32..a51cb90 100644 --- a/cmd/orchestra/main.go +++ b/cmd/orchestra/main.go @@ -126,87 +126,6 @@ func validateLocalMachine(rr registry.Registry, localMachine string) error { return nil } -type harnessCompletion struct { - store *store.Store - route func(domain.Event) error - token string -} - -func (h harnessCompletion) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - if h.token != "" && subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+h.token)) != 1 { - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - var p struct { - TaskID string `json:"task_id"` - WorkerID string `json:"worker_id"` - LeaseEpoch string `json:"lease_epoch"` - Harness string `json:"harness"` - TranscriptPath string `json:"transcript_path"` - Report string `json:"report"` - } - if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" || p.WorkerID == "" || p.LeaseEpoch == "" || p.Report == "" || p.TranscriptPath == "" { - http.Error(w, "task_id, worker_id, lease_epoch, transcript_path, and report are required", http.StatusBadRequest) - return - } - t, ok := h.store.Task(p.TaskID) - if !ok { - http.Error(w, "task not found", http.StatusNotFound) - return - } - if t.State != domain.StateLeased || t.Lease == nil || t.Lease.HarnessID != p.WorkerID || t.Lease.Epoch != p.LeaseEpoch { - http.Error(w, "lease not owned", http.StatusConflict) - return - } - var usage herdr.Usage - var err error - switch p.Harness { - case "codex": - usage, err = herdr.CodexUsage(p.TranscriptPath) - case "opencode": - usage, err = herdr.OpenCodeUsage(p.TranscriptPath) - case "", "claude": - usage, err = herdr.ClaudeUsage(p.TranscriptPath) - default: - http.Error(w, "unknown harness: "+p.Harness, http.StatusBadRequest) - return - } - if err != nil { - http.Error(w, "reading transcript: "+err.Error(), http.StatusBadRequest) - return - } - ref, err := h.store.PutArtifact([]byte(p.Report)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - payload, _ := json.Marshal(map[string]any{"report_ref": ref, "harness_id": p.WorkerID, "lease_epoch": p.LeaseEpoch, "expected_version": t.Version, "receipt": map[string]any{ - "input_tokens": usage.Input, "cache_read_tokens": usage.CacheRead, - "cache_write_tokens": usage.CacheWrite, "output_tokens": usage.Output, "numerator": usage.Numerator(), - }}) - e := domain.Event{ID: id(), Type: "TaskCompleted", TaskID: p.TaskID, Version: t.Version + 1, Payload: payload, Surface: string(authz.System)} - if err := h.store.Append(e); err != nil { - http.Error(w, err.Error(), http.StatusConflict) - return - } - if h.route != nil { - if err := h.route(e); err != nil { - log.Printf("route task: %v", err) - } - } - if t.Lease != nil && t.Lease.HarnessID != "" { - qp, _ := json.Marshal(map[string]any{"harness_id": t.Lease.HarnessID, "consumed": float64(usage.Numerator())}) - if err := h.store.Append(domain.Event{ID: id(), Type: "QuotaReported", TaskID: "system", Payload: qp, Surface: string(authz.System)}); err != nil { - log.Printf("quota report: %v", err) - } - } - json.NewEncoder(w).Encode(e) -} - func main() { dir := os.Getenv("ORCHESTRA_DATA") if dir == "" { @@ -553,27 +472,19 @@ func main() { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(b) }) - // /v1/harness/complete is the automatic TaskCompleted producer (AUDIT.md - // B3): a harness-side hook posts here when the agent has declared the - // task done (see deploy/hooks/orchestra-stop.sh), not on every turn - // boundary. It reads the transcript locally to build an honest receipt — - // same session-file assumption as CLIAdapter.Occupancy — rather than - // trusting a self-reported number. harnessToken := os.Getenv("ORCHESTRA_HARNESS_TOKEN") - // The unaffiliated harness hook has no durable worker identity or fencing - // epoch, so it cannot safely mutate a leased task. Completion is accepted - // only through the authenticated federation worker endpoint below. - mux.HandleFunc("/v1/harness/complete", func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "legacy harness completion endpoint retired; use worker completion", http.StatusGone) - }) // /v1/harness/turn is the unified turn-decision endpoint (AUDIT.md Phase - // 2 items 1-2): the Face-B stop hook posts here on every ordinary turn - // boundary (task completion is not handled here — it goes through the - // authenticated worker completion endpoint, since the retired - // /v1/harness/complete had no fencing epoch) and gets back continue / - // prepare_handoff / rotate_now / refuse, per spec §5.3. This replaces - // what would otherwise be separate ad-hoc marker-file conventions per + // 2 items 1-2): a harness-side caller posts here on an ordinary turn + // boundary and gets back continue / prepare_handoff / rotate_now / refuse, + // per spec §5.3, instead of separate ad-hoc marker-file conventions per // decision. + // + // Completion does NOT go through this endpoint, and there is no longer a + // /v1/harness/complete: an unaffiliated harness hook has no durable worker + // identity or fencing epoch, so it cannot safely mutate a leased task. + // orchestra-worker owns completion — it watches for the .orchestra/done + // marker, confirms the agent is no longer busy, and posts through the + // authenticated federation endpoints with both lease epoch and version. mux.HandleFunc("/v1/harness/turn", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) diff --git a/cmd/orchestra/main_test.go b/cmd/orchestra/main_test.go index da3b8ae..95ec54e 100644 --- a/cmd/orchestra/main_test.go +++ b/cmd/orchestra/main_test.go @@ -1,19 +1,12 @@ package main import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" "os" "path/filepath" "testing" "time" - "orchestra/internal/authz" - "orchestra/internal/domain" "orchestra/internal/registry" - "orchestra/internal/store" ) type unreachable struct{} @@ -55,75 +48,6 @@ func TestCoordinatorOwnsOnlyLocalHerdrInFederationMode(t *testing.T) { } } -func TestHarnessCompletionBuildsReceiptAndQuotaFromTranscript(t *testing.T) { - s, err := store.Open(t.TempDir()) - if err != nil { - t.Fatal(err) - } - if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "done", Surface: string(authz.System), Payload: []byte(`{"source":"qa","external_id":"done","project":"p"}`)}); err != nil { - t.Fatal(err) - } - if _, err := s.Lease("done", "local-claude", time.Minute); err != nil { - t.Fatal(err) - } - transcript := filepath.Join(t.TempDir(), "transcript.jsonl") - if err := os.WriteFile(transcript, []byte(`{"message":{"usage":{"input_tokens":100,"cache_read_input_tokens":20,"cache_creation_input_tokens":5,"output_tokens":7}}}`+"\n"), 0600); err != nil { - t.Fatal(err) - } - task, _ := s.Task("done") - body, _ := json.Marshal(map[string]string{"task_id": "done", "worker_id": "local-claude", "lease_epoch": task.Lease.Epoch, "transcript_path": transcript, "report": "# done"}) - req := httptest.NewRequest(http.MethodPost, "/v1/harness/complete", bytes.NewReader(body)) - res := httptest.NewRecorder() - harnessCompletion{store: s, token: "secret"}.ServeHTTP(res, req) - if res.Code != http.StatusUnauthorized { - t.Fatalf("missing token status = %d, want 401", res.Code) - } - req.Header.Set("Authorization", "Bearer secret") - req = httptest.NewRequest(http.MethodPost, "/v1/harness/complete", bytes.NewReader(body)) - req.Header.Set("Authorization", "Bearer secret") - res = httptest.NewRecorder() - harnessCompletion{store: s, token: "secret"}.ServeHTTP(res, req) - if res.Code != http.StatusOK { - t.Fatalf("completion status = %d: %s", res.Code, res.Body.String()) - } - task, ok := s.Task("done") - if !ok || task.State != domain.StateCompleted { - t.Fatalf("task after completion = %#v, present=%v", task, ok) - } - var completed struct { - Receipt struct { - Input int `json:"input_tokens"` - CacheRead int `json:"cache_read_tokens"` - CacheWrite int `json:"cache_write_tokens"` - Output int `json:"output_tokens"` - } `json:"receipt"` - } - for _, e := range s.Events(0) { - if e.Type == "TaskCompleted" { - if err := json.Unmarshal(e.Payload, &completed); err != nil { - t.Fatal(err) - } - } - } - if completed.Receipt.Input != 100 || completed.Receipt.CacheRead != 20 || completed.Receipt.CacheWrite != 5 || completed.Receipt.Output != 7 { - t.Fatalf("receipt = %#v", completed.Receipt) - } - var quota struct { - Harness string `json:"harness_id"` - Consumed float64 `json:"consumed"` - } - found := false - for _, e := range s.Events(0) { - if e.Type == "QuotaReported" { - _ = json.Unmarshal(e.Payload, "a) - found = true - } - } - if !found || quota.Harness != "local-claude" || quota.Consumed != 125 { - t.Fatalf("quota report = %#v, found=%v", quota, found) - } -} - func TestMultiMachineRegistryRequiresKnownLocalMachine(t *testing.T) { path := filepath.Join(t.TempDir(), "config.json") if err := os.WriteFile(path, []byte(`{ diff --git a/deploy/DEPLOYMENT.md b/deploy/DEPLOYMENT.md index 8d72237..2e1950a 100644 --- a/deploy/DEPLOYMENT.md +++ b/deploy/DEPLOYMENT.md @@ -1,8 +1,10 @@ # Deployment verification -Both binaries embed their Git revision, UTC build time, and dirty flag. Build -the coordinator with `deploy/redeploy.sh`; it installs and restarts the local -`orchestra.service`. +Both binaries embed their Git revision, UTC build time, and dirty flag. The +coordinator is deployed as a Docker Compose image — see "For the Docker +coordinator deployment" below for the build that carries provenance. (The old +`deploy/redeploy.sh` + `orchestra.service` path was deleted on 2026-07-31; +`orchestra-worker.service` is a different, still-current unit.) ## Browser operator login diff --git a/deploy/config.example.json b/deploy/config.example.json deleted file mode 100644 index 6515589..0000000 --- a/deploy/config.example.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "projects": [ - { - "id": "correx", - "machine_affinity": ["mainframe"], - "repo": "/var/lib/orchestra/repos/correx.git", - "worktree_root": "/var/lib/orchestra/worktrees/correx" - }, - { - "id": "maven", - "machine_affinity": ["mainframe", "satellite"] - } - ], - "machines": [ - { - "id": "mainframe", - "address": "10.0.0.10:9145" - }, - { - "id": "satellite", - "address": "10.0.0.11:9145" - } - ], - "herdrs": [ - { - "id": "mainframe-claude-1", - "machine_id": "mainframe", - "harness": "claude", - "protocol": "1", - "capabilities": ["code", "review"], - "concurrency": 2, - "quota_limit_5h": 50, - "quota_limit_weekly": 500 - }, - { - "id": "satellite-claude-1", - "machine_id": "satellite", - "address": "10.0.0.11:9245", - "harness": "claude", - "protocol": "1", - "capabilities": ["code"], - "concurrency": 1, - "quota_limit_5h": 20, - "quota_limit_weekly": 200 - }, - { - "id": "mainframe-codex-1", - "machine_id": "mainframe", - "harness": "codex", - "protocol": "1", - "capabilities": ["code"], - "concurrency": 1, - "quota_limit_weekly": 300 - }, - { - "id": "satellite-opencode-1", - "machine_id": "satellite", - "address": "10.0.0.11:9345", - "harness": "opencode", - "protocol": "1", - "capabilities": ["code", "review"], - "concurrency": 1, - "quota_limit_weekly": 300 - } - ] -} diff --git a/deploy/config.example.jsonc b/deploy/config.example.jsonc index 327c679..a2e584d 100644 --- a/deploy/config.example.jsonc +++ b/deploy/config.example.jsonc @@ -1,6 +1,9 @@ -// Annotated reference for config.example.json (registry.Config, internal/registry/registry.go). +// Annotated reference for registry.Config (internal/registry/registry.go). // This file is NOT valid JSON (it has comments) and is not loaded by orchestra — -// it exists purely to document fields. Copy config.example.json, not this file. +// it exists purely to document fields. Copy it, strip the comments, and install +// the result as the deployed config.jsonc (bind-mounted into /etc/orchestra/ by +// compose.override.yaml). The plain config.example.json was deleted on +// 2026-07-31 as a duplicate of this file. { // Static project topology. One entry per project the fleet routes tasks for. "projects": [ diff --git a/deploy/docker-api-entrypoint.sh b/deploy/docker-api-entrypoint.sh deleted file mode 100755 index 2454db7..0000000 --- a/deploy/docker-api-entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -set -eu -# Keep the existing coordinator's secrets/config in the host-owned env file; -# Docker never needs to read or copy it. The process runs as the same numeric -# service user and can therefore read the bind-mounted file. -if [ -r /etc/orchestra/orchestra.env ]; then - set -a - . /etc/orchestra/orchestra.env - set +a -fi -export ORCHESTRA_DATA=/data -export ORCHESTRA_PORT=9145 -exec /app/orchestra diff --git a/deploy/hooks/orchestra-codex-poll.sh b/deploy/hooks/orchestra-codex-poll.sh deleted file mode 100755 index c213cdb..0000000 --- a/deploy/hooks/orchestra-codex-poll.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/sh -# Codex turn-boundary/completion poller. -# -# Unlike Claude Code, Codex has no native Stop hook — nothing calls out at a -# turn boundary. This script is meant to run as a background loop inside the -# pane alongside the codex process (AUDIT.md Phase 2 item 4, "Not done: -# Codex/opencode Stop-hook-equivalent scripts"), polling the same -# two endpoints the Claude Stop hook (orchestra-stop.sh) calls on every turn: -# - completion: if the agent has written .orchestra-report.md, POST -# /v1/harness/complete with harness=codex and the newest active rollout -# path, then remove the marker. -# - turn decision: otherwise POST /v1/harness/turn and log (never kill the -# harness process on "refuse" — there's no turn boundary to refuse at -# from outside the process the way exit-code-2 works for a real Stop -# hook; this is advisory-only for codex until app-server integration -# exists). -# -# Requires: ORCHESTRA_TASK_ID, ORCHESTRA_URL, ORCHESTRA_WORKTREE (the pane's -# cwd) set in the pane's env. Optional: ORCHESTRA_HARNESS_TOKEN, -# ORCHESTRA_POLL_INTERVAL (seconds, default 60). -# Needs: jq, find, curl. - -set -u - -: "${ORCHESTRA_POLL_INTERVAL:=60}" -: "${ORCHESTRA_WORKTREE:=$PWD}" - -[ -z "${ORCHESTRA_TASK_ID:-}" ] && { echo "orchestra-codex-poll: ORCHESTRA_TASK_ID not set" >&2; exit 1; } -[ -z "${ORCHESTRA_URL:-}" ] && { echo "orchestra-codex-poll: ORCHESTRA_URL not set" >&2; exit 1; } - -auth_header="" -if [ -n "${ORCHESTRA_HARNESS_TOKEN:-}" ]; then - auth_header="Authorization: Bearer ${ORCHESTRA_HARNESS_TOKEN}" -fi - -# Newest rollout file under ~/.codex/sessions, matching CodexActiveUsage's -# own "most recently touched" heuristic (internal/herdr/occupancy.go). -latest_rollout() { - find "$HOME/.codex/sessions" -name 'rollout-*.jsonl' -type f -printf '%T@ %p\n' 2>/dev/null \ - | sort -rn | head -n1 | cut -d' ' -f2- -} - -report_file="${ORCHESTRA_WORKTREE%/}/.orchestra-report.md" - -while true; do - sleep "$ORCHESTRA_POLL_INTERVAL" - - rollout="$(latest_rollout)" - [ -z "$rollout" ] && continue - - if [ -f "$report_file" ]; then - report="$(cat "$report_file")" - body="$(jq -n \ - --arg task_id "$ORCHESTRA_TASK_ID" \ - --arg transcript_path "$rollout" \ - --arg report "$report" \ - '{task_id: $task_id, harness: "codex", transcript_path: $transcript_path, report: $report}')" - - if curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/complete" \ - -H "Content-Type: application/json" \ - ${auth_header:+-H "$auth_header"} \ - -d "$body" >/dev/null 2>&1; then - rm -f "$report_file" - exit 0 - else - echo "orchestra-codex-poll: failed to report completion" >&2 - fi - continue - fi - - body="$(jq -n --arg task_id "$ORCHESTRA_TASK_ID" '{task_id: $task_id}')" - response="$(curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/turn" \ - -H "Content-Type: application/json" \ - ${auth_header:+-H "$auth_header"} \ - -d "$body" 2>/dev/null)" || { - echo "orchestra-codex-poll: failed to reach turn-decision endpoint" >&2 - continue - } - - decision="$(printf '%s' "$response" | jq -r '.decision // empty')" - if [ "$decision" = "refuse" ] || [ "$decision" = "rotate_now" ]; then - echo "orchestra-codex-poll: turn decision is $decision" >&2 - fi -done diff --git a/deploy/hooks/orchestra-opencode-poll.sh b/deploy/hooks/orchestra-opencode-poll.sh deleted file mode 100755 index 9624f81..0000000 --- a/deploy/hooks/orchestra-opencode-poll.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/sh -# opencode turn-boundary/completion poller — same rationale as -# orchestra-codex-poll.sh: opencode has no native Stop hook, so this runs as -# a background loop in the pane, polling /v1/harness/turn and watching for -# the .orchestra-report.md completion marker. -# -# opencode's own SSE session-status stream (OpenCodeStatus in -# internal/herdr/occupancy.go) is a fast path this script does not use — it -# would need the pane's session id, which isn't reliably known outside the -# opencode process. Instead this falls back to the same -# most-recently-written message file under opencode's storage dir that -# OpenCodeUsage already reads as its backstop (per AUDIT.md's "Real harness -# quota sources": "the SSE stream is not rock-solid"). -# -# Requires: ORCHESTRA_TASK_ID, ORCHESTRA_URL, ORCHESTRA_WORKTREE set in the -# pane's env. Optional: ORCHESTRA_HARNESS_TOKEN, -# ORCHESTRA_POLL_INTERVAL (seconds, default 60). -# Needs: jq, find, curl. - -set -u - -: "${ORCHESTRA_POLL_INTERVAL:=60}" -: "${ORCHESTRA_WORKTREE:=$PWD}" - -[ -z "${ORCHESTRA_TASK_ID:-}" ] && { echo "orchestra-opencode-poll: ORCHESTRA_TASK_ID not set" >&2; exit 1; } -[ -z "${ORCHESTRA_URL:-}" ] && { echo "orchestra-opencode-poll: ORCHESTRA_URL not set" >&2; exit 1; } - -auth_header="" -if [ -n "${ORCHESTRA_HARNESS_TOKEN:-}" ]; then - auth_header="Authorization: Bearer ${ORCHESTRA_HARNESS_TOKEN}" -fi - -latest_message() { - find "$HOME/.local/share/opencode/storage/message" -type f -name '*.json' -printf '%T@ %p\n' 2>/dev/null \ - | sort -rn | head -n1 | cut -d' ' -f2- -} - -report_file="${ORCHESTRA_WORKTREE%/}/.orchestra-report.md" - -while true; do - sleep "$ORCHESTRA_POLL_INTERVAL" - - msg="$(latest_message)" - [ -z "$msg" ] && continue - - if [ -f "$report_file" ]; then - report="$(cat "$report_file")" - body="$(jq -n \ - --arg task_id "$ORCHESTRA_TASK_ID" \ - --arg transcript_path "$msg" \ - --arg report "$report" \ - '{task_id: $task_id, harness: "opencode", transcript_path: $transcript_path, report: $report}')" - - if curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/complete" \ - -H "Content-Type: application/json" \ - ${auth_header:+-H "$auth_header"} \ - -d "$body" >/dev/null 2>&1; then - rm -f "$report_file" - exit 0 - else - echo "orchestra-opencode-poll: failed to report completion" >&2 - fi - continue - fi - - body="$(jq -n --arg task_id "$ORCHESTRA_TASK_ID" '{task_id: $task_id}')" - response="$(curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/turn" \ - -H "Content-Type: application/json" \ - ${auth_header:+-H "$auth_header"} \ - -d "$body" 2>/dev/null)" || { - echo "orchestra-opencode-poll: failed to reach turn-decision endpoint" >&2 - continue - } - - decision="$(printf '%s' "$response" | jq -r '.decision // empty')" - if [ "$decision" = "refuse" ] || [ "$decision" = "rotate_now" ]; then - echo "orchestra-opencode-poll: turn decision is $decision" >&2 - fi -done diff --git a/deploy/hooks/orchestra-stop.sh b/deploy/hooks/orchestra-stop.sh deleted file mode 100755 index 86f8aeb..0000000 --- a/deploy/hooks/orchestra-stop.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/sh -# Claude Code Stop hook — fires on every turn boundary, not just completion. -# Convention: the agent signals "this task is done" by writing a report file -# named .orchestra-report.md at the worktree root before stopping. If that -# marker is present, report completion. If it is absent, this is an ordinary -# turn boundary — ask the unified turn-decision endpoint (AUDIT.md B3, Phase -# 2 items 1-2) what to do instead of no-op'ing. -# -# Requires: ORCHESTRA_TASK_ID and ORCHESTRA_URL set in the pane's env. -# Optional: ORCHESTRA_HARNESS_TOKEN if the server requires one. -# -# Reads the Stop hook's JSON payload from stdin (has "transcript_path" and -# "cwd"); needs jq. - -set -eu - -payload="$(cat)" -transcript_path="$(printf '%s' "$payload" | jq -r '.transcript_path // empty')" -cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty')" - -[ -z "$transcript_path" ] && exit 0 -[ -z "${ORCHESTRA_TASK_ID:-}" ] && exit 0 -[ -z "${ORCHESTRA_URL:-}" ] && exit 0 - -auth_header="" -if [ -n "${ORCHESTRA_HARNESS_TOKEN:-}" ]; then - auth_header="Authorization: Bearer ${ORCHESTRA_HARNESS_TOKEN}" -fi - -report_file="${cwd:-.}/.orchestra-report.md" - -if [ -f "$report_file" ]; then - report="$(cat "$report_file")" - - body="$(jq -n \ - --arg task_id "$ORCHESTRA_TASK_ID" \ - --arg transcript_path "$transcript_path" \ - --arg report "$report" \ - '{task_id: $task_id, harness: "claude", transcript_path: $transcript_path, report: $report}')" - - if curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/complete" \ - -H "Content-Type: application/json" \ - ${auth_header:+-H "$auth_header"} \ - -d "$body" >/dev/null 2>&1; then - rm -f "$report_file" - else - echo "orchestra-stop: failed to report completion" >&2 - exit 2 - fi - exit 0 -fi - -body="$(jq -n --arg task_id "$ORCHESTRA_TASK_ID" '{task_id: $task_id}')" - -response="$(curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/turn" \ - -H "Content-Type: application/json" \ - ${auth_header:+-H "$auth_header"} \ - -d "$body" 2>/dev/null)" || { - echo "orchestra-stop: failed to reach turn-decision endpoint" >&2 - exit 0 -} - -decision="$(printf '%s' "$response" | jq -r '.decision // empty')" - -if [ "$decision" = "refuse" ]; then - echo "orchestra-stop: turn decision is refuse — this turn boundary is not safe to stop at" >&2 - exit 2 -fi - -exit 0 diff --git a/deploy/orchestra.env.example b/deploy/orchestra.env.example index 5943da1..d744235 100644 --- a/deploy/orchestra.env.example +++ b/deploy/orchestra.env.example @@ -1,6 +1,8 @@ -# Copy to /etc/orchestra/orchestra.env (chmod 600, owned by the orchestra -# user) and fill in the values you need. Referenced by orchestra.service via -# EnvironmentFile=. Every var below is read directly from os.Getenv in +# Copy to the compose directory as `.env` (chmod 600) and fill in the values +# you need; `compose.yaml` loads it via `env_file:`. The retired +# orchestra.service EnvironmentFile= path is gone as of 2026-07-31, and the +# container has no entrypoint script that sources an env file — compose passes +# these in directly. Every var below is read directly from os.Getenv in # cmd/orchestra/main.go and the packages it wires up — grep ORCHESTRA_ in the # repo if this list ever needs re-deriving. diff --git a/deploy/orchestra.service b/deploy/orchestra.service deleted file mode 100644 index 1e56db6..0000000 --- a/deploy/orchestra.service +++ /dev/null @@ -1,25 +0,0 @@ -[Unit] -Description=Orchestra task orchestrator -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=orchestra -Group=orchestra -WorkingDirectory=/var/lib/orchestra -EnvironmentFile=/etc/orchestra/orchestra.env -ExecStart=/usr/local/bin/orchestra -Restart=on-failure -RestartSec=5s -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -PrivateTmp=true -ReadWritePaths=/var/lib/orchestra -# ORCHESTRA_WORKTREE_ROOT / per-project worktree_root paths and -# ORCHESTRA_REPO must live under one of these, or under /var/lib/orchestra — -# add further ReadWritePaths= lines here if you keep repos elsewhere. - -[Install] -WantedBy=multi-user.target diff --git a/deploy/redeploy.sh b/deploy/redeploy.sh deleted file mode 100755 index eb835e8..0000000 --- a/deploy/redeploy.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" -tmp_bin="$(mktemp)" -trap 'rm -f -- "$tmp_bin"' EXIT - -cd "$repo_dir" -echo "Building Orchestra..." -revision="$(git rev-parse HEAD)" -build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)" -dirty=false -if [[ -n "$(git status --porcelain)" ]]; then dirty=true; fi -go build -ldflags "-X orchestra/internal/buildinfo.Revision=$revision -X orchestra/internal/buildinfo.Time=$build_time -X orchestra/internal/buildinfo.Dirty=$dirty" -o "$tmp_bin" ./cmd/orchestra - -echo "Installing /usr/local/bin/orchestra..." -sudo install -o root -g root -m 0755 "$tmp_bin" /usr/local/bin/orchestra - -echo "Restarting orchestra.service..." -sudo systemctl restart orchestra.service -sudo systemctl --no-pager --lines=8 status orchestra.service