devrites 7.3.2 → 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 (69) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/README.md +21 -1
  3. package/docs/cli.md +98 -13
  4. package/docs/command-map.md +2 -2
  5. package/docs/engine/commands.md +10 -5
  6. package/docs/engine/workspace-schema.md +4 -3
  7. package/engine/commands.go +1 -1
  8. package/engine/help.go +3 -1
  9. package/engine/internal/hostpack/hostpack.go +1 -1
  10. package/engine/internal/lib/apply_bundle.go +25 -8
  11. package/engine/internal/lib/continuation.go +33 -4
  12. package/engine/internal/lib/dispatch.go +46 -31
  13. package/engine/internal/lib/frontier.go +808 -0
  14. package/engine/internal/lib/handoff.go +2 -5
  15. package/engine/internal/lib/next.go +10 -0
  16. package/engine/internal/lib/regression.go +0 -5
  17. package/engine/internal/lib/resolve.go +33 -16
  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 +13 -0
  21. package/engine/internal/release/release.go +96 -5
  22. package/engine/internal/rootfacts/facts.go +11 -0
  23. package/engine/internal/state/lock_other.go +4 -0
  24. package/engine/internal/state/write.go +54 -13
  25. package/engine/main.go +6 -2
  26. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  27. package/pack/.claude/skills/rite-autocomplete/SKILL.md +10 -10
  28. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +6 -6
  29. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +7 -7
  30. package/pack/.claude/skills/rite-doctor/SKILL.md +70 -68
  31. package/pack/generated/README.md +1 -1
  32. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  33. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +10 -10
  34. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +6 -6
  35. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +7 -7
  36. package/pack/generated/claude/skills/rite-doctor/SKILL.md +70 -68
  37. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  38. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +10 -10
  39. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +6 -6
  40. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +7 -7
  41. package/pack/generated/codex/skills/rite-doctor/SKILL.md +70 -68
  42. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  43. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +10 -10
  44. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +6 -6
  45. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +7 -7
  46. package/pack/generated/devin/skills/rite-doctor/SKILL.md +70 -68
  47. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  48. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +10 -10
  49. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +6 -6
  50. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +7 -7
  51. package/pack/generated/dsh/skills/rite-doctor/SKILL.md +70 -68
  52. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  53. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -1
  54. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +10 -10
  55. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +6 -6
  56. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +7 -7
  57. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +1 -1
  58. package/pack/generated/omp/skills/rite-doctor/SKILL.md +70 -68
  59. package/pack/generated/pi/AGENTS.md +1 -1
  60. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
  61. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +10 -10
  62. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +6 -6
  63. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +7 -7
  64. package/pack/generated/pi/skills/rite-doctor/SKILL.md +70 -68
  65. package/package.json +1 -1
  66. package/scripts/build-host-artifacts.sh +1 -1
  67. package/scripts/dsh-generate.py +5 -1
  68. package/scripts/omp-generate.sh +4 -4
  69. package/scripts/pi-generate.sh +3 -3
@@ -52,6 +52,8 @@ An abandoned writer's unreleased claim blocks overlapping writers across every
52
52
  live or archived feature in this repository, even in the same session or after
53
53
  TTL expiry, until explicit release or proven host termination.
54
54
  return --handle, when supplied, must match the role's recorded host handle.
55
+ Completed return replay requires the exact recorded role and --handle and only
56
+ reconciles claim release; failed release exits nonzero without undoing the return.
55
57
  status --wave exits
56
58
  nonzero until that wave is complete; bare status lists every open, sealed,
