devrites 7.3.4 → 7.3.6

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 (75) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/docs/adr/0039-autocomplete-structural-admission.md +87 -0
  4. package/docs/adr/0040-early-preparation-admission.md +76 -0
  5. package/docs/adr/README.md +2 -0
  6. package/docs/cli.md +103 -5
  7. package/docs/engine/commands.md +88 -1
  8. package/engine/internal/lib/claims.go +65 -8
  9. package/engine/internal/lib/context.go +50 -2
  10. package/engine/internal/lib/continuation.go +8 -1
  11. package/engine/internal/lib/dispatch.go +147 -46
  12. package/engine/internal/lib/frontier.go +139 -59
  13. package/engine/internal/lib/frontier_controller.go +667 -0
  14. package/engine/internal/lib/frontier_preparation.go +210 -0
  15. package/engine/internal/lib/frontier_roadmap.go +168 -0
  16. package/engine/internal/lib/next.go +14 -2
  17. package/engine/internal/lib/triggers.go +9 -0
  18. package/engine/internal/parallel/repository.go +10 -0
  19. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  20. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  21. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  22. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  23. package/pack/.claude/skills/rite-autocomplete/SKILL.md +35 -38
  24. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +66 -59
  25. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  26. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +17 -15
  27. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  28. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  29. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  30. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  31. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +35 -38
  32. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +66 -59
  33. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  34. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +17 -15
  35. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  36. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  37. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  38. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  39. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +35 -38
  40. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +66 -59
  41. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  42. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +17 -15
  43. package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
  44. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  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 +35 -38
  48. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +66 -59
  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 +17 -15
  51. package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
  52. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  53. package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  54. package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  55. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +35 -38
  56. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +66 -59
  57. package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  58. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +17 -15
  59. package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
  60. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  61. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  62. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  63. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +35 -38
  64. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +66 -59
  65. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  66. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +17 -15
  67. package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
  68. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  69. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  70. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  71. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +35 -38
  72. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +66 -59
  73. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  74. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +17 -15
  75. package/package.json +1 -1
@@ -40,7 +40,7 @@ type loadsManifest struct {
40
40
 
41
41
  const loadsMarker = "<!-- loads:"
42
42
 
43
- const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--inline-contract] [--whole] [--skills-root <dir>] [--out <path>]
43
+ const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--prepare-for <parent-source> | --sequence-research] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--inline-contract] [--whole] [--skills-root <dir>] [--out <path>]
44
44
 
45
45
  Emits one deduplicated context bundle for a phase or standalone skill
46
46
  (+ optional dispatch role) into .devrites/work/<slug>/ctx/ so an agent
@@ -65,6 +65,12 @@ build or autocomplete bundle (no --role, no --slice) defaults to --slice
65
65
  bounded recheck bundle: the inventory file (relative to the workspace)
66
66
  and core.md are inlined; every other pack file becomes a by-reference
67
67
  index plus the sections the inventory cites as "<file>.md § <heading>".
