fix(continuity): wire scratch-commit-before-release and rewrite bootstrap prompt (Phase 4 items 3, 5, 6)

Release now re-verifies every handoff Anchor.Dirty file hash (previously
unchecked after the top-level anchor SHA compare), snapshots dirty state
onto a per-task scratch branch before uploading, and rewrites the anchor
to the new commit so successor pickup collapses to a single HEAD compare.
ScratchCommit made idempotent for repeated rotations of the same task.

Bootstrap's prompt now points the agent at the scratch-branch commit
history instead of vague "read the handoff" prose, and does not claim a
GET /v1/artifacts/<ref> endpoint that doesn't exist.

MarkdownChanges had zero callers and zero tests; deleted per AUDIT.md's
explicit deletion option rather than half-wiring an undesigned feature.

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 22:06:06 +04:00
parent 7bcad64398
commit 62bb17e05d
6 changed files with 504 additions and 55 deletions
+15 -28
View File
@@ -200,28 +200,6 @@ func Load(ref string, cas CAS) (Handoff, error) {
return Decode(b)
}
type Notice struct {
Path string
SHA256 string
At time.Time
}
func MarkdownChanges(root string, paths []string) ([]Notice, error) {
out := []Notice{}
for _, p := range paths {
if !strings.HasSuffix(strings.ToLower(p), ".md") {
continue
}
b, e := os.ReadFile(filepath.Join(root, p))
if e != nil {
return nil, e
}
s := sha256.Sum256(b)
out = append(out, Notice{p, hex.EncodeToString(s[:]), time.Now().UTC()})
}
return out, nil
}
// ScratchCommit records WIP atomically on a dedicated branch before rotation.
func ScratchCommit(root, branch, message string) error {
if branch == "" || strings.ContainsAny(branch, " \t\n") {
@@ -237,15 +215,24 @@ func ScratchCommit(root, branch, message string) error {
if len(status) != 0 {
return errors.New("TASK.md is immutable")
}
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 {
// Reuse the branch across rotations of the same task rather than failing
// on "branch already exists" — a task can rotate more than once.
if err := exec.Command("git", "-C", root, "switch", branch).Run(); err != nil {
if err := exec.Command("git", "-C", root, "switch", "-c", branch).Run(); err != nil {
return err
}
}
return nil
if err := exec.Command("git", "-C", root, "add", "-A").Run(); err != nil {
return err
}
full, err := exec.Command("git", "-C", root, "status", "--porcelain").Output()
if err != nil {
return err
}
if len(full) == 0 {
return nil // nothing to snapshot; branch already reflects the worktree
}
return exec.Command("git", "-C", root, "commit", "-m", message).Run()
}
func ScratchPush(root, branch, remote string) error {