Add federation worker and canonical handoffs
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -36,6 +37,9 @@ func RenderTaskFile(t domain.Task) []byte {
|
||||
if t.Parent != "" {
|
||||
fmt.Fprintf(&b, "- Parent: %s\n", t.Parent)
|
||||
}
|
||||
if strings.TrimSpace(t.Description) != "" {
|
||||
fmt.Fprintf(&b, "\n## Instructions\n\n%s\n", t.Description)
|
||||
}
|
||||
b.WriteString("\nThis file is immutable for the lifetime of the task (§6.2) — its hash is\ncarried in every handoff and re-verified on every pickup. Do not edit it.\n")
|
||||
return []byte(b.String())
|
||||
}
|
||||
@@ -99,21 +103,14 @@ type Meta struct {
|
||||
RotationIndex int `json:"rotation_index"`
|
||||
}
|
||||
type Handoff struct {
|
||||
Meta Meta `json:"meta"`
|
||||
Anchor Anchor `json:"anchor"`
|
||||
Goal string `json:"goal"`
|
||||
DoneWhen []string `json:"done_when"`
|
||||
Completed []Completed `json:"completed"`
|
||||
Remaining []string `json:"remaining"`
|
||||
Action string `json:"action"`
|
||||
Command string `json:"command"`
|
||||
Files []string `json:"files"`
|
||||
Invariants []string `json:"invariants"`
|
||||
DeadEnds []DeadEnd `json:"dead_ends"`
|
||||
OpenQuestions []string `json:"open_questions"`
|
||||
Build string `json:"build"`
|
||||
Test string `json:"test"`
|
||||
LastResult Result `json:"last_result"`
|
||||
Meta Meta `json:"meta"`
|
||||
Anchor Anchor `json:"anchor"`
|
||||
Remaining []string `json:"remaining"`
|
||||
Action string `json:"action"`
|
||||
Command string `json:"command"`
|
||||
DeadEnds []DeadEnd `json:"dead_ends"`
|
||||
OpenQuestions []string `json:"open_questions"`
|
||||
Learned []string `json:"learned"`
|
||||
}
|
||||
type Result struct {
|
||||
Command string `json:"command"`
|
||||
@@ -123,13 +120,32 @@ type Result struct {
|
||||
|
||||
var reasons = map[string]bool{"threshold": true, "milestone": true, "thrash": true, "manual": true}
|
||||
|
||||
const maxAuthoredLine = 200
|
||||
|
||||
var circularAction = regexp.MustCompile(`(?i)handoff|report\.md|^continue the task`)
|
||||
var circularCommand = regexp.MustCompile(`(?i)\.orchestra-handoff|handoff-report|report\.md`)
|
||||
|
||||
func (h Handoff) Validate() error {
|
||||
if strings.TrimSpace(h.Meta.ID) == "" || !reasons[h.Meta.Reason] || h.Meta.RotationIndex < 0 {
|
||||
return errors.New("invalid handoff meta")
|
||||
}
|
||||
if len(h.Anchor.GitSHA) != 40 || h.Anchor.Branch == "" || strings.TrimSpace(h.Goal) == "" || len(h.DoneWhen) == 0 || strings.TrimSpace(h.Action) == "" || strings.TrimSpace(h.Command) == "" {
|
||||
if len(h.Anchor.GitSHA) != 40 || h.Anchor.Branch == "" || strings.TrimSpace(h.Action) == "" {
|
||||
return errors.New("invalid handoff required fields")
|
||||
}
|
||||
if circularAction.MatchString(h.Action) {
|
||||
return errors.New("invalid handoff action: must name concrete next work, not a handoff")
|
||||
}
|
||||
if err := validateAuthoredLine(h.Action); err != nil {
|
||||
return err
|
||||
}
|
||||
if circularCommand.MatchString(h.Command) {
|
||||
return errors.New("invalid handoff command: must not point to a handoff or report")
|
||||
}
|
||||
for _, item := range append(append([]string{}, h.Remaining...), append(h.OpenQuestions, h.Learned...)...) {
|
||||
if err := validateAuthoredLine(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, d := range h.Anchor.Dirty {
|
||||
if filepath.IsAbs(d.Path) || d.Path == "" || len(d.SHA256) != 64 {
|
||||
return errors.New("invalid dirty anchor")
|
||||
@@ -139,6 +155,22 @@ func (h Handoff) Validate() error {
|
||||
if strings.TrimSpace(d.Tried) == "" || strings.TrimSpace(d.WhyFailed) == "" {
|
||||
return errors.New("invalid dead end")
|
||||
}
|
||||
if err := validateAuthoredLine(d.Tried); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAuthoredLine(d.WhyFailed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAuthoredLine(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return errors.New("invalid handoff authored field: empty item")
|
||||
}
|
||||
if strings.Contains(s, "\n#") || len(s) > maxAuthoredLine {
|
||||
return errors.New("invalid handoff authored field: prose smuggled into list")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"orchestra/internal/store"
|
||||
@@ -27,7 +28,7 @@ func TestHandoffCASAndPickup(t *testing.T) {
|
||||
head := runOut(t, root, "rev-parse", "HEAD")
|
||||
task := sha256.Sum256([]byte("original"))
|
||||
s, _ := store.Open(t.TempDir())
|
||||
h := Handoff{Meta: Meta{ID: "h1", Reason: "manual"}, Anchor: Anchor{GitSHA: head, Branch: "main"}, Goal: "ship", DoneWhen: []string{"tests pass"}, Action: "test", Command: "go test ./...", LastResult: Result{AtSHA: head}}
|
||||
h := Handoff{Meta: Meta{ID: "h1", Reason: "manual"}, Anchor: Anchor{GitSHA: head, Branch: "main"}, Action: "run the focused tests", Command: "go test ./..."}
|
||||
ref, e := Save(h, s)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
@@ -55,6 +56,26 @@ func TestDecodeRejectsUnknownKnowledgeFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandoffRejectsFabricatedOrProseAuthoredFields(t *testing.T) {
|
||||
base := Handoff{Meta: Meta{ID: "h", Reason: "manual"}, Anchor: Anchor{GitSHA: strings.Repeat("a", 40), Branch: "main"}, Action: "run the focused tests"}
|
||||
if err := base.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
badAction := base
|
||||
badAction.Action = "Continue the task from the handoff"
|
||||
if err := badAction.Validate(); err == nil {
|
||||
t.Fatal("expected circular action rejection")
|
||||
}
|
||||
badProse := base
|
||||
badProse.Remaining = []string{"short\n# markdown heading"}
|
||||
if err := badProse.Validate(); err == nil {
|
||||
t.Fatal("expected prose-in-list rejection")
|
||||
}
|
||||
if _, err := Decode([]byte(`{"meta":{"id":"h","reason":"manual","rotation_index":0},"anchor":{"git_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","branch":"main"},"goal":"fabricated","action":"run tests"}`)); err == nil {
|
||||
t.Fatal("expected goal rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScratchCommitProtectsTask(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
run := func(a ...string) {
|
||||
|
||||
Reference in New Issue
Block a user