devrites 7.3.3 → 7.3.5

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 (72) hide show
  1. package/CHANGELOG.md +44 -3
  2. package/README.md +21 -1
  3. package/docs/adr/0039-autocomplete-structural-admission.md +87 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/cli.md +182 -7
  6. package/docs/command-map.md +2 -2
  7. package/docs/engine/commands.md +53 -3
  8. package/engine/commands.go +1 -1
  9. package/engine/help.go +3 -1
  10. package/engine/internal/lib/claims.go +65 -8
  11. package/engine/internal/lib/continuation.go +38 -4
  12. package/engine/internal/lib/dispatch.go +124 -63
  13. package/engine/internal/lib/frontier.go +864 -0
  14. package/engine/internal/lib/frontier_controller.go +652 -0
  15. package/engine/internal/lib/frontier_roadmap.go +168 -0
  16. package/engine/internal/lib/handoff.go +2 -5
  17. package/engine/internal/lib/next.go +19 -0
  18. package/engine/internal/parallel/cli.go +16 -5
  19. package/engine/internal/parallel/ops.go +7 -5
  20. package/engine/internal/parallel/repository.go +23 -0
  21. package/engine/internal/release/release.go +96 -5
  22. package/engine/main.go +4 -0
  23. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  24. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  25. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  26. package/pack/.claude/skills/rite-autocomplete/SKILL.md +33 -38
  27. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +46 -52
  28. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  29. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +19 -17
  30. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  31. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  32. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  33. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +33 -38
  34. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +46 -52
  35. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  36. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +19 -17
  37. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  38. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  39. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  40. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +33 -38
  41. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +46 -52
  42. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  43. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +19 -17
  44. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  45. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  46. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  47. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +33 -38
  48. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +46 -52
  49. package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  50. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +19 -17
  51. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  52. package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  53. package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  54. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +33 -38
  55. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +46 -52
  56. package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  57. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +19 -17
  58. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  59. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  60. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  61. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +33 -38
  62. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +46 -52
  63. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  64. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +19 -17
  65. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +106 -114
  66. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  67. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  68. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +33 -38
  69. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +46 -52
  70. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  71. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +19 -17
  72. package/package.json +1 -1
