devrites 6.1.1 → 6.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +4 -1
  4. package/docs/engine/commands.md +4 -4
  5. package/docs/orchestration.md +4 -3
  6. package/docs/usage.md +1 -1
  7. package/engine/internal/lib/claims.go +12 -2
  8. package/engine/internal/lib/claims_holder.go +124 -0
  9. package/engine/internal/lib/cli_observe.go +14 -0
  10. package/engine/internal/lib/taskgraph.go +124 -0
  11. package/engine/internal/parallel/cli.go +111 -9
  12. package/engine/internal/parallel/lease.go +58 -4
  13. package/engine/internal/parallel/ops.go +244 -75
  14. package/engine/internal/parallel/select.go +31 -6
  15. package/pack/.claude/agents/devrites-plan-drafter.md +2 -1
  16. package/pack/.claude/agents/devrites-slice-wright.md +2 -1
  17. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  18. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  19. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  20. package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  21. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  22. package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -2
  23. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +1 -1
  24. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  25. package/pack/.claude/skills/rite-build/SKILL.md +7 -7
  26. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +2 -2
  27. package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
  28. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +1 -1
  29. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +28 -21
  30. package/pack/.claude/skills/rite-build/reference/phase-contract.md +10 -1
  31. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  32. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +2 -2
  33. package/pack/.claude/skills/rite-define/SKILL.md +1 -1
  34. package/pack/.claude/skills/rite-handoff/SKILL.md +1 -1
  35. package/pack/.claude/skills/rite-plan/reference/slicing.md +5 -0
  36. package/pack/.claude/skills/rite-vet/reference/artifacts.md +8 -0
  37. package/pack/.claude/skills/rite-vet/reference/review-axes.md +1 -1
  38. package/pack/generated/claude/agents/devrites-plan-drafter.md +2 -1
  39. package/pack/generated/claude/agents/devrites-slice-wright.md +2 -1
  40. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  41. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  42. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  43. package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  44. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  45. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -2
  46. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +1 -1
  47. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  48. package/pack/generated/claude/skills/rite-build/SKILL.md +7 -7
  49. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +2 -2
  50. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
  51. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +1 -1
  52. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +28 -21
  53. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +10 -1
  54. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  55. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +2 -2
  56. package/pack/generated/claude/skills/rite-define/SKILL.md +1 -1
  57. package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -1
  58. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +5 -0
  59. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +8 -0
  60. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +1 -1
  61. package/pack/generated/codex/agents/devrites-plan-drafter.toml +2 -1
  62. package/pack/generated/codex/agents/devrites-slice-wright.toml +2 -1
  63. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  64. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  65. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -2
  66. package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  67. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  68. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -2
  69. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +1 -1
  70. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  71. package/pack/generated/codex/skills/rite-build/SKILL.md +7 -7
  72. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +2 -2
  73. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
  74. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +1 -1
  75. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +28 -21
  76. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +10 -1
  77. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -2
  78. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +2 -2
  79. package/pack/generated/codex/skills/rite-define/SKILL.md +1 -1
  80. package/pack/generated/codex/skills/rite-handoff/SKILL.md +1 -1
  81. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +5 -0
  82. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +8 -0
  83. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +1 -1
  84. package/pack/generated/devin/agents/devrites-plan-drafter.md +2 -1
  85. package/pack/generated/devin/agents/devrites-slice-wright.md +2 -1
  86. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  87. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  88. package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +6 -2
  89. package/pack/generated/devin/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  90. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  91. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +2 -2
  92. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +1 -1
  93. package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  94. package/pack/generated/devin/skills/rite-build/SKILL.md +7 -7
  95. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +2 -2
  96. package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
  97. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +1 -1
  98. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +28 -21
  99. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +10 -1
  100. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -2
  101. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +2 -2
  102. package/pack/generated/devin/skills/rite-define/SKILL.md +1 -1
  103. package/pack/generated/devin/skills/rite-handoff/SKILL.md +1 -1
  104. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +5 -0
  105. package/pack/generated/devin/skills/rite-vet/reference/artifacts.md +8 -0
  106. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +1 -1
  107. package/pack/generated/omp/agents/devrites-plan-drafter.md +2 -1
  108. package/pack/generated/omp/agents/devrites-slice-wright.md +2 -1
  109. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  110. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  111. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +6 -2
  112. package/pack/generated/omp/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  113. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  114. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -2
  115. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +1 -1
  116. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  117. package/pack/generated/omp/skills/rite-build/SKILL.md +7 -7
  118. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +2 -2
  119. package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
  120. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +1 -1
  121. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +28 -21
  122. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +10 -1
  123. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -2
  124. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +2 -2
  125. package/pack/generated/omp/skills/rite-define/SKILL.md +1 -1
  126. package/pack/generated/omp/skills/rite-handoff/SKILL.md +1 -1
  127. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +5 -0
  128. package/pack/generated/omp/skills/rite-vet/reference/artifacts.md +8 -0
  129. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +1 -1
  130. package/pack/generated/pi/agents/devrites-plan-drafter.md +2 -1
  131. package/pack/generated/pi/agents/devrites-slice-wright.md +2 -1
  132. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  133. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  134. package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +6 -2
  135. package/pack/generated/pi/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  136. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  137. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +2 -2
  138. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +1 -1
  139. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  140. package/pack/generated/pi/skills/rite-build/SKILL.md +7 -7
  141. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +2 -2
  142. package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
  143. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +1 -1
  144. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +28 -21
  145. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +10 -1
  146. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -2
  147. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +2 -2
  148. package/pack/generated/pi/skills/rite-define/SKILL.md +1 -1
  149. package/pack/generated/pi/skills/rite-handoff/SKILL.md +1 -1
  150. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +5 -0
  151. package/pack/generated/pi/skills/rite-vet/reference/artifacts.md +8 -0
  152. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +1 -1
  153. package/package.json +1 -1
