devrites 7.0.16 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +1 -1
  3. package/engine/help.go +4 -3
  4. package/engine/internal/lib/affected.go +233 -0
  5. package/engine/internal/lib/cli_observe.go +25 -3
  6. package/engine/internal/lib/taskgraph.go +79 -12
  7. package/engine/main.go +19 -2
  8. package/pack/.claude/agents/devrites-upgrade-planner.md +2 -1
  9. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +1 -1
  10. package/pack/.claude/skills/rite-autocomplete/SKILL.md +7 -7
  11. package/pack/.claude/skills/rite-build/SKILL.md +5 -4
  12. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +23 -17
  13. package/pack/.claude/skills/rite-build/reference/phase-contract.md +15 -3
  14. package/pack/.claude/skills/rite-define/SKILL.md +4 -2
  15. package/pack/.claude/skills/rite-plan/reference/slicing.md +6 -2
  16. package/pack/generated/claude/agents/devrites-upgrade-planner.md +2 -1
  17. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +1 -1
  18. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +7 -7
  19. package/pack/generated/claude/skills/rite-build/SKILL.md +5 -4
  20. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +23 -17
  21. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +15 -3
  22. package/pack/generated/claude/skills/rite-define/SKILL.md +4 -2
  23. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +6 -2
  24. package/pack/generated/codex/agents/devrites-upgrade-planner.toml +2 -1
  25. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +1 -1
  26. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +7 -7
  27. package/pack/generated/codex/skills/rite-build/SKILL.md +5 -4
  28. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +23 -17
  29. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +15 -3
  30. package/pack/generated/codex/skills/rite-define/SKILL.md +4 -2
  31. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +6 -2
  32. package/pack/generated/devin/agents/devrites-upgrade-planner.md +2 -1
  33. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +1 -1
  34. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +7 -7
  35. package/pack/generated/devin/skills/rite-build/SKILL.md +5 -4
  36. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +23 -17
  37. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +15 -3
  38. package/pack/generated/devin/skills/rite-define/SKILL.md +4 -2
  39. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +6 -2
  40. package/pack/generated/omp/agents/devrites-upgrade-planner.md +2 -1
  41. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +1 -1
  42. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +7 -7
  43. package/pack/generated/omp/skills/rite-build/SKILL.md +5 -4
  44. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +23 -17
  45. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +15 -3
  46. package/pack/generated/omp/skills/rite-define/SKILL.md +4 -2
  47. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +6 -2
  48. package/pack/generated/pi/agents/devrites-upgrade-planner.md +2 -1
  49. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +1 -1
  50. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +7 -7
  51. package/pack/generated/pi/skills/rite-build/SKILL.md +5 -4
  52. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +23 -17
  53. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +15 -3
  54. package/pack/generated/pi/skills/rite-define/SKILL.md +4 -2
  55. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +6 -2
  56. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
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
+
16
+ ## [7.0.17](https://github.com/ViktorsBaikers/DevRites/compare/v7.0.16...v7.0.17) (2026-10-27)
17
+
18
+ ### Fixed
19
+
20
+ * **devrites:** drop the wall-clock number the engine never reads ([#177](https://github.com/ViktorsBaikers/DevRites/issues/177)) ([8f4f1dc](https://github.com/ViktorsBaikers/DevRites/commit/8f4f1dccd8bee342619cb1b3ab152733549d8d84)), references [#176](https://github.com/ViktorsBaikers/DevRites/issues/176) [#176](https://github.com/ViktorsBaikers/DevRites/issues/176) [#176](https://github.com/ViktorsBaikers/DevRites/issues/176)
21
+ * **devrites:** mark AFK sentinel caps as non-ceilings under autocomplete ([#176](https://github.com/ViktorsBaikers/DevRites/issues/176)) ([808d767](https://github.com/ViktorsBaikers/DevRites/commit/808d767adaf79eef0fa2eebab111842e003772b0))
22
+
5
23
  ## [7.0.16](https://github.com/ViktorsBaikers/DevRites/compare/v7.0.15...v7.0.16) (2026-10-27)
6
24
 
7
25
  ### 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.16`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.0.16): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
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 rounds := scheduleRounds(graph, paths, root); rounds > 0 {
42
- fmt.Fprintf(stdout, "task-graph: schedule: %d slices in %d build rounds at cap %d\n", len(graph.Slices), rounds, parallel.MaxParallelSlices)
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
- // scheduleRounds replays /rite-build batch selection at the maximum cap: each
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 0 when a slice has no write paths or parallel select rejects them.
84
- func scheduleRounds(graph TaskGraphResult, paths map[string][]string, root string) int {
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 := 0
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
- rounds++
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 rounds
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
- writers := map[string][]string{}
323
- shown := map[string]string{} // first spelling seen, for display
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
- writers[key] = append(writers[key], s.ID)
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
- if len(args) != 1 {
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
- return lib.RunTaskGraphCheck(root, args[0], stdout, stderr)
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, built-code/intent, protected
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),
@@ -56,7 +56,7 @@ Presence = AFK active. The file body is optional YAML:
56
56
  ```yaml
57
57
  max_slices: 10 # whole-workspace writer budget; seeds state.md remaining count
58
58
  max_agents: 32 # native agent dispatches in one host activation
59
- max_minutes: 120 # wall-clock minutes in one host activation
59
+ max_minutes: 120 # min; /rite-autocomplete: not a ceiling
60
60
  max_review_queue: 8 # unresolved review/gate items admitted before fan-out stops
61
61
  # max_tokens: 200000 # optional stricter host-observed token cap
62
62
  # max_cost_usd: 10 # optional stricter host-observed cost cap
@@ -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 runs the path-disjoint batch `parallel select --from-plan` picks (cap: this invocation's
120
- `--parallel N`, else sentinel `max_parallel`, else 10) and refills a slot
121
- per landing until no pending slice remains; a one-slice round is serial for that round only, then
122
- selection runs again (host isolation still forces serial when concurrent writers
123
- cannot be separated). When `--parallel N`
124
- is present, pass it into `/rite-build` and do not consult leftover sentinel
125
- `max_parallel`.
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
- **Opt-in:** `/rite-build --parallel N` (2≤N≤10; N=1≡serial) follows
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
- - Default: one slice; writers serial on control (core.md #5). Parallel only via
49
- `--parallel N` under [`reference/parallel-batch.md`](reference/parallel-batch.md).
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
- Omitted/`1` ≡ serial; `2`–`10` is a **cap** — the root runs the largest eligible
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