devrites 6.1.1 → 6.1.3

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 (221) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +4 -1
  4. package/docs/command-map.md +1 -1
  5. package/docs/engine/commands.md +4 -4
  6. package/docs/orchestration.md +4 -3
  7. package/docs/usage.md +1 -1
  8. package/engine/commands.go +1 -1
  9. package/engine/help.go +5 -1
  10. package/engine/internal/acceptance/cli.go +14 -5
  11. package/engine/internal/acceptance/run.go +20 -5
  12. package/engine/internal/lib/apply_bundle.go +474 -0
  13. package/engine/internal/lib/claims.go +12 -2
  14. package/engine/internal/lib/claims_holder.go +124 -0
  15. package/engine/internal/lib/cli_observe.go +14 -0
  16. package/engine/internal/lib/diffscope.go +2 -1
  17. package/engine/internal/lib/taskgraph.go +124 -0
  18. package/engine/internal/parallel/cli.go +111 -9
  19. package/engine/internal/parallel/lease.go +58 -4
  20. package/engine/internal/parallel/ops.go +244 -75
  21. package/engine/internal/parallel/select.go +31 -6
  22. package/engine/main.go +4 -0
  23. package/pack/.claude/agents/devrites-plan-drafter.md +13 -3
  24. package/pack/.claude/agents/devrites-slice-wright.md +5 -1
  25. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  26. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  27. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  28. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +3 -0
  29. package/pack/.claude/skills/devrites-lib/reference/standards/gates.md +3 -0
  30. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +7 -0
  31. package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  32. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  33. package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -2
  34. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +1 -1
  35. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  36. package/pack/.claude/skills/rite-build/SKILL.md +7 -7
  37. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +2 -2
  38. package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
  39. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  40. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +28 -21
  41. package/pack/.claude/skills/rite-build/reference/phase-contract.md +10 -1
  42. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  43. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +9 -2
  44. package/pack/.claude/skills/rite-define/SKILL.md +11 -2
  45. package/pack/.claude/skills/rite-handoff/SKILL.md +1 -1
  46. package/pack/.claude/skills/rite-plan/SKILL.md +20 -9
  47. package/pack/.claude/skills/rite-plan/reference/slicing.md +5 -0
  48. package/pack/.claude/skills/rite-polish/SKILL.md +6 -4
  49. package/pack/.claude/skills/rite-prove/SKILL.md +3 -2
  50. package/pack/.claude/skills/rite-review/SKILL.md +6 -1
  51. package/pack/.claude/skills/rite-seal/SKILL.md +2 -2
  52. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +7 -1
  53. package/pack/.claude/skills/rite-vet/SKILL.md +9 -5
  54. package/pack/.claude/skills/rite-vet/reference/artifacts.md +8 -0
  55. package/pack/.claude/skills/rite-vet/reference/review-axes.md +1 -1
  56. package/pack/generated/claude/agents/devrites-plan-drafter.md +13 -3
  57. package/pack/generated/claude/agents/devrites-slice-wright.md +5 -1
  58. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  59. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  60. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  61. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +3 -0
  62. package/pack/generated/claude/skills/devrites-lib/reference/standards/gates.md +3 -0
  63. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +7 -0
  64. package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  65. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  66. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -2
  67. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +1 -1
  68. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  69. package/pack/generated/claude/skills/rite-build/SKILL.md +7 -7
  70. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +2 -2
  71. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
  72. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  73. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +28 -21
  74. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +10 -1
  75. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  76. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +9 -2
  77. package/pack/generated/claude/skills/rite-define/SKILL.md +11 -2
  78. package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -1
  79. package/pack/generated/claude/skills/rite-plan/SKILL.md +20 -9
  80. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +5 -0
  81. package/pack/generated/claude/skills/rite-polish/SKILL.md +6 -4
  82. package/pack/generated/claude/skills/rite-prove/SKILL.md +3 -2
  83. package/pack/generated/claude/skills/rite-review/SKILL.md +6 -1
  84. package/pack/generated/claude/skills/rite-seal/SKILL.md +2 -2
  85. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +7 -1
  86. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -5
  87. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +8 -0
  88. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +1 -1
  89. package/pack/generated/codex/agents/devrites-plan-drafter.toml +13 -3
  90. package/pack/generated/codex/agents/devrites-slice-wright.toml +5 -1
  91. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  92. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  93. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -2
  94. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +3 -0
  95. package/pack/generated/codex/skills/devrites-lib/reference/standards/gates.md +3 -0
  96. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +7 -0
  97. package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  98. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  99. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -2
  100. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +1 -1
  101. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  102. package/pack/generated/codex/skills/rite-build/SKILL.md +7 -7
  103. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +2 -2
  104. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
  105. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
  106. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +28 -21
  107. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +10 -1
  108. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -2
  109. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +9 -2
  110. package/pack/generated/codex/skills/rite-define/SKILL.md +11 -2
  111. package/pack/generated/codex/skills/rite-handoff/SKILL.md +1 -1
  112. package/pack/generated/codex/skills/rite-plan/SKILL.md +20 -9
  113. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +5 -0
  114. package/pack/generated/codex/skills/rite-polish/SKILL.md +6 -4
  115. package/pack/generated/codex/skills/rite-prove/SKILL.md +3 -2
  116. package/pack/generated/codex/skills/rite-review/SKILL.md +6 -1
  117. package/pack/generated/codex/skills/rite-seal/SKILL.md +2 -2
  118. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +7 -1
  119. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -5
  120. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +8 -0
  121. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +1 -1
  122. package/pack/generated/devin/agents/devrites-plan-drafter.md +13 -3
  123. package/pack/generated/devin/agents/devrites-slice-wright.md +5 -1
  124. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  125. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  126. package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +6 -2
  127. package/pack/generated/devin/skills/devrites-lib/reference/standards/core.md +3 -0
  128. package/pack/generated/devin/skills/devrites-lib/reference/standards/gates.md +3 -0
  129. package/pack/generated/devin/skills/devrites-lib/reference/standards/testing.md +7 -0
  130. package/pack/generated/devin/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  131. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  132. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +2 -2
  133. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +1 -1
  134. package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  135. package/pack/generated/devin/skills/rite-build/SKILL.md +7 -7
  136. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +2 -2
  137. package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
  138. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
  139. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +28 -21
  140. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +10 -1
  141. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -2
  142. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +9 -2
  143. package/pack/generated/devin/skills/rite-define/SKILL.md +11 -2
  144. package/pack/generated/devin/skills/rite-handoff/SKILL.md +1 -1
  145. package/pack/generated/devin/skills/rite-plan/SKILL.md +20 -9
  146. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +5 -0
  147. package/pack/generated/devin/skills/rite-polish/SKILL.md +6 -4
  148. package/pack/generated/devin/skills/rite-prove/SKILL.md +3 -2
  149. package/pack/generated/devin/skills/rite-review/SKILL.md +6 -1
  150. package/pack/generated/devin/skills/rite-seal/SKILL.md +2 -2
  151. package/pack/generated/devin/skills/rite-seal/reference/phase-contract.md +7 -1
  152. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -5
  153. package/pack/generated/devin/skills/rite-vet/reference/artifacts.md +8 -0
  154. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +1 -1
  155. package/pack/generated/omp/agents/devrites-plan-drafter.md +13 -3
  156. package/pack/generated/omp/agents/devrites-slice-wright.md +5 -1
  157. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  158. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  159. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +6 -2
  160. package/pack/generated/omp/skills/devrites-lib/reference/standards/core.md +3 -0
  161. package/pack/generated/omp/skills/devrites-lib/reference/standards/gates.md +3 -0
  162. package/pack/generated/omp/skills/devrites-lib/reference/standards/testing.md +7 -0
  163. package/pack/generated/omp/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  164. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  165. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -2
  166. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +1 -1
  167. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  168. package/pack/generated/omp/skills/rite-build/SKILL.md +7 -7
  169. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +2 -2
  170. package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
  171. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
  172. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +28 -21
  173. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +10 -1
  174. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -2
  175. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +9 -2
  176. package/pack/generated/omp/skills/rite-define/SKILL.md +11 -2
  177. package/pack/generated/omp/skills/rite-handoff/SKILL.md +1 -1
  178. package/pack/generated/omp/skills/rite-plan/SKILL.md +20 -9
  179. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +5 -0
  180. package/pack/generated/omp/skills/rite-polish/SKILL.md +6 -4
  181. package/pack/generated/omp/skills/rite-prove/SKILL.md +3 -2
  182. package/pack/generated/omp/skills/rite-review/SKILL.md +6 -1
  183. package/pack/generated/omp/skills/rite-seal/SKILL.md +2 -2
  184. package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +7 -1
  185. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -5
  186. package/pack/generated/omp/skills/rite-vet/reference/artifacts.md +8 -0
  187. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +1 -1
  188. package/pack/generated/pi/agents/devrites-plan-drafter.md +13 -3
  189. package/pack/generated/pi/agents/devrites-slice-wright.md +5 -1
  190. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +14 -5
  191. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +24 -9
  192. package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +6 -2
  193. package/pack/generated/pi/skills/devrites-lib/reference/standards/core.md +3 -0
  194. package/pack/generated/pi/skills/devrites-lib/reference/standards/gates.md +3 -0
  195. package/pack/generated/pi/skills/devrites-lib/reference/standards/testing.md +7 -0
  196. package/pack/generated/pi/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  197. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  198. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +2 -2
  199. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +1 -1
  200. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +6 -2
  201. package/pack/generated/pi/skills/rite-build/SKILL.md +7 -7
  202. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +2 -2
  203. package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
  204. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
  205. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +28 -21
  206. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +10 -1
  207. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -2
  208. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +9 -2
  209. package/pack/generated/pi/skills/rite-define/SKILL.md +11 -2
  210. package/pack/generated/pi/skills/rite-handoff/SKILL.md +1 -1
  211. package/pack/generated/pi/skills/rite-plan/SKILL.md +20 -9
  212. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +5 -0
  213. package/pack/generated/pi/skills/rite-polish/SKILL.md +6 -4
  214. package/pack/generated/pi/skills/rite-prove/SKILL.md +3 -2
  215. package/pack/generated/pi/skills/rite-review/SKILL.md +6 -1
  216. package/pack/generated/pi/skills/rite-seal/SKILL.md +2 -2
  217. package/pack/generated/pi/skills/rite-seal/reference/phase-contract.md +7 -1
  218. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -5
  219. package/pack/generated/pi/skills/rite-vet/reference/artifacts.md +8 -0
  220. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +1 -1
  221. package/package.json +1 -1
