devrites 7.3.1 → 7.3.2

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 (111) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +19 -3
  3. package/bin/devrites.mjs +31 -5
  4. package/docs/engine/commands.md +35 -3
  5. package/docs/release.md +5 -2
  6. package/engine/Makefile +2 -2
  7. package/engine/internal/acceptance/newfinal_origins.go +39 -0
  8. package/engine/internal/acceptance/preconditions.go +17 -11
  9. package/engine/internal/acceptance/transcript.go +23 -4
  10. package/engine/internal/gate/gate.go +8 -6
  11. package/engine/internal/gate/readiness_binding.go +50 -21
  12. package/engine/internal/install/apply.go +36 -21
  13. package/engine/internal/install/binary.go +67 -9
  14. package/engine/internal/install/install.go +63 -21
  15. package/engine/internal/install/manifest.go +4 -5
  16. package/engine/internal/install/merge.go +89 -33
  17. package/engine/internal/install/preflight.go +60 -19
  18. package/engine/internal/install/uninstall.go +19 -9
  19. package/engine/internal/install/update.go +0 -4
  20. package/engine/internal/lib/claims.go +170 -4
  21. package/engine/internal/lib/claims_holder.go +13 -16
  22. package/engine/internal/lib/closeout.go +52 -28
  23. package/engine/internal/lib/context_io.go +88 -4
  24. package/engine/internal/lib/continuation.go +111 -24
  25. package/engine/internal/lib/dispatch.go +157 -24
  26. package/engine/internal/lib/final_prerequisites.go +137 -0
  27. package/engine/internal/lib/manifest_union.go +62 -25
  28. package/engine/internal/lib/next.go +10 -10
  29. package/engine/internal/lib/packet.go +32 -8
  30. package/engine/internal/lib/packet_integrity.go +194 -2
  31. package/engine/internal/lib/resolve.go +128 -142
  32. package/engine/internal/lib/treebaseline.go +0 -1
  33. package/engine/internal/markdowntext/markdowntext.go +110 -31
  34. package/engine/internal/overhaul/records/check.go +45 -5
  35. package/engine/internal/overhaul/records/consistency.go +34 -8
  36. package/engine/internal/overhaul/records/run.go +55 -11
  37. package/engine/internal/parallel/landing.go +17 -0
  38. package/engine/internal/state/acceptance.go +35 -5
  39. package/engine/internal/state/feature.go +3 -2
  40. package/engine/internal/state/observation.go +1 -1
  41. package/engine/internal/state/write.go +20 -44
  42. package/engine/internal/workflow/commands.go +25 -3
  43. package/engine/main.go +19 -0
  44. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  45. package/pack/.claude/skills/overhaul/SKILL.md +8 -4
  46. package/pack/.claude/skills/overhaul/references/lifecycle.md +13 -6
  47. package/pack/.claude/skills/overhaul/references/orchestration.md +32 -21
  48. package/pack/.claude/skills/overhaul/references/records.md +23 -8
  49. package/pack/.claude/skills/overhaul/references/scope-and-safety.md +7 -2
  50. package/pack/.claude/skills/overhaul/references/scoring.md +7 -0
  51. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +17 -17
  52. package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
  53. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  54. package/pack/generated/claude/skills/overhaul/SKILL.md +8 -4
  55. package/pack/generated/claude/skills/overhaul/references/lifecycle.md +13 -6
  56. package/pack/generated/claude/skills/overhaul/references/orchestration.md +32 -21
  57. package/pack/generated/claude/skills/overhaul/references/records.md +23 -8
  58. package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +7 -2
  59. package/pack/generated/claude/skills/overhaul/references/scoring.md +7 -0
  60. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +17 -17
  61. package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
  62. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  63. package/pack/generated/codex/skills/overhaul/SKILL.md +8 -4
  64. package/pack/generated/codex/skills/overhaul/references/lifecycle.md +13 -6
  65. package/pack/generated/codex/skills/overhaul/references/orchestration.md +32 -21
  66. package/pack/generated/codex/skills/overhaul/references/records.md +23 -8
  67. package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +7 -2
  68. package/pack/generated/codex/skills/overhaul/references/scoring.md +7 -0
  69. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +17 -17
  70. package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
  71. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  72. package/pack/generated/devin/skills/overhaul/SKILL.md +8 -4
  73. package/pack/generated/devin/skills/overhaul/references/lifecycle.md +13 -6
  74. package/pack/generated/devin/skills/overhaul/references/orchestration.md +32 -21
  75. package/pack/generated/devin/skills/overhaul/references/records.md +23 -8
  76. package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +7 -2
  77. package/pack/generated/devin/skills/overhaul/references/scoring.md +7 -0
  78. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +17 -17
  79. package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
  80. package/pack/generated/dsh/runtime/cordis.patch.yml +28 -14
  81. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  82. package/pack/generated/dsh/skills/overhaul/SKILL.md +8 -4
  83. package/pack/generated/dsh/skills/overhaul/references/lifecycle.md +13 -6
  84. package/pack/generated/dsh/skills/overhaul/references/orchestration.md +32 -21
  85. package/pack/generated/dsh/skills/overhaul/references/records.md +23 -8
  86. package/pack/generated/dsh/skills/overhaul/references/scope-and-safety.md +7 -2
  87. package/pack/generated/dsh/skills/overhaul/references/scoring.md +6 -0
  88. package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +17 -17
  89. package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
  90. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  91. package/pack/generated/omp/skills/overhaul/SKILL.md +8 -4
  92. package/pack/generated/omp/skills/overhaul/references/lifecycle.md +13 -6
  93. package/pack/generated/omp/skills/overhaul/references/orchestration.md +32 -21
  94. package/pack/generated/omp/skills/overhaul/references/records.md +23 -8
  95. package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +7 -2
  96. package/pack/generated/omp/skills/overhaul/references/scoring.md +7 -0
  97. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +17 -17
  98. package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
  99. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  100. package/pack/generated/pi/skills/overhaul/SKILL.md +8 -4
  101. package/pack/generated/pi/skills/overhaul/references/lifecycle.md +13 -6
  102. package/pack/generated/pi/skills/overhaul/references/orchestration.md +32 -21
  103. package/pack/generated/pi/skills/overhaul/references/records.md +23 -8
  104. package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +7 -2
  105. package/pack/generated/pi/skills/overhaul/references/scoring.md +7 -0
  106. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +17 -17
  107. package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
  108. package/package.json +1 -1
  109. package/scripts/dsh-generate.py +1 -1
  110. package/scripts/install-lib.sh +29 -0
  111. package/uninstall.sh +1 -0
