devrites 5.2.4 → 5.4.0
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 +12 -0
- package/README.md +1 -1
- package/engine/internal/install/uninstall.go +3 -0
- package/engine/internal/lib/resolve.go +7 -0
- package/engine/internal/parallel/cli.go +62 -8
- package/engine/internal/parallel/git.go +46 -7
- package/engine/internal/parallel/lease.go +7 -0
- package/engine/internal/parallel/ops.go +256 -72
- package/engine/internal/parallel/ops_test.go +223 -6
- package/engine/internal/parallel/parallel_test.go +229 -6
- package/engine/internal/parallel/pathdisjoint.go +18 -3
- package/engine/internal/parallel/pathdisjoint_test.go +21 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/.claude/skills/rite-build/SKILL.md +9 -3
- package/pack/.claude/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +87 -20
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/generated/claude/skills/rite-build/SKILL.md +9 -3
- package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +87 -20
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/generated/codex/skills/rite-build/SKILL.md +9 -3
- package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +87 -20
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/generated/omp/skills/rite-build/SKILL.md +9 -3
- package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +87 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [5.4.0](https://github.com/ViktorsBaikers/DevRites/compare/v5.3.0...v5.4.0) (2026-09-13)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
* **rite:** auto re-batch on frozen-lease scope defect ([#67](https://github.com/ViktorsBaikers/DevRites/issues/67)) ([6728385](https://github.com/ViktorsBaikers/DevRites/commit/6728385b522a5b34bebacb89a5855e81d54c0f68))
|
|
10
|
+
|
|
11
|
+
## [5.3.0](https://github.com/ViktorsBaikers/DevRites/compare/v5.2.4...v5.3.0) (2026-09-13)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* **rite:** preserve parallel batch work, harden lease lifecycle ([#66](https://github.com/ViktorsBaikers/DevRites/issues/66)) ([f8d3e2e](https://github.com/ViktorsBaikers/DevRites/commit/f8d3e2e9870b2adaa11da0384f2b0ff23ee8785b))
|
|
16
|
+
|
|
5
17
|
## [5.2.4](https://github.com/ViktorsBaikers/DevRites/compare/v5.2.3...v5.2.4) (2026-09-12)
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ project-conventional push, tag, or PR action, and archive the workspace.
|
|
|
33
33
|
Unattended runs may create local WIP checkpoint commits along the way, but they
|
|
34
34
|
remain local unless Ship's disclosed plan includes an approved remote action.
|
|
35
35
|
|
|
36
|
-
**Status:** [`v5.
|
|
36
|
+
**Status:** [`v5.4.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v5.4.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
37
37
|
|
|
38
38
|
This is the latest published release; `main` may contain unreleased work.
|
|
39
39
|
|
|
@@ -115,5 +115,8 @@ func (r *runner) uninstall() error {
|
|
|
115
115
|
if exists(filepath.Join(r.target, ".devrites", "ACTIVE")) {
|
|
116
116
|
fmt.Fprintln(r.opts.Stdout, " kept .devrites/ACTIVE (active-feature cursor)")
|
|
117
117
|
}
|
|
118
|
+
if exists(filepath.Join(r.target, ".scratch", "parallel-wt")) {
|
|
119
|
+
fmt.Fprintln(r.opts.Stdout, " kept .scratch/parallel-wt/ (live parallel worktrees — inspect before removing)")
|
|
120
|
+
}
|
|
118
121
|
return nil
|
|
119
122
|
}
|
|
@@ -140,6 +140,12 @@ func fail(stderr io.Writer, msg string, code int) int {
|
|
|
140
140
|
return code
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// oneLine flattens an operator-supplied answer to a single line so it cannot
|
|
144
|
+
// corrupt the questions.md field structure (answer: is a one-line field).
|
|
145
|
+
func oneLine(s string) string {
|
|
146
|
+
return strings.Join(strings.Fields(s), " ")
|
|
147
|
+
}
|
|
148
|
+
|
|
143
149
|
func isDir(path string) bool {
|
|
144
150
|
info, err := os.Stat(path)
|
|
145
151
|
return err == nil && info.IsDir()
|
|
@@ -195,6 +201,7 @@ func resolveQuestion(qfile, qid, status, answer string, stderr io.Writer) int {
|
|
|
195
201
|
return fail(stderr, "questions.md missing at "+qfile, 2)
|
|
196
202
|
}
|
|
197
203
|
ts := nowUTC()
|
|
204
|
+
answer = oneLine(answer)
|
|
198
205
|
target := regexp.MustCompile(`^## ` + regexp.QuoteMeta(qid) + `([[:space:]]|$)`)
|
|
199
206
|
|
|
200
207
|
updated, found, notOpen := rewriteQuestionFields(splitLinesNoTrailing(data), target, status, answer, ts)
|
|
@@ -113,8 +113,8 @@ func parallelUsage() string {
|
|
|
113
113
|
Subcommands:
|
|
114
114
|
create --root --slug --batch --base --json
|
|
115
115
|
record-green --root --slug --slice --commit
|
|
116
|
-
abort --root --slug
|
|
117
|
-
integrate --root --slug [--apply-to-control]
|
|
116
|
+
abort --root --slug
|
|
117
|
+
integrate --root --slug [--apply-to-control]
|
|
118
118
|
cleanup --root --slug [--force]
|
|
119
119
|
status --root --slug
|
|
120
120
|
lease-write --root --slug --json
|
|
@@ -203,7 +203,11 @@ func parseFlags(args []string, stderr io.Writer) (flagSet, []string, int) {
|
|
|
203
203
|
case "--force":
|
|
204
204
|
f.Force = true
|
|
205
205
|
default:
|
|
206
|
-
|
|
206
|
+
// No parallel subcommand takes positional args; anything
|
|
207
|
+
// unrecognized (typo'd flag, stray word) is a usage error, not
|
|
208
|
+
// something to silently drop.
|
|
209
|
+
fmt.Fprintf(stderr, "parallel: unknown argument %q\n", a)
|
|
210
|
+
return f, nil, ExitUsage
|
|
207
211
|
}
|
|
208
212
|
i++
|
|
209
213
|
}
|
|
@@ -237,6 +241,9 @@ func cmdCreate(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
237
241
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
238
242
|
return code
|
|
239
243
|
}
|
|
244
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
245
|
+
return code
|
|
246
|
+
}
|
|
240
247
|
if f.Batch == "" || f.Base == "" || f.JSON == "" {
|
|
241
248
|
fmt.Fprintln(stderr, "usage: parallel create --root --slug --batch --base --json")
|
|
242
249
|
return ExitUsage
|
|
@@ -267,6 +274,21 @@ func cmdCreate(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
267
274
|
return ExitOK
|
|
268
275
|
}
|
|
269
276
|
|
|
277
|
+
// rejectUnsupportedFlags reports flags a subcommand parses but must not
|
|
278
|
+
// silently ignore: --force only exists for cleanup, --apply-to-control only
|
|
279
|
+
// for integrate.
|
|
280
|
+
func rejectUnsupportedFlags(f flagSet, stderr io.Writer, force, applyToControl bool) int {
|
|
281
|
+
if f.Force && !force {
|
|
282
|
+
fmt.Fprintln(stderr, "parallel: --force is only supported by cleanup")
|
|
283
|
+
return ExitUsage
|
|
284
|
+
}
|
|
285
|
+
if f.ApplyToControl && !applyToControl {
|
|
286
|
+
fmt.Fprintln(stderr, "parallel: --apply-to-control is only supported by integrate")
|
|
287
|
+
return ExitUsage
|
|
288
|
+
}
|
|
289
|
+
return ExitOK
|
|
290
|
+
}
|
|
291
|
+
|
|
270
292
|
func cmdRecordGreen(args []string, stdout, stderr io.Writer) int {
|
|
271
293
|
f, _, code := parseFlags(args, stderr)
|
|
272
294
|
if code != ExitOK {
|
|
@@ -275,6 +297,9 @@ func cmdRecordGreen(args []string, stdout, stderr io.Writer) int {
|
|
|
275
297
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
276
298
|
return code
|
|
277
299
|
}
|
|
300
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
301
|
+
return code
|
|
302
|
+
}
|
|
278
303
|
if f.Slice == "" || f.Commit == "" {
|
|
279
304
|
fmt.Fprintln(stderr, "usage: parallel record-green --root --slug --slice --commit")
|
|
280
305
|
return ExitUsage
|
|
@@ -296,7 +321,10 @@ func cmdAbort(args []string, stdout, stderr io.Writer) int {
|
|
|
296
321
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
297
322
|
return code
|
|
298
323
|
}
|
|
299
|
-
|
|
324
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
325
|
+
return code
|
|
326
|
+
}
|
|
327
|
+
lease, err := Abort(f.Root, f.Slug)
|
|
300
328
|
if err != nil {
|
|
301
329
|
fmt.Fprintf(stderr, "parallel abort: %v\n", err)
|
|
302
330
|
return ExitBlocked
|
|
@@ -313,11 +341,13 @@ func cmdIntegrate(args []string, stdout, stderr io.Writer) int {
|
|
|
313
341
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
314
342
|
return code
|
|
315
343
|
}
|
|
344
|
+
if code := rejectUnsupportedFlags(f, stderr, false, true); code != ExitOK {
|
|
345
|
+
return code
|
|
346
|
+
}
|
|
316
347
|
tip, lease, err := Integrate(IntegrateOpts{
|
|
317
348
|
Root: f.Root,
|
|
318
349
|
Slug: f.Slug,
|
|
319
350
|
ApplyToControl: f.ApplyToControl,
|
|
320
|
-
Force: f.Force,
|
|
321
351
|
})
|
|
322
352
|
if err != nil {
|
|
323
353
|
fmt.Fprintf(stderr, "parallel integrate: %v\n", err)
|
|
@@ -335,10 +365,21 @@ func cmdCleanup(args []string, stdout, stderr io.Writer) int {
|
|
|
335
365
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
336
366
|
return code
|
|
337
367
|
}
|
|
338
|
-
if
|
|
368
|
+
if code := rejectUnsupportedFlags(f, stderr, true, false); code != ExitOK {
|
|
369
|
+
return code
|
|
370
|
+
}
|
|
371
|
+
salvaged, err := Cleanup(f.Root, f.Slug, f.Force)
|
|
372
|
+
if err != nil {
|
|
339
373
|
fmt.Fprintf(stderr, "parallel cleanup: %v\n", err)
|
|
340
374
|
return ExitBlocked
|
|
341
375
|
}
|
|
376
|
+
for _, s := range salvaged {
|
|
377
|
+
fmt.Fprintf(stdout, "salvaged: slice=%s branch=%s commit=%s", s.SliceID, s.Branch, s.Commit)
|
|
378
|
+
if s.Worktree != "" {
|
|
379
|
+
fmt.Fprintf(stdout, " worktree=%s(kept)", s.Worktree)
|
|
380
|
+
}
|
|
381
|
+
fmt.Fprintln(stdout)
|
|
382
|
+
}
|
|
342
383
|
fmt.Fprintln(stdout, "cleanup: done")
|
|
343
384
|
return ExitOK
|
|
344
385
|
}
|
|
@@ -351,6 +392,9 @@ func cmdStatus(args []string, stdout, stderr io.Writer) int {
|
|
|
351
392
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
352
393
|
return code
|
|
353
394
|
}
|
|
395
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
396
|
+
return code
|
|
397
|
+
}
|
|
354
398
|
text, err := StatusReport(f.Root, f.Slug)
|
|
355
399
|
if err != nil {
|
|
356
400
|
fmt.Fprintf(stderr, "parallel status: %v\n", err)
|
|
@@ -385,12 +429,16 @@ func cmdLeaseWrite(args []string, stdin io.Reader, stdout, stderr io.Writer) int
|
|
|
385
429
|
if lease.CreatedAt == "" {
|
|
386
430
|
lease.CreatedAt = NowUTC()
|
|
387
431
|
}
|
|
432
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
433
|
+
return code
|
|
434
|
+
}
|
|
388
435
|
path, err := LeasePath(f.Root, f.Slug)
|
|
389
436
|
if err != nil {
|
|
390
437
|
fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
|
|
391
438
|
return ExitUsage
|
|
392
439
|
}
|
|
393
|
-
|
|
440
|
+
err = withLeaseLock(f.Root, f.Slug, func() error { return WriteLease(path, &lease) })
|
|
441
|
+
if err != nil {
|
|
394
442
|
fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
|
|
395
443
|
return ExitBlocked
|
|
396
444
|
}
|
|
@@ -406,6 +454,9 @@ func cmdLeaseRead(args []string, stdout, stderr io.Writer) int {
|
|
|
406
454
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
407
455
|
return code
|
|
408
456
|
}
|
|
457
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
458
|
+
return code
|
|
459
|
+
}
|
|
409
460
|
path, err := LeasePath(f.Root, f.Slug)
|
|
410
461
|
if err != nil {
|
|
411
462
|
fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
|
|
@@ -454,12 +505,15 @@ func cmdLeaseClear(args []string, stdout, stderr io.Writer) int {
|
|
|
454
505
|
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
455
506
|
return code
|
|
456
507
|
}
|
|
508
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
509
|
+
return code
|
|
510
|
+
}
|
|
457
511
|
path, err := LeasePath(f.Root, f.Slug)
|
|
458
512
|
if err != nil {
|
|
459
513
|
fmt.Fprintf(stderr, "parallel lease-clear: %v\n", err)
|
|
460
514
|
return ExitUsage
|
|
461
515
|
}
|
|
462
|
-
if err := ClearLease(path); err != nil {
|
|
516
|
+
if err := withLeaseLock(f.Root, f.Slug, func() error { return ClearLease(path) }); err != nil {
|
|
463
517
|
fmt.Fprintf(stderr, "parallel lease-clear: %v\n", err)
|
|
464
518
|
return ExitBlocked
|
|
465
519
|
}
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"fmt"
|
|
8
8
|
"os"
|
|
9
9
|
"os/exec"
|
|
10
|
+
"path/filepath"
|
|
10
11
|
"strings"
|
|
11
12
|
"time"
|
|
12
13
|
|
|
@@ -120,19 +121,57 @@ func headSHA(repo string) (string, error) {
|
|
|
120
121
|
return revParse(repo, "HEAD")
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// ensureScratchExcluded lists the parallel worktree root in .git/info/exclude
|
|
125
|
+
// so control `git status`/`git add -A` never sees nested worker worktrees.
|
|
126
|
+
// Best-effort: an unwritable exclude file must not fail create.
|
|
127
|
+
func ensureScratchExcluded(repo string) {
|
|
128
|
+
gitDir, err := git(repo, "rev-parse", "--git-common-dir")
|
|
129
|
+
if err != nil {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if !filepath.IsAbs(gitDir) {
|
|
133
|
+
gitDir = filepath.Join(repo, gitDir)
|
|
134
|
+
}
|
|
135
|
+
excludePath := filepath.Join(gitDir, "info", "exclude")
|
|
136
|
+
const line = ".scratch/parallel-wt/"
|
|
137
|
+
// #nosec G304 -- exclude file inside the repo's own resolved git dir
|
|
138
|
+
if data, err := os.ReadFile(excludePath); err == nil && strings.Contains(string(data), line) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if err := os.MkdirAll(filepath.Dir(excludePath), 0o755); err != nil {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
// #nosec G304 -- exclude file inside the repo's own resolved git dir
|
|
145
|
+
f, err := os.OpenFile(excludePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
defer func() { _ = f.Close() }()
|
|
150
|
+
_, _ = fmt.Fprintln(f, line)
|
|
126
151
|
}
|
|
127
152
|
|
|
128
|
-
func
|
|
129
|
-
|
|
153
|
+
func porcelainDirtyPaths(repo string, paths []string) (bool, error) {
|
|
154
|
+
args := append([]string{"status", "--porcelain", "--untracked-files=all", "--"}, paths...)
|
|
155
|
+
out, err := git(repo, args...)
|
|
130
156
|
if err != nil {
|
|
131
157
|
return false, err
|
|
132
158
|
}
|
|
133
159
|
return out != "", nil
|
|
134
160
|
}
|
|
135
161
|
|
|
162
|
+
// stagePathsAndCommit commits every change under paths (modifications,
|
|
163
|
+
// deletions, and untracked files) and returns the new HEAD sha.
|
|
164
|
+
func stagePathsAndCommit(repo, msg string, paths []string) (string, error) {
|
|
165
|
+
addArgs := append([]string{"add", "-A", "--ignore-errors", "--"}, paths...)
|
|
166
|
+
if _, err := git(repo, addArgs...); err != nil {
|
|
167
|
+
return "", err
|
|
168
|
+
}
|
|
169
|
+
if _, err := git(repo, "commit", "-m", msg); err != nil {
|
|
170
|
+
return "", err
|
|
171
|
+
}
|
|
172
|
+
return headSHA(repo)
|
|
173
|
+
}
|
|
174
|
+
|
|
136
175
|
func branchExists(repo, branch string) (bool, error) {
|
|
137
176
|
_, err := git(repo, "show-ref", "--verify", "--quiet", "refs/heads/"+branch)
|
|
138
177
|
if err == nil {
|
|
@@ -172,8 +211,8 @@ func mergeFFOnly(repo, commit string) error {
|
|
|
172
211
|
return err
|
|
173
212
|
}
|
|
174
213
|
|
|
175
|
-
func
|
|
176
|
-
_, err := git(repo, "cherry-pick", "-
|
|
214
|
+
func cherryPickNoCommit(repo, fromExclusive, toInclusive string) error {
|
|
215
|
+
_, err := git(repo, "cherry-pick", "-n", fromExclusive+".."+toInclusive)
|
|
177
216
|
return err
|
|
178
217
|
}
|
|
179
218
|
|
|
@@ -73,6 +73,13 @@ func IntegrateBranchName(slug, batchID string) string {
|
|
|
73
73
|
return fmt.Sprintf("devrites/parallel/%s/%s/integrate", slug, batchID)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// ownedBranchName reports whether branch is inside this batch's namespace —
|
|
77
|
+
// devrites/parallel/<slug>/<batch>/…. Cleanup deletes only owned branches;
|
|
78
|
+
// a lease naming anything else (e.g. a forged "main") is left alone.
|
|
79
|
+
func ownedBranchName(slug, batchID, branch string) bool {
|
|
80
|
+
return strings.HasPrefix(branch, fmt.Sprintf("devrites/parallel/%s/%s/", slug, batchID))
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
func WorkerWorktreePath(repoRoot, batchID, sliceID string) string {
|
|
77
84
|
return filepath.Join(ScratchRoot(repoRoot), batchID, sliceID)
|
|
78
85
|
}
|