3 Commits

Author SHA1 Message Date
kami 6cb2f932d8 Go and read the reply to a question Orchestra asked
F64. A plan mismatch that asks for a human decision blocks the task, and
nothing came back. Two independent gaps, either one enough to strand it:
the reconciler ran only before a lease and at a turn boundary, so a blocked
task's reply was never even read, and ResumeAnsweredBlockers listed two
block reasons, not this one. PlanMismatchAnswered had no caller anywhere.

Blocked tasks awaiting a reply are now reconciled on their own loop, the
same reasoning the submitted-work loop above it already uses: a task that
cannot be leased cannot be reconciled behind a pre-lease hook. One
predicate, BlockReasonAwaitsReply, now names the set for both loops so they
cannot drift apart again.

The existing test asserted the predicate and never the resume, which is how
this survived. It asserts the resume now, and fails without the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 19:57:11 +04:00
kami b5c37f693b Bind a manual sign-off to the tree it was given against
F63, found live on run 19. A manual check on these projects is a human
reading what the code prints. RecordPlanPhaseVerification asked only
whether a sign-off for that plan and phase existed, and one exists forever,
so rerunning a phase's automated checks at a new commit carried the human
half along with it. The rig proved it twice: two operator commits and two
re-verification requests, each coming back verified without anyone looking.

The reducer now records which tree the human confirmed, the record carries
it forward as provenance, and a run whose commit does not match it waits
for the human again. A sign-off given before any run has no confirmed tree
and still counts, so the ordinary ordering is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 19:41:50 +04:00
kami bbf3335857 Record run 19: the manual verification seam, and F63
The keyed seam holds. Automated checks alone leave a phase awaiting, a
generic approving comment satisfies nothing, and the keyed form verifies
exactly the phase it names under the plan ref Orchestra supplies. A
re-verification request reruns the plan's own commands at the new tree and
writes a record bound to it.

