fix(orchestrator): wire TASK.md writing and §6.2 pickup validation (B6, partial)

Fixes AUDIT.md's B6: nothing wrote a TASK.md into a worktree, so
continuity.ValidatePickup had no caller and no file to check.

- continuity.RenderTaskFile/TaskFileHash: render and hash the immutable
  §6.2 TASK.md from a domain.Task.
- GitWorktrees.Create writes and commits TASK.md into every freshly
  created worktree (must be committed, not dirty, for ScratchCommit's
  immutability check and for a stable hash).
- Coordinator.Start now runs continuity.ValidatePickup (anchor SHA,
  dirty-file hashes, TASK.md hash) against the real worktree before
  bootstrapping a successor onto a handoff_ref, and blocks the task
  instead of bootstrapping on a validation failure.

Still open from Phase 4: handoff production (agent writing the real
handoff; Release still refuses per B5), ScratchCommit wiring before
release, and the §6.2 bootstrap-prompt rewrite — see AUDIT.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 21:44:00 +04:00
parent a2c3d040d3
commit 7bcad64398
7 changed files with 289 additions and 3 deletions
+36
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"orchestra/internal/domain"
"os"
"os/exec"
"path/filepath"
@@ -14,6 +15,41 @@ import (
"time"
)
// RenderTaskFile produces the immutable §6.2 TASK.md content for a task.
// Deterministic in every field that comes from the task itself, so the same
// task always hashes to the same content.
func RenderTaskFile(t domain.Task) []byte {
var b strings.Builder
fmt.Fprintf(&b, "# Task %s\n\n", t.ID)
if t.Title != "" {
fmt.Fprintf(&b, "%s\n\n", t.Title)
}
fmt.Fprintf(&b, "- Project: %s\n", t.Project)
fmt.Fprintf(&b, "- Source: %s/%s\n", t.Source, t.ExternalID)
fmt.Fprintf(&b, "- Priority: %d\n", t.InherentPriority)
if len(t.Capability) > 0 {
fmt.Fprintf(&b, "- Capability: %s\n", strings.Join(t.Capability, ", "))
}
if t.Due != nil {
fmt.Fprintf(&b, "- Due: %s\n", t.Due.UTC().Format(time.RFC3339))
}
if t.Parent != "" {
fmt.Fprintf(&b, "- Parent: %s\n", t.Parent)
}
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())
}
// TaskFileHash returns the sha256 of the TASK.md at the root of a worktree.
func TaskFileHash(root string) (string, error) {
b, err := os.ReadFile(filepath.Join(root, "TASK.md"))
if err != nil {
return "", err
}
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:]), nil
}
type Dirty struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`