@@ -22,6 +22,9 @@ const (
22
22
  WrightGreen = "green"
23
23
  WrightRed = "red"
24
24
  WrightGap = "gap"
25
+ // WrightIntegrated marks a slice a per-slice integrate already landed on
26
+ // control; it no longer holds a parallel slot.
27
+ WrightIntegrated = "integrated"
25
28
 
26
29
  MinParallelSlices = 2
27
30
  MaxParallelSlices = 10
@@ -36,17 +39,22 @@ var (
36
39
  )
37
40
 
38
41
  // Lease is the machine-readable parallel batch lease (control tree only).
42
+ // ControlTip is control HEAD after this lease's most recent per-slice landing;
43
+ // empty means BaseSHA (nothing landed yet).
39
44
  type Lease struct {
40
45
  BatchID string `json:"batch_id"`
41
46
  CreatedAt string `json:"created_at"`
42
47
  BaseSHA string `json:"base_sha"`
48
+ ControlTip string `json:"control_tip,omitempty"`
43
49
  N int `json:"n"`
44
50
  Status string `json:"status"`
45
51
  ControlPIDOrSession string `json:"control_pid_or_session"`
46
52
  Slices []LeaseSlice `json:"slices"`
47
53
  }
48
54
 
49
- // LeaseSlice records one worker worktree under the lease.
55
+ // LeaseSlice records one worker worktree under the lease. BaseSHA is the
56
+ // commit its worktree was created from; empty means Lease.BaseSHA (a slice
57
+ // from the original create).
50
58
  type LeaseSlice struct {
51
59
  ID string `json:"id"`
52
60
  Paths []string `json:"paths"`
@@ -54,6 +62,35 @@ type LeaseSlice struct {
54
62
  Branch string `json:"branch"`
55
63
  WrightStatus string `json:"wright_status"`
56
64
  TransferCommit string `json:"transfer_commit,omitempty"`
65
+ BaseSHA string `json:"base_sha,omitempty"`
66
+ }
67
+
68
+ // expectedControl is where control must sit: the tip of this lease's latest
69
+ // landing, or the batch base before any landing.
70
+ func expectedControl(lease *Lease) string {
71
+ if lease.ControlTip != "" {
72
+ return lease.ControlTip
73
+ }
74
+ return lease.BaseSHA
75
+ }
76
+
77
+ // sliceBase is the commit sl's worktree was created from.
78
+ func sliceBase(lease *Lease, sl LeaseSlice) string {
79
+ if sl.BaseSHA != "" {
80
+ return sl.BaseSHA
81
+ }
82
+ return lease.BaseSHA
83
+ }
84
+
85
+ // activeSlices returns the slices that still hold a parallel slot.
86
+ func activeSlices(lease *Lease) []SlicePaths {
87
+ var active []SlicePaths
88
+ for _, sl := range lease.Slices {
89
+ if sl.WrightStatus != WrightIntegrated {
90
+ active = append(active, SlicePaths{ID: sl.ID, Paths: sl.Paths})
91
+ }
92
+ }
93
+ return active
57
94
  }
58
95
 
59
96
  func LeasePath(repoRoot, slug string) (string, error) {
@@ -132,8 +169,16 @@ func ValidateLease(lease *Lease) error {
132
169
  default:
133
170
  return fmt.Errorf("invalid lease status %q", lease.Status)
134
171
  }
135
- if err := checkParallelSliceCount(len(lease.Slices)); err != nil {
136
- return err
172
+ if lease.ControlTip != "" && !shaRE.MatchString(lease.ControlTip) {
173
+ return fmt.Errorf("invalid control_tip %q", lease.ControlTip)
174
+ }
175
+ // A rolling lease grows past the cap as landed slices free their slots;
176
+ // only the slices still active are bounded.
177
+ if len(lease.Slices) < MinParallelSlices {
178
+ return checkParallelSliceCount(len(lease.Slices))
179
+ }
180
+ if active := len(activeSlices(lease)); active > MaxParallelSlices {
181
+ return fmt.Errorf("parallel requires %d-%d slices active at once (got %d)", MinParallelSlices, MaxParallelSlices, active)
137
182
  }
138
183
  if lease.N != len(lease.Slices) {
139
184
  return fmt.Errorf("lease n=%d does not match slices=%d", lease.N, len(lease.Slices))
@@ -151,13 +196,16 @@ func ValidateLease(lease *Lease) error {
151
196
  return err
152
197
  }
153
198
  switch sl.WrightStatus {
154
- case "", WrightPending, WrightGreen, WrightRed, WrightGap:
199
+ case "", WrightPending, WrightGreen, WrightRed, WrightGap, WrightIntegrated:
155
200
  default:
156
201
  return fmt.Errorf("slice %q: invalid wright_status %q", sl.ID, sl.WrightStatus)
157
202
  }
158
203
  if sl.TransferCommit != "" && !shaRE.MatchString(sl.TransferCommit) {
159
204
  return fmt.Errorf("slice %q: invalid transfer_commit %q", sl.ID, sl.TransferCommit)
160
205
  }
206
+ if sl.BaseSHA != "" && !shaRE.MatchString(sl.BaseSHA) {
207
+ return fmt.Errorf("slice %q: invalid base_sha %q", sl.ID, sl.BaseSHA)
208
+ }
161
209
  }
162
210
  return nil
163
211
  }
@@ -176,6 +224,9 @@ func EmitLeaseMarkdown(lease *Lease) (string, error) {
176
224
  fmt.Fprintf(&b, "batch_id: %s\n", yamlQuote(lease.BatchID))
177
225
  fmt.Fprintf(&b, "created_at: %s\n", yamlQuote(lease.CreatedAt))
178
226
  fmt.Fprintf(&b, "base_sha: %s\n", yamlQuote(lease.BaseSHA))
227
+ if lease.ControlTip != "" {
228
+ fmt.Fprintf(&b, "control_tip: %s\n", yamlQuote(lease.ControlTip))
229
+ }
179
230
  fmt.Fprintf(&b, "n: %d\n", lease.N)
180
231
  fmt.Fprintf(&b, "status: %s\n", yamlQuote(lease.Status))
181
232
  fmt.Fprintf(&b, "control_pid_or_session: %s\n", yamlQuote(lease.ControlPIDOrSession))
@@ -192,6 +243,9 @@ func EmitLeaseMarkdown(lease *Lease) (string, error) {
192
243
  }
193
244
  fmt.Fprintf(&b, " worktree_path: %s\n", yamlQuote(sl.WorktreePath))
194
245
  fmt.Fprintf(&b, " branch: %s\n", yamlQuote(sl.Branch))
246
+ if sl.BaseSHA != "" {
247
+ fmt.Fprintf(&b, " base_sha: %s\n", yamlQuote(sl.BaseSHA))
248
+ }
195
249
  status := sl.WrightStatus
196
250
  if status == "" {
197
251
  status = WrightPending
@@ -90,70 +90,182 @@ func createLocked(opts CreateOpts) (*Lease, error) {
90
90
  session = fmt.Sprintf("pid:%d", os.Getpid())
91
91
  }
92
92
 
93
- createdPaths := make([]string, 0, len(opts.Slices))
94
- createdBranches := make([]string, 0, len(opts.Slices))
95
- cleanupPartial := func() {
96
- for _, p := range createdPaths {
97
- if err := removeWorktree(opts.Root, p); err != nil {
98
- warnf("worktree cleanup %s: %v", p, err)
93
+ slices, cleanupPartial, err := createWorktrees(opts.Root, opts.Slug, opts.BatchID, base, opts.Slices)
94
+ if err != nil {
95
+ return nil, err
96
+ }
97
+
98
+ lease := &Lease{
99
+ BatchID: opts.BatchID,
100
+ CreatedAt: NowUTC(),
101
+ BaseSHA: base,
102
+ N: len(slices),
103
+ Status: StatusRunning,
104
+ ControlPIDOrSession: session,
105
+ Slices: slices,
106
+ }
107
+ if err := WriteLease(leasePath, lease); err != nil {
108
+ cleanupPartial()
109
+ return nil, err
110
+ }
111
+ ensureScratchExcluded(opts.Root)
112
+ return lease, nil
113
+ }
114
+
115
+ // createWorktrees makes one pending worker worktree and branch per slice from
116
+ // base, all or nothing: a failure removes what this call already made. The
117
+ // returned rollback undoes a success when the caller's lease write fails.
118
+ func createWorktrees(root, slug, batchID, base string, specs []SlicePaths) ([]LeaseSlice, func(), error) {
119
+ made := make([]LeaseSlice, 0, len(specs))
120
+ rollback := func() {
121
+ // Worktrees first: git refuses to delete a branch still checked out.
122
+ for _, sl := range made {
123
+ if err := removeWorktree(root, sl.WorktreePath); err != nil {
124
+ warnf("worktree cleanup %s: %v", sl.WorktreePath, err)
99
125
  }
100
126
  }
101
- for _, b := range createdBranches {
102
- if err := deleteBranch(opts.Root, b); err != nil {
103
- warnf("branch cleanup %s: %v", b, err)
127
+ for _, sl := range made {
128
+ if err := deleteBranch(root, sl.Branch); err != nil {
129
+ warnf("branch cleanup %s: %v", sl.Branch, err)
104
130
  }
105
131
  }
106
132
  }
107
- slices := make([]LeaseSlice, 0, len(opts.Slices))
133
+ for _, sp := range specs {
134
+ sl, err := createWorktree(root, slug, batchID, base, sp)
135
+ if err != nil {
136
+ rollback()
137
+ return nil, nil, err
138
+ }
139
+ made = append(made, sl)
140
+ }
141
+ return made, rollback, nil
142
+ }
143
+
144
+ func createWorktree(root, slug, batchID, base string, sp SlicePaths) (LeaseSlice, error) {
145
+ if err := validateSliceID(sp.ID); err != nil {
146
+ return LeaseSlice{}, err
147
+ }
148
+ paths, err := validateSlicePaths(sp.Paths, fmt.Sprintf("slice %q", sp.ID), root)
149
+ if err != nil {
150
+ return LeaseSlice{}, err
151
+ }
152
+ branch := WorkerBranch(slug, batchID, sp.ID)
153
+ wt := WorkerWorktreePath(root, batchID, sp.ID)
154
+ if _, err := os.Stat(wt); err == nil {
155
+ return LeaseSlice{}, fmt.Errorf("worktree path already exists: %s", wt)
156
+ }
157
+ if err := os.MkdirAll(filepath.Dir(wt), 0o755); err != nil {
158
+ return LeaseSlice{}, err
159
+ }
160
+ if err := worktreeAdd(root, wt, branch, base); err != nil {
161
+ return LeaseSlice{}, fmt.Errorf("worktree add %s: %w", sp.ID, err)
162
+ }
163
+ return LeaseSlice{
164
+ ID: sp.ID,
165
+ Paths: paths,
166
+ WorktreePath: wt,
167
+ Branch: branch,
168
+ WrightStatus: WrightPending,
169
+ }, nil
170
+ }
171
+
172
+ // AddOpts configures refilling a running lease with new slices.
173
+ type AddOpts struct {
174
+ Root string
175
+ Slug string
176
+ Slices []SlicePaths
177
+ }
178
+
179
+ // Add refills freed slots of a running lease: each new slice gets a worktree
180
+ // from the current control tip, under the same path-disjoint and cap rules as
181
+ // the slices still active. Landed (integrated) slices no longer claim paths.
182
+ func Add(opts AddOpts) (lease *Lease, err error) {
183
+ err = withLeaseLock(opts.Root, opts.Slug, func() error {
184
+ lease, err = addLocked(opts)
185
+ return err
186
+ })
187
+ return lease, err
188
+ }
189
+
190
+ func addLocked(opts AddOpts) (*Lease, error) {
191
+ if abs, err := filepath.Abs(opts.Root); err == nil {
192
+ opts.Root = abs
193
+ }
194
+ if len(opts.Slices) == 0 {
195
+ return nil, fmt.Errorf("add requires at least one slice")
196
+ }
197
+ leasePath, err := LeasePath(opts.Root, opts.Slug)
198
+ if err != nil {
199
+ return nil, err
200
+ }
201
+ lease, err := ReadLease(leasePath)
202
+ if err != nil {
203
+ return nil, err
204
+ }
205
+ if lease.Status != StatusRunning {
206
+ return nil, fmt.Errorf("add requires status=running (have %s)", lease.Status)
207
+ }
208
+ head, err := revParse(opts.Root, expectedControl(lease))
209
+ if err != nil {
210
+ return nil, err
211
+ }
212
+ if err := ensureControlAtBase(opts.Root, head); err != nil {
213
+ return nil, err
214
+ }
215
+
216
+ seen := make(map[string]bool, len(lease.Slices)+len(opts.Slices))
217
+ for _, sl := range lease.Slices {
218
+ seen[sl.ID] = true
219
+ }
220
+ added := make([]SlicePaths, 0, len(opts.Slices))
221
+ var addedPaths []string
108
222
  for _, sp := range opts.Slices {
109
223
  if err := validateSliceID(sp.ID); err != nil {
110
- cleanupPartial()
111
224
  return nil, err
112
225
  }
226
+ if seen[sp.ID] {
227
+ return nil, fmt.Errorf("slice id %q already in lease", sp.ID)
228
+ }
229
+ seen[sp.ID] = true
113
230
  paths, err := validateSlicePaths(sp.Paths, fmt.Sprintf("slice %q", sp.ID), opts.Root)
114
231
  if err != nil {
115
- cleanupPartial()
116
232
  return nil, err
117
233
  }
118
- branch := WorkerBranch(opts.Slug, opts.BatchID, sp.ID)
119
- wt := WorkerWorktreePath(opts.Root, opts.BatchID, sp.ID)
120
- if _, err := os.Stat(wt); err == nil {
121
- cleanupPartial()
122
- return nil, fmt.Errorf("worktree path already exists: %s", wt)
123
- }
124
- if err := os.MkdirAll(filepath.Dir(wt), 0o755); err != nil {
125
- cleanupPartial()
234
+ added = append(added, SlicePaths{ID: sp.ID, Paths: paths})
235
+ addedPaths = append(addedPaths, paths...)
236
+ }
237
+ all := append(activeSlices(lease), added...)
238
+ if len(all) > MaxParallelSlices {
239
+ return nil, fmt.Errorf("add would make %d slices active; parallel allows at most %d", len(all), MaxParallelSlices)
240
+ }
241
+ if len(all) >= 2 {
242
+ if _, err := CheckPathDisjoint(all, opts.Root); err != nil {
126
243
  return nil, err
127
244
  }
128
- if err := worktreeAdd(opts.Root, wt, branch, base); err != nil {
129
- cleanupPartial()
130
- return nil, fmt.Errorf("worktree add %s: %w", sp.ID, err)
131
- }
132
- createdPaths = append(createdPaths, wt)
133
- createdBranches = append(createdBranches, branch)
134
- slices = append(slices, LeaseSlice{
135
- ID: sp.ID,
136
- Paths: paths,
137
- WorktreePath: wt,
138
- Branch: branch,
139
- WrightStatus: WrightPending,
140
- })
245
+ }
246
+ // The new worktree starts from control HEAD, so uncommitted user work on
247
+ // its paths would be neither in the slice nor safe from its landing.
248
+ dirty, err := porcelainDirtyPaths(opts.Root, addedPaths)
249
+ if err != nil {
250
+ return nil, err
251
+ }
252
+ if dirty {
253
+ return nil, fmt.Errorf("control has uncommitted changes on new slice paths; stop for the human to resolve them (never stash or commit them), then rerun add")
141
254
  }
142
255
 
143
- lease := &Lease{
144
- BatchID: opts.BatchID,
145
- CreatedAt: NowUTC(),
146
- BaseSHA: base,
147
- N: len(slices),
148
- Status: StatusRunning,
149
- ControlPIDOrSession: session,
150
- Slices: slices,
256
+ made, rollback, err := createWorktrees(opts.Root, opts.Slug, lease.BatchID, head, added)
257
+ if err != nil {
258
+ return nil, err
259
+ }
260
+ for i := range made {
261
+ made[i].BaseSHA = head
151
262
  }
263
+ lease.Slices = append(lease.Slices, made...)
264
+ lease.N = len(lease.Slices)
152
265
  if err := WriteLease(leasePath, lease); err != nil {
153
- cleanupPartial()
266
+ rollback()
154
267
  return nil, err
155
268
  }
156
- ensureScratchExcluded(opts.Root)
157
269
  return lease, nil
158
270
  }
159
271
 
@@ -185,6 +297,10 @@ func recordGreenLocked(repoRoot, slug, sliceID, commit string) (*Lease, error) {
185
297
  found := false
186
298
  for i := range lease.Slices {
187
299
  if lease.Slices[i].ID == sliceID {
300
+ if lease.Slices[i].WrightStatus == WrightIntegrated {
301
+ // Re-greening a landed slice would land its work a second time.
302
+ return nil, fmt.Errorf("slice %s already integrated", sliceID)
303
+ }
188
304
  lease.Slices[i].WrightStatus = WrightGreen
189
305
  lease.Slices[i].TransferCommit = sha
190
306
  found = true
@@ -222,11 +338,11 @@ func abortLocked(repoRoot, slug string) (*Lease, error) {
222
338
  if lease.Status == StatusComplete {
223
339
  return nil, fmt.Errorf("abort refused: lease already complete — the batch landed in control; nothing to discard")
224
340
  }
225
- base, err := revParse(repoRoot, lease.BaseSHA)
341
+ expected, err := revParse(repoRoot, expectedControl(lease))
226
342
  if err != nil {
227
343
  return nil, err
228
344
  }
229
- if err := ensureControlAtBase(repoRoot, base); err != nil {
345
+ if err := ensureControlAtBase(repoRoot, expected); err != nil {
230
346
  warnf("control not at base: %v", err)
231
347
  }
232
348
  lease.Status = StatusAborted
@@ -252,12 +368,17 @@ func ensureControlAtBase(repoRoot, base string) error {
252
368
 
253
369
  // IntegrateOpts configures staging integrate.
254
370
  type IntegrateOpts struct {
255
- Root string
256
- Slug string
371
+ Root string
372
+ Slug string
373
+ // Slice, when set, lands only that slice (requires ApplyToControl);
374
+ // otherwise every slice not yet integrated lands together.
375
+ Slice string
257
376
  ApplyToControl bool
258
377
  }
259
378
 
260
- // Integrate all-or-nothing applies sibling transfer commits onto a staging branch.
379
+ // Integrate all-or-nothing applies transfer commits onto a staging branch
380
+ // cut from the expected control tip — every not-yet-integrated slice, or just
381
+ // opts.Slice — and with ApplyToControl fast-forwards control to it.
261
382
  func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
262
383
  err = withLeaseLock(opts.Root, opts.Slug, func() error {
263
384
  tip, lease, err = integrateLocked(opts)
@@ -267,6 +388,9 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
267
388
  }
268
389
 
269
390
  func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
391
+ if opts.Slice != "" && !opts.ApplyToControl {
392
+ return "", nil, fmt.Errorf("integrate --slice requires --apply-to-control")
393
+ }
270
394
  leasePath, err := LeasePath(opts.Root, opts.Slug)
271
395
  if err != nil {
272
396
  return "", nil, err
@@ -278,17 +402,36 @@ func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
278
402
  if lease.Status != StatusRunning && lease.Status != StatusIntegrateFailed {
279
403
  return "", nil, fmt.Errorf("integrate requires status=running|integrate-failed (have %s)", lease.Status)
280
404
  }
281
- base, err := revParse(opts.Root, lease.BaseSHA)
405
+ // Control may only have moved by this lease's own landings; anything else
406
+ // is external work that integrate must neither rewind nor build on.
407
+ expected, err := revParse(opts.Root, expectedControl(lease))
282
408
  if err != nil {
283
409
  return "", nil, err
284
410
  }
285
- if err := ensureControlAtBase(opts.Root, base); err != nil {
411
+ if err := ensureControlAtBase(opts.Root, expected); err != nil {
286
412
  return "", nil, err
287
413
  }
288
414
 
289
- union := make([]string, 0, len(lease.Slices))
290
- for _, sl := range lease.Slices {
291
- union = append(union, sl.Paths...)
415
+ var targets []int
416
+ for i, sl := range lease.Slices {
417
+ switch {
418
+ case opts.Slice == "" && sl.WrightStatus != WrightIntegrated,
419
+ opts.Slice != "" && sl.ID == opts.Slice:
420
+ targets = append(targets, i)
421
+ }
422
+ }
423
+ switch {
424
+ case opts.Slice != "" && len(targets) == 0:
425
+ return "", nil, fmt.Errorf("slice not in lease: %s", opts.Slice)
426
+ case opts.Slice != "" && lease.Slices[targets[0]].WrightStatus == WrightIntegrated:
427
+ return "", nil, fmt.Errorf("slice %s already integrated", opts.Slice)
428
+ case len(targets) == 0:
429
+ return "", nil, fmt.Errorf("no slices left to integrate")
430
+ }
431
+
432
+ union := make([]string, 0, len(targets))
433
+ for _, i := range targets {
434
+ union = append(union, lease.Slices[i].Paths...)
292
435
  }
293
436
  if opts.ApplyToControl {
294
437
  // FF into control must not silently absorb or clobber the user's
@@ -303,7 +446,8 @@ func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
303
446
  }
304
447
  }
305
448
 
306
- for _, sl := range lease.Slices {
449
+ for _, i := range targets {
450
+ sl := lease.Slices[i]
307
451
  if sl.WrightStatus != WrightGreen {
308
452
  return "", nil, fmt.Errorf("slice %s is not green", sl.ID)
309
453
  }
@@ -326,7 +470,7 @@ func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
326
470
  if err := os.MkdirAll(filepath.Dir(stageWT), 0o755); err != nil {
327
471
  return "", nil, err
328
472
  }
329
- if err := worktreeAdd(opts.Root, stageWT, ibranch, base); err != nil {
473
+ if err := worktreeAdd(opts.Root, stageWT, ibranch, expected); err != nil {
330
474
  return "", nil, fmt.Errorf("staging worktree: %w", err)
331
475
  }
332
476
 
@@ -350,13 +494,20 @@ func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
350
494
  leasePath, StatusRunning, retryErr)
351
495
  }
352
496
  }
353
- if err := ensureControlAtBase(opts.Root, base); err != nil {
497
+ if err := ensureControlAtBase(opts.Root, expected); err != nil {
354
498
  warnf("control diverged from base during integrate: %v", err)
355
499
  }
356
500
  return "", lease, reason
357
501
  }
358
502
 
359
- for _, sl := range lease.Slices {
503
+ for _, i := range targets {
504
+ sl := lease.Slices[i]
505
+ // Each slice is proven against the commit its worktree was cut from,
506
+ // which for a refill is a later control tip than the batch base.
507
+ base, err := revParse(opts.Root, sliceBase(lease, sl))
508
+ if err != nil {
509
+ return fail(fmt.Errorf("slice %s base: %w", sl.ID, err))
510
+ }
360
511
  tc, err := revParse(opts.Root, sl.TransferCommit)
361
512
  if err != nil {
362
513
  return fail(fmt.Errorf("slice %s transfer: %w", sl.ID, err))
@@ -417,11 +568,20 @@ func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
417
568
  if err := mergeFFOnly(opts.Root, tip); err != nil {
418
569
  return fail(fmt.Errorf("control fast-forward to integrate tip failed: %w", err))
419
570
  }
420
- lease.Status = StatusComplete
571
+ for _, i := range targets {
572
+ lease.Slices[i].WrightStatus = WrightIntegrated
573
+ }
574
+ lease.ControlTip = tip
575
+ // A per-slice landing also clears an earlier integrate-failed: the
576
+ // lease is healthy again while other slices are still in flight.
577
+ lease.Status = StatusRunning
578
+ if len(activeSlices(lease)) == 0 {
579
+ lease.Status = StatusComplete
580
+ }
421
581
  if err := WriteLease(leasePath, lease); err != nil {
422
582
  return "", nil, err
423
583
  }
424
- } else if err := ensureControlAtBase(opts.Root, base); err != nil {
584
+ } else if err := ensureControlAtBase(opts.Root, expected); err != nil {
425
585
  return fail(err)
426
586
  }
427
587
  // Without --apply-to-control the lease stays running: the integrate
@@ -529,11 +689,12 @@ func salvageSlice(sl LeaseSlice, batchID string) (string, error) {
529
689
  fmt.Sprintf("devrites: salvage WIP (%s)", batchID), sl.Paths)
530
690
  }
531
691
 
532
- // Cleanup removes worker worktrees and clears the lease. A complete batch
533
- // deletes the merged slice branches; a forced non-complete cleanup first
534
- // salvages each sibling — uncommitted allowlisted changes become a commit on
535
- // the slice branch — and keeps every branch whose tip moved past base as the
536
- // durable rejected-work ref.
692
+ // Cleanup removes worker worktrees and clears the lease. A complete batch, and
693
+ // any slice already integrated, deletes the merged slice branches; a forced
694
+ // non-complete cleanup first salvages each other sibling — uncommitted
695
+ // allowlisted changes become a commit on the slice branch — and keeps every
696
+ // branch whose tip moved past that slice's base as the durable rejected-work
697
+ // ref.
537
698
  func Cleanup(repoRoot, slug string, force bool) (salvaged []Salvage, err error) {
538
699
  err = withLeaseLock(repoRoot, slug, func() error {
539
700
  salvaged, err = cleanupLocked(repoRoot, slug, force)
@@ -558,14 +719,6 @@ func cleanupLocked(repoRoot, slug string, force bool) ([]Salvage, error) {
558
719
  return nil, fmt.Errorf("cleanup refuses status=%s without --force (non-complete cleanup salvages work onto slice branches)", lease.Status)
559
720
  }
560
721
  complete := lease.Status == StatusComplete
561
- base := ""
562
- if !complete {
563
- if b, err := revParse(repoRoot, lease.BaseSHA); err != nil {
564
- warnf("base sha %s: %v", lease.BaseSHA, err)
565
- } else {
566
- base = b
567
- }
568
- }
569
722
  // Ownership comes from what git registered, read before anything is
570
723
  // removed, never from the lease's own claim.
571
724
  registered, err := registeredWorktrees(repoRoot)
@@ -576,6 +729,9 @@ func cleanupLocked(repoRoot, slug string, force bool) ([]Salvage, error) {
576
729
  var failed, stranded, unconfirmed []string
577
730
  keptWorktrees := map[string]bool{}
578
731
  for _, sl := range lease.Slices {
732
+ // A slice integrate already landed is done like a complete batch: its
733
+ // work is on control, so there is nothing to salvage or keep.
734
+ landed := complete || sl.WrightStatus == WrightIntegrated
579
735
  salvageFailed := false
580
736
  // Only the deterministic scratch layout is ours to touch. A forged or
581
737
  // stale lease worktree_path is never salvaged into (git ops would run
@@ -595,7 +751,7 @@ func cleanupLocked(repoRoot, slug string, force bool) ([]Salvage, error) {
595
751
  sl.ID, sl.WorktreePath, ScratchRoot(repoRoot))
596
752
  }
597
753
  }
598
- if !complete && owned {
754
+ if !landed && owned {
599
755
  if _, err := salvage(sl, lease.BatchID); err != nil {
600
756
  warnf("salvage %s: %v", sl.ID, err)
601
757
  salvageFailed = true
@@ -631,7 +787,13 @@ func cleanupLocked(repoRoot, slug string, force bool) ([]Salvage, error) {
631
787
  if sl.Branch != "" {
632
788
  keep := salvageFailed
633
789
  tip := ""
634
- if !complete {
790
+ if !landed {
791
+ base := ""
792
+ if b, err := revParse(repoRoot, sliceBase(lease, sl)); err != nil {
793
+ warnf("base sha %s: %v", sliceBase(lease, sl), err)
794
+ } else {
795
+ base = b
796
+ }
635
797
  if t, err := revParse(repoRoot, sl.Branch); err != nil {
636
798
  warnf("branch tip %s: %v", sl.Branch, err)
637
799
  keep = true // fail toward preservation
@@ -740,13 +902,20 @@ func StatusReport(repoRoot, slug string) (string, error) {
740
902
  fmt.Fprintf(&b, "lease: %s\n", leasePath)
741
903
  fmt.Fprintf(&b, "status: %s\n", lease.Status)
742
904
  fmt.Fprintf(&b, "base_sha: %s\n", lease.BaseSHA)
905
+ if lease.ControlTip != "" {
906
+ fmt.Fprintf(&b, "control_tip: %s\n", lease.ControlTip)
907
+ }
743
908
  for _, sl := range lease.Slices {
744
909
  tc := sl.TransferCommit
745
910
  if tc == "" {
746
911
  tc = "-"
747
912
  }
748
- fmt.Fprintf(&b, "slice %s: wright=%s wt=%s branch=%s transfer=%s\n",
913
+ fmt.Fprintf(&b, "slice %s: wright=%s wt=%s branch=%s transfer=%s",
749
914
  sl.ID, sl.WrightStatus, sl.WorktreePath, sl.Branch, tc)
915
+ if sl.BaseSHA != "" {
916
+ fmt.Fprintf(&b, " base=%s", sl.BaseSHA)
917
+ }
918
+ b.WriteString("\n")
750
919
  }
751
920
  if head, err := headSHA(repoRoot); err == nil {
752
921
  fmt.Fprintf(&b, "control_HEAD: %s\n", head)
@@ -13,6 +13,13 @@ var checkPathDisjoint = CheckPathDisjoint
13
13
  // whose size is ≤ cap. ready must already be dependency-satisfied and in plan
14
14
  // order. cap is 1–MaxParallelSlices; 1 is a legal one-slice round, not a latch.
15
15
  func SelectGreedy(cap int, ready []SlicePaths, root string) ([]SlicePaths, error) {
16
+ return SelectGreedySeeded(cap, nil, ready, root)
17
+ }
18
+
19
+ // SelectGreedySeeded is SelectGreedy with seed slices already running: they
20
+ // occupy cap slots and claim their paths, a ready candidate with a seed's id
21
+ // is skipped, and only the new picks are returned.
22
+ func SelectGreedySeeded(cap int, seed, ready []SlicePaths, root string) ([]SlicePaths, error) {
16
23
  if cap < 1 || cap > MaxParallelSlices {
17
24
  return nil, fmt.Errorf("cap must be 1-%d (got %d)", MaxParallelSlices, cap)
18
25
  }
@@ -39,11 +46,18 @@ func SelectGreedy(cap int, ready []SlicePaths, root string) ([]SlicePaths, error
39
46
  normalized = append(normalized, SlicePaths{ID: item.ID, Paths: paths})
40
47
  }
41
48
 
42
- selected := make([]SlicePaths, 0, min(cap, len(normalized)))
49
+ running := make(map[string]bool, len(seed))
50
+ for _, s := range seed {
51
+ running[s.ID] = true
52
+ }
53
+ selected := append([]SlicePaths{}, seed...)
43
54
  for _, cand := range normalized {
44
55
  if len(selected) >= cap {
45
56
  break
46
57
  }
58
+ if running[cand.ID] {
59
+ continue
60
+ }
47
61
  if len(selected) == 0 {
48
62
  selected = append(selected, cand)
49
63
  continue
@@ -57,7 +71,7 @@ func SelectGreedy(cap int, ready []SlicePaths, root string) ([]SlicePaths, error
57
71
  }
58
72
  selected = trial
59
73
  }
60
- return selected, nil
74
+ return selected[len(seed):], nil
61
75
  }
62
76
 
63
77
  func selectMode(n int) string {
@@ -71,10 +85,21 @@ func selectMode(n int) string {
71
85
  }
72
86
  }
73
87
 
74
- func formatSelect(selected []SlicePaths) string {
75
- ids := make([]string, len(selected))
76
- for i, item := range selected {
88
+ // formatSelect prints the select line. running < 0 means no lease seeded the
89
+ // selection; otherwise new picks join that many running slices, so the mode
90
+ // counts them too and the line reports running=<k>.
91
+ func formatSelect(picks []SlicePaths, running int) string {
92
+ ids := make([]string, len(picks))
93
+ for i, item := range picks {
77
94
  ids[i] = item.ID
78
95
  }
79
- return fmt.Sprintf("select: n_eff=%d mode=%s slices=%s", len(selected), selectMode(len(selected)), strings.Join(ids, ","))
96
+ mode := selectMode(len(picks))
97
+ if running > 0 && len(picks) > 0 {
98
+ mode = selectMode(len(picks) + running)
99
+ }
100
+ line := fmt.Sprintf("select: n_eff=%d mode=%s slices=%s", len(picks), mode, strings.Join(ids, ","))
101
+ if running >= 0 {
102
+ line += fmt.Sprintf(" running=%d", running)
103
+ }
104
+ return line
80
105
  }
@@ -21,7 +21,8 @@ seams supplied by the orchestrator.
21
21
  planning artifacts while preserving settled intent.
22
22
 
23
23
  Reuse existing seams and dependencies before proposing new ones. Build vertical
24
- slices in dependency order. Map every REQ and AC to a slice and executable proof,
24
+ slices in dependency order; put a hub file's shared edit in one seam slice
25
+ ([slicing.md](../skills/rite-plan/reference/slicing.md#slice-independence)). Map every REQ and AC to a slice and executable proof,
25
26
  name rollback and failure paths; keep acceptance-changing proposals separate. Durable
26
27
  proof commands must be portable repository commands, never host-local wrappers,
27
28
  user-specific absolute paths, or temporary proof trees.