devrites 4.2.0 → 4.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +15 -0
  4. package/docs/engine/commands.md +20 -2
  5. package/docs/orchestration.md +6 -3
  6. package/engine/commands.go +1 -1
  7. package/engine/internal/lib/cli_observe.go +66 -0
  8. package/engine/internal/lib/observe_summary.go +79 -0
  9. package/engine/internal/lib/open_visual.go +254 -0
  10. package/engine/internal/lib/open_visual_test.go +280 -0
  11. package/engine/internal/lib/skilltrust.go +196 -0
  12. package/engine/internal/lib/taskgraph.go +162 -0
  13. package/engine/internal/lib/taskgraph_test.go +150 -0
  14. package/engine/internal/lib/visual_outline.go +163 -0
  15. package/engine/internal/lib/visual_outline_test.go +161 -0
  16. package/engine/internal/lib/workspace_read.go +29 -0
  17. package/engine/internal/parallel/cli.go +467 -0
  18. package/engine/internal/parallel/git.go +181 -0
  19. package/engine/internal/parallel/lease.go +244 -0
  20. package/engine/internal/parallel/ops.go +416 -0
  21. package/engine/internal/parallel/parallel_test.go +242 -0
  22. package/engine/internal/parallel/pathdisjoint.go +129 -0
  23. package/engine/internal/parallel/pathdisjoint_test.go +97 -0
  24. package/engine/internal/state/status.go +41 -16
  25. package/engine/main.go +76 -3
  26. package/engine/root_routing_test.go +50 -15
  27. package/engine/testdata/visual/open-visual-smoke.html +675 -0
  28. package/engine/testdata/visual/open-visual-smoke.outline.md +54 -0
  29. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  30. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  31. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  32. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  33. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  34. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  35. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  36. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  37. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  38. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  39. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  40. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  41. package/pack/.claude/skills/rite-build/SKILL.md +29 -36
  42. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
  43. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  44. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  45. package/pack/.claude/skills/rite-build/reference/output.md +2 -0
  46. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
  47. package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
  48. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  49. package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  50. package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
  51. package/pack/.claude/skills/rite-define/SKILL.md +10 -1
  52. package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
  53. package/pack/.claude/skills/rite-explain/SKILL.md +23 -7
  54. package/pack/.claude/skills/rite-explain/reference/intake.md +11 -2
  55. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  56. package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -2
  57. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +26 -3
  58. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  59. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  60. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  61. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  62. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  63. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  64. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  65. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  66. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  67. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  68. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  69. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  70. package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
  71. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
  72. package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  73. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  74. package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
  75. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
  76. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
  77. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  78. package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  79. package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
  80. package/pack/generated/claude/skills/rite-define/SKILL.md +10 -1
  81. package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
  82. package/pack/generated/claude/skills/rite-explain/SKILL.md +23 -7
  83. package/pack/generated/claude/skills/rite-explain/reference/intake.md +11 -2
  84. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  85. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -2
  86. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +26 -3
  87. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  88. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  89. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  90. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  91. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  92. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  93. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  94. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  95. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  96. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  97. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  98. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  99. package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
  100. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
  101. package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  102. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
  103. package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
  104. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
  105. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
  106. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
  107. package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
  108. package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
  109. package/pack/generated/codex/skills/rite-define/SKILL.md +10 -1
  110. package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
  111. package/pack/generated/codex/skills/rite-explain/SKILL.md +23 -7
  112. package/pack/generated/codex/skills/rite-explain/reference/intake.md +11 -2
  113. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
  114. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -2
  115. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +26 -3
  116. package/package.json +1 -1
  117. package/scripts/install-lib.sh +1 -1
