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
+5
View File
@@ -44,6 +44,11 @@ credential: its `build` object is the coordinator provenance. `GET
/v1/federation/workers` shows every worker's `build`, supported projects, and
worker-local health without SSH.
Build both binaries with `deploy/build.sh`, which stamps them from one commit
and refuses a dirty tree. A burn-in run must never pair a new coordinator with
an old worker, and matching revisions are how that is checked rather than
assumed.
For the Docker coordinator deployment, provide the same provenance as build
arguments (the Dockerfile intentionally cannot read `.git` from its build
context):
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# Build the coordinator and the worker from one commit, with one stamp, so a
# burn-in run can never pair a new coordinator with an old worker. Both
# binaries then report the same revision at /v1/admin/diagnostics and in the
# worker's registration, which is what makes deployed identity evidence rather
# than assumption.
#
# Usage: deploy/build.sh [outdir]
set -eu
out=${1:-./build}
cd "$(dirname "$0")/.."
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "refusing to stamp a dirty tree with a commit revision" >&2
exit 1
fi
rev=$(git rev-parse HEAD)
built=$(git show -s --format=%cI HEAD)
flags="-s -w -X orchestra/internal/buildinfo.Revision=$rev -X orchestra/internal/buildinfo.Time=$built -X orchestra/internal/buildinfo.Dirty=false"
mkdir -p "$out"
go build -trimpath -ldflags="$flags" -o "$out/orchestra" ./cmd/orchestra
go build -trimpath -ldflags="$flags" -o "$out/orchestra-worker" ./cmd/orchestra-worker
echo "$rev"
+17 -3
View File
@@ -16,6 +16,20 @@
"worktree_root": "/var/lib/orchestra/worktrees/correx", // Optional per-project worktree dir.
// Overrides global ORCHESTRA_WORKTREE_ROOT.
"quality_gate": "go test ./... && go vet ./..." // Worker runs this before deterministic delivery.
,
// Cognitive phase path. Omit for the default
// frame -> research -> plan -> implement -> review. A phase left out is
// skipped, which is how a trivial project runs frame/implement/review.
"work_phases": ["frame", "research", "plan", "implement", "review"],
// Phase transitions the human must confirm before work continues. The
// task blocks with block_reason "trajectory_gate" and the decision
// packet arrives on the usual notification surfaces. Any reply is
// recorded as a human decision and outranks the sealed plan.
"trajectory_gate": { "plan_to_implement": "required" },
// Per-task budget for bounded questions to the human. Default 6. Once
// spent, the task blocks with block_reason "operator_required" and a
// reply no longer resumes it, so a task cannot become an interview.
"human_decisions": { "max_requests_per_task": 6 }
},
{
"id": "maven",
@@ -35,6 +49,7 @@
{
"id": "mainframe-claude-1", // Unique herdr id.
"machine_id": "mainframe", // Which machine (above) this herdr runs on.
"backend": "herdr", // Pane backend: "herdr" (default) or "tmux" (Claude only).
// "address" omitted: falls back to the parent machine's address (used here since
// this herdr's harness listens on the machine's default port).
"harness": "claude", // Harness adapter to use: "claude" | "codex" | "opencode".
@@ -50,10 +65,9 @@
{
"id": "satellite-claude-1",
"machine_id": "satellite",
"address": "10.0.0.11:9245", // Explicit override: this herdr listens on a non-default
// port on its machine (e.g. multiple herdrs per machine).
"backend": "tmux", // tmux entries are always worker-owned, including on the
// coordinator machine; the worker reports local health.
"harness": "claude",
"protocol": "1",
"capabilities": ["code"],
"concurrency": 1,
"quota_limit_5h": 20,
+65
View File
@@ -17,9 +17,40 @@ ORCHESTRA_PORT=9145
# ORCHESTRA_FEDERATION_ADMIT_TOKEN=<homesrv-admission-secret>
# ORCHESTRA_WORKER_HERDR_ID=workpc-opencode
# ORCHESTRA_WORKER_HARNESS=opencode
# Pane/process backend. Defaults to herdr. tmux is currently supported only
# for Claude Code workers; Codex and OpenCode should remain on herdr.
# ORCHESTRA_WORKER_BACKEND=herdr
# ORCHESTRA_WORKER_HERDR=/home/orchestra/.config/herdr/herdr.sock
# With ORCHESTRA_WORKER_BACKEND=tmux, ORCHESTRA_WORKER_HERDR is not used.
# A bare value selects an isolated tmux -L socket; an absolute path selects -S.
# ORCHESTRA_WORKER_TMUX_SOCKET=orchestra
# Optional Claude executable override; defaults to resolving `claude` in PATH.
# ORCHESTRA_WORKER_HARNESS_COMMAND=/home/orchestra/.local/bin/claude
# ORCHESTRA_WORKER_STATE=/var/lib/orchestra-worker/state.json
# ORCHESTRA_GIT_REMOTE=origin
#
# One worker process can serve several harnesses (several multiplexers and
# several agents) by declaring them in a JSON file. The variables above then
# describe nothing: each entry carries its own backend and harness. Every
# declared harness is a separate federation identity, because the coordinator
# authorizes lease calls by comparing the worker id against the lease's harness
# id, so each needs its own token and registers separately.
# ORCHESTRA_WORKER_HARNESS_CONFIG_FILE=/etc/orchestra/harnesses.json
# [
# {"id":"workpc-claude","harness":"claude","backend":"tmux",
# "tmux_socket":"orchestra","command":"/home/kami/.local/bin/claude"},
# {"id":"workpc-opencode","harness":"opencode","backend":"herdr",
# "herdr":"127.0.0.1:9247"}
# ]
# Tokens belong in the environment rather than that file. Each id maps to
# ORCHESTRA_WORKER_TOKEN_<ID>, uppercased with every other character underscored:
# ORCHESTRA_WORKER_TOKEN_WORKPC_CLAUDE=<per-identity-secret>
# ORCHESTRA_WORKER_TOKEN_WORKPC_OPENCODE=<per-identity-secret>
# Each identity keeps its own state file. Set the directory holding them, or a
# per-entry "state" path; the default derives one per id next to the worktrees.
# ORCHESTRA_WORKER_STATE_DIR=/var/lib/orchestra-worker
# ORCHESTRA_WORKER_ID still names the process for the single-harness form, where
# it must equal ORCHESTRA_WORKER_HERDR_ID.
ORCHESTRA_MACHINE_ID=homesrv # required when the registry has multiple machines
# Static project/machine/herdr topology (registry.Load). Required for
@@ -70,6 +101,30 @@ ORCHESTRA_CONTEXT_WINDOW=200000
#ORCHESTRA_GITEA_REPO=
#ORCHESTRA_GITEA_WEBHOOK_SECRET=
# Human-input reconciliation. When a Gitea source is configured, issue
# comments are imported as human decisions immediately before every lease,
# and a source that cannot be read refuses the lease instead of letting a
# successor resume from an older intent. Set to "off" only to keep leasing
# while a source is known down; agents then run without newer comments.
#ORCHESTRA_HUMAN_RECONCILE=off
# How many consecutive failed turn-boundary reconciles ask the running session
# to hand off. Default 3. The successor's pre-lease reconcile then fails closed
# while the source is still down, so the task waits instead of running on
# intent Orchestra cannot refresh.
#ORCHESTRA_RECONCILE_FAILURE_HANDOFF=3
# Base branch for pull requests created by `task pr`. Defaults to master. The
# pull request is created or updated for the task branch orchestra/<task-id>,
# never duplicated: a repeated submission refreshes the same review.
#ORCHESTRA_PR_BASE=master
# Whose words on a submitted pull request may reopen a task. ACTORS is an
# allow-list of forge logins; empty trusts anyone not ignored, which is only
# safe on a private forge with no bots. IGNORE_ACTORS always loses. Comments at
# or before the submission never reopen anything.
#ORCHESTRA_REVIEW_ACTORS=kami
#ORCHESTRA_REVIEW_IGNORE_ACTORS=gitea-actions,orchestra-bot
# --- Delivery (notify-only surfaces) ---
# Telegram: both required together.
#ORCHESTRA_TELEGRAM_BOT_TOKEN=
@@ -95,6 +150,16 @@ ORCHESTRA_WEB_PASSWORD_HASH=
#ORCHESTRA_UI_INSECURE_COOKIE=1
#ORCHESTRA_MCP_TOKEN=
#ORCHESTRA_MAVEN_TOKEN=
# The credential an in-pane coding session may hold. It buys read access plus
# the three request endpoints (approval, decision-request, deferred) and
# nothing else: phase, review, submission, completion and lease changes are
# refused for this surface at the endpoint and again at the bus. This is the
# ONLY Orchestra credential that may enter an agent pane; the forge, Vikunja,
# worker and operator tokens must stay in the worker.
#ORCHESTRA_AGENT_TOKEN=
# The turn-boundary endpoint's own token (/v1/harness/turn), authenticated in
# the handler rather than by the surface gate.
#ORCHESTRA_HARNESS_TOKEN=
# Inbound bearer tokens for the notify-only surfaces, separate from the
# credentials used to *send* (ORCHESTRA_TELEGRAM_BOT_TOKEN, ORCHESTRA_NTFY_TOKEN).
# S12: ORCHESTRA_NTFY_TOKEN used to serve both roles, so configuring ntfy