@@ -115,6 +115,24 @@ func consistency(run, genDir string, R map[string]map[string]any, add func(strin
115
115
  if err != nil {
116
116
  return err
117
117
  }
118
+ inputs := ovio.Obj(sc["inputs"])
119
+ for _, input := range []struct{ key, file string }{
120
+ {"rubric_sha256", join(run, str(ovio.Obj(ovio.Obj(R["run.json"]["revisions"])["rubric"]), "file"))},
121
+ {"results_sha256", join(genDir, results)},
122
+ {"gates_sha256", join(genDir, "gates.json")},
123
+ } {
124
+ if !isFile(input.file) {
125
+ add("scorecard: %s input is missing", input.key)
126
+ continue
127
+ }
128
+ digest, err := sha(input.file)
129
+ if err != nil {
130
+ return err
131
+ }
132
+ if !eq(inputs[input.key], digest) {
133
+ add("scorecard: %s does not match the selected calculator input", input.key)
134
+ }
135
+ }
118
136
  gates := ovio.Obj(sc["gates"])
119
137
  claims := func(g string) bool { return str(gates, g) == "PASS" }
120
138
  for _, it := range fs {
@@ -131,10 +149,10 @@ func consistency(run, genDir string, R map[string]map[string]any, add func(strin
131
149
  if !claims("G-COVERAGE") {
132
150
  return nil
133
151
  }
134
- admitted := map[string]bool{}
152
+ admitted := map[string]map[string]any{}
135
153
  for _, it := range ovio.List(R["dispatch.json"]["attempts"]) {
136
154
  if x := ovio.Obj(it); str(x, "status") == "admitted" {
137
- admitted[str(x, "receipt")] = true
155
+ admitted[str(x, "receipt")] = x
138
156
  }
139
157
  }
140
158
  if len(cells) == 0 {
@@ -147,8 +165,8 @@ func consistency(run, genDir string, R map[string]map[string]any, add func(strin
147
165
  case "blocked":
148
166
  add("scorecard: G-COVERAGE is PASS but applicability %s is blocked", key)
149
167
  case "applicable":
150
- if !checkedBy(run, str(c, "domain"), ovio.List(c["receipts"]), admitted) {
151
- add("scorecard: G-COVERAGE is PASS but no admitted, non-gap receipt of applicability %s lists %s in domains_checked", key, str(c, "domain"))
168
+ if !checkedBy(run, str(c, "domain"), str(c, "lane"), ovio.List(c["receipts"]), admitted) {
169
+ add("scorecard: G-COVERAGE is PASS but no admitted, non-gap receipt of applicability %s lists %s in domains_checked for its lane", key, str(c, "domain"))
152
170
  }
153
171
  }
154
172
  }
@@ -169,18 +187,26 @@ func consistency(run, genDir string, R map[string]map[string]any, add func(strin
169
187
  return nil
170
188
  }
171
189
 
172
- // checkedBy reports whether any admitted, non-gap receipt records domain in
173
- // domains_checked.
174
- func checkedBy(run, domain string, receipts []any, admitted map[string]bool) bool {
190
+ // checkedBy requires the receipt's domain and lane to match the cell and its
191
+ // identity to match the admitted dispatch attempt.
192
+ func checkedBy(run, domain, lane string, receipts []any, admitted map[string]map[string]any) bool {
175
193
  for _, r := range receipts {
176
194
  p := ovio.Str(r)
177
- if !admitted[p] || !inside(run, p) || !isFile(join(run, p)) {
195
+ attempt := admitted[p]
196
+ if attempt == nil || str(attempt, "lane") != lane || !inside(run, p) || !isFile(join(run, p)) {
178
197
  continue
179
198
  }
180
199
  rc, err := ovio.LoadObject(join(run, p))
181
200
  if err != nil || str(rc, "outcome") == "gap" {
182
201
  continue
183
202
  }
203
+ matches := true
204
+ for _, k := range []string{"task_id", "attempt_id", "role", "lane", "snapshot"} {
205
+ matches = matches && str(attempt, k) != "" && eq(rc[k], attempt[k])
206
+ }
207
+ if !matches {
208
+ continue
209
+ }
184
210
  if slices.ContainsFunc(ovio.List(rc["domains_checked"]), func(d any) bool { return d == any(domain) }) {
185
211
  return true
186
212
  }
@@ -111,15 +111,24 @@ func manifestOK(run, genDir string) ([]string, map[string]any, error) {
111
111
  return nil, nil, err
112
112
  }
113
113
  var errs []string
114
- files, revs := ovio.Obj(m["files"]), ovio.Obj(m["revisions"])
115
- check := func(src map[string]any) error {
114
+ files, revs, receipts := ovio.Obj(m["files"]), ovio.Obj(m["revisions"]), ovio.Obj(m["receipts"])
115
+ if receipts == nil {
116
+ errs = append(errs, "manifest: receipts must record admitted receipt digests")
117
+ }
118
+ dispatch, err := ovio.LoadObject(join(genDir, "dispatch.json"))
119
+ if err != nil {
120
+ return nil, nil, err
121
+ }
122
+ for _, it := range ovio.List(dispatch["attempts"]) {
123
+ attempt := ovio.Obj(it)
124
+ if str(attempt, "status") == "admitted" && str(receipts, str(attempt, "receipt")) == "" {
125
+ errs = append(errs, "manifest: admitted receipt digest missing: "+str(attempt, "receipt"))
126
+ }
127
+ }
128
+ check := func(src map[string]any, base string) error {
116
129
  for _, rel := range sortedKeys(src) {
117
- base := run
118
- if _, ok := files[rel]; ok {
119
- base = genDir
120
- }
121
130
  p := join(base, rel)
122
- ok := isFile(p)
131
+ ok := inside(base, rel) && isFile(p)
123
132
  if ok {
124
133
  d, err := sha(p)
125
134
  if err != nil {
@@ -133,10 +142,13 @@ func manifestOK(run, genDir string) ([]string, map[string]any, error) {
133
142
  }
134
143
  return nil
135
144
  }
136
- if err := check(files); err != nil {
145
+ if err := check(files, genDir); err != nil {
146
+ return nil, nil, err
147
+ }
148
+ if err := check(revs, run); err != nil {
137
149
  return nil, nil, err
138
150
  }
139
- if err := check(revs); err != nil {
151
+ if err := check(receipts, run); err != nil {
140
152
  return nil, nil, err
141
153
  }
142
154
  return errs, m, nil
@@ -273,13 +285,22 @@ func publish(run string, stdout, stderr io.Writer) (int, error) {
273
285
  if err != nil {
274
286
  return 0, err
275
287
  }
276
- revs := map[string]any{}
288
+ revs, receipts := map[string]any{}, map[string]any{}
289
+ settledReceipts := map[string]string{}
277
290
  if prevDir != "" {
278
291
  pmErrs, pm, err := manifestOK(run, prevDir)
279
292
  if err != nil {
280
293
  return 0, err
281
294
  }
282
295
  errs = append(errs, pmErrs...)
296
+ for rel, digest := range ovio.Obj(pm["receipts"]) {
297
+ receipts[rel] = digest
298
+ rc, err := ovio.LoadObject(join(run, rel))
299
+ if err != nil {
300
+ return 0, err
301
+ }
302
+ settledReceipts[Repr([]any{rc["task_id"], rc["attempt_id"]})] = rel
303
+ }
283
304
  prevRevs := ovio.Obj(pm["revisions"])
284
305
  for _, rel := range sortedKeys(prevRevs) {
285
306
  p := join(run, rel)
@@ -308,6 +329,29 @@ func publish(run string, stdout, stderr io.Writer) (int, error) {
308
329
  }
309
330
  revs["revisions/"+e.Name()] = d
310
331
  }
332
+ dispatch, err := ovio.LoadObject(join(tmp, "dispatch.json"))
333
+ if err != nil {
334
+ return 0, err
335
+ }
336
+ for _, it := range ovio.List(dispatch["attempts"]) {
337
+ attempt := ovio.Obj(it)
338
+ rel := str(attempt, "receipt")
339
+ if str(attempt, "status") != "admitted" || !inside(run, rel) || !isFile(join(run, rel)) {
340
+ continue
341
+ }
342
+ if prior, settled := settledReceipts[Repr([]any{attempt["task_id"], attempt["attempt_id"]})]; settled && prior != rel {
343
+ errs = append(errs, "settled admitted attempt changed receipt: "+PyStr(attempt["task_id"])+"#"+PyStr(attempt["attempt_id"]))
344
+ continue
345
+ }
346
+ if _, settled := receipts[rel]; settled {
347
+ continue
348
+ }
349
+ digest, err := sha(join(run, rel))
350
+ if err != nil {
351
+ return 0, err
352
+ }
353
+ receipts[rel] = digest
354
+ }
311
355
  if len(errs) > 0 {
312
356
  for _, e := range errs {
313
357
  fmt.Fprintln(stdout, "VIOLATION: "+e)
@@ -342,7 +386,7 @@ func publish(run string, stdout, stderr io.Writer) (int, error) {
342
386
  previous = prev
343
387
  }
344
388
  manifest := map[string]any{"schema": "overhaul.manifest/1", "generation": gen, "previous": previous,
345
- "files": files, "revisions": revs}
389
+ "files": files, "revisions": revs, "receipts": receipts}
346
390
  if err := ovio.WriteJSON(join(tmp, "manifest.json"), manifest, 0o644); err != nil {
347
391
  return 0, err
348
392
  }
@@ -199,6 +199,23 @@ func acceptedRetry(root string, lease *Lease, selector string) (string, error) {
199
199
  }
200
200
 
201
201
  func protectRawLease(current, next *Lease) error {
202
+ for _, candidate := range next.Slices {
203
+ if candidate.WrightStatus != WrightIntegrated {
204
+ continue
205
+ }
206
+ found := false
207
+ if current != nil {
208
+ for _, sl := range current.Slices {
209
+ if sl.WrightStatus == WrightIntegrated && reflect.DeepEqual(sl, candidate) {
210
+ found = true
211
+ break
212
+ }
213
+ }
214
+ }
215
+ if !found {
216
+ return fmt.Errorf("lease-write cannot introduce integrated slice %s; use integrate", SliceKey(candidate.Workspace, candidate.ID))
217
+ }
218
+ }
202
219
  if current == nil {
203
220
  if next.PendingLanding != nil || next.LastLanding != nil {
204
221
  return fmt.Errorf("lease-write cannot fabricate landing evidence")
@@ -11,7 +11,8 @@ import (
11
11
  var (
12
12
  acceptanceHeadingRE = regexp.MustCompile(`(?i)^##\s+Acceptance criteria\s*$`)
13
13
  canonicalACRE = regexp.MustCompile(`\bAC-\d{3}\b`)
14
- h2HeadingRE = regexp.MustCompile(`^##\s+`)
14
+ acceptanceEndRE = regexp.MustCompile(`^#{1,2}\s+`)
15
+ acDeclarationRE = regexp.MustCompile("^\\s*(?:#{1,6}\\s+|[-+*]\\s+(?:\\[[ xX]\\]\\s+)?|\\d+[.)]\\s+|\\|\\s*)?(?:\\*\\*|__|`)?(AC-\\d{3})(?:\\*\\*|__|`)?(?:\\s*[:|]|[ \\t]+[-–—])")
15
16
  )
16
17
 
17
18
  // AcceptanceMapResult is the deterministic ID-presence check from spec.md
@@ -50,11 +51,40 @@ func ParseAcceptanceMap(spec, tasks, testPlan []byte, requireTasks, requireTestP
50
51
  }
51
52
 
52
53
  func acceptanceSection(spec []byte) string {
53
- structural, err := markdowntext.Structural(spec)
54
+ structural, err := markdowntext.Prose(spec)
54
55
  if err != nil {
55
56
  return ""
56
57
  }
57
58
  lines := strings.Split(strings.ReplaceAll(string(structural), "\r\n", "\n"), "\n")
59
+ start, end := acceptanceSectionBounds(lines)
60
+ if start < 0 {
61
+ return ""
62
+ }
63
+ return strings.Join(lines[start:end], "\n")
64
+ }
65
+
66
+ // AcceptanceDeclarationsOutsideSection identifies definitions, not prose
67
+ // references, that the canonical section parser would otherwise silently skip.
68
+ func AcceptanceDeclarationsOutsideSection(spec []byte) []string {
69
+ structural, err := markdowntext.Prose(spec)
70
+ if err != nil {
71
+ return nil
72
+ }
73
+ lines := strings.Split(strings.ReplaceAll(string(structural), "\r\n", "\n"), "\n")
74
+ start, end := acceptanceSectionBounds(lines)
75
+ var ids []string
76
+ for i, line := range lines {
77
+ if start >= 0 && i >= start && i < end {
78
+ continue
79
+ }
80
+ if match := acDeclarationRE.FindStringSubmatch(line); match != nil {
81
+ ids = append(ids, match[1])
82
+ }
83
+ }
84
+ return ids
85
+ }
86
+
87
+ func acceptanceSectionBounds(lines []string) (int, int) {
58
88
  start := -1
59
89
  for i, line := range lines {
60
90
  if acceptanceHeadingRE.MatchString(strings.TrimSpace(line)) {
@@ -63,16 +93,16 @@ func acceptanceSection(spec []byte) string {
63
93
  }
64
94
  }
65
95
  if start < 0 {
66
- return ""
96
+ return -1, -1
67
97
  }
68
98
  end := len(lines)
69
99
  for i := start; i < len(lines); i++ {
70
- if h2HeadingRE.MatchString(lines[i]) {
100
+ if acceptanceEndRE.MatchString(strings.TrimSpace(lines[i])) {
71
101
  end = i
72
102
  break
73
103
  }
74
104
  }
75
- return strings.Join(lines[start:end], "\n")
105
+ return start, end
76
106
  }
77
107
 
78
108
  func uniqueCanonicalACs(text string) []string {
@@ -63,12 +63,13 @@ func regularFileExists(path string) bool {
63
63
  return err == nil && info.Mode().IsRegular()
64
64
  }
65
65
 
66
- func firstPhaseWord(value string) string {
66
+ // CanonicalPhase reads the leading phase name, ignoring case and trailing annotations.
67
+ func CanonicalPhase(value string) Phase {
67
68
  word := strings.ToLower(strings.TrimSpace(value))
68
69
  if i := strings.IndexAny(word, " "); i > 0 {
69
70
  word = word[:i]
70
71
  }
71
- return word
72
+ return Phase(word)
72
73
  }
73
74
 
74
75
  // blankOrHash reports whether a trimmed line carries no meaningful content: it
@@ -148,7 +148,7 @@ func (o *WorkspaceObservation) DeclaredPhase() (Phase, error) {
148
148
  if !ok {
149
149
  return "", fmt.Errorf("feature %q: no phase in %s ledger; record phase in state.md and retry", o.slug, LedgerFile)
150
150
  }
151
- phase := Phase(firstPhaseWord(value))
151
+ phase := CanonicalPhase(value)
152
152
  policy, ok := PolicyFor(phase)
153
153
  if !ok {
154
154
  return "", fmt.Errorf("feature %q: unknown phase %q; record a known phase in state.md and retry", o.slug, phase)
@@ -7,7 +7,6 @@ import (
7
7
  "os"
8
8
  "path/filepath"
9
9
  "strings"
10
- "sync"
11
10
 
12
11
  "github.com/devrites/devrites/internal/devritespaths"
13
12
  "github.com/devrites/devrites/internal/fsutil"
@@ -77,57 +76,34 @@ func AppendLog(path, record string) error {
77
76
  // is released automatically even if the process dies mid-operation (crash-safe);
78
77
  // see lock_unix.go / lock_other.go.
79
78
  func WithFeatureLock(root, slug string, fn func() error) error {
80
- lockPath, err := featureLockPath(root, slug)
81
- if err != nil {
82
- return err
79
+ if !devritespaths.ValidSlug(slug) {
80
+ return fmt.Errorf("lock feature %q: not a feature slug", slug)
81
+ }
82
+ // Keep the lock outside the movable workspace; never unlink it on release.
83
+ lockDir := filepath.Join(root, ".locks", "features", slug)
84
+ if err := os.MkdirAll(lockDir, 0o755); err != nil {
85
+ return fmt.Errorf("lock feature %q: %w", slug, err)
83
86
  }
84
- return WithLock(lockPath, fn)
87
+ return WithLock(filepath.Join(lockDir, ".lock"), func() error {
88
+ if _, err := os.Lstat(filepath.Join(root, "archive", slug)); err == nil {
89
+ return fmt.Errorf("lock feature %q: workspace is closed (archived)", slug)
90
+ } else if !errors.Is(err, fs.ErrNotExist) {
91
+ return fmt.Errorf("lock feature %q: %w", slug, err)
92
+ }
93
+ if err := os.MkdirAll(featureDir(root, slug), 0o755); err != nil {
94
+ return fmt.Errorf("lock feature %q: %w", slug, err)
95
+ }
96
+ return fn()
97
+ })
85
98
  }
86
99
 
87
100
  // WithLock runs fn while holding the same exclusive advisory lock as
88
101
  // WithFeatureLock, keyed on lockPath, for shared ledgers outside one feature.
89
102
  func WithLock(lockPath string, fn func() error) error {
90
- return withLock(lockPath, func(func()) error { return fn() })
91
- }
92
-
93
- // WithFeatureLockRelease is WithFeatureLock for a caller that must drop the lock
94
- // before fn returns, for example because the platform refuses to rename the
95
- // directory that holds the open lock file. fn receives release, which is safe to
96
- // call more than once; the lock is also released when fn returns.
97
- func WithFeatureLockRelease(root, slug string, fn func(release func()) error) error {
98
- lockPath, err := featureLockPath(root, slug)
99
- if err != nil {
100
- return err
101
- }
102
- return withLock(lockPath, fn)
103
- }
104
-
105
- // featureLockPath validates slug and creates the feature directory, returning
106
- // the lock file path inside it. A closed workspace (work/<slug> gone, archive/<slug>
107
- // present) is refused rather than resurrected by a late lock.
108
- func featureLockPath(root, slug string) (string, error) {
109
- if !devritespaths.ValidSlug(slug) {
110
- return "", fmt.Errorf("lock feature %q: not a feature slug", slug)
111
- }
112
- dir := featureDir(root, slug)
113
- if _, err := os.Lstat(dir); errors.Is(err, fs.ErrNotExist) {
114
- if _, archErr := os.Lstat(filepath.Join(root, "archive", slug)); archErr == nil {
115
- return "", fmt.Errorf("lock feature %q: workspace is closed (archived)", slug)
116
- }
117
- }
118
- if err := os.MkdirAll(dir, 0o755); err != nil {
119
- return "", fmt.Errorf("lock feature %q: %w", slug, err)
120
- }
121
- return filepath.Join(dir, ".lock"), nil
122
- }
123
-
124
- func withLock(lockPath string, fn func(release func()) error) error {
125
103
  l, err := acquireLock(lockPath)
126
104
  if err != nil {
127
105
  return fmt.Errorf("lock %s: %w", lockPath, err)
128
106
  }
129
- var once sync.Once
130
- release := func() { once.Do(func() { _ = l.release() }) }
131
- defer release()
132
- return fn(release)
107
+ defer func() { _ = l.release() }()
108
+ return fn()
133
109
  }
@@ -10,18 +10,20 @@ type Command struct {
10
10
  Verb string
11
11
  Claude string
12
12
  Codex string
13
+ Skill string
13
14
  }
14
15
 
15
- // ForVerb returns the Claude and Codex forms for a public rite verb. The empty
16
+ // ForVerb returns native invocation forms for a public rite verb. The empty
16
17
  // verb returns an empty command so callers can omit suggestions safely.
17
18
  func ForVerb(verb string) Command {
18
19
  verb = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(verb, "/"), "$"))
20
+ verb = strings.TrimPrefix(verb, "skill:")
19
21
  verb = strings.TrimPrefix(verb, "rite-")
20
22
  if verb == "" {
21
23
  return Command{}
22
24
  }
23
25
  name := "rite-" + verb
24
- return Command{Verb: verb, Claude: "/" + name, Codex: "$" + name}
26
+ return Command{Verb: verb, Claude: "/" + name, Codex: "$" + name, Skill: "/skill:" + name}
25
27
  }
26
28
 
27
29
  // ForAction extracts the first explicit rite invocation from a cursor action.
@@ -29,13 +31,33 @@ func ForVerb(verb string) Command {
29
31
  func ForAction(action string) Command {
30
32
  for _, field := range strings.Fields(action) {
31
33
  field = strings.Trim(field, "`'\"()[]{}<>,.;:")
32
- if strings.HasPrefix(field, "/rite-") || strings.HasPrefix(field, "$rite-") {
34
+ if strings.HasPrefix(field, "/rite-") || strings.HasPrefix(field, "$rite-") || strings.HasPrefix(field, "/skill:rite-") {
33
35
  return ForVerb(field)
34
36
  }
35
37
  }
36
38
  return Command{}
37
39
  }
38
40
 
41
+ // SourceIsFirstArgument distinguishes workspace slugs from phase-local
42
+ // slice/mode/scope/target arguments. Plan's optional source follows its mode.
43
+ func (c Command) SourceIsFirstArgument() bool {
44
+ switch c.Verb {
45
+ case "", "build", "plan", "review", "polish":
46
+ return false
47
+ default:
48
+ return true
49
+ }
50
+ }
51
+
52
+ // SourceInvocation renders an executable cursor command, never display prose.
53
+ // Commands without a slug argument retain the caller's selected SOURCE.
54
+ func (c Command) SourceInvocation(source string) string {
55
+ if source != "" && c.SourceIsFirstArgument() {
56
+ return c.Claude + " " + source
57
+ }
58
+ return c.Claude
59
+ }
60
+
39
61
  // Both renders a command in host-neutral prose for engine stderr/stdout messages.
40
62
  func (c Command) Both() string {
41
63
  if c.Verb == "" {
package/engine/main.go CHANGED
@@ -334,6 +334,21 @@ func cmdGate(root string, kind gate.Kind, args []string, stdout, stderr io.Write
334
334
  fmt.Fprintln(stdout, binding)
335
335
  return exitOK
336
336
  }
337
+ if kind == gate.Seal {
338
+ code := exitBlocked
339
+ if err := state.WithFeatureLock(root, args[0], func() error {
340
+ code = cmdGateLocked(root, kind, args, stdout, stderr)
341
+ return nil
342
+ }); err != nil {
343
+ fmt.Fprintf(stderr, "seal: BLOCKED: %v\n", err)
344
+ return exitBlocked
345
+ }
346
+ return code
347
+ }
348
+ return cmdGateLocked(root, kind, args, stdout, stderr)
349
+ }
350
+
351
+ func cmdGateLocked(root string, kind gate.Kind, args []string, stdout, stderr io.Writer) int {
337
352
  result, err := gate.Check(kind, root, args[0])
338
353
  if err != nil {
339
354
  fmt.Fprintf(stderr, "devrites: %v\n", err)
@@ -369,6 +384,10 @@ func cmdGate(root string, kind gate.Kind, args []string, stdout, stderr io.Write
369
384
  fmt.Fprintf(stdout, "release-union: BLOCKED: %v\n", err)
370
385
  return sealBlocked()
371
386
  }
387
+ if err := lib.CheckFinalPrerequisites(root, args[0]); err != nil {
388
+ fmt.Fprintf(stdout, "final-prerequisites: BLOCKED: %v\n", err)
389
+ return sealBlocked()
390
+ }
372
391
  code := lib.EvidenceFresh(root, []string{args[0]}, stdout, stderr)
373
392
  if code == exitUsage {
374
393
  return exitUsage
@@ -55,28 +55,27 @@ If the candidate diff has no matching surfaces, record
55
55
  The `agents` dispatch contract needs no flag: `--role` auto-fires it
56
56
  (`auto=[agents]` in the output). The command prints `unselected=[...]`; an
57
57
  omitted applicable trigger is a gap, not a shortcut.
58
- 3. Enforce the launch barrier through the engine, not prose. Open the wave
59
- (`devrites-engine dispatch <slug> open --phase <p> --wave <w> --role <r>...`;
60
- a reviewer, auditor, or test-analyst wave adds `--packet <file>`, any one of the
61
- wave's rendered `packets/<w>.<role>.md` (hand each agent its own file unedited), and `open` refuses one that seeds a gate result, outcome,
62
- count, or "already passed" claim; a Build reviewer wave adds `--slice <SLICE-ID>`
63
- so its findings key on that slice; at Review and Seal it also needs the packet to
64
- name the frozen candidate diff and the manifest to cover every checkpointed path;
65
- a same-tree writer wave adds `--claim-session <id> --claim <path>...` per
66
- [`wright-dispatch.md`](../../rite-build/reference/wright-dispatch.md#run)), launch every native agent nonblocking through the host's own agent
67
- tool (never a shell-launched host CLI such as `omp -p` or `claude -p`: that is a
68
- dispatch bridge, its PID handle dies with the session, and its return is a `gap`),
69
- record each returned host handle
70
- (`dispatch ... start --wave <w> --role <r> --handle <id>`) — the last start
71
- seals the wave (an explicit `seal` is a no-op), which makes a
72
- serial launch-then-wait pattern impossible to record. Record each completion
73
- with `dispatch ... return`; a reviewer adds `--result <file>` holding its return verbatim (the engine stores it as `packets/<wave>-<role>.return.md`, derives the outcome from its head, and records its `Finding:` keys; a file already at that path is kept), or `--outcome <findings|no-findings|gap>` when the host cannot hand the return over as a file; `return` refuses before seal. `dispatch status`
74
- exits nonzero while the wave is open, sealed, or abandoned. `start`/`return`
75
- auto-record the dispatch/return metrics events — no separate ledger call. If
76
- an opened wave cannot obtain every handle or reach `seal`, run
77
- `devrites-engine dispatch <slug> abandon --wave <w> --reason <reason>` (with
78
- `--release` on a host-confirmed stop or refused launch) before
79
- recording the gap; never leave an open wave to block a retry.
58
+ 3. Open a single-use wave; every retry needs a fresh ID:
59
+ `devrites-engine dispatch <slug> open --phase <p> --wave <w> --role <r>...`.
60
+ Reviewer/auditor/test-analyst waves add `--packet <file>` naming any rendered
61
+ `packets/<w>.<role>.md`; give each agent its own packet unedited. `open` rejects
62
+ seeded gate results, outcomes, counts, or pass claims. Build reviewers add
63
+ `--slice <SLICE-ID>`; Review/Seal packets bind the frozen diff and a manifest
64
+ covering every checkpointed path. Same-tree writers add `--claim-session <id>
65
+ --claim <path>...` per [`wright-dispatch.md`](../../rite-build/reference/wright-dispatch.md#run).
66
+ Launch all native agents nonblocking through the host tool before waiting.
67
+ Never shell-launch host CLIs (`omp -p`, `claude -p`): that bridge's handle dies
68
+ with the session and its return is a gap. Record every host handle with
69
+ `dispatch ... start --wave <w> --role <r> --handle <id>`; the last seals the wave
70
+ (explicit `seal` then does nothing). Return only after seal, with the recorded
71
+ handle: `dispatch ... return --wave <w> --role <r> --handle <id>`.
72
+ Reviewers add `--result <file>`: retain it verbatim at
73
+ `packets/<wave>-<role>.return.md` (keep an existing file), derive outcome and
74
+ `Finding:` keys. If the host cannot provide a file, use explicit
75
+ `--outcome findings|no-findings|gap`. Status is nonzero for open/sealed/abandoned
76
+ waves; start/return record metrics. If handles/seal cannot be obtained,
77
+ `dispatch ... abandon --wave <w> --reason <reason>` before recording the gap;
78
+ add `--release` only after host-confirmed stop/refused launch.
80
79
  4. Ask for each exact named agent in fresh context; omit native syntax and
81
80
  `agent_type`/`task_name`/`fork_turns` from the instruction.
82
81
  5. Every dispatch reaches a terminal classification before reconciliation:
@@ -144,32 +143,35 @@ the client renames a field its mocked test still passes, and the verdict reads c
144
143
 
145
144
  ## Cancellation and terminal reconciliation
146
145
 
147
- Cancellation acknowledgement means the host accepted a request, not that the
148
- child stopped. A transport timeout likewise leaves execution unknown. Keep the
149
- child's handle, path/resource ownership and capacity reservation until the
150
- native host confirms a terminal state and returned work has been reconciled.
151
- Query or wait on that handle; do not replace the child, release its slot, or
152
- reuse its resources while it may still run. If terminal state cannot be
153
- established, record a gap and retain ownership; for a child launched by an earlier
154
- session, the claim proof below settles it. A result classification alone
155
- does not prove host termination.
146
+ Cancellation acknowledgement or timeout leaves execution unknown. Keep handles,
147
+ path/resource ownership, and capacity until the native host confirms termination
148
+ and returned work is reconciled. Query/wait; never replace children, release slots,
149
+ or reuse resources prematurely. If unproven, record a gap and retain ownership;
150
+ earlier-session claim proof follows. Outcome classification alone proves no death.
156
151
 
157
152
  On resume (compaction or a new session), run `devrites-engine dispatch <slug>
158
153
  status`, `parallel status`, and `claim list` before any dispatch. A read-only
159
154
  wave whose handles this session cannot query: `abandon` it, record each unreturned
160
- role `gap`, and re-dispatch fresh on the current digest; its late returns are
161
- `stale`. A writer keeps its claim and lease — no relaunch on those paths until the
162
- claim is released or its holder is proven terminal. For an unqueryable writer,
163
- `claim list` showing its claim `dead` (the engine proved the holding session
164
- exited on this host), or `claim list --all --session <id>` showing it `released`
165
- (bare `list` only counts released rows), is that proof: `abandon` the wave, record the
166
- role `gap`, claim the paths, and re-dispatch without waiting on the launching
167
- session. `live` or `expired` stays held. A wright that never returned an
155
+ role `gap`, and re-dispatch under a fresh wave ID on the current digest; its late
156
+ returns are `stale`. A writer retains claim/lease: no relaunch under a fresh wave,
157
+ same session, or expired TTL until release or proven termination. For an
158
+ unqueryable writer, `claim list` `dead` proves its holding session exited on this
159
+ host; `claim list --all --session <id>` shows `released` (bare `list` counts it).
160
+ After that proof, `dispatch <slug> abandon --wave <w> --release --reason <reason>`
161
+ stamps `terminal_at` even without a claim, not a return. Record `gap`, reclaim
162
+ paths, and re-dispatch. `live`/`expired` stays held; cancellation acknowledgement,
163
+ timeout, ordinary abandonment, or replacement cannot prove writer death.
164
+ A wright that never returned an
168
165
  admitted result leaves unadmitted bytes: task-path changes since its pre-dispatch
169
166
  `git diff --name-only` baseline are never cited as built nor discarded, and are
170
167
  named in the re-dispatch contract; changes outside task paths stop for the human
171
168
  (Converge side: [`rite-converge`](../../rite-converge/SKILL.md)).
172
169
 
170
+ Finalization settles every launched role, including proof/custom/writer roles.
171
+ Abandoned proof/reviewer replacement needs a newer settled account, same canonical
172
+ phase/role/exact unordered slice set, and distinct handle. Writer termination
173
+ admits no bytes and bypasses no candidate, admission, review or proof gate.
174
+
173
175
  ## Scale limits
174
176
 
175
177
  The role table bounds rosters. Use up to ten concurrent read-only reviewers,
@@ -143,10 +143,14 @@ deployment, or anything the user did not start with this command.
143
143
  reconfirmation otherwise), the re-hashed plan digest, the task closure, the current
144
144
  plan revision, no later stop, and for PR or branch scope a checked-out `HEAD` equal
145
145
  to the pinned head. Record it in `approval.json`.
146
- 2. Execute the approved DAG with [`overhaul-implementer`](../../agents/overhaul-implementer.md): one
147
- writer per path, independent frontend and backend tasks concurrently, TDD per
148
- [`references/repair-and-measurement.md`](references/repair-and-measurement.md).
149
- Admit patches only when observed changed paths equal the contract.
146
+ 2. Execute the approved DAG with [`overhaul-implementer`](../../agents/overhaul-implementer.md)
147
+ and TDD per [`references/repair-and-measurement.md`](references/repair-and-measurement.md).
148
+ Serialize shared-repository target writes, even for independent frontend/backend
149
+ tasks: whole-repository before-capture → target writer stops → full delta →
150
+ receipt admission, then start the next writer. Preserve one owner per physical
151
+ task path; admit only observed changes within the approved contract. Read-only
152
+ reviews and verification may overlap on immutable inputs when resources permit
153
+ ([observation windows](references/orchestration.md#single-writer-budgets-and-cancellation)).
150
154
  3. Verify each fix independently (`repair-verify`), test the assembled candidate,
151
155
  remeasure approved targets, rescore, publish, and follow the convergence table in
152
156
  [`references/lifecycle.md`](references/lifecycle.md) — continuing automatically