devrites 6.1.1 → 6.1.3

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 (221) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +4 -1
  4. package/docs/command-map.md +1 -1
  5. package/docs/engine/commands.md +4 -4
  6. package/docs/orchestration.md +4 -3
  7. package/docs/usage.md +1 -1
  8. package/engine/commands.go +1 -1
  9. package/engine/help.go +5 -1
  10. package/engine/internal/acceptance/cli.go +14 -5
  11. package/engine/internal/acceptance/run.go +20 -5
  12. package/engine/internal/lib/apply_bundle.go +474 -0
  13. package/engine/internal/lib/claims.go +12 -2
  14. package/engine/internal/lib/claims_holder.go +124 -0
  15. package/engine/internal/lib/cli_observe.go +14 -0
  16. package/engine/internal/lib/diffscope.go +2 -1
  17. package/engine/internal/lib/taskgraph.go +124 -0
  18. package/engine/internal/parallel/cli.go +111 -9
  19. package/engine/internal/parallel/lease.go +58 -4
  20. package/engine/internal/parallel/ops.go +244 -75
  21. package/engine/internal/parallel/select.go +31 -6
  22. package/engine/main.go +4 -0
  23. package/pack/.claude/agents/devrites-plan-drafter.md +13 -3
  24. package/pack/.claude/agents/devrites-slice-wright.md +5 -1
  25. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  26. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  27. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  28. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +3 -0
  29. package/pack/.claude/skills/devrites-lib/reference/standards/gates.md +3 -0
  30. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +7 -0
  31. package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  32. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  33. package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -2
  34. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +1 -1
  35. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  36. package/pack/.claude/skills/rite-build/SKILL.md +7 -7
  37. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +2 -2
  38. package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
  39. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  40. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +28 -21
  41. package/pack/.claude/skills/rite-build/reference/phase-contract.md +10 -1
  42. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  43. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +9 -2
  44. package/pack/.claude/skills/rite-define/SKILL.md +11 -2
  45. package/pack/.claude/skills/rite-handoff/SKILL.md +1 -1
  46. package/pack/.claude/skills/rite-plan/SKILL.md +20 -9
  47. package/pack/.claude/skills/rite-plan/reference/slicing.md +5 -0
  48. package/pack/.claude/skills/rite-polish/SKILL.md +6 -4
  49. package/pack/.claude/skills/rite-prove/SKILL.md +3 -2
  50. package/pack/.claude/skills/rite-review/SKILL.md +6 -1
  51. package/pack/.claude/skills/rite-seal/SKILL.md +2 -2
  52. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +7 -1
  53. package/pack/.claude/skills/rite-vet/SKILL.md +9 -5
  54. package/pack/.claude/skills/rite-vet/reference/artifacts.md +8 -0
  55. package/pack/.claude/skills/rite-vet/reference/review-axes.md +1 -1
  56. package/pack/generated/claude/agents/devrites-plan-drafter.md +13 -3
  57. package/pack/generated/claude/agents/devrites-slice-wright.md +5 -1
  58. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  59. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  60. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  61. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +3 -0
  62. package/pack/generated/claude/skills/devrites-lib/reference/standards/gates.md +3 -0
  63. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +7 -0
  64. package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  65. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  66. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -2
  67. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +1 -1
  68. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  69. package/pack/generated/claude/skills/rite-build/SKILL.md +7 -7
  70. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +2 -2
  71. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
  72. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  73. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +28 -21
  74. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +10 -1
  75. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  76. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +9 -2
  77. package/pack/generated/claude/skills/rite-define/SKILL.md +11 -2
  78. package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -1
  79. package/pack/generated/claude/skills/rite-plan/SKILL.md +20 -9
  80. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +5 -0
  81. package/pack/generated/claude/skills/rite-polish/SKILL.md +6 -4
  82. package/pack/generated/claude/skills/rite-prove/SKILL.md +3 -2
  83. package/pack/generated/claude/skills/rite-review/SKILL.md +6 -1
  84. package/pack/generated/claude/skills/rite-seal/SKILL.md +2 -2
  85. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +7 -1
  86. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -5
  87. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +8 -0
  88. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +1 -1
  89. package/pack/generated/codex/agents/devrites-plan-drafter.toml +13 -3
  90. package/pack/generated/codex/agents/devrites-slice-wright.toml +5 -1
  91. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  92. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  93. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -2
  94. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +3 -0
  95. package/pack/generated/codex/skills/devrites-lib/reference/standards/gates.md +3 -0
  96. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +7 -0
  97. package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  98. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  99. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -2
  100. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +1 -1
  101. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  102. package/pack/generated/codex/skills/rite-build/SKILL.md +7 -7
  103. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +2 -2
  104. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
  105. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
  106. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +28 -21
  107. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +10 -1
  108. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -2
  109. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +9 -2
  110. package/pack/generated/codex/skills/rite-define/SKILL.md +11 -2
  111. package/pack/generated/codex/skills/rite-handoff/SKILL.md +1 -1
  112. package/pack/generated/codex/skills/rite-plan/SKILL.md +20 -9
  113. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +5 -0
  114. package/pack/generated/codex/skills/rite-polish/SKILL.md +6 -4
  115. package/pack/generated/codex/skills/rite-prove/SKILL.md +3 -2
  116. package/pack/generated/codex/skills/rite-review/SKILL.md +6 -1
  117. package/pack/generated/codex/skills/rite-seal/SKILL.md +2 -2
  118. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +7 -1
  119. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -5
  120. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +8 -0
  121. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +1 -1
  122. package/pack/generated/devin/agents/devrites-plan-drafter.md +13 -3
  123. package/pack/generated/devin/agents/devrites-slice-wright.md +5 -1
  124. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  125. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  126. package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +6 -2
  127. package/pack/generated/devin/skills/devrites-lib/reference/standards/core.md +3 -0
  128. package/pack/generated/devin/skills/devrites-lib/reference/standards/gates.md +3 -0
  129. package/pack/generated/devin/skills/devrites-lib/reference/standards/testing.md +7 -0
  130. package/pack/generated/devin/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  131. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  132. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +2 -2
  133. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +1 -1
  134. package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  135. package/pack/generated/devin/skills/rite-build/SKILL.md +7 -7
  136. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +2 -2
  137. package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
  138. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
  139. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +28 -21
  140. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +10 -1
  141. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -2
  142. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +9 -2
  143. package/pack/generated/devin/skills/rite-define/SKILL.md +11 -2
  144. package/pack/generated/devin/skills/rite-handoff/SKILL.md +1 -1
  145. package/pack/generated/devin/skills/rite-plan/SKILL.md +20 -9
  146. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +5 -0
  147. package/pack/generated/devin/skills/rite-polish/SKILL.md +6 -4
  148. package/pack/generated/devin/skills/rite-prove/SKILL.md +3 -2
  149. package/pack/generated/devin/skills/rite-review/SKILL.md +6 -1
  150. package/pack/generated/devin/skills/rite-seal/SKILL.md +2 -2
  151. package/pack/generated/devin/skills/rite-seal/reference/phase-contract.md +7 -1
  152. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -5
  153. package/pack/generated/devin/skills/rite-vet/reference/artifacts.md +8 -0
  154. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +1 -1
  155. package/pack/generated/omp/agents/devrites-plan-drafter.md +13 -3
  156. package/pack/generated/omp/agents/devrites-slice-wright.md +5 -1
  157. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  158. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  159. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +6 -2
  160. package/pack/generated/omp/skills/devrites-lib/reference/standards/core.md +3 -0
  161. package/pack/generated/omp/skills/devrites-lib/reference/standards/gates.md +3 -0
  162. package/pack/generated/omp/skills/devrites-lib/reference/standards/testing.md +7 -0
  163. package/pack/generated/omp/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  164. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  165. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -2
  166. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +1 -1
  167. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  168. package/pack/generated/omp/skills/rite-build/SKILL.md +7 -7
  169. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +2 -2
  170. package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
  171. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
  172. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +28 -21
  173. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +10 -1
  174. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -2
  175. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +9 -2
  176. package/pack/generated/omp/skills/rite-define/SKILL.md +11 -2
  177. package/pack/generated/omp/skills/rite-handoff/SKILL.md +1 -1
  178. package/pack/generated/omp/skills/rite-plan/SKILL.md +20 -9
  179. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +5 -0
  180. package/pack/generated/omp/skills/rite-polish/SKILL.md +6 -4
  181. package/pack/generated/omp/skills/rite-prove/SKILL.md +3 -2
  182. package/pack/generated/omp/skills/rite-review/SKILL.md +6 -1
  183. package/pack/generated/omp/skills/rite-seal/SKILL.md +2 -2
  184. package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +7 -1
  185. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -5
  186. package/pack/generated/omp/skills/rite-vet/reference/artifacts.md +8 -0
  187. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +1 -1
  188. package/pack/generated/pi/agents/devrites-plan-drafter.md +13 -3
  189. package/pack/generated/pi/agents/devrites-slice-wright.md +5 -1
  190. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  191. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  192. package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +6 -2
  193. package/pack/generated/pi/skills/devrites-lib/reference/standards/core.md +3 -0
  194. package/pack/generated/pi/skills/devrites-lib/reference/standards/gates.md +3 -0
  195. package/pack/generated/pi/skills/devrites-lib/reference/standards/testing.md +7 -0
  196. package/pack/generated/pi/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  197. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  198. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +2 -2
  199. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +1 -1
  200. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  201. package/pack/generated/pi/skills/rite-build/SKILL.md +7 -7
  202. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +2 -2
  203. package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
  204. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
  205. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +28 -21
  206. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +10 -1
  207. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -2
  208. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +9 -2
  209. package/pack/generated/pi/skills/rite-define/SKILL.md +11 -2
  210. package/pack/generated/pi/skills/rite-handoff/SKILL.md +1 -1
  211. package/pack/generated/pi/skills/rite-plan/SKILL.md +20 -9
  212. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +5 -0
  213. package/pack/generated/pi/skills/rite-polish/SKILL.md +6 -4
  214. package/pack/generated/pi/skills/rite-prove/SKILL.md +3 -2
  215. package/pack/generated/pi/skills/rite-review/SKILL.md +6 -1
  216. package/pack/generated/pi/skills/rite-seal/SKILL.md +2 -2
  217. package/pack/generated/pi/skills/rite-seal/reference/phase-contract.md +7 -1
  218. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -5
  219. package/pack/generated/pi/skills/rite-vet/reference/artifacts.md +8 -0
  220. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +1 -1
  221. package/package.json +1 -1
