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