devrites 7.3.3 → 7.3.4

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 (46) hide show
  1. package/CHANGELOG.md +26 -3
  2. package/README.md +21 -1
  3. package/docs/cli.md +83 -6
  4. package/docs/command-map.md +2 -2
  5. package/docs/engine/commands.md +8 -3
  6. package/engine/commands.go +1 -1
  7. package/engine/help.go +3 -1
  8. package/engine/internal/lib/continuation.go +33 -4
  9. package/engine/internal/lib/dispatch.go +46 -31
  10. package/engine/internal/lib/frontier.go +808 -0
  11. package/engine/internal/lib/handoff.go +2 -5
  12. package/engine/internal/lib/next.go +10 -0
  13. package/engine/internal/parallel/cli.go +16 -5
  14. package/engine/internal/parallel/ops.go +7 -5
  15. package/engine/internal/parallel/repository.go +13 -0
  16. package/engine/internal/release/release.go +96 -5
  17. package/engine/main.go +4 -0
  18. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  19. package/pack/.claude/skills/rite-autocomplete/SKILL.md +10 -10
  20. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +6 -6
  21. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +7 -7
  22. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  23. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +10 -10
  24. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +6 -6
  25. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +7 -7
  26. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  27. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +10 -10
  28. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +6 -6
  29. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +7 -7
  30. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  31. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +10 -10
  32. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +6 -6
  33. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +7 -7
  34. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  35. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +10 -10
  36. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +6 -6
  37. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +7 -7
  38. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  39. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +10 -10
  40. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +6 -6
  41. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +7 -7
  42. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  43. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +10 -10
  44. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +6 -6
  45. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +7 -7
  46. package/package.json +1 -1