@@ -2,8 +2,11 @@ package lib
2
2
 
3
3
  import (
4
4
  "fmt"
5
+ "path"
6
+ "sort"
5
7
  "strings"
6
8
 
9
+ "github.com/devrites/devrites/internal/parallel"
7
10
  "github.com/devrites/devrites/internal/state"
8
11
  )
9
12
 
@@ -27,6 +30,127 @@ func CheckTaskGraph(root, slug string) (TaskGraphResult, error) {
27
30
  return ParseTaskGraph(raw), nil
28
31
  }
29
32
 
33
+ // hubAdvisoryMin is how many slices must write one path before it is flagged:
34
+ // slices sharing a write path never share a parallel build round.
35
+ const hubAdvisoryMin = 3
36
+
37
+ // hubAdvisoryShown caps printed hub lines; the metric still counts every hub.
38
+ const hubAdvisoryShown = 5
39
+
40
+ // sliceWritePaths returns each slice's write set (Writer allowlist, else Files
41
+ // likely touched) and whether it records a `Serial rationale:`.
42
+ func sliceWritePaths(tasksMarkdown []byte, graph TaskGraphResult) (map[string][]string, map[string]bool) {
43
+ paths := make(map[string][]string, len(graph.Slices))
44
+ serial := make(map[string]bool)
45
+ for _, s := range graph.Slices {
46
+ block, _ := state.ExtractTaskSlice(tasksMarkdown, s.ID)
47
+ fields := sliceFields(block)
48
+ allow, _ := fieldValue(fields, "writer allowlist")
49
+ paths[s.ID] = pathTokens(allow)
50
+ if len(paths[s.ID]) == 0 {
51
+ touched, _ := fieldValue(fields, "files likely touched")
52
+ paths[s.ID] = pathTokens(touched)
53
+ }
54
+ if v, _ := fieldValue(fields, "serial rationale"); v != "" {
55
+ serial[s.ID] = true
56
+ }
57
+ }
58
+ return paths, serial
59
+ }
60
+
61
+ // pathTokens keeps the list entries that can be paths; prose and Markdown links
62
+ // (anything with whitespace) are not write paths.
63
+ func pathTokens(raw string) []string {
64
+ var out []string
65
+ for _, p := range parsePathList(raw) {
66
+ if !strings.ContainsAny(p, " \t") {
67
+ out = append(out, p)
68
+ }
69
+ }
70
+ return out
71
+ }
72
+
73
+ // scheduleRounds replays /rite-build batch selection at the maximum cap: each
74
+ // round runs the plan-order path-disjoint subset of dependency-ready slices.
75
+ // It returns 0 when a slice has no write paths or parallel select rejects them.
76
+ func scheduleRounds(graph TaskGraphResult, paths map[string][]string, root string) int {
77
+ done := make(map[string]bool, len(graph.Slices))
78
+ rounds := 0
79
+ for len(done) < len(graph.Slices) {
80
+ var ready []parallel.SlicePaths
81
+ for _, s := range graph.Slices {
82
+ if done[s.ID] || !allDone(s.Dependencies, done) {
83
+ continue
84
+ }
85
+ if len(paths[s.ID]) == 0 {
86
+ return 0
87
+ }
88
+ ready = append(ready, parallel.SlicePaths{ID: s.ID, Paths: paths[s.ID]})
89
+ }
90
+ if len(ready) == 0 {
91
+ return 0
92
+ }
93
+ picked, err := parallel.SelectGreedy(parallel.MaxParallelSlices, ready, root)
94
+ if err != nil || len(picked) == 0 {
95
+ return 0
96
+ }
97
+ for _, p := range picked {
98
+ done[p.ID] = true
99
+ }
100
+ rounds++
101
+ }
102
+ return rounds
103
+ }
104
+
105
+ func allDone(ids []string, done map[string]bool) bool {
106
+ for _, id := range ids {
107
+ if !done[id] {
108
+ return false
109
+ }
110
+ }
111
+ return true
112
+ }
113
+
114
+ // hubAdvisories names each path written by hubAdvisoryMin+ slices that carry
115
+ // no `Serial rationale:`, most-shared first.
116
+ func hubAdvisories(graph TaskGraphResult, paths map[string][]string, serial map[string]bool) []string {
117
+ writers := map[string][]string{}
118
+ shown := map[string]string{} // first spelling seen, for display
119
+ for _, s := range graph.Slices {
120
+ if serial[s.ID] {
121
+ continue
122
+ }
123
+ seen := map[string]bool{}
124
+ for _, p := range paths[s.ID] {
125
+ key := strings.ToLower(path.Clean(strings.ReplaceAll(p, `\`, "/")))
126
+ if !seen[key] {
127
+ seen[key] = true
128
+ writers[key] = append(writers[key], s.ID)
129
+ if _, ok := shown[key]; !ok {
130
+ shown[key] = p
131
+ }
132
+ }
133
+ }
134
+ }
135
+ hubs := make([]string, 0, len(writers))
136
+ for p, ids := range writers {
137
+ if len(ids) >= hubAdvisoryMin {
138
+ hubs = append(hubs, p)
139
+ }
140
+ }
141
+ sort.Slice(hubs, func(i, j int) bool {
142
+ if len(writers[hubs[i]]) != len(writers[hubs[j]]) {
143
+ return len(writers[hubs[i]]) > len(writers[hubs[j]])
144
+ }
145
+ return hubs[i] < hubs[j]
146
+ })
147
+ lines := make([]string, len(hubs))
148
+ for i, p := range hubs {
149
+ lines[i] = fmt.Sprintf("ADVISORY hub: %s is written by %d slices (%s), so they build one at a time; move the shared edit into one seam slice they depend on, or record `Serial rationale:`", shown[p], len(writers[p]), strings.Join(writers[p], ", "))
150
+ }
151
+ return lines
152
+ }
153
+
30
154
  // edgeAdvisories names each dependency edge that does not say what crosses it:
31
155
  // no `(via <interface>)`, or a via the dependency's Consumes / Produces lacks.
32
156
  // An edge the dependent reads nothing across only serializes work.
@@ -74,18 +74,23 @@ func runPathDisjoint(args []string, stdin io.Reader, stdout, stderr io.Writer) i
74
74
 
75
75
  func cmdSelect(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
76
76
  root := ""
77
+ slug := ""
77
78
  jsonPath := "-"
78
79
  capN := 0
79
80
  sawCap := false
80
81
  for i := 0; i < len(args); i++ {
81
82
  switch args[i] {
82
- case "--root":
83
+ case "--root", "--slug":
83
84
  if i+1 >= len(args) {
84
85
  fmt.Fprintln(stderr, usageSelect)
85
86
  return ExitUsage
86
87
  }
87
88
  i++
88
- root = args[i]
89
+ if args[i-1] == "--root" {
90
+ root = args[i]
91
+ } else {
92
+ slug = args[i]
93
+ }
89
94
  case "--cap":
90
95
  if i+1 >= len(args) {
91
96
  fmt.Fprintln(stderr, usageSelect)
@@ -124,7 +129,33 @@ func cmdSelect(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
124
129
  fmt.Fprintf(stderr, "parallel select: %v\n", err)
125
130
  return ExitBlocked
126
131
  }
127
- selected, err := SelectGreedy(capN, slices, root)
132
+ var seed []SlicePaths
133
+ running := -1
134
+ if slug != "" {
135
+ lease, err := runningLease(root, slug)
136
+ if err != nil {
137
+ fmt.Fprintf(stderr, "parallel select: %v\n", err)
138
+ return ExitBlocked
139
+ }
140
+ if lease != nil {
141
+ // The lease's active slices keep their slots and paths; nothing it
142
+ // already names (active or landed) is picked again.
143
+ seed = activeSlices(lease)
144
+ running = len(seed)
145
+ named := make(map[string]bool, len(lease.Slices))
146
+ for _, sl := range lease.Slices {
147
+ named[sl.ID] = true
148
+ }
149
+ var fresh []SlicePaths
150
+ for _, s := range slices {
151
+ if !named[s.ID] {
152
+ fresh = append(fresh, s)
153
+ }
154
+ }
155
+ slices = fresh
156
+ }
157
+ }
158
+ selected, err := SelectGreedySeeded(capN, seed, slices, root)
128
159
  if err != nil {
129
160
  fmt.Fprintf(stderr, "parallel select: %v\n", err)
130
161
  if strings.Contains(err.Error(), "cap must be") {
@@ -132,10 +163,30 @@ func cmdSelect(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
132
163
  }
133
164
  return ExitBlocked
134
165
  }
135
- fmt.Fprintln(stdout, formatSelect(selected))
166
+ fmt.Fprintln(stdout, formatSelect(selected, running))
136
167
  return ExitOK
137
168
  }
138
169
 
170
+ // runningLease returns slug's lease when it is still in flight
171
+ // (running|integrate-failed), or nil when there is none to seed from.
172
+ func runningLease(root, slug string) (*Lease, error) {
173
+ path, err := LeasePath(root, slug)
174
+ if err != nil {
175
+ return nil, err
176
+ }
177
+ lease, err := ReadLease(path)
178
+ if os.IsNotExist(err) {
179
+ return nil, nil
180
+ }
181
+ if err != nil {
182
+ return nil, err
183
+ }
184
+ if lease.Status != StatusRunning && lease.Status != StatusIntegrateFailed {
185
+ return nil, nil
186
+ }
187
+ return lease, nil
188
+ }
189
+
139
190
  func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
140
191
  if len(args) == 0 {
141
192
  fmt.Fprintln(stderr, parallelUsage())
@@ -145,6 +196,8 @@ func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
145
196
  switch sub {
146
197
  case "create":
147
198
  return cmdCreate(rest, stdin, stdout, stderr)
199
+ case "add":
200
+ return cmdAdd(rest, stdin, stdout, stderr)
148
201
  case "record-green":
149
202
  return cmdRecordGreen(rest, stdout, stderr)
150
203
  case "abort":
@@ -174,16 +227,17 @@ func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
174
227
 
175
228
  const (
176
229
  usageCreate = "usage: parallel create --root --slug --batch --base --json"
230
+ usageAdd = "usage: parallel add --root --slug --json <json-file>|-"
177
231
  usageRecordGreen = "usage: parallel record-green --root --slug --slice --commit"
178
232
  usageAbort = "usage: parallel abort --root --slug"
179
- usageIntegrate = "usage: parallel integrate --root --slug [--apply-to-control]"
233
+ usageIntegrate = "usage: parallel integrate --root --slug [--apply-to-control [--slice <id>]]"
180
234
  usageCleanup = "usage: parallel cleanup --root --slug [--force]"
181
235
  usageStatus = "usage: parallel status --root --slug"
182
236
  usageLeaseWrite = "usage: parallel lease-write --root --slug --json"
183
237
  usageLeaseRead = "usage: parallel lease-read --root --slug [--field name]"
184
238
  usageLeaseClear = "usage: parallel lease-clear --root --slug"
185
239
  usagePathDisjoint = "usage: check path-disjoint [--root <dir>] [<json-file>|-]"
186
- usageSelect = "usage: parallel select --cap <1-10> [--root <dir>] [<json-file>|-]"
240
+ usageSelect = "usage: parallel select --cap <1-10> [--root <dir>] [--slug <slug>] [<json-file>|-]"
187
241
  )
188
242
 
189
243
  func parallelUsage() string {
@@ -191,15 +245,16 @@ func parallelUsage() string {
191
245
 
192
246
  Subcommands:
193
247
  create --root --slug --batch --base --json
248
+ add --root --slug --json
194
249
  record-green --root --slug --slice --commit
195
250
  abort --root --slug
196
- integrate --root --slug [--apply-to-control]
251
+ integrate --root --slug [--apply-to-control [--slice <id>]]
197
252
  cleanup --root --slug [--force]
198
253
  status --root --slug
199
254
  lease-write --root --slug --json
200
255
  lease-read --root --slug [--field name]
201
256
  lease-clear --root --slug
202
- select --cap <1-10> [--root] [<json-file>|-]
257
+ select --cap <1-10> [--root] [--slug] [<json-file>|-]
203
258
 
204
259
  Exit codes: 0 ok, 2 usage, 3 blocked`)
205
260
  }
@@ -210,6 +265,8 @@ func CommandUsage(name string) string {
210
265
  switch name {
211
266
  case "create":
212
267
  return usageCreate
268
+ case "add":
269
+ return usageAdd
213
270
  case "record-green":
214
271
  return usageRecordGreen
215
272
  case "abort":
@@ -414,6 +471,41 @@ func cmdCreate(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
414
471
  return ExitOK
415
472
  }
416
473
 
474
+ func cmdAdd(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
475
+ f, code, done := parseFlagsOrHelp(args, stdout, stderr, usageAdd)
476
+ if done {
477
+ return code
478
+ }
479
+ if code := requireRootSlug(f, stderr); code != ExitOK {
480
+ return code
481
+ }
482
+ if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
483
+ return code
484
+ }
485
+ if f.JSON == "" {
486
+ fmt.Fprintln(stderr, usageAdd)
487
+ return ExitUsage
488
+ }
489
+ data, err := readJSONInput(f.JSON, stdin)
490
+ if err != nil {
491
+ fmt.Fprintf(stderr, "parallel add: %v\n", err)
492
+ return ExitBlocked
493
+ }
494
+ slices, err := ParseSlicesJSON(data)
495
+ if err != nil {
496
+ fmt.Fprintf(stderr, "parallel add: %v\n", err)
497
+ return ExitBlocked
498
+ }
499
+ lease, err := Add(AddOpts{Root: f.Root, Slug: f.Slug, Slices: slices})
500
+ if err != nil {
501
+ fmt.Fprintf(stderr, "parallel add: %v\n", err)
502
+ return ExitBlocked
503
+ }
504
+ fmt.Fprintf(stdout, "added: %d worktrees; batch=%s active=%d base=%s\n",
505
+ len(slices), lease.BatchID, len(activeSlices(lease)), lease.Slices[len(lease.Slices)-1].BaseSHA)
506
+ return ExitOK
507
+ }
508
+
417
509
  // rejectUnsupportedFlags reports flags a subcommand parses but must not
418
510
  // silently ignore: --force only exists for cleanup, --apply-to-control only
419
511
  // for integrate.
@@ -484,16 +576,26 @@ func cmdIntegrate(args []string, stdout, stderr io.Writer) int {
484
576
  if code := rejectUnsupportedFlags(f, stderr, false, true); code != ExitOK {
485
577
  return code
486
578
  }
579
+ if f.Slice != "" && !f.ApplyToControl {
580
+ fmt.Fprintln(stderr, "parallel integrate: --slice requires --apply-to-control")
581
+ fmt.Fprintln(stderr, usageIntegrate)
582
+ return ExitUsage
583
+ }
487
584
  tip, lease, err := Integrate(IntegrateOpts{
488
585
  Root: f.Root,
489
586
  Slug: f.Slug,
587
+ Slice: f.Slice,
490
588
  ApplyToControl: f.ApplyToControl,
491
589
  })
492
590
  if err != nil {
493
591
  fmt.Fprintf(stderr, "parallel integrate: %v\n", err)
494
592
  return ExitBlocked
495
593
  }
496
- fmt.Fprintf(stdout, "integrate-ok: tip=%s status=%s apply_to_control=%v\n", tip, lease.Status, f.ApplyToControl)
594
+ fmt.Fprintf(stdout, "integrate-ok: tip=%s status=%s apply_to_control=%v", tip, lease.Status, f.ApplyToControl)
595
+ if f.Slice != "" {
596
+ fmt.Fprintf(stdout, " slice=%s", f.Slice)
597
+ }
598
+ fmt.Fprintln(stdout)
497
599
  return ExitOK
498
600
  }
499
601
 
@@ -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