devrites 7.0.17 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/engine/help.go +4 -3
- package/engine/internal/lib/affected.go +233 -0
- package/engine/internal/lib/cli_observe.go +25 -3
- package/engine/internal/lib/taskgraph.go +79 -12
- package/engine/main.go +19 -2
- package/pack/.claude/agents/devrites-upgrade-planner.md +2 -1
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +7 -7
- package/pack/.claude/skills/rite-build/SKILL.md +5 -4
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +23 -17
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +15 -3
- package/pack/.claude/skills/rite-define/SKILL.md +4 -2
- package/pack/.claude/skills/rite-plan/reference/slicing.md +6 -2
- package/pack/generated/claude/agents/devrites-upgrade-planner.md +2 -1
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +7 -7
- package/pack/generated/claude/skills/rite-build/SKILL.md +5 -4
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +23 -17
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +15 -3
- package/pack/generated/claude/skills/rite-define/SKILL.md +4 -2
- package/pack/generated/claude/skills/rite-plan/reference/slicing.md +6 -2
- package/pack/generated/codex/agents/devrites-upgrade-planner.toml +2 -1
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +7 -7
- package/pack/generated/codex/skills/rite-build/SKILL.md +5 -4
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +23 -17
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +15 -3
- package/pack/generated/codex/skills/rite-define/SKILL.md +4 -2
- package/pack/generated/codex/skills/rite-plan/reference/slicing.md +6 -2
- package/pack/generated/devin/agents/devrites-upgrade-planner.md +2 -1
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +7 -7
- package/pack/generated/devin/skills/rite-build/SKILL.md +5 -4
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +23 -17
- package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +15 -3
- package/pack/generated/devin/skills/rite-define/SKILL.md +4 -2
- package/pack/generated/devin/skills/rite-plan/reference/slicing.md +6 -2
- package/pack/generated/omp/agents/devrites-upgrade-planner.md +2 -1
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +7 -7
- package/pack/generated/omp/skills/rite-build/SKILL.md +5 -4
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +23 -17
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +15 -3
- package/pack/generated/omp/skills/rite-define/SKILL.md +4 -2
- package/pack/generated/omp/skills/rite-plan/reference/slicing.md +6 -2
- package/pack/generated/pi/agents/devrites-upgrade-planner.md +2 -1
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +7 -7
- package/pack/generated/pi/skills/rite-build/SKILL.md +5 -4
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +23 -17
- package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +15 -3
- package/pack/generated/pi/skills/rite-define/SKILL.md +4 -2
- package/pack/generated/pi/skills/rite-plan/reference/slicing.md +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [7.1.0](https://github.com/ViktorsBaikers/DevRites/compare/v7.0.17...v7.1.0) (2026-10-28)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
* **devrites:** measure slice parallelism and ship the plan-width guard ([#179](https://github.com/ViktorsBaikers/DevRites/issues/179)) ([1b8defe](https://github.com/ViktorsBaikers/DevRites/commit/1b8defe92ecd3bc5feb4757f3ecf5e9d611467cc))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **ci:** mirror the new undici exceptions in osv-scanner.toml ([9a916a1](https://github.com/ViktorsBaikers/DevRites/commit/9a916a1cb2215e0d8382655c88e93f5adbea04fd))
|
|
14
|
+
* **no-release:** preserve autocomplete rolling parallelism ([#178](https://github.com/ViktorsBaikers/DevRites/issues/178)) ([604c705](https://github.com/ViktorsBaikers/DevRites/commit/604c705d4a470ca992bd9050651bfc64145ced97))
|
|
15
|
+
|
|
5
16
|
## [7.0.17](https://github.com/ViktorsBaikers/DevRites/compare/v7.0.16...v7.0.17) (2026-10-27)
|
|
6
17
|
|
|
7
18
|
### Fixed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
|
|
|
31
31
|
the feature to another engineer, the next session reads the same plan,
|
|
32
32
|
decisions, and evidence before it continues.
|
|
33
33
|
|
|
34
|
-
**Status:** [`v7.0
|
|
34
|
+
**Status:** [`v7.1.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.1.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
35
35
|
|
|
36
36
|
This is the latest published release; `main` may contain unreleased work.
|
|
37
37
|
|
package/engine/help.go
CHANGED
|
@@ -12,7 +12,7 @@ import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
const (
|
|
15
|
-
checkUsage = `usage: devrites-engine check <candidate|readiness|preconditions|seal|path-disjoint|task-graph|slice|diff-scope|skill-trust|indexes|regression|drift|windows|dup|anchors|perturb> ...
|
|
15
|
+
checkUsage = `usage: devrites-engine check <candidate|readiness|preconditions|seal|path-disjoint|task-graph|affected|slice|diff-scope|skill-trust|indexes|regression|drift|windows|dup|anchors|perturb> ...
|
|
16
16
|
|
|
17
17
|
check candidate <slug>
|
|
18
18
|
check readiness <slug>
|
|
@@ -20,7 +20,8 @@ const (
|
|
|
20
20
|
check preconditions <slug> [--slice SLICE-###] [--timeout N]
|
|
21
21
|
check seal <slug>
|
|
22
22
|
check path-disjoint [--root <dir>] [<json-file>|-]
|
|
23
|
-
check task-graph <slug>
|
|
23
|
+
check task-graph <slug> [--strict]
|
|
24
|
+
check affected <path...>
|
|
24
25
|
check slice <slug> <SLICE-ID>
|
|
25
26
|
check diff-scope <slug> --allow <csv> | --allow-file <path> [--worktree|--staged|--base <ref>] [--allow-empty] [--baseline] [--verbose] [--cwd <dir>]
|
|
26
27
|
check diff-scope <slug> --snapshot [--worktree|--staged|--base <ref>] [--cwd <dir>]
|
|
@@ -84,7 +85,7 @@ const (
|
|
|
84
85
|
readinessUsage = "usage: devrites-engine check readiness <slug>\n devrites-engine check readiness --emit-binding <slug>"
|
|
85
86
|
preconditionsUsage = "usage: devrites-engine check preconditions <slug> [--slice SLICE-###] [--timeout N]"
|
|
86
87
|
sealUsage = "usage: devrites-engine check seal <slug>"
|
|
87
|
-
taskGraphUsage = "usage: devrites-engine check task-graph <slug>"
|
|
88
|
+
taskGraphUsage = "usage: devrites-engine check task-graph <slug> [--strict]"
|
|
88
89
|
sliceCheckUsage = "usage: devrites-engine check slice <slug> <SLICE-ID>"
|
|
89
90
|
skillTrustUsage = "usage: devrites-engine check skill-trust <path>"
|
|
90
91
|
indexesUsage = "usage: devrites-engine check indexes [--root <dir>]"
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"os/exec"
|
|
9
|
+
"path"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"sort"
|
|
12
|
+
"strings"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// AffectedSelection is the validation a changed-path set requires during Build.
|
|
16
|
+
type AffectedSelection struct {
|
|
17
|
+
Packages []string // Go packages whose tests must run
|
|
18
|
+
Widen bool // full suite required
|
|
19
|
+
Reason string // why the selection widened
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// widenPrefixes force the full suite: each can change behaviour in packages
|
|
23
|
+
// that no changed path names.
|
|
24
|
+
var widenPrefixes = []string{
|
|
25
|
+
".github/workflows/",
|
|
26
|
+
".github/actions/",
|
|
27
|
+
"pack/generated/",
|
|
28
|
+
"engine/internal/lib/testdata/",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// widenBasenames are module manifests at any depth: engine/go.mod is not the
|
|
32
|
+
// only one, and a manifest change re-resolves the whole module.
|
|
33
|
+
var widenBasenames = map[string]bool{"go.mod": true, "go.sum": true, "go.work": true}
|
|
34
|
+
|
|
35
|
+
// SelectAffected maps changed repository paths to the narrowest safe validation.
|
|
36
|
+
// It fails safe: a path it cannot classify widens to the full suite instead of
|
|
37
|
+
// silently narrowing.
|
|
38
|
+
func SelectAffected(changed []string) AffectedSelection {
|
|
39
|
+
seen := map[string]bool{}
|
|
40
|
+
var pkgs []string
|
|
41
|
+
for _, raw := range changed {
|
|
42
|
+
p := strings.TrimSpace(raw)
|
|
43
|
+
if p == "" {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
p = path.Clean(strings.TrimPrefix(p, "./"))
|
|
47
|
+
if reason, widen := widenReason(p); widen {
|
|
48
|
+
return AffectedSelection{Widen: true, Reason: reason}
|
|
49
|
+
}
|
|
50
|
+
dir := path.Dir(p)
|
|
51
|
+
if seen[dir] {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
seen[dir] = true
|
|
55
|
+
pkgs = append(pkgs, dir)
|
|
56
|
+
}
|
|
57
|
+
sort.Strings(pkgs)
|
|
58
|
+
if len(pkgs) == 0 {
|
|
59
|
+
return AffectedSelection{Widen: true, Reason: "no Go package affected"}
|
|
60
|
+
}
|
|
61
|
+
return AffectedSelection{Packages: pkgs}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func widenReason(p string) (string, bool) {
|
|
65
|
+
if widenBasenames[path.Base(p)] {
|
|
66
|
+
return p + " is a module manifest", true
|
|
67
|
+
}
|
|
68
|
+
for _, w := range widenPrefixes {
|
|
69
|
+
if strings.HasPrefix(p, w) {
|
|
70
|
+
return p + " can change packages no changed path names", true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// An unclassified path can carry behaviour this selection cannot see, so it
|
|
74
|
+
// must not be dropped just because a sibling path named a package.
|
|
75
|
+
if !strings.HasSuffix(p, ".go") {
|
|
76
|
+
return p + " is not a Go source path", true
|
|
77
|
+
}
|
|
78
|
+
return "", false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// pkgDeps is one `go list` row: a package's directory, import path, and every
|
|
82
|
+
// import it reaches, test-only imports included.
|
|
83
|
+
type pkgDeps struct {
|
|
84
|
+
Dir string
|
|
85
|
+
Import string
|
|
86
|
+
Deps []string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// goListDeps is a variable so tests can inject a module graph without a toolchain.
|
|
90
|
+
var goListDeps = runGoListDeps
|
|
91
|
+
|
|
92
|
+
func runGoListDeps(moduleRoot string) ([]pkgDeps, error) {
|
|
93
|
+
const format = "{{.Dir}}\t{{.ImportPath}}\t{{join .Deps \",\"}}\t{{join .TestImports \",\"}}\t{{join .XTestImports \",\"}}"
|
|
94
|
+
cmd := exec.Command("go", "list", "-f", format, "./...") // #nosec G204 -- fixed args, no caller input
|
|
95
|
+
cmd.Dir = moduleRoot
|
|
96
|
+
out, err := cmd.Output()
|
|
97
|
+
if err != nil {
|
|
98
|
+
return nil, err
|
|
99
|
+
}
|
|
100
|
+
rows := make([]pkgDeps, 0, 64)
|
|
101
|
+
sc := bufio.NewScanner(strings.NewReader(string(out)))
|
|
102
|
+
sc.Buffer(make([]byte, 0, 1<<16), 1<<22)
|
|
103
|
+
for sc.Scan() {
|
|
104
|
+
parts := strings.Split(sc.Text(), "\t")
|
|
105
|
+
if len(parts) != 5 {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
row := pkgDeps{Dir: parts[0], Import: parts[1]}
|
|
109
|
+
for _, field := range parts[2:] {
|
|
110
|
+
for _, imp := range strings.Split(field, ",") {
|
|
111
|
+
if imp = strings.TrimSpace(imp); imp != "" {
|
|
112
|
+
row.Deps = append(row.Deps, imp)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
rows = append(rows, row)
|
|
117
|
+
}
|
|
118
|
+
return rows, sc.Err()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// moduleRootFor returns the deepest ancestor of dir holding a go.mod.
|
|
122
|
+
func moduleRootFor(dir string) (string, bool) {
|
|
123
|
+
for {
|
|
124
|
+
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
125
|
+
return dir, true
|
|
126
|
+
}
|
|
127
|
+
parent := filepath.Dir(dir)
|
|
128
|
+
if parent == dir {
|
|
129
|
+
return "", false
|
|
130
|
+
}
|
|
131
|
+
dir = parent
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// expandDependents adds every package whose imports reach a selected package: a
|
|
136
|
+
// change breaks its dependents' tests too, so those packages must run as well.
|
|
137
|
+
func expandDependents(selected []string, rows []pkgDeps) []string {
|
|
138
|
+
importers := map[string][]string{}
|
|
139
|
+
for _, r := range rows {
|
|
140
|
+
for _, dep := range r.Deps {
|
|
141
|
+
importers[dep] = append(importers[dep], r.Import)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
seen := map[string]bool{}
|
|
145
|
+
queue := append([]string(nil), selected...)
|
|
146
|
+
for _, s := range selected {
|
|
147
|
+
seen[s] = true
|
|
148
|
+
}
|
|
149
|
+
for len(queue) > 0 {
|
|
150
|
+
cur := queue[0]
|
|
151
|
+
queue = queue[1:]
|
|
152
|
+
for _, imp := range importers[cur] {
|
|
153
|
+
if !seen[imp] {
|
|
154
|
+
seen[imp] = true
|
|
155
|
+
queue = append(queue, imp)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
out := make([]string, 0, len(seen))
|
|
160
|
+
for imp := range seen {
|
|
161
|
+
out = append(out, imp)
|
|
162
|
+
}
|
|
163
|
+
sort.Strings(out)
|
|
164
|
+
return out
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// locateSelection resolves selected repository-relative dirs to one module root
|
|
168
|
+
// plus absolute dirs. A selection spanning modules or missing a go.mod widens.
|
|
169
|
+
func locateSelection(root string, dirs []string) (string, []string, bool) {
|
|
170
|
+
absRoot, err := filepath.Abs(root)
|
|
171
|
+
if err != nil {
|
|
172
|
+
return "", nil, false
|
|
173
|
+
}
|
|
174
|
+
var moduleRoot string
|
|
175
|
+
abs := make([]string, 0, len(dirs))
|
|
176
|
+
for _, d := range dirs {
|
|
177
|
+
a := filepath.Join(absRoot, filepath.FromSlash(d))
|
|
178
|
+
mr, ok := moduleRootFor(a)
|
|
179
|
+
if !ok {
|
|
180
|
+
return "", nil, false
|
|
181
|
+
}
|
|
182
|
+
if moduleRoot == "" {
|
|
183
|
+
moduleRoot = mr
|
|
184
|
+
} else if moduleRoot != mr {
|
|
185
|
+
return "", nil, false
|
|
186
|
+
}
|
|
187
|
+
abs = append(abs, a)
|
|
188
|
+
}
|
|
189
|
+
return moduleRoot, abs, moduleRoot != ""
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// RunCheckAffected prints the affected-only validation plan for changed paths.
|
|
193
|
+
func RunCheckAffected(root string, args []string, stdout, stderr io.Writer) int {
|
|
194
|
+
if len(args) == 0 {
|
|
195
|
+
fmt.Fprintln(stderr, "usage: devrites-engine check affected <path...>")
|
|
196
|
+
return 2
|
|
197
|
+
}
|
|
198
|
+
sel := SelectAffected(args)
|
|
199
|
+
if sel.Widen {
|
|
200
|
+
fmt.Fprintf(stdout, "affected: full suite (%s)\n", sel.Reason)
|
|
201
|
+
return 0
|
|
202
|
+
}
|
|
203
|
+
moduleRoot, absDirs, ok := locateSelection(root, sel.Packages)
|
|
204
|
+
if !ok {
|
|
205
|
+
fmt.Fprintln(stdout, "affected: full suite (selection does not resolve to one Go module)")
|
|
206
|
+
return 0
|
|
207
|
+
}
|
|
208
|
+
rows, err := goListDeps(moduleRoot)
|
|
209
|
+
if err != nil {
|
|
210
|
+
fmt.Fprintf(stdout, "affected: full suite (dependency graph unavailable: %v)\n", err)
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
byDir := make(map[string]string, len(rows))
|
|
214
|
+
for _, r := range rows {
|
|
215
|
+
byDir[filepath.Clean(r.Dir)] = r.Import
|
|
216
|
+
}
|
|
217
|
+
selected := make([]string, 0, len(absDirs))
|
|
218
|
+
for _, d := range absDirs {
|
|
219
|
+
imp, found := byDir[filepath.Clean(d)]
|
|
220
|
+
if !found {
|
|
221
|
+
fmt.Fprintf(stdout, "affected: full suite (%s is not a listed Go package)\n", d)
|
|
222
|
+
return 0
|
|
223
|
+
}
|
|
224
|
+
selected = append(selected, imp)
|
|
225
|
+
}
|
|
226
|
+
all := expandDependents(selected, rows)
|
|
227
|
+
suffix := ""
|
|
228
|
+
if len(all) > len(selected) {
|
|
229
|
+
suffix = " (includes dependents)"
|
|
230
|
+
}
|
|
231
|
+
fmt.Fprintf(stdout, "affected: %d package(s): %s%s\n", len(all), strings.Join(all, " "), suffix)
|
|
232
|
+
return 0
|
|
233
|
+
}
|
|
@@ -9,8 +9,20 @@ import (
|
|
|
9
9
|
"github.com/devrites/devrites/internal/state"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
// RunTaskGraphCheck validates tasks.md for one slug.
|
|
12
|
+
// RunTaskGraphCheck validates tasks.md for one slug. Shared-path findings are
|
|
13
|
+
// advisory: they never change the exit code.
|
|
13
14
|
func RunTaskGraphCheck(root, slug string, stdout, stderr io.Writer) int {
|
|
15
|
+
return runTaskGraphCheck(root, slug, false, stdout, stderr)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// RunTaskGraphCheckStrict is RunTaskGraphCheck, except a plan whose shared-path
|
|
19
|
+
// writer count breaches the hub limit is refused instead of only advised. That is
|
|
20
|
+
// the plan-shape repair point: split disjointly rather than widen the cap.
|
|
21
|
+
func RunTaskGraphCheckStrict(root, slug string, stdout, stderr io.Writer) int {
|
|
22
|
+
return runTaskGraphCheck(root, slug, true, stdout, stderr)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func runTaskGraphCheck(root, slug string, strict bool, stdout, stderr io.Writer) int {
|
|
14
26
|
graph, err := CheckTaskGraph(root, slug)
|
|
15
27
|
if err != nil {
|
|
16
28
|
fmt.Fprintf(stderr, "task-graph: %v\n", err)
|
|
@@ -38,8 +50,14 @@ func RunTaskGraphCheck(root, slug string, stdout, stderr io.Writer) int {
|
|
|
38
50
|
recordMetric(root, slug, "define", "size-advisory", "", int64(over))
|
|
39
51
|
}
|
|
40
52
|
paths, serial := sliceWritePaths(raw, graph)
|
|
41
|
-
if
|
|
42
|
-
fmt.Fprintf(stdout, "task-graph: schedule: %d slices in %d build rounds at cap %d\n", len(graph.Slices),
|
|
53
|
+
if plan, idle, maxReady := schedulePlan(graph, paths, root); len(plan) > 0 {
|
|
54
|
+
fmt.Fprintf(stdout, "task-graph: schedule: %d slices in %d build rounds at cap %d, width %d, idle-eligible %d\n", len(graph.Slices), len(plan), parallel.MaxParallelSlices, scheduleWidth(plan), idle)
|
|
55
|
+
if chain := chainAdvisories(graph, serial, len(plan), maxReady); len(chain) > 0 {
|
|
56
|
+
for _, line := range chain {
|
|
57
|
+
fmt.Fprintf(stdout, "task-graph: %s\n", line)
|
|
58
|
+
}
|
|
59
|
+
recordMetric(root, slug, "define", "chain-advisory", "", int64(len(chain)))
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
if hubs := hubAdvisories(graph, paths, serial); len(hubs) > 0 {
|
|
45
63
|
for _, line := range hubs[:min(len(hubs), hubAdvisoryShown)] {
|
|
@@ -49,6 +67,10 @@ func RunTaskGraphCheck(root, slug string, stdout, stderr io.Writer) int {
|
|
|
49
67
|
fmt.Fprintf(stdout, "task-graph: ADVISORY hub: %d more shared paths; fix the ones above first\n", extra)
|
|
50
68
|
}
|
|
51
69
|
recordMetric(root, slug, "define", "hub-advisory", "", int64(len(hubs)))
|
|
70
|
+
if strict {
|
|
71
|
+
fmt.Fprintf(stdout, "task-graph: BLOCKED: %d shared path(s) with %d+ writers; split disjointly (acceptance-preserving) or record `Serial rationale:`\n", len(hubs), hubAdvisoryMin)
|
|
72
|
+
return 3
|
|
73
|
+
}
|
|
52
74
|
}
|
|
53
75
|
}
|
|
54
76
|
if edges := edgeAdvisories(graph); len(edges) > 0 {
|
|
@@ -78,12 +78,18 @@ func pathTokens(raw string) []string {
|
|
|
78
78
|
return out
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
//
|
|
81
|
+
// schedulePlan replays /rite-build batch selection at the maximum cap: each
|
|
82
82
|
// round runs the plan-order path-disjoint subset of dependency-ready slices.
|
|
83
|
-
// It returns
|
|
84
|
-
|
|
83
|
+
// It returns the picked slice ids per round, the number of dependency-ready
|
|
84
|
+
// slices each round left idle (parallelism the cap could not use), and the
|
|
85
|
+
// largest dependency-ready set any round saw. That last value separates a
|
|
86
|
+
// dependency chain (never more than one slice ready) from write contention
|
|
87
|
+
// (many ready, one picked). It returns nil when a slice has no write paths or
|
|
88
|
+
// parallel select rejects.
|
|
89
|
+
func schedulePlan(graph TaskGraphResult, paths map[string][]string, root string) ([][]string, int, int) {
|
|
85
90
|
done := make(map[string]bool, len(graph.Slices))
|
|
86
|
-
rounds
|
|
91
|
+
var rounds [][]string
|
|
92
|
+
idle, maxReady := 0, 0
|
|
87
93
|
for len(done) < len(graph.Slices) {
|
|
88
94
|
var ready []parallel.SlicePaths
|
|
89
95
|
for _, s := range graph.Slices {
|
|
@@ -91,23 +97,42 @@ func scheduleRounds(graph TaskGraphResult, paths map[string][]string, root strin
|
|
|
91
97
|
continue
|
|
92
98
|
}
|
|
93
99
|
if len(paths[s.ID]) == 0 {
|
|
94
|
-
return 0
|
|
100
|
+
return nil, 0, 0
|
|
95
101
|
}
|
|
96
102
|
ready = append(ready, parallel.SlicePaths{ID: s.ID, Paths: paths[s.ID]})
|
|
97
103
|
}
|
|
98
104
|
if len(ready) == 0 {
|
|
99
|
-
return 0
|
|
105
|
+
return nil, 0, 0
|
|
100
106
|
}
|
|
101
107
|
picked, err := parallel.SelectGreedy(parallel.MaxParallelSlices, ready, root)
|
|
102
108
|
if err != nil || len(picked) == 0 {
|
|
103
|
-
return 0
|
|
109
|
+
return nil, 0, 0
|
|
104
110
|
}
|
|
111
|
+
ids := make([]string, 0, len(picked))
|
|
105
112
|
for _, p := range picked {
|
|
106
113
|
done[p.ID] = true
|
|
114
|
+
ids = append(ids, p.ID)
|
|
107
115
|
}
|
|
108
|
-
|
|
116
|
+
idle += len(ready) - len(picked)
|
|
117
|
+
maxReady = max(maxReady, len(ready))
|
|
118
|
+
rounds = append(rounds, ids)
|
|
119
|
+
}
|
|
120
|
+
return rounds, idle, maxReady
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// scheduleRounds returns the round count alone; see schedulePlan for the shape.
|
|
124
|
+
func scheduleRounds(graph TaskGraphResult, paths map[string][]string, root string) int {
|
|
125
|
+
rounds, _, _ := schedulePlan(graph, paths, root)
|
|
126
|
+
return len(rounds)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// scheduleWidth is the busiest round: the plan's achieved DAG width.
|
|
130
|
+
func scheduleWidth(rounds [][]string) int {
|
|
131
|
+
width := 0
|
|
132
|
+
for _, picked := range rounds {
|
|
133
|
+
width = max(width, len(picked))
|
|
109
134
|
}
|
|
110
|
-
return
|
|
135
|
+
return width
|
|
111
136
|
}
|
|
112
137
|
|
|
113
138
|
// PlanReady is the ready set for `parallel select --from-plan`; repoRoot is
|
|
@@ -319,8 +344,8 @@ func allDone(ids []string, done map[string]bool) bool {
|
|
|
319
344
|
// hubAdvisories names each path written by hubAdvisoryMin+ slices that carry
|
|
320
345
|
// no `Serial rationale:`, most-shared first.
|
|
321
346
|
func hubAdvisories(graph TaskGraphResult, paths map[string][]string, serial map[string]bool) []string {
|
|
322
|
-
|
|
323
|
-
shown := map[string]string{}
|
|
347
|
+
claimed := map[string][]string{} // key -> slices claiming exactly it
|
|
348
|
+
shown := map[string]string{} // first spelling seen, for display
|
|
324
349
|
for _, s := range graph.Slices {
|
|
325
350
|
if serial[s.ID] {
|
|
326
351
|
continue
|
|
@@ -330,13 +355,39 @@ func hubAdvisories(graph TaskGraphResult, paths map[string][]string, serial map[
|
|
|
330
355
|
key := strings.ToLower(path.Clean(strings.ReplaceAll(p, `\`, "/")))
|
|
331
356
|
if !seen[key] {
|
|
332
357
|
seen[key] = true
|
|
333
|
-
|
|
358
|
+
claimed[key] = append(claimed[key], s.ID)
|
|
334
359
|
if _, ok := shown[key]; !ok {
|
|
335
360
|
shown[key] = p
|
|
336
361
|
}
|
|
337
362
|
}
|
|
338
363
|
}
|
|
339
364
|
}
|
|
365
|
+
// A directory claim contains every path beneath it, so writers of a path
|
|
366
|
+
// include slices claiming its ancestors and its descendants; counting exact
|
|
367
|
+
// spellings alone would report one writer for a hub every slice touches.
|
|
368
|
+
writers := map[string][]string{}
|
|
369
|
+
for key := range claimed {
|
|
370
|
+
set := map[string]bool{}
|
|
371
|
+
for _, id := range claimed[key] {
|
|
372
|
+
set[id] = true
|
|
373
|
+
}
|
|
374
|
+
for other, ids := range claimed {
|
|
375
|
+
if other == key {
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
if strings.HasPrefix(other, key+"/") || strings.HasPrefix(key, other+"/") {
|
|
379
|
+
for _, id := range ids {
|
|
380
|
+
set[id] = true
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
ids := make([]string, 0, len(set))
|
|
385
|
+
for id := range set {
|
|
386
|
+
ids = append(ids, id)
|
|
387
|
+
}
|
|
388
|
+
sort.Strings(ids)
|
|
389
|
+
writers[key] = ids
|
|
390
|
+
}
|
|
340
391
|
hubs := make([]string, 0, len(writers))
|
|
341
392
|
for p, ids := range writers {
|
|
342
393
|
if len(ids) >= hubAdvisoryMin {
|
|
@@ -356,6 +407,22 @@ func hubAdvisories(graph TaskGraphResult, paths map[string][]string, serial map[
|
|
|
356
407
|
return lines
|
|
357
408
|
}
|
|
358
409
|
|
|
410
|
+
// chainAdvisories names a plan whose schedule never overlaps because only one
|
|
411
|
+
// slice was ever dependency-ready per round, so no parallel cap can help. A plan
|
|
412
|
+
// serialized by write contention instead is a hub problem, not a chain, and a
|
|
413
|
+
// plan whose slices all record `Serial rationale:` is deliberate; both stay quiet.
|
|
414
|
+
func chainAdvisories(graph TaskGraphResult, serial map[string]bool, rounds, maxReady int) []string {
|
|
415
|
+
if len(graph.Slices) < 2 || rounds != len(graph.Slices) || maxReady != 1 {
|
|
416
|
+
return nil
|
|
417
|
+
}
|
|
418
|
+
for _, s := range graph.Slices {
|
|
419
|
+
if !serial[s.ID] {
|
|
420
|
+
return []string{fmt.Sprintf("ADVISORY chain: %d slices run in %d build rounds, so no two slices overlap and a parallel cap cannot help; cut a slice that stands alone, or record `Serial rationale:`", len(graph.Slices), rounds)}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return nil
|
|
424
|
+
}
|
|
425
|
+
|
|
359
426
|
// edgeAdvisories names each dependency edge that does not say what crosses it:
|
|
360
427
|
// no `(via <interface>)`, or a via the dependency's Consumes / Produces lacks.
|
|
361
428
|
// An edge the dependent reads nothing across only serializes work.
|
package/engine/main.go
CHANGED
|
@@ -229,6 +229,8 @@ func cmdCheck(root string, args []string, stdin io.Reader, stdout, stderr io.Wri
|
|
|
229
229
|
return parallel.Run("path-disjoint", rest, stdin, stdout, stderr)
|
|
230
230
|
case "task-graph":
|
|
231
231
|
return cmdTaskGraph(root, rest, stdout, stderr)
|
|
232
|
+
case "affected":
|
|
233
|
+
return lib.RunCheckAffected(root, rest, stdout, stderr)
|
|
232
234
|
case "slice":
|
|
233
235
|
return lib.RunCheckSlice(root, rest, stdout, stderr)
|
|
234
236
|
case "diff-scope":
|
|
@@ -378,11 +380,26 @@ func cmdGate(root string, kind gate.Kind, args []string, stdout, stderr io.Write
|
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
func cmdTaskGraph(root string, args []string, stdout, stderr io.Writer) int {
|
|
381
|
-
|
|
383
|
+
strict, slug, seen := false, "", false
|
|
384
|
+
for _, a := range args {
|
|
385
|
+
if a == "--strict" {
|
|
386
|
+
strict = true
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
if a == "" || seen {
|
|
390
|
+
fmt.Fprintln(stderr, taskGraphUsage)
|
|
391
|
+
return exitUsage
|
|
392
|
+
}
|
|
393
|
+
slug, seen = a, true
|
|
394
|
+
}
|
|
395
|
+
if !seen {
|
|
382
396
|
fmt.Fprintln(stderr, taskGraphUsage)
|
|
383
397
|
return exitUsage
|
|
384
398
|
}
|
|
385
|
-
|
|
399
|
+
if strict {
|
|
400
|
+
return lib.RunTaskGraphCheckStrict(root, slug, stdout, stderr)
|
|
401
|
+
}
|
|
402
|
+
return lib.RunTaskGraphCheck(root, slug, stdout, stderr)
|
|
386
403
|
}
|
|
387
404
|
|
|
388
405
|
func cmdSkillTrust(args []string, stdout, stderr io.Writer) int {
|
|
@@ -17,7 +17,8 @@ Read named paths only. Accept released v1/v2 bullet or v3 table `state.md` under
|
|
|
17
17
|
`.devrites/work/<slug>/`. Reject aliases, release scripts/markers, and conversion.
|
|
18
18
|
|
|
19
19
|
Cite current rule plus workspace evidence for each applicable axis: cursor/topology,
|
|
20
|
-
phase artifacts, decisions, planning/traceability/proof
|
|
20
|
+
phase artifacts, decisions, planning/traceability/proof (including a `check task-graph`
|
|
21
|
+
`ADVISORY chain:` schedule that no parallel cap can widen), built-code/intent, protected
|
|
21
22
|
history, artifact budgets/feature ceiling (the supplied `devrites-engine orient`
|
|
22
23
|
`artifact_budgets`/`bulk_files` and `task_graph.slice_count` against
|
|
23
24
|
[`workspace-artifact-schema.md`](../skills/devrites-lib/reference/workspace-artifact-schema.md) and [`slicing.md`](../skills/rite-plan/reference/slicing.md) § Feature ceiling),
|
|
@@ -116,13 +116,13 @@ next sequence continuation through Vet in a second session, then stop.
|
|
|
116
116
|
`/rite-build` batch loop → `/rite-prove` → `/rite-polish` →
|
|
117
117
|
`/rite-review` → `/rite-seal`. Read and execute each skill; durable files, not
|
|
118
118
|
chat, carry state. Apply the mutable post-vet budget before first Build.
|
|
119
|
-
Build
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
Build uses `parallel select --from-plan` (cap: explicit `--parallel N`, else
|
|
120
|
+
sentinel `max_parallel`, else 10), refilling per landing until none pending;
|
|
121
|
+
a one-slice round is serial for that round only; reselect afterward. Host
|
|
122
|
+
isolation may force serial. If `--parallel N` is explicit,
|
|
123
|
+
pass it into `/rite-build` and ignore leftover sentinel `max_parallel`.
|
|
124
|
+
Otherwise pass the resolved sentinel/default cap into `/rite-build` as
|
|
125
|
+
`--parallel N`, never bare `/rite-build`.
|
|
126
126
|
**Completion:** loop reaches Seal GO or persists a valid stop before any later phase.
|
|
127
127
|
5. **Apply stops.** At every gate use
|
|
128
128
|
[stop-conditions.md](reference/stop-conditions.md). A `blocked` label alone is not a stop condition:
|
|
@@ -14,7 +14,7 @@ user-invocable: true
|
|
|
14
14
|
HITL stops; a later user invocation starts the next slice.
|
|
15
15
|
Explicit `.devrites/AFK` alone lets the controlling root chain under green proof,
|
|
16
16
|
caps and pause rules. Every wright returns after one slice.
|
|
17
|
-
**
|
|
17
|
+
**HITL:** `/rite-build --parallel N` (2≤N≤10; N=1≡serial) follows
|
|
18
18
|
[`reference/parallel-batch.md`](reference/parallel-batch.md)
|
|
19
19
|
([`one-slice-cycle.md`](reference/one-slice-cycle.md)).
|
|
20
20
|
|
|
@@ -45,8 +45,8 @@ Wright applies anti-slop; root verifies returns and never patches source. Refuse
|
|
|
45
45
|
|
|
46
46
|
## Invariants
|
|
47
47
|
|
|
48
|
-
-
|
|
49
|
-
|
|
48
|
+
- Standalone HITL default: serial on control (core.md #5). AFK/`--parallel N`
|
|
49
|
+
uses [`parallel-batch.md`](reference/parallel-batch.md).
|
|
50
50
|
Same-worktree multi-writer / root-emulated concurrency forbidden.
|
|
51
51
|
Native-worktree pilot = single-slice isolation when `wright-dispatch.md`
|
|
52
52
|
preflight + reconcile hold.
|
|
@@ -80,7 +80,8 @@ Wright applies anti-slop; root verifies returns and never patches source. Refuse
|
|
|
80
80
|
<!-- workflow-artifact-adapter: {"module":"devrites-lib/reference/standards/workflow-artifacts.md","entry":"Vet-ready admitted bytes require root authorship outside product wright","action":"ROOT_TRANSACTION; root writes only admitted .devrites/** targets","return":"saved Build slice cursor; wright product allowlist unchanged"} -->
|
|
81
81
|
## `--parallel N` (opt-in)
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
HITL default/explicit `1` ≡ serial; AFK inherits cap.
|
|
84
|
+
`2`–`10` is a **cap** — the root runs an eligible
|
|
84
85
|
path-disjoint set it can start now (`N_eff`, from `parallel select --from-plan`), takes fewer when the cap cannot be
|
|
85
86
|
filled, and refills a slot per landing (serial slice or one sibling) until
|
|
86
87
|
no pending slice remains
|
|
@@ -37,13 +37,15 @@ Select **greedily in plan order** with
|
|
|
37
37
|
reads the ready set (tasks.md dependencies checked `built` in `state.md`, minus
|
|
38
38
|
running-lease slices) and each slice's Writer allowlist (else `Files likely
|
|
39
39
|
touched`), plus its manifest/lockfile when `External dependencies:` is not none;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
`next` prints `eligible_now:`/`batch:`. `n_eff` counts new picks only.
|
|
41
|
+
With a running lease, a single new pick still uses `parallel add`;
|
|
42
|
+
`n_eff=0` with a running lease waits for siblings, not Prove. Without one:
|
|
43
|
+
≥2 → parallel batch, 1 → serial, none pending → Prove.
|
|
44
|
+
Serial is this round only; a cap need not be filled.
|
|
43
45
|
|
|
44
46
|
**Refill per landing; never wait for the batch.** Each sibling lands alone when
|
|
45
|
-
green ([Lifecycle](#lifecycle) step 6).
|
|
46
|
-
|
|
47
|
+
green ([Lifecycle](#lifecycle) step 6). Re-read state, pending slices, and the
|
|
48
|
+
tree; reselect: unlanded siblings hold
|
|
47
49
|
their slots and paths, landed ones count as built, and only new picks print;
|
|
48
50
|
`parallel add` starts each from the control tip and its wright dispatches at once.
|
|
49
51
|
No lease → plain `select` + `create`. AFK/autocomplete is one run; HITL stops after
|
|
@@ -116,6 +118,10 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
116
118
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
117
119
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
118
120
|
else proof → `green|red|gap`.
|
|
121
|
+
|
|
122
|
+
A recheck round is one dispatch per changed surface, not one per checklist: the
|
|
123
|
+
recheck agent carries every open checklist (code, test, doubt) for that diff and
|
|
124
|
+
answers each by name. Merging cuts dispatches; dropping a checklist does not.
|
|
119
125
|
Reviewer fan-out for a batch is itself batched under
|
|
120
126
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)
|
|
121
127
|
(≈4 concurrent read-only reviewers, 8 only with disjoint paths), and `max_agents`
|
|
@@ -137,11 +143,11 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
137
143
|
7. Each landing: FF control (the branch that held `HEAD` when the batch started —
|
|
138
144
|
often `main`/`master`, otherwise whatever the user was on) by one local commit,
|
|
139
145
|
never pushed; union that sibling's paths into `touched-files.md`; update
|
|
140
|
-
state/evidence; AFK +1
|
|
141
|
-
refill ([Dynamic selection](#dynamic-selection-and-re-batching))
|
|
142
|
-
feature gates
|
|
143
|
-
([Proof tiers](phase-contract.md#proof-tiers)); the next
|
|
144
|
-
|
|
146
|
+
state/evidence; AFK +1; optional `check candidate`. Land every green sibling,
|
|
147
|
+
refill ([Dynamic selection](#dynamic-selection-and-re-batching)), then run the
|
|
148
|
+
feature gates once for the train
|
|
149
|
+
([Proof tiers](phase-contract.md#proof-tiers)); the next train waits, the
|
|
150
|
+
refilled wrights do not.
|
|
145
151
|
8. Cleanup runs only after the last landing marks the lease `complete` — work
|
|
146
152
|
landed in control, so `parallel cleanup` removes worktrees/branches.
|
|
147
153
|
Blocked/human-stopped batches keep lease, worktrees, branches — nothing is
|
|
@@ -223,13 +229,13 @@ devrites-engine parallel lease-write|lease-read|lease-clear --root <repo> --slug
|
|
|
223
229
|
# parallel cleanup --root <repo> --slug <slug> --force (salvages, prints retained refs)
|
|
224
230
|
```
|
|
225
231
|
|
|
226
|
-
`integrate` without `--apply-to-control` stages
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
`integrate` without `--apply-to-control` stages commits; lease stays `running`,
|
|
233
|
+
control stays at its expected tip, cleanup refuses. `record-green` and `integrate`
|
|
234
|
+
accept `integrate-failed` for retry. Applied landings advance `control_tip`
|
|
235
|
+
(initially `B`), permitting landings/refill. External control movement or
|
|
236
|
+
uncommitted slice-path edits with `--apply-to-control` cause refusal:
|
|
237
|
+
stop for the human (never stash or commit their work); the batch is untouched.
|
|
238
|
+
Go is SSOT; never ad-hoc git/bash orchestration.
|
|
233
239
|
|
|
234
240
|
## Runtime isolation
|
|
235
241
|
|