@@ -0,0 +1,474 @@
1
+ package lib
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "io/fs"
11
+ "os"
12
+ "path/filepath"
13
+ "regexp"
14
+ "slices"
15
+ "sort"
16
+ "strings"
17
+
18
+ "github.com/devrites/devrites/internal/devritespaths"
19
+ "github.com/devrites/devrites/internal/fsutil"
20
+ "github.com/devrites/devrites/internal/state"
21
+ )
22
+
23
+ // maxBundleBytes bounds one drafter bundle read. A bundle carries hunks for a
24
+ // handful of planning artifacts, each capped at state.MaxArtifactBytes.
25
+ const maxBundleBytes = 8 << 20
26
+
27
+ // trackedIDRE matches the workspace ID families whose silent disappearance is
28
+ // a planning defect: an AC, slice, or horizon item never vanishes unrecorded.
29
+ var trackedIDRE = regexp.MustCompile(`\b(?:AC|REQ|HZN|SLICE|PF|INV|DEC|PROH|EDGE|DRIFT|ASM|Q|EVID)-\d+(?:-[A-Z][A-Z0-9]*)*\b`)
30
+
31
+ // datedHeadingRE finds a dated section heading. In the current-state planning
32
+ // artifacts it marks an appended repair delta that leaves the superseded clause
33
+ // beside its replacement, growing the file and the next review's surface.
34
+ var datedHeadingRE = regexp.MustCompile(`(?m)^#{2,4}\s.*\b\d{4}-\d{2}-\d{2}\b`)
35
+
36
+ var currentStatePlanning = map[string]bool{
37
+ "architecture.md": true, "plan.md": true, "tasks.md": true, "test-plan.md": true, "traceability.md": true,
38
+ }
39
+
40
+ // candidateBundle is the plan-drafter's candidate_files result. Unknown fields
41
+ // (acceptance_map, technical_decisions, ...) are the root's to reconcile and
42
+ // are ignored here.
43
+ type candidateBundle struct {
44
+ BaseHashes map[string]string `json:"base_hashes"`
45
+ CandidateFiles []candidateFile `json:"candidate_files"`
46
+ RetiredIDs []string `json:"retired_ids"`
47
+ }
48
+
49
+ type candidateFile struct {
50
+ Path string `json:"path"`
51
+ Operation string `json:"operation"`
52
+ Content *string `json:"content"`
53
+ Hunks []candidateHunk `json:"hunks"`
54
+ }
55
+
56
+ type candidateHunk struct {
57
+ ID string `json:"id"`
58
+ OldText string `json:"old_text"`
59
+ NewText string `json:"new_text"`
60
+ }
61
+
62
+ // bundleTarget is every requested change to one workspace file, merged across
63
+ // bundles so anchor-disjoint drafter clusters apply against one shared base.
64
+ type bundleTarget struct {
65
+ rel string
66
+ op string // create | replace | patch
67
+ content string
68
+ hunks []candidateHunk
69
+ pins []string // base hashes named by each bundle that touches rel
70
+ before []byte
71
+ existed bool
72
+ after []byte
73
+ hunkSeen int
74
+ }
75
+
76
+ // RunApplyBundle implements `devrites-engine state apply-bundle <slug>
77
+ // <bundle.json>... [--dry-run]`. It replaces the root's hand fold of drafter
78
+ // hunks: every bundle is validated against the current bytes (base hash pin,
79
+ // unique non-overlapping old_text, no lost tracked ID, no new task-graph
80
+ // problem, no new material budget overshoot) and then all files are written,
81
+ // or none are. Several bundles combine when their hunks are disjoint.
82
+ //
83
+ // Exit codes:
84
+ //
85
+ // 0 applied (or dry-run clean)
86
+ // 2 usage error
87
+ // 3 blocked: a bundle failed validation; nothing was written
88
+ // 1 internal/IO error
89
+ func RunApplyBundle(root string, args []string, stdout, stderr io.Writer) int {
90
+ dryRun := false
91
+ var positional []string
92
+ for _, arg := range args {
93
+ if arg == "--dry-run" {
94
+ dryRun = true
95
+ continue
96
+ }
97
+ if strings.HasPrefix(arg, "-") {
98
+ fmt.Fprintln(stderr, "usage: devrites-engine state apply-bundle <slug> <bundle.json>... [--dry-run]")
99
+ return 2
100
+ }
101
+ positional = append(positional, arg)
102
+ }
103
+ if len(positional) < 2 {
104
+ fmt.Fprintln(stderr, "usage: devrites-engine state apply-bundle <slug> <bundle.json>... [--dry-run]")
105
+ return 2
106
+ }
107
+ slug := positional[0]
108
+ workspace, err := devritespaths.ExistingFeatureDirChecked(root, slug)
109
+ if err != nil {
110
+ fmt.Fprintf(stderr, "apply-bundle: BLOCKED: workspace for %s: %v\n", slug, err)
111
+ return 3
112
+ }
113
+ if err := state.RequireWorkspaceSchema(root, slug); err != nil {
114
+ fmt.Fprintf(stderr, "apply-bundle: BLOCKED: %v\n", err)
115
+ return 3
116
+ }
117
+ bundles := make([]candidateBundle, 0, len(positional)-1)
118
+ for _, name := range positional[1:] {
119
+ bundle, err := readCandidateBundle(name)
120
+ if err != nil {
121
+ fmt.Fprintf(stderr, "apply-bundle: BLOCKED: bundle %s: %v\n", name, err)
122
+ return 3
123
+ }
124
+ bundles = append(bundles, bundle)
125
+ }
126
+
127
+ code := 0
128
+ if err := state.WithFeatureLock(root, slug, func() error {
129
+ code = applyBundlesLocked(slug, workspace, bundles, dryRun, stdout, stderr)
130
+ return nil
131
+ }); err != nil {
132
+ fmt.Fprintf(stderr, "apply-bundle: %v\n", err)
133
+ return 1
134
+ }
135
+ return code
136
+ }
137
+
138
+ func readCandidateBundle(name string) (candidateBundle, error) {
139
+ var bundle candidateBundle
140
+ raw, err := readBoundedRegularFile(name, maxBundleBytes)
141
+ if err != nil {
142
+ return bundle, err
143
+ }
144
+ if err := json.Unmarshal(raw, &bundle); err != nil {
145
+ return bundle, fmt.Errorf("not a JSON candidate bundle: %w", err)
146
+ }
147
+ if len(bundle.CandidateFiles) == 0 {
148
+ return bundle, errors.New("candidate_files is empty")
149
+ }
150
+ return bundle, nil
151
+ }
152
+
153
+ func applyBundlesLocked(slug, workspace string, bundles []candidateBundle, dryRun bool, stdout, stderr io.Writer) int {
154
+ ws, err := os.OpenRoot(workspace)
155
+ if err != nil {
156
+ fmt.Fprintf(stderr, "apply-bundle: %v\n", err)
157
+ return 1
158
+ }
159
+ defer func() { _ = ws.Close() }()
160
+
161
+ targets, retired, problems := collectBundleTargets(slug, bundles)
162
+ if len(problems) == 0 {
163
+ problems = materializeTargets(ws, targets)
164
+ }
165
+ if len(problems) == 0 {
166
+ problems = checkBundleResult(targets, retired)
167
+ }
168
+ if len(problems) > 0 {
169
+ for _, problem := range problems {
170
+ fmt.Fprintf(stderr, "apply-bundle: BLOCKED: %s\n", problem)
171
+ }
172
+ fmt.Fprintln(stderr, "apply-bundle: nothing written")
173
+ return 3
174
+ }
175
+
176
+ hunks := 0
177
+ for _, target := range targets {
178
+ hunks += len(target.hunks)
179
+ lines := state.CountLines(target.after)
180
+ budget := ""
181
+ if limit, ok := state.ArtifactLineBudget(filepath.Base(target.rel)); ok {
182
+ budget = fmt.Sprintf("/%d", limit)
183
+ }
184
+ fmt.Fprintf(stdout, "apply-bundle: %s %s %d hunk(s) sha256 %s -> %s lines %d%s\n",
185
+ target.rel, target.op, len(target.hunks), shortHash(target.before, target.existed), shortHash(target.after, true), lines, budget)
186
+ }
187
+ if dryRun {
188
+ fmt.Fprintf(stdout, "apply-bundle: dry-run clean: %d file(s), %d hunk(s); nothing written\n", len(targets), hunks)
189
+ return 0
190
+ }
191
+ if err := writeBundleTargets(ws, targets); err != nil {
192
+ fmt.Fprintf(stderr, "apply-bundle: %v\n", err)
193
+ return 1
194
+ }
195
+ fmt.Fprintf(stdout, "apply-bundle: applied %d file(s), %d hunk(s)\n", len(targets), hunks)
196
+ return 0
197
+ }
198
+
199
+ // collectBundleTargets merges every bundle's candidate files by workspace path.
200
+ // One path takes either one create/replace or any number of patch entries.
201
+ func collectBundleTargets(slug string, bundles []candidateBundle) ([]*bundleTarget, map[string]bool, []string) {
202
+ var problems []string
203
+ byRel := map[string]*bundleTarget{}
204
+ retired := map[string]bool{}
205
+ prefix := ".devrites/work/" + slug + "/"
206
+ for bi, bundle := range bundles {
207
+ for _, id := range bundle.RetiredIDs {
208
+ retired[strings.TrimSpace(id)] = true
209
+ }
210
+ pins := map[string]string{}
211
+ for path, hash := range bundle.BaseHashes {
212
+ pins[strings.TrimPrefix(filepath.ToSlash(path), prefix)] = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(hash), "sha256:"))
213
+ }
214
+ for _, file := range bundle.CandidateFiles {
215
+ rel := strings.TrimPrefix(filepath.ToSlash(file.Path), prefix)
216
+ where := fmt.Sprintf("bundle %d %s", bi+1, file.Path)
217
+ if !filepath.IsLocal(filepath.FromSlash(rel)) || !strings.HasSuffix(rel, ".md") {
218
+ problems = append(problems, where+": path must be a Markdown file inside the "+slug+" workspace")
219
+ continue
220
+ }
221
+ op := strings.ToLower(strings.TrimSpace(file.Operation))
222
+ switch op {
223
+ case "create", "replace":
224
+ if file.Content == nil {
225
+ problems = append(problems, where+": "+op+" needs content")
226
+ continue
227
+ }
228
+ case "patch":
229
+ if len(file.Hunks) == 0 {
230
+ problems = append(problems, where+": patch needs hunks")
231
+ continue
232
+ }
233
+ default:
234
+ problems = append(problems, fmt.Sprintf("%s: unknown operation %q", where, file.Operation))
235
+ continue
236
+ }
237
+ target, seen := byRel[rel]
238
+ if !seen {
239
+ target = &bundleTarget{rel: rel, op: op}
240
+ byRel[rel] = target
241
+ } else if op != "patch" || target.op != "patch" {
242
+ problems = append(problems, where+": conflicts with another candidate for the same file (only patch hunks combine)")
243
+ continue
244
+ }
245
+ if op != "create" {
246
+ pin, ok := pins[rel]
247
+ if !ok {
248
+ problems = append(problems, where+": base_hashes has no pin for this file")
249
+ continue
250
+ }
251
+ target.pins = append(target.pins, pin)
252
+ }
253
+ if file.Content != nil {
254
+ target.content = *file.Content
255
+ }
256
+ target.hunks = append(target.hunks, file.Hunks...)
257
+ }
258
+ }
259
+ targets := make([]*bundleTarget, 0, len(byRel))
260
+ for _, target := range byRel {
261
+ targets = append(targets, target)
262
+ }
263
+ sort.Slice(targets, func(i, j int) bool { return targets[i].rel < targets[j].rel })
264
+ return targets, retired, problems
265
+ }
266
+
267
+ // materializeTargets reads each file's current bytes, verifies its pins, and
268
+ // computes the after bytes.
269
+ func materializeTargets(ws *os.Root, targets []*bundleTarget) []string {
270
+ var problems []string
271
+ for _, target := range targets {
272
+ before, existed, err := readBundleFile(ws, target.rel)
273
+ if err != nil {
274
+ problems = append(problems, fmt.Sprintf("%s: %v", target.rel, err))
275
+ continue
276
+ }
277
+ target.before, target.existed = before, existed
278
+ if target.op == "create" {
279
+ if existed {
280
+ problems = append(problems, target.rel+": create target already exists")
281
+ continue
282
+ }
283
+ target.after = []byte(target.content)
284
+ continue
285
+ }
286
+ if !existed {
287
+ problems = append(problems, target.rel+": "+target.op+" target does not exist")
288
+ continue
289
+ }
290
+ current := sha256Hex(before)
291
+ for _, pin := range target.pins {
292
+ if pin != current {
293
+ problems = append(problems, fmt.Sprintf("%s: stale base: pinned %s, current %s", target.rel, abbrev(pin), abbrev(current)))
294
+ break
295
+ }
296
+ }
297
+ if target.op == "replace" {
298
+ target.after = []byte(target.content)
299
+ continue
300
+ }
301
+ after, hunkProblems := applyHunks(string(before), target.hunks)
302
+ for _, problem := range hunkProblems {
303
+ problems = append(problems, target.rel+": "+problem)
304
+ }
305
+ target.after = []byte(after)
306
+ }
307
+ return problems
308
+ }
309
+
310
+ // applyHunks replaces each hunk's old_text, which must occur exactly once in
311
+ // base and must not overlap another hunk's span.
312
+ func applyHunks(base string, hunks []candidateHunk) (string, []string) {
313
+ type span struct {
314
+ start, end int
315
+ hunk candidateHunk
316
+ }
317
+ var problems []string
318
+ spans := make([]span, 0, len(hunks))
319
+ for i, hunk := range hunks {
320
+ name := hunk.ID
321
+ if name == "" {
322
+ name = fmt.Sprintf("#%d", i+1)
323
+ }
324
+ hunk.ID = name
325
+ if hunk.OldText == "" {
326
+ problems = append(problems, "hunk "+name+": empty old_text")
327
+ continue
328
+ }
329
+ start := strings.Index(base, hunk.OldText)
330
+ if start < 0 {
331
+ problems = append(problems, "hunk "+name+": old_text not found (stale anchor)")
332
+ continue
333
+ }
334
+ // Search from start+1 so an overlapping second occurrence also counts.
335
+ if strings.Contains(base[start+1:], hunk.OldText) {
336
+ problems = append(problems, "hunk "+name+": old_text occurs more than once (non-unique anchor)")
337
+ continue
338
+ }
339
+ spans = append(spans, span{start, start + len(hunk.OldText), hunk})
340
+ }
341
+ sort.Slice(spans, func(i, j int) bool { return spans[i].start < spans[j].start })
342
+ for i := 1; i < len(spans); i++ {
343
+ if spans[i].start < spans[i-1].end {
344
+ problems = append(problems, fmt.Sprintf("hunks %s and %s overlap", spans[i-1].hunk.ID, spans[i].hunk.ID))
345
+ }
346
+ }
347
+ if len(problems) > 0 {
348
+ return base, problems
349
+ }
350
+ var out strings.Builder
351
+ last := 0
352
+ for _, s := range spans {
353
+ out.WriteString(base[last:s.start])
354
+ out.WriteString(s.hunk.NewText)
355
+ last = s.end
356
+ }
357
+ out.WriteString(base[last:])
358
+ return out.String(), nil
359
+ }
360
+
361
+ // checkBundleResult rejects an outcome the root would otherwise discover only
362
+ // at Vet: a tracked ID that vanished from every touched file without being
363
+ // listed in retired_ids, a new tasks.md graph problem, or a budgeted artifact
364
+ // that grew into material overshoot without a `Budget override:` line.
365
+ func checkBundleResult(targets []*bundleTarget, retired map[string]bool) []string {
366
+ var problems []string
367
+ beforeIDs, afterIDs := map[string]bool{}, map[string]bool{}
368
+ for _, target := range targets {
369
+ for _, id := range trackedIDRE.FindAllString(string(target.before), -1) {
370
+ beforeIDs[id] = true
371
+ }
372
+ for _, id := range trackedIDRE.FindAllString(string(target.after), -1) {
373
+ afterIDs[id] = true
374
+ }
375
+ }
376
+ var lost []string
377
+ for id := range beforeIDs {
378
+ if !afterIDs[id] && !retired[id] {
379
+ lost = append(lost, id)
380
+ }
381
+ }
382
+ if len(lost) > 0 {
383
+ slices.Sort(lost)
384
+ problems = append(problems, "IDs vanished from every touched file (list them in retired_ids if intended): "+strings.Join(lost, ", "))
385
+ }
386
+
387
+ for _, target := range targets {
388
+ name := filepath.Base(target.rel)
389
+ if name == "tasks.md" && filepath.Dir(target.rel) == "." {
390
+ old := map[string]bool{}
391
+ if target.existed {
392
+ for _, p := range state.ParseTaskGraph(target.before).Problems {
393
+ old[p] = true
394
+ }
395
+ }
396
+ for _, p := range state.ParseTaskGraph(target.after).Problems {
397
+ if !old[p] {
398
+ problems = append(problems, "tasks.md: "+p)
399
+ }
400
+ }
401
+ }
402
+ if filepath.Dir(target.rel) == "." && currentStatePlanning[name] &&
403
+ len(datedHeadingRE.FindAllIndex(target.after, -1)) > len(datedHeadingRE.FindAllIndex(target.before, -1)) {
404
+ problems = append(problems, target.rel+": adds a dated delta section; rewrite the owning clause in place (rationale to decisions.md, closed narrative to history/)")
405
+ }
406
+ if filepath.Dir(target.rel) != "." || !state.ArtifactBudgetGateApplies(name) {
407
+ continue
408
+ }
409
+ after, _ := state.ArtifactBudget(name, target.after)
410
+ before, _ := state.ArtifactBudget(name, target.before)
411
+ grew := !target.existed || after.Lines > before.Lines || after.Bytes > before.Bytes
412
+ if after.Material && !after.Override && grew {
413
+ problems = append(problems, fmt.Sprintf("%s: %d lines / %d bytes is a material overshoot of its %d-line budget; split or relocate instead of growing it",
414
+ target.rel, after.Lines, after.Bytes, after.LineBudget))
415
+ }
416
+ }
417
+ return problems
418
+ }
419
+
420
+ // writeBundleTargets writes every target atomically and, if one write fails,
421
+ // restores the files already written so the workspace never holds half a
422
+ // bundle.
423
+ func writeBundleTargets(ws *os.Root, targets []*bundleTarget) error {
424
+ for i, target := range targets {
425
+ if err := fsutil.WriteFileAtomicIn(ws, filepath.FromSlash(target.rel), target.after, 0o644); err != nil {
426
+ for _, done := range targets[:i] {
427
+ if done.existed {
428
+ _ = fsutil.WriteFileAtomicIn(ws, filepath.FromSlash(done.rel), done.before, 0o644)
429
+ } else {
430
+ _ = ws.Remove(filepath.FromSlash(done.rel))
431
+ }
432
+ }
433
+ return fmt.Errorf("write %s: %w (earlier files restored)", target.rel, err)
434
+ }
435
+ }
436
+ return nil
437
+ }
438
+
439
+ func readBundleFile(ws *os.Root, rel string) ([]byte, bool, error) {
440
+ info, err := ws.Lstat(filepath.FromSlash(rel))
441
+ if errors.Is(err, fs.ErrNotExist) {
442
+ return nil, false, nil
443
+ }
444
+ if err != nil {
445
+ return nil, false, err
446
+ }
447
+ if !info.Mode().IsRegular() {
448
+ return nil, false, errors.New("not a regular file")
449
+ }
450
+ if info.Size() > state.MaxArtifactBytes {
451
+ return nil, false, fmt.Errorf("exceeds the %d-byte artifact cap", state.MaxArtifactBytes)
452
+ }
453
+ raw, err := ws.ReadFile(filepath.FromSlash(rel))
454
+ return raw, true, err
455
+ }
456
+
457
+ func sha256Hex(raw []byte) string {
458
+ sum := sha256.Sum256(raw)
459
+ return hex.EncodeToString(sum[:])
460
+ }
461
+
462
+ func shortHash(raw []byte, exists bool) string {
463
+ if !exists {
464
+ return "(new)"
465
+ }
466
+ return abbrev(sha256Hex(raw))
467
+ }
468
+
469
+ func abbrev(hash string) string {
470
+ if len(hash) > 12 {
471
+ return hash[:12]
472
+ }
473
+ return hash
474
+ }
@@ -51,6 +51,10 @@ type claimRecord struct {
51
51
  ClaimedAt string `json:"claimed_at"`
52
52
  ExpiresAt string `json:"expires_at"`
53
53
  Released bool `json:"released,omitempty"`
54
+ // Host and Anchors identify the claiming session's long-lived processes,
55
+ // so a later session can prove that holder exited (see holderProbe).
56
+ Host string `json:"host,omitempty"`
57
+ Anchors []claimProc `json:"anchors,omitempty"`
54
58
  }