68
+ Autocomplete at effective cap>1 gates Temper/Define/Plan/Vet bundles, including
69
+ --skill rite-<phase>, on immediate-successor preparation before writing.
70
+ --prepare-for binds native preparation to its exact immediate parent; roleless
71
+ controlling-root context is allowed. --sequence-research permits only exact
72
+ evidence-scout investigation of sequence authority. Neither creates an account
73
+ or grants product-write authority; research does not satisfy successor preparation.
68
74
  `
69
75
 
70
76
  // coreStandard is the universal floor every bundle inlines whole.
@@ -73,12 +79,27 @@ const coreStandard = "devrites-lib/reference/standards/core.md"
73
79
  // RunContext emits one deduplicated context bundle for a phase (+ optional
74
80
  // dispatch role) so an agent reads a single file instead of selecting files.
75
81
  //
76
- // context <slug> --phase <p> [--role <r>] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
82
+ // context <slug> --phase <p> [--role <r>] [--prepare-for <parent-source> | --sequence-research] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
77
83
  func RunContext(root string, args []string, stdout, stderr io.Writer) int {
78
84
  opts, code := parseContextArgs(args, stderr)
79
85
  if code != 0 {
80
86
  return code
81
87
  }
88
+ phase := opts.phase
89
+ if opts.skill != "" {
90
+ phase = ""
91
+ if strings.HasPrefix(opts.skill, "rite-") {
92
+ phase = strings.TrimPrefix(opts.skill, "rite-")
93
+ }
94
+ }
95
+ var roles []string
96
+ if opts.role != "" {
97
+ roles = []string{opts.role}
98
+ }
99
+ if err := opts.purpose.validate(phase, roles, true); err != nil {
100
+ fmt.Fprintf(stderr, "context: %v\n", err)
101
+ return 2
102
+ }
82
103
  // A Build reviewer judges one slice; an unscoped bundle inlines whole
83
104
  // plan/tasks/architecture into every dispatch.
84
105
  if opts.phase == "build" && opts.role != "" && roleBase(opts.role) != "slice-wright" && opts.slice == "" {
@@ -94,6 +115,19 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
94
115
  return 2
95
116
  }
96
117
  }
118
+ if opts.slug != "" {
119
+ notices, err := preparationAdmission(root, opts.slug, phase, opts.purpose, nil)
120
+ if err != nil {
121
+ fmt.Fprintf(stderr, "context: BLOCKED: %v\n", err)
122
+ return 3
123
+ }
124
+ for _, notice := range notices {
125
+ fmt.Fprintln(stdout, notice)
126
+ }
127
+ } else if opts.purpose.parent != "" || opts.purpose.research {
128
+ fmt.Fprintln(stderr, "context: preparation purpose requires an exact existing source")
129
+ return 2
130
+ }
97
131
  sources := contextSources{dir: featureDir}
98
132
  // One active slice per bundle, except a parallel batch: the root has to
99
133
  // see the other slices to start them. Whole tasks/plan/decisions ride
@@ -791,6 +825,7 @@ type contextOpts struct {
791
825
  recheck string
792
826
  inlineContract bool
793
827
  whole bool
828
+ purpose preparationPurpose
794
829
  }
795
830
 
796
831
  func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
@@ -799,6 +834,19 @@ func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
799
834
  for i := 0; i < len(args); i++ {
800
835
  value := argAt(args, i+1)
801
836
  switch args[i] {
837
+ case "--prepare-for":
838
+ if opts.purpose.parent != "" {
839
+ fmt.Fprintln(stderr, "context: duplicate --prepare-for")
840
+ return opts, 2
841
+ }
842
+ if value == "" || strings.HasPrefix(value, "-") {
843
+ fmt.Fprintln(stderr, "context: --prepare-for requires a parent source")
844
+ return opts, 2
845
+ }
846
+ opts.purpose.parent = value
847
+ case "--sequence-research":
848
+ opts.purpose.research = true
849
+ continue
802
850
  case "--phase":
803
851
  opts.phase = value
804
852
  case "--skill":
@@ -40,6 +40,11 @@ func RunCheckContinuation(root string, args []string, stdout, stderr io.Writer)
40
40
  }
41
41
  return code
42
42
  }
43
+ mode, err := autocompleteMode(root)
44
+ if err != nil {
45
+ fmt.Fprintf(stderr, "check continuation: %v\n", err)
46
+ return 2
47
+ }
43
48
  verdict, err := classifyContinuation(root, slug)
44
49
  if err != nil {
45
50
  fmt.Fprintf(stderr, "check continuation: %v\n", err)
@@ -52,8 +57,10 @@ func RunCheckContinuation(root string, args []string, stdout, stderr io.Writer)
52
57
  fmt.Fprintf(stderr, "check continuation: frontier: %v\n", err)
53
58
  return 2
54
59
  }
55
- if !verdict.Required && len(frontier) > 0 && frontier[0] == "frontier: actionable" {
60
+ if mode && frontierGlobalPriority(slug, frontier) || !mode && !verdict.Required && len(frontier) > 0 && frontier[0] == "frontier: actionable" {
56
61
  verdict = continuationVerdict{Required: true, Invoke: "/rite-autocomplete", Source: slug, Reason: "global frontier requires native preparation, authority review, or selection; no Ship approval"}
62
+ } else if mode && frontierSourcePreparationWait(slug, frontier) {
63
+ verdict = continuationVerdict{Reason: frontierInheritedPreparationWait}
57
64
  }
58
65
  }
59
66
  writeContinuation(stdout, verdict)
@@ -27,9 +27,9 @@ import (
27
27
  // fan-out cannot silently degrade to serial (open -> every role started ->
28
28
  // seal -> returns -> complete). start/return also auto-record metrics events.
29
29
  //
30
- // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
30
+ // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--prepare-for <parent-source> | --sequence-research] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
31
31
 
32
- const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon|attribute> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
32
+ const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon|attribute> [--phase p] [--wave w] [--role r] [--prepare-for <parent-source> | --sequence-research] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
33
33
 
34
34
  Wave barrier for parallel agent dispatch: seal fails until every declared
35
35
  role has a distinct handle; return fails before seal. The start that gives
@@ -40,6 +40,12 @@ completes the wave releases it, and abandon keeps it unless --release (any
40
40
  host-confirmed terminal path without a completing return, such as a stop or a
41
41
  refused launch). Wave IDs are single-use, including abandoned waves; retries
42
42
  need a fresh ID and never inherit or release an earlier wave's claim.
43
+ Autocomplete at effective cap>1 checks immediate-successor preparation before
44
+ ordinary Temper/Define/Plan/Vet open writes. Open-only --prepare-for binds allowed
45
+ native preparation roles to an exact immediate parent without recursively
46
+ requiring another successor. Open-only --sequence-research admits exact
47
+ evidence-scout authority investigation, never successor preparation or writers.
48
+ Both require an existing source and persist their purpose on the wave.
43
49
  Native starts, returns and abandonment retain subsecond timestamps. abandon
44
50
  --release also retains host-terminal confirmation, even without a claim or when
45
51
  its owned claim was already released; a failed release does not certify termination.
@@ -105,18 +111,22 @@ type dispatchFile struct {
105
111
  }
106
112
 
107
113
  type dispatchWave struct {
108
- Phase string `json:"phase"`
109
- State string `json:"state"` // open, sealed, abandoned, complete
110
- Reason string `json:"reason,omitempty"`
111
- AbandonedAt string `json:"abandoned_at,omitempty"`
112
- TerminalAt string `json:"terminal_at,omitempty"`
113
- Roles map[string]*dispatchRole `json:"roles"`
114
- Slices []string `json:"slices,omitempty"`
115
- ResliceReview string `json:"reslice_review,omitempty"`
116
- ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
117
- Claim string `json:"claim,omitempty"`
118
- ClaimSession string `json:"claim_session,omitempty"`
119
- Tree string `json:"tree,omitempty"` // work tree at a build wright open, for check windows --since-dispatch
114
+ Phase string `json:"phase"`
115
+ State string `json:"state"` // open, sealed, abandoned, complete
116
+ Reason string `json:"reason,omitempty"`
117
+ AbandonedAt string `json:"abandoned_at,omitempty"`
118
+ TerminalAt string `json:"terminal_at,omitempty"`
119
+ Roles map[string]*dispatchRole `json:"roles"`
120
+ Slices []string `json:"slices,omitempty"`
121
+ ResliceReview string `json:"reslice_review,omitempty"`
122
+ ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
123
+ Claim string `json:"claim,omitempty"`
124
+ ClaimSession string `json:"claim_session,omitempty"`
125
+ Tree string `json:"tree,omitempty"` // work tree at a build wright open, for check windows --since-dispatch
126
+ ControllerBatch string `json:"controller_batch,omitempty"` // cohort admission identity, not evidence of actor checkout
127
+ AdmittedPaths map[string][]string `json:"admitted_paths,omitempty"` // immutable normalized write sets, keyed by source-local slice
128
+ PreparationFor string `json:"preparation_for,omitempty"`
129
+ SequenceResearch bool `json:"sequence_research,omitempty"`
120
130
  }
121
131
 
122
132
  // dispatchClaim carries the wave-claim flags; ttl 0 means unset.
@@ -177,6 +187,16 @@ func decodeDispatch(data []byte) (*dispatchFile, error) {
177
187
  !slices.Contains([]string{"open", "sealed", "complete", "abandoned"}, w.State) {
178
188
  return nil, fmt.Errorf("wave %q is malformed (invalid phase, state or roles)", name)
179
189
  }
190
+ purpose := preparationPurpose{parent: w.PreparationFor, research: w.SequenceResearch}
191
+ if purpose.parent != "" || purpose.research {
192
+ roles := make([]string, 0, len(w.Roles))
193
+ for role := range w.Roles {
194
+ roles = append(roles, role)
195
+ }
196
+ if err := purpose.validate(w.Phase, roles, false); err != nil {
197
+ return nil, fmt.Errorf("wave %q has invalid preparation purpose: %w", name, err)
198
+ }
199
+ }
180
200
  handles := map[string]bool{}
181
201
  allStarted, allReturned := true, true
182
202
  for role, r := range w.Roles {
@@ -252,16 +272,33 @@ func saveDispatch(path string, f *dispatchFile) error {
252
272
  func nowStamp() string { return time.Now().UTC().Format(time.RFC3339Nano) }
253
273
 
254
274
  // RunDispatch implements the dispatch wave state machine.
275
+ //
276
+ // Preparation-purpose flags apply to open only; ordinary early-phase admission
277
+ // and context generation share immediate-successor validation.
255
278
  func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
256
279
  var slug, sub, phase, wave, role, handle, reason, review, contract, packet, outcome, result string
257
280
  var roles, sliceIDs []string
258
281
  var positional []string
259
282
  all := false
283
+ var purpose preparationPurpose
260
284
  var claim dispatchClaim
261
285
  var attributed []string
262
286
  for i := 0; i < len(args); i++ {
263
287
  value := argAt(args, i+1)
264
288
  switch args[i] {
289
+ case "--prepare-for":
290
+ if purpose.parent != "" {
291
+ fmt.Fprintln(stderr, "dispatch: duplicate --prepare-for")
292
+ return 2
293
+ }
294
+ if value == "" || strings.HasPrefix(value, "-") {
295
+ fmt.Fprintln(stderr, "dispatch: --prepare-for requires a parent source")
296
+ return 2
297
+ }
298
+ purpose.parent = value
299
+ case "--sequence-research":
300
+ purpose.research = true
301
+ continue
265
302
  case "--phase":
266
303
  phase = value
267
304
  case "--wave":
@@ -319,6 +356,16 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
319
356
  return 2
320
357
  }
321
358
  slug, sub = positional[0], positional[1]
359
+ if purpose.parent != "" || purpose.research {
360
+ if sub != "open" {
361
+ fmt.Fprintln(stderr, "dispatch: preparation purpose applies to open only")
362
+ return 2
363
+ }
364
+ if err := purpose.validate(phase, roles, false); err != nil {
365
+ fmt.Fprintf(stderr, "dispatch: %v\n", err)
366
+ return 2
367
+ }
368
+ }
322
369
  if sub != "open" && (claim.session != "" || len(claim.paths) > 0 || claim.ttl != 0) {
323
370
  fmt.Fprintln(stderr, "dispatch: --claim-session, --claim, and --claim-ttl apply to open only")
324
371
  return 2
@@ -341,20 +388,64 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
341
388
  // Serialize load-modify-save so concurrent start/return calls never lose
342
389
  // an update and strand the wave short of complete.
343
390
  code := 0
344
- if err := state.WithFeatureLock(root, slug, func() error {
345
- // Feature → claims is the lock order; never acquire another feature.
346
- return state.WithLock(claimsPath(root)+".lock", func() error {
347
- code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, contract, packet, outcome, result, all, claim, stdout, stderr)
348
- return nil
391
+ recordedReturn := false
392
+ mutate := func() error {
393
+ return state.WithFeatureLock(root, slug, func() error {
394
+ // Controller → feature → claims; never acquire another feature.
395
+ return state.WithLock(claimsPath(root)+".lock", func() error {
396
+ code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, contract, packet, outcome, result, all, claim, purpose, stdout, stderr, &recordedReturn)
397
+ return nil
398
+ })
349
399
  })
350
- }); err != nil {
400
+ }
401
+ controllerOpen := false
402
+ if sub == "open" && strings.EqualFold(phase, "build") {
403
+ for _, candidate := range roles {
404
+ controllerOpen = controllerOpen || strings.Contains(candidate, "wright")
405
+ }
406
+ }
407
+ if sub == "open" && (earlyPreparationPhase(phase) || purpose.parent != "" || purpose.research) {
408
+ controllerOpen = true
409
+ }
410
+ if controllerOpen {
411
+ mode, err := autocompleteMode(root)
412
+ if err != nil {
413
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: %v\n", err)
414
+ return 3
415
+ }
416
+ controllerOpen = mode || purpose.parent != "" || purpose.research
417
+ }
418
+ var lockErr error
419
+ if controllerOpen {
420
+ lockErr = parallel.WithControlLock(root, mutate)
421
+ } else {
422
+ lockErr = mutate()
423
+ }
424
+ if err := lockErr; err != nil {
351
425
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
352
426
  return 3
353
427
  }
428
+ if recordedReturn {
429
+ mode, err := autocompleteMode(root)
430
+ if err != nil {
431
+ fmt.Fprintf(stderr, "dispatch: return recorded; global scheduling: %v\n", err)
432
+ return 3
433
+ }
434
+ if mode {
435
+ lines, err := FrontierLines(root, slug)
436
+ if err != nil {
437
+ fmt.Fprintf(stderr, "dispatch: return recorded; global scheduling: %v\n", err)
438
+ return 3
439
+ }
440
+ for _, line := range lines {
441
+ fmt.Fprintln(stdout, line)
442
+ }
443
+ }
444
+ }
354
445
  return code
355
446
  }
356
447
 
357
- func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review, contract, packet, outcome, result string, all bool, claim dispatchClaim, stdout, stderr io.Writer) int {
448
+ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review, contract, packet, outcome, result string, all bool, claim dispatchClaim, purpose preparationPurpose, stdout, stderr io.Writer, recordedReturn *bool) int {
358
449
  f, path, err := loadDispatch(root, slug)
359
450
  if err != nil {
360
451
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
@@ -382,7 +473,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
382
473
  if prev := getWave(); prev != nil {
383
474
  return fail("wave %q already exists (state %s)", wave, prev.State)
384
475
  }
385
- w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs}
476
+ w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs, PreparationFor: purpose.parent, SequenceResearch: purpose.research}
386
477
  for _, r := range roles {
387
478
  if w.Roles[r] != nil {
388
479
  return fail("duplicate role %q in wave", r)
@@ -392,6 +483,23 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
392
483
  if err := lintKnownRoles(root, phase, roles); err != nil {
393
484
  return fail("BLOCKED: %v", err)
394
485
  }
486
+ notices, err := preparationAdmission(root, slug, phase, purpose, f)
487
+ if err != nil {
488
+ return fail("BLOCKED: %v", err)
489
+ }
490
+ for _, notice := range notices {
491
+ fmt.Fprintln(stdout, notice)
492
+ }
493
+ if isBuildWrightWave(w) {
494
+ binding, notices, err := autocompleteWriterAdmission(root, slug, w, f)
495
+ if err != nil {
496
+ return fail("BLOCKED: %v", err)
497
+ }
498
+ w.ControllerBatch = binding
499
+ for _, notice := range notices {
500
+ fmt.Fprintln(stdout, notice)
501
+ }
502
+ }
395
503
  rounds, code := admitWrightRounds(root, slug, f, w, review, stderr)
396
504
  if code != 0 {
397
505
  return code
@@ -428,10 +536,16 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
428
536
  }
429
537
  admissionPaths := slices.Clone(claimPaths)
430
538
  if isWrightWave(w) {
431
- admissionPaths = append(admissionPaths, sliceWriteSet(root, slug, w.Slices)...)
539
+ if len(w.AdmittedPaths) > 0 {
540
+ for _, id := range w.Slices {
541
+ admissionPaths = append(admissionPaths, w.AdmittedPaths[id]...)
542
+ }
543
+ } else {
544
+ admissionPaths = append(admissionPaths, sliceWriteSet(root, slug, w.Slices)...)
545
+ }
432
546
  }
433
547
  if len(claimPaths) == 0 {
434
- if err := checkAbandonedWriterClaims(root, admissionPaths); err != nil {
548
+ if err := checkWriterClaimsLocked(root, claim.session, admissionPaths); err != nil {
435
549
  return fail("BLOCKED: %v", err)
436
550
  }
437
551
  }
@@ -566,6 +680,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
566
680
  if !releaseWaveClaim(root, wave, w, stdout, stderr) {
567
681
  return 3
568
682
  }
683
+ *recordedReturn = true
569
684
  return 0
570
685
  }
571
686
  if r.Returned {
@@ -604,6 +719,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
604
719
  }
605
720
  return fail("%v", err)
606
721
  }
722
+ *recordedReturn = true
607
723
  recordMetricNote(root, slug, w.Phase, "return", role, note)
608
724
  fmt.Fprintf(stdout, "dispatch: %s returned (wave %s %s)\n", role, wave, w.State)
609
725
  if all {
@@ -714,28 +830,6 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
714
830
  return 2
715
831
  }
716
832
 
717
- // Caller holds the repository claims lock through new-wave publication.
718
- func checkAbandonedWriterClaims(root string, paths []string) error {
719
- if len(paths) == 0 {
720
- return nil
721
- }
722
- latest, _, bad, err := readClaims(claimsPath(root))
723
- if err != nil {
724
- return err
725
- }
726
- if bad > 0 {
727
- return fmt.Errorf("%d unparsed ledger line(s); cannot rule out an abandoned writer claim", bad)
728
- }
729
- conflicts, err := retainedWriterConflicts(root, latest, paths, newHolderProbe())
730
- if err != nil {
731
- return err
732
- }
733
- if len(conflicts) > 0 {
734
- return fmt.Errorf("abandoned writer still holds claim %s on %s; confirm host termination and use dispatch abandon --release before retrying", conflicts[0].ID, strings.Join(conflicts[0].Paths, ","))
735
- }
736
- return nil
737
- }
738
-
739
833
  // releaseWaveClaim releases w's claim or reconciles a verified prior release.
740
834
  // Callers surface failure without undoing the recorded return or abandonment.
741
835
  func releaseWaveClaim(root, wave string, w *dispatchWave, stdout, stderr io.Writer) bool {
@@ -1039,6 +1133,13 @@ func wrightLane(role string) (string, bool) {
1039
1133
  // errors are never refused: the check widens the batch, it never stops a
1040
1134
  // wave that already starts every remaining pick.
1041
1135
  func widerBatchRefusal(root, slug string, wave []string) string {
1136
+ mode, modeErr := autocompleteMode(root)
1137
+ if modeErr != nil {
1138
+ return fmt.Sprintf("dispatch: BLOCKED: %v", modeErr)
1139
+ }
1140
+ if mode {
1141
+ return ""
1142
+ }
1042
1143
  capN := min(afkMaxParallel(root), parallel.MaxParallelSlices)
1043
1144
  if capN < 2 {
1044
1145
  return ""