F63 is what the rig found. A manual sign-off is consulted by existence
alone, so it survives every later tree the human never saw, while the
automated half is re-established at each new SHA. The proposed fix stamps
the satisfied record's sha on the decision and requires it to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 19:25:21 +04:00
39 changed files with 889 additions and 20 deletions
+88
View File
@@ -2760,3 +2760,91 @@ observed without being asked for.
worker's tick save can clobber the edit. It clobbered run 17's attempt and
survived run 18's. Stop, edit, start is the reliable sequence, and it needs
`systemctl stop` and `start` in the unattended sudo rule.
## Run 19, 2026-08-29: the manual verification seam, and F63
Two tasks, both ingested from Gitea so that comments reconcile:
`06G4W2TMZ9MW9SEBM5MGTM5288` (issue 24, plan `500464b9`) and
`06G4W63545T3RV8SSKGKN11B3G` (issue 26, plan `53f7918b`).
A burn-in task cannot test this rung. Comments reconcile only for the source
the task came from, so a task created with `source: "burnin"` has no comment
stream at all.
### Proven: the keyed seam
```text
15:09:15 PlanPhaseVerified phase-1 awaiting_manual_verification at 80728967
comment: "Looks good to me. Nice work on this one, ship it."
17:10:03 HumanDecisionRecorded subject=operator_instruction
phase-1 unchanged, still awaiting_manual_verification
comment: "orchestra verify phase-1"
17:10:25 HumanDecisionRecorded subject=plan_phase_verification:500464b9…:phase-1
phase-1 verified
```
All five phases reached `awaiting_manual_verification` on their automated
checks alone. A generic approval satisfied nothing. The keyed form flipped
exactly the phase it named, under the plan ref Orchestra supplied rather than
one the commenter chose.
### Proven: re-verification at a new SHA
On issue 26, inside one live implement lease, with the worker stopped for the
whole edit:
```text
15:23:00 PlanPhaseVerified phase-1 awaiting_manual_verification at f5d8f90b
operator commit moves the tree f5d8f90b -> 1f07b89c
.orchestra/plan-progress.json requests phase-1 again
15:23:17 PlanPhaseVerified phase-1 awaiting_manual_verification at 1f07b89c
17:23:16 HumanDecisionRecorded subject=plan_phase_verification:53f7918b…:phase-1
phase-1 verified at 1f07b89c
```
The rerun ran the plan's own commands at the new tree and wrote a fresh record
bound to it. The worker never reads a command out of the request: the request
names a phase and nothing else about it is trusted.
### F63: a manual sign-off outlives the tree it was given against
The second half of the same rig, with no new comment:
```text
operator commit moves the tree 1f07b89c -> 28af84ff
.orchestra/plan-progress.json requests phase-1 again
15:24:02 PlanPhaseVerified phase-1 verified at 28af84ff
```
`RecordPlanPhaseVerification` consults `manuallySignedOff`
(`internal/operations/planprogress.go:127,152`), which asks only whether a
decision with that subject exists. It does, forever, for that plan ref and
phase id. So the automated half of a phase is re-established at every new SHA
while the manual half is asserted from a tree the human never saw.
This is the same class of bug `AtSHA` exists to prevent, applied to the half
that `AtSHA` does not cover. A manual check on this project is a human reading
printed output; an edit between the sign-off and the rerun can change exactly
that output.
Only a replan clears it, because `PlanPhases()` drops records whose plan ref is
not the accepted one.
Proposed shape, not built: when the reducer flips a record on a keyed decision
(`internal/store/store.go:273`), it knows the record it satisfied and that
record's `AtSHA`. Stamp that sha on the decision, and have `manuallySignedOff`
require it to match the run being recorded. A sign-off that arrives before any
run has no sha and keeps today's behaviour, so the ordinary ordering is
unchanged.
### Also established
- `review -> implement` is legal in `legalPhaseTransitions`, but
`AdvanceWorkPhase` only walks the project's path forward. A task that has
left implement can return only through a review that requests changes, not
through the operator phase endpoint.
- The agent's re-verification request is a file an operator can write:
`.orchestra/plan-progress.json`, `{"phase": "...", "status":
"ready_for_verification"}`. It is the whole lever for this rung.
- Ingest turns every issue label into a required capability, so a rig issue
must carry no labels or it will not schedule.
+17
View File
@@ -0,0 +1,17 @@
# orchestra ui mockups
`final/` contains the nine accepted/current screen directions:
1. dashboard
2. task detail
3. terminal live pane
4. tasks
5. decisions
6. workers
7. projects
8. review
9. settings
`iterations/` contains every generated mockup from the design session, including superseded variants.
`orchestra-ui-spec.md` is the accompanying implementation/design specification.
+19
View File
@@ -1831,6 +1831,25 @@ func main() {
coordinator.ReconcileFailureHandoff = v
}
}
// The third reconciliation point, and the same reasoning the submitted
// work loop above already uses: a blocked task cannot be leased, so a
// pre-lease hook can never see the reply that should unblock it (F64).
// Orchestra stopped and asked the human a question; nothing else here
// will go and read the answer.
go func() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
for _, t := range s.Tasks() {
if t.State != domain.StateBlocked || !domain.BlockReasonAwaitsReply(t.BlockReason) {
continue
}
if err := reconciler.Reconcile(context.Background(), t.ID); err != nil {
log.Printf("reconcile blocked task %s: %v", t.ID, err)
}
}
}
}()
}
if path := os.Getenv("ORCHESTRA_JSONL"); path != "" {
sup := &provider.Supervisor{Name: "jsonl", Run: func(ctx context.Context) error {
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+26 -9
View File
@@ -82,6 +82,23 @@ const (
BlockReasonUnknown BlockReason = "unknown"
)
// BlockReasonAwaitsReply reports whether this stop is a question waiting on a
// human, rather than a fault or a budget. Every reason listed here resumes on
// a reply, so both the loop that goes and reads replies and the loop that
// returns answered tasks to the queue must agree on the set. They did not:
// plan_mismatch blocked for an answer that nothing ever went to fetch, and
// nothing would have resumed it if it had (F64).
//
// operator_required is deliberately absent. A task that has spent its question
// budget continues when an operator says so, not when someone replies.
func BlockReasonAwaitsReply(r BlockReason) bool {
switch r {
case BlockReasonHumanDecision, BlockReasonTrajectoryGate, BlockReasonPlanMismatch:
return true
}
return false
}
func (r BlockReason) Valid() bool {
switch r {
case BlockReasonLeaseFailure, BlockReasonWorkerOffline, BlockReasonLeaseExpired,
@@ -168,19 +185,19 @@ type Task struct {
QualityGate string `json:"quality_gate,omitempty"`
// Block evidence is projected from TaskBlocked so terminal records remain
// diagnosable after the live coordinator mapping is gone.
Blocker string `json:"blocker,omitempty"`
BlockReason BlockReason `json:"block_reason,omitempty"`
BlockedAt time.Time `json:"blocked_at,omitempty"`
LastPaneID string `json:"last_pane_id,omitempty"`
LastHarness string `json:"last_harness_id,omitempty"`
Blocker string `json:"blocker,omitempty"`
BlockReason BlockReason `json:"block_reason,omitempty"`
BlockedAt time.Time `json:"blocked_at,omitempty"`
LastPaneID string `json:"last_pane_id,omitempty"`
LastHarness string `json:"last_harness_id,omitempty"`
// LastLeaseEpoch is the fencing token of the lease that most recently
// ended. A worker can push its release anchor and only then discover the
// lease expired; the finished work is durable in git but the commit can
// never land. Retaining the epoch lets exactly that owner still commit
// while the task sits unleased.
LastLeaseEpoch string `json:"last_lease_epoch,omitempty"`
PaneState string `json:"pane_state,omitempty"` // open, closed, unreachable, unknown
LastSession SessionEvidence `json:"last_session,omitempty"`
LastLeaseEpoch string `json:"last_lease_epoch,omitempty"`
PaneState string `json:"pane_state,omitempty"` // open, closed, unreachable, unknown
LastSession SessionEvidence `json:"last_session,omitempty"`
// Recovery state is part of the durable projection, never process-local
// router memory. This makes retry and operator diagnostics survive a
// coordinator restart.
@@ -217,7 +234,7 @@ type Task struct {
// current one, oldest first. A superseded plan stays queryable: the
// verification recorded against it is provenance, not garbage.
PlanHistory []string `json:"plan_history,omitempty"`
LastError string `json:"last_error,omitempty"`
LastError string `json:"last_error,omitempty"`
}
// ReviewRef binds a sealed review artifact to one commit.
+7
View File
@@ -55,6 +55,13 @@ type PlanPhaseRecord struct {
// EvidenceRef is the CAS ref of the captured command output.
EvidenceRef string `json:"evidence_ref,omitempty"`
At time.Time `json:"at"`
// ManualAtSHA is the tree a human was actually looking at when they signed
// this phase off. A manual check on most projects is a human reading
// output, so a sign-off establishes something about one tree and nothing
// about the next one (F63). Rerunning the automated half re-establishes it
// at the new commit; the manual half has to be given again, and this is
// what makes the difference visible instead of assumed.
ManualAtSHA string `json:"manual_at_sha,omitempty"`
}
// Stale reports whether the tree has moved since this phase was verified. A
+3 -5
View File
@@ -107,11 +107,9 @@ func ResumeAnsweredBlockers(s *store.Store) ([]domain.Event, error) {
if t.State != domain.StateBlocked {
continue
}
switch t.BlockReason {
case domain.BlockReasonHumanDecision, domain.BlockReasonTrajectoryGate:
default:
// operator_required is deliberately not resumed by a reply. An
// operator decides when a task that spent its budget continues.
// operator_required is deliberately not resumed by a reply. An
// operator decides when a task that spent its budget continues.
if !domain.BlockReasonAwaitsReply(t.BlockReason) {
continue
}
if !blockerAnswered(s, t.ID, t.BlockReason) {
+9
View File
@@ -215,7 +215,16 @@ func TestHumanAnswerResumesTheSamePlanWithoutResealing(t *testing.T) {
if !PlanMismatchAnswered(s, id) {
t.Fatal("the human answered and the task is still waiting")
}
// F64: asserting the predicate is not asserting the resume. This test
// passed for as long as the predicate had no caller, while a task blocked
// on a mismatch stayed blocked forever however the human replied.
if events, err := ResumeAnsweredBlockers(s); err != nil || len(events) != 1 {
t.Fatalf("an answered mismatch did not return to the queue: events=%v err=%v", events, err)
}
after, _ := s.Task(id)
if after.State != domain.StateQueued {
t.Fatalf("state = %s after the human answered, want queued", after.State)
}
if after.PlanRef != planRef {
t.Fatal("answering the question replaced the plan")
}
+21 -5
View File
@@ -124,7 +124,12 @@ func RecordPlanPhaseVerification(s *store.Store, project registry.Project, taskI
// was established.
record.Status = domain.PlanPhaseAwaitingManual
}
if record.Status != domain.PlanPhaseInProgress && manuallySignedOff(s, t, phaseID) {
// Carry the confirmed tree forward as provenance. Without it a second
// rerun would compare against nothing and re-inherit the sign-off.
if prior, ok := t.PlanPhase(phaseID); ok {
record.ManualAtSHA = prior.ManualAtSHA
}
if record.Status != domain.PlanPhaseInProgress && manuallySignedOff(s, t, phaseID, record) {
record.Status = domain.PlanPhaseVerified
}
if ref, err := s.PutArtifact(verificationEvidence(runs)); err == nil {
@@ -135,7 +140,7 @@ func RecordPlanPhaseVerification(s *store.Store, project registry.Project, taskI
payload := map[string]any{
"plan_ref": record.PlanRef, "phase_id": record.PhaseID, "status": string(record.Status),
"commands": record.Commands, "exit_codes": record.ExitCodes, "at_sha": record.AtSHA,
"evidence_ref": record.EvidenceRef, "at": record.At,
"evidence_ref": record.EvidenceRef, "at": record.At, "manual_at_sha": record.ManualAtSHA,
}
if t.Lease != nil {
payload["harness_id"], payload["lease_epoch"] = t.Lease.HarnessID, t.Lease.Epoch
@@ -149,9 +154,20 @@ func RecordPlanPhaseVerification(s *store.Store, project registry.Project, taskI
}
// manuallySignedOff reports whether a human has already approved this exact
// phase of this exact plan. The subject carries both, so a later "looks good"
// on an unrelated thread cannot satisfy a gate nobody was discussing.
func manuallySignedOff(s *store.Store, t domain.Task, phaseID string) bool {
// phase of this exact plan, against the tree this run examined. The subject
// carries plan and phase, so a later "looks good" on an unrelated thread
// cannot satisfy a gate nobody was discussing.
//
// The tree matters as much as the subject (F63). A sign-off is a human saying
// they read what this code prints; an edit afterwards can change exactly that.
// A record whose ManualAtSHA names a different commit is therefore not signed
// off, and waits for the human again. A sign-off given before any run has no
// confirmed tree to compare against and still counts, which keeps the ordinary
// ordering unchanged.
func manuallySignedOff(s *store.Store, t domain.Task, phaseID string, record domain.PlanPhaseRecord) bool {
if record.ManualAtSHA != "" && record.ManualAtSHA != record.AtSHA {
return false
}
intent, err := s.EffectiveIntent(t.ID)
if err != nil {
return false
+61 -1
View File
@@ -13,6 +13,7 @@ import (
const shaOne = "1111111111111111111111111111111111111111"
const shaTwo = "2222222222222222222222222222222222222222"
const shaThree = "3333333333333333333333333333333333333333"
func planProject() registry.Project {
p := registry.Project{
@@ -251,6 +252,14 @@ func TestLegacyPlanIsExplicitlyNonProgressable(t *testing.T) {
// signOff records a human decision bound to one phase of one plan, which is
// the only thing that satisfies a manual verification gate.
func signOff(t *testing.T, s *store.Store, taskID, subject string) {
t.Helper()
signOffFrom(t, s, taskID, subject, "signoff-"+subject)
}
// signOffFrom names the comment the sign-off came from. Two sign-offs on one
// subject are a real sequence once a rerun sends a phase back to the human,
// and provenance is unique per comment.
func signOffFrom(t *testing.T, s *store.Store, taskID, subject, externalID string) {
t.Helper()
task, _ := s.Task(taskID)
if err := s.Append(domain.Event{
@@ -259,9 +268,60 @@ func signOff(t *testing.T, s *store.Store, taskID, subject string) {
Payload: mustJSONBytes(t, map[string]any{
"decision_id": domain.NewID(), "kind": "answer", "subject": subject,
"value": "manual steps confirmed",
"source": map[string]any{"provider": "gitea", "external_id": "signoff-" + subject},
"source": map[string]any{"provider": "gitea", "external_id": externalID},
}),
}); err != nil {
t.Fatalf("sign off: %v", err)
}
}
// F63, found live on run 19. A manual sign-off says a human read what this
// code prints. An edit afterwards can change exactly that, so rerunning the
// automated half at a new commit must not carry the human half with it.
func TestASignOffDoesNotSurviveTheTreeItWasGivenAgainst(t *testing.T) {
s, project, id := planWith(t, twoPhasePlan)
run := []VerificationRun{{Command: []string{"go", "test", "./internal/..."}, ExitCode: 0}}
if _, err := RecordPlanPhaseVerification(s, project, id, "phase-2", shaOne, run); err != nil {
t.Fatal(err)
}
task, _ := s.Task(id)
signOff(t, s, id, domain.PlanPhaseSubject(task.PlanRef, "phase-2"))
task, _ = s.Task(id)
rec, _ := task.PlanPhase("phase-2")
if rec.Status != domain.PlanPhaseVerified || rec.ManualAtSHA != shaOne {
t.Fatalf("sign-off did not bind to the tree it read: %+v", rec)
}
// The tree moves and the phase is re-verified. The commands pass again;
// the human has not seen the new output.
if _, err := RecordPlanPhaseVerification(s, project, id, "phase-2", shaTwo, run); err != nil {
t.Fatal(err)
}
task, _ = s.Task(id)
rec, _ = task.PlanPhase("phase-2")
if rec.Status != domain.PlanPhaseAwaitingManual {
t.Fatalf("status = %q at a tree the human never saw, want awaiting_manual_verification", rec.Status)
}
if rec.ManualAtSHA != shaOne {
t.Fatalf("the confirmed tree was lost: %+v", rec)
}
// A second rerun must not re-inherit it either, which is what carrying
// ManualAtSHA forward is for.
if _, err := RecordPlanPhaseVerification(s, project, id, "phase-2", shaThree, run); err != nil {
t.Fatal(err)
}
task, _ = s.Task(id)
rec, _ = task.PlanPhase("phase-2")
if rec.Status != domain.PlanPhaseAwaitingManual {
t.Fatalf("a second rerun re-inherited the sign-off: %q", rec.Status)
}
// Signing off again, on the tree that is now current, verifies it.
signOffFrom(t, s, id, domain.PlanPhaseSubject(task.PlanRef, "phase-2"), "signoff-second")
task, _ = s.Task(id)
rec, _ = task.PlanPhase("phase-2")
if rec.Status != domain.PlanPhaseVerified || rec.ManualAtSHA != shaThree {
t.Fatalf("a fresh sign-off did not verify the current tree: %+v", rec)
}
}
+3
View File
@@ -272,6 +272,9 @@ func (s *Store) apply(e domain.Event) error {
}
if p.Subject == domain.PlanPhaseSubject(rec.PlanRef, rec.PhaseID) {
t.PlanProgress.Phases[i].Status = domain.PlanPhaseVerified
// Record which tree the sign-off was about, so a later run
// at a different commit cannot inherit it (F63).
t.PlanProgress.Phases[i].ManualAtSHA = rec.AtSHA
t.Version = e.Version
s.replaceTask(e.TaskID, t)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+635
View File
@@ -0,0 +1,635 @@
# orchestra web control ui
status: mockup specification
app: orchestra
shared design system: ethos
accent: signal violet `#8F7AE5`
motif: routing fork
primary operator: single infrastructure operator
## 1. purpose
orchestra's web ui is an operator console for understanding and steering agent work without reconstructing state from logs, tmux sessions, gitea, or the event stream.
it has three jobs:
1. show what needs the operator now.
2. show what orchestra believes and what an agent is actually doing.
3. expose intervention and forensic detail without making the default screens noisy.
it is not a generic project-management product, an analytics dashboard, or a chat-first interface.
## 2. ethos application
orchestra inherits the ethos shell and neutral system unchanged.
### fingerprint
- accent: signal violet `#8F7AE5`
- motif: routing fork
- accent use: active navigation, focus, selected phase, primary operator action, routing/transition marks
- motif use: app mark, phase transitions, routing state, empty states, one hero/detail moment
suggested token block:
```css
[data-app="orchestra"] {
--accent: #8F7AE5;
--accent-hi: #A291EC;
--accent-dim: rgba(143, 122, 229, 0.14);
--accent-line: rgba(143, 122, 229, 0.32);
--accent-glow: rgba(143, 122, 229, 0.22);
}
```
### type
- geist sans: labels, task names, explanations, buttons, headings, operator-authored prose
- geist mono: task ids, epochs, worker ids, pane ids, timestamps, percentages, context counts, revisions, paths, refs, sha values, lease durations
- machine values must never silently use sans for visual convenience
### surfaces
- page room: `--bg-0`
- normal panel: `--bg-1`
- selected/raised content: `--bg-2`
- hover: `--bg-3`
- borders: 1px `--line`, `--line-hi` only where selection or risk needs stronger separation
- no blur or glass
- accent never washes a whole content panel
### motion
all ui transitions use ethos mechanical motion: 130170 ms, `cubic-bezier(0.2, 0, 0, 1)`, no spring or bounce.
## 3. shared shell
### desktop
- 64px vertical icon rail
- 56px top bar
- main content fills remaining viewport
- global `cmd+k` command/search surface in the top bar
- right side of top bar shows only high-value machine state: orchestra revision, live state, current time
primary navigation:
1. dashboard
2. tasks
3. decisions
4. workers
5. projects
6. review
7. settings
terminal is entered from a task or worker and does not need permanent primary navigation.
### mobile
- rail becomes bottom navigation with no more than five primary items
- secondary pages move behind task/project drill-down or header menus
- machine readouts relocate into detail screens rather than disappearing
- terminal remains a dedicated full-screen view
## 4. information hierarchy
orchestra should visually distinguish five classes of information.
| class | meaning | treatment |
|---|---|---|
| operator authority | human decision, correction, constraint | high prominence, accent signal |
| verified orchestra state | lifecycle, lease, phase, review, refs | neutral surfaces, machine values in mono |
| agent claims | proposed plan, handoff text, self-reported risk | visibly labeled as agent-supplied |
| historical/superseded | stale plan, previous handoff, old decision | muted, never visually dominant |
| fault/attention | blocked, refused, stale, mismatch | status color + explicit reason |
raw events and implementation details are drill-down evidence, not default-page content.
## 5. dashboard
mockup: `final/01-dashboard.png`
purpose: answer "what needs me, what is running, and is the system healthy?"
### layout
three primary regions only:
1. **needs your attention**
- trajectory gate
- blocking human decision
- pr awaiting review
- closed-without-merge/operator-required state
- one line explaining why each item needs the operator
2. **running tasks**
- task name/id
- current phase
- worker/harness
- last real progress
- context occupancy
- lease timing
- compact routing-fork phase indicator
3. **system state and capacity**
- workers online/available
- quota status
- active leases
- router queue/rejection state
- source reconciliation
- herdr health
### rules
- no disk, cpu, ram, or generic host-monitor metrics unless they directly block orchestration
- no vanity charts
- health should show actual counts/times, not vague "running" indicators
- dashboard actions should navigate to the relevant task/review/worker rather than becoming an operator-control surface itself
## 6. tasks list
mockup: `final/04-tasks.png`
purpose: browse all work under orchestra and locate a task quickly.
### groups
- needs attention
- running
- waiting/queued
- completed
- failed
### row fields
- task title/id
- project
- current phase
- compact routing-fork phase path
- worker/harness when assigned
- last update/progress
- context occupancy for active sessions
- status
### filters
- project
- phase
- worker/harness
- status
- attention state
avoid turning this into a kanban board. orchestra's lifecycle is stateful and event-backed; the list should expose state, not encourage arbitrary drag-and-drop mutation.
## 7. task detail
mockup: `final/02-task-detail.png`
purpose: provide one calm page answering "what are we doing?", "what is happening?", and "where are we in the workflow?"
### top area
- title
- task id
- project
- current phase
- steer/correct primary action
- secondary actions menu
### what we're doing
show only:
- goal
- acceptance summary
- latest effective human decisions when present
- active constraints when present
full contract, all decisions, and historical versions open in drill-down views.
### live execution
show only:
- worker/harness
- pane
- lease remaining
- context occupancy
- last progress
- one-line current activity
links:
- open live pane
- worker logs
- launch context
- git diff
### workflow
render the ace-fca path visibly:
`frame → research → plan → implement → review → pr → merged`
completed phases are quiet green, current phase uses the orchestra accent, future phases remain neutral.
phase detail, artifacts, refusal history, and raw events belong in drill-downs rather than the default task screen.
## 8. live pane / terminal
mockup: `final/03-terminal-live-pane.png`
purpose: show the actual tmux/herdr pane, not a reconstructed log view.
### behavior
- read-only by default
- exact pane dimensions and terminal rendering preserved when possible
- fit mode may scale visually but must not alter the underlying pane
- scrollback inspection must not disturb the live cursor
- `take control` explicitly enables keyboard forwarding
- strong visual state when control is captured
- escape or a fixed shortcut releases control
### why this screen exists
it must reveal real tui state such as:
- input still sitting in the editor
- queued input
- paste placeholders
- permission/approval dialogs
- `/clear`
- `@HANDOFF.md`
- agent busy/idle presentation
- tmux status line and pane identity
### surrounding chrome
keep it minimal:
- worker/pane identity
- read-only/control state
- fit/copy/scrollback controls
- lease epoch/timing
- context occupancy
- last verified progress
- pane/worker health
- small live event strip for orchestra-originated interactions and confirmations
when a pane is lost, preserve the last captured frame and state the exact loss time and lease consequence.
## 9. decisions list
mockup: `final/05-decisions.png`
purpose: inspect durable human authority and approvals across tasks.
this is not the human attention queue; that is review.
### list fields
- decision id
- short decision text
- task
- phase
- source/provenance
- requested/recorded time
- status: active, superseded, waiting, approved, rejected, auto
- operator/actor
### filters
- active/superseded
- kind: decision, correction, constraint, answer, scope change
- project
- task
- actor/source
### detail
opening a decision should show:
- exact value
- subject
- provenance
- supersedes/superseded-by links
- event sequence
- effective authority impact
machine provenance is mono; the human decision text remains sans.
## 10. workers
mockup: `final/06-workers.png`
purpose: answer "where can work run and is that execution machinery usable?"
### worker list fields
- worker/harness identity
- online/idle/busy/offline/degraded
- harness type
- current task when leased
- phase
- lease or heartbeat age
intentionally omit generic infrastructure metrics such as disk, memory, cpu, and uptime unless orchestra directly uses them for eligibility.
### worker detail
- current session/task
- pane id
- lease epoch/remaining
- last verified progress
- worker revision
- herdr status
- last heartbeat
- projects declared by the worker
- declared capabilities if orchestra actually consumes them
### capabilities
capabilities must come from worker registration/project configuration, never be guessed from host inspection.
the ui labels them **declared capabilities** and can expose registration provenance.
### registration
registration is worker-driven:
1. worker starts with coordinator credentials/configuration
2. worker announces identity, revision, harnesses, projects, capacity and declared capabilities
3. coordinator records it and heartbeat updates liveness
`add worker` should therefore be a registration guide/bootstrap flow, not a magical browser-side creation of a remote process.
## 11. projects
mockup: `final/07-projects.png`
purpose: group long-running repository/workflow configuration and its active work.
### project list
- project name
- short description
- state
- active/total task count
- attention/decision count
- last meaningful activity
avoid aggregate artifact counts or infrastructure metrics unless they help choose an action.
### project detail
- description
- source/repository identity
- configured worker/harness affinity
- workflow phases
- trajectory-gate policy
- quality gate
- human reconcile source
- safe-operation policy
- active tasks
- recent meaningful events
configuration edits belong in the project detail, while global defaults remain in settings.
## 12. review
mockup: `final/08-review.png`
purpose: the operator inbox: **what needs me to look at something and judge?**
workers show machinery. review shows human attention.
### review item types
- trajectory decision
- human blocker / decision request
- code/pr review
- ai review requiring operator override
- operator-required lifecycle state
- closed pr without merge
- information/fyi, optionally hidden by default
### list fields
- type
- task/item
- why it is here
- project
- priority
- age
worker/pane is secondary metadata, not a primary column.
### detail drawer/page
show the minimum evidence needed to act:
- why this needs the operator
- effective task goal/constraints relevant to the decision
- proposed options when applicable
- relevant plan/diff/review evidence
- provenance and age
- comments/context drill-down
primary action names must match the lifecycle action: make decision, answer blocker, review pr, request changes, retry, etc.
snooze/defer may exist for attention management but must not silently mutate underlying task truth.
## 13. settings
mockup: `final/09-settings.png`
purpose: configure orchestra itself, project-independent defaults, integrations, policies, and operator/security settings.
### tabs
1. general
2. appearance
3. notifications
4. access & security
5. integrations
6. agents & tools
7. policies
8. advanced
### general
- instance name/environment
- locale/time display
- default task preferences
- default workflow policy
- event/artifact retention
- storage/backup state only if orchestra owns it
### appearance
- theme: system/dark/light
- ethos remains fixed; no arbitrary shell redesign controls
- accessibility and reduced motion
### access & security
- surface tokens/status
- trusted review actors
- ignored actors
- operator sessions
- agent-surface auth
- secret/config source status without exposing secret values
### integrations
- gitea
- vikunja
- notification sinks
- source reconciliation state
show endpoint/identity and last successful interaction in mono.
### agents & tools
- harness definitions
- adapter/runtime configuration
- tmux/herdr ownership
- default launch behavior
- context occupancy thresholds
### policies
- work phase defaults
- trajectory gate
- bounded decision request budget
- retry policy
- reconcile failure handoff threshold
- quality/review/submission policy
### advanced
- event-store diagnostics
- snapshots/replay
- raw configuration
- migration/status information
- destructive operator actions in a visually separated danger zone
## 14. global command surface
`cmd+k` is the shared ethos command surface and should handle navigation plus safe operator actions.
examples:
- open task by id/title
- open worker/pane
- show decisions for task
- steer/correct active task
- answer current blocker
- retry failed task
- open review item
high-risk lifecycle actions require explicit confirmation and should not be the first fuzzy-search result for ordinary text.
## 15. status grammar
status is compact and consistent across screens.
recommended semantic colors:
- green: verified healthy/completed/accepted
- violet: current/selected/orchestra-controlled active state
- amber: waiting/degraded/operator attention
- red: failed/refused/danger
- gray: idle/unknown/historical
color never carries the entire meaning; every state also has text/iconography.
## 16. evidence drill-down
any important derived claim should be traceable without ssh.
from relevant detail screens the operator should be able to reach:
- event(s) that established the state
- exact generated launch context
- sealed research/plan/review/submission artifact refs
- git sha/ref
- worker journal observations
- input confirmation receipt
- pane capture/terminal
- source comment/review provenance
raw evidence is accessible but not visible by default.
## 17. copy rules
- labels describe what the operator controls, not implementation class names
- machine values remain exact and honest
- errors state what failed and what consequence follows
- no "something went wrong"
- no spinner where a queue depth, heartbeat age, poll timestamp, retry time, or progress value exists
examples:
- `source reconcile failed · retry at 14:06:00`
- `worker heartbeat 42s old · ineligible after 60s`
- `phase request refused · next phase is research`
- `pane lost at 13:42:18 · lease expires in 09:14`
## 18. responsive behavior
### tablet/mobile priorities
keep visible:
1. attention state
2. task title/phase
3. latest human authority
4. live execution state
5. primary action
collapse or drill down:
- long acceptance lists
- complete workflow history
- detailed lease metadata
- worker journal
- raw events
- project configuration
terminal becomes its own full-screen route and should not be squeezed into a card.
## 19. implementation order
recommended web-ui build order:
1. ethos shell + orchestra tokens/motif
2. dashboard
3. tasks list
4. task detail
5. live pane
6. review inbox
7. decisions list/detail
8. workers
9. projects
10. settings
11. global command surface
12. evidence/raw-event drill-down
build each screen against real orchestra api data rather than static ui-only state as early as practical.
## 20. verification
for every screen:
- render and visually inspect pixels
- check desktop and <=640px mobile layout
- assert no horizontal overflow
- verify mono/sans split visibly
- verify accent is signal-only
- verify no blur/backdrop-filter exists
- verify empty/error/loading states show real machine state
- compare implemented screen against the corresponding mockup as direction, not pixel-perfect contract
mockups communicate hierarchy, density, and interaction intent. orchestra state and the ethos design laws are the actual specification.