@@ -0,0 +1,652 @@
1
+ package lib
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "regexp"
8
+ "sort"
9
+ "strings"
10
+
11
+ "github.com/devrites/devrites/internal/devritespaths"
12
+ "github.com/devrites/devrites/internal/markdowntext"
13
+ "github.com/devrites/devrites/internal/parallel"
14
+ "github.com/devrites/devrites/internal/state"
15
+ )
16
+
17
+ func autocompleteMode(root string) (bool, error) {
18
+ raw, err := readBoundedRegularFile(filepath.Join(root, "AFK"), workspaceArtifactLimit)
19
+ if os.IsNotExist(err) {
20
+ return false, nil
21
+ }
22
+ if err != nil {
23
+ return false, err
24
+ }
25
+ seen := false
26
+ for _, line := range strings.Split(string(raw), "\n") {
27
+ key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
28
+ if !ok || strings.TrimSpace(key) != "mode" {
29
+ continue
30
+ }
31
+ if seen {
32
+ return false, fmt.Errorf("duplicate AFK mode; normalize controller identity")
33
+ }
34
+ seen = true
35
+ value = strings.TrimSpace(value)
36
+ if i := strings.Index(value, " #"); i >= 0 {
37
+ value = strings.TrimSpace(value[:i])
38
+ }
39
+ if value != "autocomplete" {
40
+ return false, fmt.Errorf("invalid AFK mode; expected autocomplete")
41
+ }
42
+ }
43
+ return seen, nil
44
+ }
45
+
46
+ func frontierRelated(nodes map[string]*frontierWorkspace, chain []string) ([]string, map[string]bool) {
47
+ related := map[string]bool{chain[len(chain)-1]: true}
48
+ for range maxSequenceChain {
49
+ changed := false
50
+ for name, n := range nodes {
51
+ if related[n.parent] && !related[name] {
52
+ related[name], changed = true, true
53
+ }
54
+ }
55
+ if !changed {
56
+ break
57
+ }
58
+ }
59
+ ordered := make([]string, 0, len(related))
60
+ depth := make(map[string]int, len(related))
61
+ for name := range related {
62
+ ordered = append(ordered, name)
63
+ ancestors, _ := frontierChain(nodes, name)
64
+ depth[name] = len(ancestors)
65
+ }
66
+ sort.Slice(ordered, func(i, j int) bool {
67
+ a, b := nodes[ordered[i]], nodes[ordered[j]]
68
+ if depth[a.slug] != depth[b.slug] {
69
+ return depth[a.slug] < depth[b.slug]
70
+ }
71
+ if a.position != b.position {
72
+ return a.position < b.position
73
+ }
74
+ return a.slug < b.slug
75
+ })
76
+ return ordered, related
77
+ }
78
+
79
+ type frontierRecords struct {
80
+ blocked map[string]string
81
+ serial map[string]string
82
+ }
83
+
84
+ var frontierBlockedRe = regexp.MustCompile("(?m)^[ \\t]*(?:[-*][ \\t]+)?frontier-blocked:[ \\t]+([a-zA-Z0-9][a-zA-Z0-9._-]*)[ \\t]+—[ \\t]+([^\\s`][^\\n]*|`[^\\s`][^\\n]*)")
85
+
86
+ func frontierEvidence(root, slug string) (frontierRecords, error) {
87
+ records := frontierRecords{}
88
+ raw, err := readSequenceArtifact(root, slug, "evidence.md")
89
+ if os.IsNotExist(err) {
90
+ return records, nil
91
+ }
92
+ if err != nil {
93
+ return records, err
94
+ }
95
+ raw, err = markdowntext.Structural(raw)
96
+ if err != nil {
97
+ return records, err
98
+ }
99
+ text := string(raw)
100
+ for _, match := range frontierBlockedRe.FindAllStringSubmatch(text, -1) {
101
+ if devritespaths.ValidSlug(match[1]) {
102
+ if records.blocked == nil {
103
+ records.blocked = map[string]string{}
104
+ }
105
+ records.blocked[match[1]] = strings.TrimSpace(match[2])
106
+ }
107
+ }
108
+ for _, match := range serialRecordRe.FindAllStringSubmatch(text, -1) {
109
+ if !serialReasonWaives(match[2]) {
110
+ continue
111
+ }
112
+ for _, token := range strings.FieldsFunc(match[1], func(r rune) bool { return r == ',' || r == ' ' || r == '\t' || r == '`' }) {
113
+ workspace, id, qualified := strings.Cut(token, "/")
114
+ if !qualified {
115
+ id, workspace = workspace, ""
116
+ }
117
+ id = strings.ToUpper(id)
118
+ if !state.ValidSliceID(id) || qualified && !devritespaths.ValidSlug(workspace) {
119
+ continue
120
+ }
121
+ if records.serial == nil {
122
+ records.serial = map[string]string{}
123
+ }
124
+ records.serial[parallel.SliceKey(workspace, "SLICE-"+sliceNumber(id))] = strings.TrimSpace(match[2])
125
+ }
126
+ }
127
+ return records, nil
128
+ }
129
+
130
+ func frontierSerialReason(records frontierRecords, source, workspace, id string) string {
131
+ if source == workspace {
132
+ workspace = ""
133
+ }
134
+ return records.serial[parallel.SliceKey(workspace, "SLICE-"+sliceNumber(id))]
135
+ }
136
+
137
+ func frontierPendingPreparation(n *frontierWorkspace) (bool, error) {
138
+ raw, err := readBoundedRegularFile(filepath.Join(n.dir, "dispatch.json"), workspaceArtifactLimit)
139
+ if os.IsNotExist(err) {
140
+ return false, nil
141
+ }
142
+ if err != nil {
143
+ return false, err
144
+ }
145
+ dispatch, err := decodeDispatch(raw)
146
+ if err != nil {
147
+ return false, err
148
+ }
149
+ for _, wave := range dispatch.Waves {
150
+ if wave.State != "open" && wave.State != "sealed" {
151
+ continue
152
+ }
153
+ for role, actor := range wave.Roles {
154
+ if actor.Handle == "" || actor.StartedAt == "" || actor.Returned {
155
+ continue
156
+ }
157
+ base := roleBase(role)
158
+ switch strings.ToLower(wave.Phase) {
159
+ case "spec", "clarify", "temper":
160
+ if base == "evidence-scout" || base == "strategy-reviewer" {
161
+ return true, nil
162
+ }
163
+ case "define", "plan":
164
+ if base == "plan-drafter" || base == "evidence-scout" {
165
+ return true, nil
166
+ }
167
+ case "vet":
168
+ if base == "plan-reviewer" || base == "devex-reviewer" || base == "strategy-reviewer" || base == "code-reviewer" {
169
+ return true, nil
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return false, nil
175
+ }
176
+
177
+ func frontierSuccessor(n *frontierWorkspace, nodes map[string]*frontierWorkspace, mapping map[int]string) (string, error) {
178
+ target := frontierSpecTarget(n)
179
+ if mapped := mapping[n.position+1]; n.hasPosition && mapped != "" {
180
+ if target != "" && mapped != target {
181
+ return "", fmt.Errorf("next Spec conflicts with recorded successor; native authority-normalization required")
182
+ }
183
+ target = mapped
184
+ }
185
+ for _, child := range nodes {
186
+ if child.parent != n.slug || !n.hasPosition || !child.hasPosition || child.position != n.position+1 {
187
+ continue
188
+ }
189
+ if target != "" && target != child.slug {
190
+ return "", fmt.Errorf("conflicting successor identities; native authority-normalization required")
191
+ }
192
+ target = child.slug
193
+ }
194
+ if child := nodes[target]; child != nil && child.parent != n.slug {
195
+ return "", fmt.Errorf("successor %s is not bound to source %s; native authority-normalization required", target, n.slug)
196
+ }
197
+ return target, nil
198
+ }
199
+
200
+ func frontierPreparationAdmission(root, source string, nodes map[string]*frontierWorkspace, mapping map[int]string, problem string, allow bool, records frontierRecords) ([]string, error) {
201
+ n := nodes[source]
202
+ if !allow || n.hasRemaining && n.remaining == 0 {
203
+ return nil, nil
204
+ }
205
+ if hold, err := frontierQuestionHold(n); err != nil || hold != "" {
206
+ return nil, err
207
+ }
208
+ target, err := frontierSuccessor(n, nodes, mapping)
209
+ for _, identity := range [...]string{frontierSpecTarget(n), mapping[n.position+1], target} {
210
+ if child := nodes[identity]; child != nil && child.problem != nil {
211
+ return nil, child.problem
212
+ }
213
+ }
214
+ if problem != "" || err != nil || target == "" && n.hasRemaining && n.remaining > 0 {
215
+ if reason := records.blocked["sequence"]; reason != "" {
216
+ return []string{"frontier-blocked: sequence — " + reason + "; scheduling deferred, not normalized"}, nil
217
+ }
218
+ reason := problem
219
+ if err != nil {
220
+ reason = err.Error()
221
+ }
222
+ if reason == "" {
223
+ reason = "positive remaining budget has no exact recorded successor"
224
+ }
225
+ return nil, fmt.Errorf("source %s native authority-normalization required before writer admission: %s; read sequence artifacts, record current roadmap through its owning decision, or retain frontier-blocked: sequence — <specific constraint>", source, reason)
226
+ }
227
+ if target == "" {
228
+ return nil, nil
229
+ }
230
+ if child := nodes[target]; child != nil {
231
+ if _, err := frontierChain(nodes, target); err != nil {
232
+ return nil, err
233
+ }
234
+ }
235
+ if reason := records.blocked[target]; reason != "" {
236
+ return []string{"frontier-blocked: " + target + " — " + reason + "; preparation deferred, not approved"}, nil
237
+ }
238
+ if child := nodes[target]; child != nil {
239
+ if hold, err := frontierQuestionHold(child); err != nil || hold != "" {
240
+ return nil, err
241
+ }
242
+ if frontierTerminal(child) {
243
+ return nil, nil
244
+ }
245
+ if pending, err := frontierPendingPreparation(child); err != nil || pending {
246
+ return nil, err
247
+ }
248
+ ready, notices, err := WorkspaceReady(filepath.Dir(root), source, target)
249
+ if err != nil {
250
+ return nil, err
251
+ }
252
+ if len(ready) > 0 || len(notices) == 0 {
253
+ var diagnostics []string
254
+ for _, notice := range notices {
255
+ diagnostics = append(diagnostics, "frontier-readiness: "+notice)
256
+ }
257
+ return diagnostics, nil
258
+ }
259
+ dependenciesOnly := true
260
+ for _, notice := range notices {
261
+ dependenciesOnly = dependenciesOnly && strings.Contains(notice, ": waiting for durable BUILT/integrated producer ")
262
+ }
263
+ if dependenciesOnly {
264
+ return nil, nil
265
+ }
266
+ }
267
+ return nil, fmt.Errorf("successor %s preparation has not actually started; invoke /rite-spec %s or its native Plan/Vet cursor and retain launched planning/review handles before /rite-build %s; alternatively retain frontier-blocked: %s — <specific constraint> in evidence.md", target, frontierQuote(target), source, target)
268
+ }
269
+
270
+ func frontierNormalizePaths(paths []string) error {
271
+ for i, path := range paths {
272
+ normalized, err := parallel.NormalizePath(path)
273
+ if err != nil {
274
+ return err
275
+ }
276
+ paths[i] = normalized
277
+ }
278
+ return nil
279
+ }
280
+
281
+ func frontierSamePaths(admitted, reserved []string) bool {
282
+ if len(admitted) == 0 || len(admitted) != len(reserved) {
283
+ return false
284
+ }
285
+ for _, reservedPath := range reserved {
286
+ found := false
287
+ for _, path := range admitted {
288
+ found = found || strings.EqualFold(path, reservedPath)
289
+ }
290
+ if !found {
291
+ return false
292
+ }
293
+ }
294
+ return true
295
+ }
296
+
297
+ func frontierLeaseReserved(lease *parallel.Lease, controller, source string, ids []string, ready []parallel.SlicePaths) bool {
298
+ if lease == nil || lease.Status != parallel.StatusRunning && lease.Status != parallel.StatusIntegrateFailed || lease.PendingLanding != nil {
299
+ return false
300
+ }
301
+ for _, id := range ids {
302
+ found := false
303
+ for _, row := range lease.Slices {
304
+ workspace := row.Workspace
305
+ if workspace == "" {
306
+ workspace = controller
307
+ }
308
+ if workspace != source || sliceNumber(row.ID) != sliceNumber(id) || row.WrightStatus == parallel.WrightIntegrated {
309
+ continue
310
+ }
311
+ for _, candidate := range ready {
312
+ if sliceNumber(candidate.ID) == sliceNumber(id) && frontierSamePaths(candidate.Paths, row.Paths) {
313
+ found = true
314
+ }
315
+ }
316
+ }
317
+ if !found {
318
+ return false
319
+ }
320
+ }
321
+ return len(ids) > 0
322
+ }
323
+
324
+ func frontierWriterRetention(root, source, controller, binding string, lease *parallel.Lease, nodes map[string]*frontierWorkspace, ordered, ids []string, current *dispatchFile) error {
325
+ var claims map[string]*claimRecord
326
+ for _, slug := range ordered {
327
+ if _, err := frontierChain(nodes, slug); err != nil {
328
+ return err
329
+ }
330
+ dispatch := current
331
+ if slug != source {
332
+ raw, err := readBoundedRegularFile(filepath.Join(nodes[slug].dir, "dispatch.json"), workspaceArtifactLimit)
333
+ if os.IsNotExist(err) {
334
+ continue
335
+ }
336
+ if err != nil {
337
+ return err
338
+ }
339
+ dispatch, err = decodeDispatch(raw)
340
+ if err != nil {
341
+ return err
342
+ }
343
+ }
344
+ for name, wave := range dispatch.Waves {
345
+ if wave.State == "complete" || wave.TerminalAt != "" {
346
+ continue
347
+ }
348
+ writerChecked := false
349
+ for role, actor := range wave.Roles {
350
+ wright := strings.Contains(role, "wright")
351
+ if actor.Returned || actor.Handle == "" && (!wright || wave.State == "abandoned") {
352
+ continue
353
+ }
354
+ if !wright {
355
+ if slug == source && (isReviewerRole(role) || roleBase(role) == "proof-runner") {
356
+ return fmt.Errorf("source %s retains a protected reviewer/proof account; reconcile it before writer admission", source)
357
+ }
358
+ continue
359
+ }
360
+ if !writerChecked {
361
+ if binding == "" || wave.ControllerBatch != binding {
362
+ return fmt.Errorf("retained writer %s/%s has unknown/same-tree isolation; reconcile its native handle before another writer; later lease IDs cannot retrofit actor checkout", slug, name)
363
+ }
364
+ if wave.Claim != "" {
365
+ if claims == nil {
366
+ latest, _, bad, err := readClaims(claimsPath(root))
367
+ if err != nil {
368
+ return err
369
+ }
370
+ if bad > 0 {
371
+ return fmt.Errorf("%d unparsed ledger line(s); retained writer claim identity cannot be validated", bad)
372
+ }
373
+ claims = latest
374
+ }
375
+ claim := claims[wave.Claim]
376
+ if claim == nil || claim.Session != wave.ClaimSession {
377
+ return fmt.Errorf("retained writer %s/%s claim %s has missing or mismatched ledger session", slug, name, wave.Claim)
378
+ }
379
+ // Release/expiry changes allocation, never the pending actor's frozen authority.
380
+ } else if wave.ClaimSession != "" {
381
+ return fmt.Errorf("retained writer %s/%s has a claim session without its exact claim identity", slug, name)
382
+ }
383
+ writerChecked = true
384
+ }
385
+ retainedID := ""
386
+ if lane, ok := wrightLane(role); ok {
387
+ for _, id := range wave.Slices {
388
+ if sliceNumber(id) == sliceNumber(lane) {
389
+ retainedID = id
390
+ break
391
+ }
392
+ }
393
+ } else if len(wave.Slices) == 1 {
394
+ retainedID = wave.Slices[0]
395
+ }
396
+ admitted := wave.AdmittedPaths[retainedID]
397
+ if retainedID == "" || len(admitted) == 0 {
398
+ return fmt.Errorf("retained writer %s/%s role %s lacks immutable admitted paths; reconcile the native actor, not mutable tasks or lease IDs", slug, name, role)
399
+ }
400
+ for _, path := range admitted {
401
+ normalized, err := parallel.NormalizePath(path)
402
+ if err != nil || normalized != path {
403
+ return fmt.Errorf("retained writer %s/%s has invalid admitted paths for %s", slug, name, retainedID)
404
+ }
405
+ }
406
+ counted, matchingPaths := false, false
407
+ for _, row := range lease.Slices {
408
+ workspace := row.Workspace
409
+ if workspace == "" {
410
+ workspace = controller
411
+ }
412
+ if workspace == slug && sliceNumber(row.ID) == sliceNumber(retainedID) && row.WrightStatus != parallel.WrightIntegrated {
413
+ counted = true
414
+ matchingPaths = frontierSamePaths(admitted, row.Paths)
415
+ break
416
+ }
417
+ }
418
+ if !counted {
419
+ return fmt.Errorf("retained writer %s/%s lost its active lease reservation for %s; reconcile the native actor before admitting more work", slug, name, retainedID)
420
+ }
421
+ if !matchingPaths {
422
+ return fmt.Errorf("retained writer %s/%s admitted paths for %s no longer match its active lease reservation; mutable tasks cannot move actor authority", slug, name, retainedID)
423
+ }
424
+ if slug == source && containsSlice(ids, retainedID) {
425
+ return fmt.Errorf("retained writer %s/%s still owns %s", slug, name, retainedID)
426
+ }
427
+ }
428
+ }
429
+ }
430
+ return nil
431
+ }
432
+
433
+ // The binding records admission into an existing cohort, never an actor's cwd/profile.
434
+ func autocompleteWriterAdmission(root, source string, wave *dispatchWave, current *dispatchFile) (string, []string, error) {
435
+ mode, err := autocompleteMode(root)
436
+ if err != nil || !mode {
437
+ return "", nil, err
438
+ }
439
+ allow, capN, err := frontierPolicy(root)
440
+ if err != nil {
441
+ return "", nil, err
442
+ }
443
+ controller, lease, err := parallel.ControllerLease(filepath.Dir(root))
444
+ if err != nil {
445
+ return "", nil, err
446
+ }
447
+ if lease != nil {
448
+ effectiveCap, occupied := capN, 0
449
+ if lease.Capacity > 0 && lease.Capacity < effectiveCap {
450
+ effectiveCap = lease.Capacity
451
+ }
452
+ if lease.Status == parallel.StatusRunning || lease.Status == parallel.StatusIntegrateFailed {
453
+ for _, row := range lease.Slices {
454
+ if row.WrightStatus != parallel.WrightIntegrated {
455
+ occupied++
456
+ }
457
+ }
458
+ }
459
+ if occupied > effectiveCap {
460
+ return "", nil, fmt.Errorf("retained cohort has %d occupied reservations above effective cap %d; reconcile capacity and drain existing work before any new writer launch; do not cancel workers", occupied, effectiveCap)
461
+ }
462
+ for i := range lease.Slices {
463
+ if lease.Slices[i].WrightStatus != parallel.WrightIntegrated {
464
+ if err := frontierNormalizePaths(lease.Slices[i].Paths); err != nil {
465
+ return "", nil, err
466
+ }
467
+ }
468
+ }
469
+ }
470
+ if capN <= 1 && lease == nil {
471
+ return "", nil, nil
472
+ }
473
+ ids := wave.Slices
474
+ nodes, err := frontierScan(root)
475
+ if err != nil {
476
+ return "", nil, err
477
+ }
478
+ chain, err := frontierChain(nodes, source)
479
+ if err != nil {
480
+ return "", nil, err
481
+ }
482
+ ordered, related := frontierRelated(nodes, chain)
483
+ mapping, _, problem, invalidCanonical := frontierRoadmap(nodes, ordered)
484
+ if invalidCanonical {
485
+ return "", nil, fmt.Errorf("invalid canonical roadmap: %s", problem)
486
+ }
487
+ if controller == "" {
488
+ for _, slug := range ordered {
489
+ if nodes[slug].location == "live" {
490
+ controller = slug
491
+ break
492
+ }
493
+ }
494
+ } else if !related[controller] {
495
+ return "", nil, fmt.Errorf("retained repository controller %s is outside source sequence", controller)
496
+ }
497
+ records, err := frontierEvidence(root, source)
498
+ if err != nil {
499
+ return "", nil, err
500
+ }
501
+ notices, err := frontierPreparationAdmission(root, source, nodes, mapping, problem, allow && capN > 1, records)
502
+ if err != nil {
503
+ return "", nil, err
504
+ }
505
+ local, withheld, err := WorkspaceReady(filepath.Dir(root), controller, source)
506
+ if err != nil {
507
+ return "", nil, err
508
+ }
509
+ if len(local) == 0 && len(withheld) > 0 {
510
+ return "", nil, fmt.Errorf("source readiness: %s", strings.Join(withheld, "; "))
511
+ }
512
+ for _, notice := range withheld {
513
+ notices = append(notices, "frontier-readiness: "+notice)
514
+ }
515
+ for _, id := range ids {
516
+ eligible := false
517
+ for _, candidate := range local {
518
+ eligible = eligible || sliceNumber(candidate.ID) == sliceNumber(id)
519
+ }
520
+ if !eligible {
521
+ return "", nil, fmt.Errorf("source readiness: %s/%s is not eligible; %s", source, id, strings.Join(withheld, "; "))
522
+ }
523
+ }
524
+ for i := range local {
525
+ if err := frontierNormalizePaths(local[i].Paths); err != nil {
526
+ return "", nil, err
527
+ }
528
+ }
529
+ binding := ""
530
+ reserved := frontierLeaseReserved(lease, controller, source, ids, local)
531
+ if reserved {
532
+ binding = controller + "/" + lease.BatchID + "@" + lease.CreatedAt
533
+ }
534
+ if err := frontierWriterRetention(root, source, controller, binding, lease, nodes, ordered, ids, current); err != nil {
535
+ return "", nil, err
536
+ }
537
+ var candidates []parallel.SlicePaths
538
+ for _, slug := range ordered {
539
+ if slug == source {
540
+ candidates = append(candidates, local...)
541
+ continue
542
+ }
543
+ if nodes[slug].location != "live" {
544
+ continue
545
+ }
546
+ n := nodes[slug]
547
+ if hold, err := frontierQuestionHold(n); err != nil {
548
+ return "", nil, err
549
+ } else if hold != "" {
550
+ continue
551
+ }
552
+ if _, _, err := frontierLocal(root, controller, n, &candidates); err != nil {
553
+ return "", nil, err
554
+ }
555
+ }
556
+ filtered := candidates[:0]
557
+ for _, candidate := range candidates {
558
+ workspace := candidate.Workspace
559
+ if workspace == "" {
560
+ workspace = controller
561
+ }
562
+ if workspace != source {
563
+ if reason := frontierSerialReason(records, source, workspace, candidate.ID); reason != "" {
564
+ notices = append(notices, "frontier-serial: "+workspace+"/"+candidate.ID+" — "+reason+"; ordered handoff retained")
565
+ continue
566
+ }
567
+ }
568
+ filtered = append(filtered, candidate)
569
+ }
570
+ _, picks, _, err := parallel.SelectForSlug(filepath.Dir(root), controller, capN, filtered)
571
+ if err != nil {
572
+ return "", nil, err
573
+ }
574
+ for _, id := range ids {
575
+ selected := reserved
576
+ for _, pick := range picks {
577
+ workspace := pick.Workspace
578
+ if workspace == "" {
579
+ workspace = controller
580
+ }
581
+ selected = selected || workspace == source && sliceNumber(pick.ID) == sliceNumber(id)
582
+ }
583
+ if !selected || source != controller && !reserved {
584
+ return "", nil, fmt.Errorf("%s/%s is not admitted by global lease capacity/path/readiness selection; run frontier and reserve the mixed cohort with parallel create/add before foreign dispatch", source, id)
585
+ }
586
+ }
587
+ var missing []string
588
+ for _, pick := range picks {
589
+ workspace := pick.Workspace
590
+ if workspace == "" {
591
+ workspace = controller
592
+ }
593
+ if workspace == source && containsSlice(ids, pick.ID) {
594
+ continue
595
+ }
596
+ if frontierSerialReason(records, source, workspace, pick.ID) != "" {
597
+ continue
598
+ }
599
+ missing = append(missing, parallel.SliceKey(pick.Workspace, pick.ID))
600
+ }
601
+ if len(missing) != 0 {
602
+ return "", nil, fmt.Errorf("global picks %s omitted; run devrites-engine parallel select --root %s --slug %s --from-plan --workspaces %s --cap %d and reserve picks with parallel create/add, or record exact serial: <source/SLICE-ID> — <constraint>", strings.Join(missing, ","), frontierQuote(filepath.Dir(root)), frontierQuote(controller), frontierQuote(strings.Join(ordered, ",")), capN)
603
+ }
604
+ wave.AdmittedPaths = make(map[string][]string, len(ids))
605
+ for _, id := range ids {
606
+ for _, candidate := range local {
607
+ if sliceNumber(candidate.ID) == sliceNumber(id) {
608
+ wave.AdmittedPaths[id] = candidate.Paths
609
+ break
610
+ }
611
+ }
612
+ }
613
+ return binding, notices, nil
614
+ }
615
+
616
+ func frontierGlobalPriority(source string, lines []string) bool {
617
+ controller := source
618
+ for _, line := range lines {
619
+ if strings.HasPrefix(line, "frontier-select-command:") {
620
+ fields := strings.Fields(line)
621
+ for i, field := range fields {
622
+ if field == "--slug" && i+1 < len(fields) {
623
+ controller = strings.Trim(fields[i+1], "'")
624
+ }
625
+ }
626
+ }
627
+ if strings.HasPrefix(line, "frontier-prepare:") || strings.HasPrefix(line, "frontier-review-sequence:") {
628
+ return true
629
+ }
630
+ if strings.HasPrefix(line, "frontier-workspace:") {
631
+ fields := strings.Fields(line)
632
+ if len(fields) > 3 && fields[1] != source && (fields[3] == "action=repair" || fields[3] == "action=inspect" || fields[3] == "action=prepare") {
633
+ return true
634
+ }
635
+ }
636
+ }
637
+ for _, line := range lines {
638
+ if !strings.HasPrefix(line, "frontier-picks: ") {
639
+ continue
640
+ }
641
+ for _, key := range strings.Split(strings.TrimPrefix(line, "frontier-picks: "), ",") {
642
+ workspace, _, qualified := strings.Cut(key, "/")
643
+ if !qualified {
644
+ workspace = controller
645
+ }
646
+ if workspace != source {
647
+ return true
648
+ }
649
+ }
650
+ }
651
+ return false
652
+ }