devrites 7.3.0 → 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.
- package/CHANGELOG.md +32 -0
- package/README.md +19 -3
- package/bin/devrites.mjs +31 -5
- package/docs/engine/commands.md +41 -3
- package/docs/release.md +5 -2
- package/engine/Makefile +2 -2
- package/engine/internal/acceptance/newfinal_origins.go +39 -0
- package/engine/internal/acceptance/preconditions.go +17 -11
- package/engine/internal/acceptance/transcript.go +23 -4
- package/engine/internal/gate/gate.go +8 -6
- package/engine/internal/gate/readiness_binding.go +54 -24
- package/engine/internal/install/apply.go +36 -21
- package/engine/internal/install/binary.go +67 -9
- package/engine/internal/install/install.go +63 -21
- package/engine/internal/install/manifest.go +4 -5
- package/engine/internal/install/merge.go +89 -33
- package/engine/internal/install/preflight.go +60 -19
- package/engine/internal/install/uninstall.go +19 -9
- package/engine/internal/install/update.go +0 -4
- package/engine/internal/lib/claims.go +170 -4
- package/engine/internal/lib/claims_holder.go +13 -16
- package/engine/internal/lib/closeout.go +52 -28
- package/engine/internal/lib/context_io.go +88 -4
- package/engine/internal/lib/continuation.go +111 -24
- package/engine/internal/lib/dispatch.go +157 -24
- package/engine/internal/lib/final_prerequisites.go +137 -0
- package/engine/internal/lib/manifest_union.go +62 -25
- package/engine/internal/lib/next.go +10 -10
- package/engine/internal/lib/packet.go +32 -8
- package/engine/internal/lib/packet_integrity.go +194 -2
- package/engine/internal/lib/resolve.go +128 -142
- package/engine/internal/lib/treebaseline.go +0 -1
- package/engine/internal/markdowntext/markdowntext.go +110 -31
- package/engine/internal/overhaul/records/check.go +45 -5
- package/engine/internal/overhaul/records/consistency.go +34 -8
- package/engine/internal/overhaul/records/run.go +55 -11
- package/engine/internal/parallel/landing.go +17 -0
- package/engine/internal/state/acceptance.go +35 -5
- package/engine/internal/state/feature.go +3 -2
- package/engine/internal/state/observation.go +50 -3
- package/engine/internal/state/write.go +20 -44
- package/engine/internal/workflow/commands.go +25 -3
- package/engine/main.go +19 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/.claude/skills/overhaul/SKILL.md +8 -4
- package/pack/.claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/.claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/.claude/skills/overhaul/references/records.md +23 -8
- package/pack/.claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/.claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/.claude/skills/rite-prove/SKILL.md +15 -7
- package/pack/.claude/skills/rite-review/SKILL.md +7 -0
- package/pack/.claude/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/claude/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/claude/skills/overhaul/references/records.md +23 -8
- package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/claude/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/claude/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/claude/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/codex/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/codex/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/codex/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/codex/skills/overhaul/references/records.md +23 -8
- package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/codex/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/codex/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/codex/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/codex/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/devin/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/devin/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/devin/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/devin/skills/overhaul/references/records.md +23 -8
- package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/devin/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/devin/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/devin/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/devin/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/dsh/runtime/cordis.patch.yml +28 -14
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/dsh/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/dsh/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/dsh/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/dsh/skills/overhaul/references/records.md +23 -8
- package/pack/generated/dsh/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/dsh/skills/overhaul/references/scoring.md +6 -0
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/dsh/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/dsh/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/dsh/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/omp/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/omp/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/omp/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/omp/skills/overhaul/references/records.md +23 -8
- package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/omp/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/omp/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/omp/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/omp/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/pi/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/pi/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/pi/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/pi/skills/overhaul/references/records.md +23 -8
- package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/pi/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/pi/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/pi/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/pi/skills/rite-vet/SKILL.md +10 -1
- package/package.json +1 -1
- package/scripts/dsh-generate.py +1 -1
- package/scripts/install-lib.sh +29 -0
- 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]
|
|
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")] =
|
|
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
|
|
173
|
-
//
|
|
174
|
-
func checkedBy(run, domain string, receipts []any, admitted map[string]
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
100
|
+
if acceptanceEndRE.MatchString(strings.TrimSpace(lines[i])) {
|
|
71
101
|
end = i
|
|
72
102
|
break
|
|
73
103
|
}
|
|
74
104
|
}
|
|
75
|
-
return
|
|
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
|
-
|
|
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 :=
|
|
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)
|
|
@@ -244,11 +244,40 @@ func ObserveWorkspace(root, slug string) (*WorkspaceObservation, error) {
|
|
|
244
244
|
return observeWorkspace(root, slug, nil)
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// ObserveReferencedWorkspace acquires a read-only planning source, live first
|
|
248
|
+
// and archived only when its canonical live directory is absent. A different
|
|
249
|
+
// valid active-workspace override never redirects this explicit source.
|
|
250
|
+
func ObserveReferencedWorkspace(root, slug string) (*WorkspaceObservation, error) {
|
|
251
|
+
return observeWorkspaceSource(root, slug, true, nil)
|
|
252
|
+
}
|
|
253
|
+
|
|
247
254
|
func observeWorkspace(root, slug string, callback observationCallback) (*WorkspaceObservation, error) {
|
|
248
|
-
|
|
255
|
+
return observeWorkspaceSource(root, slug, false, callback)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func observeWorkspaceSource(root, slug string, allowArchive bool, callback observationCallback) (*WorkspaceObservation, error) {
|
|
259
|
+
override := os.Getenv("DEVRITES_WORKSPACE")
|
|
260
|
+
overrideSlug := slug
|
|
261
|
+
if allowArchive && override != "" {
|
|
262
|
+
overrideSlug = filepath.Base(filepath.Clean(override))
|
|
263
|
+
}
|
|
264
|
+
if !observationWorkspaceOverrideValid(root, overrideSlug) {
|
|
249
265
|
return nil, ObservationWorkspaceInvalid
|
|
250
266
|
}
|
|
251
|
-
workspacePath, err := devritespaths.ExistingFeatureDirChecked(root,
|
|
267
|
+
workspacePath, err := devritespaths.ExistingFeatureDirChecked(root, overrideSlug)
|
|
268
|
+
canArchive := allowArchive && override == ""
|
|
269
|
+
if overrideSlug != slug {
|
|
270
|
+
if err != nil {
|
|
271
|
+
return nil, ObservationWorkspaceInvalid
|
|
272
|
+
}
|
|
273
|
+
workspacePath, err = devritespaths.ExistingCanonicalFeatureDirChecked(root, slug)
|
|
274
|
+
canArchive = allowArchive
|
|
275
|
+
}
|
|
276
|
+
archived := false
|
|
277
|
+
if err != nil && canArchive && errors.Is(err, fs.ErrNotExist) {
|
|
278
|
+
workspacePath, err = devritespaths.ExistingArchivedFeatureDirChecked(root, slug)
|
|
279
|
+
archived = err == nil
|
|
280
|
+
}
|
|
252
281
|
if err != nil {
|
|
253
282
|
return nil, ObservationWorkspaceInvalid
|
|
254
283
|
}
|
|
@@ -270,6 +299,9 @@ func observeWorkspace(root, slug string, callback observationCallback) (*Workspa
|
|
|
270
299
|
}
|
|
271
300
|
defer func() { _ = roots.devrites.Close() }()
|
|
272
301
|
defer func() { _ = roots.workspace.Close() }()
|
|
302
|
+
if archived && !liveWorkspaceAbsent(roots.devrites, slug) {
|
|
303
|
+
return nil, ObservationWorkspaceInvalid
|
|
304
|
+
}
|
|
273
305
|
|
|
274
306
|
inventory := observationInventory()
|
|
275
307
|
facts := make([]ArtifactFact, 0, len(inventory))
|
|
@@ -315,9 +347,24 @@ func observeWorkspace(root, slug string, callback observationCallback) (*Workspa
|
|
|
315
347
|
if err != nil {
|
|
316
348
|
return nil, ObservationWorkspaceInvalid
|
|
317
349
|
}
|
|
350
|
+
if archived && !liveWorkspaceAbsent(roots.devrites, slug) {
|
|
351
|
+
return nil, ObservationConcurrentChange
|
|
352
|
+
}
|
|
318
353
|
return &WorkspaceObservation{slug: slug, facts: facts, index: index, rootFiles: rootFiles}, nil
|
|
319
354
|
}
|
|
320
355
|
|
|
356
|
+
func liveWorkspaceAbsent(root *os.Root, slug string) bool {
|
|
357
|
+
work := inspectEntry(root, "work")
|
|
358
|
+
if work.state == entryAbsent {
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
if work.state != entryPresent || work.info.Mode()&os.ModeSymlink != 0 || !work.info.IsDir() {
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
workspace := inspectEntry(root, filepath.Join("work", slug))
|
|
365
|
+
return workspace.state == entryAbsent && sameObservedEntry(work, inspectEntry(root, "work"))
|
|
366
|
+
}
|
|
367
|
+
|
|
321
368
|
// readRootFiles lists the workspace directory's regular entries through the
|
|
322
369
|
// already-verified root handle, so the list describes the same snapshot as the
|
|
323
370
|
// artifact facts.
|
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
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
|