55
59
 
56
60
  // RunClaim manages the advisory file-claim ledger.
@@ -299,7 +303,8 @@ func claimAddLocked(path, session, reason string, ttl int, paths []string, stdou
299
303
  return 2
300
304
  }
301
305
  now := time.Now().UTC()
302
- conflicts := claimConflicts(liveClaims(latest, order, now), session, paths)
306
+ probe := newHolderProbe()
307
+ conflicts := claimConflicts(probe.withoutGone(liveClaims(latest, order, now)), session, paths)
303
308
  if len(conflicts) > 0 {
304
309
  printConflicts(stderr, conflicts)
305
310
  return 3
@@ -311,6 +316,8 @@ func claimAddLocked(path, session, reason string, ttl int, paths []string, stdou
311
316
  Reason: reason,
312
317
  ClaimedAt: now.Format(time.RFC3339),
313
318
  ExpiresAt: now.Add(time.Duration(ttl) * time.Minute).Format(time.RFC3339),
319
+ Host: probe.host,
320
+ Anchors: probe.anchors(now),
314
321
  }
315
322
  if err := appendClaim(path, rec); err != nil {
316
323
  fmt.Fprintf(stderr, "claim: %v\n", err)
@@ -389,6 +396,7 @@ func claimList(root string, args []string, stdout, stderr io.Writer) int {
389
396
  return 2
390
397
  }
391
398
  now := time.Now().UTC()
399
+ probe := newHolderProbe()
392
400
  rows := liveClaims(latest, order, now)
393
401
  if all {
394
402
  rows = rows[:0]
@@ -404,6 +412,8 @@ func claimList(root string, args []string, stdout, stderr io.Writer) int {
404
412
  state := "live"
405
413
  if rec.Released {
406
414
  state = "released"
415
+ } else if probe.gone(rec) {
416
+ state = "dead"
407
417
  } else if exp, err := time.Parse(time.RFC3339, rec.ExpiresAt); err != nil || !exp.After(now) {
408
418
  state = "expired"
409
419
  }
@@ -453,7 +463,7 @@ func claimCheck(root string, args []string, stdout, stderr io.Writer) int {
453
463
  fmt.Fprintf(stderr, unparsedLedgerMsg, bad)
454
464
  return 2
455
465
  }
456
- conflicts := claimConflicts(liveClaims(latest, order, time.Now().UTC()), session, paths)
466
+ conflicts := claimConflicts(newHolderProbe().withoutGone(liveClaims(latest, order, time.Now().UTC())), session, paths)
457
467
  if len(conflicts) > 0 {
458
468
  printConflicts(stdout, conflicts)
459
469
  return 3
@@ -0,0 +1,124 @@
1
+ package lib
2
+
3
+ import (
4
+ "os"
5
+ "os/exec"
6
+ "slices"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+ )
11
+
12
+ // claimAnchorAge separates a claiming session's long-lived host processes from
13
+ // the per-command shells and launchers that start moments before `claim add`.
14
+ const claimAnchorAge = time.Minute
15
+
16
+ // claimProc names one process by pid and start time, so a reused pid cannot
17
+ // impersonate it.
18
+ type claimProc struct {
19
+ PID int `json:"pid"`
20
+ Start string `json:"start"`
21
+ }
22
+
23
+ type procInfo struct {
24
+ ppid int
25
+ start string
26
+ }
27
+
28
+ // processTable lists running processes keyed by pid, or nil when the host
29
+ // cannot say. Tests replace it.
30
+ var processTable = psProcessTable
31
+
32
+ func psProcessTable() map[int]procInfo {
33
+ cmd := exec.Command("ps", "-A", "-o", "pid=,ppid=,lstart=")
34
+ cmd.Env = append(os.Environ(), "LC_ALL=C")
35
+ out, err := cmd.Output()
36
+ if err != nil {
37
+ return nil
38
+ }
39
+ table := map[int]procInfo{}
40
+ for _, line := range strings.Split(string(out), "\n") {
41
+ f := strings.Fields(line)
42
+ if len(f) != 7 {
43
+ continue
44
+ }
45
+ pid, perr := strconv.Atoi(f[0])
46
+ ppid, pperr := strconv.Atoi(f[1])
47
+ start, serr := time.ParseInLocation("Mon Jan 2 15:04:05 2006", strings.Join(f[2:], " "), time.Local)
48
+ if perr != nil || pperr != nil || serr != nil {
49
+ continue
50
+ }
51
+ table[pid] = procInfo{ppid: ppid, start: start.UTC().Format(time.RFC3339)}
52
+ }
53
+ if len(table) == 0 {
54
+ return nil
55
+ }
56
+ return table
57
+ }
58
+
59
+ // lineage walks from pid up to, but not including, pid 1.
60
+ func lineage(table map[int]procInfo, pid int) []claimProc {
61
+ var out []claimProc
62
+ seen := map[int]bool{}
63
+ for pid > 1 && !seen[pid] {
64
+ seen[pid] = true
65
+ p, ok := table[pid]
66
+ if !ok {
67
+ break
68
+ }
69
+ out = append(out, claimProc{PID: pid, Start: p.start})
70
+ pid = p.ppid
71
+ }
72
+ return out
73
+ }
74
+
75
+ // holderProbe answers whether a claim's holding session has exited. It only
76
+ // ever proves exit on this host; anything it cannot see counts as alive.
77
+ type holderProbe struct {
78
+ table map[int]procInfo
79
+ host string
80
+ self []claimProc
81
+ }
82
+
83
+ func newHolderProbe() holderProbe {
84
+ table := processTable()
85
+ host, _ := os.Hostname()
86
+ return holderProbe{table: table, host: host, self: lineage(table, os.Getppid())}
87
+ }
88
+
89
+ // anchors returns the caller's ancestors that started more than
90
+ // claimAnchorAge before now: the session's host processes.
91
+ func (p holderProbe) anchors(now time.Time) []claimProc {
92
+ var out []claimProc
93
+ for _, proc := range p.self {
94
+ start, err := time.Parse(time.RFC3339, proc.Start)
95
+ if err == nil && now.Sub(start) > claimAnchorAge {
96
+ out = append(out, proc)
97
+ }
98
+ }
99
+ return out
100
+ }
101
+
102
+ // gone reports whether rec's holder is proven exited: the claim was taken on
103
+ // this host and every anchor it does not share with the caller has exited.
104
+ func (p holderProbe) gone(rec *claimRecord) bool {
105
+ if p.table == nil || rec.Host == "" || rec.Host != p.host {
106
+ return false
107
+ }
108
+ unique := 0
109
+ for _, a := range rec.Anchors {
110
+ if slices.Contains(p.self, a) {
111
+ continue
112
+ }
113
+ unique++
114
+ if proc, ok := p.table[a.PID]; ok && proc.start == a.Start {
115
+ return false
116
+ }
117
+ }
118
+ return unique > 0
119
+ }
120
+
121
+ // withoutGone drops claims whose holder session has exited.
122
+ func (p holderProbe) withoutGone(claims []*claimRecord) []*claimRecord {
123
+ return slices.DeleteFunc(claims, p.gone)
124
+ }
@@ -5,6 +5,7 @@ import (
5
5
  "io"
6
6
 
7
7
  "github.com/devrites/devrites/internal/devritespaths"
8
+ "github.com/devrites/devrites/internal/parallel"
8
9
  "github.com/devrites/devrites/internal/state"
9
10
  )
10
11
 
@@ -36,6 +37,19 @@ func RunTaskGraphCheck(root, slug string, stdout, stderr io.Writer) int {
36
37
  if over > 0 {
37
38
  recordMetric(root, slug, "define", "size-advisory", "", int64(over))
38
39
  }
40
+ paths, serial := sliceWritePaths(raw, graph)
41
+ if rounds := scheduleRounds(graph, paths, root); rounds > 0 {
42
+ fmt.Fprintf(stdout, "task-graph: schedule: %d slices in %d build rounds at cap %d\n", len(graph.Slices), rounds, parallel.MaxParallelSlices)
43
+ }
44
+ if hubs := hubAdvisories(graph, paths, serial); len(hubs) > 0 {
45
+ for _, line := range hubs[:min(len(hubs), hubAdvisoryShown)] {
46
+ fmt.Fprintf(stdout, "task-graph: %s\n", line)
47
+ }
48
+ if extra := len(hubs) - hubAdvisoryShown; extra > 0 {
49
+ fmt.Fprintf(stdout, "task-graph: ADVISORY hub: %d more shared paths; fix the ones above first\n", extra)
50
+ }
51
+ recordMetric(root, slug, "define", "hub-advisory", "", int64(len(hubs)))
52
+ }
39
53
  }
40
54
  if edges := edgeAdvisories(graph); len(edges) > 0 {
41
55
  for _, line := range edges {
@@ -177,7 +177,7 @@ func (o diffScopeOpts) changedPaths() ([]string, error) {
177
177
 
178
178
  // engineMachineFiles are the feature-root files the engine itself writes:
179
179
  // the dispatch ledger, metrics ledger, drift and regression baselines, the
180
- // parallel lease, and the feature lock.
180
+ // parallel lease, the feature lock, and the gate-execution lock.
181
181
  var engineMachineFiles = map[string]bool{
182
182
  "dispatch.json": true,
183
183
  MetricsFile: true,
@@ -185,6 +185,7 @@ var engineMachineFiles = map[string]bool{
185
185
  regressionBaselineFile: true,
186
186
  "parallel-lease.md": true,
187
187
  ".lock": true,
188
+ "gates.md.lock": true,
188
189
  }
189
190
 
190
191
  // engineMachineFile reports whether rel, relative to the .devrites tree, is