close worktree transport and lifecycle contract gaps
This commit is contained in:
@@ -128,6 +128,22 @@ func ValidatePickup(root string, h Handoff, taskFileSHA string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTaskFile ensures the worktree contains the original, immutable task.
|
||||
func VerifyTaskFile(root, taskFileSHA string) error {
|
||||
if taskFileSHA == "" {
|
||||
return errors.New("TASK.md hash required")
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(root, "TASK.md"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
if hex.EncodeToString(sum[:]) != taskFileSHA {
|
||||
return errors.New("TASK.md changed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CAS interface {
|
||||
PutArtifact([]byte) (string, error)
|
||||
Artifact(string) ([]byte, error)
|
||||
@@ -175,6 +191,9 @@ func ScratchCommit(root, branch, message string) error {
|
||||
if branch == "" || strings.ContainsAny(branch, " \t\n") {
|
||||
return errors.New("invalid scratch branch")
|
||||
}
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return errors.New("scratch commit message required")
|
||||
}
|
||||
for _, args := range [][]string{{"switch", "-c", branch}, {"add", "-A"}, {"commit", "-m", message}} {
|
||||
if err := exec.Command("git", append([]string{"-C", root}, args...)...).Run(); err != nil {
|
||||
return err
|
||||
@@ -182,3 +201,22 @@ func ScratchCommit(root, branch, message string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScratchSync pushes/pulls a scratch branch. Pull uses fast-forward-only to
|
||||
// avoid silently merging independent WIP histories.
|
||||
func ScratchSync(root, branch, remote string, push bool) error {
|
||||
if branch == "" || strings.ContainsAny(branch, " \t\n") || remote == "" {
|
||||
return errors.New("invalid scratch sync")
|
||||
}
|
||||
args := []string{"-C", root, "push", remote, branch}
|
||||
if !push {
|
||||
args = []string{"-C", root, "fetch", remote, branch}
|
||||
}
|
||||
if err := exec.Command("git", args...).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !push {
|
||||
return exec.Command("git", "-C", root, "merge", "--ff-only", "FETCH_HEAD").Run()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -54,3 +54,20 @@ func TestDecodeRejectsUnknownKnowledgeFields(t *testing.T) {
|
||||
t.Fatal("expected strict schema error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTaskFileRejectsMutation(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "TASK.md"), []byte("task"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha256.Sum256([]byte("task"))
|
||||
if err := VerifyTaskFile(root, hex.EncodeToString(sum[:])); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "TASK.md"), []byte("changed"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := VerifyTaskFile(root, hex.EncodeToString(sum[:])); err == nil {
|
||||
t.Fatal("expected immutable task check to fail")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user