feat: start provider ingestion and orchestration coordination
This commit is contained in:
@@ -245,6 +245,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
if path := os.Getenv("ORCHESTRA_JSONL"); path != "" {
|
||||||
|
go func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
err := (provider.JSONLWatcher{Path: path, Interval: time.Second, Provider: provider.JSONL{Source: "jsonl"}}).Run(ctx, s)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("jsonl provider: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
port := os.Getenv("ORCHESTRA_PORT")
|
port := os.Getenv("ORCHESTRA_PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "9145"
|
port = "9145"
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// Package orchestrator connects router lease events to an opaque herdr
|
||||||
|
// session. It is deliberately small: scheduling remains in router and the
|
||||||
|
// adapter remains the only component that knows how to drive a harness.
|
||||||
|
package orchestrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"orchestra/internal/domain"
|
||||||
|
"orchestra/internal/herdr"
|
||||||
|
"orchestra/internal/store"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Worktrees interface {
|
||||||
|
Create(context.Context, domain.Task) (string, error)
|
||||||
|
}
|
||||||
|
type Adapters interface {
|
||||||
|
Adapter(string) (herdr.Adapter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinator struct {
|
||||||
|
Store *store.Store
|
||||||
|
Worktrees Worktrees
|
||||||
|
Adapters Adapters
|
||||||
|
mu sync.Mutex
|
||||||
|
sessions map[string]herdr.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Coordinator) Start(ctx context.Context, e domain.Event) error {
|
||||||
|
if e.Type != "TaskLeased" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Store == nil || c.Worktrees == nil || c.Adapters == nil {
|
||||||
|
return fmt.Errorf("orchestrator: dependencies required")
|
||||||
|
}
|
||||||
|
t, ok := c.Store.Task(e.TaskID)
|
||||||
|
if !ok {
|
||||||
|
return domain.ErrNotFound
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
HarnessID string `json:"harness_id"`
|
||||||
|
HandoffRef string `json:"handoff_ref"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(e.Payload, &p); err != nil || p.HarnessID == "" {
|
||||||
|
return fmt.Errorf("orchestrator: invalid lease")
|
||||||
|
}
|
||||||
|
w, err := c.Worktrees.Create(ctx, t)
|
||||||
|
if err != nil {
|
||||||
|
return c.block(t, "worktree: "+err.Error())
|
||||||
|
}
|
||||||
|
a, err := c.Adapters.Adapter(p.HarnessID)
|
||||||
|
if err != nil {
|
||||||
|
return c.block(t, "adapter: "+err.Error())
|
||||||
|
}
|
||||||
|
s, err := a.Lease(ctx, t.ID, w)
|
||||||
|
if err != nil {
|
||||||
|
return c.block(t, "lease: "+err.Error())
|
||||||
|
}
|
||||||
|
if p.HandoffRef != "" {
|
||||||
|
if err = a.Bootstrap(ctx, s, p.HandoffRef); err != nil {
|
||||||
|
_ = a.Kill(ctx, s)
|
||||||
|
return c.block(t, "bootstrap: "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.sessions == nil {
|
||||||
|
c.sessions = map[string]herdr.Session{}
|
||||||
|
}
|
||||||
|
c.sessions[t.ID] = s
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Coordinator) block(t domain.Task, reason string) error {
|
||||||
|
b, _ := json.Marshal(map[string]string{"blocker": reason})
|
||||||
|
return c.Store.Append(domain.Event{ID: domain.NewID(), Type: "TaskBlocked", TaskID: t.ID, Version: t.Version + 1, Payload: b})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Coordinator) Session(taskID string) (herdr.Session, bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
s, ok := c.sessions[taskID]
|
||||||
|
return s, ok
|
||||||
|
}
|
||||||
+5
-5
@@ -6,25 +6,25 @@ Updated: 2026-07-26
|
|||||||
|
|
||||||
`go test ./...` passes, but the implementation is still a tested substrate/router prototype rather than a functioning unattended multi-harness orchestra. The following gaps were verified against `orchestra-spec (1).md` and the current code:
|
`go test ./...` passes, but the implementation is still a tested substrate/router prototype rather than a functioning unattended multi-harness orchestra. The following gaps were verified against `orchestra-spec (1).md` and the current code:
|
||||||
|
|
||||||
- **Harness execution is not wired into routing.** The router records `TaskLeased`, but does not call a harness adapter to create a session, create/use a worktree, bootstrap the agent, or monitor lifecycle events.
|
- **Harness execution is only partially wired.** `internal/orchestrator` now provides lease → worktree → adapter session → optional bootstrap coordination, but `main.go` does not yet construct concrete worktree/adapter registries or run monitoring callbacks.
|
||||||
- **Rotation is not implemented.** There is no turn-boundary callback, herdr event subscription, occupancy-triggered rotation, milestone/thrash trigger, or split-then-close coordinator.
|
- **Rotation is not implemented.** There is no turn-boundary callback, herdr event subscription, occupancy-triggered rotation, milestone/thrash trigger, or split-then-close coordinator.
|
||||||
- **Lifecycle API payloads are invalid.** The release, complete, and block endpoints all emit `{"source":"api"}`, while validation requires `handoff_ref` or `reason`, `report_ref`, and `blocker` respectively. The documented lifecycle endpoints therefore cannot complete successfully.
|
- **Lifecycle API payloads are invalid.** The release, complete, and block endpoints all emit `{"source":"api"}`, while validation requires `handoff_ref` or `reason`, `report_ref`, and `blocker` respectively. The documented lifecycle endpoints therefore cannot complete successfully.
|
||||||
- **Provider integrations are not wired into the server.** JSONL and Gitea adapters exist, but `main.go` has no webhook routes or polling loops; only generic task POST ingestion is exposed.
|
- **Provider integrations are partially wired.** JSONL watching and optional Gitea webhook/poll loops now start from environment configuration, but terminal reflection, provider health, cancellation, and delivery fan-out remain absent.
|
||||||
- **CAS references are not content-verified at event append.** Lifecycle events check that referenced files exist, but do not verify that the file content hashes to the supplied reference.
|
- **CAS references are not content-verified at event append.** Lifecycle events check that referenced files exist, but do not verify that the file content hashes to the supplied reference.
|
||||||
- **Replay bypasses event validation.** Startup replay unmarshals and applies events without validating the event envelope, payload schema, or sequence/version invariants.
|
- **Replay bypasses event validation.** Startup replay unmarshals and applies events without validating the event envelope, payload schema, or sequence/version invariants.
|
||||||
- **Snapshots are written but never loaded or used for replay acceleration.** Startup always replays the complete event log.
|
- **Snapshots are written but never loaded or used for replay acceleration.** Startup always replays the complete event log.
|
||||||
- **Task creation projection is incomplete.** `parent`, `due`, `inherent_priority`, and `estimate` are defined in the domain model but are not projected from `TaskCreated` payloads.
|
- **Task creation projection is incomplete.** `parent`, `due`, `inherent_priority`, and `estimate` are defined in the domain model but are not projected from `TaskCreated` payloads.
|
||||||
- **Occupancy support is incomplete relative to the spec.** Codex active-session discovery and opencode's server/SSE plus fallback path are not implemented.
|
- **Occupancy support is incomplete relative to the spec.** Native readers exist, but Codex active-session discovery, opencode server/SSE plus fallback, and coordinator monitoring are not implemented.
|
||||||
- **Authorization is only partially enforced.** HTTP method restrictions exist, but handlers do not consistently call `AuthorizeEvent`; an absent surface defaults to full-control Web.
|
- **Authorization is only partially enforced.** HTTP method restrictions exist, but handlers do not consistently call `AuthorizeEvent`; an absent surface defaults to full-control Web.
|
||||||
|
|
||||||
The first pass closed the store/API defects (lifecycle defaults, CAS content verification, validated replay, snapshot loading, and projection of task metadata) and added optional Gitea webhook/poll wiring. The remaining server-side gaps are below.
|
The first pass closed the store/API defects (lifecycle defaults, CAS content verification, validated replay, snapshot loading, and projection of task metadata) and added optional Gitea webhook/poll wiring. The remaining server-side gaps are below.
|
||||||
|
|
||||||
### Remaining server-side gaps
|
### Remaining server-side gaps
|
||||||
|
|
||||||
- **The orchestration coordinator is still absent.** A `TaskLeased` event only changes the projection. The server does not resolve a worktree, invoke `herdr.Adapter.Lease`, bootstrap the session, or translate adapter/session failures into lifecycle events. There is no durable session/lease-to-pane mapping.
|
- **The orchestration coordinator is not fully operational.** A reusable coordinator now resolves a worktree, invokes `herdr.Adapter.Lease`, bootstraps handoffs, and blocks failed starts. It is not yet constructed from deployment configuration, persisted, or connected to turn/lifecycle monitoring in `main.go`.
|
||||||
- **Rotation is still absent.** No adapter turn-boundary callback, occupancy trigger, milestone/thrash trigger, handoff save/validate flow, split-then-close sequence, or lease transfer coordinator is wired into the server. The existing occupancy readers and anchor validator are standalone library primitives.
|
- **Rotation is still absent.** No adapter turn-boundary callback, occupancy trigger, milestone/thrash trigger, handoff save/validate flow, split-then-close sequence, or lease transfer coordinator is wired into the server. The existing occupancy readers and anchor validator are standalone library primitives.
|
||||||
- **Harness discovery and registration are not operational.** Static herdr configuration and socket clients exist, but startup does not create adapters, ping configured herdrs, discover active Codex sessions, subscribe to opencode SSE, or run the required fallback/TTL monitoring loop.
|
- **Harness discovery and registration are not operational.** Static herdr configuration and socket clients exist, but startup does not create adapters, ping configured herdrs, discover active Codex sessions, subscribe to opencode SSE, or run the required fallback/TTL monitoring loop.
|
||||||
- **Provider ingestion is only partially wired.** Gitea is available when its environment is configured, but JSONL watching is not started by `main.go`; there is no provider lifecycle management, cancellation, or error health projection. Gitea terminal-state reflection and provider fan-out are not connected to server routes or background workers.
|
- **Provider ingestion is only partially wired.** JSONL and Gitea are available when configured, but there is no provider lifecycle management, cancellation, error health projection, terminal-state reflection, or provider fan-out.
|
||||||
- **Lifecycle event contracts remain incomplete.** Validation does not enforce the spec's `expected_version`, `ttl`, `anchor_sha`, `receipt`, or optional `handoff_ref` relationships, and the HTTP API does not validate actor/surface authorization at the event construction site. Completion without a report currently creates a generated placeholder artifact rather than requiring the stop-hook/wrapper receipt described by the spec.
|
- **Lifecycle event contracts remain incomplete.** Validation does not enforce the spec's `expected_version`, `ttl`, `anchor_sha`, `receipt`, or optional `handoff_ref` relationships, and the HTTP API does not validate actor/surface authorization at the event construction site. Completion without a report currently creates a generated placeholder artifact rather than requiring the stop-hook/wrapper receipt described by the spec.
|
||||||
- **Quota and standup scheduling are not implemented.** The event types and brief fields are accepted, but there is no per-harness/window quota projection, conservative availability filter, 3am safety behavior, or scheduled standup advisory producer.
|
- **Quota and standup scheduling are not implemented.** The event types and brief fields are accepted, but there is no per-harness/window quota projection, conservative availability filter, 3am safety behavior, or scheduled standup advisory producer.
|
||||||
- **Brief delivery and provider reflection are not implemented.** `/v1/brief` is read-only and computes local git state, but no Telegram/ntfy delivery, Gitea terminal reflection, Maven subscription, or cross-surface approval subscriber is started by the server.
|
- **Brief delivery and provider reflection are not implemented.** `/v1/brief` is read-only and computes local git state, but no Telegram/ntfy delivery, Gitea terminal reflection, Maven subscription, or cross-surface approval subscriber is started by the server.
|
||||||
|
|||||||
Reference in New Issue
Block a user