@@ -0,0 +1,467 @@
1
+ package parallel
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+ "os"
8
+ "strings"
9
+ )
10
+
11
+ // Exit codes match the engine CLI conventions.
12
+ const (
13
+ ExitOK = 0
14
+ ExitUsage = 2
15
+ ExitBlocked = 3
16
+ )
17
+
18
+ // Run is the engine entrypoint for `parallel …` and `check path-disjoint`.
19
+ // command is either "parallel" or "path-disjoint".
20
+ func Run(command string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
21
+ switch command {
22
+ case "path-disjoint":
23
+ return runPathDisjoint(args, stdin, stdout, stderr)
24
+ case "parallel":
25
+ return runParallel(args, stdin, stdout, stderr)
26
+ default:
27
+ fmt.Fprintf(stderr, "parallel: unknown command %q\n", command)
28
+ return ExitUsage
29
+ }
30
+ }
31
+
32
+ func runPathDisjoint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
33
+ root := ""
34
+ jsonPath := "-"
35
+ for i := 0; i < len(args); i++ {
36
+ switch args[i] {
37
+ case "--root":
38
+ if i+1 >= len(args) {
39
+ fmt.Fprintln(stderr, "usage: check path-disjoint [--root <dir>] [<json-file>|-]")
40
+ return ExitUsage
41
+ }
42
+ i++
43
+ root = args[i]
44
+ case "-h", "--help":
45
+ fmt.Fprintln(stdout, "usage: check path-disjoint [--root <dir>] [<json-file>|-]")
46
+ return ExitOK
47
+ default:
48
+ if args[i] != "-" && strings.HasPrefix(args[i], "-") {
49
+ fmt.Fprintf(stderr, "path-disjoint: unknown flag %s\n", args[i])
50
+ return ExitUsage
51
+ }
52
+ jsonPath = args[i]
53
+ }
54
+ }
55
+ data, err := readJSONInput(jsonPath, stdin)
56
+ if err != nil {
57
+ fmt.Fprintf(stderr, "path-disjoint: %v\n", err)
58
+ return ExitBlocked
59
+ }
60
+ slices, err := ParseSlicesJSON(data)
61
+ if err != nil {
62
+ fmt.Fprintf(stderr, "path-disjoint: %v\n", err)
63
+ return ExitBlocked
64
+ }
65
+ ids, err := CheckPathDisjoint(slices, root)
66
+ if err != nil {
67
+ fmt.Fprintf(stderr, "path-disjoint: %v\n", err)
68
+ return ExitBlocked
69
+ }
70
+ fmt.Fprintf(stdout, "path-disjoint: ok (%d slices)\n", len(ids))
71
+ return ExitOK
72
+ }
73
+
74
+ func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
75
+ if len(args) == 0 {
76
+ fmt.Fprintln(stderr, parallelUsage())
77
+ return ExitUsage
78
+ }
79
+ sub, rest := args[0], args[1:]
80
+ switch sub {
81
+ case "create":
82
+ return cmdCreate(rest, stdin, stdout, stderr)
83
+ case "record-green":
84
+ return cmdRecordGreen(rest, stdout, stderr)
85
+ case "abort":
86
+ return cmdAbort(rest, stdout, stderr)
87
+ case "integrate":
88
+ return cmdIntegrate(rest, stdout, stderr)
89
+ case "cleanup":
90
+ return cmdCleanup(rest, stdout, stderr)
91
+ case "status":
92
+ return cmdStatus(rest, stdout, stderr)
93
+ case "lease-write", "write-lease":
94
+ return cmdLeaseWrite(rest, stdin, stdout, stderr)
95
+ case "lease-read", "read-lease":
96
+ return cmdLeaseRead(rest, stdout, stderr)
97
+ case "lease-clear", "clear-lease":
98
+ return cmdLeaseClear(rest, stdout, stderr)
99
+ case "check-disjoint", "path-disjoint":
100
+ return runPathDisjoint(rest, stdin, stdout, stderr)
101
+ case "-h", "--help", "help":
102
+ fmt.Fprintln(stdout, parallelUsage())
103
+ return ExitOK
104
+ default:
105
+ fmt.Fprintf(stderr, "parallel: unknown subcommand %q\n\n%s\n", sub, parallelUsage())
106
+ return ExitUsage
107
+ }
108
+ }
109
+
110
+ func parallelUsage() string {
111
+ return strings.TrimSpace(`usage: parallel <subcommand> [options]
112
+
113
+ Subcommands:
114
+ create --root --slug --batch --base --json
115
+ record-green --root --slug --slice --commit
116
+ abort --root --slug [--force]
117
+ integrate --root --slug [--apply-to-control] [--force]
118
+ cleanup --root --slug [--force]
119
+ status --root --slug
120
+ lease-write --root --slug --json
121
+ lease-read --root --slug [--field name]
122
+ lease-clear --root --slug
123
+ check-disjoint [--root] [<json-file>|-]
124
+
125
+ Exit codes: 0 ok, 2 usage, 3 blocked`)
126
+ }
127
+
128
+ type flagSet struct {
129
+ Root, Slug, Batch, Base, Slice, Commit, JSON, Field, Session string
130
+ ApplyToControl, Force bool
131
+ }
132
+
133
+ func parseFlags(args []string, stderr io.Writer) (flagSet, []string, int) {
134
+ var f flagSet
135
+ i := 0
136
+ for i < len(args) {
137
+ a := args[i]
138
+ need := func(name string) (string, bool) {
139
+ if i+1 >= len(args) {
140
+ fmt.Fprintf(stderr, "parallel: %s requires a value\n", name)
141
+ return "", false
142
+ }
143
+ i++
144
+ return args[i], true
145
+ }
146
+ switch a {
147
+ case "--root":
148
+ v, ok := need(a)
149
+ if !ok {
150
+ return f, nil, ExitUsage
151
+ }
152
+ f.Root = v
153
+ case "--slug":
154
+ v, ok := need(a)
155
+ if !ok {
156
+ return f, nil, ExitUsage
157
+ }
158
+ f.Slug = v
159
+ case "--batch":
160
+ v, ok := need(a)
161
+ if !ok {
162
+ return f, nil, ExitUsage
163
+ }
164
+ f.Batch = v
165
+ case "--base":
166
+ v, ok := need(a)
167
+ if !ok {
168
+ return f, nil, ExitUsage
169
+ }
170
+ f.Base = v
171
+ case "--slice":
172
+ v, ok := need(a)
173
+ if !ok {
174
+ return f, nil, ExitUsage
175
+ }
176
+ f.Slice = v
177
+ case "--commit":
178
+ v, ok := need(a)
179
+ if !ok {
180
+ return f, nil, ExitUsage
181
+ }
182
+ f.Commit = v
183
+ case "--json":
184
+ v, ok := need(a)
185
+ if !ok {
186
+ return f, nil, ExitUsage
187
+ }
188
+ f.JSON = v
189
+ case "--field":
190
+ v, ok := need(a)
191
+ if !ok {
192
+ return f, nil, ExitUsage
193
+ }
194
+ f.Field = v
195
+ case "--session":
196
+ v, ok := need(a)
197
+ if !ok {
198
+ return f, nil, ExitUsage
199
+ }
200
+ f.Session = v
201
+ case "--apply-to-control":
202
+ f.ApplyToControl = true
203
+ case "--force":
204
+ f.Force = true
205
+ default:
206
+ return f, args[i:], ExitOK
207
+ }
208
+ i++
209
+ }
210
+ return f, nil, ExitOK
211
+ }
212
+
213
+ func requireRootSlug(f flagSet, stderr io.Writer) int {
214
+ if f.Root == "" || f.Slug == "" {
215
+ fmt.Fprintln(stderr, "parallel: --root and --slug are required")
216
+ return ExitUsage
217
+ }
218
+ return ExitOK
219
+ }
220
+
221
+ func readJSONInput(path string, stdin io.Reader) ([]byte, error) {
222
+ if path == "" || path == "-" {
223
+ return io.ReadAll(stdin)
224
+ }
225
+ if strings.Contains(path, "..") {
226
+ return nil, fmt.Errorf("json path must not contain '..'")
227
+ }
228
+ return os.ReadFile(path)
229
+ }
230
+
231
+ func cmdCreate(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
232
+ f, _, code := parseFlags(args, stderr)
233
+ if code != ExitOK {
234
+ return code
235
+ }
236
+ if code := requireRootSlug(f, stderr); code != ExitOK {
237
+ return code
238
+ }
239
+ if f.Batch == "" || f.Base == "" || f.JSON == "" {
240
+ fmt.Fprintln(stderr, "usage: parallel create --root --slug --batch --base --json")
241
+ return ExitUsage
242
+ }
243
+ data, err := readJSONInput(f.JSON, stdin)
244
+ if err != nil {
245
+ fmt.Fprintf(stderr, "parallel create: %v\n", err)
246
+ return ExitBlocked
247
+ }
248
+ slices, err := ParseSlicesJSON(data)
249
+ if err != nil {
250
+ fmt.Fprintf(stderr, "parallel create: %v\n", err)
251
+ return ExitBlocked
252
+ }
253
+ lease, err := Create(CreateOpts{
254
+ Root: f.Root,
255
+ Slug: f.Slug,
256
+ BatchID: f.Batch,
257
+ BaseSHA: f.Base,
258
+ Session: f.Session,
259
+ Slices: slices,
260
+ })
261
+ if err != nil {
262
+ fmt.Fprintf(stderr, "parallel create: %v\n", err)
263
+ return ExitBlocked
264
+ }
265
+ fmt.Fprintf(stdout, "created: %d worktrees; batch=%s status=%s\n", lease.N, lease.BatchID, lease.Status)
266
+ return ExitOK
267
+ }
268
+
269
+ func cmdRecordGreen(args []string, stdout, stderr io.Writer) int {
270
+ f, _, code := parseFlags(args, stderr)
271
+ if code != ExitOK {
272
+ return code
273
+ }
274
+ if code := requireRootSlug(f, stderr); code != ExitOK {
275
+ return code
276
+ }
277
+ if f.Slice == "" || f.Commit == "" {
278
+ fmt.Fprintln(stderr, "usage: parallel record-green --root --slug --slice --commit")
279
+ return ExitUsage
280
+ }
281
+ lease, err := RecordGreen(f.Root, f.Slug, f.Slice, f.Commit)
282
+ if err != nil {
283
+ fmt.Fprintf(stderr, "parallel record-green: %v\n", err)
284
+ return ExitBlocked
285
+ }
286
+ fmt.Fprintf(stdout, "record-green: slice=%s status=%s\n", f.Slice, lease.Status)
287
+ return ExitOK
288
+ }
289
+
290
+ func cmdAbort(args []string, stdout, stderr io.Writer) int {
291
+ f, _, code := parseFlags(args, stderr)
292
+ if code != ExitOK {
293
+ return code
294
+ }
295
+ if code := requireRootSlug(f, stderr); code != ExitOK {
296
+ return code
297
+ }
298
+ lease, err := Abort(f.Root, f.Slug, f.Force)
299
+ if err != nil {
300
+ fmt.Fprintf(stderr, "parallel abort: %v\n", err)
301
+ return ExitBlocked
302
+ }
303
+ fmt.Fprintf(stdout, "aborted: status=%s base=%s\n", lease.Status, lease.BaseSHA)
304
+ return ExitOK
305
+ }
306
+
307
+ func cmdIntegrate(args []string, stdout, stderr io.Writer) int {
308
+ f, _, code := parseFlags(args, stderr)
309
+ if code != ExitOK {
310
+ return code
311
+ }
312
+ if code := requireRootSlug(f, stderr); code != ExitOK {
313
+ return code
314
+ }
315
+ tip, lease, err := Integrate(IntegrateOpts{
316
+ Root: f.Root,
317
+ Slug: f.Slug,
318
+ ApplyToControl: f.ApplyToControl,
319
+ Force: f.Force,
320
+ })
321
+ if err != nil {
322
+ fmt.Fprintf(stderr, "parallel integrate: %v\n", err)
323
+ return ExitBlocked
324
+ }
325
+ fmt.Fprintf(stdout, "integrate-ok: tip=%s status=%s apply_to_control=%v\n", tip, lease.Status, f.ApplyToControl)
326
+ return ExitOK
327
+ }
328
+
329
+ func cmdCleanup(args []string, stdout, stderr io.Writer) int {
330
+ f, _, code := parseFlags(args, stderr)
331
+ if code != ExitOK {
332
+ return code
333
+ }
334
+ if code := requireRootSlug(f, stderr); code != ExitOK {
335
+ return code
336
+ }
337
+ if err := Cleanup(f.Root, f.Slug, f.Force); err != nil {
338
+ fmt.Fprintf(stderr, "parallel cleanup: %v\n", err)
339
+ return ExitBlocked
340
+ }
341
+ fmt.Fprintln(stdout, "cleanup: done")
342
+ return ExitOK
343
+ }
344
+
345
+ func cmdStatus(args []string, stdout, stderr io.Writer) int {
346
+ f, _, code := parseFlags(args, stderr)
347
+ if code != ExitOK {
348
+ return code
349
+ }
350
+ if code := requireRootSlug(f, stderr); code != ExitOK {
351
+ return code
352
+ }
353
+ text, err := StatusReport(f.Root, f.Slug)
354
+ if err != nil {
355
+ fmt.Fprintf(stderr, "parallel status: %v\n", err)
356
+ return ExitBlocked
357
+ }
358
+ fmt.Fprint(stdout, text)
359
+ return ExitOK
360
+ }
361
+
362
+ func cmdLeaseWrite(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
363
+ f, _, code := parseFlags(args, stderr)
364
+ if code != ExitOK {
365
+ return code
366
+ }
367
+ if code := requireRootSlug(f, stderr); code != ExitOK {
368
+ return code
369
+ }
370
+ if f.JSON == "" {
371
+ fmt.Fprintln(stderr, "usage: parallel lease-write --root --slug --json")
372
+ return ExitUsage
373
+ }
374
+ data, err := readJSONInput(f.JSON, stdin)
375
+ if err != nil {
376
+ fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
377
+ return ExitBlocked
378
+ }
379
+ var lease Lease
380
+ if err := json.Unmarshal(data, &lease); err != nil {
381
+ fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
382
+ return ExitBlocked
383
+ }
384
+ if lease.CreatedAt == "" {
385
+ lease.CreatedAt = NowUTC()
386
+ }
387
+ path, err := LeasePath(f.Root, f.Slug)
388
+ if err != nil {
389
+ fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
390
+ return ExitUsage
391
+ }
392
+ if err := WriteLease(path, &lease); err != nil {
393
+ fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
394
+ return ExitBlocked
395
+ }
396
+ fmt.Fprintln(stdout, path)
397
+ return ExitOK
398
+ }
399
+
400
+ func cmdLeaseRead(args []string, stdout, stderr io.Writer) int {
401
+ f, _, code := parseFlags(args, stderr)
402
+ if code != ExitOK {
403
+ return code
404
+ }
405
+ if code := requireRootSlug(f, stderr); code != ExitOK {
406
+ return code
407
+ }
408
+ path, err := LeasePath(f.Root, f.Slug)
409
+ if err != nil {
410
+ fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
411
+ return ExitUsage
412
+ }
413
+ lease, err := ReadLease(path)
414
+ if err != nil {
415
+ fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
416
+ return ExitBlocked
417
+ }
418
+ switch f.Field {
419
+ case "", "yaml", "md":
420
+ text, err := EmitLeaseMarkdown(lease)
421
+ if err != nil {
422
+ fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
423
+ return ExitBlocked
424
+ }
425
+ fmt.Fprint(stdout, text)
426
+ case "json":
427
+ text, err := LeaseJSON(lease)
428
+ if err != nil {
429
+ fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
430
+ return ExitBlocked
431
+ }
432
+ fmt.Fprintln(stdout, text)
433
+ case "status":
434
+ fmt.Fprintln(stdout, lease.Status)
435
+ case "base_sha":
436
+ fmt.Fprintln(stdout, lease.BaseSHA)
437
+ case "batch_id":
438
+ fmt.Fprintln(stdout, lease.BatchID)
439
+ case "n":
440
+ fmt.Fprintln(stdout, lease.N)
441
+ default:
442
+ fmt.Fprintf(stderr, "parallel lease-read: unknown field %q\n", f.Field)
443
+ return ExitUsage
444
+ }
445
+ return ExitOK
446
+ }
447
+
448
+ func cmdLeaseClear(args []string, stdout, stderr io.Writer) int {
449
+ f, _, code := parseFlags(args, stderr)
450
+ if code != ExitOK {
451
+ return code
452
+ }
453
+ if code := requireRootSlug(f, stderr); code != ExitOK {
454
+ return code
455
+ }
456
+ path, err := LeasePath(f.Root, f.Slug)
457
+ if err != nil {
458
+ fmt.Fprintf(stderr, "parallel lease-clear: %v\n", err)
459
+ return ExitUsage
460
+ }
461
+ if err := ClearLease(path); err != nil {
462
+ fmt.Fprintf(stderr, "parallel lease-clear: %v\n", err)
463
+ return ExitBlocked
464
+ }
465
+ fmt.Fprintf(stdout, "cleared: %s\n", path)
466
+ return ExitOK
467
+ }
@@ -0,0 +1,181 @@
1
+ package parallel
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "errors"
7
+ "fmt"
8
+ "os"
9
+ "os/exec"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/devrites/devrites/internal/gitenv"
14
+ )
15
+
16
+ const gitTimeout = 60 * time.Second
17
+
18
+ func git(repo string, args ...string) (string, error) {
19
+ ctx, cancel := context.WithTimeout(context.Background(), gitTimeout)
20
+ defer cancel()
21
+ cmd := exec.CommandContext(ctx, "git", append([]string{"-C", repo}, args...)...)
22
+ env := append(gitenv.Sanitize(os.Environ()),
23
+ "GIT_TERMINAL_PROMPT=0",
24
+ "GCM_INTERACTIVE=never",
25
+ "GIT_PAGER=cat",
26
+ "PAGER=cat",
27
+ "LC_ALL=C",
28
+ )
29
+ if gitOpNeedsIdentity(args) {
30
+ env = ensureGitIdentityEnv(env)
31
+ }
32
+ cmd.Env = env
33
+ var stdout, stderr bytes.Buffer
34
+ cmd.Stdout = &stdout
35
+ cmd.Stderr = &stderr
36
+ err := cmd.Run()
37
+ out := strings.TrimSpace(stdout.String())
38
+ if err != nil {
39
+ detail := strings.TrimSpace(stderr.String())
40
+ if detail == "" {
41
+ detail = out
42
+ }
43
+ if ctx.Err() != nil {
44
+ return out, fmt.Errorf("git %s: %w", strings.Join(args, " "), ctx.Err())
45
+ }
46
+ var exitErr *exec.ExitError
47
+ if errors.As(err, &exitErr) {
48
+ return out, fmt.Errorf("git %s: exit %d: %s", strings.Join(args, " "), exitErr.ExitCode(), detail)
49
+ }
50
+ return out, fmt.Errorf("git %s: %v: %s", strings.Join(args, " "), err, detail)
51
+ }
52
+ return out, nil
53
+ }
54
+
55
+ func gitOpNeedsIdentity(args []string) bool {
56
+ for _, a := range args {
57
+ switch a {
58
+ case "cherry-pick", "commit", "merge", "rebase", "am":
59
+ return true
60
+ }
61
+ }
62
+ return false
63
+ }
64
+
65
+ // ensureGitIdentityEnv fills author/committer when the host has none (common on
66
+ // CI images). Prefer existing env identity when present.
67
+ func ensureGitIdentityEnv(env []string) []string {
68
+ keys := map[string]string{
69
+ "GIT_AUTHOR_NAME": "devrites-engine",
70
+ "GIT_AUTHOR_EMAIL": "engine@devrites.invalid",
71
+ "GIT_COMMITTER_NAME": "devrites-engine",
72
+ "GIT_COMMITTER_EMAIL": "engine@devrites.invalid",
73
+ }
74
+ have := make(map[string]bool, len(env))
75
+ for _, e := range env {
76
+ if i := strings.IndexByte(e, '='); i > 0 {
77
+ have[e[:i]] = true
78
+ }
79
+ }
80
+ for k, v := range keys {
81
+ if !have[k] {
82
+ env = append(env, k+"="+v)
83
+ }
84
+ }
85
+ return env
86
+ }
87
+
88
+ func revParse(repo, rev string) (string, error) {
89
+ return git(repo, "rev-parse", "--verify", rev+"^{commit}")
90
+ }
91
+
92
+ func isAncestor(repo, ancestor, descendant string) (bool, error) {
93
+ _, err := git(repo, "merge-base", "--is-ancestor", ancestor, descendant)
94
+ if err == nil {
95
+ return true, nil
96
+ }
97
+ if strings.Contains(err.Error(), "exit 1") {
98
+ return false, nil
99
+ }
100
+ return false, err
101
+ }
102
+
103
+ func diffNames(repo, a, b string) ([]string, error) {
104
+ out, err := git(repo, "diff", "--name-only", a, b)
105
+ if err != nil {
106
+ return nil, err
107
+ }
108
+ if out == "" {
109
+ return nil, nil
110
+ }
111
+ lines := strings.Split(out, "\n")
112
+ for i := range lines {
113
+ lines[i] = strings.TrimSpace(lines[i])
114
+ }
115
+ return lines, nil
116
+ }
117
+
118
+ func headSHA(repo string) (string, error) {
119
+ return revParse(repo, "HEAD")
120
+ }
121
+
122
+ func resetHard(repo, sha string) error {
123
+ _, err := git(repo, "reset", "--hard", sha)
124
+ return err
125
+ }
126
+
127
+ func porcelainDirty(repo string) (bool, error) {
128
+ out, err := git(repo, "status", "--porcelain", "--untracked-files=no")
129
+ if err != nil {
130
+ return false, err
131
+ }
132
+ return out != "", nil
133
+ }
134
+
135
+ func branchExists(repo, branch string) (bool, error) {
136
+ _, err := git(repo, "show-ref", "--verify", "--quiet", "refs/heads/"+branch)
137
+ if err == nil {
138
+ return true, nil
139
+ }
140
+ if strings.Contains(err.Error(), "exit 1") {
141
+ return false, nil
142
+ }
143
+ return false, err
144
+ }
145
+
146
+ func deleteBranch(repo, branch string) error {
147
+ exists, err := branchExists(repo, branch)
148
+ if err != nil || !exists {
149
+ return err
150
+ }
151
+ _, err = git(repo, "branch", "-D", branch)
152
+ return err
153
+ }
154
+
155
+ func worktreeAdd(repo, path, branch, startPoint string) error {
156
+ _, err := git(repo, "worktree", "add", "-b", branch, path, startPoint)
157
+ return err
158
+ }
159
+
160
+ func worktreeRemove(repo, path string) error {
161
+ _, err := git(repo, "worktree", "remove", "--force", path)
162
+ if err != nil {
163
+ _ = os.RemoveAll(path)
164
+ _, _ = git(repo, "worktree", "prune")
165
+ }
166
+ return err
167
+ }
168
+
169
+ func mergeFFOnly(repo, commit string) error {
170
+ _, err := git(repo, "merge", "--ff-only", commit)
171
+ return err
172
+ }
173
+
174
+ func cherryPickRange(repo, fromExclusive, toInclusive string) error {
175
+ _, err := git(repo, "cherry-pick", "-x", fromExclusive+".."+toInclusive)
176
+ return err
177
+ }
178
+
179
+ func cherryPickAbort(repo string) {
180
+ _, _ = git(repo, "cherry-pick", "--abort")
181
+ }