@@ -0,0 +1,808 @@
1
+ package lib
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "os"
7
+ "path/filepath"
8
+ "sort"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "github.com/devrites/devrites/internal/devritespaths"
13
+ "github.com/devrites/devrites/internal/gate"
14
+ "github.com/devrites/devrites/internal/markdowntext"
15
+ "github.com/devrites/devrites/internal/parallel"
16
+ "github.com/devrites/devrites/internal/state"
17
+ "github.com/devrites/devrites/internal/workflow"
18
+ )
19
+
20
+ const frontierWorkspaceLimit = 512
21
+
22
+ type frontierWorkspace struct {
23
+ slug, dir, location, parent, phase, status, next, question string
24
+ position, remaining int
25
+ hasPosition, hasRemaining bool
26
+ selectionHeld bool
27
+ problem error
28
+ }
29
+
30
+ // RunFrontier observes the recorded sequence without creating or selecting work.
31
+ func RunFrontier(root string, args []string, stdout, stderr io.Writer) int {
32
+ if len(args) > 1 || len(args) == 1 && strings.HasPrefix(args[0], "-") {
33
+ fmt.Fprintln(stderr, "usage: devrites-engine frontier [slug]")
34
+ return 2
35
+ }
36
+ slug := ""
37
+ if len(args) == 1 {
38
+ slug = args[0]
39
+ }
40
+ lines, err := FrontierLines(root, slug)
41
+ if err != nil {
42
+ fmt.Fprintf(stderr, "frontier: %v\n", err)
43
+ return 2
44
+ }
45
+ for _, line := range lines {
46
+ fmt.Fprintln(stdout, line)
47
+ }
48
+ return 0
49
+ }
50
+
51
+ // FrontierNotices preserves source-local selection; callers must explicitly opt into the pool.
52
+ func FrontierNotices(repoRoot, slug string) ([]string, error) {
53
+ return FrontierLines(filepath.Join(repoRoot, devritespaths.DevritesRootName), slug)
54
+ }
55
+
56
+ func frontierField(lines []string, key string) (string, bool, error) {
57
+ value, present := "", false
58
+ for _, line := range lines {
59
+ if v, ok := state.CursorField([]string{line}, key); ok {
60
+ if present {
61
+ return value, true, fmt.Errorf("duplicate %s cursor", key)
62
+ }
63
+ value, present = v, true
64
+ }
65
+ }
66
+ return value, present, nil
67
+ }
68
+
69
+ func frontierRead(root, slug, location string) *frontierWorkspace {
70
+ n := &frontierWorkspace{slug: slug, location: location}
71
+ if location == "live" {
72
+ n.dir, n.problem = devritespaths.ExistingCanonicalFeatureDirChecked(root, slug)
73
+ } else {
74
+ n.dir, n.problem = devritespaths.ExistingArchivedFeatureDirChecked(root, slug)
75
+ }
76
+ if n.problem != nil {
77
+ return n
78
+ }
79
+ raw, err := readBoundedRegularFile(filepath.Join(n.dir, "state.md"), workspaceArtifactLimit)
80
+ if err != nil {
81
+ n.problem = err
82
+ return n
83
+ }
84
+ raw, err = markdowntext.Structural(raw)
85
+ if err != nil {
86
+ n.problem = err
87
+ return n
88
+ }
89
+ lines := strings.Split(string(raw), "\n")
90
+ for _, field := range []struct {
91
+ key string
92
+ dst *string
93
+ }{
94
+ {state.CursorSequenceParent, &n.parent}, {state.CursorPhase, &n.phase}, {state.CursorStatus, &n.status}, {state.CursorNextAction, &n.next}, {state.CursorQuestionID, &n.question},
95
+ } {
96
+ value, _, err := frontierField(lines, field.key)
97
+ *field.dst = value
98
+ if err != nil {
99
+ n.problem = err
100
+ }
101
+ }
102
+ for _, field := range []struct {
103
+ key string
104
+ dst *int
105
+ present *bool
106
+ positive bool
107
+ }{
108
+ {state.CursorSequencePosition, &n.position, &n.hasPosition, true},
109
+ {state.CursorSequenceWorkspacesRemain, &n.remaining, &n.hasRemaining, false},
110
+ } {
111
+ value, present, err := frontierField(lines, field.key)
112
+ *field.present = present
113
+ if err != nil {
114
+ n.problem = err
115
+ continue
116
+ }
117
+ if !present {
118
+ continue
119
+ }
120
+ number, err := strconv.Atoi(value)
121
+ if err != nil || number < 0 || field.positive && number == 0 || strconv.Itoa(number) != value {
122
+ n.problem = fmt.Errorf("%s must be a canonical %sdecimal", field.key, map[bool]string{true: "positive ", false: "nonnegative "}[field.positive])
123
+ continue
124
+ }
125
+ *field.dst = number
126
+ }
127
+ if n.parent != "" && !devritespaths.ValidSlug(n.parent) {
128
+ n.problem = fmt.Errorf("invalid sequence_parent")
129
+ }
130
+ if _, known := state.PolicyFor(state.Phase(n.phase)); !known && n.problem == nil {
131
+ n.problem = fmt.Errorf("unknown or missing native phase %q", n.phase)
132
+ }
133
+ return n
134
+ }
135
+
136
+ func frontierScan(root string) (map[string]*frontierWorkspace, error) {
137
+ nodes := map[string]*frontierWorkspace{}
138
+ for _, location := range []string{"work", "archive"} {
139
+ path := filepath.Join(root, location)
140
+ info, err := os.Lstat(path)
141
+ if os.IsNotExist(err) {
142
+ continue
143
+ }
144
+ if err != nil {
145
+ return nil, err
146
+ }
147
+ if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
148
+ return nil, fmt.Errorf("%s must be a canonical directory", location)
149
+ }
150
+ f, err := os.Open(path) // #nosec G304 -- canonical directory checked above
151
+ if err != nil {
152
+ return nil, err
153
+ }
154
+ entries, err := f.ReadDir(frontierWorkspaceLimit + 1)
155
+ _ = f.Close()
156
+ if err != nil && err != io.EOF {
157
+ return nil, err
158
+ }
159
+ if len(entries)+len(nodes) > frontierWorkspaceLimit {
160
+ return nil, fmt.Errorf("frontier exceeds %d workspace entries; native sequence review required", frontierWorkspaceLimit)
161
+ }
162
+ for _, entry := range entries {
163
+ if !entry.IsDir() && entry.Type()&os.ModeSymlink == 0 {
164
+ continue
165
+ }
166
+ slug := entry.Name()
167
+ where := "live"
168
+ if location == "archive" {
169
+ where = "archive"
170
+ }
171
+ n := frontierRead(root, slug, where)
172
+ if old := nodes[slug]; old != nil {
173
+ old.problem = fmt.Errorf("duplicate live/archive identity")
174
+ continue
175
+ }
176
+ nodes[slug] = n
177
+ }
178
+ }
179
+ return nodes, nil
180
+ }
181
+
182
+ func frontierChain(nodes map[string]*frontierWorkspace, slug string) ([]string, error) {
183
+ var chain []string
184
+ seen := map[string]bool{}
185
+ for len(chain) < maxSequenceChain {
186
+ if seen[slug] {
187
+ return chain, fmt.Errorf("sequence_parent cycle at %s", slug)
188
+ }
189
+ seen[slug] = true
190
+ n := nodes[slug]
191
+ if n == nil {
192
+ return chain, fmt.Errorf("sequence_parent %s missing", slug)
193
+ }
194
+ chain = append(chain, slug)
195
+ if n.problem != nil {
196
+ return chain, fmt.Errorf("%s: %w", slug, n.problem)
197
+ }
198
+ if n.parent == "" {
199
+ if n.hasPosition && n.position != 1 {
200
+ return chain, fmt.Errorf("%s position %d has no sequence_parent", slug, n.position)
201
+ }
202
+ return chain, nil
203
+ }
204
+ if p := nodes[n.parent]; p != nil && (n.hasPosition || p.hasPosition) && (!n.hasPosition || !p.hasPosition || n.position != p.position+1) {
205
+ return chain, fmt.Errorf("%s sequence_position does not follow %s", slug, n.parent)
206
+ }
207
+ slug = n.parent
208
+ }
209
+ return chain, fmt.Errorf("sequence_parent chain exceeds %d workspaces", maxSequenceChain)
210
+ }
211
+
212
+ func frontierSpecTarget(n *frontierWorkspace) string {
213
+ command := workflow.ForAction(n.next)
214
+ if command.Verb != "spec" {
215
+ return ""
216
+ }
217
+ fields := strings.Fields(n.next)
218
+ if len(fields) < 2 || fields[0] != command.Claude && fields[0] != command.Codex && fields[0] != command.Skill {
219
+ return ""
220
+ }
221
+ if workflow.ForAction(strings.Join(fields[1:], " ")).Verb != "" {
222
+ return ""
223
+ }
224
+ for i, field := range fields {
225
+ if field == "—" {
226
+ fields = fields[:i]
227
+ break
228
+ }
229
+ }
230
+ if len(fields) != 2 || !devritespaths.ValidSlug(fields[1]) || strings.HasPrefix(fields[1], "-") || fields[1] == n.slug {
231
+ return ""
232
+ }
233
+ return fields[1]
234
+ }
235
+
236
+ func frontierTerminal(n *frontierWorkspace) bool {
237
+ return n.phase == "done" || n.status == "shipped" || n.status == "done" || n.status == "archived"
238
+ }
239
+
240
+ func frontierQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" }
241
+
242
+ // FrontierLines reports structural obligations, never semantic authorization or Ship approval.
243
+ func FrontierLines(root, slug string) ([]string, error) {
244
+ if slug == "" {
245
+ var err error
246
+ slug, err = devritespaths.ActiveSlug(root)
247
+ if err != nil {
248
+ return nil, err
249
+ }
250
+ }
251
+ if slug != "" {
252
+ if !devritespaths.ValidSlug(slug) {
253
+ return nil, fmt.Errorf("invalid source slug")
254
+ }
255
+ if _, err := devritespaths.WorkspaceOverrideChecked(root, slug); err != nil {
256
+ return nil, err
257
+ }
258
+ }
259
+ nodes, err := frontierScan(root)
260
+ if err != nil {
261
+ return nil, err
262
+ }
263
+ if slug == "" {
264
+ var tips []string
265
+ for name, n := range nodes {
266
+ if n.location != "archive" || !frontierTerminal(n) || frontierSpecTarget(n) == "" {
267
+ continue
268
+ }
269
+ if _, err := frontierChain(nodes, name); err != nil {
270
+ continue
271
+ }
272
+ hasChild := false
273
+ for _, child := range nodes {
274
+ hasChild = hasChild || child.parent == name
275
+ }
276
+ if !hasChild {
277
+ tips = append(tips, name)
278
+ }
279
+ }
280
+ if len(tips) != 1 {
281
+ return nil, fmt.Errorf("empty ACTIVE: need a unique archived terminal sequence tip with exact next Spec edge; found %d", len(tips))
282
+ }
283
+ slug = tips[0]
284
+ }
285
+ source := nodes[slug]
286
+ if source == nil {
287
+ return nil, fmt.Errorf("source %s missing", slug)
288
+ }
289
+ chain, chainErr := frontierChain(nodes, slug)
290
+ if chainErr != nil {
291
+ return []string{"frontier: blocked", "frontier-source: " + slug, fmt.Sprintf("frontier-workspace: %s location=%s action=repair reason=%s", slug, source.location, chainErr)}, nil
292
+ }
293
+ anchor := chain[len(chain)-1]
294
+ related := map[string]bool{anchor: true}
295
+ for range maxSequenceChain {
296
+ changed := false
297
+ for name, n := range nodes {
298
+ if related[n.parent] && !related[name] {
299
+ related[name] = true
300
+ changed = true
301
+ }
302
+ }
303
+ if !changed {
304
+ break
305
+ }
306
+ }
307
+ if len(related) == 1 && source.parent == "" && !source.hasPosition && !source.hasRemaining && frontierSpecTarget(source) == "" {
308
+ return []string{"frontier: none"}, nil
309
+ }
310
+ ordered := make([]string, 0, len(related))
311
+ for name := range related {
312
+ ordered = append(ordered, name)
313
+ }
314
+ depth := make(map[string]int, len(ordered))
315
+ for _, name := range ordered {
316
+ chain, _ := frontierChain(nodes, name)
317
+ depth[name] = len(chain)
318
+ }
319
+ sort.Slice(ordered, func(i, j int) bool {
320
+ a, b := nodes[ordered[i]], nodes[ordered[j]]
321
+ if depth[a.slug] != depth[b.slug] {
322
+ return depth[a.slug] < depth[b.slug]
323
+ }
324
+ if a.position != b.position {
325
+ return a.position < b.position
326
+ }
327
+ return a.slug < b.slug
328
+ })
329
+ controller := ""
330
+ for _, name := range ordered {
331
+ if nodes[name].location == "live" {
332
+ if _, err := frontierChain(nodes, name); err == nil {
333
+ controller = name
334
+ break
335
+ }
336
+ }
337
+ }
338
+ retainedController, lease, err := parallel.ControllerLease(filepath.Dir(root))
339
+ if err != nil {
340
+ return []string{"frontier: blocked", "frontier-source: " + slug, "frontier-selection: blocked reason=" + err.Error()}, nil
341
+ }
342
+ controllerProblem := ""
343
+ if retainedController != "" {
344
+ controller = retainedController
345
+ if !related[controller] || nodes[controller].location != "live" {
346
+ controllerProblem = "repository controller " + controller + " is outside the source sequence; recover retained controller before admission"
347
+ }
348
+ }
349
+ allowFuture, capN, err := frontierPolicy(root)
350
+ if err != nil {
351
+ return nil, err
352
+ }
353
+ if lease != nil && lease.Capacity > 0 && capN > lease.Capacity {
354
+ capN = lease.Capacity
355
+ }
356
+ lines := []string{"", "frontier-source: " + slug}
357
+ actionable, waiting, blocked := false, false, false
358
+ if controllerProblem != "" {
359
+ lines = append(lines, "frontier-selection: blocked reason="+controllerProblem)
360
+ blocked = true
361
+ }
362
+ var live []string
363
+ var candidates []parallel.SlicePaths
364
+ if source.hasRemaining && source.remaining == 0 {
365
+ allowFuture = false
366
+ }
367
+ maps, artifacts, mapProblem := frontierRoadmap(nodes, ordered)
368
+ if mapProblem != "" {
369
+ lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" reason=native authority-normalization required: "+mapProblem)
370
+ blocked = true
371
+ }
372
+ prepared := map[string]bool{}
373
+ for _, name := range ordered {
374
+ n := nodes[name]
375
+ action, reason := "complete", "archived predecessor; no writer admission"
376
+ held := false
377
+ _, invalid := frontierChain(nodes, name)
378
+ if invalid != nil {
379
+ action, reason, blocked = "repair", invalid.Error(), true
380
+ } else if gateReason, gateErr := frontierQuestionHold(n); gateErr != nil {
381
+ action, reason, held, blocked = "repair", gateErr.Error(), true, true
382
+ } else if gateReason != "" {
383
+ action, reason, held, blocked = "wait", gateReason, true, true
384
+ } else if n.location == "live" {
385
+ action, reason, err = frontierLocal(root, controller, n, &candidates)
386
+ if err != nil {
387
+ action, reason, n.selectionHeld = "repair", err.Error(), true
388
+ }
389
+ if !n.selectionHeld {
390
+ live = append(live, name)
391
+ }
392
+ held = action == "blocked" || err != nil
393
+ switch action {
394
+ case "inspect", "prepare", "repair":
395
+ actionable = true
396
+ case "wait":
397
+ waiting = true
398
+ case "blocked":
399
+ blocked = true
400
+ action = "wait"
401
+ }
402
+ }
403
+ lines = append(lines, fmt.Sprintf("frontier-workspace: %s location=%s action=%s reason=%s", name, n.location, action, reason))
404
+ if invalid != nil || held || !allowFuture || n.hasRemaining && n.remaining == 0 || n.status == "awaiting_human" || n.status == "blocked" || n.question != "" {
405
+ continue
406
+ }
407
+ target := frontierSpecTarget(n)
408
+ if target == "" && n.hasRemaining && n.remaining > 0 && n.hasPosition && mapProblem == "" {
409
+ target = maps[n.position+1]
410
+ }
411
+ if mapProblem != "" {
412
+ if target != "" || n.hasRemaining && n.remaining > 0 {
413
+ actionable = true
414
+ }
415
+ continue
416
+ }
417
+ if target != "" && frontierConflictingSuccessor(nodes, n, target) {
418
+ lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" source="+name+" reason=conflicting successor identities; native authority-normalization required")
419
+ actionable, blocked = true, true
420
+ continue
421
+ }
422
+ if target != "" && n.hasPosition && maps[n.position+1] != "" && maps[n.position+1] != target {
423
+ lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" source="+name+" reason=next Spec conflicts with recorded successor; native authority-normalization required")
424
+ actionable, blocked = true, true
425
+ continue
426
+ }
427
+ if target != "" && nodes[target] != nil && nodes[target].parent != name {
428
+ lines = append(lines, fmt.Sprintf("frontier-workspace: %s location=%s action=repair reason=recorded successor is not bound to source %s; native authority-normalization required", target, nodes[target].location, name))
429
+ actionable, blocked = true, true
430
+ continue
431
+ }
432
+ if target != "" && nodes[target] == nil && !prepared[target] {
433
+ prepared[target] = true
434
+ // A table identifies a preparation obligation, not approval to implement it.
435
+ lines = append(lines, fmt.Sprintf("frontier-workspace: %s location=missing action=prepare reason=recorded next workspace; native scope and independence review required", target), fmt.Sprintf("frontier-prepare: /rite-spec %s source=%s reason=read recorded sequence authority before preparation; no workspace created", frontierQuote(target), name))
436
+ actionable = true
437
+ } else if target == "" && n.hasRemaining && n.remaining > 0 {
438
+ lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" source="+name+" reason=remaining work unresolved; native authority-normalization required")
439
+ actionable, blocked = true, true
440
+ }
441
+ }
442
+ if controller != "" && controllerProblem == "" && len(live) > 0 {
443
+ lines = append(lines, fmt.Sprintf("frontier-select-command: devrites-engine parallel select --root %s --slug %s --from-plan --workspaces %s --cap %d", frontierQuote(filepath.Dir(root)), frontierQuote(controller), frontierQuote(strings.Join(live, ",")), capN))
444
+ if len(candidates) > 0 {
445
+ _, picks, running, err := parallel.SelectForSlug(filepath.Dir(root), controller, capN, candidates)
446
+ if err != nil {
447
+ lines = append(lines, "frontier-selection: blocked reason="+err.Error())
448
+ blocked = true
449
+ } else if len(picks) > 0 {
450
+ actionable = true
451
+ var ids []string
452
+ for _, pick := range picks {
453
+ ids = append(ids, parallel.SliceKey(pick.Workspace, pick.ID))
454
+ }
455
+ lines = append(lines, "frontier-picks: "+strings.Join(ids, ","))
456
+ } else if running > 0 {
457
+ waiting = true
458
+ }
459
+ }
460
+ }
461
+ verdict := "clear"
462
+ if blocked {
463
+ verdict = "blocked"
464
+ }
465
+ if waiting {
466
+ verdict = "waiting"
467
+ }
468
+ if actionable {
469
+ verdict = "actionable"
470
+ }
471
+ lines[0] = "frontier: " + verdict
472
+ return lines, nil
473
+ }
474
+
475
+ func frontierPolicy(root string) (bool, int, error) {
476
+ capN := parallel.MaxParallelSlices
477
+ raw, err := readBoundedRegularFile(filepath.Join(root, "AFK"), workspaceArtifactLimit)
478
+ if os.IsNotExist(err) {
479
+ return true, capN, nil
480
+ }
481
+ if err != nil {
482
+ return false, 0, err
483
+ }
484
+ seen := map[string]bool{}
485
+ allow := true
486
+ for _, line := range strings.Split(string(raw), "\n") {
487
+ key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
488
+ key, value = strings.TrimSpace(key), strings.TrimSpace(value)
489
+ if !ok || key != "continue_sequence" && key != "max_parallel" {
490
+ continue
491
+ }
492
+ for i := 1; i < len(value); i++ {
493
+ if value[i] == '#' && (value[i-1] == ' ' || value[i-1] == '\t') {
494
+ value = strings.TrimSpace(value[:i])
495
+ break
496
+ }
497
+ }
498
+ if seen[key] {
499
+ return false, 0, fmt.Errorf("duplicate AFK %s", key)
500
+ }
501
+ seen[key] = true
502
+ if key == "continue_sequence" {
503
+ switch value {
504
+ case "true":
505
+ case "false":
506
+ allow = false
507
+ default:
508
+ return false, 0, fmt.Errorf("invalid AFK continue_sequence")
509
+ }
510
+ } else {
511
+ capN, err = strconv.Atoi(value)
512
+ if err != nil || capN < 1 || capN > parallel.MaxParallelSlices || strconv.Itoa(capN) != value {
513
+ return false, 0, fmt.Errorf("invalid AFK max_parallel")
514
+ }
515
+ }
516
+ }
517
+ return allow, capN, nil
518
+ }
519
+
520
+ func frontierQuestionHold(n *frontierWorkspace) (string, error) {
521
+ if n.status == "awaiting_human" || n.status == "blocked" || n.question != "" {
522
+ return "human/blocked cursor; retain native gate", nil
523
+ }
524
+ questions, err := readBoundedRegularFile(filepath.Join(n.dir, "questions.md"), workspaceArtifactLimit)
525
+ if err != nil && !os.IsNotExist(err) {
526
+ return "", err
527
+ }
528
+ for _, kind := range gate.OpenBlockingQuestionGates(questions) {
529
+ if kind == "blocking" || kind == "escalating" {
530
+ return "open " + kind + " question; retain native gate", nil
531
+ }
532
+ }
533
+ return "", nil
534
+ }
535
+
536
+ func frontierLocal(root, controller string, n *frontierWorkspace, candidates *[]parallel.SlicePaths) (string, string, error) {
537
+ n.selectionHeld = true
538
+ if frontierTerminal(n) {
539
+ return "complete", "terminal workspace", nil
540
+ }
541
+ if _, err := devritespaths.WorkspaceOverrideChecked(root, n.slug); err != nil {
542
+ return "blocked", "DEVRITES_WORKSPACE scope retained; unset override before global admission", nil
543
+ }
544
+ if n.status != "running" && n.status != "ready" {
545
+ return "blocked", "status lacks an actionable native cursor; normalize state without inventing authority", nil
546
+ }
547
+ raw, err := readBoundedRegularFile(filepath.Join(n.dir, "dispatch.json"), workspaceArtifactLimit)
548
+ if err != nil && !os.IsNotExist(err) {
549
+ return "", "", err
550
+ }
551
+ if err == nil {
552
+ dispatch, err := decodeDispatch(raw)
553
+ if err != nil {
554
+ return "", "", err
555
+ }
556
+ retained, pending := false, false
557
+ var built map[string]bool
558
+ for _, wave := range dispatch.Waves {
559
+ if wave.State == "complete" {
560
+ if !isBuildWrightWave(wave) {
561
+ continue
562
+ }
563
+ if built == nil {
564
+ built, err = builtSlicesFrom(func(name string) ([]byte, error) {
565
+ return readBoundedRegularFile(filepath.Join(n.dir, name), workspaceArtifactLimit)
566
+ })
567
+ if err != nil {
568
+ return "", "", err
569
+ }
570
+ }
571
+ if len(wave.Slices) > 0 && !slicesUnbuilt(wave.Slices, built) {
572
+ continue
573
+ }
574
+ }
575
+ if wave.State == "abandoned" && wave.TerminalAt != "" {
576
+ continue
577
+ }
578
+ wavePending := false
579
+ for _, role := range wave.Roles {
580
+ if wave.State != "abandoned" || role.Handle != "" || role.StartedAt != "" {
581
+ wavePending = wavePending || !role.Returned
582
+ }
583
+ }
584
+ if wave.State == "abandoned" && !wavePending {
585
+ continue
586
+ }
587
+ retained = true
588
+ pending = pending || wavePending
589
+ }
590
+ if pending {
591
+ return "wait", "retained dispatch work; reconcile handles before another launch; isolated lease IDs do not bind actor checkout", nil
592
+ }
593
+ if retained {
594
+ return "inspect", "retained dispatch returns need native reconciliation before another launch", nil
595
+ }
596
+ }
597
+ n.selectionHeld = false
598
+ if n.phase == "vet" && n.status == "ready" || n.phase == "build" {
599
+ ready, notices, err := WorkspaceReady(filepath.Dir(root), controller, n.slug)
600
+ if err != nil {
601
+ return "", "", err
602
+ }
603
+ *candidates = append(*candidates, ready...)
604
+ if len(notices) > 0 {
605
+ action := "wait"
606
+ for _, notice := range notices {
607
+ if !strings.Contains(notice, ": waiting for durable BUILT/integrated producer ") {
608
+ action = "repair"
609
+ }
610
+ }
611
+ return action, "native Plan/Vet readiness: " + strings.Join(notices, "; "), nil
612
+ }
613
+ return "wait", "writer candidates require mixed selection; retained leases and capacity remain authoritative", nil
614
+ }
615
+ if frontierSpecTarget(n) != "" {
616
+ return "complete", "native next Spec cursor recorded", nil
617
+ }
618
+ if _, internal, err := internalPhaseEdge(n.next, n.slug); err != nil {
619
+ return "", "", err
620
+ } else if internal && n.status == "running" {
621
+ return "prepare", "native internal phase cursor: " + n.next, nil
622
+ }
623
+ if command := workflow.ForAction(n.next); command.Verb == "ship" {
624
+ fields := strings.Fields(n.next)
625
+ valid := len(fields) > 0 && (fields[0] == command.Claude || fields[0] == command.Codex || fields[0] == command.Skill)
626
+ valid = valid && workflow.ForAction(strings.Join(fields[1:], " ")).Verb == ""
627
+ for i, field := range fields {
628
+ if field == "—" {
629
+ fields = fields[:i]
630
+ break
631
+ }
632
+ }
633
+ valid = valid && (len(fields) == 1 || len(fields) == 2 && fields[1] == n.slug)
634
+ if !valid {
635
+ return "blocked", "invalid native Ship cursor; retain native gate", nil
636
+ }
637
+ // Ship approval holds this writer, not independently recorded future preparation.
638
+ n.selectionHeld = true
639
+ return "wait", "Ship requires existing native authority; controller invocation is not approval", nil
640
+ }
641
+ return "blocked", "no actionable native internal cursor; review state without inventing authority", nil
642
+ }
643
+
644
+ // Only exact position/slug or position/workspace tables are structural authority.
645
+ // Different maps require native normalization, even if prose claims one supersedes another.
646
+ func frontierRoadmap(nodes map[string]*frontierWorkspace, ordered []string) (map[int]string, []string, string) {
647
+ result := map[int]string{}
648
+ var artifacts []string
649
+ problem := ""
650
+ var first map[int]string
651
+ for _, slug := range ordered {
652
+ for _, name := range []string{"decisions.md", "roadmap.md", "spec.md"} {
653
+ path := filepath.Join(nodes[slug].dir, name)
654
+ if nodes[slug].dir == "" {
655
+ continue
656
+ }
657
+ raw, err := readBoundedRegularFile(path, workspaceArtifactLimit)
658
+ if os.IsNotExist(err) {
659
+ continue
660
+ }
661
+ artifacts = append(artifacts, path)
662
+ if err != nil {
663
+ problem = "recorded artifact unreadable"
664
+ continue
665
+ }
666
+ raw, err = markdowntext.Structural(raw)
667
+ if err != nil {
668
+ problem = "recorded artifact malformed"
669
+ continue
670
+ }
671
+ lines := strings.Split(string(raw), "\n")
672
+ for i := 0; i < len(lines); i++ {
673
+ cells := frontierCells(lines[i])
674
+ pos, workspace, headings := -1, -1, 0
675
+ for j, cell := range cells {
676
+ switch strings.ToLower(cell) {
677
+ case "position":
678
+ pos = j
679
+ headings++
680
+ case "slug", "workspace":
681
+ workspace = j
682
+ headings++
683
+ }
684
+ }
685
+ if pos < 0 || workspace < 0 {
686
+ continue
687
+ }
688
+ if headings != 2 {
689
+ problem = "ambiguous position/workspace headers"
690
+ continue
691
+ }
692
+ table := map[int]string{}
693
+ seen := map[string]bool{}
694
+ for i++; i < len(lines); i++ {
695
+ row := frontierCells(lines[i])
696
+ if len(row) != len(cells) {
697
+ break
698
+ }
699
+ if strings.Trim(row[pos], "-: ") == "" {
700
+ continue
701
+ }
702
+ n, err := strconv.Atoi(row[pos])
703
+ target := strings.Trim(row[workspace], "`")
704
+ if err != nil || n < 1 || strconv.Itoa(n) != row[pos] || !devritespaths.ValidSlug(target) || strings.ContainsAny(target, " \t") || strings.HasPrefix(target, "-") {
705
+ problem = "ambiguous position/workspace table"
706
+ continue
707
+ }
708
+ if table[n] != "" || seen[target] {
709
+ problem = "duplicate sequence position or workspace"
710
+ }
711
+ table[n], seen[target] = target, true
712
+ }
713
+ if len(table) == 0 {
714
+ problem = "empty position/workspace table"
715
+ continue
716
+ }
717
+ if first == nil {
718
+ first = table
719
+ } else {
720
+ if len(first) != len(table) {
721
+ problem = "conflicting or amended sequence maps"
722
+ }
723
+ for n, target := range table {
724
+ if first[n] != target {
725
+ problem = "conflicting or amended sequence maps"
726
+ }
727
+ }
728
+ }
729
+ for n, target := range table {
730
+ result[n] = target
731
+ }
732
+ }
733
+ }
734
+ }
735
+ for _, slug := range ordered {
736
+ n := nodes[slug]
737
+ if n.hasPosition && result[n.position] != "" && result[n.position] != slug {
738
+ problem = "live cursor conflicts with recorded sequence map"
739
+ }
740
+ }
741
+ if len(artifacts) == 0 {
742
+ for _, slug := range ordered {
743
+ artifacts = append(artifacts, filepath.Join(nodes[slug].dir, "state.md"))
744
+ }
745
+ }
746
+ return result, artifacts, problem
747
+ }
748
+
749
+ func frontierCells(line string) []string {
750
+ line = strings.TrimSpace(line)
751
+ if !strings.HasPrefix(line, "|") || !strings.HasSuffix(line, "|") {
752
+ return nil
753
+ }
754
+ cells := strings.Split(strings.Trim(line, "|"), "|")
755
+ for i := range cells {
756
+ cells[i] = strings.TrimSpace(cells[i])
757
+ }
758
+ return cells
759
+ }
760
+
761
+ func frontierBoundSpec(root, slug, action string) bool {
762
+ nodes, err := frontierScan(root)
763
+ if err != nil {
764
+ return false
765
+ }
766
+ n := nodes[slug]
767
+ if n == nil || n.next != action || n.parent == "" && !n.hasPosition && !n.hasRemaining {
768
+ return false
769
+ }
770
+ target := frontierSpecTarget(n)
771
+ if target == "" {
772
+ return false
773
+ }
774
+ chain, err := frontierChain(nodes, slug)
775
+ if frontierConflictingSuccessor(nodes, n, target) {
776
+ return false
777
+ }
778
+ if err != nil {
779
+ return false
780
+ }
781
+ allow, _, err := frontierPolicy(root)
782
+ if err != nil {
783
+ return false
784
+ }
785
+ if child := nodes[target]; child != nil {
786
+ if child.parent != slug {
787
+ return false
788
+ }
789
+ if _, err := frontierChain(nodes, target); err != nil {
790
+ return false
791
+ }
792
+ return true
793
+ }
794
+ mapping, _, problem := frontierRoadmap(nodes, chain)
795
+ if problem != "" || n.hasPosition && mapping[n.position+1] != "" && mapping[n.position+1] != target {
796
+ return false
797
+ }
798
+ return allow && (!n.hasRemaining || n.remaining > 0)
799
+ }
800
+
801
+ func frontierConflictingSuccessor(nodes map[string]*frontierWorkspace, source *frontierWorkspace, target string) bool {
802
+ for _, child := range nodes {
803
+ if child.parent == source.slug && child.slug != target && source.hasPosition && child.hasPosition && child.position == source.position+1 {
804
+ return true
805
+ }
806
+ }
807
+ return false
808
+ }