57
59
  or abandoned wave, folds complete waves into one count line (--all lists
@@ -150,32 +152,36 @@ func loadDispatch(root, slug string) (*dispatchFile, string, error) {
150
152
  }
151
153
 
152
154
  func loadDispatchFile(path string) (*dispatchFile, string, error) {
153
- f := &dispatchFile{Waves: map[string]*dispatchWave{}}
154
155
  data, err := os.ReadFile(path) // #nosec G304 -- path is a validated feature workspace artifact
155
156
  if os.IsNotExist(err) {
156
- return f, path, nil
157
+ return &dispatchFile{Waves: map[string]*dispatchWave{}}, path, nil
157
158
  }
158
159
  if err != nil {
159
160
  return nil, path, err
160
161
  }
161
- f = &dispatchFile{}
162
+ f, err := decodeDispatch(data)
163
+ return f, path, err
164
+ }
165
+
166
+ func decodeDispatch(data []byte) (*dispatchFile, error) {
167
+ f := &dispatchFile{}
162
168
  if err := json.Unmarshal(data, f); err != nil {
163
- return nil, path, fmt.Errorf("dispatch.json is not valid JSON: %w", err)
169
+ return nil, fmt.Errorf("dispatch.json is not valid JSON: %w", err)
164
170
  }
165
171
  if f.Waves == nil {
166
- return nil, path, fmt.Errorf("dispatch.json is malformed (missing waves)")
172
+ return nil, fmt.Errorf("dispatch.json is malformed (missing waves)")
167
173
  }
168
174
  // Validate every account before any caller can use or mutate the ledger.
169
175
  for name, w := range f.Waves {
170
176
  if w == nil || strings.TrimSpace(name) == "" || strings.TrimSpace(w.Phase) == "" || len(w.Roles) == 0 ||
171
177
  !slices.Contains([]string{"open", "sealed", "complete", "abandoned"}, w.State) {
172
- return nil, path, fmt.Errorf("wave %q is malformed (invalid phase, state or roles)", name)
178
+ return nil, fmt.Errorf("wave %q is malformed (invalid phase, state or roles)", name)
173
179
  }
174
180
  handles := map[string]bool{}
175
181
  allStarted, allReturned := true, true
176
182
  for role, r := range w.Roles {
177
183
  if r == nil || strings.TrimSpace(role) == "" {
178
- return nil, path, fmt.Errorf("wave %q role %q is malformed (missing account)", name, role)
184
+ return nil, fmt.Errorf("wave %q role %q is malformed (missing account)", name, role)
179
185
  }
180
186
  started := r.Handle != ""
181
187
  if started != (r.StartedAt != "") || r.Returned != (r.ReturnedAt != "") ||
@@ -183,19 +189,19 @@ func loadDispatchFile(path string) (*dispatchFile, string, error) {
183
189
  (w.State == "open" && r.Returned) ||
184
190
  ((w.State == "sealed" || w.State == "complete") && !started) ||
185
191
  (w.State == "complete" && !r.Returned) {
186
- return nil, path, fmt.Errorf("wave %q role %q is malformed (inconsistent start or return)", name, role)
192
+ return nil, fmt.Errorf("wave %q role %q is malformed (inconsistent start or return)", name, role)
187
193
  }
188
194
  allStarted = allStarted && started
189
195
  allReturned = allReturned && r.Returned
190
196
  if started {
191
197
  start, startErr := time.Parse(time.RFC3339Nano, r.StartedAt)
192
198
  if startErr != nil {
193
- return nil, path, fmt.Errorf("wave %q role %q is malformed (invalid start timestamp)", name, role)
199
+ return nil, fmt.Errorf("wave %q role %q is malformed (invalid start timestamp)", name, role)
194
200
  }
195
201
  if r.Returned {
196
202
  end, endErr := time.Parse(time.RFC3339Nano, r.ReturnedAt)
197
203
  if endErr != nil || end.Before(start) {
198
- return nil, path, fmt.Errorf("wave %q role %q is malformed (invalid return timestamp)", name, role)
204
+ return nil, fmt.Errorf("wave %q role %q is malformed (invalid return timestamp)", name, role)
199
205
  }
200
206
  }
201
207
  }
@@ -204,32 +210,32 @@ func loadDispatchFile(path string) (*dispatchFile, string, error) {
204
210
  }
205
211
  }
206
212
  if (w.State == "open" && allStarted) || (w.State == "sealed" && allReturned) {
207
- return nil, path, fmt.Errorf("wave %q is malformed (inconsistent barrier state)", name)
213
+ return nil, fmt.Errorf("wave %q is malformed (inconsistent barrier state)", name)
208
214
  }
209
215
  if w.AbandonedAt != "" {
210
216
  if _, err := time.Parse(time.RFC3339Nano, w.AbandonedAt); err != nil || w.State != "abandoned" {
211
- return nil, path, fmt.Errorf("wave %q is malformed (invalid abandonment timestamp)", name)
217
+ return nil, fmt.Errorf("wave %q is malformed (invalid abandonment timestamp)", name)
212
218
  }
213
219
  }
214
220
  if w.TerminalAt != "" {
215
221
  terminal, err := time.Parse(time.RFC3339Nano, w.TerminalAt)
216
222
  abandoned, abandonErr := time.Parse(time.RFC3339Nano, w.AbandonedAt)
217
223
  if err != nil || abandonErr != nil || w.State != "abandoned" || terminal.Before(abandoned) {
218
- return nil, path, fmt.Errorf("wave %q is malformed (invalid terminal timestamp)", name)
224
+ return nil, fmt.Errorf("wave %q is malformed (invalid terminal timestamp)", name)
219
225
  }
220
226
  for _, account := range w.Roles {
221
227
  for _, stamp := range []string{account.StartedAt, account.ReturnedAt} {
222
228
  if stamp != "" {
223
229
  at, _ := time.Parse(time.RFC3339Nano, stamp)
224
230
  if terminal.Before(at) {
225
- return nil, path, fmt.Errorf("wave %q is malformed (terminal timestamp precedes account)", name)
231
+ return nil, fmt.Errorf("wave %q is malformed (terminal timestamp precedes account)", name)
226
232
  }
227
233
  }
228
234
  }
229
235
  }
230
236
  }
231
237
  }
232
- return f, path, nil
238
+ return f, nil
233
239
  }
234
240
 
235
241
  func saveDispatch(path string, f *dispatchFile) error {
@@ -543,7 +549,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
543
549
 
544
550
  case "return":
545
551
  w := getWave()
546
- if w == nil || w.State != "sealed" {
552
+ if w == nil || (w.State != "sealed" && w.State != "complete") {
547
553
  return fail("wave %q is not sealed (state %s)", wave, waveState(w))
548
554
  }
549
555
  r := w.Roles[role]
@@ -553,6 +559,15 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
553
559
  if handle != "" && handle != r.Handle {
554
560
  return fail("handle %q does not match role %q's recorded handle %q", handle, role, r.Handle)
555
561
  }
562
+ if w.State == "complete" {
563
+ if !r.Returned || handle == "" || r.Handle == "" || w.Claim == "" {
564
+ return fail("completed wave %q needs a returned role, its recorded --handle, and an owned claim to reconcile", wave)
565
+ }
566
+ if !releaseWaveClaim(root, wave, w, stdout, stderr) {
567
+ return 3
568
+ }
569
+ return 0
570
+ }
556
571
  if r.Returned {
557
572
  return fail("role %q already returned", role)
558
573
  }
@@ -594,8 +609,8 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
594
609
  if all {
595
610
  absorbWave(root, slug, w, stderr)
596
611
  }
597
- if all && w.Claim != "" {
598
- releaseWaveClaim(root, wave, w, stdout, stderr)
612
+ if all && w.Claim != "" && !releaseWaveClaim(root, wave, w, stdout, stderr) {
613
+ return 3
599
614
  }
600
615
  if stored != "" {
601
616
  head, _, _ := strings.Cut(note, " slice=")
@@ -721,11 +736,11 @@ func checkAbandonedWriterClaims(root string, paths []string) error {
721
736
  return nil
722
737
  }
723
738
 
724
- // releaseWaveClaim releases w's claim; explicit terminal abandonment also
725
- // reconciles a verified prior release. A failure only warns.
739
+ // releaseWaveClaim releases w's claim or reconciles a verified prior release.
740
+ // Callers surface failure without undoing the recorded return or abandonment.
726
741
  func releaseWaveClaim(root, wave string, w *dispatchWave, stdout, stderr io.Writer) bool {
727
742
  n, err := releaseClaim(root, w.ClaimSession, w.Claim)
728
- if w.State == "abandoned" && errors.Is(err, errClaimAlreadyReleased) {
743
+ if errors.Is(err, errClaimAlreadyReleased) {
729
744
  fmt.Fprintf(stdout, "dispatch: wave %s claim %s already released\n", wave, w.Claim)
730
745
  return true
731
746
  }
@@ -914,21 +929,21 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
914
929
  if kind == "wright" {
915
930
  lines = append(lines, "dispatch: ADVISORY: wright contract reminder: name the writer-safe checks the wright runs (syntax/type/lint, unit and self-check suites, a narrow local smoke) and never forbid them; only artifact-producing, consumptive, or full canonical gates stay root-owned")
916
931
  }
932
+ if kind == "wright" {
933
+ if refusal := wrightFanoutRefusal(w); refusal != "" {
934
+ fmt.Fprintln(stderr, refusal)
935
+ return nil, 3
936
+ }
937
+ if refusal := widerBatchRefusal(root, slug, w.Slices); refusal != "" {
938
+ fmt.Fprintln(stderr, refusal)
939
+ return nil, 3
940
+ }
941
+ }
917
942
  if len(overCap) == 0 {
918
943
  if review != "" {
919
944
  fmt.Fprintf(stderr, "dispatch: --reslice-review given but no slice has reached its %d-round cap\n", wrightRoundsPerReview)
920
945
  return nil, 2
921
946
  }
922
- if kind == "wright" {
923
- if refusal := wrightFanoutRefusal(w); refusal != "" {
924
- fmt.Fprintln(stderr, refusal)
925
- return nil, 3
926
- }
927
- if refusal := widerBatchRefusal(root, slug, w.Slices); refusal != "" {
928
- fmt.Fprintln(stderr, refusal)
929
- return nil, 3
930
- }
931
- }
932
947
  return lines, 0
933
948
  }
934
949
  if review == "" {