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.
- package/CHANGELOG.md +41 -2
- package/README.md +21 -1
- package/docs/cli.md +98 -13
- package/docs/command-map.md +2 -2
- package/docs/engine/commands.md +10 -5
- package/docs/engine/workspace-schema.md +4 -3
- package/engine/commands.go +1 -1
- package/engine/help.go +3 -1
- package/engine/internal/hostpack/hostpack.go +1 -1
- package/engine/internal/lib/apply_bundle.go +25 -8
- package/engine/internal/lib/continuation.go +33 -4
- package/engine/internal/lib/dispatch.go +46 -31
- package/engine/internal/lib/frontier.go +808 -0
- package/engine/internal/lib/handoff.go +2 -5
- package/engine/internal/lib/next.go +10 -0
- package/engine/internal/lib/regression.go +0 -5
- package/engine/internal/lib/resolve.go +33 -16
- package/engine/internal/parallel/cli.go +16 -5
- package/engine/internal/parallel/ops.go +7 -5
- package/engine/internal/parallel/repository.go +13 -0
- package/engine/internal/release/release.go +96 -5
- package/engine/internal/rootfacts/facts.go +11 -0
- package/engine/internal/state/lock_other.go +4 -0
- package/engine/internal/state/write.go +54 -13
- package/engine/main.go +6 -2
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/.claude/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/README.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/generated/devin/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/generated/dsh/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +1 -1
- package/pack/generated/omp/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/pi/AGENTS.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +41 -40
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +10 -10
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +6 -6
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +7 -7
- package/pack/generated/pi/skills/rite-doctor/SKILL.md +70 -68
- package/package.json +1 -1
- package/scripts/build-host-artifacts.sh +1 -1
- package/scripts/dsh-generate.py +5 -1
- package/scripts/omp-generate.sh +4 -4
- 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
|
|
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
|
|
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,
|
|
169
|
+
return nil, fmt.Errorf("dispatch.json is not valid JSON: %w", err)
|
|
164
170
|
}
|
|
165
171
|
if f.Waves == nil {
|
|
166
|
-
return nil,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
725
|
-
//
|
|
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
|
|
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 == "" {
|