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:
@@ -67,6 +67,9 @@ func (w GitWorktrees) Create(ctx context.Context, t domain.Task) (string, error)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", fmt.Errorf("%s: %w", string(out), err)
|
||||
}
|
||||
if err := writeTaskFile(ctx, p, t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if w.TaskFileSHA != "" {
|
||||
if err := continuity.VerifyTaskFile(p, w.TaskFileSHA); err != nil {
|
||||
return "", err
|
||||
@@ -75,6 +78,27 @@ func (w GitWorktrees) Create(ctx context.Context, t domain.Task) (string, error)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// writeTaskFile commits the §6.2 immutable TASK.md into a freshly created
|
||||
// worktree. It must be committed, not left dirty, so ScratchCommit's
|
||||
// "TASK.md is immutable" check (which inspects `git status`) sees it as
|
||||
// clean, and so its hash survives independent of any later scratch commits.
|
||||
func writeTaskFile(ctx context.Context, worktree string, t domain.Task) error {
|
||||
path := filepath.Join(worktree, "TASK.md")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile(path, continuity.RenderTaskFile(t), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, args := range [][]string{{"add", "TASK.md"}, {"commit", "-m", "orchestra: TASK.md"}} {
|
||||
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", worktree}, args...)...)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%s: %w", string(out), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w GitWorktrees) Remove(ctx context.Context, _ domain.Task, path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("worktree: path required")
|
||||
@@ -541,17 +565,37 @@ func (c *Coordinator) Start(ctx context.Context, e domain.Event) error {
|
||||
if err != nil {
|
||||
return c.block(t, "worktree: "+err.Error())
|
||||
}
|
||||
// Best-effort: TASK.md only exists for worktrees this process can read
|
||||
// locally (the GitWorktrees path). A herdr-hosted worktree on a remote
|
||||
// machine (WorktreeCreator path) is the same cross-host gap named in
|
||||
// AUDIT.md's federation-fork section — not solved here.
|
||||
taskFileSHA, _ := continuity.TaskFileHash(w)
|
||||
s, err := a.Lease(ctx, t.ID, w)
|
||||
if err != nil {
|
||||
return c.block(t, "lease: "+err.Error())
|
||||
}
|
||||
if p.HandoffRef != "" {
|
||||
// §6.2 pickup validation: never bootstrap a successor onto a handoff
|
||||
// whose anchor/dirty-file/TASK.md hashes don't match what's actually
|
||||
// in the worktree. A failure here blocks the task rather than
|
||||
// silently trusting an unvalidated ref (this is the gap AUDIT.md's
|
||||
// B6 named as unreached from the live path).
|
||||
h, err := continuity.Load(p.HandoffRef, c.Store)
|
||||
if err != nil {
|
||||
_ = a.Kill(ctx, s)
|
||||
return c.block(t, "handoff: "+err.Error())
|
||||
}
|
||||
if err := continuity.ValidatePickup(w, h, taskFileSHA); err != nil {
|
||||
_ = a.Kill(ctx, s)
|
||||
return c.block(t, "pickup: "+err.Error())
|
||||
}
|
||||
if err = a.Bootstrap(ctx, s, p.HandoffRef); err != nil {
|
||||
_ = a.Kill(ctx, s)
|
||||
return c.block(t, "bootstrap: "+err.Error())
|
||||
}
|
||||
}
|
||||
s.HerdrID = p.HarnessID
|
||||
s.TaskFileSHA = taskFileSHA
|
||||
c.mu.Lock()
|
||||
if c.sessions == nil {
|
||||
c.sessions = map[string]herdr.Session{}
|
||||
|
||||
Reference in New Issue
Block a user